Atmosphere & Particles
Particle systems, atmospheric effects, special effects and audio.
Updated
How do you create particle effects on a website?
Particle effects fill a scene with many small moving elements: stars, snow, rain, smoke, fire, confetti. They are drawn on an HTML canvas, where a JavaScript loop driven by requestAnimationFrame updates each particle’s position, speed and lifetime, then redraws the frame. That keeps hundreds of particles smooth where DOM elements would stall.
How to choose, performance and accessibility
- Use Starfield or Snow Fall for ambient backgrounds, Confetti Explosion to celebrate a sign-up or a purchase, and Attraction Field when particles should react to the pointer.
- Scale the particle count with the screen size, stop the loop when the canvas leaves the screen or the tab is hidden, and show a still frame for
prefers-reduced-motion. - The page also offers interface sounds, such as Click Sound Button and Notification Chime, that play only after a user action.
- Starfield, Confetti Explosion and several sound effects are free; the rest come with Premium.
Tune these particles yourself
The effects below can be copied as they are. The particle generator goes further: you set the shapes, the colours and the sequence, you feed it your own logo or text, and the result updates live — then you take the code away as a single self-contained file.
This is not a picture: the engine is really running, and it reacts to the cursor.
Open the generatorView source code
<div class="demo-preview">
<div class="ps-canvas-wrap">
<canvas class="ps-canvas" id="psStarfield"></canvas>
</div>
</div>.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
box-sizing: border-box
}
.demo-preview {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 280px;
overflow: hidden;
position: relative;
background: rgb(10, 10, 15);
color: rgb(255, 255, 255);
font-family: sans-serif;
}
.demo-preview:has(.ps-canvas-wrap) {
min-height: 280px;
background: #0a0a0f;
}
.ps-canvas {
width: 100%;
height: 280px;
display: block;
border-radius: 0;
}
.ps-canvas-wrap {
position: relative;
width: 100%;
height: 280px;
}
.ps-canvas-wrap canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}(function() {
/* fx-0485 — Starfield (IIFE scopée) — restauré le 11/08/2026 : le code de cet effet
gratuit (data-free) avait été retiré par erreur avec le code premium (anti-scalping #13). */
(function() {
const canvas = document.getElementById('psStarfield');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const rect = canvas.parentElement.getBoundingClientRect();
canvas.width = rect.width;
canvas.height = rect.height;
const w = canvas.width,
h = canvas.height;
const stars = [];
for (let i = 0; i < 200; i++) {
stars.push({
x: Math.random() * w - w / 2,
y: Math.random() * h - h / 2,
z: Math.random() * w
});
}
function draw() {
ctx.fillStyle = 'rgba(10,10,15,0.2)';
ctx.fillRect(0, 0, w, h);
stars.forEach(s => {
s.z -= 2;
if (s.z <= 0) {
s.z = w;
s.x = Math.random() * w - w / 2;
s.y = Math.random() * h - h / 2;
}
const sx = (s.x / s.z) * w / 2 + w / 2;
const sy = (s.y / s.z) * h / 2 + h / 2;
const r = Math.max(0.5, (1 - s.z / w) * 2.5);
const a = 1 - s.z / w;
ctx.beginPath();
ctx.arc(sx, sy, r, 0, Math.PI * 2);
ctx.fillStyle = `rgba(255,255,255,${a})`;
ctx.fill();
});
requestAnimationFrame(draw);
}
draw();
})();
})();Snow Fall
Falling snowflakes with varied sizes, horizontal oscillation and random opacity.
Effect page →Fire Effect
Rising fire particles with color gradient (yellow to orange to red) and progressive dissipation.
Effect page →Smoke
Rising smoke with progressive expansion, gentle lateral drift and fade-out transparency.
Effect page →Attraction Field
Particles attracted to the cursor with force proportional to distance. Lines connecting nearby particles.
Effect page →Explosion
Click to trigger a colorful particle explosion with gravity, friction and dissipation.
Effect page →Fireflies
Floating fireflies with individual light pulsation, erratic movement and light halo.
Effect page →Galaxy Spiral
Spiral galaxy with rotating arms, stars of varied colors and luminous central core.
Effect page →Trail Effect
Trail of colored particles following the cursor with rotation, shrinking and progressive fading.
Effect page →Ambient Light Glow
Element emitting a colored ambient light halo via multi-layer box-shadow with progressive spread.
Effect page →Day/Night Cycle
Smooth transition through a full day/night cycle with animated gradients and moving sun.
Effect page →Fog Layer
Animated semi-transparent fog drifting across content via pseudo-elements and translateX.
Effect page →Fireflies
Fireflies floating randomly with soft pulsating glow on dark background, pure CSS.
Effect page →Northern Lights
Animated aurora borealis with multi-layer gradients, fluid movement and hue rotation.
Effect page →Heat Haze
Subtle wavy distortion simulating hot air via animated SVG feTurbulence filter.
Effect page →Underwater Caustics
Animated light patterns as if beneath the water surface with conic gradients and rising bubbles.
Effect page →Snow Fall
Realistic snowfall with snowflakes of varied sizes and speeds, gentle wind drift, pure CSS.
Effect page →Light Leak
Animated film-style light leaks and flares with gradients and mix-blend-mode screen.
Effect page →Confetti Explosion
A radial explosion of metallic paper confetti bursts from the central button or any clicked point in the scene, then drifts back down for 2-3 seconds. Each piece — rectangle, circle, curved ribbon or star — carries a simulated 3D normal (diffuse lighting, dark edge, brief specular glint, two-tone faces). Two depth layers create space, and a few slow floaters keep the scene alive between bursts. Under prefers-reduced-motion, a single short, gentle burst fires then stops.
💡 Perfect for: goal-reached celebrations, user milestones, congratulations screens, unlocked badges, subscription activation.
View source code
<div class="demo-preview">
<div class="cxp-scene" id="cxpScene"
role="img"
aria-label="Confetti Explosion — radial burst of metallic confetti from the button"
data-colors="#FF4D9E,#FFD700,#00E5FF,#8AFF59,#F0F4F8,#FF8040,#E040FB,#FDFFB6">
<canvas class="cxp-canvas" aria-hidden="true"></canvas>
<div class="cxp-label" aria-hidden="true">Click anywhere · or</div>
<button class="cxp-btn" aria-label="Launch a confetti explosion">🎉 Celebrate!</button>
</div>
</div>.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
box-sizing: border-box
}
.demo-preview {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 250px;
overflow: hidden;
position: relative;
background: rgb(10, 10, 26);
color: rgb(255, 255, 255);
font-family: sans-serif;
}
.cxp-scene {
position: relative;
width: 100%;
aspect-ratio: 16 / 9;
border-radius: 14px;
overflow: hidden;
isolation: isolate;
cursor: crosshair;
contain: layout style paint;
background: #0a0a0f;
}
.cxp-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
display: block;
}
.cxp-btn {
position: absolute;
bottom: 50%;
left: 50%;
transform: translate(-50%, 50%);
z-index: 2;
background: rgba(255, 255, 255, 0.10);
border: 1px solid rgba(255, 255, 255, 0.22);
color: #f1f5f9;
padding: 10px 26px;
border-radius: 28px;
font-size: 15px;
font-weight: 700;
cursor: pointer;
transition: background 0.14s, transform 0.10s;
outline: 2px solid transparent;
letter-spacing: 0.02em;
white-space: nowrap;
animation: cxp-idle-pulse 3.4s ease-in-out infinite;
}
.cxp-btn:hover {
background: rgba(255, 255, 255, 0.18);
}
.cxp-btn:active {
transform: translate(-50%, 50%) scale(0.95);
}
.cxp-btn:focus-visible {
outline-color: #fbbf24;
outline-offset: 3px;
}
.cxp-label {
position: absolute;
bottom: 10px;
left: 14px;
pointer-events: none;
z-index: 1;
color: rgba(148, 163, 184, 0.28);
font-size: clamp(9px, 1.2vw, 11px);
letter-spacing: 0.10em;
text-transform: uppercase;
font-weight: 500;
line-height: 1.5;
}
.cxp-btn.cxp-punch {
animation: cxp-btn-punch 0.30s cubic-bezier(0.36, 0.07, 0.19, 0.97) forwards;
}
@media (prefers-reduced-motion: reduce) {
.cxp-btn {
animation: none;
transition: none;
}
.cxp-btn.cxp-punch {
animation: none;
}
.cxp-btn:active {
transform: translate(-50%, 50%);
}
}
@media (max-width: 480px) {
.cxp-btn {
font-size: 13px;
padding: 8px 18px;
}
}
@keyframes cxp-idle-pulse {
0%,
100% {
box-shadow: 0 0 0 0 rgba(251, 191, 36, 0);
}
50% {
box-shadow: 0 0 8px 2px rgba(251, 191, 36, 0.18);
}
}
@keyframes cxp-btn-punch {
0% {
transform: translate(-50%, 50%) scale(0.92);
}
55% {
transform: translate(-50%, 50%) scale(1.05);
}
100% {
transform: translate(-50%, 50%) scale(1.00);
}
}(function() {
/* fx-0505 */
(function() {
'use strict';
var REDUCE = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var PI2 = Math.PI * 2;
var PI = Math.PI;
/* ── Physique (valeurs Owen) ──────────────────────────────────────────── */
var G = 0.06; /* gravité px/frame² @ 60fps → terminal ≈ 1,44 px/frame */
var DRAG = 0.96; /* traînée ×0,96/frame (Owen : 0,955–0,965) */
var WOB = 0.32; /* flottement feuille-morte */
/* Δt normalisé (60fps=1,0 ; 120fps=0,5) — recalculé dans tick() */
var _tickDt = 1.0;
var _lastTickTs = 0;
/* ── Vitesses initiales absolues par couche (px/frame @ 60fps) ──────── */
/* rayon max ≈ 40 % de la largeur (Owen) : 8/0,04 ≈ 200 px ≈ 42 % de 480 */
var SPEED = [{
spMin: 2,
spMax: 5,
alpha: 0.70
}, /* 0 = lointain */
{
spMin: 4,
spMax: 8,
alpha: 1.00
} /* 1 = proche */
];
/* ── Biais vers le haut (px/frame soustrait à vy) ────────────────────── */
var BIAS_UP = 0; /* annulé : décalait toutes les particules vers le haut, sortie par le top */
/* ── Tailles (px, non scalées) ───────────────────────────────────────── */
var SZ_NEAR = {
rw: [8, 14],
rh: [3.5, 6.5],
c: [4, 10],
s: [6, 13]
};
var SZ_FAR = {
rw: [4, 7],
rh: [1.8, 3.2],
c: [2, 5],
s: [3, 7]
};
/* ── Palette candy saturée ────────────────────────────────────────────── */
var PALETTES = {
candy: ['#FF1A8C', '#FFD700', '#00EEFF', '#77FF00', '#FFFFFF', '#FF6600', '#CC00FF', '#FFE500'],
gold: ['#FFD700', '#FFF0A0', '#F0E0A0', '#C8A840', '#FFFBE0', '#E8C840', '#FFE880', '#FFF8D0'],
pastel: ['#FFB3D9', '#FFE4B3', '#B3F0FF', '#C8FFB3', '#F0E8FF', '#FFD0C8', '#E8C8FF', '#FFF8E0'],
rainbow: ['#FF0000', '#FF8800', '#FFFF00', '#00FF00', '#0055FF', '#AA00FF', '#FF0099', '#FFFFFF']
};
var COLORS_DEFAULT = PALETTES.candy;
var MAX_PARTS = 550;
/* ── 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) ───────────────────────────────────────────── */
function darken(hex, f) {
var r = parseInt(hex.slice(1, 3), 16);
var g = parseInt(hex.slice(3, 5), 16);
var b = parseInt(hex.slice(5, 7), 16);
return 'rgb(' + Math.round(r * f) + ',' + Math.round(g * f) + ',' + Math.round(b * f) + ')';
}
/* ── Types de formes ─────────────────────────────────────────────────── */
var SHAPES = ['rect', 'rect', 'rect', 'rect', 'circle', 'ribbon', 'ribbon', 'star', 'star'];
/* ── Alpha de vie : fondu UNIQUEMENT sur le dernier 25 % ────────────── */
function lifeAlpha(p) {
var t = p.life / p.lifeMax;
var fi = p.life < 8 ? p.life / 8 : 1;
var fo = t > 0.75 ? 1 - (t - 0.75) / 0.25 : 1;
return p.alpha * fi * fo;
}
/* ── Instance par scène ──────────────────────────────────────────────── */
function createInstance(scene) {
var canvas = scene.querySelector('.cxp-canvas');
if (!canvas) return null;
var ctx = canvas.getContext('2d');
var dpr = window.devicePixelRatio || 1;
var W = 0,
H = 0;
var raf = null;
var running = false;
var lastBurst = 0;
var AUTO_INTERVAL = 7000;
/* ── Pool LOCAL ─────────────────────────────────────────────────────── */
var pool = [];
var active = [];
var floaters = [];
function mkP() {
return {
alive: false,
x: 0,
y: 0,
vx: 0,
vy: 0,
w: 0,
h: 0,
shape: 'rect',
color: '#fff',
colorDark: '#888',
rotX: 0,
rotY: 0,
rotVX: 0,
rotVY: 0,
wobPhase: 0,
wobSpeed: 0,
layer: 0,
alpha: 1,
life: 0,
lifeMax: 1,
seg: null,
segLen: 0,
glintT: 0,
glintDur: 0
};
}
function getP() {
for (var i = 0; i < pool.length; i++) {
if (!pool[i].alive) return pool[i];
}
if (pool.length >= MAX_PARTS) return null;
var p = mkP();
pool.push(p);
return p;
}
/* ── Configuration d'une particule d'explosion ───────────────────────── */
function setupP(p, x, y, angle, spd, layerIdx, colors, isFloater) {
var S = SPEED[layerIdx];
var sz = layerIdx === 1 ? SZ_NEAR : SZ_FAR;
p.alive = true;
p.x = x;
p.y = y;
p.vx = Math.cos(angle) * spd;
p.vy = Math.sin(angle) * spd - (isFloater ? 0 : BIAS_UP);
p.layer = layerIdx;
p.alpha = S.alpha;
p.shape = SHAPES[(Math.random() * SHAPES.length) | 0];
p.color = pick(colors);
p.colorDark = darken(p.color, 0.30);
if (p.shape === 'rect' || p.shape === 'ribbon') {
p.w = rnd(sz.rw[0], sz.rw[1]);
p.h = rnd(sz.rh[0], sz.rh[1]);
} else if (p.shape === 'circle') {
p.w = rnd(sz.c[0], sz.c[1]);
p.h = p.w;
} else {
p.w = rnd(sz.s[0], sz.s[1]);
p.h = p.w;
}
p.rotX = rnd(0, PI2);
p.rotY = rnd(0, PI2);
p.rotVX = rnd(0.04, 0.14) * sgn();
p.rotVY = rnd(0.03, 0.11) * sgn();
p.wobPhase = rnd(0, PI2);
p.wobSpeed = rnd(0.032, 0.090);
/* Vie 2,4–3,4 s @ 60fps → 144–204 frames ; fondu dernier 25 % */
p.lifeMax = isFloater ? 99999 : rnd(144, 204);
p.life = 0;
p.glintT = 0;
p.glintDur = rnd(10, 30);
if (p.shape === 'ribbon') {
p.segLen = 9;
p.seg = [];
for (var s = 0; s < p.segLen; s++) p.seg.push({
x: p.x,
y: p.y
});
} else {
p.seg = null;
}
}
/* ── Émission d'un lot ───────────────────────────────────────────────── */
function emitBatch(cx, cy, count, colors, power) {
var emitR = rnd(0, 8),
emitA = rnd(0, PI2);
for (var i = 0; i < count; i++) {
var p = getP();
if (!p) break;
var ea = rnd(0, PI2);
var ox = cx + Math.cos(ea) * rnd(0, 8);
var oy = cy + Math.sin(ea) * rnd(0, 8);
var angle = rnd(-PI, PI);
var layerIdx = Math.random() < 0.38 ? 0 : 1;
var S = SPEED[layerIdx];
var spd = rnd(S.spMin, S.spMax) * power;
setupP(p, ox, oy, angle, spd, layerIdx, colors, false);
active.push(p);
}
}
/* ── Émission étalée sur 4 vagues (120–180 ms) ──────────────────────── */
var WAVE_DELAYS = [0, 42, 90, 148]; /* ms */
var WAVE_FRAC = [0.30, 0.28, 0.25]; /* 3 premières vagues, la 4e = reste */
function burstStaggered(cx, cy, total, colors, power) {
if (!running) start();
colors = colors || getColors();
power = power || (REDUCE ? 0.50 : 1.0);
WAVE_DELAYS.forEach(function(ms, idx) {
var cnt;
if (idx < 3) {
cnt = Math.floor(total * WAVE_FRAC[idx]);
} else {
cnt = total - Math.floor(total * (WAVE_FRAC[0] + WAVE_FRAC[1] + WAVE_FRAC[2]));
}
setTimeout(function() {
emitBatch(cx, cy, cnt, colors, power);
}, ms);
});
lastBurst = performance.now();
}
/* ── Confettis flotteurs permanents ─────────────────────────────────── */
function initFloaters(colors) {
floaters = [];
if (REDUCE) return;
for (var i = 0; i < 12; i++) {
var p = mkP();
var lIdx = Math.random() < 0.5 ? 0 : 1;
var S = SPEED[lIdx];
p.alive = true;
p.x = rnd(0.06, 0.94) * W;
p.y = rnd(-0.10, 0.90) * H;
p.vx = rnd(-0.3, 0.3);
p.vy = rnd(0.15, 0.50);
p.layer = lIdx;
p.alpha = rnd(0.50, 0.80);
p.shape = SHAPES[(Math.random() * SHAPES.length) | 0];
p.color = pick(colors);
p.colorDark = darken(p.color, 0.30);
var sz = lIdx === 1 ? SZ_NEAR : SZ_FAR;
if (p.shape === 'rect' || p.shape === 'ribbon') {
p.w = rnd(sz.rw[0], sz.rw[1]);
p.h = rnd(sz.rh[0], sz.rh[1]);
} else if (p.shape === 'circle') {
p.w = rnd(sz.c[0], sz.c[1]);
p.h = p.w;
} else {
p.w = rnd(sz.s[0], sz.s[1]);
p.h = p.w;
}
p.rotX = rnd(0, PI2);
p.rotY = rnd(0, PI2);
p.rotVX = rnd(0.006, 0.022) * sgn();
p.rotVY = rnd(0.005, 0.018) * sgn();
p.wobPhase = rnd(0, PI2);
p.wobSpeed = rnd(0.015, 0.045);
p.lifeMax = 99999;
p.life = 30; /* dépasse le fade-in */
if (p.shape === 'ribbon') {
p.segLen = 9;
p.seg = [];
for (var s = 0; s < p.segLen; s++) p.seg.push({
x: p.x,
y: p.y
});
}
floaters.push(p);
}
}
function updateFloater(p) {
p.wobPhase += p.wobSpeed;
p.vx += Math.sin(p.wobPhase) * 0.018;
p.vy += 0.014; /* gravité douce */
p.vx *= 0.992;
p.vy *= 0.992;
p.x += p.vx;
p.y += p.vy;
p.rotX += p.rotVX;
p.rotY += p.rotVY;
if (p.shape === 'ribbon' && p.seg) {
for (var s = p.segLen - 1; s > 0; s--) {
p.seg[s].x = p.seg[s - 1].x;
p.seg[s].y = p.seg[s - 1].y;
}
p.seg[0].x = p.x;
p.seg[0].y = p.y;
}
if (p.y > H + 60) {
p.x = rnd(0.06, 0.94) * W;
p.y = -rnd(15, 70);
p.vx = rnd(-0.2, 0.2);
p.vy = rnd(0.15, 0.45);
}
}
/* ── Dessin particules ────────────────────────────────────────────────── */
function drawRect(p) {
var cosX = Math.cos(p.rotX),
cosY = Math.cos(p.rotY);
var nz = cosX * cosY;
var aw = Math.abs(cosY) * p.w,
ah = Math.abs(cosX) * p.h;
if (aw < 0.6 || ah < 0.4) return;
var a = lifeAlpha(p);
if (a <= 0.004) return;
var glint = 0;
if (p.glintT < p.glintDur) {
glint = Math.sin(p.glintT / p.glintDur * PI) * 0.92;
p.glintT++;
}
ctx.save();
ctx.globalAlpha = a;
ctx.translate(p.x, p.y);
ctx.rotate(p.rotX * 0.45);
if (p.layer === 1) {
ctx.shadowColor = p.color;
ctx.shadowBlur = Math.abs(p.vx + p.vy) * 0.28;
}
if (glint > 0.20) ctx.fillStyle = glint > 0.58 ? '#ffffff' : p.color;
else if (nz < 0) ctx.fillStyle = p.colorDark;
else ctx.fillStyle = p.color;
ctx.fillRect(-aw / 2, -ah / 2, aw, ah);
ctx.shadowBlur = 0;
ctx.restore();
}
function drawCircle(p) {
var r = Math.abs(Math.cos(p.rotX)) * p.w * 0.5;
if (r < 0.4) return;
var a = lifeAlpha(p);
if (a <= 0.004) return;
ctx.save();
ctx.globalAlpha = a;
ctx.beginPath();
ctx.arc(p.x, p.y, r, 0, PI2);
ctx.fillStyle = Math.cos(p.rotY) < 0 ? p.colorDark : p.color;
ctx.fill();
ctx.restore();
}
function drawRibbon(p) {
if (!p.seg || p.seg.length < 2) return;
var a = lifeAlpha(p);
if (a <= 0.004) return;
ctx.save();
ctx.globalAlpha = a;
ctx.lineWidth = Math.max(1.2, p.h * 0.88);
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.strokeStyle = Math.cos(p.rotY) >= 0 ? p.color : p.colorDark;
ctx.beginPath();
ctx.moveTo(p.seg[0].x, p.seg[0].y);
for (var s = 1; s < p.seg.length; s++) {
var mx = (p.seg[s - 1].x + p.seg[s].x) * 0.5,
my = (p.seg[s - 1].y + p.seg[s].y) * 0.5;
ctx.quadraticCurveTo(p.seg[s - 1].x, p.seg[s - 1].y, mx, my);
}
ctx.stroke();
ctx.restore();
}
function drawStar(p) {
var r = Math.abs(Math.cos(p.rotX)) * p.w * 0.52;
if (r < 0.7) return;
var a = lifeAlpha(p);
if (a <= 0.004) return;
ctx.save();
ctx.globalAlpha = a;
ctx.translate(p.x, p.y);
ctx.rotate(p.rotY);
ctx.beginPath();
for (var i = 0; i < 10; i++) {
var ang = (i * PI) / 5 - PI / 2;
var rad = (i % 2 === 0) ? r : r * 0.40;
if (i === 0) ctx.moveTo(Math.cos(ang) * rad, Math.sin(ang) * rad);
else ctx.lineTo(Math.cos(ang) * rad, Math.sin(ang) * rad);
}
ctx.closePath();
ctx.fillStyle = Math.cos(p.rotY) >= 0 ? p.color : p.colorDark;
ctx.fill();
ctx.restore();
}
function drawP(p) {
if (p.shape === 'rect') drawRect(p);
else if (p.shape === 'circle') drawCircle(p);
else if (p.shape === 'ribbon') drawRibbon(p);
else if (p.shape === 'star') drawStar(p);
}
/* ── Physique particule (explosion) ─────────────────────────────────── */
function updateP(p) {
var dt = _tickDt;
p.life += dt;
/* Physique Δt-correcte : DRAG^dt compense les fps > 60 */
var drag = Math.pow(DRAG, dt);
p.vy += G * dt;
p.vx *= drag;
p.vy *= drag;
p.wobPhase += p.wobSpeed * dt;
p.vx += Math.sin(p.wobPhase) * WOB * (0.20 + 0.80 * Math.random()) * 0.04 * dt;
p.x += p.vx * dt;
p.y += p.vy * dt; /* ×dt : pixels/s indépendants du fps */
/* Clamp X avec rebond mou (Owen) */
if (p.x < -10) {
p.x = -10;
if (p.vx < 0) p.vx *= -0.35;
}
if (p.x > W + 10) {
p.x = W + 10;
if (p.vx > 0) p.vx *= -0.35;
}
var spinDecay = Math.max(0.07, 1 - p.life / (p.lifeMax * 1.8));
p.rotX += p.rotVX * spinDecay * dt;
p.rotY += p.rotVY * spinDecay * dt;
if (p.shape === 'ribbon' && p.seg) {
for (var s = p.segLen - 1; s > 0; s--) {
p.seg[s].x = p.seg[s - 1].x;
p.seg[s].y = p.seg[s - 1].y;
}
p.seg[0].x = p.x;
p.seg[0].y = p.y;
}
}
/* ── Sparkles ambiants ─────────────────────────────────────────────── */
var sparkles = [];
function initSparkles(colors) {
sparkles = [];
if (REDUCE) return;
for (var i = 0; i < 14; i++) sparkles.push({
x: rnd(0.06, 0.94),
y: rnd(0.06, 0.94),
r: rnd(0.8, 2.0),
phase: rnd(0, PI2),
speed: rnd(0.022, 0.058),
color: pick(colors),
base: rnd(0.07, 0.20)
});
}
function drawSparkles(colors) {
for (var i = 0; i < sparkles.length; i++) {
var sp = sparkles[i];
sp.phase += sp.speed;
var a = sp.base * (0.3 + 0.7 * Math.abs(Math.sin(sp.phase)));
if (Math.random() < 0.002) sp.color = pick(colors);
ctx.save();
ctx.globalAlpha = a;
ctx.fillStyle = sp.color;
ctx.beginPath();
ctx.arc(sp.x * W, sp.y * H, sp.r, 0, PI2);
ctx.fill();
ctx.restore();
}
}
/* ── Lecture palette ───────────────────────────────────────────────── */
function getColors() {
var attr = scene.dataset.colors;
return attr ? attr.split(',').map(function(s) {
return s.trim();
}) : COLORS_DEFAULT;
}
/* ── Redimensionnement ─────────────────────────────────────────────── */
function resize() {
var rect = scene.getBoundingClientRect();
W = rect.width;
H = rect.height;
canvas.width = Math.round(W * dpr);
canvas.height = Math.round(H * dpr);
canvas.style.width = W + 'px';
canvas.style.height = H + 'px';
ctx.scale(dpr, dpr);
}
/* ── API burst ─────────────────────────────────────────────────────── */
function burst(x, y, count, colors, power) {
if (!running) start();
burstStaggered(x, y, count || (REDUCE ? 20 : 260), colors, power);
}
function burstFromBtn(count) {
var btn = scene.querySelector('.cxp-btn');
if (btn) {
var sr = scene.getBoundingClientRect();
var br = btn.getBoundingClientRect();
burst(br.left + br.width / 2 - sr.left, br.top + br.height / 2 - sr.top, count || 260);
} else {
burst(W / 2, H / 2, count || 260);
}
}
/* ── Boucle de rendu ───────────────────────────────────────────────── */
function tick(ts) {
if (!running) return;
raf = requestAnimationFrame(tick);
/* Δt normalisé : 60fps→1,0 ; 120fps→0,5 ; cap 3× pour éviter spirale */
_tickDt = _lastTickTs > 0 ? Math.min((ts - _lastTickTs) / 16.667, 3.0) : 1.0;
_lastTickTs = ts;
/* Auto-explosion toutes les 7 s si scène vide */
if (!REDUCE && active.length === 0 && ts - lastBurst >= AUTO_INTERVAL) {
burstFromBtn();
return;
}
ctx.clearRect(0, 0, W, H);
drawSparkles(getColors());
/* Flotteurs */
for (var i = 0; i < floaters.length; i++) {
updateFloater(floaters[i]);
drawP(floaters[i]);
}
/* Particules explosion (lointain d'abord) */
active.sort(function(a, b) {
return a.layer - b.layer;
});
for (var j = active.length - 1; j >= 0; j--) {
var p = active[j];
updateP(p);
/* Sortie : sol uniquement (Owen : X clampé, pas de sortie latérale) */
if (p.life >= p.lifeMax || p.y > H + 50) {
p.alive = false;
active.splice(j, 1);
continue;
}
drawP(p);
}
}
function start() {
if (running) return;
running = true;
resize();
var cols = getColors();
initSparkles(cols);
initFloaters(cols);
raf = requestAnimationFrame(tick);
}
function stop() {
running = false;
if (raf) {
cancelAnimationFrame(raf);
raf = null;
}
for (var i = 0; i < active.length; i++) active[i].alive = false;
active.length = 0;
if (ctx) ctx.clearRect(0, 0, W, H);
}
/* ── Événements (hiérarchie clic sécurisée) ──────────────────────── */
var btn = scene.querySelector('.cxp-btn');
if (btn) {
btn.addEventListener('click', function(e) {
e.stopPropagation();
burstFromBtn(260);
if (!REDUCE) {
btn.classList.add('cxp-punch');
setTimeout(function() {
btn.classList.remove('cxp-punch');
}, 320);
}
});
}
scene.addEventListener('click', function(e) {
if (btn && (e.target === btn || btn.contains(e.target))) return;
var rect = scene.getBoundingClientRect();
burst(e.clientX - rect.left, e.clientY - rect.top, 160);
});
/* IntersectionObserver */
if (typeof IntersectionObserver !== 'undefined') {
new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
start();
if (lastBurst === 0) {
setTimeout(function() {
if (REDUCE) burstStaggered(W / 2, H / 2, 20, null, 0.50);
else burstFromBtn(260);
}, 180);
}
} else {
stop();
}
});
}, {
threshold: 0.15
}).observe(scene);
} else {
start();
setTimeout(function() {
burstFromBtn(260);
}, 180);
}
if (typeof ResizeObserver !== 'undefined') {
new ResizeObserver(resize).observe(scene);
} else {
window.addEventListener('resize', resize);
}
return {
burst: burst,
burstFromBtn: burstFromBtn,
stop: stop,
start: start
};
}
/* ── Initialisation ───────────────────────────────────────────────────── */
function init() {
var scenes = document.querySelectorAll('.cxp-scene');
var instances = [];
scenes.forEach(function(scene) {
var inst = createInstance(scene);
if (inst) instances.push({
scene: scene,
inst: inst
});
});
window.ConfettiExplosion = {
burst: function(opts) {
opts = opts || {};
instances.forEach(function(item) {
var r = item.scene.getBoundingClientRect();
item.inst.burst(
opts.x != null ? opts.x : r.width / 2,
opts.y != null ? opts.y : r.height / 2,
opts.count, opts.colors, opts.power
);
});
}
};
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
})();View source code
<div class="demo-preview">
<div class="frequency-bars" id="frequencyBars"></div>
</div>.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
box-sizing: border-box
}
.demo-preview {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 250px;
overflow: hidden;
position: relative;
background: rgb(10, 10, 26);
color: rgb(255, 255, 255);
font-family: sans-serif;
}
.frequency-bars {
display: flex;
align-items: flex-end;
justify-content: center;
gap: 4px;
height: 120px;
}
.frequency-bars .bar {
width: 8px;
background: linear-gradient(to top, #6366f1, #d946ef);
border-radius: 4px 4px 0 0;
transition: height 0.05s ease;
}(function() {
/* fx-0515 — Frequency Bars (gratuit, autonome — restauré 22/08/2026) */
(function() {
const frequencyBars = document.getElementById('frequencyBars');
if (!frequencyBars) return;
const barCount = 20;
for (let i = 0; i < barCount; i++) {
const bar = document.createElement('div');
bar.className = 'bar';
bar.style.height = '10px';
frequencyBars.appendChild(bar);
}
// Audio simulé (pas de micro ni de fichier) : superposition de sinus + bruit
function getSimulatedAudio(count, time, baseFreq = 1) {
const values = [];
for (let i = 0; i < count; i++) {
const freq = baseFreq + i * 0.5;
const value = Math.abs(
Math.sin(time * freq * 0.002) * 0.5 +
Math.sin(time * freq * 0.003 + i) * 0.3 +
Math.random() * 0.2
);
values.push(Math.min(1, value));
}
return values;
}
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
function animate(time) {
const bars = frequencyBars.querySelectorAll('.bar');
const data = getSimulatedAudio(barCount, time);
bars.forEach((bar, i) => {
bar.style.height = (10 + data[i] * 100) + 'px';
});
if (!reduceMotion) requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
})();
})();Sound Effects
View source code
<div class="demo-preview" style="display: flex; align-items: center; justify-content: center;">
<button class="snd-click-btn" id="sndClickBtn">
<span class="snd-wave">🔊</span> Click Me
</button>
</div>.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
box-sizing: border-box
}
.demo-preview {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 250px;
overflow: hidden;
position: relative;
background: rgb(10, 10, 26);
color: rgb(255, 255, 255);
font-family: sans-serif;
}
.snd-click-btn {
padding: 14px 28px;
border: none;
border-radius: 10px;
background: linear-gradient(135deg, #6366f1, #a855f7);
color: #fff;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: transform 0.15s, box-shadow 0.15s;
display: flex;
align-items: center;
gap: 8px;
}
.snd-click-btn:active {
transform: scale(0.95);
}
.snd-click-btn:hover {
box-shadow: 0 4px 20px rgba(99, 102, 241, 0.4);
}
.snd-click-btn .snd-wave {
display: inline-block;
transition: transform 0.2s;
}(function() {
(function() {
var btn = document.getElementById('sndClickBtn');
if (!btn) return;
var ctx = null;
// Le contexte audio ne peut naitre que sur un geste : on le cree au 1er clic.
function audio() {
return ctx || (ctx = new(window.AudioContext || window.webkitAudioContext)());
}
btn.addEventListener('click', function() {
var a = audio(),
t = a.currentTime;
var o = a.createOscillator(),
g = a.createGain();
o.type = 'triangle';
o.frequency.setValueAtTime(880, t);
o.frequency.exponentialRampToValueAtTime(320, t + 0.05);
g.gain.setValueAtTime(0.14, t);
g.gain.exponentialRampToValueAtTime(0.0001, t + 0.09);
o.connect(g).connect(a.destination);
o.start(t);
o.stop(t + 0.1);
});
})();
})();View source code
<div class="demo-preview" style="display: flex; align-items: center; justify-content: center;">
<div class="snd-hover-card" id="sndHoverCard">
<div class="snd-hover-icon">🎵</div>
<div class="snd-hover-label">Hover me</div>
</div>
</div>.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
box-sizing: border-box
}
.demo-preview {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 250px;
overflow: hidden;
position: relative;
background: rgb(10, 10, 26);
color: rgb(255, 255, 255);
font-family: sans-serif;
}
/* Variables du design system du site, dont ce code dépend (valeurs de css/style.css — à adapter librement). */
.demo-preview {
--text-secondary: #a1a1aa;
}
.snd-hover-card {
width: 180px;
padding: 20px;
border-radius: 12px;
background: rgba(99, 102, 241, 0.1);
border: 1px solid rgba(99, 102, 241, 0.2);
text-align: center;
transition: all 0.3s ease;
cursor: pointer;
}
.snd-hover-card:hover {
transform: translateY(-4px);
border-color: #6366f1;
background: rgba(99, 102, 241, 0.2);
}
.snd-hover-icon {
font-size: 2rem;
margin-bottom: 8px;
}
.snd-hover-label {
font-size: 0.8rem;
color: var(--text-secondary);
}(function() {
(function() {
var carte = document.getElementById('sndHoverCard');
if (!carte) return;
var ctx = null,
dernier = 0;
function audio() {
return ctx || (ctx = new(window.AudioContext || window.webkitAudioContext)());
}
carte.addEventListener('mouseenter', function() {
if (Date.now() - dernier < 250) return; // pas de mitraillage au survol
dernier = Date.now();
var a = audio(),
t = a.currentTime;
var o = a.createOscillator(),
g = a.createGain();
o.type = 'sine';
o.frequency.setValueAtTime(523.25, t); // do5
o.frequency.linearRampToValueAtTime(659.25, t + 0.12);
g.gain.setValueAtTime(0.0001, t);
g.gain.exponentialRampToValueAtTime(0.1, t + 0.02);
g.gain.exponentialRampToValueAtTime(0.0001, t + 0.22);
o.connect(g).connect(a.destination);
o.start(t);
o.stop(t + 0.24);
});
})();
})();Typing Sound Input
Text input with synthetically generated keyboard typing sound on each keystroke.
Effect page →Notification Chime
Notification with synthesized chime sound and appearance animation.
Effect page →View source code
<div class="demo-preview" style="display: flex; align-items: center; justify-content: center;">
<div class="snd-notif-container">
<button class="snd-notif-trigger" id="sndNotifBtn">🔔 Notification</button>
<div class="snd-notif-toast" id="sndNotifToast">✅ New message received!</div>
</div>
</div>.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
box-sizing: border-box
}
.demo-preview {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 250px;
overflow: hidden;
position: relative;
background: rgb(10, 10, 26);
color: rgb(255, 255, 255);
font-family: sans-serif;
}
.snd-notif-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.snd-notif-trigger {
padding: 10px 20px;
border-radius: 8px;
background: rgba(16, 185, 129, 0.2);
border: 1px solid rgba(16, 185, 129, 0.3);
color: #10b981;
cursor: pointer;
font-size: 0.85rem;
}
.snd-notif-toast {
padding: 10px 16px;
border-radius: 8px;
background: rgba(16, 185, 129, 0.15);
border: 1px solid rgba(16, 185, 129, 0.3);
color: #10b981;
font-size: 0.8rem;
opacity: 0;
transform: translateY(10px);
transition: all 0.3s ease;
}
.snd-notif-toast.show {
opacity: 1;
transform: translateY(0);
}(function() {
(function() {
var btn = document.getElementById('sndNotifBtn'),
toast = document.getElementById('sndNotifToast');
if (!btn || !toast) return;
var ctx = null,
minuteur = null;
function audio() {
return ctx || (ctx = new(window.AudioContext || window.webkitAudioContext)());
}
function note(a, freq, depart, duree, vol) {
var o = a.createOscillator(),
g = a.createGain();
o.type = 'sine';
o.frequency.setValueAtTime(freq, depart);
g.gain.setValueAtTime(0.0001, depart);
g.gain.exponentialRampToValueAtTime(vol, depart + 0.015);
g.gain.exponentialRampToValueAtTime(0.0001, depart + duree);
o.connect(g).connect(a.destination);
o.start(depart);
o.stop(depart + duree + 0.02);
}
btn.addEventListener('click', function() {
var a = audio(),
t = a.currentTime;
note(a, 880, t, 0.18, 0.12); // la5
note(a, 1318.5, t + 0.1, 0.35, 0.1); // mi6 — carillon a deux notes
toast.classList.add('show');
clearTimeout(minuteur);
minuteur = setTimeout(function() {
toast.classList.remove('show');
}, 2600);
});
})();
})();View source code
<div class="demo-preview" style="display: flex; align-items: center; justify-content: center;">
<div class="snd-toggle-container">
<div class="snd-toggle" id="sndToggle">
<div class="snd-toggle-knob"></div>
</div>
<div class="snd-toggle-label" id="sndToggleLabel">OFF</div>
</div>
</div>.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
box-sizing: border-box
}
.demo-preview {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 250px;
overflow: hidden;
position: relative;
background: rgb(10, 10, 26);
color: rgb(255, 255, 255);
font-family: sans-serif;
}
/* Variables du design system du site, dont ce code dépend (valeurs de css/style.css — à adapter librement). */
.demo-preview {
--text-secondary: #a1a1aa;
}
.snd-toggle-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.snd-toggle {
width: 60px;
height: 32px;
border-radius: 16px;
background: rgba(255, 255, 255, 0.1);
position: relative;
cursor: pointer;
transition: background 0.3s;
}
.snd-toggle.on {
background: #6366f1;
}
.snd-toggle-knob {
width: 26px;
height: 26px;
border-radius: 50%;
background: #fff;
position: absolute;
top: 3px;
left: 3px;
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.snd-toggle.on .snd-toggle-knob {
transform: translateX(28px);
}
.snd-toggle-label {
font-size: 0.8rem;
color: var(--text-secondary);
}(function() {
(function() {
var inter = document.getElementById('sndToggle'),
label = document.getElementById('sndToggleLabel');
if (!inter) return;
var ctx = null;
function audio() {
return ctx || (ctx = new(window.AudioContext || window.webkitAudioContext)());
}
inter.addEventListener('click', function() {
var allume = inter.classList.toggle('on');
if (label) label.textContent = allume ? 'ON' : 'OFF';
var a = audio(),
t = a.currentTime;
var o = a.createOscillator(),
g = a.createGain();
o.type = 'square';
// montee a l'allumage, descente a l'extinction : le son dit l'etat
o.frequency.setValueAtTime(allume ? 420 : 760, t);
o.frequency.exponentialRampToValueAtTime(allume ? 760 : 420, t + 0.07);
g.gain.setValueAtTime(0.07, t);
g.gain.exponentialRampToValueAtTime(0.0001, t + 0.11);
o.connect(g).connect(a.destination);
o.start(t);
o.stop(t + 0.12);
});
})();
})();Drag Feedback Sound
Continuous friction sound during drag, varying with movement speed.
Effect page →View source code
<div class="demo-preview" style="display: flex; align-items: center; justify-content: center;">
<div class="snd-tones">
<button class="snd-tone-btn snd-tone-success" id="sndSuccess">✅ Success</button>
<button class="snd-tone-btn snd-tone-error" id="sndError">❌ Error</button>
</div>
</div>.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
box-sizing: border-box
}
.demo-preview {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 250px;
overflow: hidden;
position: relative;
background: rgb(10, 10, 26);
color: rgb(255, 255, 255);
font-family: sans-serif;
}
.snd-tones {
display: flex;
gap: 16px;
}
.snd-tone-btn {
padding: 12px 24px;
border-radius: 8px;
border: none;
cursor: pointer;
font-size: 0.85rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 6px;
transition: transform 0.2s;
}
.snd-tone-btn:active {
transform: scale(0.95);
}
.snd-tone-success {
background: rgba(16, 185, 129, 0.2);
color: #10b981;
border: 1px solid rgba(16, 185, 129, 0.3);
}
.snd-tone-error {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
border: 1px solid rgba(239, 68, 68, 0.3);
}(function() {
(function() {
var ok = document.getElementById('sndSuccess'),
ko = document.getElementById('sndError');
if (!ok && !ko) return;
var ctx = null;
function audio() {
return ctx || (ctx = new(window.AudioContext || window.webkitAudioContext)());
}
function jouer(freqs, type, pas, duree, vol) {
var a = audio(),
t = a.currentTime;
freqs.forEach(function(f, i) {
var o = a.createOscillator(),
g = a.createGain(),
d = t + i * pas;
o.type = type;
o.frequency.setValueAtTime(f, d);
g.gain.setValueAtTime(0.0001, d);
g.gain.exponentialRampToValueAtTime(vol, d + 0.015);
g.gain.exponentialRampToValueAtTime(0.0001, d + duree);
o.connect(g).connect(a.destination);
o.start(d);
o.stop(d + duree + 0.02);
});
}
// arpege majeur qui monte = reussite ; deux notes basses qui descendent = erreur
if (ok) ok.addEventListener('click', function() {
jouer([523.25, 659.25, 783.99], 'sine', 0.09, 0.22, 0.1);
});
if (ko) ko.addEventListener('click', function() {
jouer([311.13, 233.08], 'sawtooth', 0.13, 0.3, 0.07);
});
})();
})();Liquid Metal Shader
Liquid metallic surface in WebGL. Simplified raymarching, Perlin ripples, dynamic light reflection.
Effect page →View source code
<div class="ps-canvas-wrap">
<canvas class="ps-canvas" id="psLiquidMetal"></canvas>
</div>.demo-preview:has(.ps-canvas-wrap) {
min-height: 280px;
background: rgb(10, 10, 15);
}
.ps-canvas {
width: 100%;
height: 280px;
display: block;
border-radius: 0px;
}
.ps-canvas-wrap {
position: relative;
width: 100%;
height: 280px;
}
.ps-canvas-wrap canvas {
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
}(function() {
var settings = { color: 'silver', speed: 1.0, distortion: 0.6, lightAngle: 45 };
var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
function initMetal(canvas) {
// Reduced motion : fallback CSS gradient déjà en place via @media
if (reduceMotion) return;
const gl = canvas.getContext('webgl', { antialias: false, alpha: false }) ||
canvas.getContext('experimental-webgl');
// WebGL not supported → CSS gradient fallback (handled in CSS @media query)
if (!gl) {
canvas.style.display = 'none';
return;
}
// === Vertex shader : fullscreen quad ===
const VS = `
attribute vec2 a_pos;
void main() { gl_Position = vec4(a_pos, 0.0, 1.0); }
`;
// === Fragment shader : liquid metal via Perlin noise + raymarching simplifié ===
const FS = `
precision highp float;
uniform vec2 u_res;
uniform float u_time;
uniform float u_speed;
uniform float u_distortion;
uniform float u_light_angle;
uniform vec3 u_color_a;
uniform vec3 u_color_b;
uniform vec3 u_color_c;
// Classic 2D simplex noise (Ashima Arts, MIT)
vec3 mod289(vec3 x) { return x - floor(x * (1.0/289.0)) * 289.0; }
vec2 mod289(vec2 x) { return x - floor(x * (1.0/289.0)) * 289.0; }
vec3 permute(vec3 x) { return mod289(((x*34.0)+1.0)*x); }
float snoise(vec2 v){
const vec4 C = vec4(0.211324865405187, 0.366025403784439,
-0.577350269189626, 0.024390243902439);
vec2 i = floor(v + dot(v, C.yy));
vec2 x0 = v - i + dot(i, C.xx);
vec2 i1 = (x0.x > x0.y) ? vec2(1.0,0.0) : vec2(0.0,1.0);
vec4 x12 = x0.xyxy + C.xxzz;
x12.xy -= i1;
i = mod289(i);
vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0))
+ i.x + vec3(0.0, i1.x, 1.0));
vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0);
m = m*m; m = m*m;
vec3 x = 2.0 * fract(p * C.www) - 1.0;
vec3 h = abs(x) - 0.5;
vec3 ox = floor(x + 0.5);
vec3 a0 = x - ox;
m *= 1.79284291400159 - 0.85373472095314 * (a0*a0 + h*h);
vec3 g;
g.x = a0.x * x0.x + h.x * x0.y;
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
return 130.0 * dot(m, g);
}
// Octave fbm for richer surface
float fbm(vec2 p) {
float v = 0.0;
float a = 0.5;
for (int i = 0; i < 4; i++) {
v += a * snoise(p);
p *= 2.0;
a *= 0.5;
}
return v;
}
void main() {
vec2 uv = (gl_FragCoord.xy - 0.5 * u_res) / min(u_res.x, u_res.y);
float t = u_time * u_speed * 0.3;
// Surface height via fbm
float h = fbm(uv * 2.0 + vec2(t, t * 0.7)) * u_distortion;
// Surface normal via gradient (finite differences)
float eps = 0.01;
float hx = fbm((uv + vec2(eps,0.0)) * 2.0 + vec2(t, t*0.7)) * u_distortion;
float hy = fbm((uv + vec2(0.0,eps)) * 2.0 + vec2(t, t*0.7)) * u_distortion;
vec3 normal = normalize(vec3((h - hx)/eps, (h - hy)/eps, 1.0));
// Light direction
float angle = radians(u_light_angle);
vec3 lightDir = normalize(vec3(cos(angle), sin(angle), 0.6));
// Specular metal reflection
float diffuse = max(dot(normal, lightDir), 0.0);
vec3 viewDir = vec3(0.0, 0.0, 1.0);
vec3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
// Iridescence factor (used by gold/iridescent presets)
float irid = 0.5 + 0.5 * sin(h * 8.0 + t * 2.0);
// Mix three colour stops based on diffuse + iridescence
vec3 base = mix(u_color_a, u_color_b, diffuse);
base = mix(base, u_color_c, irid * 0.5);
// Metal gloss : add specular highlight
vec3 finalColor = base + vec3(spec * 1.2);
// Vignette subtle
float vig = 1.0 - 0.4 * length(uv);
finalColor *= vig;
gl_FragColor = vec4(finalColor, 1.0);
}
`;
// === Compile + link program ===
function compile(type, src) {
const sh = gl.createShader(type);
gl.shaderSource(sh, src);
gl.compileShader(sh);
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
console.error('Shader compile error:', gl.getShaderInfoLog(sh));
gl.deleteShader(sh);
return null;
}
return sh;
}
const vs = compile(gl.VERTEX_SHADER, VS);
const fs = compile(gl.FRAGMENT_SHADER, FS);
if (!vs || !fs) return;
const prog = gl.createProgram();
gl.attachShader(prog, vs);
gl.attachShader(prog, fs);
gl.linkProgram(prog);
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
console.error('Program link error:', gl.getProgramInfoLog(prog));
return;
}
gl.useProgram(prog);
// === Fullscreen quad ===
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, 1,1]), gl.STATIC_DRAW);
const aPos = gl.getAttribLocation(prog, 'a_pos');
gl.enableVertexAttribArray(aPos);
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
// === Uniforms ===
const uRes = gl.getUniformLocation(prog, 'u_res');
const uTime = gl.getUniformLocation(prog, 'u_time');
const uSpeed = gl.getUniformLocation(prog, 'u_speed');
const uDist = gl.getUniformLocation(prog, 'u_distortion');
const uLight = gl.getUniformLocation(prog, 'u_light_angle');
const uColA = gl.getUniformLocation(prog, 'u_color_a');
const uColB = gl.getUniformLocation(prog, 'u_color_b');
const uColC = gl.getUniformLocation(prog, 'u_color_c');
// === Color presets ===
const PALETTES = {
silver: { a: [0.10, 0.11, 0.13], b: [0.85, 0.88, 0.92], c: [0.55, 0.60, 0.70] },
gold: { a: [0.20, 0.13, 0.05], b: [0.95, 0.78, 0.30], c: [0.70, 0.45, 0.15] },
iridescent: { a: [0.10, 0.05, 0.20], b: [0.30, 0.85, 0.95], c: [0.95, 0.40, 0.85] }
};
function syncCanvasSize() {
const rect = canvas.parentElement.getBoundingClientRect();
const dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.round(rect.width * dpr);
canvas.height = Math.round(rect.height * dpr);
canvas.style.width = rect.width + 'px';
canvas.style.height = rect.height + 'px';
gl.viewport(0, 0, canvas.width, canvas.height);
}
syncCanvasSize();
window.addEventListener('resize', syncCanvasSize);
// === Pause when offscreen (perf) ===
let visible = true;
if ('IntersectionObserver' in window) {
new IntersectionObserver((entries) => {
entries.forEach(e => visible = e.isIntersecting);
}, { threshold: 0.01 }).observe(canvas);
}
// === Render loop ===
const start = performance.now();
function render() {
if (visible) {
const t = (performance.now() - start) / 1000;
const pal = PALETTES[settings.color] || PALETTES.silver;
gl.uniform2f(uRes, canvas.width, canvas.height);
gl.uniform1f(uTime, t);
gl.uniform1f(uSpeed, settings.speed);
gl.uniform1f(uDist, settings.distortion);
gl.uniform1f(uLight, settings.lightAngle);
gl.uniform3fv(uColA, pal.a);
gl.uniform3fv(uColB, pal.b);
gl.uniform3fv(uColC, pal.c);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
requestAnimationFrame(render);
}
render();
} // fin initMetal
var el = document.getElementById('psLiquidMetal');
if (el) initMetal(el);
})();Generative Flow Field
Perlin vector field with 5000 particles following the currents. Inspired by the northern lights and ocean currents. Pure Canvas 2D, 60fps.
Effect page →WebGPU Particle Field
Galaxy of particles rendered in WebGPU: tens of thousands of points animated on the GPU, fluid and luminous. Canvas 2D fallback if the browser doesn't support WebGPU yet.
💡 Perfect for: next-gen hero, premium section background, tech launch page, spectacular loading screen.
WebGPU Spirit Vortex
A swarm of spirits rendered in WebGPU: they drift ambientally then get pulled into a spiral around the cursor, like an opening invocation portal. Canvas 2D fallback if WebGPU is unavailable.
💡 Perfect for: Halloween/mystical hero, invocation portal, premium section background, game intro screen, otherworldly atmosphere.
Snow Globe
Interactive semi-3D snow globe (glass, reflections, depth). Shake it to swirl snow truly contained inside the globe, which falls back down and accumulates on the ground. Customizable scene.
💡 Perfect for: greeting card, Christmas hero, festive easter egg, seasonal product page, interactive message.
Confetti Cannon
Two cannons in the bottom corners fire confetti diagonally toward the centre: rectangles with simulated 3D spin (two-tone front/back), wavy streamers (chained segments with lateral wobble) and dots, all under gravity, air drag and flutter. Fires when the scene enters the viewport, on click and via the ConfettiCannon.fire() API. Under prefers-reduced-motion: a single short, gentle burst on load, then nothing.
💡 Perfect for: order/payment confirmations, successful onboarding, user milestones, subscription anniversaries, viral share buttons.
Effect page →View source code
<div class="demo-preview">
<div class="ccn-scene" id="ccnScene"
role="img"
aria-label="Premium Confetti Cannon — two cannons with 3D lighting, parallax and sparks"
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">Click anywhere · or</div>
<button class="ccn-celebrate-btn" aria-label="Fire the confetti cannons">🎉 Celebrate</button>
</div>
</div>.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
box-sizing: border-box
}
.demo-preview {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 250px;
overflow: hidden;
position: relative;
background: rgb(10, 10, 26);
color: rgb(255, 255, 255);
font-family: sans-serif;
}
.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;
}
.ccn-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
display: block;
}
.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;
}
.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;
}
@media (prefers-reduced-motion: reduce) {
.ccn-celebrate-btn,
.ccn-order-btn {
transition: none;
}
.ccn-celebrate-btn:active {
transform: translateX(-50%);
}
}
@media (max-width:480px) {
.ccn-celebrate-btn {
font-size: 12px;
padding: 7px 16px;
}
}(function() {
/* fx-0836 */
(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
};
}());
})();Balloon Release
A single Canvas 2D renders semi-transparent balloons (offset radial gradient, specular highlights, rim light) with a supple Verlet-chain string drawn as a smooth Catmull-Rom curve. Overlapping balloons tint each other; the pointer gently repels them and clicking pops one (fragments + flash). Recycled pool, rAF paused off-screen. Reduced motion: static balloons drawn once.
💡 Perfect for: SaaS anniversary banners (« 1 year with us »), subscription confirmation pages, onboarding welcome screens, product milestone celebrations.
Effect page →Success Celebration
An orchestrated confirmation component: SVG circle drawn progressively from 12 o'clock, checkmark traced in, two pulsing halos, a ring burst of particles (Canvas 2D) and staggered text reveal. Compact toast and full « Payment confirmed » modal share the same code; replayable. In reduced-motion mode the checkmark and text appear instantly, with no particles.
💡 Perfect for: payment confirmations, form saves, SaaS onboarding success, order validation, end of an interactive tutorial.
Effect page →Reaction Burst
Live-streaming reaction bar: each click launches an emoji along a cubic Bézier path with sine wobble and rotation. A simulated live feed keeps reactions from other viewers coming in from the right. Recycled DOM pool (max 40 particles), animated counters. With prefers-reduced-motion: counters only, plain fade — no trajectory.
💡 Perfect for: live video players, interactive webinars, product launch pages, real-time community feeds, social comment streams.
Effect page →Sparkler Light Painting
Two-layer canvas: an accumulation buffer faded each frame (destination-out, low alpha) holds the persistent light-painting trail. Your cursor becomes a sparkler — incandescent white-orange halo, jetting sparks with gravity and individual flicker. Without interaction, an invisible hand loops a glowing parametric heart. Additive compositing, pre-rendered radial sprites, no shadowBlur. Static outline if motion is reduced.
💡 Perfect for: dark product-launch heroes, New Year greeting pages, premium celebration sections, event landing pages, SaaS anniversary banners.
Effect page →