An animation is previewed above its timeline: the root at the playhead, drawn but never written (#99)
The Animation tab shows the root asset over the timeline, with every row's value at the playhead written onto a copy of it – the sprite, text or container placed at the origin as a scene would place it, a scene as itself. Rows reach the root through that placed object and its children through the object's instance edits, the route a placed container's content takes, so the ids match what the runtime gives them. The runtime player and the preview evaluate a row through one function (trackWriteAt), relative rows included; past the end of a once-reset pass the preview shows the starting values, as the player would. Play, pause and the ruler drive the preview through the timeline's playhead; a drag pans, the wheel zooms, Centre frames it. The document is never touched and undo never hears of scrubbing. On animation finished no longer describes itself as hearing "a sprite of the owner". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
4ed2edd0f0
commit
89c46c30a0
9 files changed
+513
-13
No files matched your search
@@ -7,7 +7,7 @@ export const OnAnimationFinished: NodeDef = {
|
||||
label: 'On animation finished',
|
||||
category: 'event',
|
||||
description:
|
||||
'Runs once when a sprite of the owner reaches the end of an animation that plays once. Leave Animation at None to hear every animation.',
|
||||
'Runs once when an animation played once on the owner, or on an object below it, reaches its end. Leave Animation at None to hear every animation.',
|
||||
inputs: [],
|
||||
outputs: [exec(), obj('object')],
|
||||
params: [{ key: 'animation', label: 'Animation', kind: 'asset', default: '', assetTypes: ['animation'] }],
|
||||
|
||||
@@ -132,6 +132,23 @@ export function trackValueAt(
|
||||
return keys[keys.length - 1]!.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* What a row writes at a frame: its value, or for a relative number row that value added to `base`,
|
||||
* what the target held when the animation started. Shared by the runtime player and the tab's
|
||||
* preview, so both show the same picture. `undefined` for a row without keys.
|
||||
*/
|
||||
export function trackWriteAt(
|
||||
track: AnimationTrack,
|
||||
kind: string,
|
||||
frame: number,
|
||||
base: PropertyValue | undefined,
|
||||
): PropertyValue | undefined {
|
||||
const value = trackValueAt(track.keys, kind, frame);
|
||||
if (value === undefined) return undefined;
|
||||
const offset = track.relative && kind === 'number' && typeof base === 'number';
|
||||
return offset ? base + Number(value) : value;
|
||||
}
|
||||
|
||||
/** The kind a row's property has on an asset of that type; undefined for one the timeline cannot animate. */
|
||||
export function animatedKind(type: AssetType, key: string): string | undefined {
|
||||
if (isPositionKey(key)) return 'number';
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* What the Animation tab shows above the timeline (#99): the root asset with every row's value at
|
||||
* the playhead written over it. Nothing here touches the document – the holder returned is built
|
||||
* fresh on every call from copies, and the preview draws it the way the Scene tab draws a scene.
|
||||
*/
|
||||
import { animatedKind, frameCount, isPositionKey, playbackOf, rootObjects, trackWriteAt } from './animation';
|
||||
import type { AnimationAsset, AnimationTrack, Asset } from './Asset';
|
||||
import { type InstanceEdit, instanceChildId } from './instance';
|
||||
import { findAsset, type ObjectHolder, type Project } from './Project';
|
||||
import type { PropertyValue } from './Properties';
|
||||
import { resolveSceneObjectProps } from '../properties/resolve';
|
||||
import type { SceneObject } from './SceneObject';
|
||||
|
||||
/** The one object the preview places the root on – what a row aimed at the root itself writes to. */
|
||||
export const PREVIEW_ROOT_ID = 'preview-root';
|
||||
|
||||
/**
|
||||
* The holder the preview draws at `frame`: the scene itself for a scene root, otherwise a holder
|
||||
* of one object placing the root at the origin, its content resolved from the template like any
|
||||
* placed container's. Rows reach the root through that object and its children through the
|
||||
* object's instance edits – the same route a placed container's content takes. `undefined`
|
||||
* without a root. Past the end of a `once-reset` pass nothing is written: the player has put the
|
||||
* starting values back by then.
|
||||
*/
|
||||
export function previewHolder(
|
||||
project: Project,
|
||||
animation: AnimationAsset,
|
||||
frame: number,
|
||||
): ObjectHolder | undefined {
|
||||
const root = findAsset(project, animation.root);
|
||||
if (!root) return undefined;
|
||||
const reset = playbackOf(animation) === 'once-reset' && frame >= frameCount(animation);
|
||||
const tracks = reset ? [] : animation.tracks;
|
||||
return root.type === 'scene'
|
||||
? sceneAt(project, tracks, root, frame)
|
||||
: placedAt(project, tracks, root, frame);
|
||||
}
|
||||
|
||||
/** The id the preview draws a row's target under – for a check that wants to look at it. */
|
||||
export function previewObjectId(root: Asset | undefined, target: string): string {
|
||||
if (root?.type === 'scene') return target;
|
||||
return target === '' ? PREVIEW_ROOT_ID : instanceChildId(PREVIEW_ROOT_ID, target);
|
||||
}
|
||||
|
||||
function sceneAt(
|
||||
project: Project,
|
||||
tracks: readonly AnimationTrack[],
|
||||
root: ObjectHolder,
|
||||
frame: number,
|
||||
): ObjectHolder {
|
||||
const objects = root.objects.map((o) => ({
|
||||
...o,
|
||||
position: { ...o.position },
|
||||
overrides: { ...o.overrides },
|
||||
}));
|
||||
const byId = new Map(objects.map((o) => [o.id, o]));
|
||||
for (const track of tracks) {
|
||||
const object = byId.get(track.target);
|
||||
const asset = object && findAsset(project, object.assetId);
|
||||
const kind = asset && animatedKind(asset.type, track.property);
|
||||
if (!object || !asset || !kind) continue;
|
||||
const base = isPositionKey(track.property)
|
||||
? object.position[track.property]
|
||||
: resolveSceneObjectProps(project, asset, object)[track.property];
|
||||
const value = trackWriteAt(track, kind, frame, base);
|
||||
if (value === undefined) continue;
|
||||
if (isPositionKey(track.property)) object.position[track.property] = Number(value);
|
||||
else object.overrides[track.property] = value;
|
||||
}
|
||||
return { ...root, objects };
|
||||
}
|
||||
|
||||
function placedAt(
|
||||
project: Project,
|
||||
tracks: readonly AnimationTrack[],
|
||||
root: Asset,
|
||||
frame: number,
|
||||
): ObjectHolder {
|
||||
const placed: SceneObject = {
|
||||
id: PREVIEW_ROOT_ID,
|
||||
assetId: root.id,
|
||||
name: root.name,
|
||||
position: { x: 0, y: 0, z: 0 },
|
||||
editorOffset: { x: 0, y: 0 },
|
||||
overrides: {},
|
||||
parentId: '',
|
||||
instanceEdits: {},
|
||||
};
|
||||
const templates = rootObjects(root);
|
||||
for (const track of tracks) {
|
||||
const template = track.target === '' ? undefined : templates.find((o) => o.id === track.target);
|
||||
const aimed = track.target === '' ? root : template && findAsset(project, template.assetId);
|
||||
const kind = aimed && animatedKind(aimed.type, track.property);
|
||||
if (!aimed || !kind) continue;
|
||||
const base = template
|
||||
? baseOf(project, aimed, template, track.property)
|
||||
: baseOf(project, aimed, placed, track.property);
|
||||
const value = trackWriteAt(track, kind, frame, base);
|
||||
if (value === undefined) continue;
|
||||
if (!template) writeTo(placed, track.property, value);
|
||||
else
|
||||
writeEdit(
|
||||
(placed.instanceEdits[template.id] ??= { position: {}, overrides: {} }),
|
||||
track.property,
|
||||
value,
|
||||
);
|
||||
}
|
||||
return { id: PREVIEW_ROOT_ID, type: 'container', name: root.name, props: {}, objects: [placed] };
|
||||
}
|
||||
|
||||
/** What the target holds before the animation writes it: the zero of a relative row. */
|
||||
function baseOf(project: Project, asset: Asset, object: SceneObject, key: string): PropertyValue | undefined {
|
||||
if (isPositionKey(key)) return object.position[key];
|
||||
return resolveSceneObjectProps(project, asset, object)[key];
|
||||
}
|
||||
|
||||
function writeTo(object: SceneObject, key: string, value: PropertyValue): void {
|
||||
if (isPositionKey(key)) object.position[key] = Number(value);
|
||||
else object.overrides[key] = value;
|
||||
}
|
||||
|
||||
function writeEdit(edit: InstanceEdit, key: string, value: PropertyValue): void {
|
||||
if (isPositionKey(key)) edit.position[key] = Number(value);
|
||||
else edit.overrides[key] = value;
|
||||
}
|
||||
Reference in new issue
Block a user