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:
1 parent
94749b4f3e
commit
147f8ffbea
9 files changed
+500
-74
No files matched your search
@@ -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) => {
|
||||
|
||||
Reference in new issue
Block a user