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

+3 -1
View File
@@ -12,6 +12,7 @@ import {
trackWriteAt,
} from '../../core/model/animation';
import type { AnimationAsset, AnimationTrack } from '../../core/model/Asset';
import { curveLookup } from '../../core/model/curve';
import { instanceChildId, splitInstanceId } from '../../core/model/instance';
import { findAsset, findAssetOf } from '../../core/model/Project';
import type { PropertyValue } from '../../core/model/Properties';
@@ -52,6 +53,7 @@ export class AnimationSystem implements System {
running.sort((a, b) => a.player.order - b.player.order);
// What the players before this one wrote this tick – a blend mixes from that value, live.
const written = new Set<string>();
const curves = curveLookup(world.project);
for (const { entity, player } of running) {
const asset = findAssetOf(world.project, player.animationId, 'animation');
if (!asset) {
@@ -81,7 +83,7 @@ export class AnimationSystem implements System {
const fade = player.fade;
const fadeShare = fade ? blendShare(fade) : 0;
for (const b of bindings) {
let value = trackWriteAt(b.track, b.def, head.frame, player.bases[b.base], head.reverse);
let value = trackWriteAt(b.track, b.def, head.frame, player.bases[b.base], head.reverse, curves);
if (value === undefined) continue;
const below = written.has(b.base) ? b.read() : undefined;
if (share < 1 && blends(b.def.kind)) {