🏆 Partie terminée — Classement final
Confetti Cannon
Deux canons aux coins inférieurs propulsent des confettis en papier métallisé — rectangles avec éclairage par normale 3D, rubans torsadés bicolores, étoiles, cercles et 3 couches de parallaxe. Tir au clic, API window.ConfettiCannon.
Cet effet est gratuit — la catégorie Atmosphère en contient 57 au total, dont 10 gratuits. Effect.Labs regroupe 811 effets vanilla. Découvrir la catégorie →
Exemples d'usage
Abonnement Starter · 9,90 €/mois · sans engagement
Comment ça marche
Un seul requestAnimationFrame pilote toutes les scènes de la page. Chaque instance Scene maintient un tableau de particules actives (≤ 450) et un pool de bruine ambiante (≤ 55 particules légères qui dérivent en continu).
À chaque tir, deux canons s'ouvrent depuis les coins bas avec un éclair de bouche (dégradé radial en mode composite:lighter) et 7 étincelles courtes par canon. Les confettis partent en 5 vagues étalées sur 150 ms (60 % immédiat, 40 % différé) pour éviter un pic de spawn.
Les rectangles et carrés calculent leur éclairage par normale 3D : rotX + rotY → vecteur normal de surface → produit scalaire avec la source lumineuse (haut-gauche-avant) → composante diffuse ± 50 % de luminosité + glint spéculaire blanc quand diff > 0,60. Les rubans torsadés (serpentins) varient leur largeur apparente par segment (|cos(twist)|) pour simuler la torsion. La parallaxe répartit les particules sur 3 couches (lointain · milieu · proche) avec des échelles et des vitesses différentes ; la couche proche reçoit un léger motion-blur (fantôme semi-transparent décalé de 2,2 px dans la direction du vecteur vitesse).
Le DPR (devicePixelRatio) est pris en compte au redimensionnement : le canvas est mis à l'échelle physique, le contexte est transformé, les particules restent en coordonnées CSS. Un ResizeObserver recalibre automatiquement quand le conteneur change de taille.
Accessibilité
- prefers-reduced-motion : si réduit, un unique tir court (20 particules, 0,7× vitesse) se déclenche à l'initialisation puis l'animation s'arrête — aucune boucle perpétuelle.
- La scène porte
role="img"etaria-labeldescriptif ; le canvas est marquéaria-hidden="true". - Le bouton « Célébrer » est un
<button>natif, focusable au clavier, avec:focus-visibleen doré (#fbbf24). - Le bouton Pause respecte WCAG 2.2.2 (mécanisme de pause pour toute animation auto-lancée de plus de 5 secondes).
- La bruine ambiante est désactivée via
prefers-reduced-motion: reduce(la fonctiondrizzle()retourne immédiatement).
Compatibilité navigateur
Nécessite Canvas 2D et requestAnimationFrame — supportés dans tous les navigateurs modernes. Zéro dépendance externe.
Sans support Canvas, la scène reste vide (aucune erreur JS fatale) ; le CTA et le texte restent lisibles.
Le code
Copiez les trois blocs dans votre page. Aucune dépendance.
<div class="ccn-scene" id="ccnScene"
role="img"
aria-label="Confetti Cannon premium — deux canons avec éclairage 3D, parallaxe et étincelles"
data-colors="#FF6B6B,#FFD166,#06D6A0,#9B5DE5,#F0F4F8,#FF8E53,#00BBF9,#F15BB5,#FEE440,#FDFFB6">
<canvas class="ccn-canvas" aria-hidden="true"></canvas>
<div class="ccn-label" aria-hidden="true">Clic · ou</div>
<button class="ccn-celebrate-btn" aria-label="Lancer les canons à confettis">🎉 Célébrer</button>
</div>
/* -- Scène (conteneur) -- */
.ccn-scene {
position: relative;
width: 100%;
aspect-ratio: 16 / 9;
border-radius: 14px;
overflow: hidden;
isolation: isolate;
cursor: crosshair;
contain: layout style paint;
background: #0a0a0f;
}
/* -- Canvas -- */
.ccn-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
display: block;
}
/* -- Label démo (bas-gauche) -- */
.ccn-label {
position: absolute;
bottom: 10px;
left: 14px;
pointer-events: none;
z-index: 1;
color: rgba(148, 163, 184, 0.35);
font-size: clamp(9px, 1.2vw, 11px);
letter-spacing: 0.10em;
text-transform: uppercase;
font-weight: 500;
line-height: 1.5;
}
/* -- Bouton Célébrer (dans la démo) -- */
.ccn-celebrate-btn {
position: absolute;
bottom: 14px;
left: 50%;
transform: translateX(-50%);
z-index: 2;
background: rgba(255, 255, 255, 0.10);
border: 1px solid rgba(255, 255, 255, 0.22);
color: #f1f5f9;
padding: 8px 22px;
border-radius: 24px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: background 0.14s, transform 0.10s;
outline: 2px solid transparent;
letter-spacing: 0.01em;
}
.ccn-celebrate-btn:hover { background: rgba(255,255,255,0.18); }
.ccn-celebrate-btn:active { transform: translateX(-50%) scale(0.96); }
.ccn-celebrate-btn:focus-visible { outline-color: #fbbf24; outline-offset: 2px; }
/* -- Contraste fort (toggle review) -- */
.ccn-scene.ccn-hc { filter: contrast(1.45) brightness(1.12); }
/* -- Exemple d'usage : carte commande confirmée -- */
.ccn-order-scene {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
border-radius: 14px;
}
.ccn-order-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
pointer-events: none;
}
.ccn-order-card {
background: rgba(15, 15, 22, 0.88);
border: 1px solid rgba(255, 255, 255, 0.10);
border-radius: 18px;
padding: clamp(20px,4%,32px) clamp(24px,5%,44px);
text-align: center;
pointer-events: all;
box-shadow: 0 8px 32px rgba(0,0,0,0.60);
min-width: 220px;
}
.ccn-order-icon { font-size:38px; line-height:1; margin-bottom:10px; }
.ccn-order-title { margin:0 0 4px; font-size:clamp(15px,2.2vw,19px); font-weight:700; color:#f1f5f9; letter-spacing:-0.02em; }
.ccn-order-ref { margin:0 0 18px; font-size:clamp(11px,1.4vw,13px); color:#52525b; }
.ccn-order-btn {
background: linear-gradient(135deg,#f97316,#f59e0b);
border: none; color:#fff; padding:10px 26px; border-radius:10px;
font-size:clamp(12px,1.5vw,14px); font-weight:700; cursor:pointer;
transition:opacity 0.14s,transform 0.10s;
outline:2px solid transparent; letter-spacing:0.01em;
}
.ccn-order-btn:hover { opacity:0.88; }
.ccn-order-btn:active { transform:scale(0.96); }
.ccn-order-btn:focus-visible { outline-color:#fbbf24; outline-offset:2px; }
/* -- prefers-reduced-motion -- */
@media (prefers-reduced-motion: reduce) {
.ccn-celebrate-btn, .ccn-order-btn { transition:none; }
.ccn-celebrate-btn:active { transform:translateX(-50%); }
.ccn-order-btn:active { transform:none; }
}
/* -- prefers-reduced-transparency -- */
@media (prefers-reduced-transparency: reduce) {
.ccn-order-card { background:#0f0f16; border-color:#3f3f46; }
}
/* -- Responsive mini -- */
@media (max-width:480px) {
.ccn-order-card { padding:16px 18px; min-width:180px; }
.ccn-celebrate-btn { font-size:12px; padding:7px 16px; }
}
(function () {
'use strict';
var REDUCE = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var PI2 = Math.PI * 2;
var PI = Math.PI;
/* ── Physique ──────────────────────────────────────────────────────────── */
var G = 0.17; // gravité (légèrement moins pour flottement premium)
var DRAG = 0.976; // traînée aérienne
var WOB = 0.42; // amplitude flottement feuille morte
/* ── Vecteur lumière normalisé (haut-gauche-avant) ─────────────────────── */
var LX = 0.32, LY = -0.58, LZ = 0.75;
/* ── 3 couches de profondeur ─────────────────────────────────────────────
scale : facteur taille particule
vs : facteur vitesse initiale
alpha : opacité de base
blur : coefficient de motion-blur (0 = aucun) */
var LAYERS = [
{ scale:0.44, vs:0.54, alpha:0.50, blur:0 }, /* 0 = lointain */
{ scale:0.82, vs:0.84, alpha:0.84, blur:0 }, /* 1 = milieu */
{ scale:1.28, vs:1.32, alpha:1.00, blur:0.30 } /* 2 = proche */
];
/* ── Palette premium par défaut ──────────────────────────────────────────
Corail · Or · Turquoise · Violet · Blanc nacré · Orange · Cyan · Rose · Jaune · Crème */
var COLORS_DEFAULT = ['#FF6B6B','#FFD166','#06D6A0','#9B5DE5','#F0F4F8','#FF8E53','#00BBF9','#F15BB5','#FEE440','#FDFFB6'];
var MAX_PARTS = 450;
/* ── Utilitaires ─────────────────────────────────────────────────────────*/
function rnd(a,b) { return a + Math.random()*(b-a); }
function pick(arr){ return arr[(Math.random()*arr.length)|0]; }
function clamp(v,a,b){ return v<a?a:v>b?b:v; }
function sgn(){ return Math.random()<0.5?1:-1; }
/* ── Couleur sombre (dos de particule) ───────────────────────────────────*/
function hexDark(hex) {
var r=parseInt(hex.slice(1,3),16), g=parseInt(hex.slice(3,5),16), b=parseInt(hex.slice(5,7),16);
return 'rgb('+Math.max(0,r-60)+','+Math.max(0,g-60)+','+Math.max(0,b-60)+')';
}
/* ── Applique éclairage : diff ∈ [-1,1] → luminosité +/- 50% ─────────── */
function applyLight(hex, diff) {
var r=parseInt(hex.slice(1,3),16), g=parseInt(hex.slice(3,5),16), b=parseInt(hex.slice(5,7),16);
var f = 1 + clamp(diff,-1,1)*0.50;
return 'rgb('+Math.round(clamp(r*f,0,255))+','+Math.round(clamp(g*f,0,255))+','+Math.round(clamp(b*f,0,255))+')';
}
/* ── Fabricants de particules ─────────────────────────────────────────── */
/* Rectangle avec éclairage 3D (rotX + rotY) */
function mkRect(x,y,vx,vy,col,layer) {
var L=LAYERS[layer];
return { t:'r', x:x,y:y, vx:vx*L.vs,vy:vy*L.vs,
w:rnd(10,20)*L.scale, h:rnd(4,9)*L.scale,
rotY:rnd(0,PI2), rotX:rnd(0,PI2),
spinY:rnd(0.05,0.14)*sgn(), spinX:rnd(0.03,0.08)*sgn(),
wph:rnd(0,PI2), wfr:rnd(0.04,0.09),
col:col, back:hexDark(col),
layer:layer, alpha:L.alpha, life:1, decay:rnd(0.0011,0.0032) };
}
/* Carré (rect carré) */
function mkSquare(x,y,vx,vy,col,layer) {
var L=LAYERS[layer], s=rnd(5,13)*L.scale;
return { t:'r', x:x,y:y, vx:vx*L.vs,vy:vy*L.vs,
w:s, h:s,
rotY:rnd(0,PI2), rotX:rnd(0,PI2),
spinY:rnd(0.05,0.16)*sgn(), spinX:rnd(0.03,0.09)*sgn(),
wph:rnd(0,PI2), wfr:rnd(0.04,0.09),
col:col, back:hexDark(col),
layer:layer, alpha:L.alpha, life:1, decay:rnd(0.0012,0.0034) };
}
/* Cercle */
function mkCircle(x,y,vx,vy,col,layer) {
var L=LAYERS[layer];
return { t:'c', x:x,y:y, vx:vx*L.vs,vy:vy*L.vs,
r:rnd(3,6.5)*L.scale,
wph:rnd(0,PI2), wfr:rnd(0.05,0.10),
col:col, layer:layer, alpha:L.alpha, life:1, decay:rnd(0.0018,0.0045) };
}
/* Étoile 5 branches */
function mkStar(x,y,vx,vy,col,layer) {
var L=LAYERS[layer];
return { t:'star', x:x,y:y, vx:vx*L.vs,vy:vy*L.vs,
r:rnd(5,9)*L.scale,
theta:rnd(0,PI2), spin:rnd(0.06,0.18)*sgn(),
wph:rnd(0,PI2), wfr:rnd(0.04,0.09),
col:col, layer:layer, alpha:L.alpha, life:1, decay:rnd(0.0015,0.0038) };
}
/* Ruban torsadé (serpentin) */
function mkSerpentin(x,y,vx,vy,col,layer) {
var L=LAYERS[layer], n=(rnd(7,13)|0), seg=[];
for(var i=0;i<n;i++) seg.push({x:x,y:y});
return { t:'s', x:x,y:y, vx:vx*L.vs,vy:vy*L.vs,
segs:seg, n:n,
slen:rnd(5,12)*L.scale,
ph:rnd(0,PI2), wfr:rnd(0.06,0.12),
lw:rnd(2.5,5)*L.scale,
col:col, back:hexDark(col),
twspd:rnd(0.05,0.14), /* vitesse de torsion */
tck:0,
layer:layer, alpha:L.alpha, life:1, decay:rnd(0.0011,0.0030) };
}
/* Étincelle de bouche (vie très courte, bright) */
function mkSpark(x,y,mainAng) {
var a=mainAng+(Math.random()-0.5)*1.4, v=rnd(9,20);
return { t:'spark', x:x,y:y, vx:Math.cos(a)*v,vy:Math.sin(a)*v,
r:rnd(1.5,3.5), col:'#FFF9C4', life:1, decay:rnd(0.05,0.12), layer:2, alpha:1 };
}
/* Bruine ambiante – mkAmb* : particules recyclées au passage en bas */
function mkAmbRect(W,col) {
return { t:'r', amb:true, alpha:rnd(0.40,0.62),
x:rnd(0,W),y:rnd(-30,-5), vx:rnd(-0.7,0.7),vy:rnd(1.1,2.6),
w:rnd(5,12),h:rnd(3,7),
rotY:rnd(0,PI2),rotX:rnd(0,PI2), spinY:rnd(0.02,0.06)*sgn(),spinX:rnd(0.01,0.04)*sgn(),
wph:rnd(0,PI2),wfr:rnd(0.03,0.07), col:col,back:hexDark(col), layer:1, life:1,decay:0 };
}
function mkAmbCircle(W,col) {
return { t:'c', amb:true, alpha:rnd(0.40,0.62),
x:rnd(0,W),y:rnd(-30,-5), vx:rnd(-0.4,0.4),vy:rnd(1.1,2.6),
r:rnd(1.5,4), wph:rnd(0,PI2),wfr:rnd(0.04,0.08), col:col, layer:1, life:1,decay:0 };
}
function mkAmbStar(W,col) {
return { t:'star', amb:true, alpha:rnd(0.40,0.62),
x:rnd(0,W),y:rnd(-30,-5), vx:rnd(-0.3,0.3),vy:rnd(0.9,2.0),
r:rnd(2.5,4.5), theta:rnd(0,PI2),spin:rnd(0.01,0.04)*sgn(),
wph:rnd(0,PI2),wfr:rnd(0.03,0.07), col:col, layer:1, life:1,decay:0 };
}
function mkAmbSerpentin(W,col) {
var n=(rnd(3,6)|0),sx=rnd(0,W),sy=rnd(-30,-5),seg=[];
for(var i=0;i<n;i++) seg.push({x:sx,y:sy});
return { t:'s', amb:true, alpha:rnd(0.40,0.62),
x:sx,y:sy, vx:rnd(-0.4,0.4),vy:rnd(0.9,2.0),
segs:seg,n:n, slen:rnd(4,8),ph:rnd(0,PI2),wfr:rnd(0.04,0.09),
lw:rnd(1.5,3),col:col,back:hexDark(col), twspd:rnd(0.03,0.08), tck:0,
layer:1, life:1,decay:0 };
}
/* ── Reset particules ambiantes ──────────────────────────────────────────*/
function resetAmb(p,W) {
p.x=rnd(0,W); p.y=rnd(-30,-5); p.vx=rnd(-0.7,0.7); p.vy=rnd(1.1,2.6); p.alpha=rnd(0.40,0.62);
}
function resetAmbC(p,W) {
p.x=rnd(0,W); p.y=rnd(-30,-5); p.vx=rnd(-0.4,0.4); p.vy=rnd(1.1,2.6); p.alpha=rnd(0.40,0.62);
}
function resetAmbStar(p,W) {
p.x=rnd(0,W); p.y=rnd(-30,-5); p.vx=rnd(-0.3,0.3); p.vy=rnd(0.9,2.0); p.alpha=rnd(0.40,0.62);
}
function resetAmbSerpentin(p,W) {
p.x=rnd(0,W); p.y=rnd(-30,-5);
for(var k=0;k<p.n;k++){p.segs[k].x=p.x;p.segs[k].y=p.y;}
p.vx=rnd(-0.4,0.4); p.vy=rnd(0.9,2.0); p.alpha=rnd(0.25,0.48);
}
/* ── Update ──────────────────────────────────────────────────────────────*/
function upRect(p,dt) {
var dd=Math.pow(DRAG,dt);
p.vy=(p.vy+G*dt)*dd; p.vx*=dd;
p.wph+=p.wfr*dt; p.vx+=Math.sin(p.wph)*WOB*0.10*dt;
p.x+=p.vx*dt; p.y+=p.vy*dt;
p.rotY+=p.spinY*dt; p.rotX+=p.spinX*dt;
p.life-=p.decay*dt;
}
function upCircle(p,dt) {
var dd=Math.pow(DRAG,dt);
p.vy=(p.vy+G*dt)*dd; p.vx*=dd;
p.wph+=p.wfr*dt; p.vx+=Math.sin(p.wph)*WOB*0.08*dt;
p.x+=p.vx*dt; p.y+=p.vy*dt; p.life-=p.decay*dt;
}
function upStar(p,dt) {
var dd=Math.pow(DRAG,dt);
p.vy=(p.vy+G*dt)*dd; p.vx*=dd;
p.wph+=p.wfr*dt; p.vx+=Math.sin(p.wph)*WOB*0.09*dt;
p.x+=p.vx*dt; p.y+=p.vy*dt; p.theta+=p.spin*dt; p.life-=p.decay*dt;
}
function upSerpentin(p,dt) {
var dd=Math.pow(DRAG,dt);
p.vy=(p.vy+G*0.60*dt)*dd; p.vx*=dd;
p.x+=p.vx*dt; p.y+=p.vy*dt; p.tck+=dt;
p.segs[0].x=p.x; p.segs[0].y=p.y;
for(var i=1;i<p.n;i++) {
var dx=p.segs[i-1].x-p.segs[i].x, dy=p.segs[i-1].y-p.segs[i].y;
var d=Math.sqrt(dx*dx+dy*dy);
if(d>p.slen){ var pull=(d-p.slen)/d*0.80; p.segs[i].x+=dx*pull; p.segs[i].y+=dy*pull; }
p.segs[i].x+=Math.sin(p.tck*p.wfr+i*0.75+p.ph)*0.80*dt;
}
p.life-=p.decay*dt;
}
function upSpark(p,dt) {
p.vy+=G*1.8*dt; p.vx*=0.90; p.vy*=0.90;
p.x+=p.vx*dt; p.y+=p.vy*dt; p.life-=p.decay*dt;
}
/* ── Draw ─────────────────────────────────────────────────────────────── */
/* Rectangle 3D : éclairage normal + glint spéculaire + motion-blur couche proche */
function drRect(ctx,p,lAlpha) {
var cosY=Math.cos(p.rotY), cosX=Math.cos(p.rotX);
var sinY=Math.sin(p.rotY), sinX=Math.sin(p.rotX);
var visW=p.w*Math.abs(cosY), visH=p.h*Math.abs(cosX);
if(visW<0.4||visH<0.3) return;
/* Normale de surface & éclairage diffus */
var nx=sinY, ny=-sinX*cosY, nz=Math.abs(cosX*cosY);
var diff=nx*LX+ny*LY+nz*LZ; /* ∈ [-1, 1] */
/* Face visible (cosY ≥ 0 = face avant) */
var faceCol=cosY>=0 ? p.col : p.back;
var litCol=applyLight(faceCol, diff);
/* Glint spéculaire : reflet blanc bref quand diff est élevé */
var glint=0;
if(diff>0.60) glint=Math.pow((diff-0.60)/0.40,1.8)*0.95;
var finalA=(p.amb?p.alpha:Math.max(0,p.life))*lAlpha;
if(finalA<0.005) return;
var tilt=Math.atan2(p.vy,p.vx)*0.25;
/* Motion-blur pour le layer proche — alpha fixe 0.22, indépendant de dt */
if(p.layer===2&&!p.amb&&LAYERS[2].blur>0) {
ctx.save();
ctx.globalAlpha=0.22;
ctx.fillStyle=litCol;
ctx.translate(p.x-p.vx*2.2, p.y-p.vy*2.2);
ctx.rotate(tilt);
ctx.fillRect(-visW/2,-visH/2,visW,visH);
ctx.restore();
}
ctx.save();
ctx.globalAlpha=finalA;
ctx.fillStyle=litCol;
ctx.translate(p.x,p.y);
ctx.rotate(tilt);
ctx.fillRect(-visW/2,-visH/2,visW,visH);
/* Overlay glint — durée 1.10 pour effet métallisé perceptible */
if(glint>0.04) {
ctx.globalAlpha=finalA*glint*1.10;
ctx.fillStyle='#FFFFFF';
ctx.fillRect(-visW/2,-visH/2,visW,visH);
}
ctx.restore();
}
/* Cercle */
function drCircle(ctx,p,lAlpha) {
var finalA=(p.amb?p.alpha:Math.max(0,p.life))*lAlpha;
if(finalA<0.005) return;
ctx.save(); ctx.globalAlpha=finalA; ctx.fillStyle=p.col;
ctx.beginPath(); ctx.arc(p.x,p.y,p.r,0,PI2); ctx.fill(); ctx.restore();
}
/* Étoile 5 branches */
function drStar(ctx,p,lAlpha) {
var finalA=(p.amb?p.alpha:Math.max(0,p.life))*lAlpha;
if(finalA<0.005) return;
var R=p.r, r=p.r*0.42;
ctx.save(); ctx.globalAlpha=finalA; ctx.fillStyle=p.col;
ctx.translate(p.x,p.y); ctx.rotate(p.theta);
ctx.beginPath();
for(var i=0;i<10;i++){
var a=(i*PI/5)-PI/2, rad=(i&1)?r:R;
if(i===0) ctx.moveTo(Math.cos(a)*rad,Math.sin(a)*rad);
else ctx.lineTo(Math.cos(a)*rad,Math.sin(a)*rad);
}
ctx.closePath(); ctx.fill(); ctx.restore();
}
/* Ruban torsadé : largeur varie par segment (torsion visible) */
function drSerpentin(ctx,p,lAlpha) {
if(p.n<2) return;
var finalA=(p.amb?p.alpha:Math.max(0,p.life))*lAlpha;
if(finalA<0.005) return;
ctx.save(); ctx.lineCap='round';
for(var i=1;i<p.n;i++){
var twist=Math.cos(p.tck*p.twspd+i*0.85+p.ph);
var lw=p.lw*(0.35+0.65*Math.abs(twist));
/* largeur plancher : le ruban torsadé ne se hache jamais */
/* Face vs dos selon signe de la torsion */
var fc=twist>=0 ? p.col : p.back;
/* Éclairage simplifié basé sur angle de torsion */
var diff2=twist*LZ+0.28;
var litCol2=applyLight(fc, clamp(diff2,-0.5,0.82));
ctx.globalAlpha=finalA;
ctx.strokeStyle=litCol2;
ctx.lineWidth=lw;
ctx.beginPath();
ctx.moveTo(p.segs[i-1].x,p.segs[i-1].y);
ctx.lineTo(p.segs[i].x,p.segs[i].y);
/* Ombre légère (critère 1) */
ctx.save(); ctx.globalAlpha=finalA*0.18; ctx.strokeStyle='rgba(0,0,0,0.6)'; ctx.translate(0.8,1.5); ctx.stroke(); ctx.restore();
ctx.stroke();
}
ctx.restore();
}
/* Étincelle avec halo */
function drSpark(ctx,p) {
var a=Math.max(0,p.life);
if(a<0.005) return;
ctx.save();
ctx.globalAlpha=a;
ctx.fillStyle='#FFFDE7';
ctx.beginPath(); ctx.arc(p.x,p.y,p.r,0,PI2); ctx.fill();
/* Halo lumineux */
var g=ctx.createRadialGradient(p.x,p.y,0,p.x,p.y,p.r*3.5);
g.addColorStop(0,'rgba(255,253,200,'+(a*0.55)+')');
g.addColorStop(1,'rgba(255,253,200,0)');
ctx.globalAlpha=1;
ctx.fillStyle=g;
ctx.beginPath(); ctx.arc(p.x,p.y,p.r*3.5,0,PI2); ctx.fill();
ctx.restore();
}
/* Éclair de bouche : glow radial en composite:lighter */
function drFlash(ctx,fl) {
var prog=fl.t/fl.maxT;
var alpha=(1-prog)*0.92, radius=16+prog*68;
var g=ctx.createRadialGradient(fl.x,fl.y,0,fl.x,fl.y,radius);
g.addColorStop(0, 'rgba(255,248,190,'+alpha+')');
g.addColorStop(0.3,'rgba(255,210,80,'+(alpha*0.62)+')');
g.addColorStop(0.7,'rgba(255,130,30,'+(alpha*0.28)+')');
g.addColorStop(1, 'rgba(255,100,20,0)');
ctx.save();
ctx.globalCompositeOperation='lighter';
ctx.fillStyle=g;
ctx.beginPath(); ctx.arc(fl.x,fl.y,radius,0,PI2); ctx.fill();
ctx.restore();
}
/* ── Sélecteur de couche (35 % lointain, 40 % milieu, 25 % proche) ───── */
function pickLayer() {
var r=Math.random(); return r<0.35?0:r<0.75?1:2;
}
/* ── Scène ───────────────────────────────────────────────────────────────*/
function Scene(el) {
this.el = el;
this.parts = [];
this.flashes = []; /* éclairs de bouche */
this.queue = []; /* émission différée : {x,y,vx,vy,col,delay} */
this.qEl = 0; /* ms écoulées dans la queue courante */
this.running = false;
this.raf = null;
this.W = 480; this.H = 300;
this._lastTs = null;
this._ambAcc = 0;
this.cv = el.querySelector('.ccn-canvas');
if(!this.cv){
this.cv=document.createElement('canvas');
this.cv.className='ccn-canvas';
this.cv.setAttribute('aria-hidden','true');
el.insertBefore(this.cv,el.firstChild);
}
this.ctx = this.cv.getContext('2d');
var attr=el.getAttribute('data-colors');
this.colors=attr?attr.split(',').map(function(c){return c.trim();}):COLORS_DEFAULT.slice();
this.resize();
var self=this;
if(window.ResizeObserver) new ResizeObserver(function(){self.resize();}).observe(el);
el.addEventListener('click',function(e){
if(REDUCE) return;
if(e.target&&e.target.classList&&e.target.classList.contains('ccn-celebrate-btn')) return;
self.fireCannons({});
});
var btn=el.querySelector('.ccn-celebrate-btn');
if(btn) btn.addEventListener('click',function(e){e.stopPropagation();if(!REDUCE)self.fireCannons({});});
if(window.IntersectionObserver){
var lastAuto=0;
new IntersectionObserver(function(entries){
if(!entries[0].isIntersecting){self.stopLoop();return;}
if(!self.running) self.startLoop();
var now=Date.now();
if(!REDUCE&&now-lastAuto>6000){lastAuto=now;self.fireCannons({});}
},{threshold:0.35}).observe(el);
}
}
Scene.prototype.resize=function(){
var dpr=window.devicePixelRatio||1;
var w=this.el.offsetWidth||480, h=this.el.offsetHeight||300;
var cw=Math.round(w*dpr), ch=Math.round(h*dpr);
if(this.cv.width!==cw||this.cv.height!==ch){
this.cv.width=cw; this.cv.height=ch;
this.cv.style.width=w+'px'; this.cv.style.height=h+'px';
this.ctx.setTransform(dpr,0,0,dpr,0,0);
}
this.W=w; this.H=h;
};
Scene.prototype.spawnOne=function(x,y,vx,vy,col,layer){
if(this.parts.length>=MAX_PARTS) return;
var r=Math.random();
if (r<0.32) this.parts.push(mkRect(x,y,vx,vy,col,layer));
else if(r<0.44) this.parts.push(mkSquare(x,y,vx,vy,col,layer));
else if(r<0.60) this.parts.push(mkSerpentin(x,y,vx,vy,col,layer));
else if(r<0.73) this.parts.push(mkCircle(x,y,vx,vy,col,layer));
else this.parts.push(mkStar(x,y,vx,vy,col,layer));
};
/* fire() : un tir depuis (x,y) — émission étalée + flash + étincelles */
Scene.prototype.fire=function(opts){
opts=opts||{};
var x =opts.x !==undefined?opts.x :this.W/2;
var y =opts.y !==undefined?opts.y :this.H;
var ang=opts.angle !==undefined?opts.angle :-PI/2;
var spd=opts.spread!==undefined?opts.spread:PI/3;
var cnt=opts.count ||60;
var col=opts.colors||this.colors;
var spf=opts.speed ||1;
var self=this;
/* Recul du canon (critère 3) : 5 frames de décalage visuel */
this._recoilFrames = 5;
this._recoilDx = Math.cos(ang) * 3;
this._recoilDy = Math.sin(ang) * 3;
/* Éclair de bouche */
this.flashes.push({x:x,y:y,t:0,maxT:210});
/* Étincelles de bouche */
if(!REDUCE){
for(var s=0;s<7;s++){
if(this.parts.length<MAX_PARTS) this.parts.push(mkSpark(x,y,ang));
}
}
/* Émission : 60% immédiate + 40% étalée en 5 vagues (30ms chacune) */
var immediate=Math.ceil(cnt*0.60);
var deferred =cnt-immediate;
var waveSz =Math.max(1,Math.ceil(deferred/5));
for(var i=0;i<immediate;i++){
var a=ang+(Math.random()-0.5)*spd;
var v=rnd(7,14)*spf;
self.spawnOne(x,y,Math.cos(a)*v,Math.sin(a)*v,pick(col),pickLayer());
}
for(var w=0;w<5;w++){
for(var j=0;j<waveSz;j++){
if(this.parts.length+this.queue.length>=MAX_PARTS) break;
var a2=ang+(Math.random()-0.5)*spd;
var v2=rnd(7,14)*spf;
this.queue.push({x:x,y:y,vx:Math.cos(a2)*v2,vy:Math.sin(a2)*v2,col:col,delay:(w+1)*30});
}
}
this.qEl=0;
if(!this.running) this.startLoop();
this.step(1); /* synchro canvas avant capture éventuelle */
};
Scene.prototype.fireCannons=function(opts){
opts=opts||{};
var col=opts.colors||this.colors;
var cnt=opts.count ||140;
var spd=Math.max(1,this.H/270);
this.fire({x:0, y:this.H,angle:-1.13,spread:PI/3,count:(cnt/2)|0,colors:col,speed:spd});
this.fire({x:this.W, y:this.H,angle:-2.01,spread:PI/3,count:(cnt/2)|0,colors:col,speed:spd});
};
/* Traitement de la queue différée */
Scene.prototype.processQueue=function(dtMs){
if(!this.queue.length) return;
this.qEl+=dtMs;
var remaining=[];
for(var i=0;i<this.queue.length;i++){
var it=this.queue[i];
if(it.delay<=this.qEl) this.spawnOne(it.x,it.y,it.vx,it.vy,pick(it.col),pickLayer());
else remaining.push(it);
}
this.queue=remaining;
};
Scene.prototype.startLoop=function(){
if(this.running) return;
this.running=true; this._lastTs=null;
var lbl=this.el.querySelector('.ccn-label');
if(lbl) lbl.style.opacity='0';
var self=this;
function tick(ts){
if(!self.running) return;
var dt=self._lastTs?Math.min((ts-self._lastTs)/16.67,3):1;
self._lastTs=ts;
self.drizzle(dt);
self.processQueue(dt*16.67);
self.step(dt);
/* Restaurer le label quand l'écran est vide (correction juge) */
if(!self.parts.length && !self.queue.length && lbl) lbl.style.opacity='1';
if(self.parts.length||self.flashes.length||self.queue.length||!REDUCE)
self.raf=requestAnimationFrame(tick);
else {
self.running=false;
self.ctx.clearRect(0,0,self.W,self.H);
var l=self.el.querySelector('.ccn-label');
if(l) l.style.opacity='1';
}
}
this.raf=requestAnimationFrame(tick);
};
Scene.prototype.stopLoop=function(){
this.running=false; this._ambAcc=0;
if(this.raf){cancelAnimationFrame(this.raf);this.raf=null;}
};
/* Mise à jour + rendu d'un frame */
Scene.prototype.step=function(dt){
dt=dt||1;
var ctx=this.ctx, W=this.W, H=this.H;
ctx.clearRect(0,0,W,H);
/* Recul du canon : décalage du repère de rendu sur 5 frames */
var hasRecoil=(this._recoilFrames||0)>0;
if(hasRecoil){
ctx.save();
ctx.translate(this._recoilDx||0, this._recoilDy||0);
this._recoilFrames--;
}
/* 1. Éclairs de bouche (fond) */
var aliveF=[];
for(var f=0;f<this.flashes.length;f++){
var fl=this.flashes[f];
drFlash(ctx,fl); fl.t+=dt*16.67;
if(fl.t<fl.maxT) aliveF.push(fl);
}
this.flashes=aliveF;
/* 2. Update toutes les particules → alive */
var alive=[];
for(var i=0;i<this.parts.length;i++){
var p=this.parts[i], dead=false;
if(p.t==='r'||p.t==='sq'){
upRect(p,dt);
if(p.amb){ if(p.y>H+10) resetAmb(p,W); }
else dead=p.life<=0.01||p.y>H+70;
} else if(p.t==='s'){
upSerpentin(p,dt);
if(p.amb){ if(p.segs[0].y>H+10) resetAmbSerpentin(p,W); }
else dead=p.life<=0.01||p.segs[0].y>H+90;
} else if(p.t==='c'){
upCircle(p,dt);
if(p.amb){ if(p.y>H+10) resetAmbC(p,W); }
else dead=p.life<=0.01||p.y>H+60;
} else if(p.t==='star'){
upStar(p,dt);
if(p.amb){ if(p.y>H+10) resetAmbStar(p,W); }
else dead=p.life<=0.01||p.y>H+60;
} else if(p.t==='spark'){
upSpark(p,dt); dead=p.life<=0.01||p.y>H+30;
}
if(!dead) alive.push(p);
}
this.parts=alive;
/* 3. Rendu en 3 passes (far→mid→near) pour la parallaxe */
for(var pass=0;pass<3;pass++){
for(var i=0;i<this.parts.length;i++){
var p=this.parts[i];
var rl=p.amb?1:p.layer;
/* Étincelles et sparks toujours au pass 2 */
if(p.t==='spark'){ if(pass===2) drSpark(ctx,p); continue; }
if(rl!==pass) continue;
var la=p.amb?1:LAYERS[p.layer].alpha;
if(p.t==='r') drRect(ctx,p,la);
else if(p.t==='s') drSerpentin(ctx,p,la);
else if(p.t==='c') drCircle(ctx,p,la);
else if(p.t==='star') drStar(ctx,p,la);
}
}
if(hasRecoil) ctx.restore();
};
/* Bruine ambiante : 1 particule toutes ~230ms, max 55 ambiantes */
Scene.prototype.drizzle=function(dt){
if(REDUCE) return;
this._ambAcc=(this._ambAcc||0)+dt;
if(this._ambAcc<14) return;
this._ambAcc-=14;
var ambN=0;
for(var i=0;i<this.parts.length;i++) if(this.parts[i].amb) ambN++;
if(ambN>=55||this.parts.length>=MAX_PARTS) return;
var r=Math.random(), c=pick(this.colors);
if (r<0.40) this.parts.push(mkAmbRect(this.W,c));
else if(r<0.65) this.parts.push(mkAmbCircle(this.W,c));
else if(r<0.85) this.parts.push(mkAmbStar(this.W,c));
else this.parts.push(mkAmbSerpentin(this.W,c));
};
/* ── Init ────────────────────────────────────────────────────────────────*/
var _scenes=[];
function init(){
document.querySelectorAll('.ccn-scene').forEach(function(el){
var s=new Scene(el);
_scenes.push(s);
if(REDUCE){
s.fire({x:s.W/2,y:s.H,angle:-PI/2,spread:PI*0.55,count:20,speed:0.70});
} else {
s.fireCannons({});
}
});
}
if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',init);
else init();
/* ── API publique ────────────────────────────────────────────────────────*/
window.ConfettiCannon={
fire: function(opts){ _scenes.forEach(function(s){s.fire(opts||{});});},
fireCannons: function(opts){ _scenes.forEach(function(s){s.fireCannons(opts||{});});},
fireScene: function(idx,opts){ if(_scenes[idx]) _scenes[idx].fireCannons(opts||{});},
scenes: _scenes
};
}());
Personnaliser
Options passées à l'API ou attributs data-* :
| Option / propriété | Défaut | Effet |
|---|---|---|
| data-colors | 10 couleurs premium | Palette de confettis sur le .ccn-scene — liste de hex séparés par des virgules. |
| opts.count | 140 | Nombre total de confettis par appel à fireCannons(). Entre 40 et 250 pour des performances fluides. |
| opts.colors | scene.colors | Tableau de couleurs hex pour ce tir spécifique — surcharge data-colors. |
| opts.x / opts.y | centre / bas | Position en pixels CSS du point de départ pour fire(). Utile pour tirer depuis un bouton précis. |
| opts.angle | -π/2 (vertical) | Angle de tir en radians. -1,13 = coin gauche, -2,01 = coin droit (défaut fireCannons). |
| opts.spread | π/3 (60°) | Ouverture du cône de dispersion en radians. |
| opts.speed | auto (H/270) | Multiplicateur de vitesse initiale. 1 = normal, 0,7 = doux, 1,4 = énergique. |
| scene.colors | — | Propriété directe : ConfettiCannon.scenes[0].colors = ['#f00','#0f0'] — change la palette pour les prochains tirs. |
| .ccn-scene.ccn-hc | absent | Classe CSS à ajouter pour activer le mode contraste fort (filter: contrast(1.45) brightness(1.12)). |
FAQ
Confetti Cannon est-il vraiment gratuit ?
Oui — cet effet est disponible sans compte. Le CSS, le HTML et le JS complets sont en accès libre sur cette page. La bibliothèque Effect.Labs contient des dizaines d'effets dans la catégorie Atmosphère, dont plusieurs gratuits et de nombreux premium.
Puis-je changer les couleurs des confettis ?
Oui, via l'attribut data-colors sur le .ccn-scene (liste de hex) ou en passant un tableau colors à fireCannons(). Vous pouvez aussi modifier window.ConfettiCannon.scenes[0].colors à la volée pour le prochain tir.
L'effet respecte-t-il prefers-reduced-motion ?
Oui. Quand prefers-reduced-motion est réduit, le script déclenche un unique tir court et doux (20 particules, vitesse 0,7×) puis s'arrête : aucune animation en boucle. La bruine ambiante continue est également désactivée.