DistorsionGratuit

Cursed Blood Goo

Sang liquide en canvas : des gouttes tombent, fusionnent et remplissent une flaque au fond ; votre curseur ou votre doigt porte un glob qui les attire.

CanvasMetaballsLiquid

Mis à jour le

Survolez ou cliquez la scène pour interagir

Cet effet est gratuit — la catégorie Distorsion en contient 31 au total, dont 5 gratuits. Effect.Labs regroupe 840 effets vanilla. Découvrir la catégorie →

Exemples d'usage

Escape game · salle 4 · 16 ans et +

La Cave qui saigne

60 minutes pour sortir de la cave du docteur Vorn. De 2 à 6 joueurs, tous les soirs jusqu'à 1 h.

Réserver un créneau

① Haut de page d'un escape game d'horreur — le sang en fond suit le curseur

QuandLe site de l'escape game Moricaille présente la salle « La Cave qui saigne » : le sang coule derrière le titre, la flaque monte au bas de la page et le glob suit la souris du visiteur.
PourquoiL'ambiance est posée avant la première ligne de texte, et le visiteur « touche » la salle avec sa souris. Le texte reste lisible car le sang passe derrière.
RéglagesScène étirée à toute la zone (aspect-ratio: auto; height: 100%, coins carrés, fond noir), phrase d'aide masquée ; texte posé au-dessus avec pointer-events: none pour laisser passer le pointeur, sauf le bouton « Réserver un créneau ». Couleur par défaut.

VELMARO

Slime qui s'étire, pot de 250 g

Dès 6 ans · lavable · sans paillettes

6,90 €

② Slime à essayer sur la fiche d'un magasin de jouets

QuandSur la fiche du slime Velmaro, la matière gigote dans un bac ; l'enfant (ou le parent) la touche au doigt ou à la souris et choisit sa couleur avec trois pastilles : vert fluo, rose fluo, glacier.
PourquoiChanger la couleur en direct montre le produit dans toutes ses versions sans trois photos ; le glob qui aspire les gouttes donne envie de toucher.
RéglagesBac de 54 % de la largeur ; data-color="#00ff00" au départ ; les pastilles écrivent #ff00ff ou #00ffff dans data-color, que le code relit à chaque image. Couleurs pures choisies car le filtre pousse chaque canal à 0 ou 255. Phrase d'aide remplacée par « Touche le slime… ».

La nuit du film d'horreur

Samedi 31 octobre · de 21 h à 6 h · 6 films

PROGRAMME DE LA NUIT6 films

21 hLa Maison des marais

23 hLa Cale noyée

1 hLa Douzième Cloche

③ Séparateur qui dégouline entre deux sections d'une page d'événement

QuandSur la page de la nuit Sanguevry, une bande de sang sépare l'annonce (fond noir) du programme : les gouttes tombent, la flaque se forme au bas de la bande et se fond dans le bandeau rouge « Programme de la nuit ».
PourquoiLa transition entre deux sections devient le motif de la page ; la flaque qui monte relie visuellement les deux blocs.
RéglagesBande de 26 % de la hauteur (32 % sur mobile), coins carrés, fond noir, phrase d'aide masquée ; le bandeau suivant est en #ff0000, la couleur que le filtre donne réellement à #c81e2d, et remonte de 7 px sur la bande pour couvrir le flou du bord.

Comment ça marche

La scène .goo-scene occupe toute la largeur au format 16/10. Son canvas dessine des disques pleins (gouttes, flaque, glob) puis le CSS lui applique filter: blur(7px) contrast(16) : le flou fait déborder les formes les unes sur les autres, le contraste très fort redessine un bord net. Deux gouttes proches se soudent alors comme du liquide. Ce contraste pousse aussi chaque canal de couleur vers 0 ou 255 : le rouge #c81e2d par défaut s'affiche en rouge pur.

Toutes les 0,18 s, une goutte de 9 à 18 px de rayon naît en haut à un endroit tiré au hasard et tombe en accélérant. En touchant la flaque, elle disparaît et la fait monter ; la flaque s'écoule doucement en même temps. Elle grandit donc vite puis se stabilise : mesuré sur une scène de 800 px de large, environ 14 % de la hauteur au bout de 3 s, 20 % à 6 s, 23 à 25 % à partir de 10 s (plafond : la moitié). Sa surface ondule en continu.

Le pointeur, souris ou doigt, porte un gros glob de 34 px de rayon qui le suit avec un léger retard. Il attire les gouttes situées à moins de 150 px, d'autant plus fort qu'elles sont proches ; s'il va vite, il projette de nouvelles gouttelettes dans sa direction ; elles retombent dans la flaque, qui monte alors plus haut (mesuré : près de la moitié de la hauteur après 10 s de mouvement continu au doigt). La scène compte au plus 130 gouttes. Les vitesses sont calculées sur le temps écoulé (borné à 50 ms) : même rythme à 60, 120 ou 144 Hz.

Le canvas suit la taille de la scène (ResizeObserver) à la densité de l'écran, plafonnée à 2. La boucle s'arrête quand la scène sort de l'écran ou que l'onglet est caché. Chaque [data-goo] a sa propre simulation : deux copies sur une page sont indépendantes (mesuré dans Chromium 151, Firefox 153 et WebKit 26.5).

Accessibilité

  • Mouvement réduit : sous prefers-reduced-motion: reduce, plus aucune goutte ne tombe d'elle-même et la flaque ne bouge plus tant que personne ne touche la scène ; la scène s'ouvre sur une flaque et neuf gouttes immobiles. Le geste reste actif : quand vous passez le pointeur ou le doigt, le glob apparaît, attire et projette les gouttes, qui retombent dans la flaque et la font monter (mesuré : de 22 % à 43 % de la hauteur après 10 s de mouvement au doigt) ; tout s'arrête 1,5 s après votre dernier mouvement.
  • Toucher : touch-action: none sur la scène laisse le doigt piloter le glob au lieu de faire défiler la page (mesuré : 12 déplacements reçus, aucune annulation, le glob sous le doigt). La page se fait défiler normalement en dehors de la scène ; sur mobile, laissez de la marge autour si la scène est grande.
  • Clavier : il n'existe pas d'équivalent clavier au glob. L'effet est décoratif : rien d'important ne doit dépendre du geste.
  • Lecteurs d'écran : le canvas et la phrase d'aide portent aria-hidden="true" ; la scène est muette. Si le sang est un élément de sens (page 404 « qui saigne »), dites-le dans le texte voisin.
  • Contenu sensible : du sang animé peut heurter. Sur une page grand public, préférez une autre couleur (data-color) ou annoncez le thème.

Compatibilité navigateur

Canvas 2D, filtre CSS blur() + contrast(), aspect-ratio, Pointer Events et 6,6 Ko de JavaScript. Version minimale fixée par aspect-ratio d'après les données MDN (Chrome 88, Firefox 89, Safari 15). Mesuré dans Chromium 151, Firefox 153 et WebKit 26.5 ; le doigt est mesuré dans Chromium en mode tactile.

Chrome 88+✓ Complet (mesuré)
Firefox 89+✓ Complet (mesuré)
Safari 15+✓ Complet (mesuré dans WebKit)
Edge 88+✓ Complet (moteur Chromium)
Mobile iOS 15+✓ Non vérifié sur un vrai iPhone
Android Chrome 88+✓ Complet (doigt mesuré dans Chromium)

Sans aspect-ratio, la scène n'a plus de hauteur : donnez-lui une hauteur fixe (height: 320px). Sans JavaScript, la scène reste un rectangle sombre.

Le code

Copiez les trois blocs dans votre page. Aucune dépendance.

HTML
<div class="fx-0803-root">
  <div class="goo-scene" data-goo data-color="#c81e2d">
    <canvas class="goo-canvas" aria-hidden="true"></canvas>
    <div class="goo-hint" aria-hidden="true">Balaye le sang…</div>
  </div>
</div>
CSS
.fx-0803-root,
.fx-0803-root *,
.fx-0803-root *::before,
.fx-0803-root *::after {
  box-sizing: border-box
}

.fx-0803-root {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 200px;
  position: relative;
  background: rgb(10, 10, 15);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

.goo-scene {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 10;
  border-radius: 14px;
  overflow: hidden;
  isolation: isolate;
  background: #0b0406;
  cursor: crosshair;
  touch-action: none;
}

.goo-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  filter: blur(7px) contrast(16);
}

.goo-hint {
  position: absolute;
  top: 12px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 2;
  font-size: 12px;
  color: #ff5a5a;
  opacity: .85;
  white-space: nowrap;
  pointer-events: none;
}
JavaScript (fx-0803)
(function() {
  /* ============================================================
     JS scopé (IIFE) — Cursed Blood Goo (fx-0803)
     Metaballs de sang sur chaque [data-goo]
  ============================================================ */
  (function() {
    var reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
    var MAX = 130;

    document.querySelectorAll('.fx-0803-root [data-goo]').forEach(function(scene) {
      var canvas = scene.querySelector('.goo-canvas');
      var ctx = canvas.getContext('2d');
      var dpr = Math.min(window.devicePixelRatio || 1, 2);
      var W = 0, H = 0, drops = [], pool = 0, t = 0, drip = 0;
      var mx = -999, my = -999, on = false, bx = -999, by = -999, pmx = -999, pmy = -999;
      var visible = true, raf = 0, last = 0, awake = 0;

      function blob(x, y, r) {
        ctx.beginPath();
        ctx.arc(x, y, r, 0, 6.2832);
        ctx.fill();
      }

      function render() {
        if (!W || !H) return;
        ctx.fillStyle = '#0b0406';
        ctx.fillRect(0, 0, W, H);
        ctx.fillStyle = scene.dataset.color || '#c81e2d';
        // flaque (surface ondulée)
        var surf = H - pool;
        ctx.beginPath();
        ctx.moveTo(0, H + 2);
        ctx.lineTo(0, surf);
        for (var x = 0; x <= W + 18 * dpr; x += 18 * dpr) ctx.lineTo(x, surf + Math.sin(x * 0.012 + t * 3) * 6 * dpr);
        ctx.lineTo(W, H + 2);
        ctx.closePath();
        ctx.fill();
        // gouttes
        for (var i = 0; i < drops.length; i++) blob(drops[i].x, drops[i].y, drops[i].r);
        // glob du pointeur
        if (on) blob(bx, by, 34 * dpr);
      }

      function resize() {
        var w = Math.round(scene.clientWidth * dpr);
        var h = Math.round(scene.clientHeight * dpr);
        if (!w || !h || (w === W && h === H)) return;
        if (H) pool = pool * h / H;
        else if (reduce.matches) {
          // mouvement réduit : flaque et gouttes posées d'emblée, rien ne bouge sans le pointeur
          pool = h * 0.22;
          for (var i = 0; i < 9; i++) drops.push({
            x: (0.12 + Math.random() * 0.76) * w,
            y: (0.2 + Math.random() * 0.45) * h,
            vx: 0,
            vy: 0,
            r: (12 + Math.random() * 14) * dpr
          });
        }
        W = canvas.width = w;
        H = canvas.height = h;
        render();
      }

      // f = durée de l'image en images à 60 i/s (bornée à 50 ms) : même vitesse à 60, 120 ou 144 Hz
      function step(f) {
        t += f / 60;
        // dégoulinade depuis le haut (une goutte toutes les 11/60 s), coupée en mouvement réduit
        if (!reduce.matches) {
          drip += f;
          while (drip >= 11) {
            drip -= 11;
            if (drops.length < MAX) drops.push({
              x: Math.random() * W,
              y: -12 * dpr,
              vx: (Math.random() - 0.5) * 0.5,
              vy: 1 + Math.random() * 1.4,
              r: (9 + Math.random() * 9) * dpr
            });
          }
        }
        var floorY = H - pool, damp = Math.pow(0.99, f);
        for (var i = drops.length - 1; i >= 0; i--) {
          var d = drops[i];
          d.vy += 0.28 * dpr * f;
          d.vx *= damp;
          if (on) {
            var ax = mx - d.x,
              ay = my - d.y,
              dist = Math.hypot(ax, ay);
            if (dist < 150 * dpr && dist > 1) {
              var k = (1 - dist / (150 * dpr)) * 0.7 * f;
              d.vx += ax / dist * k;
              d.vy += ay / dist * k;
            }
          }
          d.x += d.vx * f;
          d.y += d.vy * f;
          if (d.y + d.r * 0.4 >= floorY) {
            // chaque goutte nourrit la flaque (part de la hauteur, quelle que soit la largeur)
            pool = Math.min(H * 0.5, pool + H * 0.012 * d.r * d.r / (169 * dpr * dpr));
            drops.splice(i, 1);
            continue;
          }
          if (d.x < -40 * dpr || d.x > W + 40 * dpr) drops.splice(i, 1);
        }
        // la flaque s'écoule doucement : elle monte jusqu'à un équilibre (≈ 20 % de la hauteur)
        pool -= pool * 0.3 * f / 60;
        if (on) {
          var e = 1 - Math.pow(0.7, f);
          bx += (mx - bx) * e;
          by += (my - by) * e;
          var sp = Math.hypot(mx - pmx, my - pmy) / f;
          if (sp > 13 * dpr && drops.length < MAX) drops.push({
            x: bx + (Math.random() - 0.5) * 22 * dpr,
            y: by + (Math.random() - 0.5) * 22 * dpr,
            vx: (mx - pmx) / f * 0.07,
            vy: (my - pmy) / f * 0.07 + 1,
            r: (6 + Math.random() * 6) * dpr
          });
        }
        pmx = mx;
        pmy = my;
      }

      function frame(now) {
        raf = 0;
        var f = last ? Math.min(now - last, 50) * 0.06 : 1;
        last = now;
        step(f);
        render();
        sync();
      }

      // boucle en pause hors écran / onglet caché ; en mouvement réduit, elle ne tourne que 1,5 s après un geste
      function sync() {
        var run = visible && !document.hidden && W > 0 && (!reduce.matches || performance.now() < awake);
        if (run && !raf) raf = requestAnimationFrame(frame);
        if (!run) {
          if (raf) cancelAnimationFrame(raf);
          raf = 0;
          last = 0;
        }
      }

      function point(e) {
        var r = scene.getBoundingClientRect();
        mx = (e.clientX - r.left) * dpr;
        my = (e.clientY - r.top) * dpr;
        if (!on) {
          bx = pmx = mx;
          by = pmy = my;
        }
        on = true;
        awake = performance.now() + 1500;
        sync();
      }

      function leave() {
        on = false;
        mx = my = -999;
      }

      scene.addEventListener('pointerdown', point);
      scene.addEventListener('pointermove', point);
      scene.addEventListener('pointerleave', leave);
      scene.addEventListener('pointercancel', leave);

      resize();
      if ('ResizeObserver' in window) new ResizeObserver(function() {
        resize();
        sync();
      }).observe(scene);
      else window.addEventListener('resize', resize);
      if ('IntersectionObserver' in window) {
        new IntersectionObserver(function(entries) {
          visible = entries[0].isIntersecting;
          sync();
        }).observe(scene);
      }
      if ('MutationObserver' in window) new MutationObserver(render).observe(scene, {
        attributes: true,
        attributeFilter: ['data-color']
      });
      document.addEventListener('visibilitychange', sync);
      if (reduce.addEventListener) reduce.addEventListener('change', sync);
      sync();
    });
  })();
})();

Personnaliser

Options passées à l'API ou attributs data-* :

Option / propriétéDéfautEffet
data-color (sur .goo-scene) #c81e2d (affiché rouge pur) Couleur du liquide, relue à chaque image : on peut la changer en direct. Le filtre pousse chaque canal à 0 ou 255 : préférez des couleurs pures (#00ff00, #ff00ff, #00ffff, #ffff00).
Format (CSS — aspect-ratio de .goo-scene) 16 / 10 Remplacez par aspect-ratio: auto; height: 100% pour remplir un conteneur, ou une hauteur fixe pour une bande.
Fusion des gouttes (CSS — filter de .goo-canvas) blur(7px) contrast(16) Plus de flou (10–12 px) = gouttes plus molles qui se soudent de plus loin ; moins (4 px) = gouttes plus nettes et séparées.
Cadence des gouttes (JS — drip >= 11) Une goutte toutes les 11/60 s 6 pour une pluie serrée, 20 pour un goutte-à-goutte. La flaque monte d'autant plus haut que les gouttes sont nombreuses.
Hauteur de la flaque (JS — H * 0.012 et pool * 0.3) ≈ 25 % de la hauteur Le premier chiffre est l'apport de chaque goutte, le second la vitesse d'écoulement. Doublez l'apport pour une flaque deux fois plus haute (plafond : la moitié de la scène).
Attraction (JS — 150 * dpr et 0.7) Rayon 150 px, force 0,7 Rayon d'action du glob et force de l'aspiration. Mettez la force à 0 pour un glob qui ne fait que projeter.
Phrase d'aide (HTML — .goo-hint) « Balaye le sang… » Remplacez le texte ou supprimez la balise.

FAQ

Pourquoi ma couleur ne s'affiche-t-elle pas telle quelle ?

Le contrast(16) qui soude les gouttes multiplie les écarts de chaque canal par 16 : tout ce qui est au-dessus de la moitié part à 255, le reste à 0. Un orange #ff8000 sort à peu près orange, mais un brun ou un pastel deviennent rouge, jaune ou blanc. Choisissez une couleur pure, ou baissez le contraste (le bord des gouttes devient alors flou).

La page peut-elle encore défiler sur mobile ?

Oui, en dehors de la scène. Sur la scène elle-même, le doigt pilote le glob (touch-action: none) : c'est voulu, sinon le navigateur reprend le geste pour défiler et le glob ne suit pas. Pour une scène plein écran sur mobile, laissez une marge ou réduisez sa hauteur.

Peut-on vider la flaque ?

Elle s'écoule en permanence et ne tient que grâce aux gouttes qui tombent : augmentez le 11 de drip >= 11 (moins de gouttes) pour une flaque plus basse, ou mettez une très grande valeur pour qu'elle se vide. En mouvement réduit, elle ne change que pendant vos gestes. Pour une flaque permanente plus haute, augmentez l'apport H * 0.012.