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
@@ -1,8 +1,8 @@
|
||||
import { isHandleEnabled } from '../../core/graph';
|
||||
import {
|
||||
type AnimationPlayer,
|
||||
animatedKind,
|
||||
animatedKindOf,
|
||||
animatedDef,
|
||||
animatedDefOf,
|
||||
animationPlayheadAt,
|
||||
blendShare,
|
||||
blends,
|
||||
@@ -15,6 +15,7 @@ import type { AnimationAsset, AnimationTrack } from '../../core/model/Asset';
|
||||
import { instanceChildId, splitInstanceId } from '../../core/model/instance';
|
||||
import { findAsset, findAssetOf } from '../../core/model/Project';
|
||||
import type { PropertyValue } from '../../core/model/Properties';
|
||||
import type { PropertyDef } from '../../core/properties/PropertySchema';
|
||||
import type { Entity } from '../Entity';
|
||||
import type { World } from '../World';
|
||||
import type { System } from './System';
|
||||
@@ -28,7 +29,7 @@ export interface AnimationFinish {
|
||||
/** A row that reaches something this tick: how to read and write it, and the kind it blends as. */
|
||||
interface Binding {
|
||||
track: AnimationTrack;
|
||||
kind: string;
|
||||
def: PropertyDef;
|
||||
/** Where the player keeps the starting value: `<entity id> <property>`, or `scene <property>`. */
|
||||
base: string;
|
||||
read(): PropertyValue;
|
||||
@@ -80,16 +81,17 @@ 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.kind, head.frame, player.bases[b.base], head.reverse);
|
||||
let value = trackWriteAt(b.track, b.def, head.frame, player.bases[b.base], head.reverse);
|
||||
if (value === undefined) continue;
|
||||
const below = written.has(b.base) ? b.read() : undefined;
|
||||
if (share < 1 && blends(b.kind)) {
|
||||
if (share < 1 && blends(b.def.kind)) {
|
||||
const from = below ?? blend?.from?.[b.base];
|
||||
if (from !== undefined) value = blendValue(b.kind, from, value, share);
|
||||
if (from !== undefined) value = blendValue(b.def.kind, from, value, share);
|
||||
}
|
||||
if (fade) {
|
||||
const back = below ?? player.bases[b.base];
|
||||
if (back !== undefined) value = blends(b.kind) ? blendValue(b.kind, value, back, fadeShare) : back;
|
||||
if (back !== undefined)
|
||||
value = blends(b.def.kind) ? blendValue(b.def.kind, value, back, fadeShare) : back;
|
||||
}
|
||||
b.write(value);
|
||||
written.add(b.base);
|
||||
@@ -139,11 +141,11 @@ function bind(world: World, asset: AnimationAsset, owner: Entity): Binding[] {
|
||||
for (const track of asset.tracks) {
|
||||
const key = track.property;
|
||||
if (root.type === 'scene' && track.target === '') {
|
||||
const kind = world.sceneId === root.id ? animatedKind('scene', key) : undefined;
|
||||
if (kind)
|
||||
const def = world.sceneId === root.id ? animatedDef('scene', key) : undefined;
|
||||
if (def)
|
||||
bindings.push({
|
||||
track,
|
||||
kind,
|
||||
def,
|
||||
base: `scene ${key}`,
|
||||
read: () => world.sceneProps[key] ?? 0,
|
||||
write: (value) => {
|
||||
@@ -157,12 +159,12 @@ function bind(world: World, asset: AnimationAsset, owner: Entity): Binding[] {
|
||||
entity = world.sceneId === root.id ? world.findByObjectId(track.target) : undefined;
|
||||
else if (track.target === '') entity = owner;
|
||||
else entity = world.findByObjectId(childId(owner.objectId, track.target));
|
||||
const kind = entity && kindOn(world, entity, key);
|
||||
if (entity && kind) {
|
||||
const def = entity && defOn(world, entity, key);
|
||||
if (entity && def) {
|
||||
const aimed = entity;
|
||||
bindings.push({
|
||||
track,
|
||||
kind,
|
||||
def,
|
||||
base: `${aimed.id} ${key}`,
|
||||
read: () => read(aimed, key),
|
||||
write: (value) => write(aimed, key, value),
|
||||
@@ -173,12 +175,12 @@ function bind(world: World, asset: AnimationAsset, owner: Entity): Binding[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* The kind `key` has on the entity as it is now: its bag names the shader it wears, and swapping that
|
||||
* The property `key` is on the entity as it is now: its bag names the shader it wears, and swapping that
|
||||
* shader at runtime takes the parameter rows with it (#104).
|
||||
*/
|
||||
function kindOn(world: World, entity: Entity, key: string): string | undefined {
|
||||
function defOn(world: World, entity: Entity, key: string): PropertyDef | undefined {
|
||||
const asset = findAsset(world.project, entity.assetId);
|
||||
return asset && animatedKindOf(world.project, asset, entity.props, key);
|
||||
return asset && animatedDefOf(world.project, asset, entity.props, key);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user