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 { /** 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(); 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); } }