TL;博士:
- 使用 Supabase、React、WebGazer.js、Motion One、anime.js、穩定音訊建置
- 利用 Supabase 即時呈現和廣播(完全不使用資料庫表格!)
- GitHub 儲存庫
- 網站
- 示範影片
又一場 Supabase 啟動週黑客馬拉松和另一個實驗項目,名為 凝視深淵。 這最終成為了最簡單又最複雜的項目之一。幸運的是,我最近很喜歡 Cursor,所以我得到了一些幫助來完成它!我還想驗證我心中的一個問題:是否可以使用僅 Supabase 的即時功能而無需任何資料庫表? (也許有些明顯)答案是:是的,是的(愛你,即時團隊♥️)。因此,讓我們更深入地了解實現。
這個想法
有一天,我隨機想到了尼采關於深淵的名言,如果能夠以某種方式實際想像它會很好(而且很酷):你凝視著黑暗的屏幕,有東西在凝視著你。沒有更多了!
建構專案
最初我的想法是使用 Three.js 來製作這個項目,但我意識到這意味著我需要為 3D 眼睛創建或找到一些免費資源。我認為這有點太多了,特別是因為我沒有太多時間來處理專案本身,因此決定使用 SVG 進行 2D 製作。
我也不希望它只是視覺效果:如果有些音訊也會有更好的體驗。所以我有一個想法,如果參與者可以對著麥克風說話,而其他人可以聽到不合格的低語或風過時的聲音,那就太棒了。然而,這非常具有挑戰性,因此我決定完全放棄它,因為我無法將 WebAudio 和 WebRTC 很好地連接在一起。我的程式碼庫中確實有一個剩餘組件,如果您想看一下,它會監聽本地麥克風並為當前用戶觸發“風聲”。也許將來會添加一些東西?
即時房間
在處理任何視覺內容之前,我想測試一下我想要的即時設定。由於即時功能存在一些限制,我希望它能夠工作,以便:
- 最多有。一個頻道一次有 10 位參與者
- 你應該只看到其他參與者的眼睛
為此,我想出了一個 useEffect 設置,它遞歸地加入到實時通道,如下所示:
這個 joinRoom 位於 useEffect 鉤子內,並在安裝房間元件時被呼叫。我在開發此功能時發現的一個警告是,currentPresences 參數在連接事件中不包含任何值,即使它可用。我不確定這是否是實施中的錯誤或按預期工作。因此,每當用戶加入時,需要手動獲取 room.presenceState 來獲取房間中的參與者數量。
我們檢查參與者數量,然後取消訂閱當前房間並嘗試加入另一個房間,或者然後繼續當前房間。我們在加入事件中執行此操作,因為同步太晚了(它在加入或離開事件後觸發)。
我透過在瀏覽器中開啟大量分頁來測試此實現,一切看起來都很棒!
之後,我想透過滑鼠位置更新來調試解決方案,但很快就遇到了一些在頻道中發送過多訊息的問題!解決方案:限制調用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
function createThrottledFunction<T extends (...args: unknown[]) => unknown>(
functionToThrottle: T,
waitTimeMs: number
): (...args: Parameters<T>) => void {
let isWaitingToExecute = false
return function throttledFunction(...args: Parameters<T>) {
if (!isWaitingToExecute) {
functionToThrottle.apply(this, args)
isWaitingToExecute = true
setTimeout(() => {
isWaitingToExecute = false
}, waitTimeMs)
}
}
}
|
登入後複製
登入後複製
遊標想出了這個小油門函數創建器,我將它與眼動追蹤廣播一起使用,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | const throttledBroadcast = createThrottledFunction((data: EyeTrackingData) => {
if (currentChannel) {
currentChannel.send({
type: 'broadcast' ,
event: 'eye_tracking' ,
payload: data
})
}
}, THROTTLE_MS)
throttledBroadcast({
userId: userId.current,
isBlinking: isCurrentlyBlinking,
gazeX,
gazeY
})
|
登入後複製
登入後複製
這很有幫助!另外,在最初的版本中,我在存在狀態下發送了眼動追蹤訊息,但是廣播每秒允許更多訊息,所以我將實現切換到了這一點。這在眼動追蹤中尤其重要,因為相機會一直記錄一切。
眼球追蹤
當我第一次有了這個專案的想法時,我就遇到了 WebGazer.js。這是一個非常有趣的項目,而且效果出奇的好!
整個眼球追蹤功能是在 useEffect 掛鉤中的一個函數中完成的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | window.webgazer
.setGazeListener(async (data: any) => {
if (data == null || !currentChannel || !ctxRef.current) return
try {
const gazeX = data.x / windowSize.width
const gazeY = data.y / windowSize.height
const videoElement = document.getElementById( 'webgazerVideoFeed' ) as HTMLVideoElement
if (!videoElement) {
console.error( 'WebGazer video element not found' )
return
}
imageCanvasRef.current.width = videoElement.videoWidth
imageCanvasRef.current.height = videoElement.videoHeight
ctxRef.current?.drawImage(videoElement, 0, 0)
const tracker = window.webgazer.getTracker()
const patches = await tracker.getEyePatches(
videoElement,
imageCanvasRef.current,
videoElement.videoWidth,
videoElement.videoHeight
)
if (!patches?.right?.patch?.data || !patches?.left?.patch?.data) {
console.error( 'No eye patches detected' )
return
}
const calculateBrightness = (imageData: ImageData) => {
let total = 0
for (let i = 0; i < imageData.data.length; i += 16) {
const r = imageData.data[i]
const g = imageData.data[i + 1]
const b = imageData.data[i + 2]
total += (r + g + b) / 3
}
return total / (imageData.width * imageData.height / 4)
}
const rightEyeBrightness = calculateBrightness(patches.right.patch)
const leftEyeBrightness = calculateBrightness(patches.left.patch)
const avgBrightness = (rightEyeBrightness + leftEyeBrightness) / 2
if (brightnessSamples.current.length >= SAMPLES_SIZE) {
brightnessSamples.current.shift()
}
brightnessSamples.current.push(avgBrightness)
const rollingAverage = brightnessSamples.current.reduce((a, b) => a + b, 0) / brightnessSamples.current.length
const dynamicThreshold = rollingAverage * THRESHOLD_MULTIPLIER
const blinkDetected = avgBrightness > dynamicThreshold
if (blinkDetected !== isCurrentlyBlinking) {
const now = Date .now()
if (now - lastBlinkTime > 100) {
isCurrentlyBlinking = blinkDetected
lastBlinkTime = now
}
}
throttledBroadcast({
userId: userId.current,
isBlinking: isCurrentlyBlinking,
gazeX,
gazeY
})
} catch (error) {
console.error( 'Error processing gaze data:' , error)
}
})
|
登入後複製
取得使用者正在查看的資訊很簡單,就像取得螢幕上的滑鼠位置一樣。然而,我還想添加眨眼檢測作為(一個很酷的)功能,這需要跳過一些環節。
當您在 google 上搜尋有關 WebGazer 和眨眼偵測的資訊時,您可以看到初始實作的一些剩餘內容。就像原始碼中甚至有註解掉的程式碼一樣。不幸的是,庫中不存在此類功能。您需要手動完成。
經過大量的試驗和錯誤,Cursor 和我想出了一個解決方案,可以根據眼罩數據計算像素和亮度級別,以確定用戶何時眨眼。它還具有一些動態照明調整功能,因為我注意到(至少對我來說)網路攝影機並不總是根據您的照明來識別您何時眨眼。對我來說,我的照片/房間越亮,效果越差,而在較暗的燈光下效果更好(見圖)。
在調試眼動追蹤功能時(WebGazer 有一個非常好的.setPredictionPoints 調用,它在屏幕上顯示一個紅點以可視化您正在看的位置),我注意到跟踪不是很準確除非您進行校準 這是專案要求您在加入任何房間之前要做的事情。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
function createThrottledFunction<T extends (...args: unknown[]) => unknown>(
functionToThrottle: T,
waitTimeMs: number
): (...args: Parameters<T>) => void {
let isWaitingToExecute = false
return function throttledFunction(...args: Parameters<T>) {
if (!isWaitingToExecute) {
functionToThrottle.apply(this, args)
isWaitingToExecute = true
setTimeout(() => {
isWaitingToExecute = false
}, waitTimeMs)
}
}
}
|
登入後複製
登入後複製
看到它的實際應用是一次非常酷的體驗!我對周圍的線條應用了相同的方法,並指示遊標將它們向中心“折疊”:它幾乎一氣呵成!
然後,眼睛將在一個簡單的 CSS 網格內渲染,單元格對齊,這樣整個房間看起來就像一隻大眼睛。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | const throttledBroadcast = createThrottledFunction((data: EyeTrackingData) => {
if (currentChannel) {
currentChannel.send({
type: 'broadcast' ,
event: 'eye_tracking' ,
payload: data
})
}
}, THROTTLE_MS)
throttledBroadcast({
userId: userId.current,
isBlinking: isCurrentlyBlinking,
gazeX,
gazeY
})
|
登入後複製
登入後複製
最後的潤飾
然後播放一些不錯的介紹螢幕和背景音樂,專案就可以開始了!
當您處理此類事情時,音訊總是可以改善體驗,因此我使用穩定音訊在使用者「進入深淵」時生成背景音樂。我用於音樂的提示如下:
環境、令人毛骨悚然、背景音樂、低語聲、風、慢節奏、怪異、深淵
我還覺得純黑的螢幕有點無聊,所以我在背景上添加了一些動畫 SVG 濾鏡。此外,我在螢幕中央添加了一個黑暗的、模糊的圓圈,以產生一些漂亮的淡入淡出效果。我可能可以使用 SVG 濾鏡來完成此操作,但我不想在這方面花費太多時間。然後為了有更多的運動,我讓背景繞著軸旋轉。有時使用 SVG 濾鏡製作動畫有點奇怪,所以我決定採取這種方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <div>
<h2>
結論
</h2>
<p>現在您已經了解了:相當直接地了解如何使用 Supabase 的即時功能實現程式化的眼球追蹤。就我個人而言,我發現這是一個非常有趣的實驗,並且在進行過程中沒有遇到太多問題。令人驚訝的是,在提交專案之前我不需要在最後一晚熬夜! </p>
<p>請隨意查看該專案或示範影片的結果。如果一群人同時使用它,可能會出現一些問題(很難測試,因為它需要多個設備和網路攝影機才能正確完成),但我想這是黑客馬拉松專案的時尚?如果您確實進行了測試,請記住,如果您看到一隻眼睛,那就是其他人透過網路在某個地方看著您! </p>
</div>
|
登入後複製
以上是使用 Supabase 和 WebGazer.js 建立即時眼動追蹤體驗的詳細內容。更多資訊請關注PHP中文網其他相關文章!