aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
507 lines (504 loc) • 18.5 kB
JavaScript
'use client';
import { jsx, jsxs } from 'react/jsx-runtime';
import { forwardRef, useState, useRef, useEffect, useCallback } from 'react';
import '../../primitives/GlassCore.js';
import '../../primitives/glass/GlassAdvanced.js';
import { OptimizedGlassCore } from '../../primitives/OptimizedGlassCore.js';
import '../../primitives/glass/OptimizedGlassAdvanced.js';
import '../../primitives/MotionNative.js';
import { MotionFramer } from '../../primitives/motion/MotionFramer.js';
import { cn } from '../../lib/utilsComprehensive.js';
import { useA11yId } from '../../utils/a11y.js';
import { useMotionPreference } from '../../hooks/useMotionPreference.js';
const GlassARPreview = /*#__PURE__*/forwardRef(({
objects,
markers = [],
camera = {
position: {
x: 0,
y: 0,
z: 5
},
target: {
x: 0,
y: 0,
z: 0
},
fov: 75,
near: 0.1,
far: 1000
},
lighting = {
ambient: {
color: "var(--glass-white)",
intensity: 0.6
},
directional: {
color: "var(--glass-white)",
intensity: 0.8,
position: {
x: 1,
y: 1,
z: 1
},
castShadow: true
}
},
background = "transparent",
backgroundColor = "var(--glass-black)",
environmentMap,
enableCamera = true,
debug = false,
sessionActive = false,
xrMode = "inline",
quality = "medium",
onObjectSelect,
onObjectMove,
onObjectRotate,
onObjectScale,
onMarkerFound,
onMarkerLost,
onSessionStart,
onSessionEnd,
onError,
renderObject,
loadingComponent,
errorComponent,
controlsOverlay,
respectMotionPreference = true,
className,
...props
}, ref) => {
const {
shouldAnimate
} = useMotionPreference();
const arPreviewId = useA11yId("glass-ar-preview");
const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false);
const [selectedObject, setSelectedObject] = useState(null);
const [cameraStream, setCameraStream] = useState(null);
const [foundMarkers, setFoundMarkers] = useState(new Set());
const [xrSupported, setXrSupported] = useState(false);
const [isTracking, setIsTracking] = useState(false);
useRef(null);
const videoRef = useRef(null);
const canvasRef = useRef(null);
const rendererRef = useRef(null);
const sceneRef = useRef(null);
const animationFrameRef = useRef();
// Check WebXR support
useEffect(() => {
if ("xr" in navigator) {
navigator.xr?.isSessionSupported?.(xrMode).then(supported => {
setXrSupported(supported);
}).catch(() => {
setXrSupported(false);
});
}
}, [xrMode]);
// Initialize camera stream
useEffect(() => {
if (enableCamera && background === "camera") {
navigator.mediaDevices?.getUserMedia({
video: {
facingMode: "environment",
width: {
ideal: 1280
},
height: {
ideal: 720
}
}
}).then(stream => {
setCameraStream(stream);
if (videoRef.current) {
videoRef.current.srcObject = stream;
videoRef.current.play();
}
}).catch(error => {
console.warn("Camera access denied:", error);
onError?.(new Error("Camera access required for AR preview"));
});
return () => {
if (cameraStream) {
cameraStream.getTracks().forEach(track => track.stop());
}
};
}
}, [enableCamera, background, cameraStream, onError]);
// Initialize AR renderer (simplified simulation)
useEffect(() => {
const initializeRenderer = () => {
setIsLoading(true);
try {
// In a real implementation, this would initialize Three.js or WebGL
// For this simulation, we'll use canvas 2D rendering
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
canvas.width = canvas.offsetWidth * window.devicePixelRatio;
canvas.height = canvas.offsetHeight * window.devicePixelRatio;
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
rendererRef.current = ctx;
sceneRef.current = {
objects: [],
markers: []
};
setIsLoading(false);
} catch (error) {
setHasError(true);
setIsLoading(false);
onError?.(error);
}
};
initializeRenderer();
}, [onError]);
// Render AR scene
const renderScene = useCallback(() => {
const ctx = rendererRef.current;
const canvas = canvasRef.current;
if (!ctx || !canvas) return;
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw background
if (background === "color") {
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
} else if (background === "camera" && videoRef.current) {
ctx.drawImage(videoRef.current, 0, 0, canvas.width, canvas.height);
}
// Render objects (simplified 3D to 2D projection)
objects.forEach(object => {
if (!object.visible) return;
const screenPos = project3DToScreen(object.position, camera, canvas.width, canvas.height);
if (renderObject) ; else {
renderDefaultObject(ctx, object, screenPos);
}
});
// Render markers
markers.forEach(marker => {
if (foundMarkers.has(marker.id)) {
renderMarker(ctx, marker);
}
});
// Render debug info
if (debug) {
renderDebugInfo(ctx);
}
}, [objects, markers, camera, background, backgroundColor, foundMarkers, debug, renderObject]);
// Simplified 3D to 2D projection
const project3DToScreen = useCallback((position, camera, width, height) => {
// Simplified perspective projection
const distance = Math.sqrt(Math.pow(position.x - camera.position.x, 2) + Math.pow(position.y - camera.position.y, 2) + Math.pow(position.z - camera.position.z, 2));
const scale = 100 / (distance || 1);
const x = width / 2 + (position.x - camera.position.x) * scale;
const y = height / 2 - (position.y - camera.position.y) * scale;
return {
x,
y,
scale,
distance
};
}, []);
// Render default object representation
const renderDefaultObject = useCallback((ctx, object, screenPos) => {
ctx.save();
ctx.globalAlpha = object.opacity;
const size = 40 * screenPos.scale * object.scale.x;
const isSelected = selectedObject === object.id;
// Draw object shadow
ctx.beginPath();
ctx.arc(screenPos.x + 2, screenPos.y + size / 2 + 2, size / 2, 0, 2 * Math.PI);
ctx.fillStyle = "rgba(var(--glass-color-black) / var(--glass-opacity-20))";
ctx.fill();
// Draw object
ctx.beginPath();
ctx.arc(screenPos.x, screenPos.y, size / 2, 0, 2 * Math.PI);
ctx.fillStyle = object.material?.color || "var(--glass-color-primary)";
ctx.fill();
// Draw selection ring
if (isSelected) {
ctx.strokeStyle = "var(--glass-color-success)";
ctx.lineWidth = 3;
ctx.stroke();
}
// Draw object label
ctx.fillStyle = "var(--glass-white)";
ctx.font = "12px sans-serif";
ctx.textAlign = "center";
ctx.fillText(object.name, screenPos.x, screenPos.y + size / 2 + 20);
// Animation effects
if (object.animation?.enabled) {
const time = Date.now() * 0.001 * object.animation.speed;
switch (object.animation.type) {
case "pulse":
const pulseScale = 1 + Math.sin(time) * 0.2;
ctx.scale(pulseScale, pulseScale);
break;
case "rotate":
ctx.rotate(time);
break;
}
}
ctx.restore();
}, [selectedObject]);
// Render AR marker
const renderMarker = useCallback((ctx, marker) => {
// Simplified marker representation
const x = Math.random() * ctx.canvas.width;
const y = Math.random() * ctx.canvas.height;
const size = marker.size || 50;
ctx.save();
ctx.strokeStyle = "var(--glass-color-success)";
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
// Draw marker outline
ctx.strokeRect(x - size / 2, y - size / 2, size, size);
// Draw marker type indicator
ctx.fillStyle = "var(--glass-color-success)";
ctx.font = "10px sans-serif";
ctx.textAlign = "center";
ctx.fillText(marker.type.toUpperCase(), x, y + size / 2 + 15);
ctx.restore();
}, []);
// Render debug information
const renderDebugInfo = useCallback(ctx => {
ctx.save();
ctx.fillStyle = "var(--glass-text-secondary-dark)";
ctx.fillRect(10, 10, 200, 120);
ctx.fillStyle = "var(--glass-white)";
ctx.font = "11px monospace";
ctx.textAlign = "left";
let y = 30;
ctx.fillText(`Objects: ${objects.length}`, 15, y);
y += 15;
ctx.fillText(`Markers: ${foundMarkers.size}/${markers.length}`, 15, y);
y += 15;
ctx.fillText(`Tracking: ${isTracking ? "Active" : "Inactive"}`, 15, y);
y += 15;
ctx.fillText(`XR Support: ${xrSupported ? "Yes" : "No"}`, 15, y);
y += 15;
ctx.fillText(`Quality: ${quality}`, 15, y);
y += 15;
ctx.fillText(`Mode: ${xrMode}`, 15, y);
ctx.restore();
}, [objects.length, foundMarkers.size, markers.length, isTracking, xrSupported, quality, xrMode]);
// Animation loop
useEffect(() => {
const animate = () => {
renderScene();
animationFrameRef.current = requestAnimationFrame(animate);
};
if (!isLoading && !hasError) {
animate();
}
return () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, [isLoading, hasError, renderScene]);
// Handle canvas interactions
const handleCanvasClick = useCallback(event => {
const canvas = canvasRef.current;
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
// Check if click intersects with any object
for (const object of objects) {
if (!object.visible) continue;
const screenPos = project3DToScreen(object.position, camera, canvas.width, canvas.height);
const distance = Math.sqrt(Math.pow(x - screenPos.x, 2) + Math.pow(y - screenPos.y, 2));
const objectSize = 40 * screenPos.scale * object.scale.x;
if (distance <= objectSize / 2) {
setSelectedObject(object.id);
onObjectSelect?.(object);
return;
}
}
// Click on empty space deselects
setSelectedObject(null);
}, [objects, camera, project3DToScreen, onObjectSelect]);
// Start AR session
const startARSession = useCallback(async () => {
try {
if (xrSupported && xrMode === "immersive-ar") {
// In a real implementation, this would start WebXR session
setIsTracking(true);
onSessionStart?.();
} else {
// Fallback to inline mode
setIsTracking(true);
onSessionStart?.();
}
} catch (error) {
onError?.(error);
}
}, [xrSupported, xrMode, onSessionStart, onError]);
// End AR session
const endARSession = useCallback(() => {
setIsTracking(false);
onSessionEnd?.();
}, [onSessionEnd]);
// Simulate marker detection
useEffect(() => {
if (isTracking && markers.length > 0) {
const interval = setInterval(() => {
const randomMarker = markers[Math.floor(Math.random() * markers.length)];
const wasFound = foundMarkers.has(randomMarker.id);
if (Math.random() > 0.7) {
if (!wasFound) {
setFoundMarkers(prev => new Set([...prev, randomMarker.id]));
onMarkerFound?.(randomMarker);
}
} else if (wasFound && Math.random() > 0.8) {
setFoundMarkers(prev => {
const newSet = new Set(prev);
newSet.delete(randomMarker.id);
return newSet;
});
onMarkerLost?.(randomMarker);
}
}, 2000);
return () => clearInterval(interval);
}
}, [isTracking, markers, foundMarkers, onMarkerFound, onMarkerLost]);
return jsx(OptimizedGlassCore, {
"data-glass-component": true,
ref: ref,
id: arPreviewId,
elevation: "level1",
intensity: "medium",
depth: 1,
tint: "neutral",
border: "subtle",
className: cn("glass-ar-preview relative glass-radius-lg glass-backdrop-blur-md border border-border/20 overflow-hidden", className),
...props,
children: jsxs(MotionFramer, {
preset: shouldAnimate && respectMotionPreference ? "fadeIn" : "none",
className: 'relative glass-w-full glass-h-full',
children: [background === "camera" && enableCamera && jsx("video", {
ref: videoRef,
className: 'absolute inset-0 glass-w-full glass-h-full object-cover',
autoPlay: true,
muted: true,
playsInline: true
}), jsx("canvas", {
ref: canvasRef,
className: 'absolute inset-0 glass-w-full glass-h-full cursor-pointer',
onClick: handleCanvasClick
}), isLoading && jsx("div", {
className: 'absolute inset-0 glass-flex glass-items-center glass-justify-center',
children: loadingComponent || jsxs("div", {
className: "glass-flex glass-flex-col glass-items-center glass-gap-4",
children: [jsx("div", {
className: 'w-12 h-12 glass-border-4 glass-border-primary glass-border-t-transparent glass-radius-full animate-spin'
}), jsx("div", {
className: "glass-text-sm glass-text-secondary",
children: "Initializing AR..."
})]
})
}), hasError && jsx("div", {
className: 'absolute inset-0 glass-flex glass-items-center glass-justify-center',
children: errorComponent || jsxs("div", {
className: 'glass-flex glass-flex-col glass-items-center glass-gap-4 text-center',
children: [jsx("div", {
className: "glass-text-4xl",
children: "\uD83D\uDD34"
}), jsx("div", {
className: "glass-text-sm glass-text-secondary",
children: "AR initialization failed"
})]
})
}), controlsOverlay && jsx("div", {
className: 'absolute inset-0 pointer-events-none',
children: controlsOverlay
}), !isLoading && !hasError && jsx("div", {
className: 'absolute bottom-4 left-1/2 transform -translate-x-1/2',
children: jsxs(OptimizedGlassCore, {
elevation: "level3",
intensity: "strong",
depth: 2,
tint: "neutral",
border: "subtle",
className: "glass-flex glass-items-center glass-gap-2 glass-px-4 glass-py-2 glass-radius-lg glass-glass-backdrop-blur-md glass-border glass-border-glass-border/20 glass-contrast-guard",
children: [jsx("button", {
onClick: isTracking ? endARSession : startARSession,
className: cn("glass-px-4 glass-py-2 glass-radius-md transition-all glass-text-sm font-medium", isTracking ? "bg-red-500/20 text-red-400 hover:bg-red-500/30" : "bg-primary/20 text-primary hover:bg-primary/30"),
children: isTracking ? "Stop AR" : "Start AR"
}), xrSupported && jsxs("div", {
className: 'glass-flex glass-items-center glass-gap-1 glass-text-xs text-primary',
children: [jsx("div", {
className: 'w-2 h-2 glass-surface-green glass-radius-full'
}), jsx("span", {
children: "XR Ready"
})]
}), jsxs("div", {
className: "glass-text-xs glass-text-secondary",
children: ["Objects: ", objects.filter(o => o.visible).length]
})]
})
}), selectedObject && jsx(MotionFramer, {
preset: shouldAnimate && respectMotionPreference ? "slideUp" : "none",
children: jsx(OptimizedGlassCore, {
elevation: "level2",
intensity: "medium",
depth: 1,
tint: "neutral",
border: "subtle",
className: 'absolute top-4 right-4 glass-p-4 glass-radius-lg glass-glass-backdrop-blur-md glass-border glass-border-glass-border/20 max-w-xs glass-contrast-guard',
children: (() => {
const object = objects.find(o => o.id === selectedObject);
if (!object) return null;
return jsxs("div", {
className: "glass-gap-2",
children: [jsx("h3", {
className: 'font-semibold text-primary',
children: object.name
}), jsxs("div", {
className: "glass-text-xs glass-gap-1 glass-text-secondary",
children: [jsxs("div", {
children: ["Position: (", object.position.x.toFixed(1), ",", " ", object.position.y.toFixed(1), ",", " ", object.position.z.toFixed(1), ")"]
}), jsxs("div", {
children: ["Scale: ", object.scale.x.toFixed(2)]
}), jsxs("div", {
children: ["Opacity: ", (object.opacity * 100).toFixed(0), "%"]
}), object.animation?.enabled && jsxs("div", {
children: ["Animation: ", object.animation.type]
})]
}), jsx("button", {
onClick: () => setSelectedObject(null),
className: 'glass-text-xs glass-text-secondary hover:text-primary transition-colors',
children: "Close \u00D7"
})]
});
})()
})
}), !isLoading && !hasError && jsx("div", {
className: 'absolute top-4 left-4',
children: jsx(OptimizedGlassCore, {
elevation: "level2",
intensity: "medium",
depth: 1,
tint: "neutral",
border: "subtle",
className: "glass-px-3 glass-py-2 glass-radius-lg glass-glass-backdrop-blur-md glass-border glass-border-glass-border/20 glass-contrast-guard",
children: jsxs("div", {
className: "glass-flex glass-items-center glass-gap-2 glass-text-xs",
children: [jsx("div", {
className: cn("w-2 h-2 glass-radius-full", isTracking ? "bg-green-500 animate-pulse" : "bg-gray-500")
}), jsx("span", {
className: "glass-text-secondary",
children: isTracking ? "Tracking Active" : "Tracking Inactive"
})]
})
})
})]
})
});
});
GlassARPreview.displayName = "GlassARPreview";
export { GlassARPreview, GlassARPreview as default };
//# sourceMappingURL=GlassARPreview.js.map