From ec36032d47bbf872d54f595aaea5a5ee8b5d5aef Mon Sep 17 00:00:00 2001 From: Nicholas Date: Tue, 4 Aug 2026 11:49:46 -0400 Subject: [PATCH] Add interactive Omi wearable device lab --- .gitignore | 2 + PRODUCT.md | 23 + app/device-lab/page.tsx | 11 + components/device-lab/DeviceControls.tsx | 135 ++++ components/device-lab/DeviceLab.tsx | 831 ++++++++++++++++++++ components/device-lab/device-lab.module.css | 306 +++++++ components/device-lab/device-settings.ts | 166 ++++ components/device-lab/unified-shell.ts | 203 +++++ package-lock.json | 694 +++++++++++++++- package.json | 9 +- 10 files changed, 2376 insertions(+), 4 deletions(-) create mode 100644 PRODUCT.md create mode 100644 app/device-lab/page.tsx create mode 100644 components/device-lab/DeviceControls.tsx create mode 100644 components/device-lab/DeviceLab.tsx create mode 100644 components/device-lab/device-lab.module.css create mode 100644 components/device-lab/device-settings.ts create mode 100644 components/device-lab/unified-shell.ts diff --git a/.gitignore b/.gitignore index 144a169..eb07d49 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,8 @@ next-env.d.ts # Playwright MCP cache .playwright-mcp/ +.playwright-cli/ +/output/playwright/ # Local scripts /scripts/ diff --git a/PRODUCT.md b/PRODUCT.md new file mode 100644 index 0000000..813dabd --- /dev/null +++ b/PRODUCT.md @@ -0,0 +1,23 @@ +# Product + +## Register + +brand + +## Users + +People evaluating Omi as a personal AI companion and wearable computing device. They need to understand what the product is, trust its presence in their life, and feel that the hardware is desirable enough to wear every day. + +## Product Purpose + +Present Omi as an intimate, proactive AI that can accompany its owner across conversations and computer work. The marketing surface should make the product feel tangible, capable, and considered rather than abstract software. + +## Brand Personality + +Premium, intimate, futuristic. The voice should feel confident and precise, with moments of cinematic wonder grounded by believable product detail. + +## Design Principles + +- Make the product tangible before explaining it. +- Use cinematic restraint with carefully controlled light and purposeful motion. +- Preserve physical credibility through accurate proportions, materials, depth, and attachment details. diff --git a/app/device-lab/page.tsx b/app/device-lab/page.tsx new file mode 100644 index 0000000..cd55f68 --- /dev/null +++ b/app/device-lab/page.tsx @@ -0,0 +1,11 @@ +import type { Metadata } from "next"; +import DeviceLab from "@/components/device-lab/DeviceLab"; + +export const metadata: Metadata = { + title: "Omi Wearable — 3D Study", + description: "A standalone interactive material and lighting study of the Omi wearable.", +}; + +export default function DeviceLabPage() { + return ; +} diff --git a/components/device-lab/DeviceControls.tsx b/components/device-lab/DeviceControls.tsx new file mode 100644 index 0000000..2f3437c --- /dev/null +++ b/components/device-lab/DeviceControls.tsx @@ -0,0 +1,135 @@ +"use client"; + +import { useEffect, useRef } from "react"; +import GUI from "lil-gui"; +import type { + LabSettings, + PlasticSurfaceSettings, + SurfaceSettings, +} from "./device-settings"; +import styles from "./device-lab.module.css"; + +function addSurfaceFolder( + gui: GUI, + title: string, + surface: SurfaceSettings | PlasticSurfaceSettings, +) { + const folder = gui.addFolder(title); + if (title !== "Unified plastic shell") folder.close(); + folder.addColor(surface, "color").name("color"); + if ("transmission" in surface) { + folder.add(surface, "transmission", 0, 1, 0.01).name("transmission"); + folder.add(surface, "thickness", 0, 2, 0.01).name("thickness"); + folder.add(surface, "ior", 1, 2.333, 0.001).name("IOR"); + folder.addColor(surface, "attenuationColor").name("absorption color"); + folder + .add(surface, "attenuationDistance", 0.05, 10, 0.05) + .name("absorption distance"); + + const noise = folder.addFolder("Noise texture"); + noise.add(surface, "noiseEnabled").name("enabled"); + noise.add(surface, "noiseScale", 0.25, 256, 0.25).name("scale / repeat"); + noise.add(surface, "noiseContrast", 0, 8, 0.01).name("contrast"); + noise.add(surface, "noiseSeed", 0, 10000, 1).name("seed"); + noise.add(surface, "bumpScale", -1, 1, 0.001).name("bump strength"); + } + folder.add(surface, "metalness", 0, 1, 0.01).name("metalness"); + folder.add(surface, "roughness", 0, 1, 0.01).name("roughness"); + folder.add(surface, "clearcoat", 0, 1, 0.01).name("clearcoat"); + folder + .add(surface, "clearcoatRoughness", 0, 1, 0.01) + .name("coat roughness"); + if (!("transmission" in surface)) { + folder.add(surface, "bumpScale", 0, 0.1, 0.001).name("bump scale"); + } + folder + .add(surface, "envMapIntensity", 0, 4, 0.01) + .name("environment"); +} + +interface DeviceControlsProps { + settings: LabSettings; + onChange: () => void; +} + +export default function DeviceControls({ settings, onChange }: DeviceControlsProps) { + const mount = useRef(null); + + useEffect(() => { + if (!mount.current) return; + + const gui = new GUI({ + container: mount.current, + title: "Omi material lab", + width: 320, + }); + + const texture = gui.addFolder("Texture").close(); + texture.add(settings.texture, "enabled").name("grain enabled"); + texture.add(settings.texture, "repeat", 1, 24, 1).name("grain repeat"); + texture.add(settings.texture, "contrast", 0, 1, 0.01).name("grain contrast"); + + const cord = gui.addFolder("Cord").close(); + cord.addColor(settings.cord, "color").name("color"); + cord.add(settings.cord, "metalness", 0, 1, 0.01).name("metalness"); + cord.add(settings.cord, "roughness", 0, 1, 0.01).name("roughness"); + cord.add(settings.cord, "bumpScale", 0, 0.1, 0.001).name("bump scale"); + + addSurfaceFolder(gui, "Unified plastic shell", settings.shell); + addSurfaceFolder(gui, "Body seam", settings.seam); + addSurfaceFolder(gui, "Gunmetal bezel", settings.bezel); + addSurfaceFolder(gui, "Gunmetal core", settings.core); + + const led = gui.addFolder("LED").close(); + led.addColor(settings.led, "outerColor").name("outer glow"); + led.add(settings.led, "outerOpacity", 0, 1, 0.01).name("outer opacity"); + led.addColor(settings.led, "innerColor").name("inner glow"); + led.add(settings.led, "innerOpacity", 0, 1, 0.01).name("inner opacity"); + led.addColor(settings.led, "coreColor").name("core"); + led.addColor(settings.led, "lightColor").name("light"); + led.add(settings.led, "lightIntensity", 0, 8, 0.05).name("intensity"); + + const stage = gui.addFolder("Stage lighting").close(); + stage.addColor(settings.stage, "background").name("background"); + stage.addColor(settings.stage, "fogColor").name("fog"); + stage.addColor(settings.stage, "ambientColor").name("ambient color"); + stage.add(settings.stage, "ambientIntensity", 0, 2, 0.01).name("ambient intensity"); + stage.addColor(settings.stage, "hemisphereSky").name("sky color"); + stage.addColor(settings.stage, "hemisphereGround").name("ground color"); + stage.add(settings.stage, "hemisphereIntensity", 0, 3, 0.01).name("hemisphere"); + stage.addColor(settings.stage, "keyColor").name("key color"); + stage.add(settings.stage, "keyIntensity", 0, 250, 1).name("key intensity"); + stage.addColor(settings.stage, "fillColor").name("fill color"); + stage.add(settings.stage, "fillIntensity", 0, 250, 1).name("fill intensity"); + stage.addColor(settings.stage, "rimColor").name("rim color"); + stage.add(settings.stage, "rimIntensity", 0, 100, 1).name("rim intensity"); + stage.add(settings.stage, "exposure", 0.2, 2.5, 0.01).name("exposure"); + + const environment = gui.addFolder("Environment").close(); + environment.addColor(settings.environment, "keyColor").name("key color"); + environment.add(settings.environment, "keyIntensity", 0, 12, 0.1).name("key intensity"); + environment.addColor(settings.environment, "sideColor").name("side color"); + environment.add(settings.environment, "sideIntensity", 0, 12, 0.1).name("side intensity"); + environment.addColor(settings.environment, "ringColor").name("ring color"); + environment.add(settings.environment, "ringIntensity", 0, 12, 0.1).name("ring intensity"); + + const actions = { + copySettings: async () => { + await navigator.clipboard.writeText(JSON.stringify(settings, null, 2)); + }, + resetAll: () => { + gui.reset(true); + onChange(); + }, + }; + gui.add(actions, "copySettings").name("Copy settings JSON"); + gui.add(actions, "resetAll").name("Reset all"); + gui.onChange(onChange); + + gui.close(); + + return () => gui.destroy(); + }, [onChange, settings]); + + return
; +} diff --git a/components/device-lab/DeviceLab.tsx b/components/device-lab/DeviceLab.tsx new file mode 100644 index 0000000..ab21cce --- /dev/null +++ b/components/device-lab/DeviceLab.tsx @@ -0,0 +1,831 @@ +"use client"; + +import { + Suspense, + type MutableRefObject, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { + Canvas, + type ThreeEvent, + useFrame, + useThree, +} from "@react-three/fiber"; +import { + Environment, + Lightformer, + OrbitControls, + PerspectiveCamera, + Sparkles, +} from "@react-three/drei"; +import { Bloom, EffectComposer } from "@react-three/postprocessing"; +import { useReducedMotion } from "framer-motion"; +import * as THREE from "three"; +import DeviceControls from "./DeviceControls"; +import { createDefaultLabSettings, type LabSettings } from "./device-settings"; +import styles from "./device-lab.module.css"; +import { createUnifiedShellGeometry } from "./unified-shell"; + +const GUNMETAL_CORE_PROFILE = [ + new THREE.Vector2(0, -0.18), + new THREE.Vector2(0.75, -0.18), + new THREE.Vector2(0.81, -0.15), + new THREE.Vector2(0.83, -0.09), + new THREE.Vector2(0.83, 0.26), + new THREE.Vector2(0.81, 0.35), + new THREE.Vector2(0.74, 0.42), + new THREE.Vector2(0.6, 0.5), + new THREE.Vector2(0.35, 0.54), + new THREE.Vector2(0, 0.55), +]; + +function createGrainTexture() { + const size = 128; + const data = new Uint8Array(size * size * 4); + const texture = new THREE.DataTexture(data, size, size, THREE.RGBAFormat); + texture.wrapS = THREE.RepeatWrapping; + texture.wrapT = THREE.RepeatWrapping; + return texture; +} + +function updateGrainTexture( + texture: THREE.DataTexture, + repeat: number, + contrast: number, + initialSeed = 0x5f3759df, +) { + const data = texture.image.data as Uint8Array; + let seed = (initialSeed ^ 0x5f3759df) >>> 0; + for (let index = 0; index < data.length / 4; index += 1) { + seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0; + const noise = (seed >>> 24) / 255 - 0.5; + const value = THREE.MathUtils.clamp(128 + noise * 255 * contrast, 0, 255); + const offset = index * 4; + data[offset] = value; + data[offset + 1] = value; + data[offset + 2] = value; + data[offset + 3] = 255; + } + texture.repeat.set(repeat, repeat); + texture.needsUpdate = true; +} + +function Cord({ + attachment, + angularVelocity, + grainTexture, + impactImpulse, + pendulumAngle, + reduceMotion, + settings, +}: { + attachment: MutableRefObject; + angularVelocity: MutableRefObject; + grainTexture: THREE.DataTexture; + impactImpulse: MutableRefObject; + pendulumAngle: MutableRefObject; + reduceMotion: boolean; + settings: LabSettings; +}) { + const rope = useMemo(() => { + const guide = new THREE.CatmullRomCurve3( + [ + new THREE.Vector3(-5.1, 7.35, -0.18), + new THREE.Vector3(-3.55, 5.45, -0.12), + new THREE.Vector3(-1.82, 2.75, -0.05), + new THREE.Vector3(-0.58, 1.34, -0.015), + new THREE.Vector3(0, 0.78, 0), + new THREE.Vector3(0.58, 1.34, -0.015), + new THREE.Vector3(1.82, 2.75, -0.05), + new THREE.Vector3(3.55, 5.45, -0.12), + new THREE.Vector3(5.1, 7.35, -0.18), + ], + false, + "catmullrom", + 0.42, + ); + const particleCount = 41; + const particles = Array.from({ length: particleCount }, (_, index) => ( + guide.getPointAt(index / (particleCount - 1)) + )); + const previousParticles = particles.map((particle) => particle.clone()); + const restLengths = new Float32Array(particleCount - 1); + const inverseMasses = new Float32Array(particleCount); + + for (let index = 0; index < particleCount - 1; index += 1) { + restLengths[index] = particles[index].distanceTo(particles[index + 1]); + } + for (let index = 0; index < particleCount; index += 1) { + const progress = index / (particleCount - 1); + const pendantLoad = Math.exp(-Math.pow((progress - 0.5) / 0.062, 2)); + inverseMasses[index] = 1 / (1 + pendantLoad * 18); + } + inverseMasses[0] = 0; + inverseMasses[particleCount - 1] = 0; + + const curve = new THREE.CatmullRomCurve3( + particles, + false, + "catmullrom", + 0.46, + ); + const geometry = new THREE.TubeGeometry(curve, 160, 0.038, 10, false); + return { + curve, + geometry, + inverseMasses, + particles, + previousParticles, + restLengths, + }; + }, []); + const meshUpdateElapsed = useRef(0); + + useFrame((state, delta) => { + if (reduceMotion) { + const center = rope.particles[Math.floor(rope.particles.length / 2)]; + attachment.current.set(center.x, center.y); + return; + } + + const impact = impactImpulse.current; + if (impact.lengthSq() > 0.000001) { + const centerIndex = Math.floor(rope.particles.length / 2); + for (let offset = -4; offset <= 4; offset += 1) { + const particleIndex = centerIndex + offset; + const influence = Math.exp(-Math.pow(offset / 2.15, 2)); + rope.previousParticles[particleIndex].x -= impact.x * influence; + rope.previousParticles[particleIndex].y -= impact.y * influence; + } + impact.set(0, 0); + } + + let remainingTime = Math.min(delta, 0.05); + while (remainingTime > 0) { + const step = Math.min(remainingTime, 1 / 60); + const stepSquared = step * step; + const particleCount = rope.particles.length; + + for (let index = 1; index < particleCount - 1; index += 1) { + const particle = rope.particles[index]; + const previous = rope.previousParticles[index]; + const progress = index / (particleCount - 1); + const pendantLoad = Math.exp(-Math.pow((progress - 0.5) / 0.062, 2)); + const freeSpan = Math.sin(Math.PI * progress); + const velocityX = (particle.x - previous.x) * 0.987; + const velocityY = (particle.y - previous.y) * 0.987; + const velocityZ = (particle.z - previous.z) * 0.982; + const currentX = particle.x; + const currentY = particle.y; + const currentZ = particle.z; + const pendantReaction = ( + Math.sin(pendulumAngle.current) * 3.4 + + angularVelocity.current * 0.42 + ) * pendantLoad; + const airCurrent = Math.sin( + state.clock.elapsedTime * 0.58 + index * 0.29, + ) * freeSpan * 0.045; + + particle.set( + particle.x + velocityX + (pendantReaction + airCurrent) * stepSquared, + particle.y + velocityY - (1.25 + pendantLoad * 28) * stepSquared, + particle.z + velocityZ + airCurrent * 0.22 * stepSquared, + ); + previous.set(currentX, currentY, currentZ); + } + + for (let iteration = 0; iteration < 12; iteration += 1) { + for (let index = 0; index < particleCount - 1; index += 1) { + const first = rope.particles[index]; + const second = rope.particles[index + 1]; + const deltaX = second.x - first.x; + const deltaY = second.y - first.y; + const deltaZ = second.z - first.z; + const distance = Math.hypot(deltaX, deltaY, deltaZ) || 1; + const error = (distance - rope.restLengths[index]) / distance; + const firstWeight = rope.inverseMasses[index]; + const secondWeight = rope.inverseMasses[index + 1]; + const weightTotal = firstWeight + secondWeight; + if (weightTotal === 0) continue; + + const firstCorrection = error * (firstWeight / weightTotal); + const secondCorrection = error * (secondWeight / weightTotal); + first.x += deltaX * firstCorrection; + first.y += deltaY * firstCorrection; + first.z += deltaZ * firstCorrection; + second.x -= deltaX * secondCorrection; + second.y -= deltaY * secondCorrection; + second.z -= deltaZ * secondCorrection; + } + } + remainingTime -= step; + } + + const center = rope.particles[Math.floor(rope.particles.length / 2)]; + attachment.current.set(center.x, center.y); + meshUpdateElapsed.current += delta; + if (meshUpdateElapsed.current < 1 / 30) return; + meshUpdateElapsed.current = 0; + + rope.curve.updateArcLengths(); + const updatedGeometry = new THREE.TubeGeometry( + rope.curve, + 160, + 0.038, + 10, + false, + ); + const positions = rope.geometry.getAttribute("position") as THREE.BufferAttribute; + const normals = rope.geometry.getAttribute("normal") as THREE.BufferAttribute; + (positions.array as Float32Array).set( + updatedGeometry.getAttribute("position").array, + ); + (normals.array as Float32Array).set( + updatedGeometry.getAttribute("normal").array, + ); + positions.needsUpdate = true; + normals.needsUpdate = true; + updatedGeometry.dispose(); + }); + + useEffect(() => () => rope.geometry.dispose(), [rope.geometry]); + + return ( + + + + ); +} + +interface DeviceProps { + reduceMotion: boolean; + settings: LabSettings; + position?: [number, number, number]; + onWakeChange?: (awake: boolean) => void; +} + +function Device({ + reduceMotion, + settings, + position = [0, -0.16, 0], + onWakeChange, +}: DeviceProps) { + const pivot = useRef(null); + const outerGlow = useRef(null); + const innerGlow = useRef(null); + const ledCore = useRef(null); + const ledLight = useRef(null); + const isHovered = useRef(false); + const pendulumAngle = useRef(0); + const angularVelocity = useRef(0); + const cordAttachment = useRef(new THREE.Vector2(0, 1.15)); + const cordImpactImpulse = useRef(new THREE.Vector2()); + const glowLevel = useRef(0); + const grainTexture = useMemo(createGrainTexture, []); + const shellGrainTexture = useMemo(createGrainTexture, []); + const shellGeometry = useMemo(createUnifiedShellGeometry, []); + const outerGlowColor = useMemo( + () => new THREE.Color(settings.led.outerColor), + [settings.led.outerColor], + ); + const innerGlowColor = useMemo( + () => new THREE.Color(settings.led.innerColor), + [settings.led.innerColor], + ); + const ledCoreColor = useMemo( + () => new THREE.Color(settings.led.coreColor), + [settings.led.coreColor], + ); + + useEffect(() => { + updateGrainTexture(grainTexture, settings.texture.repeat, settings.texture.contrast); + }, [grainTexture, settings.texture.contrast, settings.texture.repeat]); + + useEffect(() => { + updateGrainTexture( + shellGrainTexture, + settings.shell.noiseScale, + settings.shell.noiseContrast, + settings.shell.noiseSeed, + ); + }, [ + settings.shell.noiseContrast, + settings.shell.noiseScale, + settings.shell.noiseSeed, + shellGrainTexture, + ]); + + useEffect( + () => () => { + shellGeometry.dispose(); + grainTexture.dispose(); + shellGrainTexture.dispose(); + }, + [grainTexture, shellGeometry, shellGrainTexture], + ); + + const bumpMap = settings.texture.enabled ? grainTexture : undefined; + const shellBumpMap = settings.shell.noiseEnabled ? shellGrainTexture : undefined; + + const applyPointerImpact = useCallback(( + event: ThreeEvent, + initialImpact: boolean, + ) => { + if (reduceMotion) return; + + const movementX = event.nativeEvent.movementX; + const movementY = event.nativeEvent.movementY; + let impactX = THREE.MathUtils.clamp(movementX / 48, -1, 1); + let impactY = THREE.MathUtils.clamp(-movementY / 48, -1, 1); + + if (Math.hypot(impactX, impactY) < 0.06 && event.uv) { + impactX = 0.5 - event.uv.x; + impactY = 0.5 - event.uv.y; + const fallbackLength = Math.hypot(impactX, impactY) || 1; + impactX /= fallbackLength; + impactY /= fallbackLength; + } + + const angularStrength = initialImpact ? 0.78 : 0.16; + angularVelocity.current = THREE.MathUtils.clamp( + angularVelocity.current + impactX * angularStrength, + -1.45, + 1.45, + ); + + const cordStrength = initialImpact ? 0.052 : 0.012; + cordImpactImpulse.current.x = THREE.MathUtils.clamp( + cordImpactImpulse.current.x + impactX * cordStrength, + -0.11, + 0.11, + ); + cordImpactImpulse.current.y = THREE.MathUtils.clamp( + cordImpactImpulse.current.y + impactY * cordStrength, + -0.11, + 0.11, + ); + }, [reduceMotion]); + + const wakePendant = useCallback((event: ThreeEvent) => { + event.stopPropagation(); + if (isHovered.current) return; + isHovered.current = true; + applyPointerImpact(event, true); + onWakeChange?.(true); + document.documentElement.style.cursor = "pointer"; + }, [applyPointerImpact, onWakeChange]); + + const movePendant = useCallback((event: ThreeEvent) => { + event.stopPropagation(); + if (!isHovered.current) return; + if (Math.hypot( + event.nativeEvent.movementX, + event.nativeEvent.movementY, + ) < 1.5) return; + applyPointerImpact(event, false); + }, [applyPointerImpact]); + + const restPendant = useCallback((event: ThreeEvent) => { + event.stopPropagation(); + isHovered.current = false; + onWakeChange?.(false); + document.documentElement.style.cursor = "default"; + }, [onWakeChange]); + + useEffect(() => () => { + document.documentElement.style.cursor = "default"; + }, []); + + useFrame((state, delta) => { + if (!pivot.current) return; + + const step = Math.min(delta, 1 / 30); + if (reduceMotion) { + pendulumAngle.current = 0; + angularVelocity.current = 0; + } else { + const acceleration = -10.5 * Math.sin(pendulumAngle.current) + - 2.35 * angularVelocity.current; + angularVelocity.current += acceleration * step; + pendulumAngle.current = THREE.MathUtils.clamp( + pendulumAngle.current + angularVelocity.current * step, + -0.15, + 0.15, + ); + } + + const rotationEase = 1 - Math.exp(-delta * 3.5); + pivot.current.position.x = cordAttachment.current.x; + pivot.current.position.y = cordAttachment.current.y; + pivot.current.rotation.z = pendulumAngle.current; + pivot.current.rotation.y = THREE.MathUtils.lerp( + pivot.current.rotation.y, + isHovered.current ? state.pointer.x * 0.055 : 0, + rotationEase, + ); + pivot.current.rotation.x = THREE.MathUtils.lerp( + pivot.current.rotation.x, + isHovered.current ? -state.pointer.y * 0.018 : 0, + rotationEase, + ); + + const lightTarget = isHovered.current ? 1 : 0; + const lightEase = reduceMotion + ? 1 + : 1 - Math.exp(-delta * (lightTarget > glowLevel.current ? 5.5 : 3.2)); + glowLevel.current = THREE.MathUtils.lerp( + glowLevel.current, + lightTarget, + lightEase, + ); + + if (outerGlow.current) { + outerGlow.current.opacity = settings.led.outerOpacity * glowLevel.current * 2.8; + outerGlow.current.color + .copy(outerGlowColor) + .multiplyScalar(0.25 + glowLevel.current * 3.2); + } + if (innerGlow.current) { + innerGlow.current.opacity = settings.led.innerOpacity * glowLevel.current * 2.3; + innerGlow.current.color + .copy(innerGlowColor) + .multiplyScalar(0.25 + glowLevel.current * 4.5); + } + if (ledCore.current) { + ledCore.current.color + .copy(ledCoreColor) + .multiplyScalar(0.015 + glowLevel.current * 5.5); + } + if (ledLight.current) { + ledLight.current.intensity = settings.led.lightIntensity * glowLevel.current * 4.5; + } + }); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +function Atmosphere({ reduceMotion, positionX }: { + reduceMotion: boolean; + positionX: number; +}) { + const drift = useRef(null); + + useFrame((state, delta) => { + if (!drift.current || reduceMotion) return; + drift.current.rotation.y += delta * 0.012; + drift.current.position.y = Math.sin(state.clock.elapsedTime * 0.12) * 0.08; + }); + + return ( + + + + + ); +} + +function RendererSettings({ exposure }: { exposure: number }) { + const { gl } = useThree(); + useEffect(() => { + gl.toneMappingExposure = exposure; + }, [exposure, gl]); + return null; +} + +function Stage({ + reduceMotion, + settings, + onWakeChange, +}: DeviceProps) { + const { size } = useThree(); + const isNarrow = size.width < 760; + const pendantX = isNarrow ? 0 : 1.08; + const pendantY = isNarrow ? -0.62 : -0.14; + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +export default function DeviceLab() { + const reduceMotion = useReducedMotion() ?? false; + const settings = useMemo(createDefaultLabSettings, []); + const [, setVersion] = useState(0); + const [isAwake, setIsAwake] = useState(false); + const handleSettingsChange = useCallback(() => { + setVersion((version) => version + 1); + }, []); + + return ( +
+
+
+
+ ); +} diff --git a/components/device-lab/device-lab.module.css b/components/device-lab/device-lab.module.css new file mode 100644 index 0000000..9decf2d --- /dev/null +++ b/components/device-lab/device-lab.module.css @@ -0,0 +1,306 @@ +.lab { + min-height: 100svh; + background: #121212; + color: #f6f7fa; +} + +.productSection { + position: relative; + isolation: isolate; + min-height: max(720px, 100svh); + overflow: hidden; + background: + radial-gradient(circle at 73% 48%, rgba(81, 106, 170, 0.13), transparent 30%), + radial-gradient(circle at 58% 82%, rgba(122, 74, 126, 0.09), transparent 34%), + #121212; +} + +.productSection::after { + position: absolute; + z-index: 3; + inset: 0; + background: linear-gradient( + 90deg, + rgba(18, 18, 18, 0.94) 0%, + rgba(18, 18, 18, 0.7) 29%, + rgba(18, 18, 18, 0.04) 58%, + rgba(18, 18, 18, 0.12) 100% + ); + content: ""; + pointer-events: none; +} + +.canvas { + position: absolute !important; + z-index: 2; + inset: 0; + width: 100% !important; + height: 100% !important; +} + +.halo { + position: absolute; + z-index: 1; + top: 3%; + left: 72%; + width: min(66vw, 920px); + aspect-ratio: 1; + transform: translateX(-50%); + border-radius: 50%; + background: radial-gradient( + circle, + rgba(89, 116, 185, 0.14) 0%, + rgba(92, 73, 125, 0.07) 38%, + transparent 68% + ); + filter: blur(12px); + pointer-events: none; +} + +.haze { + position: absolute; + z-index: 1; + right: -12%; + bottom: -38%; + width: 82vw; + height: 72vw; + border-radius: 50%; + background: radial-gradient(circle, rgba(177, 136, 180, 0.09), transparent 63%); + filter: blur(46px); + pointer-events: none; +} + +.copy { + position: absolute; + z-index: 8; + top: 46%; + left: clamp(32px, 7vw, 112px); + width: min(39vw, 540px); + transform: translateY(-50%); + pointer-events: none; +} + +.productName { + margin: 0 0 22px; + color: #aeb7c8; + font-size: 14px; + font-weight: 600; + letter-spacing: 0.04em; +} + +.heading { + max-width: 9ch; + margin: 0; + color: #f7f8fa; + font-family: "Montserrat", -apple-system, BlinkMacSystemFont, sans-serif; + font-size: clamp(3.5rem, 6.35vw, 6rem); + font-weight: 600; + letter-spacing: -0.04em; + line-height: 0.94; + text-wrap: balance; +} + +.heading span { + display: block; + color: #c9ced9; +} + +.description { + max-width: 38ch; + margin: 30px 0 0; + color: #a6adba; + font-size: clamp(1rem, 1.35vw, 1.18rem); + font-weight: 400; + line-height: 1.55; + text-wrap: pretty; +} + +.wakeCue { + display: inline-flex; + align-items: center; + gap: 10px; + margin-top: 32px; + color: #8f98a8; + font-size: 13px; + font-weight: 600; + transition: color 220ms ease-out; +} + +.wakeDot { + width: 7px; + height: 7px; + border-radius: 50%; + background: #52617a; + box-shadow: 0 0 0 5px rgba(82, 97, 122, 0.12); + transition: + background-color 220ms ease-out, + box-shadow 220ms ease-out; +} + +.wakeCueActive { + color: #dbe7ff; +} + +.wakeCueActive .wakeDot { + background: #8db2ff; + box-shadow: + 0 0 0 5px rgba(77, 117, 203, 0.14), + 0 0 18px rgba(90, 140, 255, 0.76); +} + +.guiMount { + position: absolute; + z-index: 20; + top: 18px; + right: 18px; + max-height: calc(100svh - 36px); + overflow: auto; + border-radius: 10px; + opacity: 0.72; + transition: opacity 180ms ease-out; +} + +.guiMount:hover, +.guiMount:focus-within { + opacity: 1; +} + +.guiMount :global(.lil-gui) { + --background-color: #11151b; + --widget-color: #303743; + --hover-color: #3a4351; + --focus-color: #465267; + --text-color: #f1f3f7; + --number-color: #8fb4ff; + --string-color: #d8e2f0; + --font-family: inherit; + --font-size: 13px; + --input-font-size: 13px; + border: 1px solid #252d38; +} + +.identity, +.instructions { + position: absolute; + z-index: 9; + bottom: 28px; + display: flex; + align-items: center; + gap: 14px; + color: #707887; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.13em; + text-transform: uppercase; + pointer-events: none; +} + +.identity { + left: clamp(32px, 7vw, 112px); +} + +.instructions { + right: 52px; + gap: 24px; +} + +.mark { + color: #fff; + font-size: 17px; + letter-spacing: -0.03em; + text-transform: lowercase; +} + +.divider { + width: 32px; + height: 1px; + background: #38404c; +} + +@media (max-width: 960px) { + .copy { + left: 40px; + width: min(44vw, 440px); + } + + .identity { + left: 40px; + } + + .instructions { + right: 32px; + } +} + +@media (max-width: 760px) { + .productSection { + min-height: max(760px, 100svh); + } + + .productSection::after { + background: linear-gradient( + 180deg, + rgba(18, 18, 18, 0.82) 0%, + rgba(18, 18, 18, 0.45) 33%, + transparent 58%, + rgba(18, 18, 18, 0.18) 100% + ); + } + + .copy { + top: 64px; + right: 24px; + left: 24px; + width: auto; + transform: none; + } + + .productName { + margin-bottom: 14px; + font-size: 13px; + } + + .heading { + max-width: 10ch; + font-size: clamp(2.75rem, 13vw, 4.1rem); + line-height: 0.96; + } + + .description { + max-width: 32ch; + margin-top: 18px; + font-size: 15px; + } + + .wakeCue { + margin-top: 18px; + } + + .halo { + top: 30%; + left: 50%; + width: 125vw; + } + + .guiMount { + top: 10px; + right: 10px; + } + + .identity { + bottom: 20px; + left: 24px; + } + + .instructions { + display: none; + } +} + +@media (prefers-reduced-motion: reduce) { + .wakeCue, + .wakeDot, + .guiMount { + transition: none; + } +} diff --git a/components/device-lab/device-settings.ts b/components/device-lab/device-settings.ts new file mode 100644 index 0000000..e01c3d2 --- /dev/null +++ b/components/device-lab/device-settings.ts @@ -0,0 +1,166 @@ +export interface SurfaceSettings { + color: string; + metalness: number; + roughness: number; + clearcoat: number; + clearcoatRoughness: number; + bumpScale: number; + envMapIntensity: number; +} + +export interface PlasticSurfaceSettings extends SurfaceSettings { + transmission: number; + thickness: number; + ior: number; + attenuationColor: string; + attenuationDistance: number; + noiseEnabled: boolean; + noiseScale: number; + noiseContrast: number; + noiseSeed: number; +} + +export interface LabSettings { + texture: { + enabled: boolean; + repeat: number; + contrast: number; + }; + cord: { + color: string; + metalness: number; + roughness: number; + bumpScale: number; + }; + shell: PlasticSurfaceSettings; + seam: SurfaceSettings; + bezel: SurfaceSettings; + core: SurfaceSettings; + led: { + outerColor: string; + outerOpacity: number; + innerColor: string; + innerOpacity: number; + coreColor: string; + lightColor: string; + lightIntensity: number; + }; + stage: { + background: string; + fogColor: string; + ambientColor: string; + ambientIntensity: number; + hemisphereSky: string; + hemisphereGround: string; + hemisphereIntensity: number; + keyColor: string; + keyIntensity: number; + fillColor: string; + fillIntensity: number; + rimColor: string; + rimIntensity: number; + exposure: number; + }; + environment: { + keyColor: string; + keyIntensity: number; + sideColor: string; + sideIntensity: number; + ringColor: string; + ringIntensity: number; + }; +} + +export function createDefaultLabSettings(): LabSettings { + return { + texture: { + enabled: true, + repeat: 8, + contrast: 0.68, + }, + cord: { + color: "#373a3e", + metalness: 0.16, + roughness: 0.58, + bumpScale: 0, + }, + shell: { + color: "#ededed", + metalness: 0, + roughness: 0.45, + clearcoat: 0.71, + clearcoatRoughness: 0.59, + bumpScale: 0.108, + envMapIntensity: 0.84, + transmission: 0.94, + thickness: 1.35, + ior: 1.556, + attenuationColor: "#8fa3bd", + attenuationDistance: 2.65, + noiseEnabled: true, + noiseScale: 3.25, + noiseContrast: 3.45, + noiseSeed: 4491, + }, + seam: { + color: "#ededed", + metalness: 1, + roughness: 0.37, + clearcoat: 0.28, + clearcoatRoughness: 0.16, + bumpScale: 0.056, + envMapIntensity: 1.04, + }, + bezel: { + color: "#ededed", + metalness: 1, + roughness: 0.37, + clearcoat: 0.28, + clearcoatRoughness: 0.16, + bumpScale: 0.056, + envMapIntensity: 1.04, + }, + core: { + color: "#ededed", + metalness: 1, + roughness: 0.37, + clearcoat: 0.28, + clearcoatRoughness: 0.16, + bumpScale: 0.056, + envMapIntensity: 1.04, + }, + led: { + outerColor: "#326eff", + outerOpacity: 0.09, + innerColor: "#4f83ff", + innerOpacity: 0.24, + coreColor: "#e3edff", + lightColor: "#2f6fff", + lightIntensity: 1.35, + }, + stage: { + background: "#121212", + fogColor: "#4d4d4d", + ambientColor: "#ffffff", + ambientIntensity: 0.96, + hemisphereSky: "#edf3ff", + hemisphereGround: "#11141a", + hemisphereIntensity: 0, + keyColor: "#a8c6ff", + keyIntensity: 115, + fillColor: "#ffffff", + fillIntensity: 88, + rimColor: "#214aaf", + rimIntensity: 22, + exposure: 0.8, + }, + environment: { + keyColor: "#e8f1ff", + keyIntensity: 8.2, + sideColor: "#b9c7dd", + sideIntensity: 0.6, + ringColor: "#b188b4", + ringIntensity: 10.8, + }, + }; +} diff --git a/components/device-lab/unified-shell.ts b/components/device-lab/unified-shell.ts new file mode 100644 index 0000000..17323b6 --- /dev/null +++ b/components/device-lab/unified-shell.ts @@ -0,0 +1,203 @@ +import * as THREE from "three"; +import { MarchingCubes } from "three/examples/jsm/objects/MarchingCubes.js"; +import { mergeVertices } from "three/examples/jsm/utils/BufferGeometryUtils.js"; + +const BODY_PROFILE: ReadonlyArray = [ + [0, -0.34], [0.48, -0.34], [0.76, -0.3], [0.94, -0.2], + [1.01, -0.07], [1.02, 0.04], [0.98, 0.17], [0.85, 0.3], + [0.57, 0.39], [0, 0.41], +]; + +const BAIL_BLEND_PROFILE: ReadonlyArray = [ + [0, 0.72], [0.12, 0.73], [0.22, 0.78], [0.27, 0.86], + [0.24, 0.94], [0.18, 1], [0.13, 1.06], [0.095, 1.12], [0, 1.13], +]; + +const GRID_RESOLUTION = 112; +const MAX_POLYGON_COUNT = 160_000; +const BOUNDS_MIN = new THREE.Vector3(-1.2, -1.2, -0.5); +const BOUNDS_MAX = new THREE.Vector3(1.2, 1.54, 0.64); + +function pointSegmentDistance( + px: number, + py: number, + ax: number, + ay: number, + bx: number, + by: number, +) { + const abx = bx - ax; + const aby = by - ay; + const apx = px - ax; + const apy = py - ay; + const denominator = abx * abx + aby * aby; + const t = denominator === 0 + ? 0 + : THREE.MathUtils.clamp((apx * abx + apy * aby) / denominator, 0, 1); + return Math.hypot(px - (ax + abx * t), py - (ay + aby * t)); +} + +function revolvedProfileSdf( + radial: number, + axial: number, + profile: ReadonlyArray, +) { + let inside = false; + let distance = Number.POSITIVE_INFINITY; + + for ( + let index = 0, previous = profile.length - 1; + index < profile.length; + previous = index, index += 1 + ) { + const [ax, ay] = profile[previous]; + const [bx, by] = profile[index]; + distance = Math.min( + distance, + pointSegmentDistance(radial, axial, ax, ay, bx, by), + ); + + if ((ay > axial) !== (by > axial)) { + const intersection = ax + ((ax - bx) * (axial - ay)) / (ay - by); + if (radial < intersection) inside = !inside; + } + } + + return inside ? -distance : distance; +} + +function ellipsoidSdf( + x: number, + y: number, + z: number, + radiusX: number, + radiusY: number, + radiusZ: number, +) { + const normalizedLength = Math.hypot(x / radiusX, y / radiusY, z / radiusZ); + const gradientLength = Math.hypot( + x / (radiusX * radiusX), + y / (radiusY * radiusY), + z / (radiusZ * radiusZ), + ); + if (gradientLength < 1e-6) return -Math.min(radiusX, radiusY, radiusZ); + return (normalizedLength * (normalizedLength - 1)) / gradientLength; +} + +function bailTorusSdf(x: number, y: number, z: number) { + const translatedX = x; + const translatedY = y - 1.08; + const translatedZ = z - 0.015; + const localX = -translatedZ / 0.78; + const localY = translatedY / 1.32; + const localZ = translatedX / 0.82; + const ringDistance = Math.hypot(localX, localY) - 0.16; + return (Math.hypot(ringDistance, localZ) - 0.105) * 0.78; +} + +function smoothUnion(first: number, second: number, radius: number) { + const blend = THREE.MathUtils.clamp( + 0.5 + (0.5 * (second - first)) / radius, + 0, + 1, + ); + return THREE.MathUtils.lerp(second, first, blend) + - radius * blend * (1 - blend); +} + +function roundedCylinderSdf( + x: number, + y: number, + z: number, + radius: number, + halfDepth: number, + cornerRadius: number, + centerZ: number, +) { + const radialDistance = Math.hypot(x, y) - (radius - cornerRadius); + const axialDistance = Math.abs(z - centerZ) - (halfDepth - cornerRadius); + return Math.min(Math.max(radialDistance, axialDistance), 0) + + Math.hypot(Math.max(radialDistance, 0), Math.max(axialDistance, 0)) + - cornerRadius; +} + +function unifiedShellSdf(x: number, y: number, z: number) { + const body = revolvedProfileSdf(Math.hypot(x, y), z, BODY_PROFILE); + const outerFace = ellipsoidSdf(x, y, z - 0.29, 0.91, 0.91, 0.17); + const blend = revolvedProfileSdf(Math.hypot(x, z), y, BAIL_BLEND_PROFILE); + const bail = bailTorusSdf(x, y, z); + + let distance = smoothUnion(body, outerFace, 0.035); + distance = smoothUnion(distance, blend, 0.08); + distance = smoothUnion(distance, bail, 0.055); + + // Front-opening bore: the rear stop at z=-0.21 leaves a thin plastic back wall. + const cavity = roundedCylinderSdf(x, y, z, 0.855, 0.465, 0.04, 0.255); + return Math.max(distance, -cavity); +} + +function addPlanarUvs(geometry: THREE.BufferGeometry) { + const positions = geometry.getAttribute("position"); + const uvs = new Float32Array(positions.count * 2); + const width = BOUNDS_MAX.x - BOUNDS_MIN.x; + const height = BOUNDS_MAX.y - BOUNDS_MIN.y; + + for (let index = 0; index < positions.count; index += 1) { + uvs[index * 2] = (positions.getX(index) - BOUNDS_MIN.x) / width; + uvs[index * 2 + 1] = (positions.getY(index) - BOUNDS_MIN.y) / height; + } + geometry.setAttribute("uv", new THREE.BufferAttribute(uvs, 2)); +} + +export function createUnifiedShellGeometry() { + const placeholderMaterial = new THREE.MeshBasicMaterial(); + const marchingCubes = new MarchingCubes( + GRID_RESOLUTION, + placeholderMaterial, + false, + false, + MAX_POLYGON_COUNT, + ); + marchingCubes.isolation = 0; + + const center = BOUNDS_MIN.clone().add(BOUNDS_MAX).multiplyScalar(0.5); + const halfExtent = BOUNDS_MAX.clone().sub(BOUNDS_MIN).multiplyScalar(0.5); + const halfResolution = GRID_RESOLUTION / 2; + + for (let z = 0; z < GRID_RESOLUTION; z += 1) { + const worldZ = center.z + ((z - halfResolution) / halfResolution) * halfExtent.z; + for (let y = 0; y < GRID_RESOLUTION; y += 1) { + const worldY = center.y + ((y - halfResolution) / halfResolution) * halfExtent.y; + for (let x = 0; x < GRID_RESOLUTION; x += 1) { + const worldX = center.x + ((x - halfResolution) / halfResolution) * halfExtent.x; + marchingCubes.setCell(x, y, z, -unifiedShellSdf(worldX, worldY, worldZ)); + } + } + } + + marchingCubes.update(); + const activePositions = marchingCubes.positionArray.slice(0, marchingCubes.count * 3); + const rawGeometry = new THREE.BufferGeometry(); + rawGeometry.setAttribute("position", new THREE.BufferAttribute(activePositions, 3)); + + const positions = rawGeometry.getAttribute("position"); + for (let index = 0; index < positions.count; index += 1) { + positions.setXYZ( + index, + center.x + positions.getX(index) * halfExtent.x, + center.y + positions.getY(index) * halfExtent.y, + center.z + positions.getZ(index) * halfExtent.z, + ); + } + + const geometry = mergeVertices(rawGeometry, 0.0005); + geometry.computeVertexNormals(); + addPlanarUvs(geometry); + geometry.computeBoundingBox(); + geometry.computeBoundingSphere(); + + rawGeometry.dispose(); + marchingCubes.geometry.dispose(); + placeholderMaterial.dispose(); + return geometry; +} diff --git a/package-lock.json b/package-lock.json index 3090241..94c03e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,19 @@ "version": "0.1.0", "dependencies": { "@lottiefiles/dotlottie-react": "^0.17.13", + "@react-three/drei": "^10.7.7", + "@react-three/fiber": "^9.7.0", + "@react-three/postprocessing": "^3.0.4", + "@types/three": "^0.180.0", "framer-motion": "^12.29.0", + "lil-gui": "^0.21.0", "mixpanel-browser": "^2.74.0", "next": "^15.1.4", "posthog-js": "^1.336.4", + "postprocessing": "^6.39.4", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "three": "^0.180.0" }, "devDependencies": { "@types/node": "^22.10.5", @@ -39,6 +46,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@dimforge/rapier3d-compat": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", + "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", + "license": "Apache-2.0" + }, "node_modules/@emnapi/runtime": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", @@ -572,6 +594,12 @@ "integrity": "sha512-1QwcDchh/TXTvP9zXUHA5oJOdhEdUrn6U7gAiQ1AXEUrcK6PTU6v5D3byP76BK56fQjoY2WQ2A4pYDCCvoVRqw==", "license": "MIT" }, + "node_modules/@mediapipe/tasks-vision": { + "version": "0.10.17", + "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz", + "integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==", + "license": "Apache-2.0" + }, "node_modules/@mixpanel/rrdom": { "version": "2.0.0-alpha.18.2", "resolved": "https://registry.npmjs.org/@mixpanel/rrdom/-/rrdom-2.0.0-alpha.18.2.tgz", @@ -628,6 +656,18 @@ "integrity": "sha512-OomKIB6GTx5xvCLJ7iic2khT/t/tnCJUex13aEqsbSqIT/UzUUsqf+LTrgUK5ex+f6odmkCNjre2y5jvpNqn+g==", "license": "MIT" }, + "node_modules/@monogrid/gainmap-js": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.4.0.tgz", + "integrity": "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==", + "license": "MIT", + "dependencies": { + "promise-worker-transferable": "^1.0.4" + }, + "peerDependencies": { + "three": ">= 0.159.0" + } + }, "node_modules/@next/env": { "version": "15.5.9", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.9.tgz", @@ -1125,6 +1165,120 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "license": "BSD-3-Clause" }, + "node_modules/@react-three/drei": { + "version": "10.7.7", + "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-10.7.7.tgz", + "integrity": "sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mediapipe/tasks-vision": "0.10.17", + "@monogrid/gainmap-js": "^3.0.6", + "@use-gesture/react": "^10.3.1", + "camera-controls": "^3.1.0", + "cross-env": "^7.0.3", + "detect-gpu": "^5.0.56", + "glsl-noise": "^0.0.0", + "hls.js": "^1.5.17", + "maath": "^0.10.8", + "meshline": "^3.3.1", + "stats-gl": "^2.2.8", + "stats.js": "^0.17.0", + "suspend-react": "^0.1.3", + "three-mesh-bvh": "^0.8.3", + "three-stdlib": "^2.35.6", + "troika-three-text": "^0.52.4", + "tunnel-rat": "^0.1.2", + "use-sync-external-store": "^1.4.0", + "utility-types": "^3.11.0", + "zustand": "^5.0.1" + }, + "peerDependencies": { + "@react-three/fiber": "^9.0.0", + "react": "^19", + "react-dom": "^19", + "three": ">=0.159" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/@react-three/fiber": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.7.0.tgz", + "integrity": "sha512-EWm9FwcaOZQu/ExFW5rggoCMM1NJet5YbxVxKaOE+KSncrjU0Wx7017qSyGFvupviK89nMYGCWU3BIK4dI1clw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.17.8", + "@types/webxr": "*", + "base64-js": "^1.5.1", + "buffer": "^6.0.3", + "its-fine": "^2.0.0", + "react-use-measure": "^2.1.7", + "scheduler": "^0.27.0", + "suspend-react": "^0.1.3", + "use-sync-external-store": "^1.4.0", + "zustand": "^5.0.3" + }, + "peerDependencies": { + "expo": ">=43.0", + "expo-asset": ">=8.4", + "expo-file-system": ">=11.0", + "expo-gl": ">=11.0", + "react": ">=19 <19.3", + "react-dom": ">=19 <19.3", + "react-native": ">=0.78", + "three": ">=0.156" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + }, + "expo-asset": { + "optional": true + }, + "expo-file-system": { + "optional": true + }, + "expo-gl": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@react-three/postprocessing": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@react-three/postprocessing/-/postprocessing-3.0.4.tgz", + "integrity": "sha512-e4+F5xtudDYvhxx3y0NtWXpZbwvQ0x1zdOXWTbXMK6fFLVDd4qucN90YaaStanZGS4Bd5siQm0lGL/5ogf8iDQ==", + "license": "MIT", + "dependencies": { + "maath": "^0.6.0", + "n8ao": "^1.9.4", + "postprocessing": "^6.36.6" + }, + "peerDependencies": { + "@react-three/fiber": "^9.0.0", + "react": "^19.0", + "three": ">= 0.156.0" + } + }, + "node_modules/@react-three/postprocessing/node_modules/maath": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/maath/-/maath-0.6.0.tgz", + "integrity": "sha512-dSb2xQuP7vDnaYqfoKzlApeRcR2xtN8/f7WV/TMAkBC8552TwTLtOO0JTcSygkYMjNDPoo6V01jTw/aPi4JrMw==", + "license": "MIT", + "peerDependencies": { + "@types/three": ">=0.144.0", + "three": ">=0.144.0" + } + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1134,12 +1288,24 @@ "tslib": "^2.8.0" } }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "license": "MIT" + }, "node_modules/@types/css-font-loading-module": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz", "integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==", "license": "MIT" }, + "node_modules/@types/draco3d": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz", + "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.19.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz", @@ -1149,11 +1315,16 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/offscreencanvas": { + "version": "2019.7.3", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", + "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.9", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz", "integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==", - "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -1169,6 +1340,42 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/react-reconciler": { + "version": "0.28.9", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", + "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.180.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.180.0.tgz", + "integrity": "sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg==", + "license": "MIT", + "dependencies": { + "@dimforge/rapier3d-compat": "~0.12.0", + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": "*", + "@webgpu/types": "*", + "fflate": "~0.8.2", + "meshoptimizer": "~0.22.0" + } + }, + "node_modules/@types/three/node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -1176,6 +1383,36 @@ "license": "MIT", "optional": true }, + "node_modules/@types/webxr": { + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", + "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", + "license": "MIT" + }, + "node_modules/@use-gesture/core": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz", + "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==", + "license": "MIT" + }, + "node_modules/@use-gesture/react": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz", + "integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==", + "license": "MIT", + "dependencies": { + "@use-gesture/core": "10.3.1" + }, + "peerDependencies": { + "react": ">= 16.8.0" + } + }, + "node_modules/@webgpu/types": { + "version": "0.1.71", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.71.tgz", + "integrity": "sha512-mMy8/ODcKhab808co15eW+yN+HgXoQxRQHTiBV9Mrvl1r0ufnid7YOcI+gi4eUWSWl9ezD6TW2KXccrL8HCh2A==", + "license": "BSD-3-Clause" + }, "node_modules/@xstate/fsm": { "version": "1.6.5", "resolved": "https://registry.npmjs.org/@xstate/fsm/-/fsm-1.6.5.tgz", @@ -1256,6 +1493,26 @@ "node": ">= 0.6.0" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.9.17", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.17.tgz", @@ -1266,6 +1523,15 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -1326,6 +1592,30 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -1336,6 +1626,19 @@ "node": ">= 6" } }, + "node_modules/camera-controls": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-3.1.2.tgz", + "integrity": "sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==", + "license": "MIT", + "engines": { + "node": ">=22.0.0", + "npm": ">=10.5.1" + }, + "peerDependencies": { + "three": ">=0.126.1" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001765", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz", @@ -1421,6 +1724,24 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1452,9 +1773,17 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT" }, + "node_modules/detect-gpu": { + "version": "5.0.70", + "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz", + "integrity": "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==", + "license": "MIT", + "dependencies": { + "webgl-constants": "^1.1.1" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1488,6 +1817,12 @@ "@types/trusted-types": "^2.0.7" } }, + "node_modules/draco3d": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz", + "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==", + "license": "Apache-2.0" + }, "node_modules/electron-to-chromium": { "version": "1.5.277", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.277.tgz", @@ -1643,6 +1978,12 @@ "node": ">=10.13.0" } }, + "node_modules/glsl-noise": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz", + "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==", + "license": "MIT" + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -1656,6 +1997,38 @@ "node": ">= 0.4" } }, + "node_modules/hls.js": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.16.tgz", + "integrity": "sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==", + "license": "Apache-2.0" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -1718,12 +2091,30 @@ "node": ">=0.12.0" } }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/its-fine": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz", + "integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==", + "license": "MIT", + "dependencies": { + "@types/react-reconciler": "^0.28.9" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, "node_modules/jiti": { "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", @@ -1734,6 +2125,21 @@ "jiti": "bin/jiti.js" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lil-gui": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/lil-gui/-/lil-gui-0.21.0.tgz", + "integrity": "sha512-tpvxN7v1GvE/Tv+GRopfOp0W7fVEjF4PltkuX8vOCIfim22rD1ztvfkoEMcv9lzQeuNUSeIrUmUjBwmlW/oUew==", + "license": "MIT" + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -1760,6 +2166,16 @@ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "license": "Apache-2.0" }, + "node_modules/maath": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz", + "integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==", + "license": "MIT", + "peerDependencies": { + "@types/three": ">=0.134.0", + "three": ">=0.134.0" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -1770,6 +2186,21 @@ "node": ">= 8" } }, + "node_modules/meshline": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz", + "integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.137" + } + }, + "node_modules/meshoptimizer": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.22.0.tgz", + "integrity": "sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==", + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -1830,6 +2261,16 @@ "thenify-all": "^1.0.0" } }, + "node_modules/n8ao": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/n8ao/-/n8ao-1.10.3.tgz", + "integrity": "sha512-JFRk4WgUAUWO2KdJTqmBT02dP9kG7rE2oQbBy64E9+YwQ3d96KB9QBanqNy/NnYUfUTYU6Z/5OS5t/yLey7+6Q==", + "license": "ISC", + "peerDependencies": { + "postprocessing": ">=6.30.0", + "three": ">=0.137" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -2203,6 +2644,21 @@ "web-vitals": "^5.1.0" } }, + "node_modules/postprocessing": { + "version": "6.39.4", + "resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.39.4.tgz", + "integrity": "sha512-oAS/PjAbc/xT3OzjUrGsorJ4J064XwhVD2t0OwKLP/E8QwDMUJ0oOv6ZI1SYMtLchv4g0ySEx+JcLy92+48vlA==", + "license": "Zlib", + "peerDependencies": { + "three": ">= 0.168.0 < 0.186.0" + } + }, + "node_modules/potpack": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", + "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==", + "license": "ISC" + }, "node_modules/preact": { "version": "10.28.2", "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.2.tgz", @@ -2213,6 +2669,16 @@ "url": "https://opencollective.com/preact" } }, + "node_modules/promise-worker-transferable": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz", + "integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==", + "license": "Apache-2.0", + "dependencies": { + "is-promise": "^2.1.0", + "lie": "^3.0.2" + } + }, "node_modules/protobufjs": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", @@ -2285,6 +2751,21 @@ "react": "^19.2.3" } }, + "node_modules/react-use-measure": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", + "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.13", + "react-dom": ">=16.13" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -2308,6 +2789,15 @@ "node": ">=8.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -2458,6 +2948,32 @@ "node": ">=0.10.0" } }, + "node_modules/stats-gl": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz", + "integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==", + "license": "MIT", + "dependencies": { + "@types/three": "*", + "three": "^0.170.0" + }, + "peerDependencies": { + "@types/three": "*", + "three": "*" + } + }, + "node_modules/stats-gl/node_modules/three": { + "version": "0.170.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz", + "integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==", + "license": "MIT" + }, + "node_modules/stats.js": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", + "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==", + "license": "MIT" + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -2517,6 +3033,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/suspend-react": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", + "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=17.0" + } + }, "node_modules/tailwindcss": { "version": "3.4.19", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", @@ -2578,6 +3103,44 @@ "node": ">=0.8" } }, + "node_modules/three": { + "version": "0.180.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.180.0.tgz", + "integrity": "sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==", + "license": "MIT" + }, + "node_modules/three-mesh-bvh": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.8.3.tgz", + "integrity": "sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==", + "license": "MIT", + "peerDependencies": { + "three": ">= 0.159.0" + } + }, + "node_modules/three-stdlib": { + "version": "2.36.1", + "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.1.tgz", + "integrity": "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==", + "license": "MIT", + "dependencies": { + "@types/draco3d": "^1.4.0", + "@types/offscreencanvas": "^2019.6.4", + "@types/webxr": "^0.5.2", + "draco3d": "^1.4.1", + "fflate": "^0.6.9", + "potpack": "^1.0.1" + }, + "peerDependencies": { + "three": ">=0.128.0" + } + }, + "node_modules/three-stdlib/node_modules/fflate": { + "version": "0.6.11", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.11.tgz", + "integrity": "sha512-3JyEFWGjFn7zHmoa9+zG1BmW7X2okcmAB+0Cnu9UFbVs/jCBnl2A8o065ZlXiw145K3eBM3uLuzrYXC0RK7eDg==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -2639,6 +3202,36 @@ "node": ">=8.0" } }, + "node_modules/troika-three-text": { + "version": "0.52.5", + "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.5.tgz", + "integrity": "sha512-Ry3jRhic9pzcY4JduSvRRyDmVOSqEW19gT4vtK+aCiPNVcDlmkxvGG0YbFd36RTDq1wExOupXnvNF/j1oiHHDA==", + "license": "MIT", + "dependencies": { + "bidi-js": "^1.0.2", + "troika-three-utils": "^0.52.5", + "troika-worker-utils": "^0.52.0", + "webgl-sdf-generator": "1.1.1" + }, + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-three-utils": { + "version": "0.52.5", + "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.5.tgz", + "integrity": "sha512-WsePbcX8RtfidRfsxK1eCZCjF81ZDzAKHH/evLs0hdV2wpoCb0vArGZHdzdOJrSS3k4zfdtbKDaBh8+phkrYnw==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-worker-utils": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz", + "integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==", + "license": "MIT" + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -2652,6 +3245,43 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tunnel-rat": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz", + "integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==", + "license": "MIT", + "dependencies": { + "zustand": "^4.3.2" + } + }, + "node_modules/tunnel-rat/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -2703,6 +3333,15 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2710,12 +3349,32 @@ "dev": true, "license": "MIT" }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/web-vitals": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.1.0.tgz", "integrity": "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==", "license": "Apache-2.0" }, + "node_modules/webgl-constants": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz", + "integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==" + }, + "node_modules/webgl-sdf-generator": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz", + "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==", + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -2730,6 +3389,35 @@ "engines": { "node": ">= 8" } + }, + "node_modules/zustand": { + "version": "5.0.14", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz", + "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 6269de7..7c6b9a8 100644 --- a/package.json +++ b/package.json @@ -10,12 +10,19 @@ }, "dependencies": { "@lottiefiles/dotlottie-react": "^0.17.13", + "@react-three/drei": "^10.7.7", + "@react-three/fiber": "^9.7.0", + "@react-three/postprocessing": "^3.0.4", + "@types/three": "^0.180.0", "framer-motion": "^12.29.0", + "lil-gui": "^0.21.0", "mixpanel-browser": "^2.74.0", "next": "^15.1.4", "posthog-js": "^1.336.4", + "postprocessing": "^6.39.4", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "three": "^0.180.0" }, "devDependencies": { "@types/node": "^22.10.5",