Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/sanchedev/tiny-engine/llms.txt

Use this file to discover all available pages before exploring further.

tiny-engine animates sprites by generating keyframes directly from sprite sheet textures. Instead of writing per-frame offsets by hand, you describe the grid layout of your sheet and let the keyframe helpers do the math — then hand the result to an <animation-player> node that drives playback every frame.

Keyframe helper functions

Three helpers from tiny-engine let you build keyframe arrays that the AnimationPlayer understands.

kfFromSpriteSheet

Generates keyframes for every cell in a sprite sheet grid, optionally restricted to a sub-range of frames.
import { kfFromSpriteSheet } from 'tiny-engine'

kfFromSpriteSheet(sprite, textureId, spritesCountX, spritesCounty, range?)
ParameterTypeDescription
spriteSpriteThe sprite instance to animate.
textureIdsymbol | nullTexture symbol. Pass null to keep the sprite’s current texture and only update the frame margin.
spritesCountXnumberNumber of columns in the sheet. Defaults to 1.
spritesCountynumberNumber of rows in the sheet. Defaults to 1.
range[number, number]Optional [from, to] tuple (0-indexed, inclusive) to select a subset of frames.
kfFromSpriteSheet sets sprite.sourceSize automatically from the computed cell dimensions, so you do not need to set it yourself.

kfFromProp

Returns a single keyframe that sets one property on any Node instance.
import { kfFromProp } from 'tiny-engine'

kfFromProp(node, property, value)
// e.g.
kfFromProp(sprite, 'textureId', WALK_TEXTURE)
kfFromProp(sprite, 'margin', new Vector2(32, 0))
ParameterTypeDescription
nodeT extends NodeAny node instance.
propertyK extends keyof TThe property name to set.
valueT[K]The value to assign on every tick of that frame.

multiKF

Combines several keyframes into one so they all execute at the same animation step.
import { multiKF } from 'tiny-engine'

multiKF([frameA, frameB, frameC]) // returns a single AnimationKeyframe
ParameterTypeDescription
kfsAnimationKeyframe[]Array of keyframes to run simultaneously.

Generating walk and idle frames from a sheet

The example below uses a single 4×2 sprite sheet where the top row holds idle frames and the bottom row holds walk frames.
import { kfFromSpriteSheet } from 'tiny-engine'
import { useRefNode } from 'tiny-engine/hooks'
import { PrimaryNode } from 'tiny-engine'

const SHEET = Symbol('character-sheet')

function CharacterSprite() {
  const sprite = useRefNode(PrimaryNode.Sprite)

  // All 8 cells — used to derive idle and walk subsets
  // Rows: 0-3 idle (first row), 4-7 walk (second row)
  const idleFrames = kfFromSpriteSheet(sprite.node, SHEET, 4, 2, [0, 3])
  const walkFrames = kfFromSpriteSheet(sprite.node, SHEET, 4, 2, [4, 7])

  return (
    <sprite ref={sprite} textureId={SHEET} sourceSize={new Vector2(16, 16)}>
      <animation-player
        animations={() => ({
          idle: { keyframes: idleFrames, fps: 6, loop: true },
          walk: { keyframes: walkFrames, fps: 8, loop: true },
        })}
        currentAnim="idle"
      />
    </sprite>
  )
}
The range tuple is 0-indexed and inclusive on both ends. For a 4×2 sheet, frames 0–3 are the first row and frames 4–7 are the second row.

The <animation-player> node

PropTypeDescription
animations() => Record<string, Animation>Deferred function returning a map of animation names to Animation objects.
currentAnimstring | SignalGetter<string>The animation to play. Accepts a static name or a reactive signal getter.
destroyOnEndbooleanWhen true, the node destroys itself when the current non-looping animation ends. Defaults to false.
refNodeReference<PrimaryNode.AnimationPlayer>Optional ref to access the node imperatively.
Each Animation object has the shape:
interface Animation {
  fps: number                // Frames per second
  keyframes: AnimationKeyframe[]
  loop?: boolean             // Whether to loop (default false)
}

Reactive animation switching with useComputed

When currentAnim receives a SignalGetter<string>, the player subscribes to signal changes and calls .play() automatically whenever the value changes. No manual wiring required.
import { useSignal, useComputed } from 'tiny-engine/hooks'

function Hero() {
  const sprite = useRefNode(PrimaryNode.Sprite)
  const [isWalking, setIsWalking] = useSignal(false)

  // Derived signal: resolves to 'walk' or 'idle' reactively
  const animName = useComputed(() => (isWalking() ? 'walk' : 'idle'))

  return (
    <sprite ref={sprite} textureId={SHEET} sourceSize={new Vector2(16, 16)}>
      <animation-player
        animations={() => ({
          idle: { keyframes: idleFrames, fps: 6, loop: true },
          walk: { keyframes: walkFrames, fps: 8, loop: true },
        })}
        currentAnim={animName}
      />
    </sprite>
  )
}
When isWalking flips to true, animName re-evaluates to 'walk' and the player switches immediately.

Why animations is a deferred function

The animations prop is intentionally a function rather than a plain object. The AnimationPlayer constructor is called synchronously during JSX evaluation — at that moment, child sprite nodes may not yet exist in the scene tree, so calling sprite.node on a ref would return undefined. By wrapping the animation definitions in a function, tiny-engine defers the call until the node’s started event fires. By that point, all child nodes are guaranteed to have been initialised and sprite.node resolves correctly.
// ✅ Correct — evaluated after all nodes start
animations={() => ({
  idle: { keyframes: kfFromSpriteSheet(sprite.node, SHEET, 4, 2), fps: 6 },
})}

// ❌ Wrong — sprite.node is undefined at construction time
animations={{
  idle: { keyframes: kfFromSpriteSheet(sprite.node, SHEET, 4, 2), fps: 6 },
}}
Grab a ref on the <animation-player> and listen to its animationEnded event with useEvent to trigger one-shot effects or chain animations. Use setNext(animName) to queue the next animation imperatively.
const anim = useRefNode(PrimaryNode.AnimationPlayer)

useEvent(anim, 'animationEnded', (finishedAnim) => {
  if (finishedAnim === 'attack') {
    anim.node.play('idle')
  }
})

Build docs developers (and LLMs) love