5.3.3

Dynamic components

Learn how to use Vue's Transition and dynamic components in TresJS

This recipe covers how to use <Transition> and Dynamic Vue built-in components.

Set up our main scene

Let's start with a simple scene.

main.vue
<script setup>
import { TresCanvas } from '@tresjs/core'
// import our two custom components
import Sphere from './Sphere.vue'
import Box from './Box.vue'

</script>
<template>
    <TresCanvas window-size clear-color="#82DBC5">
      <TresPerspectiveCamera
        :position="[0, 0, 5]"
      />
      <TresAmbientLight :intensity="0.5" />
      <TresDirectionalLight
        :position="[5, 5, 5]"
        :intensity="1"
      />
    </TresCanvas>
</template>

Let's create two simple components

I'm going to add some simple animation logic just to add a little more life.

Box.vue
<script setup>
import { shallowRef } from 'vue'
import { useLoop } from '@tresjs/core'

// Retrieving the object
const boxRef = shallowRef()

const { onBeforeRender } = useLoop()

onBeforeRender(({ elapsed }) => {
  // little animation logic completely optional
  if (boxRef.value) {
    boxRef.value.rotation.y = elapsed * 0.5
    boxRef.value.rotation.x = elapsed * 0.2
  }
})
</script>

<template>
  <TresMesh ref="boxRef">
    <TresBoxGeometry :args="[1, 1, 1]" />
    <TresMeshStandardMaterial color="orange" transparent />
  </TresMesh>
</template>
Sphere.vue
<script setup>
import { shallowRef } from 'vue'
import { useLoop } from '@tresjs/core'

const sphereRef = shallowRef()

const { onBeforeRender } = useLoop()

onBeforeRender(({ elapsed }) => {
  if (sphereRef.value) {
    // Moving my sphere instead of rotating
    sphereRef.value.position.y = Math.sin(elapsed) * 0.5
    sphereRef.value.position.x = Math.cos(elapsed) * 0.2
  }
})
</script>

<template>
  <TresMesh ref="sphereRef">
    <TresSphereGeometry :args="[1, 32]" />
    <TresMeshStandardMaterial color="orange" transparent />
  </TresMesh>
</template>

Using Vue dynamic components

You can use dynamic components just as you would in Vue.js. There are several ways to do this, but here we’ll follow the official Vue example.

main.vue
<script setup>
import { ref } from 'vue'
import { TresCanvas } from '@tresjs/core'
import Sphere from './Sphere.vue'
import Box from './Box.vue'


const current = ref('Box')
const meshes = {
  Box,
  Sphere,
}
const handleComponents = (component) => {
  current.value = component
}
</script>
<template>
    <TresCanvas window-size clear-color="#82DBC5">
      <TresPerspectiveCamera
        :position="[0, 0, 5]"
      />
      <!-- Dynamic component -->
      <component :is="meshes[current]" />

      <TresAmbientLight :intensity="0.5" />
      <TresDirectionalLight
        :position="[5, 5, 5]"
        :intensity="1"
      />
    </TresCanvas>
</template>

Adding UI controls to switch components

To be able to switch between components, lets add a floating UI containing buttons to change between the Box and the Sphere

<template>
  <div class="floating-container">
    <button
      :class="{ isActive: meshes[current] === Box }"
      @click="handleComponents('Box')"
    >
        Cube
    </button>
    <button
      :class="{ isActive: meshes[current] === Sphere }"
      @click="handleComponents('Sphere')"
    >
       Sphere
    </button>
</div>
</template>

Let's add a little CSS.

<style scoped>
.floating-container {
  position: absolute;
  top: 0;
  left: 50%;
  z-index: 10;
  background-color: #f7f7f7;
  color: #333;
  border-radius: 8px;
  padding: 0.25rem;
  display: flex;
  gap: 0.25rem;
  transform: translateX(-50%);
  button {
    padding: 8px 16px;
    border: none;
    background-color: #4caf50;
    color: white;
    cursor: pointer;
    border-radius: 4px;
    font-size: 14px;
  }
}

button.isActive {
  background-color: #388e3c;
}
</style>
You can use KeepAlive if you want component instances to be cached and preserved between component switches

Adding transitions

Wrap the component inside the <transition>

Once we know the power of using dynamic components, we can create more interactive scenes using built-in Vue components! Now let's add a little animation using GSAP and <Transition> components.

For that, the first step is to wrap our dynamic component in a <Transition>.

Very important: we need to tell our component that we're going to handle our animations using JS, not CSS, by using the prop :css="false". Otherwise, the component will search for a DOM element and will fail.

Elements inside Tres.js live inside a canvas, not in the DOM.
<Transition :css="false">
  <component :is="meshes[current]" />
</Transition>

Using JS hooks

Then we can use the provided JS hooks; in this demo, we're going to use @enter and @leave.

<Transition :css="false" @enter="onEnter" @leave="onLeave">
  <component :is="meshes[current]" />
</Transition>

Now it's time to animate!

In case you haven't installed it already, install GSAP as a dependency in your project:

npm install gsap

In our onEnter and onLeave functions, we put our desired animations. As the names indicate, one controls when the element enters the scene and the other when it leaves.

import { gsap } from 'gsap' // don't forget to import GSAP

function onEnter(el) {
  gsap.from(el.material, { duration: 1, opacity: 0 })
}
function onLeave(el, done) {
  gsap.to(el.material, { duration: 0.05, opacity: 0 })
}
Important: note that animating the opacity works here because we set transparent in our materials.

Full example of our main component

<script setup>
import { ref } from 'vue'
import { TresCanvas } from '@tresjs/core'
import { gsap } from 'gsap'
import Sphere from './Sphere.vue'
import Box from './Box.vue'

const current = ref('Box')

const handleComponents = (component) => {
  current.value = component
}

function onEnter(el) {
  gsap.from(el.material, { duration: 1, opacity: 0 })
}
function onLeave(el) {
  gsap.to(el.material, { duration: 0.05, opacity: 0 })
}

const meshes = {
  Box,
  Sphere,
}
</script>

<template>
  <!---->
  <SceneWrapper>
    <div class="floating-container">
      <button :class="{ isActive: meshes[current] === Box }" @click="handleComponents('Box')">Cube</button>
      <button :class="{ isActive: meshes[current] === Sphere }" @click="handleComponents('Sphere')">Sphere</button>
    </div>
    <TresCanvas clear-color="#82DBC5">
      <TresPerspectiveCamera
        :position="[0, 0, 5]"
      />
      <Transition :css="false" @enter="onEnter" @leave="onLeave">
        <component :is="meshes[current]" />
      </Transition>
      <TresAmbientLight :intensity="0.5" />
      <TresDirectionalLight
        :position="[5, 5, 5]"
        :intensity="1"
      />
    </TresCanvas>
  </SceneWrapper>
</template>

<style scoped>
.floating-container {
  position: absolute;
  top: 0;
  left: 50%;
  z-index: 10;
  background-color: #f7f7f7;
  color: #333;
  border-radius: 8px;
  padding: 0.25rem;
  display: flex;
  gap: 0.25rem;
  transform: translateX(-50%);
  button {
    padding: 8px 16px;
    border: none;
    background-color: #4caf50;
    color: white;
    cursor: pointer;
    border-radius: 4px;
    font-size: 14px;
  }
}

button.isActive {
  background-color: #388e3c;
}
</style>