Efecto de fuegos artificiales en Canvas
Haz clic en el cielo nocturno para desatar un efecto interactivo de partículas con coloridos fuegos artificiales.
Tras activar JavaScript, puedes editar, ejecutar y guardar este ejemplo. El código fuente fijado es el siguiente:
<!doctype html>
<html lang="es" dir="ltr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Efecto de fuegos artificiales en Canvas</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="Haz clic en el cielo nocturno o pulsa Intro para lanzar fuegos artificiales"></canvas>
<header><span class="eyebrow">MAKE A LITTLE CELEBRATION</span><h1>Enciende un deseo</h1><p>Cada clic genera un haz de fuegos artificiales único.</p></header>
<div class="hint">Toca o haz clic en el cielo nocturno</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>