import { useEffect, useId, useRef, useState } from "react"; import { parseSpringBounce } from "@hyperframes/core/spring-ease"; import { parseWiggleEase, type WiggleEaseConfig, type WiggleType, } from "@hyperframes/core/wiggle-ease"; import { roundToCenti } from "../../utils/rounding"; import { MiniCurveSvg } from "./easeCurveSvg"; type Pts = [number, number, number, number]; const round2 = roundToCenti; const BEZIER_Y_MIN = -1; const BEZIER_Y_MAX = 2; const WIGGLE_TYPES = ["easeOut", "easeInOut", "anticipate", "uniform"] as const; const WIGGLE_DEFAULT_AMPLITUDE = { easeOut: 0.16, easeInOut: 0.08, anticipate: 0.12, uniform: 0.14, } satisfies Record; function isWiggleType(value: string): value is WiggleType { return WIGGLE_TYPES.some((type) => type === value); } function commitWiggle( onCommit: (ease: string) => void, count: number, type: WiggleType, amplitude: number, ): void { const ease = `wiggle(${count},${type},${roundToCenti(amplitude)})`; if (parseWiggleEase(ease)) onCommit(ease); } // Editable cubic-bezier control points, Figma-style ("0.33, 0, 0, 1"). export function EaseBezierField({ tuple, onCommit, }: { tuple: Pts; onCommit: (ease: string) => void; }) { const text = tuple.join(", "); const inputRef = useRef(null); const errorId = useId(); const [error, setError] = useState(null); useEffect(() => { if (inputRef.current) inputRef.current.value = text; setError(null); }, [text]); const commit = (raw: string) => { const tokens = raw.trim().split(/[\s,]+/); const nums = tokens.map(Number); if (tokens.length !== 4 || nums.some((value) => !Number.isFinite(value))) { setError("Enter four finite numbers"); return; } const [x1, y1, x2, y2] = nums as [number, number, number, number]; if (y1 < BEZIER_Y_MIN || y1 > BEZIER_Y_MAX || y2 < BEZIER_Y_MIN || y2 > BEZIER_Y_MAX) { setError(`Y values must be between ${BEZIER_Y_MIN} and ${BEZIER_Y_MAX}`); return; } const cx = (v: number) => Math.max(0, Math.min(1, v)); setError(null); onCommit(`custom(M0,0 C${round2(cx(x1))},${round2(y1)} ${round2(cx(x2))},${round2(y2)} 1,1)`); }; return (
setError(null)} onKeyDown={(event) => { if (event.key === "Enter") event.currentTarget.blur(); if (event.key === "Escape") { event.currentTarget.value = text; setError(null); } }} onBlur={(event) => commit(event.currentTarget.value)} className={`w-full rounded border bg-black/20 px-1.5 py-1 font-mono text-[10px] text-neutral-300 outline-none ${ error ? "border-red-500/70 focus:border-red-400" : "border-white/10 focus:border-panel-accent/50" }`} />

{error ?? "Valid bezier values"}

); } function NumericCommitInput({ label, value, min, max, step, className, onCommit, }: { label: string; value: number; min: number; max?: number; step: number; className: string; onCommit: (value: number) => void; }) { const [draft, setDraft] = useState(String(value)); useEffect(() => setDraft(String(value)), [value]); const commit = () => { if (draft.trim() === "") { setDraft(String(value)); return; } const next = Number(draft); if (!Number.isFinite(next) || next < min || (max !== undefined && next > max)) { setDraft(String(value)); return; } onCommit(next); }; return ( setDraft(event.currentTarget.value)} onBlur={commit} onKeyDown={(event) => { if (event.key === "Enter") event.currentTarget.blur(); if (event.key === "Escape") { setDraft(String(value)); } }} className={className} /> ); } export function SpringBounceField({ springBounce, onCommit, }: { springBounce: number; onCommit: (ease: string) => void; }) { return (
{ const bounce = parseSpringBounce(`spring(${value})`); if (bounce !== null) onCommit(`spring(${round2(bounce)})`); }} className="w-16 rounded border border-white/10 bg-black/20 px-1.5 py-1 font-mono text-[10px] text-neutral-300 outline-none focus:border-panel-accent/50" />
); } export function WiggleField({ config, onCommit, }: { config: WiggleEaseConfig; onCommit: (ease: string) => void; }) { const amplitude = config.amplitude ?? WIGGLE_DEFAULT_AMPLITUDE[config.type]; return (
commitWiggle(onCommit, value, config.type, amplitude)} className="w-14 rounded border border-white/10 bg-black/20 px-1.5 py-1 font-mono text-[10px] text-neutral-300 outline-none focus:border-panel-accent/50" />
commitWiggle(onCommit, config.wiggles, config.type, value)} className="w-16 rounded border border-white/10 bg-black/20 px-1.5 py-1 font-mono text-[10px] text-neutral-300 outline-none focus:border-panel-accent/50" />
); }