()
| 657 | } |
| 658 | |
| 659 | export function MarksLab() { |
| 660 | const [configs, setConfigs] = useState<Record<string, MarkConfig>>(initConfigs) |
| 661 | const [markId, setMarkId] = useState(MARKS[0].id) |
| 662 | const [amt, setAmt] = useState(0) |
| 663 | const [playing, setPlaying] = useState(false) |
| 664 | const rafRef = useRef<number | null>(null) |
| 665 | |
| 666 | const mark = MARKS.find((m) => m.id === markId) as LabMark |
| 667 | const cfg = ensureConfig(configs[markId], mark) |
| 668 | |
| 669 | // Persist a complete config for the active mark so edits always have a target |
| 670 | // — covers marks/params added during development without a full reload. |
| 671 | useEffect(() => { |
| 672 | setConfigs((prev) => { |
| 673 | const ensured = ensureConfig(prev[markId], mark) |
| 674 | return prev[markId] === ensured ? prev : { ...prev, [markId]: ensured } |
| 675 | }) |
| 676 | }, [markId, mark]) |
| 677 | |
| 678 | useEffect(() => { |
| 679 | if (!playing) return |
| 680 | const start = performance.now() |
| 681 | const loop = () => { |
| 682 | const t = (performance.now() - start) / 1000 |
| 683 | setAmt((1 - Math.cos(t * 1.4)) / 2) |
| 684 | rafRef.current = requestAnimationFrame(loop) |
| 685 | } |
| 686 | rafRef.current = requestAnimationFrame(loop) |
| 687 | return () => { |
| 688 | if (rafRef.current != null) cancelAnimationFrame(rafRef.current) |
| 689 | } |
| 690 | }, [playing]) |
| 691 | |
| 692 | const resolved = useMemo(() => { |
| 693 | const p: Record<string, number> = {} |
| 694 | for (const def of mark.params) { |
| 695 | const c = cfg.params[def.key] |
| 696 | p[def.key] = lerp(c.rest, c.hover, amt) |
| 697 | } |
| 698 | return p |
| 699 | }, [mark, cfg, amt]) |
| 700 | |
| 701 | const strokeNow = lerp(cfg.stroke.rest, cfg.stroke.hover, amt) |
| 702 | const gooNow = lerp(cfg.goo.rest, cfg.goo.hover, amt) |
| 703 | const gradCx = lerp(cfg.gradient.cx.rest, cfg.gradient.cx.hover, amt) |
| 704 | const gradCy = lerp(cfg.gradient.cy.rest, cfg.gradient.cy.hover, amt) |
| 705 | const gradR = lerp(cfg.gradient.r.rest, cfg.gradient.r.hover, amt) |
| 706 | const d = mark.build(resolved, amt, cfg.params) |
| 707 | |
| 708 | const setPair = ( |
| 709 | group: 'params' | 'stroke' | 'goo', |
| 710 | key: string, |
| 711 | side: 'rest' | 'hover', |
| 712 | v: number |
| 713 | ) => |
| 714 | setConfigs((prev) => { |
| 715 | const next = structuredClone(prev) |
| 716 | const target = group === 'params' ? next[markId].params[key] : next[markId][group] |
nothing calls this directly
no test coverage detected