Let animation keys ease with Sine to Bounce in, out or in-out

A key could only ease quadratically, which makes quick moves like a
sword swing feel soft. Animators know the Penner curves from CSS,
GSAP and Unity, so a key now offers all of them.

- Ten curves (Sine, Quad, Cubic, Quart, Quint, Expo, Circ, Back,
  Elastic, Bounce), each in, out and in-out, next to Linear and Step.
- The inspector shows two fields instead of one long list: Easing,
  and Direction only when a curve is picked. Switching the curve
  keeps the direction. Coming from Linear or Step starts at in-out.
- Old projects keep working. ease-in/out/in-out now read as Quad and
  are still saved under that name. New curves save as cubic-out,
  back-in-out and so on.
- Back and Elastic overshoot on purpose. A number property with min
  or max is clamped in play and preview, so a Back in from scale 0
  does not flip the sprite. trackWriteAt now takes the property
  definition, and animatedKind* is renamed to animatedDef*.
- The timeline marker still shows only the direction.
- In-out is built for every curve from its in-curve and its mirror.
  easings.net uses own constants for Back and Elastic in-out, so our
  Back in-out winds up about 5 % instead of 10 %.

Tests:
- ./check.sh: 165 files, 2413 tests, CHECK EXIT 0.
- 12 new unit tests in tests/core/easing-curves.test.ts: round trip
  of all 30 values, start/end/midpoint of every curve, reference
  values, only Back and Elastic leave 0..1, clamping in play and
  preview, unknown values are rejected.
- e2e 25: ALL PASSED, 28 checks. New 25b-ii (3 checks) clicks both
  fields and undoes the four choices. Check sum 1393 -> 1396.
- Counter-checks, each fails its test: no clamping (2), Quad saved
  as quad-* (2), mirrored in-out wrong (2), curve lookup with `in`
  instead of Object.hasOwn (1), no Direction field (e2e 25).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-09-25 22:02:23 +02:00
1 parent 94749b4f3e
commit 147f8ffbea
9 files changed
+500 -74

No files matched your search

+67
View File
@@ -122,6 +122,73 @@ export async function animation(t) {
info,
);
// 25b-ii. Easing is two fields: the curve, and for a curve its direction. Each pick is a step,
// all three are undone here so 25h counts only its own.
const easingOf = async () => (await keysOf()).find((k) => k.frame === 10)?.interpolation ?? 'linear';
const pick = async (field, label) => {
const row = await json(`__gm.inspectorField(${JSON.stringify(field)})`);
await click(row.x, row.y);
await sleep(100);
const rows = await json('__gm.menuRows()');
const entry = await t.locate(label, true);
await click(entry.x, entry.y);
await settle();
return rows;
};
const shapes = await pick('Easing', 'Cubic');
const cubicDirection = await json("__gm.inspectorField('Direction')");
check(
'the Easing field lists every curve, and a curve brings the Direction field, eased both ways',
JSON.stringify(shapes) ===
JSON.stringify([
'Linear',
'Step',
'Sine',
'Quad',
'Cubic',
'Quart',
'Quint',
'Expo',
'Circ',
'Back',
'Elastic',
'Bounce',
]) &&
(await easingOf()) === 'cubic-in-out' &&
Boolean(cubicDirection),
JSON.stringify({ shapes, easing: await easingOf(), cubicDirection }),
);
const directions = await pick('Direction', 'Out');
await shot('25-animation-easing');
const outward = await easingOf();
await pick('Easing', 'Back');
const back = await easingOf();
check(
'Direction offers In, Out and In-out, and a new curve keeps the direction picked',
JSON.stringify(directions) === JSON.stringify(['In', 'Out', 'In-out']) &&
outward === 'cubic-out' &&
back === 'back-out',
JSON.stringify({ directions, outward, back }),
);
await pick('Easing', 'Linear');
const linearDirection = await json("__gm.inspectorField('Direction')");
const linear = await easingOf();
await focusTimeline();
await undo();
const backAgain = await easingOf();
for (let i = 0; i < 3; i++) await undo();
const easingUndone = await easingOf();
const keptValue = (await keysOf()).find((k) => k.frame === 10)?.value;
check(
'linear drops the Direction field and the stored easing, and undo takes the four picks back one by one',
!linearDirection &&
linear === 'linear' &&
backAgain === 'back-out' &&
easingUndone === 'linear' &&
keptValue === 100,
JSON.stringify({ linearDirection, linear, backAgain, easingUndone, keptValue }),
);
// 25c. The ruler scrubs, and the preview draws the child at the playhead. Framed at the far end,
// so both ends and the middle are on screen together.
const scrubTo = async (frame) => {