eyetrack / index.html
devarajns's picture
Update index.html
281469b verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Webcam Eye 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;
}
.camera-container {
position: relative;
width: 100%;
max-width: 640px;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
#video {
width: 100%;
display: block;
background-color: #000;
}
#canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
}
.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#gaze-chart {
width: 100%;
height: 100%;
}
.controls {
display: flex;
gap: 1rem;
margin-top: 1rem;
flex-wrap: wrap;
justify-content: center;
}
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;
}
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 2rem;
background-color: white;
border-radius: 12px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
width: 100%;
}
.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top: 4px solid var(--primary-color);
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-message {
color: #e74c3c;
background-color: #fceae9;
padding: 1rem;
border-radius: 8px;
max-width: 100%;
text-align: center;
}
footer {
margin-top: 3rem;
text-align: center;
color: #888;
font-size: 0.9rem;
}
@media (max-width: 600px) {
.tracker-container {
gap: 1rem;
}
.metric-card {
min-width: 150px;
padding: 1rem;
}
.metric-value {
font-size: 1.5rem;
}
}
</style>
</head>
<body>
<header>
<h1><i class="fas fa-eye"></i> Webcam Eye Tracker</h1>
<p class="subtitle">Real-time eye tracking using your webcam and face detection</p>
</header>
<div class="tracker-container">
<div class="loading" id="loading">
<div class="spinner"></div>
<p>Loading face detection models...</p>
</div>
<div class="camera-container" id="camera-container" style="display: none;">
<video id="video" width="640" height="480" autoplay muted playsinline></video>
<canvas id="canvas" width="640" height="480"></canvas>
</div>
<div class="metrics-container">
<div class="metric-card">
<div class="metric-title">
<i class="fas fa-crosshairs"></i> Eye Position
</div>
<div class="metric-value" id="eye-position">
<span id="eye-x">0</span>, <span id="eye-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 Tracked
</div>
<div class="metric-value" id="time-tracked">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="debug-btn" class="secondary"><i class="fas fa-bug"></i> Toggle Debug</button>
</div>
<div class="error-message" id="error-message" style="display: none;"></div>
</div>
<footer>
<p>Webcam Eye Tracker &copy; 2024 | Uses face-api.js for face and eye detection</p>
</footer>
<!-- Load face-api.js from CDN -->
<script src="https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.min.js"></script>
<!-- Load Chart.js from CDN -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
document.addEventListener('DOMContentLoaded', async function() {
// DOM Elements
const video = document.getElementById('video');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const startBtn = document.getElementById('start-btn');
const resetBtn = document.getElementById('reset-btn');
const debugBtn = document.getElementById('debug-btn');
const eyeX = document.getElementById('eye-x');
const eyeY = document.getElementById('eye-y');
const movementSpeed = document.getElementById('movement-speed');
const timeTracked = document.getElementById('time-tracked');
const fixationCount = document.getElementById('fixation-count');
const loadingElement = document.getElementById('loading');
const cameraContainer = document.getElementById('camera-container');
const errorMessage = document.getElementById('error-message');
// 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 showDebug = false;
let modelsLoaded = false;
let stream = null;
// Chart setup
const chartCtx = document.getElementById('gaze-chart').getContext('2d');
const gazeChart = new Chart(chartCtx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'Eye 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
}
}
});
// Load face-api.js models
async function loadModels() {
try {
await faceapi.nets.tinyFaceDetector.loadFromUri('https://justadudewhohacks.github.io/face-api.js/models');
await faceapi.nets.faceLandmark68Net.loadFromUri('https://justadudewhohacks.github.io/face-api.js/models');
modelsLoaded = true;
// Hide loading and show camera UI
loadingElement.style.display = 'none';
cameraContainer.style.display = 'block';
// Prompt for camera access
initCamera();
} catch (error) {
console.error('Error loading models:', error);
showError("Failed to load face detection models. Please check your internet connection and try again.");
}
}
// Initialize camera
async function initCamera() {
try {
stream = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 640 },
height: { ideal: 480 },
facingMode: 'user'
},
audio: false
});
video.srcObject = stream;
video.play();
} catch (error) {
console.error('Camera error:', error);
if (error.name === 'NotAllowedError') {
showError("Camera access was denied. Please allow camera access to use this feature.");
} else if (error.name === 'NotFoundError') {
showError("No camera found. Please connect a webcam to use this feature.");
} else {
showError("Failed to access camera. Please try again.");
}
}
}
// Show error message
function showError(message) {
errorMessage.textContent = message;
errorMessage.style.display = 'block';
loadingElement.style.display = 'none';
}
// Calculate eye position based on landmarks
function getEyePosition(landmarks) {
if (!landmarks || !landmarks.getLeftEye || !landmarks.getRightEye) {
return { x: 0, y: 0 };
}
const leftEye = landmarks.getLeftEye();
const rightEye = landmarks.getRightEye();
// Calculate center of each eye
const leftEyeCenter = leftEye.reduce((sum, point) => {
return { x: sum.x + point.x, y: sum.y + point.y };
}, { x: 0, y: 0 });
const rightEyeCenter = rightEye.reduce((sum, point) => {
return { x: sum.x + point.x, y: sum.y + point.y };
}, { x: 0, y: 0 });
leftEyeCenter.x /= leftEye.length;
leftEyeCenter.y /= leftEye.length;
rightEyeCenter.x /= rightEye.length;
rightEyeCenter.y /= rightEye.length;
// Calculate midpoint between eyes
return {
x: (leftEyeCenter.x + rightEyeCenter.x) / 2,
y: (leftEyeCenter.y + rightEyeCenter.y) / 2
};
}
// Update metrics based on eye position
function updateMetrics(position) {
const now = Date.now();
const timeElapsed = (now - lastTime) / 1000;
if (timeElapsed > 0) {
const dx = position.x - lastPosition.x;
const dy = position.y - 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) {
fixations++;
fixationCount.textContent = fixations;
isFixated = false; // Reset to count new fixations
}
} else {
isFixated = false;
}
}
// Update metrics display
movementSpeed.textContent = Math.round(currentSpeed);
timeTracked.textContent = Math.round((now - startTime) / 1000);
eyeX.textContent = Math.round(position.x);
eyeY.textContent = Math.round(position.y);
// 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 = position;
lastTime = now;
}
// Process video frame for face detection
async function processVideo() {
if (!trackingActive || !modelsLoaded) {
requestAnimationFrame(processVideo);
return;
}
try {
// Detect faces with landmarks
const options = new faceapi.TinyFaceDetectorOptions({
inputSize: 128,
scoreThreshold: 0.5
});
const result = await faceapi.detectSingleFace(video, options)
.withFaceLandmarks();
// Clear canvas before drawing
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (result) {
const { landmarks, detection } = result;
// Calculate eye position
const eyePosition = getEyePosition(landmarks);
// Update metrics with eye position
updateMetrics(eyePosition);
// Draw debug information if enabled
if (showDebug) {
// Draw face detection box
const box = detection.box;
ctx.strokeStyle = '#00FF00';
ctx.lineWidth = 2;
ctx.strokeRect(box.x, box.y, box.width, box.height);
// Draw landmarks
faceapi.draw.drawFaceLandmarks(canvas, landmarks);
// Draw eye position
ctx.fillStyle = '#FF0000';
ctx.beginPath();
ctx.arc(eyePosition.x, eyePosition.y, 5, 0, 2 * Math.PI);
ctx.fill();
}
}
} catch (error) {
console.error('Detection error:', error);
}
requestAnimationFrame(processVideo);
}
// Start tracking
function startTracking() {
if (!modelsLoaded) {
showError("Face detection models not loaded yet. Please wait.");
return;
}
trackingActive = true;
startTime = Date.now();
lastTime = Date.now();
startBtn.innerHTML = '<i class="fas fa-pause"></i> Pause Tracking';
startBtn.style.backgroundColor = '#ff7e5f';
// Start processing video
processVideo();
}
// Pause tracking
function pauseTracking() {
trackingActive = false;
startBtn.innerHTML = '<i class="fas fa-play"></i> Resume Tracking';
startBtn.style.backgroundColor = '#5d69b2';
}
// Reset tracking
function resetTracking() {
pauseTracking();
startTime = 0;
currentSpeed = 0;
fixations = 0;
gazeHistory = [];
// Reset metrics display
eyeX.textContent = '0';
eyeY.textContent = '0';
movementSpeed.textContent = '0';
timeTracked.textContent = '0';
fixationCount.textContent = '0';
// Reset chart
gazeChart.data.labels = [];
gazeChart.data.datasets[0].data = [];
gazeChart.update();
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
// Event listeners
startBtn.addEventListener('click', function() {
if (trackingActive) {
pauseTracking();
} else {
startTracking();
}
});
resetBtn.addEventListener('click', resetTracking);
debugBtn.addEventListener('click', function() {
showDebug = !showDebug;
debugBtn.innerHTML = showDebug ?
'<i class="fas fa-eye-slash"></i> Hide Debug' :
'<i class="fas fa-eye"></i> Show Debug';
});
// Initialize
loadModels();
});
</script>
</body>
</html>