threlte logo
@threlte/extras

<AnimatedSpriteMaterial>

Provides animation tools for spritesheets.

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

<div class="wrapper">
  <Canvas>
    <Scene />
  </Canvas>
</div>

<style>
  div.wrapper {
    background-color: black;
    position: relative;
    width: 100%;
    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={0.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'

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

  let animation = 'IdleRight'

  export const playerPosition: [number, number, number] = [-2.0, -2.75, 0.01]
  const mesh = new Mesh()
  mesh.position.set(...playerPosition)

  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') play()
    if (e.key === 'e') 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
    playerPosition[0] += -keyboard.x * (delta * 2)
    mesh.position.set(...playerPosition)
  })

  let play: () => void
  let pause: () => void
</script>

<svelte:window
  on:keydown={handleKeydown}
  on:keyup={handleKeyup}
/>

<Suspense>
  <T is={mesh}>
    <AnimatedSpriteMaterial
      is={new MeshStandardMaterial()}
      bind:play
      bind:pause
      {animation}
      textureUrl="/textures/sprites/player.png"
      dataUrl="/textures/sprites/player.json"
    />
    <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 ThrelteSeven from './ThrelteSeven.svelte'
  import { tweened } from 'svelte/motion'
  import { cubicOut } from 'svelte/easing'

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

  let playerPosition: [number, number, number]
  $: playerAtFire = playerPosition && Math.abs(playerPosition[0]) < 0.7

  const fov = tweened(50, {
    easing: cubicOut,
    duration: 900
  })
  $: fov.set(playerAtFire ? 45 : 50)
  const cameraPosY = tweened(-0.2, {
    easing: cubicOut,
    duration: 900
  })
  $: cameraPosY.set(playerAtFire ? -0.9 : -0.2)
</script>

<Fire />

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

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

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

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

<Player bind:playerPosition />

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

  export let show: boolean

  let animation: 'Hidden' | 'In' | 'Out' = 'Hidden'
  $: if (show) {
    animation = 'In'
  } else if (!show && animation === 'In') {
    animation = 'Out'
  }
</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]}
  on:create={({ 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 texture image.

alphaTest
number
no
0.1
Sets the alpha value to be used when running an alpha test.

animation
string
no
The current playing animation name.

autoplay
boolean
no
true
Controls whether or not to automatically run an animation on load.

columns
number
no
The number of columns in the spritesheet.

dataUrl
string
no
The URL of the spritesheet JSON.

delay
number
no
0
Delay the start of the animation in ms.

endFrame
number
no
totalFrames
The end frame of the current animation.

filter
"nearest" | "linear"
no
"nearest"
The texture filtering applied to the spritesheet.

flipX
boolean
no
false
Whether or not the Sprite should flip sides on the x-axis.

fps
boolean
no
10
The desired frames per second of the animation.

loop
boolean
no
true
Whether or not the current animation should loop.

rows
number
no
1
The number of rows in the spritesheet.

startFrame
number
no
0
The start frame of the current animation.

totalFrames
number
no
rows * columns - 1
The total number of frames in the spritesheet.

transparent
boolean
no
true
Whether or not the material should be transparent.

Events

name
payload
description

load
void
Fires when all resources have loaded.

start
void
Fires when an animation starts.

end
void
Fires when an animation ends.

loop
void
Fires when an animation loop completes.

Bindings

name
type

play
() => void

pause
() => void