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]}
/>
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]}
/>
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:
- A set of radio inputs: small, medium, large (medium by default)
- When I select a radio input, the octahedron should scale to match the selected size
- 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”
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>
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>
It should look something like this:
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]}
/>
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 thescaleType
- we pass the
scale
value into our mesh’s scale array
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);
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;
}
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]}
/>
Now look at that transition!
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 });
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!
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>
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>
Edits
24 May 2022: Updated octohedron to be spelled correctly octahedron (smh)
Top comments (3)
Is it possible to Load models (GLTF, obj, FBX or similar) into SvelteCubed? if so, how? Thanks! great article!
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
Hi there. Is it possible to add multiple elements/shapes side by side and interact with them independently? If so, how can I go about doing this? Thanks