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

+6 -6
View File
@@ -5,7 +5,7 @@
*/
import {
animatableDefs,
animatedKindOf,
animatedDefOf,
frameCount,
isPositionRow,
PLACED_ROOT_ID,
@@ -72,7 +72,7 @@ function placedHolder(root: Asset): ObjectHolder {
function writeScene(holder: ObjectHolder, track: AnimationTrack, frame: number, reverse: boolean): void {
const def = animatableDefs('scene').find((d) => d.key === track.property);
if (!def) return;
const value = trackWriteAt(track, def.kind, frame, holder.props[track.property] ?? def.default, reverse);
const value = trackWriteAt(track, def, frame, holder.props[track.property] ?? def.default, reverse);
if (value !== undefined) holder.props[track.property] = value;
}
@@ -89,14 +89,14 @@ function writeObject(
if (!ref || !asset) return;
// Resolved, so a shader worn through an override or an instance edit brings its parameters (#104).
const props = resolveSceneObjectProps(project, asset, ref.object);
const kind = animatedKindOf(project, asset, props, track.property);
if (!kind) return;
const def = animatedDefOf(project, asset, props, track.property);
if (!def) return;
const key = track.property;
if (isPositionRow(asset.type, key)) {
const value = trackWriteAt(track, kind, frame, ref.object.position[key], reverse);
const value = trackWriteAt(track, def, frame, ref.object.position[key], reverse);
if (value !== undefined) ref.setPosition({ ...ref.object.position, [key]: Number(value) });
return;
}
const value = trackWriteAt(track, kind, frame, props[key], reverse);
const value = trackWriteAt(track, def, frame, props[key], reverse);
if (value !== undefined) ref.setOverride(key, value);
}