import { useCallback, useRef, useState } from "react"; import { EASE_CURVES, EASE_LABELS, parseCustomEaseFromString } from "./gsapAnimationConstants"; import { roundToCenti } from "../../utils/rounding"; const PRESET_GRID_EASES = [ "ae-ease", "ae-ease-in", "ae-ease-out", "none", "power2.out", "power2.in", "back.out", "expo.out", ] as const; function MiniCurveSvg({ curve, active, }: { curve: [number, number, number, number]; active: boolean; }) { const [x1, y1, x2, y2] = curve; const s = 24; const p = 3; const g = s - p * 2; const sx = (px: number) => p + g * px; const sy = (py: number) => s - p - g * py; const d = `M${p},${s - p} C${sx(x1)},${sy(y1)} ${sx(x2)},${sy(y2)} ${s - p},${p}`; return ( ); } const EasePresetGrid = function EasePresetGrid({ currentEase, onSelect, }: { currentEase: string; onSelect: (ease: string) => void; }) { return (
{PRESET_GRID_EASES.map((name) => { const curve = EASE_CURVES[name]; if (!curve) return null; const isActive = currentEase === name; return ( ); })}
); }; const round2 = roundToCenti; export function EaseCurveSection({ ease, duration, onCustomEaseCommit, }: { ease: string; duration?: number; onCustomEaseCommit: (ease: string) => void; }) { const isCustom = ease.startsWith("custom("); const curveFromPreset = EASE_CURVES[ease]; const customPoints = isCustom ? parseCustomEaseFromString(ease) : null; const curve: [number, number, number, number] | null = isCustom && customPoints ? [customPoints.x1, customPoints.y1, customPoints.x2, customPoints.y2] : (curveFromPreset ?? null); const [draft, setDraft] = useState<[number, number, number, number] | null>(null); const [progress, setProgress] = useState(null); const draggingRef = useRef<"p1" | "p2" | null>(null); const svgRef = useRef(null); const rafRef = useRef(0); const play = useCallback(() => { const start = performance.now(); const dur = 1000; const tick = (now: number) => { const t = Math.min((now - start) / dur, 1); setProgress(t); if (t < 1) rafRef.current = requestAnimationFrame(tick); else setTimeout(() => setProgress(null), 400); }; cancelAnimationFrame(rafRef.current); rafRef.current = requestAnimationFrame(tick); }, []); const active = draft ?? curve; if (!active) return null; const [x1, y1, x2, y2] = active; const w = 200; const h = 100; const pad = 14; const gw = w - pad * 2; const gh = h - pad * 2; const toSvg = (px: number, py: number) => ({ x: pad + gw * px, y: h - pad - gh * py, }); const curvePath = `M${pad},${h - pad} C${toSvg(x1, y1).x},${toSvg(x1, y1).y} ${toSvg(x2, y2).x},${toSvg(x2, y2).y} ${w - pad},${pad}`; let dotX = pad; let dotY = h - pad; if (progress !== null) { const t = progress; const mt = 1 - t; dotX = pad + gw * (mt * mt * mt * 0 + 3 * mt * mt * t * x1 + 3 * mt * t * t * x2 + t * t * t); dotY = h - pad - gh * (mt * mt * mt * 0 + 3 * mt * mt * t * y1 + 3 * mt * t * t * y2 + t * t * t); } const handlePointerDown = (handle: "p1" | "p2", e: React.PointerEvent) => { e.preventDefault(); e.stopPropagation(); draggingRef.current = handle; (e.target as SVGElement).setPointerCapture(e.pointerId); if (!draft) setDraft([x1, y1, x2, y2]); }; const handlePointerMove = (e: React.PointerEvent) => { if (!draggingRef.current || !svgRef.current) return; e.preventDefault(); const rect = svgRef.current.getBoundingClientRect(); const sx = ((e.clientX - rect.left) / rect.width) * w; const sy = ((e.clientY - rect.top) / rect.height) * h; const px = Math.max(0, Math.min(1, (sx - pad) / gw)); const py = Math.max(-1, Math.min(2, (h - pad - sy) / gh)); const prev = draft ?? [x1, y1, x2, y2]; const next: [number, number, number, number] = draggingRef.current === "p1" ? [round2(px), round2(py), prev[2], prev[3]] : [prev[0], prev[1], round2(px), round2(py)]; setDraft(next); }; const handlePointerUp = () => { if (!draggingRef.current || !draft) return; draggingRef.current = null; const path = `M0,0 C${draft[0]},${draft[1]} ${draft[2]},${draft[3]} 1,1`; onCustomEaseCommit(`custom(${path})`); setDraft(null); }; const p1 = toSvg(x1, y1); const p2 = toSvg(x2, y2); const start = toSvg(0, 0); const end = toSvg(1, 1); const label = isCustom ? "Custom curve" : (EASE_LABELS[ease] ?? ease); return (
onCustomEaseCommit(name)} />
Speed curve
{progress !== null && } handlePointerDown("p1", e)} /> handlePointerDown("p2", e)} /> {duration != null && duration > 0 && ( <> 0s {(duration / 2).toFixed(1)}s {duration}s )}

{label}

); }