11'use client' ;
22
3- import { useEffect , useMemo , useRef , useState } from 'react' ;
4- import { createPortal } from 'react-dom' ;
3+ import { useEffect , useMemo , useRef } from 'react' ;
54import styled from 'styled-components' ;
65import { theme } from '../../utils/theme' ;
76
@@ -135,32 +134,17 @@ function qNorm(q: Quat): Quat {
135134 return len > 0 ? [ q [ 0 ] / len , q [ 1 ] / len , q [ 2 ] / len , q [ 3 ] / len ] : q ;
136135}
137136
138- // Inverse of the INIT_QUAT construction: decompose a quaternion (built
139- // as qMul(qMul(qx, qy), qz)) into degrees for the X, Y, Z half-angles.
140- // Matches the convention used by the initXH/initYH/initZH constants.
141- function quatToXYZDeg ( q : Quat ) : { x : number ; y : number ; z : number } {
142- const w = q [ 0 ] ,
143- qx = q [ 1 ] ,
144- qy = q [ 2 ] ,
145- qz = q [ 3 ] ;
146- const beta = Math . asin ( 2 * ( qx * qz + w * qy ) ) ;
147- const alpha = Math . atan2 ( 2 * ( w * qx - qy * qz ) , 1 - 2 * ( qx * qx + qy * qy ) ) ;
148- const gamma = Math . atan2 ( 2 * ( w * qz - qx * qy ) , 1 - 2 * ( qy * qy + qz * qz ) ) ;
149- const r2d = 180 / Math . PI ;
150- return { x : alpha * r2d , y : beta * r2d , z : gamma * r2d } ;
151- }
152-
153- const IDLE_SPEED = 0.09 ;
137+ const IDLE_SPEED = 0.22 ;
154138const IDLE_VEL = { dx : IDLE_SPEED , dy : IDLE_SPEED * - 0.4 } ;
155139const BLEND_RATE = 0.04 ;
156140const SENSITIVITY = 0.015 ;
157141
158142// Initial pose: X/Y/Z rotations chosen so the cube reads as mid-spin on
159- // first paint (3 faces visible, slight roll) . Used both as the rAF seed
160- // and as the SSR transform so the unhydrated logo is already in 3D.
161- const initXH = ( - 22 * Math . PI ) / 180 / 2 ;
162- const initYH = ( 35 * Math . PI ) / 180 / 2 ;
163- const initZH = ( 15 * Math . PI ) / 180 / 2 ;
143+ // first paint. Used both as the rAF seed and as the SSR transform so the
144+ // unhydrated logo is already in 3D.
145+ const initXH = ( - 107.8 * Math . PI ) / 180 / 2 ;
146+ const initYH = ( 4.4 * Math . PI ) / 180 / 2 ;
147+ const initZH = ( - 27.6 * Math . PI ) / 180 / 2 ;
164148const INIT_QUAT : Quat = qMul (
165149 qMul ( [ Math . cos ( initXH ) , Math . sin ( initXH ) , 0 , 0 ] , [ Math . cos ( initYH ) , 0 , Math . sin ( initYH ) , 0 ] ) ,
166150 [ Math . cos ( initZH ) , 0 , 0 , Math . sin ( initZH ) ]
@@ -192,7 +176,16 @@ const sharedRotation: SharedRotation = ((globalThis as Record<string, unknown>)[
192176
193177 start ( ) {
194178 if ( this . rafId != null ) return ;
195- const loop = ( ) => {
179+ // Cap at ~60fps. rAF fires at the display refresh rate, so 120Hz /
180+ // 144Hz screens would otherwise do 2+ times the work for no benefit.
181+ // 15.4ms (1000/65) threshold survives 60Hz frame jitter without
182+ // accidentally skipping a paint.
183+ const FRAME_MS = 1000 / 65 ;
184+ let lastTime = 0 ;
185+ const loop = ( time : number ) => {
186+ this . rafId = requestAnimationFrame ( loop ) ;
187+ if ( time - lastTime < FRAME_MS ) return ;
188+ lastTime = time ;
196189 if ( ! this . dragging ) {
197190 const yHalf = ( this . velocity . dx * SENSITIVITY ) / 2 ;
198191 const xHalf = ( - this . velocity . dy * SENSITIVITY ) / 2 ;
@@ -203,7 +196,6 @@ const sharedRotation: SharedRotation = ((globalThis as Record<string, unknown>)[
203196 this . velocity . dy = this . velocity . dy * ( 1 - BLEND_RATE ) + this . idleVel . dy * BLEND_RATE ;
204197 }
205198 for ( const cb of this . subscribers ) cb ( ) ;
206- this . rafId = requestAnimationFrame ( loop ) ;
207199 } ;
208200 this . rafId = requestAnimationFrame ( loop ) ;
209201 } ,
@@ -256,44 +248,11 @@ const FaceTile = styled.div`
256248 pointer-events: none;
257249` ;
258250
259- const Readout = styled . div `
260- position: fixed;
261- top: 12px;
262- left: 50%;
263- translate: -50% 0;
264- z-index: 9999;
265- padding: 8px 14px;
266- font-family: var(--font-mono, monospace);
267- font-size: 13px;
268- line-height: 1.5;
269- color: white;
270- background: oklch(0 0 0 / 0.82);
271- border-radius: 6px;
272- pointer-events: none;
273- white-space: pre;
274- letter-spacing: 0.02em;
275- ` ;
276-
277- export default function PlatonicLogo ( {
278- size = 120 ,
279- className,
280- showReadout = false ,
281- } : {
282- size ?: number ;
283- className ?: string ;
284- showReadout ?: boolean ;
285- } ) {
251+ export default function PlatonicLogo ( { size = 120 , className } : { size ?: number ; className ?: string } ) {
286252 const sceneRef = useRef < HTMLDivElement > ( null ) ;
287253 const faceRefs = useRef < ( HTMLDivElement | null ) [ ] > ( [ ] ) ;
288254 const lastZRef = useRef < number [ ] > ( [ ] ) ;
289- const readoutRef = useRef < HTMLDivElement > ( null ) ;
290255 const lastPointerRef = useRef < { x : number ; y : number } > ( { x : 0 , y : 0 } ) ;
291- // Portal target only resolved after mount, so the Readout doesn't
292- // attempt to render server-side (where document doesn't exist).
293- const [ portalRoot , setPortalRoot ] = useState < HTMLElement | null > ( null ) ;
294- useEffect ( ( ) => {
295- if ( showReadout ) setPortalRoot ( document . body ) ;
296- } , [ showReadout ] ) ;
297256
298257 const s = size * SCALE ;
299258 const faceEdgePx = FACE_EDGE * s ;
@@ -340,10 +299,6 @@ export default function PlatonicLogo({
340299 lastZRef . current [ i ] = z ;
341300 }
342301 }
343- if ( readoutRef . current ) {
344- const e = quatToXYZDeg ( sharedRotation . quat ) ;
345- readoutRef . current . textContent = `X: ${ e . x . toFixed ( 1 ) . padStart ( 6 ) } °\nY: ${ e . y . toFixed ( 1 ) . padStart ( 6 ) } °\nZ: ${ e . z . toFixed ( 1 ) . padStart ( 6 ) } °` ;
346- }
347302 } ) ;
348303 } , [ localMats ] ) ;
349304
@@ -398,30 +353,25 @@ export default function PlatonicLogo({
398353 } , [ ] ) ;
399354
400355 return (
401- < >
402- < Scene ref = { sceneRef } className = { className } style = { { width : size , height : size } } >
403- { FACES . map ( ( f , i ) => (
404- < FaceTile
405- key = { i }
406- ref = { el => {
407- faceRefs . current [ i ] = el ;
408- } }
409- style = { {
410- width : faceEdgePx ,
411- height : faceEdgePx ,
412- marginLeft : - halfEdge ,
413- marginTop : - halfEdge ,
414- background : theme . palette [ f . paletteStep ] ,
415- transform : initialFaceStyles [ i ] . transform ,
416- zIndex : initialFaceStyles [ i ] . zIndex ,
417- mixBlendMode : f . blendMode ,
418- } }
419- />
420- ) ) }
421- </ Scene >
422- { showReadout &&
423- portalRoot &&
424- createPortal ( < Readout ref = { readoutRef } > X: ---° Y: ---° Z: ---°</ Readout > , portalRoot ) }
425- </ >
356+ < Scene ref = { sceneRef } className = { className } style = { { width : size , height : size } } >
357+ { FACES . map ( ( f , i ) => (
358+ < FaceTile
359+ key = { i }
360+ ref = { el => {
361+ faceRefs . current [ i ] = el ;
362+ } }
363+ style = { {
364+ width : faceEdgePx ,
365+ height : faceEdgePx ,
366+ marginLeft : - halfEdge ,
367+ marginTop : - halfEdge ,
368+ background : theme . palette [ f . paletteStep ] ,
369+ transform : initialFaceStyles [ i ] . transform ,
370+ zIndex : initialFaceStyles [ i ] . zIndex ,
371+ mixBlendMode : f . blendMode ,
372+ } }
373+ />
374+ ) ) }
375+ </ Scene >
426376 ) ;
427377}
0 commit comments