Galaxy Spiral
Un canvas 2D anime 400 étoiles en rotation différentielle sur 3 bras spiraux — noyau radial lumineux et persistance par alpha compositing.
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
Au chargement, 400 étoiles sont placées selon une formule spirale. Chaque étoile appartient à l'un des 3 bras (index % 3) ; son angle initial vaut bras/3 × 2π + dist/80. Le terme dist/80 est le twist radial : plus l'étoile est éloignée du centre, plus elle s'enroule dans son bras. Un jitter de ±0,5 rad la disperse naturellement autour de l'axe du bras. La distance au centre est tirée aléatoirement jusqu'à Math.min(w,h) × 0,42.
La boucle RAF met à jour angle += speed à chaque frame. La vitesse décroît avec la distance : 3×10⁻⁴ + 0,001 × (1 − r/r_max) — c'est la rotation différentielle caractéristique des galaxies réelles (le noyau tourne plus vite que les bras extérieurs). Le canvas n'est pas effacé entre frames : un fillRect semi-transparent avec alpha 0,08 recouvre progressivement les positions passées, produisant un trail de persistance par alpha compositing — il faut ~12 frames (~200 ms à 60 fps) pour qu'une position s'efface complètement.
Un dégradé radial est redessiné à chaque frame sur le noyau central (rayon fixe 30 px) : il va de rgba(200,180,255,0.1) vers transparent, simulant l'éclat d'un bulbe galactique. Chaque étoile est un arc() de 0,5–2 px peint dans l'une des 5 couleurs hardcodées ["#fff","#a78bfa","#ec4899","#fbbf24","#60a5fa"], avec une opacité individuelle de 0,3–0,8.
L'effet s'initialise sur l'id="psGalaxy" via getElementById — une seule instance par page dans le code source. Aucun IntersectionObserver ne coupe le RAF hors viewport (boucle en continu). Aucune API publique n'est exposée : les paramètres sont des constantes JS à modifier directement dans le source.
Accessibilité
- prefers-reduced-motion non géré : l'animation tourne inconditionnellement, sans fallback statique. Côté intégration, wrappez l'init dans
if (!matchMedia('(prefers-reduced-motion:reduce)').matches) {…}et masquez le canvas via@media (prefers-reduced-motion:reduce){.ps-canvas{display:none}}. - Le
<canvas>ne porte pasaria-hidden="true"dans le code source — ajoutez-le pour que les lecteurs d'écran ignorent l'élément graphique. - Aucun
role="img"niaria-labelsur le conteneur — à renseigner selon le contexte (ex.aria-label="Galaxie spirale animée — décoration"). - L'effet est purement décoratif et non interactif : aucun gestionnaire clavier, aucune cible de focus, aucun événement pointeur.
- Les étoiles colorées sur fond
#0a0a0foffrent un contraste visuel élevé (décoration seule — aucun texte à risque de contraste n'est rendu dans le canvas).
Compatibilité navigateur
Nécessite Canvas 2D Context et requestAnimationFrame — disponibles dans tous les navigateurs modernes. Le CSS utilise :has() pour styler le conteneur (Chrome 105+, Firefox 121+, Safari 15.4+) ; l'absence de ce sélecteur n'affecte que le style de fallback du wrap, pas l'animation.
Si <code>canvas</code> n'est pas supporté, le <code>.ps-canvas-wrap</code> s'affiche vide sur fond sombre. Le guard <code>if (!t) return</code> en tête d'IIFE empêche toute erreur JS.
Le code
Structure HTML à coller dans votre page (CSS + JS disponibles avec un compte premium) :
<div class="demo-preview">
<div class="ps-canvas-wrap">
<!-- ⚠ id="psGalaxy" est hardcodé dans le JS : une seule instance par page sans adaptation -->
<canvas class="ps-canvas" id="psGalaxy" 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 |
|---|---|---|
| l (palette couleurs) | ["#fff","#a78bfa","#ec4899","#fbbf24","#60a5fa"] | Tableau des couleurs des étoiles (ligne const l=[…]). 3 couleurs pour un rendu épuré, 7+ pour un look festif. Chaque couleur est convertie manuellement en RGB dans la boucle de dessin — si vous ajoutez une couleur, ajoutez aussi son entrée dans le bloc if/else de conversion. |
| 400 (nombre d'étoiles) | 400 | Taille de la boucle for(let a=0;a<400;a++). Réduisez à 150–200 pour une intégration légère sur petite surface ou mobile faible puissance. |
| a % 3 (bras spiraux) | 3 | Le modulo définit le nombre de bras. Changez en a%2 pour une galaxie barrée, a%5 pour une spirale très dense. Mettez à jour la formule d'angle en conséquence : n/3 → n/N. |
| 80 (twist radial) | 80 | Diviseur dans dist/80. Abaissez à 40 pour des bras très enroulés et compacts, augmentez à 150 pour une spirale presque droite. |
| 0.08 (trail alpha) | 0.08 | Alpha du fillRect de persistance (rgba(10,10,15,0.08)). Réduisez à 0,03 pour des traînées longues et fantasmagoriques, augmentez à 0,25 pour un rendu sec sans trail visible. |
| 0.42 (rayon max) | 0.42 | Facteur appliqué à Math.min(w,h) pour la distance maximale des étoiles. 0,42 = spirale dans 84 % du petit côté. Réduisez à 0,3 pour concentrer, augmentez à 0,5 pour étaler jusqu'aux bords. |
| 30 (rayon noyau) | 30 px | Rayon du dégradé radial central (createRadialGradient(cx,cy,0,cx,cy,30)). Augmentez à 60–80 pour un noyau très lumineux de type quasar ; réduisez à 10 pour un point central discret. |
| 1.5*Math.random()+.5 (taille étoiles) | 0,5–2 px | Plage de rayon des étoiles. Changez en 2.5*Math.random()+1 pour des étoiles plus visibles sur grand écran, ou Math.random()+0.3 pour des points minuscules et plus réalistes. |
FAQ
fillRect(0,0,w,h) avec fillStyle="rgba(10,10,15,0.08)" recouvre progressivement les positions précédentes — technique dite alpha compositing trail. À alpha 0,08, une position met ~12 frames (~200 ms à 60 fps) avant de disparaître. Augmentez cet alpha pour réduire les traînées, diminuez-le pour les allonger.const n = a % 3 en const n = a % 4 (ou % 5). Mettez à jour la formule d'angle : n / 3 * Math.PI * 2 devient n / 4 * Math.PI * 2. Ajustez aussi le twist en augmentant le diviseur (ex. dist/120) pour éviter que les bras supplémentaires se chevauchent trop près du centre.prefers-reduced-motion. Pour corriger côté intégration : wrappez l'initialisation dans if (!window.matchMedia('(prefers-reduced-motion:reduce)').matches) { /* init */ }, et masquez le canvas via @media (prefers-reduced-motion:reduce){.ps-canvas{display:none}} en CSS.