UNPKG

aura-glass

Version:

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

507 lines (504 loc) 18.5 kB
'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