Planted: May 2025
Status: seed
Hits: 474
Intended Audience: Creative coders, Front-end developers
Tech: three.js, GSAP
This note details how to make a camera move around a three.js scene as the user scrolls.
Two layers
The page is made of two layers:
- ▪ Background: A three.js scene, position: fixed so it remains within the viewport as the user scrolls.
- ▪ Foreground: A column of sections, each containing text.
Intersection observer
When a section intersects the center of the viewport, the scene's camera changes to a different position. This is controlled using a data-camera-state attribute on each section, with values: floor, cube, cone, or sphere. An intersection observer watches the sections and triggers a move camera function based on the attribute.
...
<main>
<section data-camera-state="floor">
...
function initIntersectionObserver(camera) {
const copySections = document.querySelectorAll(
`.${config.classNames.copySection}`
);
if (copySections.length === 0) {
throw new Error(`Elements not found`);
}
const options = { threshold: "0.5" };
function onIntersect(entries) {
if (config.gui.enableOrbitControls) {
return;
}
entries.forEach((entry) => {
if (entry.isIntersecting) {
const state = entry.target.getAttribute(config.attributes.cameraState);
if (state) {
moveCameraImpl({ camera, state });
}
}
});
}
const observer = new IntersectionObserver(onIntersect, options);
copySections.forEach((section) => {
observer.observe(section);
});
}
To avoid rapid camera movement, each section needs a minimum height of at least half the viewport height — I'm using 80vh.
Camera positions
Each camera state has:
- ▪ position: the camera's coordinates (x, y, z)
- ▪ lookAt: the coordinates the camera is looking at
const config = {
camera: {
positions: {
floor: {
position: [-1, 17, 0],
lookAt: [-1, 0, 0]
},
cube: {
position: [11, 3, 9],
lookAt: [-2, 0, 3]
},
...
To make it easy to capture desired camera positions, I've added a GUI option enableOrbitControls. When enabled, the page enters a dev mode:
- ▪ The Intersection Observer is disabled
- ▪ Orbit controls are enabled
- ▪ A listener logs the camera position and target to the console whenever the camera moves
This lets us freely explore the scene. When we find a position we like, we can copy and paste the logged values into the config object.
function initLoggingCameraPosition({ controls, camera }) {
function logCameraPosition() {
if (!config.gui.enableOrbitControls) {
return;
}
const { position } = camera;
const { target } = controls;
const positionImpl = [position.x, position.y, position.z].map(Math.round);
const targetImpl = [target.x, target.y, target.z].map(Math.round);
console.log("Camera Position:", positionImpl);
console.log("Camera Target:", targetImpl);
const data = {
position: positionImpl,
target: targetImpl
};
const json = JSON.stringify(data);
navigator.clipboard.writeText(json);
}
controls.addEventListener("change", logCameraPosition);
}
Moving the camera
GSAP is used to animate the camera movement. For position, we apply the config's position coordinates to camera.position. For rotation, instead of changing camera.rotation (Euler angles), we use camera.quaternion. While Euler angles (x, y, z) are intuitive, they can cause issues like gimbal lock and result in choppy transitions. Quaternions are a more robust way to represent 3D rotation and avoids those issues. Note: camera.rotation and camera.quaternion are linked - updating one automatically updates the other.
function getLookAtQuaternion({ position, lookAt }) {
const tempCam = new THREE.PerspectiveCamera();
tempCam.position.copy(new THREE.Vector3(...position));
tempCam.lookAt(new THREE.Vector3(...lookAt));
return tempCam.quaternion.clone();
}
function moveCamera({ camera, position, lookAt }) {
const targetQuat = getLookAtQuaternion({ position, lookAt });
gsap.to(camera.position, {
x: position[0],
y: position[1],
z: position[2],
...config.camera.animation
});
gsap.to(camera.quaternion, {
x: targetQuat.x,
y: targetQuat.y,
z: targetQuat.z,
w: targetQuat.w,
...config.camera.animation,
onUpdate: () => camera.updateMatrixWorld()
});
}
Feedback
Have any feedback about this note or just want to comment on the state of the economy?
Where to next?
YOU ARE HERE