Spaces:
Running
Running
File size: 4,943 Bytes
bb2a2db 37a8ba6 bb2a2db 37a8ba6 bb2a2db 37a8ba6 bb2a2db 37a8ba6 bb2a2db 37a8ba6 bb2a2db 37a8ba6 bb2a2db 37a8ba6 bb2a2db 37a8ba6 bb2a2db 37a8ba6 bb2a2db 37a8ba6 bb2a2db 37a8ba6 bb2a2db | 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 148 149 | import React, { useState, useEffect, useRef, useCallback } from 'react';
const COLLECT_MS = 2000;
const CENTER_MS = 3000;
const VERIFY_MS = 3000;
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.verifying ? VERIFY_MS : (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) {
const success = calibration.success;
return (
<div ref={overlayRef} className="cal-overlay">
<div className={`cal-done-card ${success ? 'cal-done-success' : 'cal-done-fail'}`}>
<div className="cal-done-eyebrow">
{success ? 'Complete' : 'Failed'}
</div>
<h2 className="cal-done-title">
{success ? 'Calibration Complete' : 'Calibration Failed'}
</h2>
<p className="cal-done-subtitle">
{success
? 'Gaze tracking is now active.'
: 'Not enough samples collected. Try again.'}
</p>
</div>
</div>
);
}
const [tx, ty] = calibration.target || [0.5, 0.5];
const isVerifying = calibration.verifying;
const accent = isVerifying ? '#007BFF' : '#28a745';
const glow = isVerifying ? 'rgba(0, 123, 255, 0.6)' : 'rgba(40, 167, 69, 0.6)';
return (
<div ref={overlayRef} className="cal-overlay">
<div className="cal-header">
{isVerifying ? (
<>
<span className="cal-eyebrow cal-eyebrow-verify">Verification</span>
<p className="cal-instruction">
Look at the dot to confirm calibration accuracy
</p>
</>
) : (
<>
<span className="cal-eyebrow cal-eyebrow-collect">
Point {calibration.index + 1} of {calibration.numPoints}
</span>
<p className="cal-instruction">
{calibration.index === 0
? 'Look at the center dot \u2014 this sets your baseline'
: 'Hold your gaze steady on the target'}
</p>
</>
)}
</div>
<div
className="cal-target"
style={{ left: `${tx * 100}%`, top: `${ty * 100}%` }}
>
<svg width="60" height="60" className="cal-ring">
<circle cx="30" cy="30" r="24" fill="none" stroke="rgba(255,255,255,0.12)" strokeWidth="3" />
<circle
cx="30" cy="30" r="24" fill="none" stroke={accent} strokeWidth="3"
strokeDasharray={`${progress * 150.8} 150.8`} strokeLinecap="round"
transform="rotate(-90, 30, 30)"
/>
</svg>
<div
className="cal-dot"
style={{
background: `radial-gradient(circle, #fff 30%, ${accent} 100%)`,
boxShadow: `0 0 24px ${glow}`,
}}
/>
</div>
<button onClick={handleCancel} className="cal-cancel">
Cancel Calibration
</button>
</div>
);
}
export default CalibrationOverlay;
|