Files
feuerwerk/src/editor/animation/AnimationEditor.ts
T
rootandClaude Opus 5 1135bf4562 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>
2026-09-11 23:52:34 +02:00

71 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { animationDuration, fpsOf, framesOfTexture } from '../../core/model/animation';
import type { AnimationAsset, Asset, TextureAsset } from '../../core/model/Asset';
import { pictureChoices } from '../../core/properties/textureRef';
import { Button, Dropdown, formatNumber, Menu, type MenuItem } from '../../ui';
import { AssetTabEditor } from '../AssetTabEditor';
import type { EditorState } from '../state';
import { AnimationCanvas } from './AnimationCanvas';
/** Room kept for the "N frames · 10 fps · 0.8 s" readout. */
const INFO_WIDTH = 170;
export class AnimationEditor extends AssetTabEditor<AnimationAsset, AnimationCanvas> {
/** Public for the check scripts. */
readonly playButton: Button;
readonly addButton: Dropdown;
constructor(state: EditorState) {
super(state, { noun: 'Animation', infoWidth: INFO_WIDTH });
this.playButton = new Button('❚❚', { width: 36, onClick: () => this.canvas.togglePlaying() });
this.addButton = new Dropdown('+ Frames', { onClick: () => this.openAddMenu() });
this.mount(new AnimationCanvas(state), [this.playButton, this.addButton]);
this.canvas.onPlayingChange = () => this.syncExtras(this.asset());
}
/** Only called while the tab is visible: the preview has no clock of its own. */
tick(dt: number): void {
this.canvas.tick(dt);
}
protected override selectedId(): string {
return this.state.selectedAnimationId ?? '';
}
protected override isMine(asset: Asset): asset is AnimationAsset {
return asset.type === 'animation';
}
protected override infoText(asset: AnimationAsset): string {
const count = asset.frames.length;
const seconds = formatNumber(animationDuration(asset), 1);
const fps = Number(fpsOf(asset).toFixed(2));
return `${count} ${count === 1 ? 'frame' : 'frames'} · ${fps} fps · ${seconds} s`;
}
protected override syncExtras(asset: AnimationAsset | null): void {
this.playButton.setText(this.canvas.playing ? '❚❚' : '▶');
this.playButton.setEnabled(Boolean(asset && asset.frames.length > 0));
this.addButton.setEnabled(asset !== null);
}
/** Each sheet offers all its frames at once before the single pictures. */
private openAddMenu(): void {
const items: MenuItem[] = [];
const sheets = new Set<TextureAsset>();
for (const choice of pictureChoices(this.state.project)) {
const sheet = choice.asset as TextureAsset;
if (!sheets.has(sheet) && sheet.frames.length > 1) {
sheets.add(sheet);
items.push({
label: `${sheet.name} – all ${sheet.frames.length} frames`,
onSelect: () => this.canvas.addFrames(framesOfTexture(sheet)),
});
}
items.push({ label: choice.label, onSelect: () => this.canvas.addFrames([choice.id]) });
}
if (items.length === 0)
items.push({ label: 'No textures yet', disabled: true, onSelect: () => undefined });
new Menu(items, { searchable: true }).openBelow(this.addButton);
}
}