@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
, andcolumns
, 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>