Skip to content

Commit 517e18e

Browse files
committed
refactor(logo): lock initial pose, 60fps cap, faster idle, remove dev readout
1 parent a56183a commit 517e18e

2 files changed

Lines changed: 39 additions & 89 deletions

File tree

app/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export default async function Index() {
2121
<HeroContent>
2222
<HomepageHeroEditor latestPost={<LatestBlogPost post={posts[0]} />}>
2323
<HeroLogoObserver>
24-
<PlatonicLogo size={200} showReadout />
24+
<PlatonicLogo size={200} />
2525
</HeroLogoObserver>
2626

2727
<h1 className="hero-tagline">

components/LogoConcepts/PlatonicLogo.tsx

Lines changed: 38 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
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';
54
import styled from 'styled-components';
65
import { 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;
154138
const IDLE_VEL = { dx: IDLE_SPEED, dy: IDLE_SPEED * -0.4 };
155139
const BLEND_RATE = 0.04;
156140
const 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;
164148
const 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

Comments
 (0)