An analog clock face where the hour and second hands are concentric text arcs -- 'A Uniform for the Sharp,' orbits at 123px radius, 'the Critical, and the Authentic.' at 106px -- rotating in real time via requestAnimationFrame. The hour numerals are static SVG text nodes positioned on a 155px ring; the current hour highlights in black while the rest stay mid-grey. No libraries, just SVG textPath elements whose parent groups get a rotate transform each frame.
Source
"use client";
import { useEffect, useRef } from "react";
import ScrambleLink from "../ScrambleLink";
const SIZE = 377;
const CX = SIZE / 2;
const CY = SIZE / 2;
const HOUR_R = 155;
const OUTER_R = 123;
const INNER_R = 106;
function circlePath(cx: number, cy: number, r: number): string {
return [
`M ${cx} ${cy - r}`,
`A ${r} ${r} 0 1 1 ${cx} ${cy + r}`,
`A ${r} ${r} 0 1 1 ${cx} ${cy - r}`,
].join(" ");
}
const FONT: React.CSSProperties = {
fontFamily: "var(--font-kt-flux)",
fontVariationSettings: "'wght' 600",
};
export default function CookCanvas() {
const hourHandRef = useRef<SVGGElement>(null);
const secondHandRef = useRef<SVGGElement>(null);
const hourTextsRef = useRef<(SVGTextElement | null)[]>([]);
const rafRef = useRef(0);
const prevHourRef = useRef(-1);
useEffect(() => {
const tick = () => {
const now = new Date();
const h = now.getHours() % 12;
const m = now.getMinutes();
const s = now.getSeconds();
const ms = now.getMilliseconds();
const sec = s + ms / 1000;
const min = m + sec / 60;
const hr = h + min / 60;
hourHandRef.current?.setAttribute(
"transform",
`rotate(${hr * 30} ${CX} ${CY})`,
);
secondHandRef.current?.setAttribute(
"transform",
`rotate(${sec * 6} ${CX} ${CY})`,
);
const display = h === 0 ? 12 : h;
if (display !== prevHourRef.current) {
prevHourRef.current = display;
hourTextsRef.current.forEach((el, i) => {
el?.setAttribute("fill", i + 1 === display ? "#000" : "#a8a8a8");
});
}
rafRef.current = requestAnimationFrame(tick);
};
rafRef.current = requestAnimationFrame(tick);
return () => cancelAnimationFrame(rafRef.current);
}, []);
return (
<div className="fixed inset-0 flex items-center justify-center bg-white font-[family-name:var(--font-kt-flux)]">
{/* Back link */}
<div className="fixed top-8 left-8 z-10">
<ScrambleLink
from="NEMAWASHI LAB"
to="← BACK"
href="/lab"
className="text-[14px] tracking-[0.08em] text-foreground/60 uppercase pointer-events-auto font-[family-name:var(--font-kt-flux)]"
style={{ fontVariationSettings: "'wght' 600, 'SRIF' 400" }}
/>
</div>
<div className="relative" style={{ width: SIZE, height: SIZE }}>
{/* Circle with inner shadow */}
<div
className="absolute inset-0 rounded-full"
style={{ boxShadow: "inset 8px 8px 24px rgba(0,0,0,0.07)" }}
/>
<svg
className="absolute inset-0"
width={SIZE}
height={SIZE}
viewBox={`0 0 ${SIZE} ${SIZE}`}
>
{/* Hour numbers */}
{Array.from({ length: 12 }, (_, i) => {
const hour = i + 1;
return (
<text
key={hour}
ref={(el) => {
hourTextsRef.current[i] = el;
}}
x={CX}
y={CY - HOUR_R}
fill="#a8a8a8"
fontSize={14}
letterSpacing="0.28px"
textAnchor="middle"
dominantBaseline="central"
style={FONT}
transform={`rotate(${hour * 30} ${CX} ${CY})`}
>
{hour}
</text>
);
})}
{/* Hour hand — outer motto arc */}
<g ref={hourHandRef}>
<path
id="hour-ring"
d={circlePath(CX, CY, OUTER_R)}
fill="none"
/>
<text
fill="#000"
fontSize={14}
letterSpacing="0.28px"
style={FONT}
>
<textPath href="#hour-ring" startOffset="0%">
A Uniform for the Sharp,
</textPath>
</text>
</g>
{/* Second hand — inner motto arc */}
<g ref={secondHandRef}>
<path
id="second-ring"
d={circlePath(CX, CY, INNER_R)}
fill="none"
/>
<text
fill="#000"
fontSize={14}
letterSpacing="0.28px"
style={FONT}
>
<textPath href="#second-ring" startOffset="0%">
the Critical, and the Authentic.
</textPath>
</text>
</g>
{/* Static center text */}
<text
x={CX}
y={CY}
fill="#000"
fontSize={14}
letterSpacing="0.28px"
textAnchor="middle"
dominantBaseline="central"
style={FONT}
>
GIRAGIRA
</text>
</svg>
</div>
</div>
);
}