Smoke
Un canvas 2D fait monter des particules de fumée circulaires semi-transparentes avec dérive sinusoïdale et expansion radiale — rAF pur, zéro dépendance.
Vous aurez accès à la démo interactive avec un compte gratuit.
Cet effet fait partie d'Effect.Labs — 811 effets vanilla, une partie gratuite, une partie premium. Atmosphère comprend 57 effets, dont 10 gratuits. Découvrir la catégorie →
3 exemples d'usage



Comment ça marche
L'IIFE initialise le canvas via setupCanvas("psSmoke") — la fonction lit le getBoundingClientRect() du parent pour fixer width/height, puis renvoie {canvas, ctx, w, h}. Un tableau e accumule les particules vivantes. La boucle requestAnimationFrame repeint chaque frame : clearRect puis fillRect en #0a0a0f effacent le frame précédent, puis toutes les particules sont mises à jour et dessinées avant les particules expirées retirées par splice en arrière.
À chaque frame, si Math.random() > 0.3 (70 % de probabilité), une particule naît près du centre bas — x = w/2 ± 5 px, y = h − 30. Ses propriétés : drift horizontal vx = ±0,25 px/frame, montée vy entre −0,5 et −2,0 px/frame, life = 1, decay entre 0,003 et 0,008 (durée de vie 125–333 frames), taille initiale 4–12 px.
Chaque frame, la position évolue : x += vx + 0,3 × sin(0,02 × y) — la composante sinusoïdale indexée sur Y produit une dérive ondulante progressive qui simule la turbulence naturelle de la fumée. y += vy (ascension), life -= decay, size += 0,15 (expansion radiale). Chaque particule est un arc() plein en rgba(180,180,190, 0,15 × life) : 15 % d'opacité max à la naissance, invisible à la mort.
Le code ne gère ni prefers-reduced-motion, ni IntersectionObserver, ni redimensionnement du canvas après l'init. L'IIFE cible un seul canvas par identifiant fixe (id="psSmoke") — pour plusieurs colonnes simultanées, la boucle particules doit être extraite dans une fonction paramétrable par id.
Accessibilité
- prefers-reduced-motion absent : le code ne vérifie pas cette préférence OS — l'animation boucle indéfiniment quelle que soit la configuration accessibilité. Correction : ajouter
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;avant le lancement du rAF. - Le canvas ne porte ni
aria-hidden="true"niroledans le HTML source — à corriger en ajoutantaria-hidden="true"pour tout usage décoratif, afin d'exclure l'élément du flux d'accessibilité. - Aucun contrôle pause/arrêt n'est exposé — WCAG 2.2.2 impose un mécanisme de pause pour toute animation auto-lancée de plus de 5 secondes.
- Pas d'interaction clavier par design (effet purement décoratif) — convient à un fond d'ambiance à condition que le canvas soit isolé du flux d'accessibilité via
aria-hidden. - Faible opacité des particules (
rgba(180,180,190)à 15 % max) — la fumée ne masque pas le contenu textuel superposé sur fond sombre, ce qui limite la gêne visuelle pour les utilisateurs sensibles au contraste.
Compatibilité navigateur
Nécessite Canvas 2D Context et requestAnimationFrame — supportés dans tous les navigateurs modernes. Zéro dépendance externe.
Sans support Canvas, le conteneur s'affiche vide sur fond sombre — aucune erreur JS fatale, le contenu textuel superposé reste lisible.
Le code
Structure HTML à coller dans votre page (CSS + JS disponibles avec un compte premium) :
<div class="demo-preview">
<div class="ps-canvas-wrap">
<canvas class="ps-canvas" id="psSmoke" width="392" height="280"
aria-hidden="true"></canvas>
</div>
</div>
HTML + CSS + JS complet, prêt à coller — avec des centaines d'effets premium.
Personnaliser
Options passées à l'API ou attributs data-* :
| Option / propriété | Défaut | Effet |
|---|---|---|
| rgba(180,180,190, ...) | gris-bleu | Valeurs RGB dans ctx.fillStyle — passez 160,120,80 pour fumée ambrée, 100,130,180 pour fumée bleue, 200,60,40 pour vapeur volcanique rougeâtre. |
| 0.15 * a.life | 0.15 | Multiplicateur d'opacité max — augmentez à 0,35 pour une fumée dense et visible, réduisez à 0,06 pour des volutes à peine perceptibles en arrière-plan. |
| Math.random() > .3 | 0.3 (70 % spawn) | Seuil de spawn par frame — montez à 0,8 (20 %) pour une fumée éparse et distante, descendez à 0,05 (95 %) pour une colonne dense et continue. |
| vy: 1.5 * -Math.random() - .5 | −0,5 à −2,0 px/frame | Vitesse de montée — multipliez le coefficient 1,5 par 0,4 pour une fumée paresseuse, par 2,5 pour une vapeur vive. |
| .3 * Math.sin(.02 * a.y) | amplitude 0,3 px | Amplitude de la dérive sinusoïdale — mettez à 0 pour un panache vertical droit, à 1,2 pour une fumée très ondulée et serpentine. |
| size: 8 * Math.random() + 4 | 4–12 px | Taille initiale des particules — utilisez 20 * Math.random() + 10 pour de gros nuages opulents, 3 * Math.random() + 1 pour des volutes fines et précises. |
| size += .15 | 0,15 px/frame | Taux d'expansion radiale — augmentez à 0,4 pour une fumée qui s'évase rapidement, mettez à 0 pour des particules à taille fixe (style brume flottante). |
FAQ
getElementById("psSmoke") — l'IIFE ne peut s'exécuter qu'une fois. Pour plusieurs colonnes, extrayez la boucle particules dans une fonction prenant un id en paramètre et appelez-la pour chaque canvas. La logique interne est entièrement indépendante : chaque appel maintient son propre tableau de particules et son propre rAF.requestAnimationFrame : if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;. Pour un comportement dégradé (une frame statique), dessinez les premières particules sans démarrer la boucle. Depuis Chrome 76+, le MediaQueryList émet un événement change — vous pouvez réagir en direct si l'utilisateur modifie la préférence pendant la session.fillStyle = `rgba(180,180,190,${i})` par fillStyle = `hsla(${200 + (a.life * 20) | 0},20%,75%,${i})` — la particule naît froide (HSL 220, bleuté) et vire vers le gris neutre (HSL 200) en vieillissant, simulant un refroidissement progressif en montant.