"use client"; import { useEffect, useRef } from "react"; import * as THREE from "three"; import { usePrefersReducedMotion } from "@/hooks/usePrefersReducedMotion"; export interface IntradaySignalMetric { key: string; label: string; value: string; hint: string; fill: number | null; tone: string; } function clamp(value: number, min: number, max: number) { return Math.min(Math.max(value, min), max); } function getToneColor(tone: string) { if (tone === "cyan") return "#4DA3FF"; if (tone === "blue") return "#60a5fa"; if (tone === "amber") return "#f59e0b"; return "#9FB2C7"; } export function IntradaySignalScene({ metrics, score, }: { metrics: IntradaySignalMetric[]; score: number; }) { const containerRef = useRef(null); const prefersReducedMotion = usePrefersReducedMotion(); useEffect(() => { const host = containerRef.current; if (!host) return; const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true, powerPreference: "low-power", }); renderer.setClearColor(0x000000, 0); renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.5)); const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(34, 1, 0.1, 100); camera.position.set(0, 2.8, 7.2); camera.lookAt(0, 1.2, 0); const ambient = new THREE.AmbientLight(0xbfe8ff, 1.25); const keyLight = new THREE.PointLight(0x67e8f9, 22, 18, 2); keyLight.position.set(-3.8, 5.6, 4.8); const warmLight = new THREE.PointLight(0xf59e0b, 12, 16, 2); warmLight.position.set(4.2, 2.8, 4); scene.add(ambient, keyLight, warmLight); const stage = new THREE.Group(); scene.add(stage); const floor = new THREE.Mesh( new THREE.CylinderGeometry(3.3, 3.8, 0.12, 48), new THREE.MeshStandardMaterial({ color: new THREE.Color(score >= 0 ? "#10263b" : "#2c1d12"), emissive: new THREE.Color(score >= 0 ? "#0e7490" : "#b45309"), emissiveIntensity: 0.18 + clamp(Math.abs(score) / 8, 0, 0.24), metalness: 0.2, roughness: 0.78, }), ); floor.position.y = -0.12; stage.add(floor); const ring = new THREE.Mesh( new THREE.TorusGeometry(2.8, 0.03, 18, 100), new THREE.MeshBasicMaterial({ color: new THREE.Color(score >= 0 ? "#4DA3FF" : "#F59E0B"), transparent: true, opacity: 0.5, }), ); ring.rotation.x = Math.PI / 2; ring.position.y = 0.03; stage.add(ring); const barGeometry = new THREE.BoxGeometry(0.8, 1, 0.8); const capGeometry = new THREE.SphereGeometry(0.16, 16, 16); const bars: Array<{ mesh: THREE.Mesh; cap: THREE.Mesh; glow: THREE.Mesh; baseY: number; targetHeight: number; }> = []; const xPositions = [-1.8, -0.6, 0.6, 1.8]; metrics.slice(0, 4).forEach((metric, index) => { const height = 0.5 + ((metric.fill ?? 20) / 100) * 2.8; const color = new THREE.Color(getToneColor(metric.tone)); const material = new THREE.MeshStandardMaterial({ color, emissive: color, emissiveIntensity: 0.22, metalness: 0.14, roughness: 0.38, }); const mesh = new THREE.Mesh(barGeometry, material); mesh.position.set(xPositions[index] || 0, height / 2, 0); mesh.scale.y = height; stage.add(mesh); const cap = new THREE.Mesh( capGeometry, new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.95, }), ); cap.position.set(mesh.position.x, height + 0.2, 0); stage.add(cap); const glow = new THREE.Mesh( new THREE.CylinderGeometry(0.46, 0.58, 0.08, 32), new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.22, }), ); glow.position.set(mesh.position.x, 0.06, 0); stage.add(glow); bars.push({ mesh, cap, glow, baseY: cap.position.y, targetHeight: height, }); }); const resize = () => { const width = Math.max(host.clientWidth, 1); const height = Math.max(host.clientHeight, 1); renderer.setSize(width, height, false); camera.aspect = width / height; camera.updateProjectionMatrix(); }; resize(); host.appendChild(renderer.domElement); const clock = new THREE.Clock(); let frameId = 0; const renderFrame = () => { frameId = window.requestAnimationFrame(renderFrame); const elapsed = clock.getElapsedTime(); stage.rotation.y = Math.sin(elapsed * 0.35) * 0.16; ring.material.opacity = 0.38 + Math.sin(elapsed * 0.8) * 0.08; bars.forEach((bar, index) => { const pulse = prefersReducedMotion ? 0 : Math.sin(elapsed * 1.5 + index * 0.8) * 0.08; bar.cap.position.y = bar.baseY + pulse; bar.glow.scale.x = 1 + Math.sin(elapsed * 1.2 + index) * 0.06; bar.glow.scale.z = 1 + Math.sin(elapsed * 1.2 + index) * 0.06; }); renderer.render(scene, camera); }; const observer = new ResizeObserver(resize); observer.observe(host); frameId = window.requestAnimationFrame(renderFrame); return () => { observer.disconnect(); window.cancelAnimationFrame(frameId); stage.traverse((child) => { if (child instanceof THREE.Mesh) { child.geometry.dispose(); if (Array.isArray(child.material)) { child.material.forEach((material) => material.dispose()); } else { child.material.dispose(); } } }); renderer.dispose(); if (renderer.domElement.parentNode === host) { host.removeChild(renderer.domElement); } }; }, [metrics, prefersReducedMotion, score]); return (
); }