TL;博士:
- 使用 Supabase、React、WebGazer.js、Motion One、anime.js、稳定音频构建
- 利用 Supabase 实时呈现和广播(根本不使用数据库表!)
- GitHub 存储库
- 网站
- 演示视频
又一场 Supabase 启动周黑客马拉松和另一个实验项目,名为 凝视深渊。 这最终成为了最简单又最复杂的项目之一。幸运的是,我最近很喜欢 Cursor,所以我得到了一些帮助来完成它!我还想验证我心中的一个问题:是否可以使用仅 Supabase 的实时功能而无需任何数据库表? (也许有些明显)答案是:是的,是的(爱你,实时团队♥️)。因此,让我们更深入地了解实现。
有一天,我随机想到了尼采关于深渊的名言,如果能够以某种方式实际想象它会很好(而且很酷):你凝视着黑暗的屏幕,有东西在凝视着你。没有更多了!
最初我的想法是使用 Three.js 来制作这个项目,但我意识到这意味着我需要为 3D 眼睛创建或找到一些免费资源。我认为这有点太多了,特别是因为我没有太多时间来处理项目本身,因此决定使用 SVG 进行 2D 制作。
我也不希望它只是视觉效果:如果有一些音频也会有更好的体验。所以我有一个想法,如果参与者可以对着麦克风说话,而其他人可以听到不合格的低语或风过时的声音,那就太棒了。然而,这非常具有挑战性,因此我决定完全放弃它,因为我无法将 WebAudio 和 WebRTC 很好地连接在一起。我的代码库中确实有一个剩余组件,如果您想看一下,它会监听本地麦克风并为当前用户触发“风声”。也许将来会添加一些东西?
在处理任何视觉内容之前,我想测试一下我想要的实时设置。由于实时功能存在一些限制,我希望它能够工作,以便:
为此,我想出了一个 useEffect 设置,它递归地加入到实时通道,如下所示:
这个 joinRoom 位于 useEffect 钩子内,并在安装房间组件时被调用。我在开发此功能时发现的一个警告是,currentPresences 参数在连接事件中不包含任何值,即使它可用。我不确定这是否是实施中的错误或按预期工作。因此,每当用户加入时,需要手动获取 room.presenceState 来获取房间中的参与者数量。
我们检查参与者数量,然后取消订阅当前房间并尝试加入另一个房间,或者然后继续当前房间。我们在加入事件中执行此操作,因为同步太晚了(它在加入或离开事件后触发)。
我通过在浏览器中打开大量选项卡来测试此实现,一切看起来都很棒!
之后,我想通过鼠标位置更新来调试解决方案,但很快遇到了一些在频道中发送过多消息的问题!解决方案:限制调用。
/** * Creates a throttled version of a function that can only be called at most once * in the specified time period. */ 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) } } }
光标想出了这个小油门函数创建器,我将它与眼动追踪广播一起使用,如下所示:
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 挂钩中的一个函数中完成的:
window.webgazer .setGazeListener(async (data: any) => { if (data == null || !currentChannel || !ctxRef.current) return try { // Get normalized gaze coordinates const gazeX = data.x / windowSize.width const gazeY = data.y / windowSize.height // Get video element const videoElement = document.getElementById('webgazerVideoFeed') as HTMLVideoElement if (!videoElement) { console.error('WebGazer video element not found') return } // Set canvas size to match video imageCanvasRef.current.width = videoElement.videoWidth imageCanvasRef.current.height = videoElement.videoHeight // Draw current frame to canvas ctxRef.current?.drawImage(videoElement, 0, 0) // Get eye patches 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 } // Calculate brightness for each eye const calculateBrightness = (imageData: ImageData) => { let total = 0 for (let i = 0; i < imageData.data.length; i += 16) { // Convert RGB to grayscale 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 // Update rolling average if (brightnessSamples.current.length >= SAMPLES_SIZE) { brightnessSamples.current.shift() // Remove oldest sample } brightnessSamples.current.push(avgBrightness) // Calculate dynamic threshold from rolling average const rollingAverage = brightnessSamples.current.reduce((a, b) => a + b, 0) / brightnessSamples.current.length const dynamicThreshold = rollingAverage * THRESHOLD_MULTIPLIER // Detect blink using dynamic threshold const blinkDetected = avgBrightness > dynamicThreshold // Debounce blink detection to avoid rapid changes if (blinkDetected !== isCurrentlyBlinking) { const now = Date.now() if (now - lastBlinkTime > 100) { // Minimum time between blink state changes isCurrentlyBlinking = blinkDetected lastBlinkTime = now } } // Use throttled broadcast instead of direct send throttledBroadcast({ userId: userId.current, isBlinking: isCurrentlyBlinking, gazeX, gazeY }) } catch (error) { console.error('Error processing gaze data:', error) } })
获取用户正在查看的信息很简单,就像获取屏幕上的鼠标位置一样。然而,我还想添加眨眼检测作为(一项很酷的)功能,这需要跳过一些环节。
当您在 google 上搜索有关 WebGazer 和眨眼检测的信息时,您可以看到初始实现的一些剩余内容。就像源代码中甚至有注释掉的代码一样。不幸的是,库中不存在此类功能。您需要手动完成。
经过大量的试验和错误,Cursor 和我想出了一个解决方案,可以根据眼罩数据计算像素和亮度级别,以确定用户何时眨眼。它还具有一些动态照明调整功能,因为我注意到(至少对我来说)网络摄像头并不总是能根据您的照明来识别您何时眨眼。对我来说,我的照片/房间越亮,效果越差,而在较暗的灯光下效果更好(见图)。
在调试眼动追踪功能时(WebGazer 有一个非常好的 .setPredictionPoints 调用,它在屏幕上显示一个红点以可视化您正在看的位置),我注意到跟踪不是很准确 除非您进行校准 这是项目要求您在加入任何房间之前要做的事情。
/** * Creates a throttled version of a function that can only be called at most once * in the specified time period. */ 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 网格内渲染,单元格对齐,这样整个房间看起来就像一只大眼睛。
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 滤镜制作动画有点奇怪,所以我决定采用这种方式。
<div> <h2> 结论 </h2> <p>现在您已经了解了:相当直接地了解如何使用 Supabase 的实时功能实现程式化的眼球追踪。就我个人而言,我发现这是一个非常有趣的实验,并且在进行过程中没有遇到太多问题。令人惊讶的是,在提交项目之前我不需要在最后一晚熬夜!</p> <p>请随意查看该项目或演示视频的结果。如果一群人同时使用它,可能会出现一些问题(很难测试,因为它需要多个设备和网络摄像头才能正确完成),但我想这是黑客马拉松项目的时尚?如果您确实进行了测试,请记住,如果您看到一只眼睛,那就是其他人通过互联网在某个地方看着您!</p> </div>
以上是使用 Supabase 和 WebGazer.js 构建实时眼动追踪体验的详细内容。更多信息请关注PHP中文网其他相关文章!