"use client"; import { motion } from "framer-motion"; import { useEffect, useRef } from "react"; import * as THREE from "three"; import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"; import type { Messages } from "@/messages"; const MODEL_URL = "/models/ray-ban-meta-smart-glasses/Untitled.glb"; const ANCHORS = [ new THREE.Vector3(-0.78, 0.12, 0.2), new THREE.Vector3(0.92, 0.08, 0.22), new THREE.Vector3(0, -0.24, 0.26) ]; export default function GlassesModelSection({ t }: { t: Messages }) { const canvasRef = useRef(null); const wrapRef = useRef(null); const annotationRefs = useRef>([]); const anchorDotRefs = useRef>([]); const lineRefs = useRef>([]); const pointerRef = useRef({ x: 0, y: 0 }); useEffect(() => { const canvas = canvasRef.current; const wrap = wrapRef.current; if (!canvas || !wrap) return; let frame = 0; let disposed = false; const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(34, 1, 0.1, 100); camera.position.set(0, 0.15, 7.9); const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true, powerPreference: "high-performance" }); renderer.setClearColor(0x000000, 0); renderer.outputColorSpace = THREE.SRGBColorSpace; renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1.08; const group = new THREE.Group(); const anchorObjects = ANCHORS.map((position) => { const anchor = new THREE.Object3D(); anchor.position.copy(position); group.add(anchor); return anchor; }); const fallback = createFallbackGlassesModel(); group.add(fallback); group.position.y = -0.34; scene.add(group); scene.add(new THREE.HemisphereLight(0xffffff, 0xaeb9c6, 1.8)); const key = new THREE.DirectionalLight(0xffffff, 3.8); key.position.set(3.4, 4.6, 5.2); scene.add(key); const rim = new THREE.DirectionalLight(0x9ec7ff, 1.7); rim.position.set(-4, 1.8, -2); scene.add(rim); const shadow = new THREE.Mesh( new THREE.CircleGeometry(2.8, 96), new THREE.MeshBasicMaterial({ color: 0x315f8f, transparent: true, opacity: 0.08, depthWrite: false }) ); shadow.position.set(0, -1.12, -0.92); shadow.scale.y = 0.18; group.add(shadow); const loader = new GLTFLoader(); loader.load( MODEL_URL, (gltf) => { if (disposed) return; fallback.visible = false; const model = gltf.scene; normalizeModel(model); group.add(model); }, undefined, () => { fallback.visible = true; } ); const resize = () => { const { width, height } = wrap.getBoundingClientRect(); const pixelRatio = Math.min(window.devicePixelRatio || 1, 1.7); const scale = width < 640 ? 0.42 : width < 1024 ? 0.68 : 0.9; renderer.setPixelRatio(pixelRatio); renderer.setSize(width, height, false); group.scale.setScalar(scale); group.position.y = width < 640 ? -0.44 : -0.34; camera.aspect = width / Math.max(height, 1); camera.updateProjectionMatrix(); }; const updateAnnotations = () => { const bounds = wrap.getBoundingClientRect(); const projected = new THREE.Vector3(); const world = new THREE.Vector3(); anchorObjects.forEach((anchor, index) => { anchor.getWorldPosition(world); projected.copy(world).project(camera); const x = (projected.x * 0.5 + 0.5) * bounds.width; const y = (-projected.y * 0.5 + 0.5) * bounds.height; const visible = projected.z > -1 && projected.z < 1; const dot = anchorDotRefs.current[index]; const line = lineRefs.current[index]; const annotation = annotationRefs.current[index]; if (dot) { dot.style.opacity = visible ? "1" : "0"; dot.style.transform = `translate3d(${x}px, ${y}px, 0) translate(-50%, -50%)`; } if (line && annotation) { const labelBounds = annotation.getBoundingClientRect(); const labelX = labelBounds.left - bounds.left + (index === 1 ? labelBounds.width - 8 : 8); const labelY = labelBounds.top - bounds.top + 12; line.setAttribute("x1", String(x)); line.setAttribute("y1", String(y)); line.setAttribute("x2", String(labelX)); line.setAttribute("y2", String(labelY)); line.style.opacity = visible ? "0.62" : "0"; } }); }; const observer = new ResizeObserver(resize); observer.observe(wrap); resize(); const handlePointerMove = (event: PointerEvent) => { const bounds = wrap.getBoundingClientRect(); pointerRef.current.x = ((event.clientX - bounds.left) / bounds.width - 0.5) * 2; pointerRef.current.y = ((event.clientY - bounds.top) / bounds.height - 0.5) * 2; }; const handlePointerLeave = () => { pointerRef.current.x = 0; pointerRef.current.y = 0; }; wrap.addEventListener("pointermove", handlePointerMove); wrap.addEventListener("pointerleave", handlePointerLeave); const animate = () => { if (disposed) return; const targetY = pointerRef.current.x * 0.28; const targetX = pointerRef.current.y * 0.16; group.rotation.y += (targetY + Math.sin(performance.now() * 0.00045) * 0.22 - group.rotation.y) * 0.055; group.rotation.x += (-0.08 - targetX - group.rotation.x) * 0.055; group.rotation.z = Math.sin(performance.now() * 0.00062) * 0.025; renderer.render(scene, camera); updateAnnotations(); frame = window.requestAnimationFrame(animate); }; animate(); return () => { disposed = true; window.cancelAnimationFrame(frame); observer.disconnect(); wrap.removeEventListener("pointermove", handlePointerMove); wrap.removeEventListener("pointerleave", handlePointerLeave); scene.traverse((object) => { if (!(object instanceof THREE.Mesh)) return; object.geometry.dispose(); const material = object.material; if (Array.isArray(material)) material.forEach((item) => item.dispose()); else material.dispose(); }); renderer.dispose(); }; }, []); return (
{t.model.annotations.map((item, index) => ( { lineRefs.current[index] = node; }} className="transition-opacity duration-200" stroke="rgba(49,95,143,0.55)" strokeWidth="1.5" strokeDasharray="5 7" /> ))} {t.model.annotations.map((item, index) => ( { anchorDotRefs.current[index] = node; }} className="pointer-events-none absolute left-0 top-0 z-[9] grid size-4 place-items-center rounded-full bg-optical shadow-[0_0_0_8px_rgba(49,95,143,0.12)] transition-opacity duration-200" aria-hidden> ))}

{t.model.eyebrow}

{t.model.title}

{t.model.body}

{ annotationRefs.current[0] = node; }} className="left-4 top-[17rem] sm:left-10 lg:left-16 lg:top-[24rem]" align="left" label={t.model.annotations[0].label} body={t.model.annotations[0].body} /> { annotationRefs.current[1] = node; }} className="right-4 top-[33rem] sm:right-10 lg:right-20 lg:top-[24rem]" align="right" label={t.model.annotations[1].label} body={t.model.annotations[1].body} /> { annotationRefs.current[2] = node; }} className="bottom-8 left-6 sm:left-16 lg:bottom-16 lg:left-[42%]" align="left" label={t.model.annotations[2].label} body={t.model.annotations[2].body} />
); } function Annotation({ label, body, align, className, refCallback }: { label: string; body: string; align: "left" | "right"; className: string; refCallback: (node: HTMLDivElement | null) => void }) { return (
{align === "right" ? : null} {align === "left" ? : null}

{label}

{body}

); } function normalizeModel(model: THREE.Object3D) { const box = new THREE.Box3().setFromObject(model); const center = box.getCenter(new THREE.Vector3()); const size = box.getSize(new THREE.Vector3()); const maxDimension = Math.max(size.x, size.y, size.z) || 1; model.position.sub(center); model.scale.setScalar(3.25 / maxDimension); model.rotation.set(0, Math.PI, 0); model.traverse((object) => { if (!(object instanceof THREE.Mesh)) return; object.castShadow = false; object.receiveShadow = false; const material = object.material; const materials = Array.isArray(material) ? material : [material]; materials.forEach((item) => { if ("roughness" in item) item.roughness = Math.max(0.26, item.roughness ?? 0.26); if ("metalness" in item) item.metalness = Math.min(0.65, item.metalness ?? 0.35); }); }); } function createFallbackGlassesModel() { const group = new THREE.Group(); group.scale.setScalar(0.9); const frameMaterial = new THREE.MeshPhysicalMaterial({ color: 0x161a20, roughness: 0.28, metalness: 0.55, clearcoat: 0.85, clearcoatRoughness: 0.18 }); const lensMaterial = new THREE.MeshPhysicalMaterial({ color: 0xbfd7e3, roughness: 0.12, metalness: 0, transmission: 0.48, transparent: true, opacity: 0.28, thickness: 0.12, side: THREE.DoubleSide }); const padMaterial = new THREE.MeshPhysicalMaterial({ color: 0xf8fbff, roughness: 0.18, transparent: true, opacity: 0.52 }); const accentMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.42 }); const ringGeometry = new THREE.TorusGeometry(1.04, 0.065, 18, 112); const lensGeometry = new THREE.CircleGeometry(0.93, 72); const hingeGeometry = new THREE.BoxGeometry(0.22, 0.22, 0.22); const padGeometry = new THREE.SphereGeometry(0.16, 24, 16); [-1.18, 1.18].forEach((xPosition) => { const ring = new THREE.Mesh(ringGeometry, frameMaterial); ring.position.x = xPosition; ring.scale.y = 0.68; group.add(ring); const lens = new THREE.Mesh(lensGeometry, lensMaterial); lens.position.set(xPosition, 0, -0.03); lens.scale.set(1, 0.66, 1); group.add(lens); const highlight = new THREE.Mesh(new THREE.PlaneGeometry(0.58, 0.045), accentMaterial); highlight.position.set(xPosition - 0.2, 0.34, 0.02); highlight.rotation.z = -0.22; group.add(highlight); const hinge = new THREE.Mesh(hingeGeometry, frameMaterial); hinge.position.set(xPosition > 0 ? 2.18 : -2.18, 0.03, -0.02); hinge.scale.set(0.55, 0.8, 0.62); group.add(hinge); }); const bridgeCurve = new THREE.CatmullRomCurve3([ new THREE.Vector3(-0.3, 0.08, 0), new THREE.Vector3(-0.12, 0.25, 0.04), new THREE.Vector3(0, 0.3, 0.05), new THREE.Vector3(0.12, 0.25, 0.04), new THREE.Vector3(0.3, 0.08, 0) ]); group.add(new THREE.Mesh(new THREE.TubeGeometry(bridgeCurve, 38, 0.052, 14, false), frameMaterial)); [-0.28, 0.28].forEach((xPosition) => { const pad = new THREE.Mesh(padGeometry, padMaterial); pad.position.set(xPosition, -0.22, 0.14); pad.scale.set(0.55, 0.9, 0.28); pad.rotation.z = xPosition > 0 ? -0.22 : 0.22; group.add(pad); }); const leftTemple = new THREE.CatmullRomCurve3([ new THREE.Vector3(-2.18, 0.03, -0.03), new THREE.Vector3(-2.75, 0.03, -0.72), new THREE.Vector3(-3.1, -0.04, -1.75), new THREE.Vector3(-2.84, -0.28, -2.42) ]); const rightTemple = new THREE.CatmullRomCurve3([ new THREE.Vector3(2.18, 0.03, -0.03), new THREE.Vector3(2.75, 0.03, -0.72), new THREE.Vector3(3.1, -0.04, -1.75), new THREE.Vector3(2.84, -0.28, -2.42) ]); group.add(new THREE.Mesh(new THREE.TubeGeometry(leftTemple, 54, 0.055, 14, false), frameMaterial)); group.add(new THREE.Mesh(new THREE.TubeGeometry(rightTemple, 54, 0.055, 14, false), frameMaterial)); group.rotation.set(-0.08, -0.2, 0); return group; }