Before the doublework change

This commit is contained in:
2026-08-23 22:40:43 +02:00
parent dfe2cdf666
commit 95b17e945c
8 changed files with 170 additions and 4 deletions
+73
View File
@@ -0,0 +1,73 @@
import { useEffect, useRef } from 'react'
import * as THREE from 'three'
// A self-contained three.js stage that slowly turns a single model and lets the viewer
// swipe to spin it faster, the momentum easing back to the idle rate — the same feel as
// the inventory rack, reused for one-off showpieces like a merit award.
export function SpinningModel({ build, className, cameraPos = [0.6, 0.4, 5], baseSpin = 0.008 }: {
build: () => THREE.Group
className?: string
cameraPos?: [number, number, number]
baseSpin?: number
}) {
const stageRef = useRef<HTMLDivElement>(null)
const [camX, camY, camZ] = cameraPos
useEffect(() => {
const stage = stageRef.current
if (!stage) return
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
stage.appendChild(renderer.domElement)
renderer.domElement.style.cssText = 'position:absolute;inset:0;width:100%;height:100%'
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(34, 1, 0.1, 100)
camera.position.set(camX, camY, camZ)
camera.lookAt(0, 0, 0)
scene.add(new THREE.AmbientLight(0x40483a, 0.9))
const key = new THREE.DirectionalLight(0xfff4e0, 1.1); key.position.set(2, 3, 3); scene.add(key)
const rim = new THREE.DirectionalLight(0x9fd020, 0.4); rim.position.set(-2, 1, -2); scene.add(rim)
const holder = new THREE.Group(); holder.add(build()); scene.add(holder)
const resize = () => { const w = stage.clientWidth, h = stage.clientHeight; if (w && h) { camera.aspect = w / h; camera.updateProjectionMatrix(); renderer.setSize(w, h, false) } }
resize(); const ro = new ResizeObserver(resize); ro.observe(stage)
// Drag to spin (see inventory rack): a swipe rotates 1:1 and hands its speed to `spin`.
let spin = baseSpin
let dragging = false
let lastX = 0
const dom = renderer.domElement
dom.style.touchAction = 'none'
dom.style.cursor = 'grab'
const onDown = (event: PointerEvent) => { dragging = true; lastX = event.clientX; dom.style.cursor = 'grabbing'; dom.setPointerCapture(event.pointerId) }
const onMove = (event: PointerEvent) => {
if (!dragging) return
const delta = (event.clientX - lastX) * 0.01
lastX = event.clientX
holder.rotation.y += delta
spin = THREE.MathUtils.clamp(delta, -0.35, 0.35)
}
const onUp = (event: PointerEvent) => { dragging = false; dom.style.cursor = 'grab'; try { dom.releasePointerCapture(event.pointerId) } catch { /* already released */ } }
dom.addEventListener('pointerdown', onDown)
dom.addEventListener('pointermove', onMove)
dom.addEventListener('pointerup', onUp)
dom.addEventListener('pointercancel', onUp)
let raf = 0
const tick = () => {
if (!dragging) { holder.rotation.y += spin; spin += (baseSpin - spin) * 0.04 }
renderer.render(scene, camera); raf = requestAnimationFrame(tick)
}
tick()
return () => {
cancelAnimationFrame(raf); ro.disconnect()
dom.removeEventListener('pointerdown', onDown); dom.removeEventListener('pointermove', onMove)
dom.removeEventListener('pointerup', onUp); dom.removeEventListener('pointercancel', onUp)
renderer.dispose(); dom.remove()
scene.traverse(o => { if (o instanceof THREE.Mesh) { o.geometry.dispose(); (o.material as THREE.Material).dispose() } })
}
}, [build, camX, camY, camZ, baseSpin])
return <div ref={stageRef} className={className || 'model3d-stage'} />
}