Create a THREE.JS Advanced Object Wheel
I offer an upgrade to a basic THREE.JS object wheel: Snapping points and visual feedback.
This tutorial builds off of a previous tutorial where I built a BASIC THREE.JS image wheel.
You’ll need to understand that code before moving on.
In this tutorial we’re going to add two features to our wheel:
Snapping points — the ability to choose an angle within your wheel and have the wheel automatically “snap back” to that point.
Active card visual feedback — the card the user spins to will scale upwards while other cards scale downwards
Let’s begin.
We’ll start by creating some variables.
let wheel_theta = 0.0;
let spin_in_progress = false;
let snap_in_progress = false;
const snap_point = {
x: 0,
y: 0,
theta: 0.0
}
Note:
1. wheel_theta — keep track of where we’ve spun the wheel
2. spin_in_progress — keep track of when the wheel is spinning
3. snap_in_progress — keep track of when the wheel is automatically spinning to the snapping point
4. snap_point — used to calculate properties of our snapping point
Next, we’ll need to build some sort of mechanism for knowing WHEN the wheel has stopped spinning, so we can tell our wheel to “snap back” to the active card.
We’ll achieve this with a ‘setTimeout()’ function.
Every time our wheel spins, we’ll set a timer to call the ‘snap_back()’ function.
We’ll also CLEAR/CANCEL that timer every time the wheel spins, EXCEPT FOR THE LAST SPIN.
let scroll_speed = 0.0;
window.addEventListener('wheel', event => {
if (!snap_in_progress) {
clearTimeout(spin_in_progress); scroll_speed = event.deltaY * (Math.PI / 180) * 0.2; group_cards.rotation.z += -1.0 * scroll_speed; for (let i = 0; i < group_cards.children.length; i++) {
group_cards.children[i].scale.set(1, 1, 1);
group_cards.children[i].rotation.z +=
scroll_speed;
} spin_in_progress = setTimeout(() => {
snap_back();
}, 100);
} else {
return;
}
});
Note:
We’ve also included an ‘if’ conditional. If we’re in the middle of
“snapping back”, we don’t want to allow the user to spin the wheel manually.
The ‘snap_back()’ function.
Two things need to happen here.
We need to know which image/object is closest to our snapping point.
We need to know what the distance between our snapping point and active image/object is in radians.
To start off, let’s calculate the ‘x’, ‘y’, and ‘theta’ (angle of) our snapping point.
We can get the ‘x’ and ‘y’ from ‘.position()’.
We can then get the ‘theta’ (angle of) our snapping point by using a bit of Trigonometry; specifically, the inverse tan() function.
‘Tan’ of ‘y’ over ‘x’ is equal to the ‘theta’ of that point on a unit circle.
// ...
const snap_point = {
x: Math.floor(group_cards.children[2].position.x),
y: Math.floor(group_cards.children[2].position.y),
theta: 0.0
}
snap_point.theta = Math.abs(Math.atan2(snap_point.y, snap_point.x));
// ...
Now that we have the properties of the snapping point solidified, we’ll use the Pythagorean Theorem to calculate the distance between two points; the snapping point and one image/object on our wheel.
We’ll then loop through our entire wheel calculating the distance between the snapping point and every image/object. The smallest number will obviously be the shortest distance. The shortest distance will obviously be the closet card.
The closest card is the ‘active card’; we’ll save this card by its index.
To find the angle between our snapping point and active card, we’ll use Trigonometry again and some basic arithmetic.
Note:
1. The ‘if’ tree on lines 34–42 are used to determine which way to spin the wheel based on which QUADRANT the snapping point and image/object fall on.
It looks A LOT more complicated than it actually is.
2. Once we determine the ‘theta_between’, we set a delay timer for ‘100ms’ and…
- save the ‘wheel_theta’ BEFORE snapping,
- set ‘snap_in_progress’ to true,
- and set a ‘target’ which we’ll use later on in the video.
3. We don’t actually move the wheel in the ‘snap_back()’ function. That gets handled in the animation loop.
Speaking of which, let’s animate this wheel.
Filling out our ‘animate()’ function.
We’ll start by creating some variables.
const clock = new THREE.Clock();
let delta = 0.0;
let duration = 3;
let current_time = duration;
let target = 0.0;
Note:
clock — a THREE.JS object used to help us sync up a frame rate for the animation
delta — the change in time from one frame to the next; used to standardize a frame; we’ll use this to help us “drain” the ‘current_time’ variable
duration — the max time it takes for our “snapping” animation to play out
current_time — a counter for our animation; starts at ‘duration’ and counts down to 0
target — the distance in radians between our snapping point and active card
Next, we’ll build an ‘if’ conditional structure that tells our wheel when it should be “snapping back”.
When our timer ‘current_time’ reaches below zero, we’ll reset the state of our wheel.
‘current_time’ will be set back up to ‘duration’.
‘snap_in_progress’ will be set back to ‘false’.
‘target’ will be set back to ‘0.0’.
‘wheel_theta’ will be SAVED as the final position of the wheel AFTER “snapping back”.
Note:
The formula “wheel_theta + (target * (1.0 — (current_time / duration)))” is incredibly hard to explain in text.
I’ll try my best here.
‘current_time’ starts out at the same value as ‘duration’.
So ‘(current_time / duration)’ starts at 1.0 or 100%.
As ‘current_time’ decreases because of ‘current_time -= delta’, ‘(current_time / duration)’ goes from 1.0 to 0.0 or 100% to 0%.
However, when we put a ‘1.0 -‘ in front of ‘(current_time / duration)’, to make ‘1.0 -(current_time / duration)’,
we flip the ‘100% to 0%’ pattern
to a ‘0% to 100%’ pattern
So we’ve just built a mechanism that counts up from 0.0 to 1.0 based off of an initial time duration.
When we multiply this mechanism by a number, ‘target’, we go from
‘0.0 to target’.
If we add this relationship to another number, the distance we have to move to get to the snapping point, then we have a way of steadily moving from:
A — where our wheel starts out BEFORE moving to the snapping point
B — where our wheel ends up AFTER moving to the snapping point
Let’s round this thing off by providing visual feedback as to which card is the active card.
We simply add to our ‘animate()’ function within our ‘for loop’ the conditional that…
- if we are rotating the active card, we’ll scale it up by 1 point to a total of 2 (using the same mechanism we built earlier)
- any other card will be scaled down by 0.2 points to a total of 0.8
If you would like a more in-depth guide, check out my full video tutorial on YouTube, An Object Is A.