threlte logo
@threlte/extras

<AnimatedSpriteMaterial>

webgl <AnimatedSpriteMaterial> works with WebGL only.

Provides animation tools for spritesheets.

<script lang="ts">
  import { Canvas } from '@threlte/core'
  import Scene from './Scene.svelte'
</script>

<div>
  <Canvas>
    <Scene />
  </Canvas>
</div>

<style>
  div {
    background-color: black;
    height: 100%;
  }
</style>
<script lang="ts">
  import { T, useTask } from '@threlte/core'
  import { AnimatedSpriteMaterial } from '@threlte/extras'
  import { PointLight } from 'three'

  const light = new PointLight('#FF893D', 100)

  let rate = 1 / 10
  let fixedStepTimeAccumulator = 0
  useTask((delta) => {
    fixedStepTimeAccumulator += delta
    while (fixedStepTimeAccumulator >= rate) {
      fixedStepTimeAccumulator -= rate
      // random light intensity between 22 and 44
      light.intensity = Math.random() * 24 + 22
    }
  })
</script>

<T.Sprite position.y={-2.3}>
  <AnimatedSpriteMaterial
    textureUrl="/textures/sprites/fire.png"
    totalFrames={8}
    fps={10}
  />
  <T
    is={light}
    distance={1.8}
    decay={0.5}
    position.y={-0.2}
    position.z={0.02}
  />
</T.Sprite>
<script lang="ts">
  import { T, useTask } from '@threlte/core'
  import { AnimatedSpriteMaterial, Suspense } from '@threlte/extras'
  import { Mesh, MeshStandardMaterial } from 'three'

  type Props = {
    position: [number, number, number]
  }
  let { position = $bindable() }: Props = $props()

  const keyboard = { x: 0 }
  const pressed = new Set<string>()

  let sprite = $state.raw<AnimatedSpriteMaterial>()
  let animation = $state('IdleRight')

  const mesh = new Mesh()
  mesh.position.set(...position)

  const handleKey = (key: string, value: 0 | 1) => {
    switch (key.toLowerCase()) {
      case 'a':
      case 'arrowleft':
        return (keyboard.x = +value)
      case 'd':
      case 'arrowright':
        return (keyboard.x = -value)
    }
    return
  }

  const handleKeydown = (e: KeyboardEvent) => {
    pressed.add(e.key)
    pressed.forEach((key) => handleKey(key, 1))
  }

  const handleKeyup = (e: KeyboardEvent) => {
    pressed.delete(e.key)
    handleKey(e.key, 0)
    pressed.forEach((key) => handleKey(key, 1))

    if (e.key === 'q') sprite?.play()
    if (e.key === 'e') sprite?.pause()
  }

  useTask((delta) => {
    if (keyboard.x > 0) {
      animation = 'RunLeft'
    } else if (keyboard.x < 0) {
      animation = 'RunRight'
    } else {
      animation = animation.replace('Run', 'Idle')
    }

    if (keyboard.x === 0) return
    position[0] += -keyboard.x * (delta * 2)
    mesh.position.set(...position)
  })
</script>

<svelte:window
  onkeydown={handleKeydown}
  onkeyup={handleKeyup}
/>

<Suspense>
  <T is={mesh}>
    <AnimatedSpriteMaterial
      is={new MeshStandardMaterial()}
      {animation}
      textureUrl="/textures/sprites/player.png"
      dataUrl="/textures/sprites/player.json"
      bind:this={sprite}
    />
    <T.PlaneGeometry args={[0.5, 0.5]} />
  </T>
</Suspense>
<script lang="ts">
  import { T } from '@threlte/core'
  import { AnimatedSpriteMaterial, Suspense, useTexture } from '@threlte/extras'
  import Fire from './Fire.svelte'
  import Player from './Player.svelte'
  import ThrelteLogo from './ThrelteLogo.svelte'
  import { Tween } from 'svelte/motion'
  import { cubicOut } from 'svelte/easing'

  const texture = useTexture('/textures/sprites/bg.png')

  let playerPosition = $state<[number, number, number]>([-2.0, -2.75, 0.01])
  let playerAtFire = $derived(Math.abs(playerPosition[0]) < 0.7)

  const fov = Tween.of(() => (playerAtFire ? 45 : 50), {
    easing: cubicOut,
    duration: 900
  })

  const cameraPosY = Tween.of(() => (playerAtFire ? -0.9 : -0.2), {
    easing: cubicOut,
    duration: 900
  })
</script>

<Suspense>
  <Fire />
</Suspense>

<T.AmbientLight
  color="#6697C7"
  intensity={0.3}
/>

<Suspense>
  {#each { length: 9 }, index}
    <T.Sprite
      scale={0.5}
      position.y={-1.99}
      position.x={index < 5
        ? index / 2.4 + Math.random() * 0.4 - 2.8
        : index / 2.4 + Math.random() * 0.4 - 1}
    >
      <AnimatedSpriteMaterial
        textureUrl="/textures/sprites/grass.png"
        totalFrames={6}
        fps={5}
        delay={index * 40}
      />
    </T.Sprite>
  {/each}
</Suspense>

{#await texture then map}
  <T.Sprite
    scale={7.5}
    position.z={-0.01}
    position.y={0.4}
  >
    <T.MeshBasicMaterial {map} />
  </T.Sprite>
{/await}

<Suspense>
  <ThrelteLogo show={playerAtFire} />
</Suspense>

<Player bind:position={playerPosition} />

<T.PerspectiveCamera
  makeDefault
  position.z={7}
  position.y={cameraPosY.current}
  fov={fov.current}
/>
<script lang="ts">
  import { T } from '@threlte/core'
  import { AnimatedSpriteMaterial } from '@threlte/extras'

  interface Props {
    show: boolean
  }

  let { show }: Props = $props()

  let animation = $state<'Hidden' | 'In' | 'Out'>('Hidden')
  let mounted = false

  $effect(() => {
    if (mounted && show) {
      animation = 'In'
    } else if (!show && animation === 'In') {
      animation = 'Out'
    } else {
      mounted = true
    }
  })
</script>

<T.Sprite
  scale={[3.5, 1.75, 3.5]}
  position.y={0.2}
>
  <AnimatedSpriteMaterial
    textureUrl="/textures/sprites/Threlte_7.png"
    dataUrl="/textures/sprites/Threlte_7.json"
    {animation}
    autoplay
    loop={false}
  />
</T.Sprite>

This material is most easily used by passing it a spritesheet URL and a JSON metadata file URL.

Currently, JSON metadata using Aseprite’s hash export format is supported.

Animation names from tags can be used to transition to specific animations in the spritesheet.

<T.Sprite>
  <AnimatedSpriteMaterial
    animation="Idle"
    textureUrl="./player.png"
    dataUrl="./player.json"
  />
</T.Sprite>

If no metadata file is provided, additional props must be passed to run an animation:

  • totalFrames, if the spritesheet is only a single row.
  • totalFrames, rows, and columns, otherwise.
<T.Sprite>
  <AnimatedSpriteMaterial
    textureUrl="./fire.png"
    totalFrames={14}
    rows={4}
    columns={4}
  />
</T.Sprite>

Additionally, if a sheet with no JSON supplied has multiple animations, start and end frames must be passed to run an animation within the sheet.

<T.Sprite>
  <AnimatedSpriteMaterial
    textureUrl="./fire.png"
    totalFrames={14}
    rows={4}
    columns={4}
    startFrame={4}
    endFrame={8}
  />
</T.Sprite>

<AnimatedSpriteMaterial> can be attached to a <T.Sprite> as well as a <T.Mesh>.

<script lang="ts">
  import { Canvas, T } from '@threlte/core'
  import Scene from './Scene.svelte'
</script>

<div>
  <Canvas>
    <Scene />

    <T.DirectionalLight
      intensity={2}
      castShadow
      position={[1, 1, 1]}
    />
  </Canvas>
</div>

<style>
  div {
    height: 100%;
  }
</style>
<script lang="ts">
  import { T } from '@threlte/core'
  import { Grid, OrbitControls, Sky, AnimatedSpriteMaterial } from '@threlte/extras'
</script>

<T.OrthographicCamera
  makeDefault
  near={-100}
  far={100}
  zoom={150}
  position={[5, 1.5, 3]}
  oncreate={(ref) => ref.lookAt(0, 0, 0)}
>
  <OrbitControls
    enableDamping
    enablePan={false}
    enableZoom={false}
    maxPolarAngle={Math.PI / 2.5}
    minPolarAngle={Math.PI / 6}
  />
</T.OrthographicCamera>

<Sky />

<Grid
  position.y={0.001}
  type="polar"
  fadeDistance={10}
  infiniteGrid
/>

<T.Mesh
  position.y={1}
  position.x={-2}
  castShadow
  receiveShadow
>
  <T.MeshStandardMaterial color="white" />
  <T.SphereGeometry />
</T.Mesh>

<T.Mesh
  receiveShadow
  rotation.x={-Math.PI / 2}
>
  <T.PlaneGeometry args={[1000, 1000]} />
  <T.MeshStandardMaterial />
</T.Mesh>

<T.Mesh
  position.y={0.5}
  rotation.y={Math.PI / 2}
  castShadow
  receiveShadow
>
  <AnimatedSpriteMaterial
    animation="Idle_Left"
    textureUrl="/textures/sprites/punk.png"
    dataUrl="/textures/sprites/punk.json"
  />
  <T.PlaneGeometry />
</T.Mesh>

In the case of a Mesh parent a MeshBasicMaterial will be used by default, instead of a SpriteMaterial when attached to a Sprite. A custom depth material will be attached when parented to a mesh to support shadows.

Any other material type can be used as well.

<T.Mesh>
  <AnimatedSpriteMaterial
    is={THREE.MeshStandardMaterial}
    textureUrl="./fire.png"
    totalFrames={14}
  />
</T.Mesh>

Component Signature

Props

name
type
required
default
description

textureUrl
string
yes
The URL of the spritesheet image.

alphaTest
number
no
0.1
Pixels with an alpha value below this are discarded. See https://threejs.org/docs/#api/en/materials/Material.alphaTest.

animation
string
no
''
The name of the frame tag in the JSON data to play. When no tag matches, every frame between startFrame and endFrame is played.

autoplay
boolean
no
true
Starts playing as soon as the spritesheet has loaded.

columns
number
no
totalFrames, a single row
The number of columns on the spritesheet. Only used without dataUrl.

dataUrl
string
no
''
The URL of the spritesheet's JSON data, in the Aseprite "hash" format. Without it, frames are laid out on a grid described by rows, columns and totalFrames.

delay
number
no
0
Time in milliseconds to wait before playback starts.

endFrame
number
no
the last frame of the spritesheet
The last frame to play. Ignored when animation matches a frame tag.

filter
'nearest' | 'linear'
no
'nearest'
The texture filter used for the spritesheet. 'nearest' keeps pixel art crisp, 'linear' smooths it.

flipX
boolean
no
false
Mirrors the sprite horizontally.

fps
number
no
10
Playback speed in frames per second, for frames that don't define their own duration in the JSON data.

loop
boolean
no
true
Restarts the animation after its last frame. When false, playback stops on the last frame and onend fires.

rows
number
no
1
The number of rows on the spritesheet. Only used without dataUrl.

startFrame
number
no
0
The first frame to play. Ignored when animation matches a frame tag.

totalFrames
number
no
0
The number of frames on the spritesheet. Only used without dataUrl, where it is required.

transparent
boolean
no
true
Whether the material is rendered with transparency.

Events

name
payload
description

onload
void
Called once the spritesheet image and data have loaded.

onstart
void
Called whenever an animation is set and restarts from its first frame.

onend
void
Called when an animation that doesn't loop reaches its last frame.

onloop
void
Called each time a looping animation wraps back to its first frame.

Exports

name
type

play
() => void

pause
() => void