UNPKG

aura-glass

Version:

A comprehensive glassmorphism design system for React applications with 142+ production-ready components

743 lines (740 loc) 23.6 kB
'use client'; import { jsx, jsxs } from 'react/jsx-runtime'; import { useReducedMotion } from '../../hooks/useReducedMotion.js'; import { useRef, useState, useEffect, useCallback, useContext, createContext } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { cn } from '../../lib/utilsComprehensive.js'; // WebGazer integration class class WebGazerIntegration { constructor() { this.isInitialized = false; this.isCalibrated = false; this.gazeListeners = []; this.calibrationPoints = []; this.lastGaze = null; } async initialize() { try { // Check if WebGazer is already loaded if (typeof window !== 'undefined' && !window.webgazer) { // Dynamically load WebGazer await this.loadWebGazer(); } const webgazer = window.webgazer; if (!webgazer) { throw new Error('WebGazer failed to load'); } // Initialize WebGazer await webgazer.setRegression('ridge').setTracker('TFFacemesh').setGazeListener((data, timestamp) => { if (data) { const gazePoint = { x: data.x, y: data.y, timestamp, confidence: data.confidence || 0.5 }; this.lastGaze = gazePoint; this.gazeListeners.forEach(listener => listener(gazePoint)); } }).saveDataAcrossSessions(true).begin(); // Hide the video overlay webgazer.showVideoPreview(false).showPredictionPoints(false); this.isInitialized = true; return true; } catch (error) { console.warn('Failed to initialize eye tracking:', error); return false; } } async loadWebGazer() { return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = 'https://webgazer.cs.brown.edu/webgazer.js'; script.onload = () => resolve(); script.onerror = () => reject(new Error('Failed to load WebGazer')); document.head.appendChild(script); }); } addGazeListener(listener) { this.gazeListeners.push(listener); return () => { const index = this.gazeListeners.indexOf(listener); if (index > -1) { this.gazeListeners.splice(index, 1); } }; } async startCalibration() { if (!this.isInitialized) return; this.calibrationPoints = [{ x: 0.1, y: 0.1 }, { x: 0.5, y: 0.1 }, { x: 0.9, y: 0.1 }, { x: 0.1, y: 0.5 }, { x: 0.5, y: 0.5 }, { x: 0.9, y: 0.5 }, { x: 0.1, y: 0.9 }, { x: 0.5, y: 0.9 }, { x: 0.9, y: 0.9 }]; } async calibratePoint(x, y) { if (!this.isInitialized) return; const webgazer = window.webgazer; if (webgazer) { // Add calibration point await new Promise(resolve => { webgazer.recordScreenPosition(x, y, 'click'); setTimeout(resolve, 1000); // Allow time for calibration }); } } finishCalibration() { this.isCalibrated = true; } getLastGaze() { return this.lastGaze; } cleanup() { if (this.isInitialized && window.webgazer) { window.webgazer.end(); } this.gazeListeners = []; this.isInitialized = false; this.isCalibrated = false; } } // Eye tracking engine class EyeTrackingEngine { constructor() { this.gazeHistory = []; this.regions = new Map(); this.activeInteractions = new Map(); this.fixationThreshold = 100; // ms this.saccadeThreshold = 50; // pixels this.isTracking = false; this.webgazer = new WebGazerIntegration(); } async initialize() { const success = await this.webgazer.initialize(); if (success) { this.webgazer.addGazeListener(this.handleGazePoint.bind(this)); this.isTracking = true; } return success; } handleGazePoint(gaze) { if (!this.isTracking) return; // Add to history this.gazeHistory.push(gaze); // Keep only recent history if (this.gazeHistory.length > 1000) { this.gazeHistory = this.gazeHistory.slice(-500); } // Analyze gaze patterns this.analyzeGazePatterns(gaze); // Check region interactions this.checkRegionInteractions(gaze); } analyzeGazePatterns(currentGaze) { if (this.gazeHistory.length < 2) return; const previousGaze = this.gazeHistory[this.gazeHistory.length - 2]; const distance = Math.sqrt(Math.pow(currentGaze.x - previousGaze.x, 2) + Math.pow(currentGaze.y - previousGaze.y, 2)); const timeDiff = currentGaze.timestamp - previousGaze.timestamp; // Detect fixations, saccades, etc. if (distance < this.saccadeThreshold && timeDiff > this.fixationThreshold) { // Potential fixation this.handleFixation(currentGaze); } else if (distance > this.saccadeThreshold) { // Saccade movement this.handleSaccade(previousGaze, currentGaze); } } handleFixation(gaze) { // Find which region this fixation is in const region = this.findRegionAt(gaze.x, gaze.y); if (region) { const existingInteraction = this.activeInteractions.get(region.id); if (existingInteraction) { // Update existing interaction existingInteraction.endTime = gaze.timestamp; existingInteraction.duration = gaze.timestamp - existingInteraction.startTime; existingInteraction.intensity = Math.min(1, existingInteraction.intensity + 0.1); } else { // Start new interaction this.activeInteractions.set(region.id, { region, duration: 0, intensity: 0.1, type: 'fixation', startTime: gaze.timestamp, endTime: gaze.timestamp }); } } } handleSaccade(from, to) { // Handle rapid eye movement between regions const fromRegion = this.findRegionAt(from.x, from.y); const toRegion = this.findRegionAt(to.x, to.y); if (fromRegion && toRegion && fromRegion.id !== toRegion.id) { // End interaction with previous region const fromInteraction = this.activeInteractions.get(fromRegion.id); if (fromInteraction) { fromInteraction.endTime = from.timestamp; fromInteraction.type = 'saccade'; } // Start interaction with new region this.activeInteractions.set(toRegion.id, { region: toRegion, duration: 0, intensity: 0.1, type: 'saccade', startTime: to.timestamp, endTime: to.timestamp }); } } checkRegionInteractions(gaze) { const currentRegion = this.findRegionAt(gaze.x, gaze.y); // End interactions for regions that are no longer being gazed at this.activeInteractions.forEach((interaction, regionId) => { if (!currentRegion || regionId !== currentRegion.id) { const timeSinceEnd = gaze.timestamp - interaction.endTime; if (timeSinceEnd > 500) { // 500ms timeout this.activeInteractions.delete(regionId); } } }); } findRegionAt(x, y) { for (const region of this.regions.values()) { if (x >= region.x && x <= region.x + region.width && y >= region.y && y <= region.y + region.height) { return region; } } return null; } registerRegion(region) { this.regions.set(region.id, region); } unregisterRegion(regionId) { this.regions.delete(regionId); this.activeInteractions.delete(regionId); } getActiveInteractions() { return Array.from(this.activeInteractions.values()); } getRegionInteraction(regionId) { return this.activeInteractions.get(regionId) || null; } async startCalibration() { return this.webgazer.startCalibration(); } async calibratePoint(x, y) { return this.webgazer.calibratePoint(x, y); } finishCalibration() { this.webgazer.finishCalibration(); } cleanup() { this.webgazer.cleanup(); this.activeInteractions.clear(); this.regions.clear(); this.gazeHistory = []; this.isTracking = false; } } // React context for eye tracking const EyeTrackingContext = /*#__PURE__*/createContext({ engine: null, isInitialized: false, isCalibrating: false, activeInteractions: [], startCalibration: async () => {}, finishCalibration: () => {} }); // Provider component function GlassEyeTrackingProvider({ children, autoInitialize = false, onGazeInteraction }) { useReducedMotion(); const engineRef = useRef(); const [isInitialized, setIsInitialized] = useState(false); const [isCalibrating, setIsCalibrating] = useState(false); const [activeInteractions, setActiveInteractions] = useState([]); // Initialize engine useEffect(() => { engineRef.current = new EyeTrackingEngine(); if (autoInitialize) { engineRef.current.initialize().then(success => { setIsInitialized(success); if (!success) { console.warn('Eye tracking initialization failed. Using fallback mode.'); } }); } return () => { if (engineRef.current) { engineRef.current.cleanup(); } }; }, [autoInitialize]); // Update active interactions useEffect(() => { if (!engineRef.current || !isInitialized) return; const interval = setInterval(() => { const interactions = engineRef.current.getActiveInteractions(); setActiveInteractions(interactions); // Trigger callback for new interactions interactions.forEach(interaction => onGazeInteraction?.(interaction)); }, 100); return () => clearInterval(interval); }, [isInitialized, onGazeInteraction]); const startCalibration = useCallback(async () => { if (!engineRef.current) return; setIsCalibrating(true); await engineRef.current.startCalibration(); }, []); const finishCalibration = useCallback(() => { if (!engineRef.current) return; engineRef.current.finishCalibration(); setIsCalibrating(false); }, []); const value = { engine: engineRef.current || null, isInitialized, isCalibrating, activeInteractions, startCalibration, finishCalibration }; return jsx(EyeTrackingContext.Provider, { value: value, children: children }); } // Hook to use eye tracking function useEyeTracking() { const context = useContext(EyeTrackingContext); if (!context) { throw new Error('useEyeTracking must be used within GlassEyeTrackingProvider'); } return context; } // Calibration component function GlassEyeTrackingCalibration({ onComplete, className }) { const prefersReducedMotion = useReducedMotion(); const { startCalibration, finishCalibration, isCalibrating } = useEyeTracking(); const [currentPoint, setCurrentPoint] = useState(0); const [isCalibrationActive, setIsCalibrationActive] = useState(false); const calibrationPoints = [{ x: 10, y: 10 }, { x: 50, y: 10 }, { x: 90, y: 10 }, { x: 10, y: 50 }, { x: 50, y: 50 }, { x: 90, y: 50 }, { x: 10, y: 90 }, { x: 50, y: 90 }, { x: 90, y: 90 }]; const handleStartCalibration = async () => { await startCalibration(); setIsCalibrationActive(true); setCurrentPoint(0); }; const handlePointClick = async (point, index) => { if (!isCalibrationActive) return; // Convert percentage to screen coordinates const screenX = point.x / 100 * window.innerWidth; const screenY = point.y / 100 * window.innerHeight; // Simulate calibration point click const engine = useEyeTracking().engine; if (engine) { await engine.calibratePoint(screenX, screenY); } // Move to next point if (index < calibrationPoints.length - 1) { setCurrentPoint(index + 1); } else { // Calibration complete finishCalibration(); setIsCalibrationActive(false); onComplete?.(); } }; if (!isCalibrating && !isCalibrationActive) { return jsx("div", { className: cn("glass-flex glass-flex-col glass-items-center glass-gap-4", className), children: jsxs("div", { className: cn("glass-text-center"), children: [jsx("h3", { className: cn("glass-text-lg glass-font-medium glass-text-primary glass-mb-2"), children: "Eye Tracking Calibration" }), jsx("p", { className: cn("glass-text-sm glass-text-secondary glass-mb-4"), children: "Look at each dot and click to calibrate your gaze tracking" }), jsx(motion.button, { className: cn("glass-px-6 glass-py-3 glass-surface-primary glass-elev-2 glass-radius-lg", "glass-text-primary font-medium transition-all duration-300", "hover:glass-elev-3 focus:outline-none focus:ring-2 focus:ring-blue-500"), onClick: handleStartCalibration, whileHover: { scale: 1.02 }, whileTap: { scale: 0.98 }, children: "Start Calibration" })] }) }); } if (isCalibrationActive) { return jsx("div", { className: cn("fixed inset-0 z-50 glass-surface-primary", className), children: jsx("div", { className: cn("glass-absolute glass-inset-0"), children: jsxs("div", { className: cn("glass-relative glass-w-full glass-h-full"), children: [jsx("div", { className: cn("glass-absolute glass-top-4 glass-left-1-2 glass-translate-x-1/2-neg"), children: jsx("div", { className: cn("glass-surface-secondary glass-radius-lg glass-px-4 glass-py-2"), children: jsxs("span", { className: cn("glass-text-sm glass-text-primary"), children: ["Point ", currentPoint + 1, " of ", calibrationPoints.length] }) }) }), jsxs("div", { className: cn("glass-absolute glass-top-20 glass-left-1-2 glass-translate-x-1/2-neg glass-text-center"), children: [jsx("p", { className: cn("glass-text-lg glass-text-primary glass-mb-2"), children: "Look at the blue dot and click it" }), jsx("p", { className: cn("glass-text-sm glass-text-secondary"), children: "Keep your head still and follow the dot with your eyes" })] }), calibrationPoints.map((point, index) => jsx(motion.div, { className: cn("glass-absolute glass-w-4 glass-h-4 glass-translate-x-1/2-neg glass-translate-y-1/2-neg glass-cursor-pointer glass-focus"), ref: el => { if (el) { el.style.left = `${point.x}%`; el.style.top = `${point.y}%`; } }, initial: { scale: 0, opacity: 0 }, animate: { scale: index === currentPoint ? [1, 1.2, 1] : index < currentPoint ? 1 : 0, opacity: index <= currentPoint ? 1 : 0.3 }, transition: prefersReducedMotion ? { duration: 0 } : { duration: index === currentPoint ? 0.5 : 0.2, repeat: index === currentPoint ? Infinity : 0 }, onClick: () => handlePointClick(point, index), children: jsx("div", { className: cn("glass-w-full glass-h-full glass-radius-full", index === currentPoint ? "glass-surface-accent glass-ring-4 glass-ring-accent/30" : index < currentPoint ? "glass-surface-success" : "glass-surface-muted") }) }, index))] }) }) }); } return null; } // Gaze-responsive component wrapper function GlassGazeResponsive({ children, className, regionId, onGazeEnter, onGazeLeave, onGazeIntensityChange, glassIntensity = true, glassRadius = true, glassBlur = true }) { const prefersReducedMotion = useReducedMotion(); const { engine, activeInteractions } = useEyeTracking(); const elementRef = useRef(null); const [isGazed, setIsGazed] = useState(false); const [gazeIntensity, setGazeIntensity] = useState(0); // Register region with eye tracking engine useEffect(() => { if (!engine || !elementRef.current) return; const element = elementRef.current; const rect = element.getBoundingClientRect(); const region = { id: regionId, x: rect.left, y: rect.top, width: rect.width, height: rect.height, element }; engine.registerRegion(region); // Update region on resize/scroll const updateRegion = () => { const newRect = element.getBoundingClientRect(); engine.registerRegion({ ...region, x: newRect.left, y: newRect.top, width: newRect.width, height: newRect.height }); }; window.addEventListener('resize', updateRegion); window.addEventListener('scroll', updateRegion); return () => { engine.unregisterRegion(regionId); window.removeEventListener('resize', updateRegion); window.removeEventListener('scroll', updateRegion); }; }, [engine, regionId]); // Track gaze interactions useEffect(() => { const interaction = activeInteractions.find(i => i.region.id === regionId); if (interaction && !isGazed) { setIsGazed(true); onGazeEnter?.(interaction); } else if (!interaction && isGazed) { setIsGazed(false); setGazeIntensity(0); onGazeLeave?.(); } if (interaction) { setGazeIntensity(interaction.intensity); onGazeIntensityChange?.(interaction.intensity); } }, [activeInteractions, regionId, isGazed, onGazeEnter, onGazeLeave, onGazeIntensityChange]); return jsxs(motion.div, { ref: elementRef, className: cn("transition-all duration-300", glassIntensity && isGazed && "OptimizedGlassCore intensity={0.2} glassBlur={6}", className), animate: { scale: isGazed ? 1 + gazeIntensity * 0.02 : 1, ...(glassBlur && { // Use createGlassStyle() instead, }), ...(glassRadius && { borderRadius: `${8 + gazeIntensity * 4}px` }) }, transition: prefersReducedMotion ? { duration: 0 } : { duration: 0.3 }, children: [children, jsx(AnimatePresence, { children: isGazed && jsx(motion.div, { className: cn("glass-absolute glass-inset-0 glass-pointer-events-none glass-radius-inherit"), initial: { opacity: 0 }, animate: prefersReducedMotion ? {} : { opacity: 0.8 }, exit: { opacity: 0 }, children: jsx("div", { className: cn("glass-absolute glass-inset-0 glass-radius-inherit"), style: { boxShadow: `inset 0 0 ${20 + gazeIntensity * 30}px var(--glass-color-primary, ${0.2 + gazeIntensity * 0.3})` } }) }) })] }); } // Gaze visualization overlay function GlassGazeVisualization({ show = false, className }) { const { activeInteractions } = useEyeTracking(); if (!show) return null; return jsx("div", { className: cn("glass-fixed glass-inset-0 glass-pointer-events-none glass-z-40", className), children: jsx(AnimatePresence, { children: activeInteractions.map(interaction => jsxs(motion.div, { className: "glass-absolute", ref: el => { if (!el) return; const r = interaction.region; el.style.left = typeof r.x === 'number' ? `${r.x}px` : r.x; el.style.top = typeof r.y === 'number' ? `${r.y}px` : r.y; el.style.width = typeof r.width === 'number' ? `${r.width}px` : r.width; el.style.height = typeof r.height === 'number' ? `${r.height}px` : r.height; }, initial: { opacity: 0, scale: 0.9 }, animate: { opacity: 0.6, scale: 1 }, exit: { opacity: 0, scale: 0.9 }, children: [jsx("div", { className: cn("glass-w-full glass-h-full glass-border-2 glass-radius-md"), ref: el => { if (!el) return; const a = interaction.intensity; el.style.borderColor = `var(--glass-color-primary,${a})`; el.style.backgroundColor = `var(--glass-color-primary,${a * 0.1})`; } }), jsxs("div", { className: cn("glass-absolute glass--top-2 glass-left-0 glass-text-xs glass-font-mono glass-text-primary"), children: [interaction.type, " (", (interaction.intensity * 100).toFixed(0), "%)"] })] }, interaction.region.id)) }) }); } // Presets for different eye tracking modes const eyeTrackingPresets = { subtle: { glassIntensity: 0.5, responsiveness: 0.3, visualFeedback: false }, standard: { glassIntensity: 1.0, responsiveness: 0.6, visualFeedback: true }, dramatic: { glassIntensity: 1.5, responsiveness: 1.0, visualFeedback: true }, accessibility: { glassIntensity: 0.8, responsiveness: 0.4, visualFeedback: true, highContrast: true } }; function EyeTrackingSummaryCard() { const { isInitialized, isCalibrating, activeInteractions } = useEyeTracking(); return jsxs("div", { className: cn("glass-surface-primary glass-radius-2xl glass-p-6 glass-space-y-4", "glass-border glass-border-white/10 glass-shadow-soft-lg"), "data-testid": "glass-eye-tracking-summary", children: [jsxs("div", { children: [jsx("p", { className: "glass-text-xs glass-text-tertiary uppercase tracking-wide", children: "Eye tracking" }), jsx("h2", { className: "glass-text-2xl glass-text-primary font-semibold", children: isInitialized ? "Active" : "Initializing" }), jsx("p", { className: "glass-text-sm glass-text-secondary", children: isCalibrating ? "Calibration in progress" : "Capturing gaze interactions" })] }), jsxs("div", { className: "glass-grid glass-grid-cols-2 glass-gap-3", children: [jsxs("div", { className: "glass-surface-subtle glass-radius-xl glass-p-4", children: [jsx("p", { className: "glass-text-xs glass-text-tertiary mb-1", children: "Interactions" }), jsx("p", { className: "glass-text-lg glass-text-primary font-semibold", children: activeInteractions.length })] }), jsxs("div", { className: "glass-surface-subtle glass-radius-xl glass-p-4", children: [jsx("p", { className: "glass-text-xs glass-text-tertiary mb-1", children: "Status" }), jsx("p", { className: "glass-text-lg glass-text-primary font-semibold", children: isCalibrating ? "Calibrating" : "Tracking" })] })] })] }); } const GlassEyeTracking = ({ autoInitialize = false, showCalibration = true, showVisualization = true, className, children, onGazeInteraction, ...rest }) => jsx(GlassEyeTrackingProvider, { autoInitialize: autoInitialize, onGazeInteraction: onGazeInteraction, children: jsxs("div", { className: cn("glass-eye-tracking glass-relative glass-space-y-4", className), ...rest, children: [children ?? jsx(EyeTrackingSummaryCard, {}), showCalibration && jsx(GlassEyeTrackingCalibration, {}), showVisualization && jsx(GlassGazeVisualization, { show: true })] }) }); export { GlassEyeTracking, GlassEyeTrackingCalibration, GlassEyeTrackingProvider, GlassGazeResponsive, GlassGazeVisualization, GlassEyeTracking as default, eyeTrackingPresets, useEyeTracking }; //# sourceMappingURL=GlassEyeTracking.js.map