Skip to content

packages/core/src/features/sprites/model.ts

Read as Markdown

This is the source snapshot used to build these API details. View this revision on GitHub.

Back to reference

1 import { defineAnimation } from '../../lib/animation/definition.js';
2 import type { PibblAnimationDefinition } from '../../lib/animation/types.js';
3 
4 /** Pixel rectangle within an atlas page or original frame. See {@link SpriteFrame}. */
5 export interface SpriteRectangle { /** Horizontal pixel offset. */ readonly x: number; /** Vertical pixel offset. */ readonly y: number; /** Width in pixels. */ readonly width: number; /** Height in pixels. */ readonly height: number }
6 /** Normalized frame information returned by a format adapter. See {@link SpriteFrame}. */
7 export interface SpriteFrameData {
8   /** Unique frame name in this sheet. */ readonly name: string;
9   /** Zero-based atlas page index; defaults to zero. */ readonly page?: number;
10   /** Packed rectangle within the atlas page. */ readonly rect: SpriteRectangle;
11   /** Original frame dimensions before trimming. */ readonly sourceSize?: Readonly<{ /** Width in pixels. */ width: number; /** Height in pixels. */ height: number }>;
12   /** Trimmed content offset within the original frame. */ readonly trim?: Readonly<{ /** Horizontal pixel offset. */ x: number; /** Vertical pixel offset. */ y: number }>;
13   /** Whether the packed rectangle is rotated clockwise. */ readonly rotated?: boolean;
14 }
15 /** One normalized timed frame reference. Durations are milliseconds. See {@link SpriteFrame}. */
16 export interface SpriteAnimationFrameData { /** Name of the frame used by this animation step. */ readonly frame: string; /** Positive duration of the animation step in milliseconds. */ readonly duration: number }
17 /** Adapter result. Image paths resolve relative to the metadata URL. See {@link SpriteFrame}. */
18 export interface SpriteSheetData {
19   /** Atlas image URLs, relative to the metadata URL. */ readonly images: readonly string[];
20   /** Normalized frame records for this sheet. */ readonly frames: readonly SpriteFrameData[];
21   /** Named sequences of timed frame references. */ readonly animations?: Readonly<Record<string, readonly SpriteAnimationFrameData[]>>;
22 }
23 /** Import adapter; loaders invoke this directly without a format registry. See {@link SpriteFrame}. */
24 export interface SpriteSheetFormat { /** Convert decoded metadata to normalized sheet data.
25  * @param data - Parsed atlas JSON.
26  * @returns Validated adapter data for loading the sheet.
27  */ readonly parse: (data: unknown) => SpriteSheetData }
28 
29 export interface FrameRecord extends SpriteFrameData { readonly image: HTMLImageElement; readonly owner: { disposed: boolean } }
30 const records = new WeakMap<SpriteFrame, FrameRecord>();
31 /** Immutable atlas frame. Obtain frames from loadSpriteSheet; images belong to the sheet. See {@link SpriteFrame}. */
32 export class SpriteFrame {
33   private readonly identity = true;
34   /** Original logical frame width, before transparent borders were trimmed.
35    * @returns Width in logical pixels.
36    */
37   get width(): number { return frameRecord(this).sourceSize?.width ?? frameRecord(this).rect.width; }
38   /** Original logical frame height, before transparent borders were trimmed.
39    * @returns Height in logical pixels.
40    */
41   get height(): number { return frameRecord(this).sourceSize?.height ?? frameRecord(this).rect.height; }
42   /** Stable name supplied by the atlas or numeric grid index.
43    * @returns The frame name.
44    */
45   get name(): string { void this.identity; return frameRecord(this).name; }
46 }
47 export function makeFrame(record: FrameRecord): SpriteFrame {
48   const frame = new SpriteFrame(); records.set(frame, record); Object.freeze(frame); return frame;
49 }
50 export function frameRecord(frame: SpriteFrame): FrameRecord {
51   const record = records.get(frame);
52   if (!record) throw new TypeError('Expected a SpriteFrame from loadSpriteSheet.');
53   if (record.owner.disposed) throw new Error('Sprite sheet has been disposed.');
54   return record;
55 }
56 /** Author a finite discrete animation using FPS or explicit positive millisecond durations. See {@link SpriteFrame}.
57  * @param options - Frames with a shared FPS or individual millisecond durations.
58  * @returns A seekable discrete animation definition.
59  */
60 export function defineSpriteAnimation(options: {
61   readonly frames: readonly SpriteFrame[];
62   readonly fps: number;
63 } | {
64   readonly frames: readonly Readonly<{ frame: SpriteFrame; duration: number }>[];
65 }): PibblAnimationDefinition<SpriteFrame> {
66   if (!options.frames.length) throw new RangeError('Sprite animation requires at least one frame.');
67   if ('fps' in options && (!Number.isFinite(options.fps) || options.fps <= 0)) throw new RangeError('Sprite FPS must be finite and positive.');
68   const entries = options.frames.map(item => {
69     const entry = 'fps' in options ? { frame: item as SpriteFrame, duration: 1000 / options.fps } : item as { frame: SpriteFrame; duration: number };
70     frameRecord(entry.frame);
71     if (!Number.isFinite(entry.duration) || entry.duration <= 0) throw new RangeError('Sprite duration must be finite and positive.');
72     return { ...entry };
73   });
74   let duration = 0;
75   const ends = entries.map(entry => duration += entry.duration);
76   return defineAnimation({ duration, sample: ({ localTime }) => {
77     let low = 0, high = ends.length - 1;
78     while (low < high) { const mid = (low + high) >>> 1; if (localTime < ends[mid]) high = mid; else low = mid + 1; }
79     return entries[low].frame;
80   } });
81 }
82 

Documentation built with @pibbl/core 0.0.2, revision 2dccb19. ALPHA — NOT FOR PRODUCTION USE.