🏆 Game Over — Final ranking
Confetti Cannon
Two bottom-corner cannons fire metallic paper confetti — rectangles with 3D normal lighting, bicolor twisted streamers, stars, circles, and 3 parallax layers. Click-triggered, window.ConfettiCannon API.
This effect is free — the Atmosphere category contains 57 effects total, including 10 free. Effect.Labs has 811 vanilla effects. Explore the category →
Usage examples
Starter plan · €9.90/month · no commitment
How it works
A single requestAnimationFrame drives all scenes on the page. Each Scene instance maintains an array of active particles (≤ 450) and an ambient drizzle pool (≤ 55 lightweight particles drifting continuously).
On each fire, two cannons open from the bottom corners with a muzzle flash (radial gradient in composite:lighter mode) and 7 short sparks per cannon. Confetti launches in 5 waves spread over 150 ms (60% immediate, 40% deferred) to avoid spawn spikes.
Rectangles and squares compute 3D normal lighting: rotX + rotY → surface normal vector → dot product with the light source (top-left-front) → diffuse component ±50% brightness + white specular glint when diff > 0.60. Twisted streamers vary their apparent width per segment (|cos(twist)|) to simulate torsion. Parallax distributes particles across 3 layers (far · mid · near) with different scales and speeds; the near layer gets a subtle motion blur (semi-transparent ghost offset 2.2 px along the velocity vector).
DPR (devicePixelRatio) is handled on resize: the canvas scales to physical pixels, the context is transformed, particles stay in CSS coordinates. A ResizeObserver recalibrates automatically when the container changes size.
Accessibility
- prefers-reduced-motion: if reduced, a single short burst (20 particles, 0.7× speed) fires on init then the animation stops — no perpetual loop.
- The scene carries
role="img"and a descriptivearia-label; the canvas is markedaria-hidden="true". - The Celebrate button is a native
<button>, keyboard-focusable, with a golden:focus-visiblering (#fbbf24). - The Pause button meets WCAG 2.2.2 (pause mechanism for any auto-started animation lasting more than 5 seconds).
- Ambient drizzle is disabled via
prefers-reduced-motion: reduce(thedrizzle()function returns immediately).
Browser compatibility
Requires Canvas 2D and requestAnimationFrame — supported in all modern browsers. Zero external dependencies.
Without Canvas support, the scene stays empty (no fatal JS errors); the CTA and text remain readable.
The code
Copy the three blocks into your page. No dependencies.
<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
};
}());
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
| data-colors | 10 premium colors | Confetti color palette on the .ccn-scene — comma-separated hex values. |
| opts.count | 140 | Total confetti count per fireCannons() call. 40–250 for smooth performance. |
| opts.colors | scene.colors | Hex color array for this specific burst — overrides data-colors. |
| opts.x / opts.y | center / bottom | CSS-pixel origin for fire(). Useful to shoot from a specific button position. |
| opts.angle | -π/2 (straight up) | Launch angle in radians. -1.13 = left corner, -2.01 = right corner (fireCannons default). |
| opts.spread | π/3 (60°) | Dispersion cone width in radians. |
| opts.speed | auto (H/270) | Initial speed multiplier. 1 = normal, 0.7 = gentle, 1.4 = energetic. |
| scene.colors | — | Direct property: ConfettiCannon.scenes[0].colors = ['#f00','#0f0'] — changes the palette for next bursts. |
| .ccn-scene.ccn-hc | absent | CSS class to add to enable high-contrast mode (filter: contrast(1.45) brightness(1.12)). |
FAQ
Is Confetti Cannon really free?
Yes — this effect is available without an account. Full CSS, HTML, and JS are openly accessible on this page. Effect.Labs includes dozens of effects in the Atmosphere category, some free and many premium.
Can I change the confetti colors?
Yes, via the data-colors attribute on the .ccn-scene (comma-separated hex list) or by passing a colors array to fireCannons(). You can also modify window.ConfettiCannon.scenes[0].colors on the fly for the next burst.
Does the effect respect prefers-reduced-motion?
Yes. When prefers-reduced-motion is reduced, the script fires a single short, gentle burst (20 particles, 0.7× speed) then stops — no looping animation. The ambient drizzle is also disabled.