Play sprite animations from an animation asset worn through texture

An animation asset is a list of pictures (whole textures or frames) with
frames per second and Loop/Once/Ping-pong playback. A sprite wears it
through its texture property, so the picker, instance overrides and the
Properties node work unchanged; there is no dedicated play node.

A different animation starts from its first frame, the same one set again
runs on, so a graph may choose Run or Idle every frame. The clock lives on
the entity, not in props, which the world writes back as overrides. The
AnimationSystem ticks before behaviors, and On animation finished fires
once when a Once animation reaches its end.

The Animation tab plays a preview above a frame strip: reorder by dragging,
Delete, duplicate, + Frames, pause/resume, and wheel zoom about the pointer
with pan and Fit. Scene and Container tabs show the first frame; edit mode
has no clock. Right-clicking a texture creates an animation from its frames.

Button now measures its label with the bold font it draws in, which cut
the Animation tab and red dialog buttons short.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-11 23:52:34 +02:00
1 parent bf4191956c
commit 1135bf4562
39 files changed
+1935 -87

No files matched your search

+19
View File
@@ -139,6 +139,17 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
walk(app.stage);
return out;
};
/** What the last visible button labelled `text` draws – shorter when it was truncated. */
const shownLabel = (text: string): string | null => {
let shown: string | null = null;
const walk = (node: Container): void => {
if (!node.visible) return;
if (node instanceof Button && node.text === text) shown = node.shownText;
for (const child of node.children) walk(child);
};
walk(app.stage);
return shown;
};
/** Screen rectangle of the open popup (the backdrop is not a widget), or null when none is. */
const overlayRect = (): { x: number; y: number; w: number; h: number } | null => {
const popups = ui().overlay.children.filter((node): node is Widget => node instanceof Widget);
@@ -168,6 +179,7 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
overlayRect,
textOverflows,
labelsDropped,
shownLabel,
viewportCenter: () => centreOf(viewport),
viewportInfo: () => viewport.debugInfo(),
/** Where a dragged asset would land in the Scene tab, and why not when it may not. */
@@ -228,6 +240,13 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
/** Where an image pixel of the shown sheet sits on screen (frames are dragged in image coordinates). */
pointAt: (x: number, y: number) => textureEditor.imageToScreen(x, y),
},
animation: {
/** Frames, the one on screen, play state and where the preview picture is drawn. */
current: () => editor.animationEditor.canvas.debugInfo(),
tileCentre: (index: number) => editor.animationEditor.canvas.tileCentre(index),
playButton: () => centreOf(editor.animationEditor.playButton),
playLabel: () => editor.animationEditor.playButton.text,
},
container: {
current: () => containerEditor.currentAssetId,
pickerWidth: () => containerEditor.picker.w,