A parallax effect creates depth by moving layered elements at different rates as the viewer scrolls or as an animation advances. Distant backgrounds move more slowly than foreground content, while closer simulated objects move faster. section 10.2.4 section 14.2.2
A geometric implementation ties movement directly to depth:
- Assign each layer a data-parallax value. The update uses parallaxOffset = scrollTop × (1 − depth), so smaller depth values produce greater movement. section 10.2.4
- Respect prefers-reduced-motion: reduce and limit the effect on smaller screens because motion can affect people with vestibular problems and can be difficult on mobile. section 10.2.4
Animated star field
A canvas star field gives every star a random z depth. Speed uses 0.25 / z, radius uses (maxDepth − z) / maxDepth × 2, and opacity uses (maxDepth − z) / maxDepth. Nearby stars therefore move faster, appear larger, and look brighter than distant stars. section 14.2.2
Listing 10.3 Geometric parallax scrolling
initializeLayers() {
return Array.from(document
.querySelectorAll('[data-parallax]')) #1
.map(element => {
const depth = parseFloat(element.dataset #2
.parallax) || 1; #2
const rect = element.getBoundingClientRect(); #3
return {
element: element,
depth: depth,
initialY: rect.top + window.scrollY, #4
initialX: rect.left #4
};
});
}
setUpEventListeners() {
window.addEventListener('scroll', () => { #5
if (!this.isScrolling) {
requestAnimationFrame(() => { #6
this.updateParallax(); #6
this.isScrolling = false;
});
this.isScrolling = true;
}
}, { passive: true });
}
updateParallax() {
const scrollTop = window.scrollY; #7
const windowHeight = window.innerHeight; #8
this.layers.forEach(layer => {
const elementTop = layer.initialY - scrollTop; #9
const elementBottom = elementTop + #9
layer.element.offsetHeight; #9
if (elementBottom >= -windowHeight && #10
elementTop <= windowHeight * 2) { #10
const parallaxOffset = scrollTop * #11
(1 - layer.depth); #11
let transform = `translate3d(0, ${parallaxOffset}px, 0)`; #12
layer.element.style.transform = transform; #12
}
});
}
Inspect the complete geometric implementation for layer discovery, initial-position measurement, passive scroll handling, and requestAnimationFrame updates. section 10.2.4
Listing 14.5b Creating a generative star field background effect (part 2)
function animate() {
ctx.clearRect(0, 0, canvas.width,
➥ canvas.height); #1
stars.forEach(star => {
star.x -= star.speed; #2
if (star.x < 0) { #3
star.x = canvas.width; #3
star.y = Math.random() * canvas.height; #4
}
ctx.beginPath(); #5
ctx.arc(star.x, star.y, star.radius, #5
➥ 0, Math.PI * 2); #5
ctx.fillStyle = `rgba(255, 255, 255, #5
➥ ${star.opacity})`; #5
ctx.fill(); #5
}); #5
requestAnimationFrame(animate);
}
window.addEventListener('resize', () => { #6
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
stars.forEach(star => { #7
if (star.x > canvas.width) star.x = Math.random() * canvas.width;
if (star.y > canvas.height) star.y = Math.random() * canvas.height;
});
});
canvas.style.background = 'black';
animate();
Inspect the animation loop that clears the canvas, advances stars, wraps them at the edge, redraws them, and responds to resizing. section 14.2.2
