Pointer Parallax
The layers of a scene shift with the pointer, near ones further than far ones, so a flat picture opens up into depth. Background layers can drift the other way, and when the pointer leaves everything floats back to the middle.
Depth
How to use it
- Paste the snippet just before
</head>. - Add
data-pointer-parallaxto the scene. - Give each layer inside a
data-depth: 0 stays put, 1 moves the full range, and negative values move the other way.
Options
Add any of these attributes to the same element.
data-range | How far a depth-1 layer moves at the edges, in px. Default 40. |
data-invert | Layers move away from the pointer instead of towards it. |
data-autoplay | A pretend pointer circles slowly while a real one isn’t over the scene. Handy for demos and touch screens. |
Give the scene overflow: hidden and the background layers some extra size, so their edges never come into view.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Pointer Parallax: the layers of a scene shift as the pointer moves over
// it, near ones more than far ones, so a flat picture opens up into depth.
// When the pointer leaves, everything floats back to the middle.
// Usage: <div data-pointer-parallax>
// <img data-depth="0.2" src="sky.png">
// <img data-depth="0.6" src="hills.png">
// <h2 data-depth="1">Foreground</h2>
// </div>
// data-depth on each layer sets how far it moves: 0 stays put, 1 moves the
// full range, and negative values move the other way (for a background
// that drifts against the rest).
// Optional (on the container): data-range (px a depth-1 layer moves at the
// edges, default 40), data-invert (layers move away from the pointer
// instead of towards it), data-autoplay (a pretend pointer circles slowly
// while no real one is over it; handy for demos and touch screens).
var DEFAULTS = { range: 40 };
function setup(el) {
var d = el.dataset;
var range = +d.range || DEFAULTS.range;
var sign = d.invert !== undefined && d.invert !== 'false' ? -1 : 1;
var layers = Array.prototype.slice.call(el.querySelectorAll('[data-depth]')).map(function (n) {
n.style.willChange = 'transform';
return { depth: parseFloat(n.dataset.depth) || 0, anim: anime.createAnimatable(n, { x: 900, y: 900, ease: 'out(3)' }) };
});
if (!layers.length) return;
// Shift every layer for a pointer at (u, v), each -1 … 1 from the centre.
function shift(u, v, duration, ease) {
layers.forEach(function (l) {
l.anim.x(u * l.depth * range * sign, duration, ease);
l.anim.y(v * l.depth * range * sign, duration, ease);
});
}
function at(cx, cy) {
var r = el.getBoundingClientRect();
shift(Math.max(-1, Math.min(1, ((cx - r.left) / r.width) * 2 - 1)), Math.max(-1, Math.min(1, ((cy - r.top) / r.height) * 2 - 1)), 900, 'out(3)');
}
var real = false;
el.addEventListener('pointermove', function (e) {
if (e.pointerType === 'touch') return;
real = true;
at(e.clientX, e.clientY);
});
el.addEventListener('pointerleave', function (e) {
if (e.pointerType === 'touch') return;
real = false;
if (!timer) shift(0, 0, 1600, 'outElastic(1, .6)');
});
// A pretend pointer drifting round in a slow, lopsided loop.
var timer = null;
if (d.autoplay !== undefined && d.autoplay !== 'false') {
var t = 0;
timer = anime.createTimer({
loop: true,
duration: 1000,
autoplay: false,
onUpdate: function () {
if (real) return;
t += 0.012;
shift(0.8 * Math.sin(t), 0.6 * Math.sin(t * 1.7 + 0.6), 600, 'out(2)');
}
});
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) timer.play(); else timer.pause();
}).observe(el);
}
}
function init() {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
document.querySelectorAll('[data-pointer-parallax]').forEach(function (el) {
if (el._ppx) return;
el._ppx = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div class="hero" data-pointer-parallax>
<img data-depth="-0.3" src="sky.png" alt="">
<img data-depth="0.4" src="hills.png" alt="">
<h1 data-depth="0.8">Hello</h1>
</div>