Rumah >hujung hadapan web >tutorial js >Membina pengalaman penjejakan mata masa nyata dengan Supabase dan WebGazer.js
TL;DR:
- Dibina dengan Supabase, React, WebGazer.js, Motion One, anime.js, Audio Stabil
- Memanfaatkan Kehadiran & Siaran Masa Nyata Supabase (tiada jadual pangkalan data digunakan sama sekali!)
- Repositori GitHub
- Tapak web
- Video demo
Satu lagi Hackathon Minggu Pelancaran Supabase dan satu lagi projek percubaan, yang dipanggil Merenung ke dalam Abyss. Ini akhirnya menjadi salah satu projek paling mudah dan kompleks pada masa yang sama. Nasib baik saya telah menikmati Cursor agak baru-baru ini, jadi saya mempunyai beberapa bantuan untuk berjaya! Saya juga ingin mengesahkan soalan dalam fikiran saya: adakah mungkin untuk menggunakan hanya ciri masa nyata daripada Supabase tanpa sebarang jadual pangkalan data? Jawapannya (mungkin agak jelas) ialah: ya, ya (sayangi awak, pasukan Realtime ♥️). Jadi mari kita menyelami sedikit lebih mendalam tentang pelaksanaannya.
Saya hanya satu hari secara rawak hanya memikirkan petikan Nietzsche tentang jurang dan bahawa adalah bagus (dan sejuk) untuk benar-benar memvisualisasikannya entah bagaimana: anda merenung ke dalam skrin gelap dan sesuatu merenung anda kembali. Tiada apa-apa lagi!
Pada mulanya saya mempunyai idea bahawa saya akan menggunakan Three.js untuk membuat projek ini, namun saya menyedari ini bermakna saya perlu mencipta atau mencari beberapa aset percuma untuk mata 3D. Saya memutuskan bahawa ia agak terlalu banyak, terutamanya kerana saya tidak mempunyai terlalu banyak masa untuk mengerjakan projek itu sendiri, dan sebaliknya memutuskan untuk melakukannya dalam 2D dengan SVG.
Saya juga tidak mahu ia hanya visual: ia akan menjadi pengalaman yang lebih baik dengan beberapa audio juga. Jadi saya mempunyai idea bahawa ia akan menjadi hebat jika peserta boleh bercakap dengan mikrofon dan orang lain boleh mendengarnya sebagai bisikan yang tidak layak atau angin yang berlalu. Ini, bagaimanapun, ternyata sangat mencabar dan memutuskan untuk menggugurkannya sepenuhnya kerana saya tidak dapat menyambungkan WebAudio dan WebRTC bersama-sama dengan baik. Saya mempunyai komponen sisa dalam pangkalan kod yang mendengar mikrofon tempatan dan mencetuskan "bunyi angin" untuk pengguna semasa jika anda ingin melihatnya. Mungkin sesuatu untuk ditambah pada masa hadapan?
Sebelum mengerjakan sebarang bahan visual, saya ingin menguji persediaan masa nyata yang saya fikirkan. Memandangkan terdapat beberapa batasan dalam ciri masa nyata, saya mahu ia berfungsi supaya:
Untuk ini saya menghasilkan persediaan useEffect di mana ia bergabung secara rekursif ke saluran masa nyata seperti:
JoinRoom ini tinggal di dalam cangkuk useEffect dan dipanggil apabila komponen bilik dipasang. Satu kaveat yang saya dapati semasa mengusahakan ciri ini ialah param currentPresences tidak mengandungi sebarang nilai dalam acara sertai walaupun ia tersedia. Saya tidak pasti sama ada ia adalah pepijat dalam pelaksanaan atau berfungsi seperti yang dimaksudkan. Oleh itu perlu melakukan pengambilan bilik secara manual.presenceState untuk mendapatkan bilangan peserta dalam bilik apabila pengguna menyertainya.
Kami menyemak kiraan peserta dan sama ada berhenti melanggan bilik semasa dan cuba menyertai bilik lain, atau kemudian meneruskan dengan bilik semasa. Kami melakukan ini dalam acara sertai kerana penyegerakan akan terlambat (ia tercetus selepas menyertai atau meninggalkan acara).
Saya telah menguji pelaksanaan ini dengan membuka banyak tab dalam penyemak imbas saya dan semuanya kelihatan membengkak!
Selepas itu saya ingin menyahpepijat penyelesaian dengan kemas kini kedudukan tetikus dan dengan cepat menghadapi beberapa isu menghantar terlalu banyak mesej dalam saluran! Penyelesaian: pendikit panggilan.
/** * 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) } } }
Kursor muncul dengan pencipta fungsi pendikit kecil ini dan saya menggunakannya dengan siaran penjejakan mata seperti ini:
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 })
Ini banyak membantu! Selain itu, dalam versi awal saya mempunyai mesej penjejakan mata yang dihantar dengan kehadiran namun siaran membenarkan lebih banyak mesej sesaat, jadi saya menukar pelaksanaan kepada itu. Ia amat penting dalam penjejakan mata kerana kamera akan merakam segala-galanya sepanjang masa.
Saya telah berjumpa dengan WebGazer.js suatu ketika dahulu apabila saya mula-mula mendapat idea untuk projek ini. Ia adalah projek yang sangat menarik dan berfungsi dengan baik!
Keupayaan pengesanan mata keseluruhan dilakukan dalam satu fungsi dalam cangkuk 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) } })
Mendapatkan maklumat di mana pengguna melihat adalah mudah dan berfungsi seperti mendapatkan kedudukan tetikus pada skrin. Walau bagaimanapun, saya juga ingin menambahkan pengesanan kelipan sebagai ciri (sejuk), yang memerlukan melompat melalui beberapa gelung.
Apabila anda menggoogle maklumat tentang WebGazer dan pengesanan berkelip, anda boleh melihat beberapa baki pelaksanaan awal. Seperti terdapat komen keluar kod dalam sumber walaupun. Malangnya keupayaan seperti ini tidak keluar di perpustakaan. Anda perlu melakukannya secara manual.
Selepas banyak percubaan dan ralat, Cursor dan saya dapat menghasilkan penyelesaian yang mengira piksel & tahap kecerahan daripada data patch mata untuk menentukan masa pengguna berkelip. Ia juga mempunyai beberapa pelarasan pencahayaan dinamik kerana saya perhatikan bahawa (sekurang-kurangnya bagi saya) kamera web tidak selalu mengenali apabila anda berkelip bergantung pada pencahayaan anda. Bagi saya ia berfungsi lebih teruk apabila gambar/bilik saya lebih terang dan lebih baik dalam pencahayaan yang lebih gelap (pergi angka).
Semasa menyahpepijat keupayaan penjejakan mata (WebGazer mempunyai panggilan .setPredictionPoints yang sangat bagus yang memaparkan titik merah pada skrin untuk menggambarkan di mana anda sedang melihat), saya mendapati penjejakan itu tidak begitu tepat melainkan anda menentukur itu. Yang mana projek meminta anda lakukan sebelum menyertai mana-mana bilik.
/** * 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) } } }
Ia adalah pengalaman yang sangat menarik untuk melihat ini dalam tindakan! Saya menggunakan pendekatan yang sama pada garisan sekeliling dan mengarahkan Kursor untuk "meruntuhkan"nya ke arah tengah: yang ia lakukan hampir sekali dengan sekali jalan!
Mata kemudiannya akan dipaparkan dalam grid CSS ringkas dengan sel dijajarkan supaya bilik penuh kelihatan seperti mata besar.
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 })
Kemudian tampar beberapa skrin pengenalan dan muzik latar belakang yang bagus dan projek itu boleh dilaksanakan!
Audio sentiasa meningkatkan pengalaman semasa anda mengerjakan perkara seperti ini, jadi saya menggunakan Audio Stabil untuk menjana muzik latar belakang apabila pengguna "memasuki jurang". Gesaan yang saya gunakan untuk muzik itu ialah yang berikut:
Ambien, menyeramkan, muzik latar belakang, bunyi bisikan, angin, tempo perlahan, ngeri, jurang
Saya juga berpendapat bahawa skrin hitam biasa agak membosankan, jadi saya menambah beberapa bahan penapis SVG animasi pada latar belakang. Selain itu, saya menambah bulatan gelap dan kabur di tengah-tengah skrin untuk mempunyai kesan pudar yang bagus. Saya mungkin boleh melakukan ini dengan penapis SVG, namun saya tidak mahu menghabiskan terlalu banyak masa untuk perkara ini. Kemudian untuk mempunyai lebih banyak pergerakan, saya membuat latar belakang berputar pada paksinya. Kadangkala membuat animasi dengan penapis SVG agak sukar, jadi saya memutuskan untuk melakukannya dengan cara ini.
<div> <h2> Kesimpulan </h2> <p>Jadi begitulah: lihat secara lurus ke hadapan tentang cara melaksanakan penjejakan mata yang digayakan dengan keupayaan masa nyata Supabase. Secara peribadi saya dapati percubaan ini sangat menarik dan tidak mengalami terlalu banyak gangguan semasa mengerjakannya. Dan yang menghairankan saya tidak perlu melakukan sepanjang malam untuk malam terakhir sebelum menyerahkan projek!</p> <p>Sila lihat projek atau video demo bagaimana hasilnya. Mungkin terdapat beberapa isu jika sekumpulan orang menggunakannya pada masa yang sama (sangat sukar untuk diuji kerana memerlukan berbilang peranti & kamera web untuk melakukannya dengan betul), tetapi saya rasa itu dalam fesyen projek hackathon? Dan jika anda mengujinya, ingat bahawa jika anda melihat mata, ia adalah orang lain yang memerhati anda di suatu tempat melalui internet!</p>
Atas ialah kandungan terperinci Membina pengalaman penjejakan mata masa nyata dengan Supabase dan WebGazer.js. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!