Download src/components/loop/LoopFigure.tsx from whassan/attention: direct link, hf CLI and curl.
- Browser
- Download file 7.98 kB
-
https://huggingface.co/spaces/whassan/attention/resolve/main/src/components/loop/LoopFigure.tsx
- Command line
-
hf download hf://spaces/whassan/attention/src/components/loop/LoopFigure.tsx
-
curl -L -o LoopFigure.tsx https://huggingface.co/spaces/whassan/attention/resolve/main/src/components/loop/LoopFigure.tsx
7.98 kB
| 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 ( | |
| <div className="overflow-hidden rounded-xl border border-white/10 bg-[#f7f4ec]"> | |
| <div className="flex flex-wrap items-center justify-between gap-3 border-b border-black/10 bg-[#111827] px-3 py-3 sm:px-4"> | |
| <div className="min-w-0"> | |
| <p className="font-mono text-[10px] tracking-[0.22em] text-cyan-200/80 uppercase"> | |
| Loop · {motion.step + 1} / {stops.length} | |
| </p> | |
| <p className="text-sm font-medium text-white">{current.title}</p> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <button | |
| type="button" | |
| aria-label={playing ? "Pause animation" : "Play animation"} | |
| onClick={() => { | |
| if (playing) { | |
| pausedAt.current = performance.now(); | |
| setPlaying(false); | |
| } else { | |
| started.current += performance.now() - pausedAt.current; | |
| setPlaying(true); | |
| } | |
| }} | |
| className="inline-flex min-h-11 items-center gap-1.5 rounded-md border border-white/15 bg-white/5 px-3 py-2 text-xs text-slate-100 hover:bg-white/10" | |
| > | |
| {playing ? <Pause className="h-3.5 w-3.5" /> : <Play className="h-3.5 w-3.5" />} | |
| {playing ? "Pause" : "Play"} | |
| </button> | |
| <button | |
| type="button" | |
| aria-label="Step to next block" | |
| onClick={() => { | |
| setPlaying(false); | |
| jump(motion.step + 1); | |
| }} | |
| className="inline-flex min-h-11 items-center gap-1.5 rounded-md border border-white/15 bg-white/5 px-3 py-2 text-xs text-slate-100 hover:bg-white/10" | |
| > | |
| <SkipForward className="h-3.5 w-3.5" /> | |
| Step | |
| </button> | |
| <button | |
| type="button" | |
| aria-label="Restart animation" | |
| onClick={() => { | |
| jump(0); | |
| setPlaying(true); | |
| }} | |
| className="inline-flex min-h-11 items-center gap-1.5 rounded-md border border-white/15 bg-white/5 px-3 py-2 text-xs text-slate-100 hover:bg-white/10" | |
| > | |
| <RotateCcw className="h-3.5 w-3.5" /> | |
| Restart | |
| </button> | |
| </div> | |
| </div> | |
| <div className="border-b border-black/10 bg-[#111827] px-3 py-2 sm:px-4"> | |
| <label className="flex items-center gap-3"> | |
| <span className="hidden font-mono text-[10px] text-slate-400 uppercase sm:inline"> | |
| Scrub | |
| </span> | |
| <input | |
| type="range" | |
| min={0} | |
| max={1} | |
| step={0.001} | |
| value={progress} | |
| aria-valuemin={0} | |
| aria-valuemax={100} | |
| aria-valuenow={Math.round(progress * 100)} | |
| aria-label="Scrub architecture animation" | |
| onChange={(event) => { | |
| const t = Number(event.target.value); | |
| setPlaying(false); | |
| pausedAt.current = performance.now(); | |
| started.current = performance.now() - t * loopMs; | |
| setProgress(t); | |
| }} | |
| className="h-11 w-full accent-cyan-300" | |
| /> | |
| </label> | |
| </div> | |
| <div className="overflow-x-auto"> | |
| <svg | |
| viewBox={viewBox} | |
| className="h-auto w-full min-w-[20rem]" | |
| role="img" | |
| aria-label={ariaLabel} | |
| > | |
| <defs> | |
| <marker | |
| id={markerId} | |
| markerWidth="8" | |
| markerHeight="8" | |
| refX="6" | |
| refY="4" | |
| orient="auto" | |
| > | |
| <path d="M0 0 L8 4 L0 8 Z" fill="#444" /> | |
| </marker> | |
| </defs> | |
| {children({ | |
| on: (id) => lit.has(id), | |
| markerId, | |
| token: { x: motion.x, y: motion.y }, | |
| step: motion.step, | |
| })} | |
| <circle | |
| cx={motion.x} | |
| cy={motion.y} | |
| r="8" | |
| fill="#e11d48" | |
| stroke="#fff" | |
| strokeWidth="2" | |
| /> | |
| </svg> | |
| </div> | |
| <p className="border-t border-black/10 bg-white/70 px-4 py-3 text-sm text-slate-800"> | |
| {current.note} | |
| </p> | |
| </div> | |
| ); | |
| } | |
| 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 ( | |
| <g onClick={onClick} className="cursor-pointer"> | |
| {lit ? ( | |
| <rect | |
| x={x - 4} | |
| y={y - 4} | |
| width={w + 8} | |
| height={h + 8} | |
| rx="8" | |
| fill="none" | |
| stroke="#e11d48" | |
| strokeWidth="2" | |
| className="pulse-ring" | |
| /> | |
| ) : null} | |
| <rect | |
| x={x} | |
| y={y} | |
| width={w} | |
| height={h} | |
| rx="5" | |
| fill={fill} | |
| stroke={lit ? "#111" : "#555"} | |
| strokeWidth={lit ? 2.4 : 1} | |
| /> | |
| <text | |
| x={x + w / 2} | |
| y={y + h / 2 + 4} | |
| textAnchor="middle" | |
| fontSize={label.length > 18 ? 11 : 13} | |
| fill="#1a1a1a" | |
| fontWeight={lit ? 700 : 600} | |
| > | |
| {label} | |
| </text> | |
| </g> | |
| ); | |
| } | |
| export function Arrow({ | |
| x1, | |
| y1, | |
| x2, | |
| y2, | |
| active, | |
| markerId, | |
| }: { | |
| x1: number; | |
| y1: number; | |
| x2: number; | |
| y2: number; | |
| active: boolean; | |
| markerId: string; | |
| }) { | |
| return ( | |
| <line | |
| x1={x1} | |
| y1={y1} | |
| x2={x2} | |
| y2={y2} | |
| stroke={active ? "#111" : "#888"} | |
| strokeWidth={active ? 2.3 : 1.2} | |
| markerEnd={`url(#${markerId})`} | |
| /> | |
| ); | |
| } | |