Effet de traînée de particules
Particules lumineuses colorées qui se déplacent en suivant le curseur de la souris ou le toucher.
Après activation de JavaScript, ce cas peut être modifié, exécuté et enregistré. Code source fixe ci-dessous :
<!doctype html>
<html lang="fr" dir="ltr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Effet de traînée de particules</title>
<style>
* { box-sizing: border-box; }
:root { color-scheme: dark; }
body { min-height: 100vh; margin: 0; overflow: hidden; color: #f3f5ff; font-family: "Avenir Next", "Microsoft YaHei", sans-serif; background: #05050d; }
canvas { position: fixed; inset: 0; width: 100%; height: 100%; touch-action: none; cursor: crosshair; }
.noise { position: fixed; inset: 0; pointer-events: none; opacity: .18; background-image: radial-gradient(rgba(255,255,255,.22) .7px, transparent .7px); background-size: 7px 7px; mask-image: linear-gradient(to bottom, black, transparent 85%); }
header { position: fixed; z-index: 2; top: clamp(28px, 8vh, 78px); left: clamp(24px, 7vw, 80px); max-width: 620px; pointer-events: none; }
.eyebrow { color: #71f5d0; font-size: 10px; font-weight: 900; letter-spacing: .28em; }
h1 { margin: 12px 0 14px; font: 500 clamp(46px, 10vw, 96px)/.9 Georgia, "STSong", serif; letter-spacing: -.05em; }
p { max-width: 470px; margin: 0; color: #848aa3; line-height: 1.75; }
.palette { position: fixed; z-index: 3; right: 24px; bottom: 24px; display: flex; align-items: center; gap: 10px; padding: 9px 12px; border: 1px solid #24263a; border-radius: 999px; color: #8e94ad; font-size: 11px; background: rgba(10,10,22,.78); backdrop-filter: blur(10px); }
button { width: 30px; height: 30px; border: 2px solid #fff; border-radius: 50%; background: linear-gradient(135deg, #71f5d0, #6a78ff, #ff6fae); cursor: pointer; box-shadow: 0 0 15px rgba(113,245,208,.35); }
button:focus-visible { outline: 3px solid #fff; outline-offset: 3px; }
</style>
</head>
<body>
<canvas id="particles" aria-label="Génération de trajectoire de particules par déplacement du pointeur"></canvas><div class="noise" aria-hidden="true"></div>
<header><span class="eyebrow">INTERACTIVE LIGHT FIELD</span><h1>Toucher un faisceau de lumière</h1><p>Déplacez la souris ou le doigt, et les particules colorées se développeront le long de la trajectoire, dériveront puis se dissiperont lentement.</p></header>
<div class="palette"><span>Cliquer pour basculer la teinte</span><button id="palette" type="button" aria-label="Changer la teinte des particules"></button></div>
<script>
const canvas = document.querySelector('#particles');
const context = canvas.getContext('2d');
const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
const palettes = [[160, 250], [280, 350], [25, 90]];
let paletteIndex = 0, width = 0, height = 0, particles = [], pointer = { x: 0, y: 0, active: false };
function resize() {
const ratio = Math.min(devicePixelRatio || 1, 2);
width = innerWidth; height = innerHeight;
canvas.width = Math.round(width * ratio); canvas.height = Math.round(height * ratio);
context.setTransform(ratio, 0, 0, ratio, 0, 0);
}
function emit(x, y, amount = 7) {
const range = palettes[paletteIndex];
for (let index = 0; index < (reduceMotion ? Math.min(amount, 2) : amount); index += 1) {
const angle = Math.random() * Math.PI * 2, speed = .3 + Math.random() * 1.8;
particles.push({ x, y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, size: 1.5 + Math.random() * 4, life: 1, decay: .012 + Math.random() * .018, hue: range[0] + Math.random() * (range[1] - range[0]) });
}
if (particles.length > 650) particles.splice(0, particles.length - 650);
}
function movePointer(event) {
const rect = canvas.getBoundingClientRect(); pointer = { x: event.clientX - rect.left, y: event.clientY - rect.top, active: true }; emit(pointer.x, pointer.y);
}
function frame() {
context.fillStyle = 'rgba(5, 5, 13, .16)'; context.fillRect(0, 0, width, height);
context.globalCompositeOperation = 'lighter';
particles = particles.filter(particle => {
particle.x += particle.vx; particle.y += particle.vy; particle.vx *= .985; particle.vy = particle.vy * .985 - .008; particle.life -= particle.decay;
if (particle.life <= 0) return false;
context.globalAlpha = particle.life;
context.fillStyle = `hsl(${particle.hue} 90% 65%)`;
context.beginPath(); context.arc(particle.x, particle.y, particle.size * particle.life, 0, Math.PI * 2); context.fill(); return true;
});
context.globalAlpha = 1; context.globalCompositeOperation = 'source-over'; requestAnimationFrame(frame);
}
canvas.addEventListener('pointermove', movePointer);
canvas.addEventListener('pointerdown', event => { movePointer(event); emit(pointer.x, pointer.y, 28); });
canvas.addEventListener('pointerleave', () => { pointer.active = false; });
document.querySelector('#palette').addEventListener('click', () => { paletteIndex = (paletteIndex + 1) % palettes.length; emit(width * .75, height * .65, 40); });
addEventListener('resize', resize); resize(); emit(width * .72, height * .64, 55); requestAnimationFrame(frame);
</script>
</body>
</html>