// Frame animation via createAnimation: a row of sprites sharing one looping // clip (one clock, one setSprite per sprite per STEP - an app's whole crowd // animates off a handful of no-demand frame callbacks, presenting only on // a step), plus a one-shot clip // that holds its last frame and fires onFinish. Self-asserting: samples the // sprites' frames against the wall clock and logs ANIM-OK / ANIM-FAIL. // // The atlas is the core logo sliced 2x2 by grid(): a 4-frame "clip". Real // art would slice a sheet the same way and pass a consecutive slice of the // frames array per animation. import { decodeImage, render } from "@solidrt/core" import { addSprite, createAnimation, createAtlas, createSpriteLayer, getSprite, grid } from "@solidrt/2d" import type { Frame, SpriteHandle } from "@solidrt/2d" import logoBytes from "./logo.png" with { type: "binary" } const W = 520 const H = 200 const SPRITE = 96 // Slow enough that a late timer cannot blur a sampled frame boundary: the // checks sample mid-frame, half a period from either edge. const LOOP_FPS = 2 const SHOT_FPS = 5 function App() { let atlas = createAtlas(decodeImage(logoBytes), { label: "logo-atlas" }) let frames = grid(atlas, 2, 2) let layer = createSpriteLayer([atlas], { label: "anim" }) let view = layer.createView({ width: W, height: H, clearColor: [0.05, 0.05, 0.09, 1] }) // Three sprites share the looping clip - one clock steps all of them. let looping = createAnimation(frames, LOOP_FPS) let looped: SpriteHandle[] = [] for (let i = 0; i < 3; i++) { let sprite = addSprite(layer, { x: 80 + i * 120, y: H / 2, w: SPRITE, h: SPRITE }) looping.add(sprite) looped.push(sprite) } // The one-shot: plays through once, holds the last frame, fires onFinish. let shot = createAnimation(frames, SHOT_FPS, { loop: false }) let ended = 0 shot.onFinish = () => ended++ let shotSprite = addSprite(layer, { x: 440, y: H / 2, w: SPRITE, h: SPRITE }) shot.add(shotSprite) let failures: string[] = [] function check(ok: boolean, what: string) { if (!ok) failures.push(what) } function indexOf(frame: Frame): number { return frames.findIndex(f => Math.abs(f.u0 - frame.u0) < 1e-6 && Math.abs(f.v0 - frame.v0) < 1e-6) } function shownIndex(sprite: SpriteHandle): number { return indexOf(getSprite(sprite)!.frame) } // Sample mid-frame on timers. At LOOP_FPS = 2 a frame is 500ms and the // clip steps on the display frame a boundary falls in, so mid-frame // samples have ~250ms of margin either way. let period = 1000 / LOOP_FPS setTimeout(() => { check(shownIndex(looped[0]!) === 0, "loop shows frame 0 in its first period") }, period / 2) setTimeout(() => { check(shownIndex(looped[1]!) === 1, "loop advanced to frame 1") check(looping.frame() === shownIndex(looped[1]!), "anim.frame matches the sprite's shown frame") }, period * 1.5) setTimeout(() => { check(shownIndex(looped[2]!) === 0, "loop wrapped back to frame 0 after a full cycle") // One-shot: 4 frames at 5fps end at 800ms - long since done here. check(shownIndex(shotSprite) === frames.length - 1, "one-shot holds its last frame") check(!shot.playing(), "one-shot stopped playing") check(ended === 1, `onFinish fired once, got ${ended}`) check(looping.playing(), "the looping clip is still playing") console.log(failures.length === 0 ? "ANIM-OK" : `ANIM-FAIL: ${failures.join("; ")}`) }, period * 4.5) return ( ) } render(() => )