Canvas effet de feu d’artifice
Effet de particules interactif qui fait éclore des feux d’artifice colorés dans le ciel nocturne au clic.
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>Canvas effet de feu d’artifice</title>
<style>
* { box-sizing: border-box; }
body { min-height: 100vh; margin: 0; overflow: hidden; color: #fff; font-family: "Avenir Next", "Microsoft YaHei", sans-serif; background: radial-gradient(ellipse at bottom, #17234b, #080b1b 60%, #03040a); }
body::after { content: ""; position: fixed; left: 0; right: 0; bottom: 0; height: 17vh; background: #03050b; clip-path: polygon(0 62%, 7% 62%, 8% 28%, 10% 28%, 11% 62%, 18% 62%, 20% 45%, 24% 45%, 25% 62%, 36% 62%, 38% 18%, 41% 18%, 42% 62%, 55% 62%, 57% 38%, 61% 38%, 62% 62%, 73% 62%, 75% 12%, 78% 12%, 79% 62%, 88% 62%, 90% 44%, 94% 44%, 95% 62%, 100% 62%, 100% 100%, 0 100%); pointer-events: none; }
canvas { position: fixed; inset: 0; width: 100%; height: 100%; cursor: crosshair; touch-action: none; }
canvas:focus-visible { outline: 3px solid #f4ce6b; outline-offset: -6px; }
header { position: fixed; z-index: 2; top: clamp(28px, 7vh, 70px); left: 50%; width: min(680px, calc(100% - 40px)); text-align: center; transform: translateX(-50%); pointer-events: none; }
.eyebrow { color: #f4ce6b; font-size: 11px; font-weight: 900; letter-spacing: .25em; }
h1 { margin: 10px 0 12px; font: 500 clamp(43px, 9vw, 84px)/1 Georgia, "STSong", serif; text-shadow: 0 8px 36px rgba(0,0,0,.6); }
p { margin: 0; color: #aeb8d2; font-size: 14px; letter-spacing: .05em; }
.hint { position: fixed; z-index: 3; left: 50%; bottom: 28px; padding: 9px 15px; border: 1px solid rgba(255,255,255,.18); border-radius: 999px; color: #c8d0e4; font-size: 11px; background: rgba(5,8,18,.55); transform: translateX(-50%); pointer-events: none; }
</style>
</head>
<body>
<canvas id="fireworks" role="button" tabindex="0" aria-label="Cliquez sur le ciel nocturne ou appuyez sur Entrée pour faire éclore des feux d’artifice"></canvas>
<header><span class="eyebrow">MAKE A LITTLE CELEBRATION</span><h1>Allumer les vœux</h1><p>Chaque clic génère un feu d’artifice unique.</p></header>
<div class="hint">Cliquez ou touchez le ciel nocturne</div>
<script>
const canvas = document.querySelector('#fireworks');
const context = canvas.getContext('2d');
const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
let width = 0, height = 0, rockets = [], particles = [], lastAuto = 0;
const colors = ['#ff5e7d', '#ffd166', '#63e6ff', '#9b8cff', '#73e2a7'];
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 launch(targetX, targetY) {
rockets.push({ x: width / 2 + (Math.random() - .5) * 100, y: height, targetX, targetY, speed: 7 + Math.random() * 2, hue: colors[Math.floor(Math.random() * colors.length)] });
}
function explode(rocket) {
const count = reduceMotion ? 28 : 70;
for (let index = 0; index < count; index += 1) {
const angle = Math.PI * 2 * index / count + Math.random() * .08;
const speed = 1.5 + Math.random() * 4.8;
particles.push({ x: rocket.x, y: rocket.y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, life: 1, decay: .012 + Math.random() * .012, color: rocket.hue });
}
}
function frame(time) {
context.fillStyle = 'rgba(3, 4, 10, .2)'; context.fillRect(0, 0, width, height);
rockets = rockets.filter(rocket => {
const dx = rocket.targetX - rocket.x, dy = rocket.targetY - rocket.y, distance = Math.hypot(dx, dy);
if (distance < rocket.speed) { explode(rocket); return false; }
rocket.x += dx / distance * rocket.speed; rocket.y += dy / distance * rocket.speed;
context.fillStyle = rocket.hue; context.beginPath(); context.arc(rocket.x, rocket.y, 2.2, 0, Math.PI * 2); context.fill(); return true;
});
particles = particles.filter(particle => {
particle.vy += .035; particle.vx *= .99; particle.x += particle.vx; particle.y += particle.vy; particle.life -= particle.decay;
if (particle.life <= 0) return false;
context.globalAlpha = particle.life; context.fillStyle = particle.color; context.beginPath(); context.arc(particle.x, particle.y, 1.8, 0, Math.PI * 2); context.fill(); return true;
});
context.globalAlpha = 1;
if (!reduceMotion && time - lastAuto > 2200) { launch(width * (.2 + Math.random() * .6), height * (.25 + Math.random() * .35)); lastAuto = time; }
requestAnimationFrame(frame);
}
canvas.addEventListener('pointerdown', event => { const rect = canvas.getBoundingClientRect(); launch(event.clientX - rect.left, event.clientY - rect.top); });
canvas.addEventListener('keydown', event => {
if (event.key !== 'Enter' && event.key !== ' ') return;
event.preventDefault();
launch(width * (.3 + Math.random() * .4), height * (.3 + Math.random() * .25));
});
addEventListener('resize', resize); resize(); launch(width * .5, height * .4); requestAnimationFrame(frame);
</script>
</body>
</html>