Voilà une animation en html/js de Feux d'artifices
<!-- === FEUX D'ARTIFICE — SNIPPET AUTONOME === -->
<div
id="fireworks-odoo-1"
class="fw-container"
data-autoplay="true" <!-- "false" pour déclencher seulement au clic -->
data-density="1.0" <!-- 0.3 (léger) → 1.5 (chargé) -->
data-max="450" <!-- nombre max de particules vivantes -->
data-colors="#ff3b3b,#ffd93b,#3bff9c,#3bbdff,#b93bff" <!-- palette -->
data-height="420" <!-- hauteur en px (optionnel) -->
style="position:relative; width:100%; max-width:100%; display:block;"
></div>
<style>
/* Le canvas couvre le conteneur */
.fw-container canvas {
position:absolute; inset:0; width:100%; height:100%;
display:block; pointer-events:auto; touch-action:manipulation;
}
/* Hauteur par défaut si data-height non fourni */
.fw-container { min-height: 360px; }
@media (prefers-reduced-motion: reduce) {
.fw-container { background: radial-gradient(ellipse at bottom, #0b1020, #04070f); }
}
</style>
<script>
(function () {
"use strict";
// ====== UTIL ======
const rand = (min, max) => min + Math.random() * (max - min);
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
const parseColors = (str) =>
(str || "").split(",").map(s => s.trim()).filter(Boolean);
// ====== CLASSES ======
class Particle {
constructor(x, y, vx, vy, color, life, size) {
this.x = x; this.y = y;
this.vx = vx; this.vy = vy;
this.ax = 0; this.ay = 0.06; // gravité
this.drag = 0.995;
this.life = life; this.maxLife = life;
this.size = size;
this.color = color;
this.dead = false;
this.spark = Math.random() < 0.15;
}
step() {
this.vx *= this.drag; this.vy = this.vy * this.drag + this.ay;
this.x += this.vx; this.y += this.vy;
this.life--;
if (this.life <= 0) this.dead = true;
}
draw(ctx) {
const t = this.life / this.maxLife; // 1 → 0
const alpha = Math.pow(t, 1.5);
ctx.globalCompositeOperation = "lighter";
ctx.globalAlpha = alpha;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fillStyle = this.color;
ctx.fill();
if (this.spark && Math.random() < 0.3) {
ctx.globalAlpha = alpha * 0.7;
ctx.fillRect(this.x, this.y, 1, 1);
}
ctx.globalAlpha = 1;
}
}
class Rocket {
constructor(x, y, targetY, hueColor) {
this.x = x; this.y = y;
this.vx = rand(-0.8, 0.8);
this.vy = rand(-9.5, -7.8);
this.targetY = targetY;
this.dead = false;
this.hueColor = hueColor;
this.trail = [];
this.maxTrail = 6;
}
step() {
this.vy += 0.06; // gravité
this.x += this.vx; this.y += this.vy;
this.trail.push({x:this.x, y:this.y});
if (this.trail.length > this.maxTrail) this.trail.shift();
if (this.vy >= 0 || this.y <= this.targetY) this.dead = true; // apogée
}
draw(ctx) {
ctx.globalCompositeOperation = "lighter";
ctx.strokeStyle = this.hueColor;
ctx.lineWidth = 2;
ctx.beginPath();
for (let i=0;i<this.trail.length;i++) {
const p = this.trail[i];
if (i === 0) ctx.moveTo(p.x, p.y); else ctx.lineTo(p.x, p.y);
}
ctx.stroke();
ctx.beginPath();
ctx.arc(this.x, this.y, 2, 0, Math.PI*2);
ctx.fillStyle = this.hueColor; ctx.fill();
}
}
// ====== MOTEUR ======
function createFireworks(container) {
const dpr = Math.max(1, Math.min(2, window.devicePixelRatio || 1));
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d", { alpha: true });
container.appendChild(canvas);
// Options via data-*
const autoplay = (container.dataset.autoplay || "true") === "true";
const density = clamp(parseFloat(container.dataset.density || "1.0"), 0.1, 2.0);
const maxParticles = Math.floor(clamp(parseInt(container.dataset.max || "450", 10), 100, 1200));
const palette = parseColors(container.dataset.colors) ;
const userHeight = parseInt(container.dataset.height || "0", 10);
// Taille
function resize() {
const rect = container.getBoundingClientRect();
const w = Math.floor(rect.width);
const h = Math.floor(userHeight > 0 ? userHeight : rect.height || 360);
canvas.width = Math.max(1, w * dpr);
canvas.height = Math.max(1, h * dpr);
canvas.style.width = w + "px";
canvas.style.height = h + "px";
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
resize();
let ro;
if ("ResizeObserver" in window) {
ro = new ResizeObserver(resize);
ro.observe(container);
} else {
window.addEventListener("resize", resize);
}
// État
const particles = [];
const rockets = [];
let running = true;
let lastTime = performance.now();
let accum = 0;
const spawnInterval = 800 / density; // ms entre rockets auto
const prefersReduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
// Utilitaires
const pickColor = () => {
if (palette.length) return palette[Math.floor(Math.random() * palette.length)];
// fallback: couleurs HSL variées
const h = Math.floor(rand(0, 360));
return `hsl(${h} 100% 60%)`;
};
function explode(x, y) {
const color = pickColor();
const count = Math.floor(rand(60, 120) * density);
const speed = rand(2.5, 4.2);
for (let i = 0; i < count; i++) {
if (particles.length >= maxParticles) break;
const angle = (i / count) * Math.PI * 2 + rand(-0.04, 0.04);
const mag = speed * (0.6 + Math.random() * 0.8);
const vx = Math.cos(angle) * mag;
const vy = Math.sin(angle) * mag;
const life = Math.floor(rand(45, 90));
const size = rand(1.2, 2.4);
particles.push(new Particle(x, y, vx, vy, color, life, size));
}
}
function launchRandom() {
const w = canvas.width / dpr;
const h = canvas.height / dpr;
const x = rand(w * 0.1, w * 0.9);
const y = h + 10;
const targetY = rand(h * 0.18, h * 0.45);
const rocket = new Rocket(x, y, targetY, pickColor());
rockets.push(rocket);
}
// Interaction
function pointer(e) {
const rect = canvas.getBoundingClientRect();
const x = (e.clientX ?? (e.touches && e.touches[0].clientX)) - rect.left;
const y = (e.clientY ?? (e.touches && e.touches[0].clientY)) - rect.top;
explode(x, y);
}
canvas.addEventListener("click", pointer, { passive: true });
canvas.addEventListener("touchend", (e) => { if (e.changedTouches && e.changedTouches[0]) {
const t = e.changedTouches[0];
const rect = canvas.getBoundingClientRect();
explode(t.clientX - rect.left, t.clientY - rect.top);
}}, { passive: true });
// API simple pour Odoo (start/stop)
const api = {
start() { running = true; },
stop() { running = false; },
destroy() {
running = false;
canvas.remove();
ro && ro.disconnect();
window.removeEventListener("resize", resize);
}
};
container._fireworks = api;
if (!window.fireworks) window.fireworks = {};
window.fireworks[container.id] = api;
// Boucle
let spawnTimer = 0;
function frame(now) {
const dt = now - lastTime; lastTime = now;
if (!running) { requestAnimationFrame(frame); return; }
// Effet "traînée"
ctx.globalCompositeOperation = "source-over";
ctx.fillStyle = "rgba(0,0,16,0.25)";
ctx.fillRect(0, 0, canvas.width / dpr, canvas.height / dpr);
// Auto-spawn
if (autoplay && !prefersReduce) {
spawnTimer += dt;
if (spawnTimer >= spawnInterval) {
spawnTimer = 0;
if (particles.length < maxParticles * 0.7) launchRandom();
}
}
// Update/draw rockets
for (let i = rockets.length - 1; i >= 0; i--) {
const r = rockets[i];
r.step(); r.draw(ctx);
if (r.dead) {
explode(r.x, r.y);
rockets.splice(i, 1);
}
}
// Update/draw particles
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.step(); p.draw(ctx);
// purge hors écran ou morts
if (p.dead || p.y > canvas.height / dpr + 50) {
particles.splice(i, 1);
}
}
requestAnimationFrame(frame);
}
requestAnimationFrame((t) => { lastTime = t; frame(t); });
// Pause quand l'onglet n'est pas visible
document.addEventListener("visibilitychange", () => {
running = document.visibilityState === "visible";
});
}
// Init auto pour ce conteneur
document.addEventListener("DOMContentLoaded", () => {
const el = document.getElementById("fireworks-odoo-1");
if (el) createFireworks(el);
});
})();
</script>
<!-- === FIN SNIPPET === -->