Build your own easing curve and use it on keys and Lerp (#247)
game-maker / gate (push) Successful in 3m44s
game-maker / suite (push) Successful in 10m25s

The built-in eases cover the usual shapes, but a game often wants its
own: a coin that bounces twice, a door that overshoots and settles.
A new asset type holds such a curve, and keys and the Lerp node can
pick it instead of a built-in ease.

- New asset type "curve": points like in Vector art, x is t and y is
  the value. A new curve is the straight line from (0, 0) to (1, 1).
- One pure evaluation in src/core/model/curve.ts serves the timeline
  preview, the running game and the Lerp node.
- The curve always has one value per t. The end points stay at t 0
  and 1, points cannot pass each other, and a handle that reaches past
  its neighbour is shortened along its direction. Loading a hand-edited
  file applies the same rules.
- Curve tab: click on the line to insert a point that keeps the shape,
  click on free space to put one there, drag points and handles,
  double-click or Delete removes, Alt-click toggles round and sharp.
  Every step is one undo step.
- Presets: all 30 built-in eases plus Linear, fitted with adaptive
  subdivision. Each stays within 0.4 % of the built-in ease.
- Preview: a dot runs along the curve once a second, a second dot
  shows only the value. The Preview button stops it.
- Animation keys get "Curve" under Easing with a curve picker. Lerp
  gets "Curve" under Ease, and its result may overshoot b.
- A missing or unpicked curve runs linear. The inspector says so, the
  node shows it as a problem, and deleting a used curve warns with the
  number of keys and Lerp nodes.

Tests:
- ./check.sh: 180 files, 2612 tests, CHECK EXIT 0
- GM_STEPS=1-6,25,53: ALL PASSED, 309 checks
- New e2e module 53 with 13 checks, all with the real mouse. Expected
  values come from its own Bezier evaluation.
- Counter-checks bite: without Bezier evaluation 4 unit tests fail,
  without handle clamping 2. In the browser, a key that cannot find its
  curve, a click that sets no point and a handle drag that moves the
  point all turn checks red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-09-27 15:58:05 +02:00
1 parent 320d3e27e1
commit 393b8ec7f9
41 files changed
+1981 -67

No files matched your search

+23 -5
View File
@@ -1,7 +1,10 @@
import type { GraphNode } from '../../Graph';
import type { GraphLookup } from '../../lookup';
import type { NodeDef } from '../../NodeRegistry';
import { asNumber } from '../../values';
import { num, withDefault } from '../shared';
import { curveValue, LERP, LERP_CURVE } from '../../../model/curve';
import { textOf } from '../../../model/Properties';
/** Quadratic curves over t in 0…1, each running from 0 to 1. */
@@ -13,10 +16,12 @@ const EASES: Record<string, (t: number) => number> = {
};
export const Lerp: NodeDef = {
type: 'Lerp',
type: LERP,
label: 'Lerp',
category: 'data',
description: 'Blends from a to b as t runs from 0 to 1, along the chosen ease. t is held in 0…1.',
description:
'Blends from a to b as t runs from 0 to 1, along the chosen ease. t is held in 0…1. With Ease ' +
'set to Curve it follows the Curve asset picked below, and may overshoot b.',
inputs: [withDefault(num('a'), 0), withDefault(num('b'), 1), withDefault(num('t'), 0)],
outputs: [num('result')],
params: [
@@ -30,13 +35,26 @@ export const Lerp: NodeDef = {
{ value: 'in', label: 'In' },
{ value: 'out', label: 'Out' },
{ value: 'inOut', label: 'In-out' },
{ value: 'curve', label: 'Curve' },
],
},
{ key: LERP_CURVE, label: 'Curve', kind: 'asset', default: '', assetTypes: ['curve'] },
],
run: (_ctx, inputs, params) => {
hiddenParams: (params) => (textOf(params.ease) === 'curve' ? [] : [LERP_CURVE]),
run: (_ctx, inputs, params, _def, _entry, lookup) => {
const a = asNumber(inputs.a);
const t = Math.min(Math.max(asNumber(inputs.t), 0), 1);
const ease = EASES[textOf(params.ease)] ?? EASES.linear!;
return { outputs: { result: a + (asNumber(inputs.b) - a) * ease(t) } };
const ease = textOf(params.ease);
const points = ease === 'curve' ? lookup.curve(textOf(params[LERP_CURVE] ?? '')) : null;
const share = points ? curveValue(points, t) : (EASES[ease] ?? EASES.linear!)(t);
return { outputs: { result: a + (asNumber(inputs.b) - a) * share } };
},
};
/** Why a Lerp set to Curve runs linear instead, or null when it eases as it says. */
export function lerpProblem(node: GraphNode, lookup: GraphLookup): string | null {
if (node.type !== LERP || textOf(node.params.ease) !== 'curve') return null;
const id = textOf(node.params[LERP_CURVE] ?? '');
if (!id) return 'No curve picked – runs linear';
return lookup.curve(id) ? null : 'That curve is gone – runs linear';
}