Skip to content

<AnimatedSpriteMaterial>

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: [number, number, number] = $state([-2.0, -2.75, 0.01])
let playerAtFire = $derived(playerPosition && Math.abs(playerPosition[0]) < 0.7)
const fov = new Tween(50, {
easing: cubicOut,
duration: 900
})
const cameraPosY = new Tween(-0.2, {
easing: cubicOut,
duration: 900
})
$effect(() => {
fov.set(playerAtFire ? 45 : 50)
})
$effect(() => {
cameraPosY.set(playerAtFire ? -0.9 : -0.2)
})
</script>
<Suspense>
<Fire />
</Suspense>
<T.AmbientLight
color="#6697C7"
intensity={0.3}
/>
<Suspense>
{#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}
</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>

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.

Exports

name
type

play
() => void

pause
() => void