Files
feuerwerk/src/editor/animation/PreviewCanvas.ts
T
2026-09-17 14:19:53 +02:00

85 lines
3.2 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 type { FederatedPointerEvent } from 'pixi.js';
import { previewHolder } from '../../core/model/animationPreview';
import type { AnimationAsset } from '../../core/model/Asset';
import { findAssetOf } from '../../core/model/Project';
import type { EditorState } from '../state';
import { Theme } from '../../ui';
import { collectHolderItems } from '../../viewport/items';
import { FreeViewport } from '../viewport/FreeViewport';
import type { Frame } from '../viewport/WorldViewport';
/**
* The picture above the timeline (#99): the animation's root with every row's value at the
* playhead written over it, drawn like the Scene tab draws a scene but never edited – a drag pans,
* the wheel zooms, nothing is picked. What it draws comes from `previewHolder`, so the document
* stays untouched and undo never hears of it.
*/
export class PreviewCanvas extends FreeViewport {
private assetId = '';
/** Frames the content on the first draw with a size – a fresh subject gets a whole view. */
private fitPending = false;
constructor(
state: EditorState,
/** Where the timeline's playhead stands, in frames, and whether it runs ping-pong's way back. */
private readonly playhead: () => { frame: number; reverse: boolean },
) {
super(state);
this.on('pointerdown', (e: FederatedPointerEvent) => {
if (e.button === 0 || e.button === 1) this.startPan(e);
});
this.on('globalpointermove', (e: FederatedPointerEvent) => this.panTo(e));
this.on('pointerup', () => this.endPan());
this.on('pointerupoutside', () => this.endPan());
state.on('load', () => this.show(''));
}
show(assetId: string): void {
if (this.assetId === assetId) return;
this.assetId = assetId;
this.fitPending = true;
this.redraw();
}
override fit(): void {
this.fitPending = false;
super.fit();
}
/** The values as drawn, for a check: an object's position and property bag at the playhead. */
itemDrawn(objectId: string): { x: number; y: number; props: Record<string, unknown> } | null {
const item = this.lastItems.find((i) => i.id === objectId);
return item ? { x: item.x, y: item.y, props: item.props } : null;
}
/** Skipped while the tab is hidden, like the Game tab: a frame costs a collect of the root. */
override redraw(): void {
if (!this.visible) return;
if (this.fitPending && this.w > 0 && this.h > 0) {
this.fit();
return;
}
super.redraw();
}
protected override collectFrame(): Frame {
const asset = this.asset();
const head = this.playhead();
const holder = asset && previewHolder(this.state.project, asset, head.frame, head.reverse);
return collectHolderItems(this.state.project, holder, Theme.colors.viewport);
}
protected override get baseColor(): number {
return Theme.colors.viewport;
}
private asset(): AnimationAsset | undefined {
return this.assetId ? findAssetOf(this.state.project, this.assetId, 'animation') : undefined;
}
/** A scene root paints its own background; anything placed on the preview's holder has none. */
protected override paintsBackground(): boolean {
return findAssetOf(this.state.project, this.asset()?.root ?? '', 'scene') !== undefined;
}
}