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éneauSang liquide en canvas : des gouttes tombent, fusionnent et remplissent une flaque au fond ; votre curseur ou votre doigt porte un glob qui les attire.
Mis à jour le
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 →
Escape game · salle 4 · 16 ans et +
60 minutes pour sortir de la cave du docteur Vorn. De 2 à 6 joueurs, tous les soirs jusqu'à 1 h.
Réserver un créneauVELMARO
SANGUEVRY
Samedi 31 octobre · de 21 h à 6 h · 6 films
21 hLa Maison des marais
23 hLa Cale noyée
1 hLa Douzième Cloche
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).
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.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.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.data-color) ou annoncez le thème.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.
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.
Copiez les trois blocs dans votre page. Aucune dépendance.
<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>
.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;
}
(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();
});
})();
})();
Options passées à l'API ou attributs data-* :
| Option / propriété | Défaut | Effet |
|---|---|---|
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. |
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).
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.
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.