Svelte-Cubed: Adding Motion to 3D Scenes
Tags: svelte, threejs, beginners, tutorial
Published: 2022-01-31 22:29:40

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](https://dev.to/alexwarnes/svelte-cubed-an-introduction-to-3d-in-the-browser-1ea3)**

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](https://www.khanacademy.org/math/algebra2/x2ec2f6f830c9fb89:trig/x2ec2f6f830c9fb89:radians/v/introduction-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:

```html

<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]}
  />

```

![A salmon color octahedron rotating on its y-axis](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ko6ri4d5ncl1sslf35vz.gif)

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

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

![A salmon color octahedron rotating on every axis simultaneously](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s6x37mn5zk7aqqfyjnxs.gif)

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”.

```javascript
let scaleType = “MEDIUM”
```

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

```html
<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>
```

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](https://svelte.dev/tutorial/group-inputs).

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

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

```

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.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/je35wsv7602kbpdesyl2.png)

### 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](more info here) 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.

```html
<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]}
  />
```

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.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6tc3sfnini0kluvo685m.gif)

### 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](more info here)

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:

```javascript
import { tweened } from “svelte/motion”;

let scale = tweened(1);
```

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](https://svelte.dev/tutorial/writable-stores). 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:

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

And inside our mesh:
```html
 <SC.Mesh
    geometry={new THREE.OctahedronGeometry()}
    material={new THREE.MeshStandardMaterial({
      color: new THREE.Color('salmon')
    })}
    rotation={[rotate, rotate, rotate]}
    scale={[$scale, $scale, $scale]}
  />
```

Now look at that transition!


![A salmon color octahedron that smoothly transitions to its new size when small, medium, or large is selected.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bsdrgllordegfsqpz38r.gif)

### 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](https://svelte.dev/examples/easing) out of the box. I’ll use one here, but you should definitely play around with different options!

```javascript
import { elasticOut } from “svelte/easing”;

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

![A salmon color octahedron that has a bouncy elastic transition to its new size when small, medium, or large is selected.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gl128vfa92ovsse3t2y5.gif)

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.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yxcf26r0ij3hbnw2992l.gif)

## References

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

### App.svelte
```html
<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>
```

### Octo.svelte
```html
<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>

```

#### Edits
**24 May 2022:** Updated _octohedron_ to be spelled correctly _octahedron_ (smh)