import { useEffect, useId, useMemo, useRef, useState, type ReactNode } from "react";
import { Pause, Play, RotateCcw, SkipForward } from "lucide-react";
export type LoopStop = {
x: number;
y: number;
ids: string[];
inspect: string;
title: string;
note: string;
};
type Props = {
stops: LoopStop[];
loopMs?: number;
viewBox: string;
ariaLabel: string;
onSelect: (id: string) => void;
children: (api: {
on: (id: string) => boolean;
markerId: string;
token: { x: number; y: number };
step: number;
}) => ReactNode;
};
const HOLD = 0.38;
export function LoopFigure({
stops,
loopMs = 16000,
viewBox,
ariaLabel,
onSelect,
children,
}: Props) {
const [playing, setPlaying] = useState(true);
const [progress, setProgress] = useState(0);
const started = useRef(performance.now());
const pausedAt = useRef(0);
const rawId = useId().replace(/:/g, "");
const markerId = `arr-${rawId}`;
useEffect(() => {
if (!playing) return;
let frame = 0;
const tick = (now: number) => {
setProgress(((now - started.current) % loopMs) / loopMs);
frame = requestAnimationFrame(tick);
};
frame = requestAnimationFrame(tick);
return () => cancelAnimationFrame(frame);
}, [playing, loopMs]);
const motion = useMemo(() => alongPath(stops, progress), [stops, progress]);
const current = stops[motion.step];
const lit = useMemo(() => new Set(current.ids), [current]);
useEffect(() => {
onSelect(current.inspect);
}, [current.inspect, onSelect]);
const jump = (next: number) => {
const idx = ((next % stops.length) + stops.length) % stops.length;
started.current = performance.now() - (idx / stops.length) * loopMs;
setProgress(idx / stops.length);
};
return (
Loop ยท {motion.step + 1} / {stops.length}
{current.title}
{current.note}
);
}
function alongPath(stops: LoopStop[], t: number) {
const n = stops.length;
const x = ((((t % 1) + 1) % 1) * n);
const step = Math.min(n - 1, Math.floor(x));
const f = x - step;
const a = stops[step];
const b = stops[(step + 1) % n];
if (f < HOLD) return { x: a.x, y: a.y, step };
const u = (f - HOLD) / (1 - HOLD);
const ease = u * u * (3 - 2 * u);
return {
x: a.x + (b.x - a.x) * ease,
y: a.y + (b.y - a.y) * ease,
step,
};
}
export const FILL = {
orange: "#e39b4a",
yellow: "#e6cf4e",
cyan: "#78c3c4",
pink: "#efb0c2",
green: "#86c86f",
purple: "#9b8ec8",
white: "#f4f4f4",
blue: "#7eb6d9",
};
export function FigBox({
x,
y,
w,
h,
fill,
label,
lit,
onClick,
}: {
x: number;
y: number;
w: number;
h: number;
fill: string;
label: string;
lit: boolean;
onClick: () => void;
}) {
return (
{lit ? (
) : null}
18 ? 11 : 13}
fill="#1a1a1a"
fontWeight={lit ? 700 : 600}
>
{label}
);
}
export function Arrow({
x1,
y1,
x2,
y2,
active,
markerId,
}: {
x1: number;
y1: number;
x2: number;
y2: number;
active: boolean;
markerId: string;
}) {
return (
);
}