// Native node transitions at the sprite layer's scale: every sprite // declares a spring, and the app writes TARGETS - one setSprite per sprite // per shuffle (about once a second), ZERO JS per frame. The core // integrates all the springs each frame and publishes one coalesced // pose-buffer write; between shuffles the app runs no code at all (there // is no onFrame here - the running tracks drive frame demand themselves, // and once everything settles the layer costs nothing). Compare // sprites.tsx, which moves the same population imperatively every frame: // here the JS cost is proportional to target CHANGES, not frames. The // console logs each shuffle's burst cost. // // Rotation targets ride the quaternion geodesic, so a spin to a new // random angle always takes the short arc, and the position spring keeps // its velocity when a shuffle lands mid-flight - retarget as fast as you // like, the motion stays continuous. // // Tap a sprite and it LEAVES: destroySprite lets go of the handle, and the // `exit` on its scale entry shrinks it away on an ease-in of its own while // the sprite is a ghost - drawn, but no tap or pick reaches it - then the // core frees it. A fresh sprite pops into the empty slot a moment later, // entering from scale zero (`from`); no settle callback, no bookkeeping. import { decodeImage, render } from "@solidrt/core" import { addSprite, createAtlas, createSpriteLayer, destroySprite, grid, setSprite, setSpriteTransition } from "@solidrt/2d" import type { SpriteHandle, SpriteTransition } from "@solidrt/2d" import logoBytes from "./logo.png" with { type: "binary" } const COLS = 20 const ROWS = 20 const COUNT = COLS * ROWS const W = 720 const H = 720 const SPRITE = 30 // The pop-in / shrink-out of a sprite's size, and the pause before a // tapped-away sprite is replaced. const POP_MS = 300 const RESPAWN_MS = 600 const TRANSITION: SpriteTransition = { position: { duration: 700, bounce: 0.3 }, rotation: { duration: 700 }, scale: { duration: POP_MS, bounce: 0.4, from: [0, 0], exit: { value: [0, 0], curve: "ease-in", duration: POP_MS / 2 } }, } function App() { let atlas = createAtlas(decodeImage(logoBytes), { label: "logo-atlas" }) let frames = grid(atlas, 2, 2) let layer = createSpriteLayer([atlas], { capacity: COUNT, label: "springs" }) let view = layer.createView({ width: W, height: H, clearColor: [0.05, 0.05, 0.09, 1] }) let slotX = (slot: number) => ((slot % COLS) + 0.5) * (W / COLS) let slotY = (slot: number) => (Math.floor(slot / COLS) + 0.5) * (H / ROWS) // Sprite k sits at grid slot slots[k]; each shuffle re-deals the slots. let slots = Array.from({ length: COUNT }, (_, i) => i) let spawn = (k: number): SpriteHandle => { let sprite = addSprite(layer, { x: slotX(slots[k]!), y: slotY(slots[k]!), w: SPRITE, h: SPRITE, frame: frames[k % 4] }) // Declared in the adding tick, so the `from` plays: the sprite pops in. setSpriteTransition(sprite, TRANSITION) sprite.onTap = () => { destroySprite(sprite) setTimeout(() => (sprites[k] = spawn(k)), RESPAWN_MS) } return sprite } let sprites = slots.map((_, k) => spawn(k)) let shuffle = () => { for (let i = slots.length - 1; i > 0; i--) { let j = Math.floor(Math.random() * (i + 1)) ;[slots[i], slots[j]] = [slots[j]!, slots[i]!] } let start = performance.now() for (let k = 0; k < COUNT; k++) { setSprite(sprites[k]!, { x: slotX(slots[k]!), y: slotY(slots[k]!), rotation: Math.random() * Math.PI * 2 }) } let ms = performance.now() - start console.log(`retarget x${COUNT}: ${ms.toFixed(2)} ms; JS idles until the next shuffle`) } setInterval(shuffle, 1200) return ( ) } render(() => )