threlte logo
@threlte/extras

transitions

Experimental

The plugin transitions uses Svelte internals. Changes to the runtime of Svelte may break this plugin. If you encounter any issues, please open an issue on GitHub. It’s recommended to lock the version of Svelte to a specific version.

The plugin transitions enables Svelte-like transitions on Threlte components.

{#if visible}
  <T.Mesh transition={scale({ duration: 400 })} />
{/if}
<script>
  import { Canvas } from '@threlte/core'
  import Scene from './Scene.svelte'
  import { Checkbox, Pane } from 'svelte-tweakpane-ui'
  import { Suspense } from '@threlte/extras'

  let red = $state(true)
  let blue = $state(true)
</script>

<Pane
  title="Transitions"
  position="fixed"
>
  <Checkbox
    bind:value={red}
    label="Toggle Red"
  />

  <Checkbox
    bind:value={blue}
    label="Toggle Blue"
  />
</Pane>

<div>
  <Canvas>
    <Suspense>
      <Scene
        {red}
        {blue}
      />
    </Suspense>
  </Canvas>
</div>

<style>
  :global(body) {
    margin: 0;
  }

  div {
    width: 100%;
    height: 100%;
  }
</style>
<script lang="ts">
  import { T } from '@threlte/core'
  import { Environment, Grid, OrbitControls, SoftShadows, transitions } from '@threlte/extras'
  import { fade, scale } from './transitions'

  transitions()

  let { red, blue }: { red: boolean; blue: boolean } = $props()
</script>

{#if red}
  <T.Mesh
    castShadow
    transition={scale(0)}
    position.y={1}
    position.x={-1.5}
  >
    <T.SphereGeometry />
    <T.MeshStandardMaterial
      transparent
      color="red"
    />
  </T.Mesh>
{/if}

{#if blue}
  <T.Mesh
    castShadow
    position.y={1}
    position.x={1.5}
  >
    <T.SphereGeometry />
    <T.MeshToonMaterial
      transparent
      transition={fade()}
      color="blue"
    />
  </T.Mesh>
{/if}

<!-- Environment -->
<SoftShadows />
<Environment url="/textures/equirectangular/hdr/shanghai_riverside_1k.hdr" />

<!-- Camera -->
<T.PerspectiveCamera
  makeDefault
  position={[0, 3, 10]}
  fov={30}
>
  <OrbitControls
    enableDamping
    target={[0, 0.8, 0]}
    enableZoom={false}
    enablePan={false}
  />
</T.PerspectiveCamera>

<!-- Lights -->
<T.DirectionalLight
  position={[10, 10, 10]}
  castShadow
  intensity={Math.PI / 2}
/>
<T.AmbientLight intensity={0.1} />

<!-- Floor -->
<Grid
  sectionColor="#374668"
  cellColor="#374668"
/>
<T.Mesh
  receiveShadow
  position.y={-0.01}
  scale={20}
  rotation.x={-Math.PI / 2}
>
  <T.PlaneGeometry />
  <T.MeshStandardMaterial color="#0F141F" />
</T.Mesh>
import { isInstanceOf } from '@threlte/core'
import { createTransition } from '@threlte/extras'
import { cubicOut } from 'svelte/easing'

export const fade = (opacity = 0) => {
  return createTransition((ref: any) => {
    if (!isInstanceOf(ref, 'Material')) return

    if (!ref.transparent) {
      ref.transparent = true
      ref.needsUpdate = true
    }

    return {
      duration: 600,
      tick: (t: number) => {
        ref.opacity = t * (1 - opacity)
      },
      easing: cubicOut
    }
  })
}
import { isInstanceOf } from '@threlte/core'
import { createTransition } from '@threlte/extras'
import { cubicOut } from 'svelte/easing'
import { mapLinear } from 'three/src/math/MathUtils.js'

export const fly = (options: { x?: number; y?: number; z?: number }) => {
  return createTransition((ref) => {
    if (!isInstanceOf(ref, 'Object3D')) return

    return {
      duration: 600,
      tick(t) {
        const x = mapLinear(t, 0, 1, options.x ?? 0, 0)
        const y = mapLinear(t, 0, 1, options.y ?? 0, 0)
        const z = mapLinear(t, 0, 1, options.z ?? 0, 0)
        ref.position.set(x, y, z)
      },
      easing: cubicOut
    }
  })
}
export * from './fade'
export * from './fly'
export * from './scale'
import { isInstanceOf } from '@threlte/core'
import { createTransition } from '@threlte/extras'
import { cubicOut } from 'svelte/easing'
import { mapLinear } from 'three/src/math/MathUtils.js'

export const scale = (scale: number) => {
  return createTransition((ref) => {
    if (!isInstanceOf(ref, 'Object3D')) return

    return {
      duration: 600,
      tick(t) {
        ref.scale.setScalar(mapLinear(t, 0, 1, scale, 1))
      },
      easing: cubicOut
    }
  })
}

Usage

To use Threlte transitions, you need to inject the plugin first via invoking transitions(). All child <T> components will then accept the transition properties in, out and transition.

createTransition

Threlte Transitions use regular Svelte transitions under the hood and therefore provide a similar API. The function createTransition is used to conveniently create a transition.

To create a transition, you need to provide a function which accepts a reference to the object referenced by the <T> component and returns an object with the following properties:

  • duration: The duration of the transition in milliseconds.
  • tick: A function that is called on every tick of the transition.
  • easing (optional): The easing function to use.
  • delay (optional): The delay of the transition in milliseconds.
import { isInstanceOf } from '@threlte/core'
import { createTransition } from '@threlte/extras'
import { cubicOut } from 'svelte/easing'

const fade = createTransition((ref) => {
  // Only apply the transition to materials
  if (!isInstanceOf(ref, 'Material')) return

  // Make the material transparent if it's not already
  if (!ref.transparent) {
    ref.transparent = true
    ref.needsUpdate = true
  }

  return {
    tick(t) {
      // t is [0, 1]
      ref.opacity = t
    },
    easing: cubicOut,
    duration: 400,
    delay: 100
  }
})

The transition fade can now be applied to all <T> components that instantiate classes extending THREE.Material like THREE.MeshBasicMaterial or THREE.MeshStandardMaterial:

<T.MeshStandardMaterial transition={fade} />

Transition Directions

Run a transition only when the component mounts:

<T.MeshStandardMaterial in={fade} />

Run a transition only when the component unmounts:

<T.MeshStandardMaterial out={fade} />

Run a transition when the component mounts or unmounts:

<T.MeshStandardMaterial transition={fade} />

To react on different transition directions in the same transition, you can use the direction parameter:

import { createTransition } from '@threlte/extras'

// direction is 'in', 'out' or 'both'
const fly = createTransition((ref, { direction }) => {
  // …
})

Transition Parameters

To make reusing transitions throughout your application easier, make createTransition the return value of a function that accepts parameters:

import { isInstanceOf } from '@threlte/core'
import { createTransition } from '@threlte/extras'

const scale = (duration: number) => {
  return createTransition((ref) => {
    // Only apply the transition to objects
    if (!isInstanceOf(ref, 'Object3D')) return
    return {
      tick(t) {
        ref.scale.setScalar(t)
      },
      duration
    }
  })
}

The transition can now be used like this:

<T.Mesh transition={scale(400)} />

Transition Events

Similar to Svelte transitions, Threlte transitions also emit events:

{#if visible}
  <T.Mesh
    {geometry}
    {material}
    transition={fade}
    onintrostart={() => console.log('intro started')}
    onoutrostart={() => console.log('outro started')}
    onintroend={() => console.log('intro ended')}
    onoutroend={() => console.log('outro ended')}
  />
{/if}

Global Transitions

Transitions are local by default. Local transitions only play when the block they belong to is created or destroyed, not when parent blocks are created or destroyed. Threlte offers a function global that marks a transition as global.

<script>
  import { global } from '@threlte/extras'
</script>

{#if x}
  {#if y}
    <T.Mesh transition={global(scale(400))} />
  {/if}
{/if}

TypeScript

Prop Types

By default, the transitions plugin does not add any prop types to the <T> component. You can however extend the types of the <T> component by defining the Threlte.UserProps type in your ambient type definitions. In a typical SvelteKit application, you can find these in src/app.d.ts. The transitions plugin exports the TransitionsProps type which you can use as shown below:

src/app.d.ts
import type { TransitionsProps } from '@threlte/extras'

declare global {
  namespace Threlte {
    interface UserProps extends TransitionsProps {}
  }
}

export {}

Now all relevant properties on <T> components will be type safe.

Scene.svelte
<script>
  import { transitions } from '@threlte/extras'
  transitions()
</script>

<T.Mesh
  transition={scale(400)}
  onintrostart={() => console.log('intro started')}
/>