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
{label}
);
}