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:
1 parent
bf4191956c
commit
1135bf4562
39 files changed
+1935
-87
No files matched your search
@@ -14,6 +14,7 @@ import { type FrameRect, MIN_FRAME_SIZE, normalizeFrameRect } from '../../core/m
|
||||
import { type Size, Theme, textStyle, ui, Widget, zoomFactor } from '../../ui';
|
||||
import { textureLibrary } from '../../viewport/TextureLibrary';
|
||||
import type { EditorState } from '../state';
|
||||
import { drawChecker } from './checker';
|
||||
import {
|
||||
dragFrame,
|
||||
fitView,
|
||||
@@ -33,10 +34,6 @@ import {
|
||||
const HANDLE_SIZE = 8;
|
||||
/** Pixels the pointer must travel before a press becomes a drag rather than a click. */
|
||||
const DRAG_THRESHOLD = 3;
|
||||
/** Side of one square of the transparency checkerboard, in screen pixels. */
|
||||
const CHECKER_SIZE = 8;
|
||||
const CHECKER_DARK = 0x1a1c21;
|
||||
const CHECKER_LIGHT = 0x24272e;
|
||||
/** Below this on-screen frame size the name is illegible clutter and is left out. */
|
||||
const MIN_LABEL_SIZE = 28;
|
||||
/** Inset of a frame's name plate from the frame's top-left corner, and its own padding. */
|
||||
@@ -349,28 +346,7 @@ export class TextureCanvas extends Widget {
|
||||
const origin = imageToLocal(this.view, 0, 0);
|
||||
const w = asset.width * this.view.zoom;
|
||||
const h = asset.height * this.view.zoom;
|
||||
this.checker.rect(origin.x, origin.y, w, h).fill(CHECKER_DARK);
|
||||
// Only the squares inside the widget: a 4000 px sheet zoomed in would otherwise cost tens of
|
||||
// thousands of rectangles per redraw.
|
||||
const from = (start: number, limit: number) => Math.max(0, Math.floor((limit - start) / CHECKER_SIZE));
|
||||
const x0 = from(origin.x, 0);
|
||||
const y0 = from(origin.y, 0);
|
||||
const x1 = Math.min(Math.ceil(w / CHECKER_SIZE), from(origin.x, this.w) + 1);
|
||||
const y1 = Math.min(Math.ceil(h / CHECKER_SIZE), from(origin.y, this.h) + 1);
|
||||
for (let cy = y0; cy < y1; cy++) {
|
||||
for (let cx = x0; cx < x1; cx++) {
|
||||
if ((cx + cy) % 2 === 0) continue;
|
||||
const px = origin.x + cx * CHECKER_SIZE;
|
||||
const py = origin.y + cy * CHECKER_SIZE;
|
||||
this.checker.rect(
|
||||
px,
|
||||
py,
|
||||
Math.min(CHECKER_SIZE, w - cx * CHECKER_SIZE),
|
||||
Math.min(CHECKER_SIZE, h - cy * CHECKER_SIZE),
|
||||
);
|
||||
}
|
||||
}
|
||||
this.checker.fill(CHECKER_LIGHT);
|
||||
drawChecker(this.checker, origin.x, origin.y, w, h, this.w, this.h);
|
||||
}
|
||||
|
||||
private drawFrames(asset: TextureAsset | null): void {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { Graphics } from 'pixi.js';
|
||||
|
||||
/** Side of one square of the transparency checkerboard, in screen pixels. */
|
||||
const CHECKER_SIZE = 8;
|
||||
const CHECKER_DARK = 0x1a1c21;
|
||||
const CHECKER_LIGHT = 0x24272e;
|
||||
|
||||
/**
|
||||
* The checkerboard behind a picture at (x, y, w, h). Only squares inside `0..clipW × 0..clipH` are
|
||||
* drawn: a 4000 px sheet zoomed in would otherwise cost tens of thousands of rectangles per redraw.
|
||||
*/
|
||||
export function drawChecker(
|
||||
g: Graphics,
|
||||
x: number,
|
||||
y: number,
|
||||
w: number,
|
||||
h: number,
|
||||
clipW: number,
|
||||
clipH: number,
|
||||
): void {
|
||||
if (w <= 0 || h <= 0) return;
|
||||
g.rect(x, y, w, h).fill(CHECKER_DARK);
|
||||
const from = (start: number, limit: number) => Math.max(0, Math.floor((limit - start) / CHECKER_SIZE));
|
||||
const x0 = from(x, 0);
|
||||
const y0 = from(y, 0);
|
||||
const x1 = Math.min(Math.ceil(w / CHECKER_SIZE), from(x, clipW) + 1);
|
||||
const y1 = Math.min(Math.ceil(h / CHECKER_SIZE), from(y, clipH) + 1);
|
||||
for (let cy = y0; cy < y1; cy++) {
|
||||
for (let cx = x0; cx < x1; cx++) {
|
||||
if ((cx + cy) % 2 === 0) continue;
|
||||
g.rect(
|
||||
x + cx * CHECKER_SIZE,
|
||||
y + cy * CHECKER_SIZE,
|
||||
Math.min(CHECKER_SIZE, w - cx * CHECKER_SIZE),
|
||||
Math.min(CHECKER_SIZE, h - cy * CHECKER_SIZE),
|
||||
);
|
||||
}
|
||||
}
|
||||
g.fill(CHECKER_LIGHT);
|
||||
}
|
||||
Reference in new issue
Block a user