DEV Community

Cover image for Svelte-Cubed: Adding Motion to 3D Scenes
Alex Warnes
Alex Warnes

Posted on • Updated on

Svelte-Cubed: Adding Motion to 3D Scenes

This article is the second in a beginner series on creating 3D scenes with svelte-cubed and three.js. We’re picking up where we left off, so if you want to learn how we got here you can start from the beginning:

Part One: Svelte-Cubed: An Introduction to 3D in the Browser

In this article we will cover two different approaches to moving things around in your scene with constant motion using onFrame and on-demand motion using svelte’s tweened stores and easing functions.

Here’s the REPL where part one left off to get you started:
https://svelte.dev/repl/71b063fc410543598e8a727999cf7bbe

Now let’s get moving!

Constant Motion

That octahedron is cool, but it’s the kind of shape that would be cooler if it were spinning all the time. What we want to do is adjust the rotation a little bit, multiple times per second. First thought might be to use JavaScript’s setInterval and make an update every x number of milliseconds. But there’s a more performant way!

Svelte-cubed gives us a method called onFrame(callback) that accepts a callback method where we can make some change to our scene on each frame. Let’s give it a try.

A mesh has a rotation property that accepts an array with x, y, z radian values that each describe the mesh’s rotation along the respective axis (you can brush up on your radians here: Khan Academy: Intro to Radians). We want to rotate our mesh along the y-axis, so we’ll declare a rotate variable, update it inside the onFrame callback, and then pass it into our mesh in the Octo.svelte file:


<script>
// … 

// Declare our variable
let rotate = 0;

SC.onFrame(() => {
  // Every frame, assign these radians to rotationY
  rotationY += .01;
})
</script>

<!-- add the rotation property to the mesh and use our new variable -->
 <SC.Mesh
    geometry={new THREE.OctahedronGeometry()}
    material={new THREE.MeshStandardMaterial({
      color: new THREE.Color('salmon')
    })}
    rotation={[0, rotate, 0]}
  />

Enter fullscreen mode Exit fullscreen mode

A salmon color octahedron rotating on its y-axis

It’s moving! I just drank a red bull, so let’s go big and use the rotation variable for ALL THREE mesh axes:

 <SC.Mesh
    geometry={new THREE.OctahedronGeometry()}
    material={new THREE.MeshStandardMaterial({
      color: new THREE.Color('salmon')
    })}
    rotation={[rotate, rotate, rotate]}
  />
Enter fullscreen mode Exit fullscreen mode

A salmon color octahedron rotating on every axis simultaneously

If that’s too spintense for your taste, adjust as you will. Experiment with the amount of rotation we used in the onFrame callback (but don’t forget we’re using radians!).

This motion makes our mesh a lot more visually engaging, and you can see how it would be helpful for something like a planet. Remember we can use this approach for updating any value: rotation, position, or scale.

But what if we have some kind of motion that we only want to happen once? Say for example we want to toggle our octahedron size between small, medium, and large. It would be cumbersome (and perform poorly) to add a bunch of conditional logic inside the onFrame call back. Svelte gives us the perfect tool for the job with a tweened store.

Transitional Motion: On-Demand

Let’s break down what we want:

  1. A set of radio inputs: small, medium, large (medium by default)
  2. When I select a radio input, the octahedron should scale to match the selected size
  3. The scaling should transition smoothly from one size to another

STEP 1: Svelte Radio Input Binding (bonus Lesson)

We’ll create a variable to hold the selection called scaleType and set the initial value to “MEDIUM”.

let scaleType = MEDIUM
Enter fullscreen mode Exit fullscreen mode

Nailed it. Now below our canvas markup, we’ll create three radio inputs with labels:

<div class="controls">
    <label>
    SMALL
        <input type="radio" bind:group={scaleType} value="SMALL" />
    </label>
    <label>
    MEDIUM
        <input type="radio" bind:group={scaleType} value="MEDIUM" />
    </label>
    <label>
    LARGE
        <input type="radio" bind:group={scaleType} value="LARGE" />
    </label>
</div>
Enter fullscreen mode Exit fullscreen mode

Notice that each input has a value and we bind all of them to the scaleType variable. This is some classic Svelte simplicity, no event handling to worry about. If you want to learn more about group bindings for radio and checkbox inputs, check out the official tutorial.

But we still can’t see anything, so add a style block under all the markup:

<style>
    .controls {
        position: absolute;
        top: .5rem;
        left: .5rem;
        background: #00000088;
        padding: .5rem;
        color: white;
    }
</style>

Enter fullscreen mode Exit fullscreen mode

It should look something like this:

A salmon octahedron in front of a seagreen background with a form in the upper left corner containing small, medium, large radio buttons.

STEP 2: Reactive Scaling

Our scaleType variable updates whenever the selection changes, and any time scaleType changes we want to update the mesh’s scale properties. Back up in our script, let’s use another wonderful Svelte feature called a reactive statement https://svelte.dev/tutorial/reactive-statements to do just that.

Below we'll declare a variable called scale with an initial value of 1, setup a reactive statement to update scale based on scaleType, and then add the scale array to our mesh and use our scale value for the x, y, and z scaling.

<script>
// … other stuff in our script

let scale = 1;

// reactive statement
$: if (scaleType === "SMALL"){
    scale = .25;
} else if (scaleType === "MEDIUM"){
    scale = 1;
} else if (scaleType === "LARGE") {
    scale = 1.75;
}
</script>

<!-- … other stuff in our markup -->
 <SC.Mesh
    geometry={new THREE.OctahedronGeometry()}
    material={new THREE.MeshStandardMaterial({
      color: new THREE.Color('salmon')
    })}
    rotation={[rotate, rotate, rotate]}
    scale={[scale, scale, scale]}
  />
Enter fullscreen mode Exit fullscreen mode

I know, that was a lot. Just to review:

  • the inputs control the scaleType small/medium/large
  • the scale variable reacts to any change to the scaleType
  • we pass the scale value into our mesh’s scale array

A salmon color octahedron that immediately changes size when small, medium, or large is selected.

STEP 3: Smooth Transitions

Right now our mesh jumps directly from one scale to another, and what we’d like to see is a smooth transition. When we go from small (.5) to large (1.75) we’d like that to take about 2 seconds and progress through a handful of values in between the current scale and the next scale. Svelte provides something called a tweened store for just this type of thing! https://svelte.dev/tutorial/tweened

A tweened store is like a fancy variable that we can give a value, say 1. When we update that value to, say 1.75, the store value will shift to that value over time based on how we configure it. Let’s see what that looks like by updating our scale to be a tweened store and seeing what breaks:

import { tweened } from svelte/motion;

let scale = tweened(1);
Enter fullscreen mode Exit fullscreen mode

Everything is broken! That’s because tweened stores are fancy variables (they’re actually just objects) and we need to access them in a special way more info here. The shorthand way for reading and writing the value of a svelte store is with the $ prefix. So everywhere we read or write to scale needs to be updated to $scale:

Inside our reactive statement:

// reactive statement
$: if (scaleType === "SMALL"){
    $scale = .25;
} else if (scaleType === "MEDIUM"){
    $scale = 1;
} else if (scaleType === "LARGE") {
    $scale = 1.75;
}
Enter fullscreen mode Exit fullscreen mode

And inside our mesh:

 <SC.Mesh
    geometry={new THREE.OctahedronGeometry()}
    material={new THREE.MeshStandardMaterial({
      color: new THREE.Color('salmon')
    })}
    rotation={[rotate, rotate, rotate]}
    scale={[$scale, $scale, $scale]}
  />
Enter fullscreen mode Exit fullscreen mode

Now look at that transition!

A salmon color octahedron that smoothly transitions to its new size when small, medium, or large is selected.

Customizing Transitions

That’s a smooth transition, but you know what would make it even better? Changing the duration and the easing. Well that’s just the second argument to a tweened store! And of course Svelte provides a ton of great easing functions out of the box. I’ll use one here, but you should definitely play around with different options!

import { elasticOut } from svelte/easing;

let scale = tweened(1, { duration: 2000, easing: elasticOut });
Enter fullscreen mode Exit fullscreen mode

A salmon color octahedron that has a bouncy elastic transition to its new size when small, medium, or large is selected.

In the next article we’ll cover animation accessibility concerns and how to use prefers-reduced-motion to avoid using animations for users who don’t want them, and how to accommodate screens with varying frame-rates so your animations can look consistent across devices.

Nice work!

A robot giving you a thumbs up.

References

REPL: https://svelte.dev/repl/9b3b351fe187421b84a6f1616e2c9e3d

App.svelte

<script>
  import Octo from "./Octo.svelte";
</script>
<h1>What is an Octahedron?</h1>

<div class="scene-container">
    <Octo></Octo>
</div>

<p>An octahedron is a three-dimensional shape having eight plane faces, especially a regular solid figure with eight equal triangular faces.</p>

<style>
    .scene-container {
        /* position relative let's the canvas position itself relative to this container */
        position: relative; 
        width: 75%;
        max-width: 400px;
        height: 400px;
        margin: 0 auto;
    }
</style>
Enter fullscreen mode Exit fullscreen mode

Octo.svelte

<script>
    import * as THREE from "three";
    import * as SC from "svelte-cubed";
    import { tweened } from "svelte/motion"
    import { elasticOut } from "svelte/easing"

    let scaleType = "MEDIUM";
    let scale = tweened(1, {duration: 2000, easing: elasticOut});

    // reactive statement to update scale based on scaleType
    $: if (scaleType === "SMALL"){
        $scale = .25;
    } else if (scaleType === "MEDIUM"){
        $scale = 1;
    } else if (scaleType === "LARGE") {
        $scale = 1.75;
    }

    let rotate = 0;
    SC.onFrame(() => {
        rotate += .01;
    })
</script>

<SC.Canvas background={new THREE.Color('seagreen')}>
    <SC.AmbientLight
    color={new THREE.Color('white')}
    intensity={.5}
  />  
    <SC.DirectionalLight
    color={new THREE.Color('white')}
    intensity={.75}
    position={[10, 10, 10]}
  />

  <!-- MESHES -->
   <SC.Mesh
    geometry={new THREE.OctahedronGeometry()}
    material={new THREE.MeshStandardMaterial({
      color: new THREE.Color('salmon')
    })}
        rotation={[rotate, rotate, rotate]}
    scale={[$scale, $scale, $scale]}
  />

  <!-- CAMERA -->
  <SC.PerspectiveCamera near={1} far={100} fov={55}>

    </SC.PerspectiveCamera>
            <SC.OrbitControls />

<!-- all of our scene stuff will go here! -->

</SC.Canvas>

<div class="controls">
    <label>
    SMALL
        <input type="radio" bind:group={scaleType} value="SMALL" />
    </label>
    <label>
    MEDIUM
        <input type="radio" bind:group={scaleType} value="MEDIUM" />
    </label>
    <label>
    LARGE
        <input type="radio" bind:group={scaleType} value="LARGE" />
    </label>
</div>

<style>
    .controls {
        position: absolute;
        top: .5rem;
        left: .5rem;
        background: #00000088;
        padding: .5rem;
        color: white;
    }
</style>

Enter fullscreen mode Exit fullscreen mode

Edits

24 May 2022: Updated octohedron to be spelled correctly octahedron (smh)

Discussion (2)

Collapse
andriusmv profile image
Andres Moreno Vasquez

Is it possible to Load models (GLTF, obj, FBX or similar) into SvelteCubed? if so, how? Thanks! great article!

Collapse
alexwarnes profile image
Alex Warnes Author

Absolutely! Svelte Cubed has a <SC.Primitive /> element you can use to drop any 3d object into. You would use threejs GLTFLoader like usual, and then just pass it to the element like <SC.Primitive object={myGLTFScene} />

For reference, you can add glTF models and see what the the svelte-cubed code looks like using this tool (disclosure, I made it for just this purpose!): sc3-lab.netlify.app