Spaces:
Running
Running
File size: 5,306 Bytes
7b53d75 | 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 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | import React, { useState, useEffect, useRef, useCallback } from 'react';
const COLLECT_MS = 2000;
const CENTER_MS = 3000; // centre point gets extra time (bias reference)
function CalibrationOverlay({ calibration, videoManager }) {
const [progress, setProgress] = useState(0);
const timerRef = useRef(null);
const startRef = useRef(null);
const overlayRef = useRef(null);
const enterFullscreen = useCallback(() => {
const el = overlayRef.current;
if (!el) return;
const req = el.requestFullscreen || el.webkitRequestFullscreen || el.msRequestFullscreen;
if (req) req.call(el).catch(() => {});
}, []);
const exitFullscreen = useCallback(() => {
if (document.fullscreenElement || document.webkitFullscreenElement) {
const exit = document.exitFullscreen || document.webkitExitFullscreen || document.msExitFullscreen;
if (exit) exit.call(document).catch(() => {});
}
}, []);
useEffect(() => {
if (calibration && calibration.active && !calibration.done) {
const t = setTimeout(enterFullscreen, 100);
return () => clearTimeout(t);
}
}, [calibration?.active]);
useEffect(() => {
if (!calibration || !calibration.active) exitFullscreen();
}, [calibration?.active]);
useEffect(() => {
if (!calibration || !calibration.collecting || calibration.done) {
setProgress(0);
if (timerRef.current) cancelAnimationFrame(timerRef.current);
return;
}
startRef.current = performance.now();
const duration = calibration.index === 0 ? CENTER_MS : COLLECT_MS;
const tick = () => {
const pct = Math.min((performance.now() - startRef.current) / duration, 1);
setProgress(pct);
if (pct >= 1) {
if (videoManager) videoManager.nextCalibrationPoint();
startRef.current = performance.now();
setProgress(0);
}
timerRef.current = requestAnimationFrame(tick);
};
timerRef.current = requestAnimationFrame(tick);
return () => { if (timerRef.current) cancelAnimationFrame(timerRef.current); };
}, [calibration?.index, calibration?.collecting, calibration?.done]);
const handleCancel = () => {
if (videoManager) videoManager.cancelCalibration();
exitFullscreen();
};
if (!calibration || !calibration.active) return null;
if (calibration.done) {
return (
<div ref={overlayRef} style={overlayStyle}>
<div style={messageBoxStyle}>
<h2 style={{ margin: '0 0 10px', color: calibration.success ? '#4ade80' : '#f87171' }}>
{calibration.success ? 'Calibration Complete' : 'Calibration Failed'}
</h2>
<p style={{ color: '#ccc', margin: 0 }}>
{calibration.success
? 'Gaze tracking is now active.'
: 'Not enough samples collected. Try again.'}
</p>
</div>
</div>
);
}
const [tx, ty] = calibration.target || [0.5, 0.5];
return (
<div ref={overlayRef} style={overlayStyle}>
<div style={{
position: 'absolute', top: '30px', left: '50%', transform: 'translateX(-50%)',
color: '#fff', fontSize: '16px', textAlign: 'center',
textShadow: '0 0 8px rgba(0,0,0,0.8)', pointerEvents: 'none',
}}>
<div style={{ fontWeight: 'bold', fontSize: '20px' }}>
Look at the dot ({calibration.index + 1}/{calibration.numPoints})
</div>
<div style={{ fontSize: '14px', color: '#aaa', marginTop: '6px' }}>
{calibration.index === 0
? 'Look at the center dot - this sets your baseline'
: 'Hold your gaze steady on the target'}
</div>
</div>
<div style={{
position: 'absolute', left: `${tx * 100}%`, top: `${ty * 100}%`,
transform: 'translate(-50%, -50%)',
}}>
<svg width="60" height="60" style={{ position: 'absolute', left: '-30px', top: '-30px' }}>
<circle cx="30" cy="30" r="24" fill="none" stroke="rgba(255,255,255,0.15)" strokeWidth="3" />
<circle cx="30" cy="30" r="24" fill="none" stroke="#4ade80" strokeWidth="3"
strokeDasharray={`${progress * 150.8} 150.8`} strokeLinecap="round"
transform="rotate(-90, 30, 30)" />
</svg>
<div style={{
width: '20px', height: '20px', borderRadius: '50%',
background: 'radial-gradient(circle, #fff 30%, #4ade80 100%)',
boxShadow: '0 0 20px rgba(74, 222, 128, 0.8)',
}} />
</div>
<button onClick={handleCancel} style={{
position: 'absolute', bottom: '40px', left: '50%', transform: 'translateX(-50%)',
padding: '10px 28px', background: 'rgba(255,255,255,0.1)',
border: '1px solid rgba(255,255,255,0.3)', color: '#fff',
borderRadius: '20px', cursor: 'pointer', fontSize: '14px',
}}>
Cancel Calibration
</button>
</div>
);
}
const overlayStyle = {
position: 'fixed', top: 0, left: 0, width: '100vw', height: '100vh',
background: 'rgba(0, 0, 0, 0.92)', zIndex: 10000,
display: 'flex', alignItems: 'center', justifyContent: 'center',
};
const messageBoxStyle = {
textAlign: 'center', padding: '30px 40px',
background: 'rgba(30, 30, 50, 0.9)', borderRadius: '16px',
border: '1px solid rgba(255,255,255,0.1)',
};
export default CalibrationOverlay;
|