Files
feuerwerk/src/core/properties/aspect.ts
T
adminandClaude Opus 5 193378e6a9 An animation is a timeline of rows over one asset, played on an object rather than worn as a picture (#97)
Part (a) of the animation rework: model, commands, saving, runtime and nodes.

- AnimationAsset holds a root (sprite, text, container or scene) and rows; a row keys one
  property of the root or of an object inside it, keys sit on whole frames.
- The last key decides the length (its frame counts as a frame of its own). The playhead runs
  between frames, so numbers and colors blend every tick even at 10 fps; everything else holds.
- Playback gains `once-reset`, which puts back what was there before the start. A row can be
  relative, adding to where its target stood.
- Play / Pause / Stop animation act on an object; each instance plays on its own, the later
  start wins on a shared property, and a behavior writing the same property still writes last.
- A sprite no longer wears an animation through `texture`. "Create animation from texture"
  builds a sprite plus a texture row keyed frame by frame instead.
- The Animation tab shows a placeholder until the timeline editor (part b) lands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 20:02:25 +02:00

66 lines
2.6 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 { PropertyBag } from '../model/Properties';
import type { Project } from '../model/Project';
import { resolveTexture, type TextureRef } from '../model/texture';
export type SizeAxis = 'width' | 'height';
export interface PixelSize {
w: number;
h: number;
}
/**
* Pixel size of the picture a sprite wears. Null while that
* is not known: an upload still running has no measured size yet, and a ratio taken from 0 × 0
* would be invented.
*/
export function texturePixelSize(project: Project, props: PropertyBag): PixelSize | null {
const ref = resolveTexture(project, props);
return ref ? pictureSize(ref) : null;
}
/** A frame's rectangle, else the whole image. */
export function pictureSize(ref: TextureRef): PixelSize | null {
const w = ref.rect ? ref.rect.w : ref.sourceWidth;
const h = ref.rect ? ref.rect.h : ref.sourceHeight;
return w > 0 && h > 0 ? { w, h } : null;
}
/** A sprite wearing the texture at its own pixel size – or at the default size while that is unknown. */
export function spriteFromTexture(project: Project, textureId: string): PropertyBag {
const props: PropertyBag = { texture: textureId };
const size = texturePixelSize(project, props);
return size ? { ...props, width: size.w, height: size.h } : props;
}
/**
* What `axis` has to become for the sprite to carry the picture's proportions, leaving the
* other side where it is. Null when the other side is not a usable number.
*/
export function fitAxisToAspect(size: PixelSize, props: PropertyBag, axis: SizeAxis): number | null {
const other = Number(props[axis === 'width' ? 'height' : 'width']);
if (!Number.isFinite(other) || other <= 0) return null;
const fitted = axis === 'width' ? (other * size.w) / size.h : (other * size.h) / size.w;
return Number.isFinite(fitted) && fitted > 0 ? fitted : null;
}
/**
* Whether the sprite is drawn at the picture's own pixel size – one image pixel per world unit.
* Exact, unlike `matchesAspect`: these two numbers are written as they are, with no division in
* between that could cost a digit.
*/
export function matchesPixelSize(size: PixelSize, props: PropertyBag): boolean {
return Number(props.width) === size.w && Number(props.height) === size.h;
}
/**
* Cross product with a relative tolerance, because 3135 of the 40 000 sizes up to 200 × 200 fail an
* exact test right after their own fit (rounding).
*/
export function matchesAspect(size: PixelSize, props: PropertyBag): boolean {
const w = Number(props.width);
const h = Number(props.height);
if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) return false;
return Math.abs(w * size.h - h * size.w) <= 1e-9 * w * size.h;
}