Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Eye Movement Tracker</title> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --primary-color: #5d69b2; | |
| --secondary-color: #3a416f; | |
| --accent-color: #ff7e5f; | |
| --bg-color: #f5f7fa; | |
| --text-color: #333; | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background-color: var(--bg-color); | |
| color: var(--text-color); | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| min-height: 100vh; | |
| padding: 2rem; | |
| position: relative; | |
| overflow-x: hidden; | |
| } | |
| header { | |
| text-align: center; | |
| margin-bottom: 2rem; | |
| width: 100%; | |
| } | |
| h1 { | |
| color: var(--primary-color); | |
| margin-bottom: 0.5rem; | |
| font-size: 2.2rem; | |
| } | |
| .subtitle { | |
| color: var(--secondary-color); | |
| opacity: 0.8; | |
| margin-bottom: 1.5rem; | |
| } | |
| .tracker-container { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| width: 100%; | |
| max-width: 800px; | |
| gap: 2rem; | |
| } | |
| .face-container { | |
| position: relative; | |
| width: 300px; | |
| height: 300px; | |
| background-color: #fff; | |
| border-radius: 50%; | |
| box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| margin-bottom: 1rem; | |
| transition: transform 0.3s ease; | |
| } | |
| .face-container:hover { | |
| transform: scale(1.02); | |
| } | |
| .eyes-wrapper { | |
| display: flex; | |
| gap: 60px; | |
| position: relative; | |
| top: -20px; | |
| } | |
| .eye { | |
| width: 60px; | |
| height: 60px; | |
| background-color: #fff; | |
| border-radius: 50%; | |
| position: relative; | |
| box-shadow: inset 0 0 15px rgba(0, 0, 0, 0.2); | |
| overflow: hidden; | |
| } | |
| .pupil { | |
| width: 30px; | |
| height: 30px; | |
| background-color: var(--secondary-color); | |
| border-radius: 50%; | |
| position: absolute; | |
| top: 15px; | |
| left: 15px; | |
| transition: all 0.1s ease; | |
| } | |
| .metrics-container { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 1rem; | |
| justify-content: center; | |
| width: 100%; | |
| } | |
| .metric-card { | |
| background-color: white; | |
| border-radius: 12px; | |
| padding: 1.5rem; | |
| min-width: 200px; | |
| box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05); | |
| flex-grow: 1; | |
| transition: transform 0.3s ease, box-shadow 0.3s ease; | |
| } | |
| .metric-card:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); | |
| } | |
| .metric-title { | |
| color: var(--primary-color); | |
| font-size: 0.9rem; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| margin-bottom: 0.5rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .metric-value { | |
| font-size: 1.8rem; | |
| font-weight: bold; | |
| color: var(--secondary-color); | |
| } | |
| .metric-unit { | |
| font-size: 0.9rem; | |
| color: #888; | |
| margin-left: 0.3rem; | |
| } | |
| .chart-container { | |
| width: 100%; | |
| height: 200px; | |
| background-color: white; | |
| border-radius: 12px; | |
| padding: 1rem; | |
| box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| canvas { | |
| width: 100%; | |
| height: 100%; | |
| } | |
| .controls { | |
| display: flex; | |
| gap: 1rem; | |
| margin-top: 1rem; | |
| } | |
| button { | |
| padding: 0.8rem 1.5rem; | |
| background-color: var(--primary-color); | |
| color: white; | |
| border: none; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-weight: 600; | |
| transition: all 0.3s ease; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| button:hover { | |
| background-color: var(--secondary-color); | |
| transform: translateY(-2px); | |
| } | |
| button.secondary { | |
| background-color: white; | |
| color: var(--primary-color); | |
| border: 1px solid #ddd; | |
| } | |
| button.secondary:hover { | |
| background-color: #f0f0f0; | |
| } | |
| .data-points { | |
| position: absolute; | |
| top: 20px; | |
| right: 20px; | |
| width: 15px; | |
| height: 15px; | |
| background-color: var(--accent-color); | |
| border-radius: 50%; | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: opacity 0.3s ease; | |
| } | |
| footer { | |
| margin-top: 3rem; | |
| text-align: center; | |
| color: #888; | |
| font-size: 0.9rem; | |
| } | |
| @media (max-width: 600px) { | |
| .tracker-container { | |
| gap: 1rem; | |
| } | |
| .face-container { | |
| width: 250px; | |
| height: 250px; | |
| } | |
| .eyes-wrapper { | |
| gap: 40px; | |
| } | |
| .eye { | |
| width: 50px; | |
| height: 50px; | |
| } | |
| .pupil { | |
| width: 25px; | |
| height: 25px; | |
| top: 12.5px; | |
| left: 12.5px; | |
| } | |
| .metric-card { | |
| min-width: 150px; | |
| padding: 1rem; | |
| } | |
| .metric-value { | |
| font-size: 1.5rem; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <h1><i class="fas fa-eye"></i> Eye Movement Tracker</h1> | |
| <p class="subtitle">Interactive demonstration of gaze tracking using mouse position</p> | |
| </header> | |
| <div class="tracker-container"> | |
| <div class="face-container"> | |
| <div class="eyes-wrapper"> | |
| <div class="eye"> | |
| <div class="pupil" id="left-pupil"></div> | |
| </div> | |
| <div class="eye"> | |
| <div class="pupil" id="right-pupil"></div> | |
| </div> | |
| </div> | |
| <div class="data-points" id="data-point"></div> | |
| </div> | |
| <div class="metrics-container"> | |
| <div class="metric-card"> | |
| <div class="metric-title"> | |
| <i class="fas fa-crosshairs"></i> Gaze Position | |
| </div> | |
| <div class="metric-value" id="gaze-position"> | |
| <span id="gaze-x">0</span>, <span id="gaze-y">0</span> | |
| </div> | |
| </div> | |
| <div class="metric-card"> | |
| <div class="metric-title"> | |
| <i class="fas fa-running"></i> Movement Speed | |
| </div> | |
| <div class="metric-value" id="movement-speed">0<span class="metric-unit">px/s</span></div> | |
| </div> | |
| <div class="metric-card"> | |
| <div class="metric-title"> | |
| <i class="fas fa-history"></i> Time Focused | |
| </div> | |
| <div class="metric-value" id="time-focused">0<span class="metric-unit">s</span></div> | |
| </div> | |
| <div class="metric-card"> | |
| <div class="metric-title"> | |
| <i class="fas fa-bullseye"></i> Fixations | |
| </div> | |
| <div class="metric-value" id="fixation-count">0</div> | |
| </div> | |
| </div> | |
| <div class="chart-container"> | |
| <canvas id="gaze-chart"></canvas> | |
| </div> | |
| <div class="controls"> | |
| <button id="start-btn"><i class="fas fa-play"></i> Start Tracking</button> | |
| <button id="reset-btn" class="secondary"><i class="fas fa-redo"></i> Reset</button> | |
| <button id="heatmap-btn" class="secondary"><i class="fas fa-fire"></i> Show Heatmap</button> | |
| </div> | |
| </div> | |
| <footer> | |
| <p>Eye Movement Tracker © 2025 | Uses mouse position to simulate eye tracking</p> | |
| </footer> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // Elements | |
| const leftPupil = document.getElementById('left-pupil'); | |
| const rightPupil = document.getElementById('right-pupil'); | |
| const gazeX = document.getElementById('gaze-x'); | |
| const gazeY = document.getElementById('gaze-y'); | |
| const movementSpeed = document.getElementById('movement-speed'); | |
| const timeFocused = document.getElementById('time-focused'); | |
| const fixationCount = document.getElementById('fixation-count'); | |
| const startBtn = document.getElementById('start-btn'); | |
| const resetBtn = document.getElementById('reset-btn'); | |
| const heatmapBtn = document.getElementById('heatmap-btn'); | |
| const dataPoint = document.getElementById('data-point'); | |
| const faceContainer = document.querySelector('.face-container'); | |
| // Variables | |
| let trackingActive = false; | |
| let startTime = 0; | |
| let lastPosition = { x: 0, y: 0 }; | |
| let lastTime = 0; | |
| let currentSpeed = 0; | |
| let fixations = 0; | |
| let fixationStartTime = 0; | |
| let isFixated = false; | |
| let gazeHistory = []; | |
| let heatmapPoints = []; | |
| // Chart setup | |
| const ctx = document.getElementById('gaze-chart').getContext('2d'); | |
| const gazeChart = new Chart(ctx, { | |
| type: 'line', | |
| data: { | |
| labels: [], | |
| datasets: [{ | |
| label: 'Gaze Movement Speed (px/s)', | |
| data: [], | |
| borderColor: '#5d69b2', | |
| backgroundColor: 'rgba(93, 105, 178, 0.1)', | |
| tension: 0.4, | |
| fill: true | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| scales: { | |
| y: { | |
| beginAtZero: true | |
| } | |
| }, | |
| animation: { | |
| duration: 0 | |
| } | |
| } | |
| }); | |
| // Helper functions | |
| function calculatePupilPosition(eye, event) { | |
| const eyeRect = eye.getBoundingClientRect(); | |
| const eyeCenterX = eyeRect.left + eyeRect.width / 2; | |
| const eyeCenterY = eyeRect.top + eyeRect.height / 2; | |
| const angle = Math.atan2(event.clientY - eyeCenterY, event.clientX - eyeCenterX); | |
| const distance = Math.min(15, Math.sqrt( | |
| Math.pow(event.clientX - eyeCenterX, 2) + | |
| Math.pow(event.clientY - eyeCenterY, 2) | |
| ) / 10); | |
| const x = distance * Math.cos(angle); | |
| const y = distance * Math.sin(angle); | |
| return { x, y }; | |
| } | |
| function updateMetrics(event) { | |
| const now = Date.now(); | |
| const timeElapsed = (now - lastTime) / 1000; | |
| if (timeElapsed > 0) { | |
| const dx = event.clientX - lastPosition.x; | |
| const dy = event.clientY - lastPosition.y; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| currentSpeed = distance / timeElapsed; | |
| // Check for fixation (hovering in the same area) | |
| if (distance < 15) { // 15px threshold for fixation | |
| if (!isFixated) { | |
| isFixated = true; | |
| fixationStartTime = now; | |
| } | |
| // If fixation lasts more than 200ms, count it | |
| if (isFixated && now - fixationStartTime > 200 && !fixationCounted) { | |
| fixations++; | |
| fixationCount.textContent = fixations; | |
| fixationCounted = true; | |
| // Show fixation point | |
| if (dataPoint) { | |
| dataPoint.style.left = `${event.clientX - faceContainer.getBoundingClientRect().left - 7.5}px`; | |
| dataPoint.style.top = `${event.clientY - faceContainer.getBoundingClientRect().top - 7.5}px`; | |
| dataPoint.style.opacity = '0.7'; | |
| setTimeout(() => { | |
| dataPoint.style.opacity = '0'; | |
| }, 500); | |
| } | |
| // Store heatmap point | |
| const rect = faceContainer.getBoundingClientRect(); | |
| const x = event.clientX - rect.left; | |
| const y = event.clientY - rect.top; | |
| heatmapPoints.push({ x, y }); | |
| } | |
| } else { | |
| isFixated = false; | |
| fixationCounted = false; | |
| } | |
| } | |
| // Update metrics display | |
| movementSpeed.textContent = Math.round(currentSpeed); | |
| timeFocused.textContent = Math.round((now - startTime) / 1000); | |
| // Update chart data | |
| if (gazeHistory.length > 50) { | |
| gazeHistory.shift(); | |
| gazeChart.data.labels.shift(); | |
| gazeChart.data.datasets[0].data.shift(); | |
| } | |
| gazeHistory.push(currentSpeed); | |
| gazeChart.data.labels.push(''); | |
| gazeChart.data.datasets[0].data.push(currentSpeed); | |
| gazeChart.update(); | |
| // Update last position and time | |
| lastPosition = { x: event.clientX, y: event.clientY }; | |
| lastTime = now; | |
| } | |
| function startTracking() { | |
| trackingActive = true; | |
| startTime = Date.now(); | |
| startBtn.innerHTML = '<i class="fas fa-pause"></i> Pause Tracking'; | |
| startBtn.style.backgroundColor = '#ff7e5f'; | |
| } | |
| function pauseTracking() { | |
| trackingActive = false; | |
| startBtn.innerHTML = '<i class="fas fa-play"></i> Resume Tracking'; | |
| startBtn.style.backgroundColor = '#5d69b2'; | |
| } | |
| function resetTracking() { | |
| pauseTracking(); | |
| startTime = 0; | |
| currentSpeed = 0; | |
| fixations = 0; | |
| gazeHistory = []; | |
| heatmapPoints = []; | |
| // Reset metrics display | |
| gazeX.textContent = '0'; | |
| gazeY.textContent = '0'; | |
| movementSpeed.textContent = '0'; | |
| timeFocused.textContent = '0'; | |
| fixationCount.textContent = '0'; | |
| // Reset chart | |
| gazeChart.data.labels = []; | |
| gazeChart.data.datasets[0].data = []; | |
| gazeChart.update(); | |
| // Reset pupils to center | |
| leftPupil.style.transform = 'translate(0, 0)'; | |
| rightPupil.style.transform = 'translate(0, 0)'; | |
| } | |
| function showHeatmap() { | |
| const heatmapOverlay = document.createElement('div'); | |
| heatmapOverlay.style.position = 'absolute'; | |
| heatmapOverlay.style.top = '0'; | |
| heatmapOverlay.style.left = '0'; | |
| heatmapOverlay.style.width = '100%'; | |
| heatmapOverlay.style.height = '100%'; | |
| heatmapOverlay.style.background = 'rgba(255, 126, 95, 0.1)'; | |
| heatmapOverlay.style.borderRadius = '50%'; | |
| heatmapPoints.forEach(point => { | |
| const heatPoint = document.createElement('div'); | |
| heatPoint.style.position = 'absolute'; | |
| heatPoint.style.width = '20px'; | |
| heatPoint.style.height = '20px'; | |
| heatPoint.style.background = 'rgba(255, 126, 95, 0.3)'; | |
| heatPoint.style.borderRadius = '50%'; | |
| heatPoint.style.left = `${point.x - 10}px`; | |
| heatPoint.style.top = `${point.y - 10}px`; | |
| heatPoint.style.pointerEvents = 'none'; | |
| heatmapOverlay.appendChild(heatPoint); | |
| }); | |
| faceContainer.appendChild(heatmapOverlay); | |
| setTimeout(() => { | |
| faceContainer.removeChild(heatmapOverlay); | |
| }, 5000); | |
| } | |
| // Event listeners | |
| document.addEventListener('mousemove', function(event) { | |
| // Update gaze position display | |
| gazeX.textContent = event.clientX; | |
| gazeY.textContent = event.clientY; | |
| // Calculate pupil positions | |
| const leftPupilPos = calculatePupilPosition(leftPupil.parentElement, event); | |
| const rightPupilPos = calculatePupilPosition(rightPupil.parentElement, event); | |
| // Apply pupil positions | |
| leftPupil.style.transform = `translate(${leftPupilPos.x}px, ${leftPupilPos.y}px)`; | |
| rightPupil.style.transform = `translate(${rightPupilPos.x}px, ${rightPupilPos.y}px)`; | |
| // Update metrics if tracking is active | |
| if (trackingActive) { | |
| updateMetrics(event); | |
| } | |
| }); | |
| startBtn.addEventListener('click', function() { | |
| if (trackingActive) { | |
| pauseTracking(); | |
| } else { | |
| startTracking(); | |
| } | |
| }); | |
| resetBtn.addEventListener('click', resetTracking); | |
| heatmapBtn.addEventListener('click', showHeatmap); | |
| // Initialize | |
| resetTracking(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |