Browse Source

fix(42-03): correct easing bezier values and wire up All Easing Functions

All Easing Functions presets were not updating the editor because
handleSelectEasing fell back to the same DEFAULT_CUSTOM_BEZIER for every
non-preset easing. Added EASING_BEZIER_MAP with correct cubic-bezier
values from easings.net and a getEasingBezier() lookup. Also corrected
three PRESET_BEZIERS entries (easeInOutExpo, easeInOutCubic, easeInOutSine)
that diverged from the easings.net source.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 5 months ago
parent
commit
53a099a307
  1. 40
      src/components/nodes/EaseCurveNode.tsx
  2. 38
      src/lib/easing-presets.ts

40
src/components/nodes/EaseCurveNode.tsx

@ -9,10 +9,9 @@ import { EaseCurveNodeData } from "@/types";
import { checkEncoderSupport } from "@/hooks/useStitchVideos";
import { CubicBezierEditor } from "@/components/CubicBezierEditor";
import {
PRESET_BEZIERS,
EASING_PRESETS,
DEFAULT_CUSTOM_BEZIER,
getPresetBezier,
getEasingBezier,
} from "@/lib/easing-presets";
import { getAllEasingNames, getEasingFunction } from "@/lib/easing-functions";
@ -117,13 +116,9 @@ export function EaseCurveNode({ id, data, selected }: NodeProps<EaseCurveNodeTyp
const handleSelectEasing = useCallback(
(name: string) => {
// For presets in PRESET_BEZIERS, use those handles. For others, use DEFAULT_CUSTOM_BEZIER.
const handles = PRESET_NAMES.has(name as any)
? getPresetBezier(name)
: ([...DEFAULT_CUSTOM_BEZIER] as [number, number, number, number]);
updateNodeData(id, {
easingPreset: name,
bezierHandles: handles,
bezierHandles: getEasingBezier(name),
});
setShowPresets(false);
},
@ -262,7 +257,7 @@ export function EaseCurveNode({ id, data, selected }: NodeProps<EaseCurveNodeTyp
<button
className={`px-3 py-1.5 text-xs font-medium transition-colors ${
activeTab === "editor"
? "text-amber-400 border-b-2 border-amber-400"
? "text-lime-300 border-b-2 border-lime-300"
: "text-neutral-400 hover:text-neutral-300"
}`}
onClick={() => setActiveTab("editor")}
@ -272,7 +267,7 @@ export function EaseCurveNode({ id, data, selected }: NodeProps<EaseCurveNodeTyp
<button
className={`px-3 py-1.5 text-xs font-medium transition-colors ${
activeTab === "video"
? "text-amber-400 border-b-2 border-amber-400"
? "text-lime-300 border-b-2 border-lime-300"
: "text-neutral-400 hover:text-neutral-300"
}`}
onClick={() => setActiveTab("video")}
@ -284,8 +279,8 @@ export function EaseCurveNode({ id, data, selected }: NodeProps<EaseCurveNodeTyp
{/* Tab content */}
{activeTab === "editor" && (
<div className="flex-1 flex flex-col min-h-0 gap-2">
{/* Bezier curve editor */}
<div className="mx-auto w-full" style={{ maxWidth: 260 }}>
{/* Bezier curve editor - fills available width */}
<div className="flex-1 min-h-0 px-2">
<CubicBezierEditor
value={nodeData.bezierHandles}
onChange={handleBezierChange}
@ -296,8 +291,8 @@ export function EaseCurveNode({ id, data, selected }: NodeProps<EaseCurveNodeTyp
{/* Preset label */}
{nodeData.easingPreset && (
<div className="text-center">
<span className="text-[10px] text-amber-400/70 font-medium">
<div className="text-center -mt-1">
<span className="text-[10px] text-lime-300/70 font-medium">
{nodeData.easingPreset}
</span>
</div>
@ -347,7 +342,7 @@ export function EaseCurveNode({ id, data, selected }: NodeProps<EaseCurveNodeTyp
key={name}
className={`nodrag nopan flex flex-col items-center gap-0.5 p-1 rounded transition-colors ${
isActive
? "bg-amber-500/20 border border-amber-500/40"
? "bg-lime-300/20 border border-lime-300/40"
: "hover:bg-neutral-700 border border-transparent"
}`}
onClick={() => handleSelectPreset(name)}
@ -360,7 +355,7 @@ export function EaseCurveNode({ id, data, selected }: NodeProps<EaseCurveNodeTyp
<polyline
points={thumb.polyline}
fill="none"
stroke={isActive ? "#f59e0b" : "rgba(255,255,255,0.5)"}
stroke={isActive ? "#bef264" : "rgba(255,255,255,0.5)"}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
@ -389,7 +384,7 @@ export function EaseCurveNode({ id, data, selected }: NodeProps<EaseCurveNodeTyp
key={name}
className={`nodrag nopan flex flex-col items-center gap-0.5 p-1 rounded transition-colors ${
isActive
? "bg-amber-500/20 border border-amber-500/40"
? "bg-lime-300/20 border border-lime-300/40"
: "hover:bg-neutral-700 border border-transparent"
}`}
onClick={() => handleSelectEasing(name)}
@ -401,7 +396,7 @@ export function EaseCurveNode({ id, data, selected }: NodeProps<EaseCurveNodeTyp
<polyline
points={polyline}
fill="none"
stroke={isActive ? "#f59e0b" : "rgba(255,255,255,0.5)"}
stroke={isActive ? "#bef264" : "rgba(255,255,255,0.5)"}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
@ -419,6 +414,17 @@ export function EaseCurveNode({ id, data, selected }: NodeProps<EaseCurveNodeTyp
)}
</div>
</div>
{/* Apply button */}
<div className="px-2 pb-1">
<button
className="nodrag nopan px-3 py-1.5 bg-lime-300/15 hover:bg-lime-300/25 border border-lime-300/30 rounded text-xs text-lime-300 font-medium transition-colors disabled:opacity-40 disabled:pointer-events-none"
onClick={handleRun}
disabled={isRunning || nodeData.status === "loading"}
>
Apply
</button>
</div>
</div>
)}

38
src/lib/easing-presets.ts

@ -2,10 +2,10 @@ export const DEFAULT_CUSTOM_BEZIER: [number, number, number, number] = [0.42, 0,
export const PRESET_BEZIERS = {
easeInExpoOutCubic: [0.85, 0, 0.15, 1],
easeInOutExpo: [1, 0, 0, 1],
easeInOutExpo: [0.87, 0, 0.13, 1],
easeInQuartOutQuad: [0.8, 0, 0.2, 1],
easeInOutCubic: [0.645, 0.045, 0.355, 1],
easeInOutSine: [0.445, 0.05, 0.55, 0.95],
easeInOutCubic: [0.65, 0, 0.35, 1],
easeInOutSine: [0.37, 0, 0.63, 1],
} as const satisfies Record<string, readonly [number, number, number, number]>;
export type EasingPresetName = keyof typeof PRESET_BEZIERS;
@ -14,8 +14,40 @@ export const EASING_PRESETS: EasingPresetName[] = [
'easeInExpoOutCubic', 'easeInOutExpo', 'easeInQuartOutQuad', 'easeInOutCubic', 'easeInOutSine',
];
// Cubic-bezier values for standard easings from easings.net (ai/easings.net).
// Hybrid easings (easeInExpoOutCubic, easeInQuartOutQuad) are approximations
// since asymmetric compositions can't be exactly represented by a single cubic bezier.
const EASING_BEZIER_MAP: Record<string, readonly [number, number, number, number]> = {
...PRESET_BEZIERS,
linear: [0, 0, 1, 1],
easeInSine: [0.12, 0, 0.39, 0],
easeOutSine: [0.61, 1, 0.88, 1],
easeInQuad: [0.11, 0, 0.5, 0],
easeOutQuad: [0.5, 1, 0.89, 1],
easeInOutQuad: [0.45, 0, 0.55, 1],
easeInCubic: [0.32, 0, 0.67, 0],
easeOutCubic: [0.33, 1, 0.68, 1],
easeInQuart: [0.5, 0, 0.75, 0],
easeOutQuart: [0.25, 1, 0.5, 1],
easeInOutQuart: [0.76, 0, 0.24, 1],
easeInQuint: [0.64, 0, 0.78, 0],
easeOutQuint: [0.22, 1, 0.36, 1],
easeInOutQuint: [0.83, 0, 0.17, 1],
easeInExpo: [0.7, 0, 0.84, 0],
easeOutExpo: [0.16, 1, 0.3, 1],
easeInCirc: [0.55, 0, 1, 0.45],
easeOutCirc: [0, 0.55, 0.45, 1],
easeInOutCirc: [0.85, 0, 0.15, 1],
};
export function getPresetBezier(preset?: string | null): [number, number, number, number] {
const handles = preset ? PRESET_BEZIERS[preset as EasingPresetName] : null;
const source = handles ?? DEFAULT_CUSTOM_BEZIER;
return [...source] as [number, number, number, number];
}
export function getEasingBezier(name?: string | null): [number, number, number, number] {
const handles = name ? EASING_BEZIER_MAP[name] : null;
const source = handles ?? DEFAULT_CUSTOM_BEZIER;
return [...source] as [number, number, number, number];
}

Loading…
Cancel
Save