aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
743 lines (740 loc) • 23.6 kB
JavaScript
'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