Scroll et survol
Faire descendre la caméra au rythme du scroll de la page, puis détecter l’objet sous le pointeur avec un raycaster pour le faire grossir.
import * as THREE from "three";
const SECTION_HEIGHT = 4;
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
100,
);
camera.position.z = 6;
const light = new THREE.DirectionalLight("#ffffff", 3);
light.position.set(1, 1, 1);
const ambientLight = new THREE.AmbientLight("#ffffff", 0.3);
scene.add(light, ambientLight);
const material = new THREE.MeshStandardMaterial({ color: "#d5d30e" });
const objects = [
new THREE.Mesh(new THREE.TorusGeometry(0.6, 0.25, 32, 64), material),
new THREE.Mesh(new THREE.ConeGeometry(0.6, 1.4, 32), material),
new THREE.Mesh(new THREE.TorusKnotGeometry(0.5, 0.18, 128, 16), material),
];
objects.forEach((object, index) => {
object.position.y = -index * SECTION_HEIGHT;
scene.add(object);
});
let scrollY = window.scrollY;
window.addEventListener("scroll", () => {
scrollY = window.scrollY;
});
const renderer = new THREE.WebGLRenderer({
antialias: true,
});
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
let hovered = null;
const mouse = new THREE.Vector2(10, 10);
const raycaster = new THREE.Raycaster();
window.addEventListener("pointermove", (event) => {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
});
renderer.setAnimationLoop(() => {
raycaster.setFromCamera(mouse, camera);
const hits = raycaster.intersectObjects(objects);
hovered = hits.length > 0 ? hits[0].object : null;
for (const object of objects) {
const targetScale = object === hovered ? 1.3 : 1;
object.scale.setScalar(
THREE.MathUtils.lerp(object.scale.x, targetScale, 0.15),
);
}
document.body.style.cursor = hovered ? "pointer" : "";
camera.position.y = -(scrollY / window.innerHeight) * SECTION_HEIGHT;
renderer.render(scene, camera);
});
window.addEventListener("resize", () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
Une page qui défile devant la scène
Ici, la page HTML a un vrai contenu : trois sections de la hauteur de l’écran. Le canvas, lui, reste fixe derrière elles :
canvas {
position: fixed;
top: 0;
left: 0;
z-index: -1;
}position: fixed le colle à l’écran pendant que la page défile, z-index: -1 le passe sous le texte. Fais défiler la démo : c’est la caméra qui descend, au même rythme que les sections.
Un objet par section
const SECTION_HEIGHT = 4;
objects.forEach((object, index) => {
object.position.y = -index * SECTION_HEIGHT;
scene.add(object);
});SECTION_HEIGHT est la distance, en unités 3D, qui correspond à une section (un écran) de la page. Le premier objet est à y = 0, le deuxième à -4, le troisième à -8. Les trois partagent le même matériau : inutile d’en créer un par objet.
Relier le scroll à la caméra
let scrollY = window.scrollY;
window.addEventListener("scroll", () => {
scrollY = window.scrollY;
});Puis, dans la boucle d’animation :
camera.position.y = -(scrollY / window.innerHeight) * SECTION_HEIGHT;scrollY / window.innerHeight compte le nombre d’écrans déjà défilés : 0 en haut, 1 à la deuxième section, 2 à la troisième. En multipliant par SECTION_HEIGHT, la caméra arrive pile sur l’objet de chaque section. Le signe moins la fait descendre quand la page descend.
Si un objet n’est pas centré dans sa section, change SECTION_HEIGHT ou la position z de la caméra : les deux sont liés.
Savoir quel objet est sous le pointeur
Pour savoir ce que le pointeur survole, on lance un rayon invisible depuis la caméra, à travers le pointeur, et on regarde ce qu’il touche : c’est le rôle du raycaster.
const mouse = new THREE.Vector2(10, 10);
const raycaster = new THREE.Raycaster();
window.addEventListener("pointermove", (event) => {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
});Comme dans la doc « Suivre la souris », la position est ramenée entre -1 et 1, mais cette fois le y est inversé tout de suite : le raycaster attend un y qui monte. On démarre à (10, 10), hors de l’écran, pour que rien ne soit survolé avant le premier mouvement.
raycaster.setFromCamera(mouse, camera);
const hits = raycaster.intersectObjects(objects);
hovered = hits.length > 0 ? hits[0].object : null;intersectObjects renvoie la liste des objets touchés, du plus proche au plus lointain. Le premier est celui qu’on voit sous le pointeur.
Grossir en douceur
for (const object of objects) {
const targetScale = object === hovered ? 1.3 : 1;
object.scale.setScalar(
THREE.MathUtils.lerp(object.scale.x, targetScale, 0.15),
);
}
document.body.style.cursor = hovered ? "pointer" : "";lerp (interpolation linéaire) avance de 15 % vers la taille visée à chaque image : l’objet grossit et rétrécit en douceur au lieu de sauter d’une taille à l’autre. Le curseur devient une main au survol, comme sur un lien.
Pour aller plus loin
- Ouvre un lien ou change de couleur au clic : écoute
clicket regarde sihoveredn’est pasnull. - Fais tourner chaque objet sur lui-même dans la boucle.
- Ajoute une quatrième section et un quatrième objet.