From a9d525f9381e5c21516f30f3dbfcacbeab028d49 Mon Sep 17 00:00:00 2001 From: Alberto Moretti Date: Thu, 2 Jul 2026 16:02:23 +0200 Subject: [PATCH] fix fiamme --- app.js | 132 +++++++++++++++++++++++++++++++++++++++++++++++++++++ styles.css | 92 +++++++++++++++++++++++++++---------- 2 files changed, 201 insertions(+), 23 deletions(-) diff --git a/app.js b/app.js index 180c686..f121a9c 100644 --- a/app.js +++ b/app.js @@ -399,6 +399,135 @@ }); } + /* ---------- Fuoco a particelle (canvas 2D additivo) ---------- + Un vero falò: particelle emesse dal bordo inferiore che salgono, si + allargano e sfumano, colorate dal nucleo bianco-giallo alla brace rossa. + Blending additivo ("lighter") + sprite pre-renderizzati = fluido e realistico. */ + const Fire = { + canvas: null, + ctx: null, + parts: [], + sprites: null, + raf: 0, + running: false, + w: 0, + h: 0, + last: 0, + acc: 0, + // palette giovane -> vecchio (nucleo caldo verso brace scura) + PALETTE: [ + [255, 255, 235], + [255, 244, 170], + [255, 200, 70], + [255, 130, 20], + [225, 65, 15], + [130, 35, 18], + ], + buildSprites() { + this.sprites = this.PALETTE.map(([r, g, b]) => { + const s = document.createElement("canvas"); + s.width = s.height = 64; + const c = s.getContext("2d"); + const grad = c.createRadialGradient(32, 32, 0, 32, 32, 32); + grad.addColorStop(0, `rgba(${r},${g},${b},1)`); + grad.addColorStop(0.32, `rgba(${r},${g},${b},0.55)`); + grad.addColorStop(1, `rgba(${r},${g},${b},0)`); + c.fillStyle = grad; + c.fillRect(0, 0, 64, 64); + return s; + }); + }, + ensure() { + if (!this.canvas) { + this.canvas = document.querySelector(".shalom-fire"); + this.ctx = this.canvas.getContext("2d"); + } + if (!this.sprites) this.buildSprites(); + }, + resize() { + this.w = this.canvas.width = window.innerWidth; + this.h = this.canvas.height = window.innerHeight; + }, + spawn(n) { + for (let i = 0; i < n; i++) { + // più denso al centro (sotto il leone): due gaussiane sommate + const bias = (Math.random() + Math.random()) / 2; + const x = bias * this.w; + const life = 700 + Math.random() * 1200; + this.parts.push({ + x, + y: this.h + 12, + vx: (Math.random() - 0.5) * 36, + vy: -(190 + Math.random() * 240), + life, + max: life, + size: 15 + Math.random() * 30, + wob: Math.random() * 6.283, + wobSpeed: 4 + Math.random() * 6, + }); + } + }, + start() { + this.ensure(); + this.resize(); + this.parts.length = 0; + this.last = 0; + this.acc = 0; + this.running = true; + const loop = (t) => { + if (!this.running) return; + if (!this.last) this.last = t; + let dt = (t - this.last) / 1000; + this.last = t; + if (dt > 0.05) dt = 0.05; // niente salti dopo un lag + this.acc += dt * 900; // ~900 particelle/sec + const n = Math.floor(this.acc); + this.acc -= n; + this.spawn(n); + const ctx = this.ctx; + ctx.clearRect(0, 0, this.w, this.h); + ctx.globalCompositeOperation = "lighter"; + for (let i = this.parts.length - 1; i >= 0; i--) { + const p = this.parts[i]; + p.life -= dt * 1000; + if (p.life <= 0) { + this.parts.splice(i, 1); + continue; + } + const k = p.life / p.max; // 1 -> 0 + p.vy -= 70 * dt; // il calore accelera verso l'alto + p.wob += dt * p.wobSpeed; + p.x += (p.vx + Math.sin(p.wob) * 34) * dt; + p.y += p.vy * dt; + const age = 1 - k; // 0 -> 1 + const idx = Math.min( + this.sprites.length - 1, + (age * this.sprites.length) | 0, + ); + const size = p.size * (0.5 + k * 0.85); + const hs = size * (1.5 + (1 - k) * 1.4); // stiro verticale -> lingua di fuoco + ctx.globalAlpha = Math.min(1, k * 1.6) * 0.28; + ctx.drawImage( + this.sprites[idx], + p.x - size, + p.y - hs * 1.4, + size * 2, + hs * 2, + ); + } + ctx.globalAlpha = 1; + this.raf = requestAnimationFrame(loop); + }; + this.raf = requestAnimationFrame(loop); + }, + stop() { + this.running = false; + cancelAnimationFrame(this.raf); + if (this.ctx) this.ctx.clearRect(0, 0, this.w, this.h); + this.parts.length = 0; + }, + }; + /* ---------- Effetto SHALOM (vittoria stanze con idromassaggio) ---------- */ let shalomOverlay = null; function ensureShalomOverlay() { @@ -408,6 +537,7 @@ o.innerHTML = '
' + '
' + + '' + '' + '
' + '' + @@ -426,6 +556,7 @@ try { v.pause(); } catch (e) {} + Fire.stop(); o.classList.remove("on", "mode-fire", "mode-video"); } // Attende: fine video, tetto di tempo, oppure tap sull'overlay (skip). @@ -450,6 +581,7 @@ async function shalomFireLion() { const o = ensureShalomOverlay(); showShalom(o, "mode-fire"); + Fire.start(); screenShake(); const stop = Samples.play("leone", { vol: 1.0, diff --git a/styles.css b/styles.css index 57247ad..dbbbf75 100644 --- a/styles.css +++ b/styles.css @@ -760,50 +760,96 @@ body { color: rgba(255, 255, 255, 0.55); } -/* fiammate dai due angoli in basso */ +/* canvas del fuoco a particelle (falò vero, disegnato in JS) */ +.shalom-fire { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + z-index: 1; /* dietro al leone (z-index 2) */ + opacity: 0; + pointer-events: none; +} +.shalom-overlay.mode-fire .shalom-fire { + opacity: 1; +} + +/* fuoco che lambisce il bordo inferiore: lingue di fiamma animate. + Ora fanno solo da base morbida sotto le particelle canvas. */ .shalom-flames { position: absolute; - bottom: -6%; - width: 50vw; - height: 92vh; + bottom: 0; + height: 40vh; + width: 52vw; pointer-events: none; opacity: 0; - filter: blur(2px); mix-blend-mode: screen; - background: - radial-gradient(55% 42% at 50% 100%, rgba(255, 235, 140, 0.95), transparent 70%), - linear-gradient(0deg, #ff2d00 0%, #ff7a00 26%, #ffd23f 48%, rgba(255, 210, 63, 0) 74%); + z-index: 1; } .shalom-flames.left { - left: -8%; - transform-origin: bottom left; + left: 0; } .shalom-flames.right { - right: -8%; - transform-origin: bottom right; + right: 0; + transform: scaleX(-1); /* specchia così le due metà non sono identiche */ +} +.shalom-flames::before, +.shalom-flames::after { + content: ""; + position: absolute; + inset: 0; + transform-origin: bottom center; + background-repeat: no-repeat; + background-position: bottom; + filter: blur(5px) contrast(1.5) brightness(1.08); + -webkit-mask: linear-gradient(0deg, #000 20%, rgba(0, 0, 0, 0.55) 55%, transparent 88%); + mask: linear-gradient(0deg, #000 20%, rgba(0, 0, 0, 0.55) 55%, transparent 88%); +} +.shalom-flames::before { + background-image: + radial-gradient(22% 58% at 18% 100%, rgba(255, 244, 190, 0.95), rgba(255, 120, 0, 0.55) 46%, transparent 72%), + radial-gradient(26% 74% at 42% 100%, rgba(255, 224, 130, 0.9), rgba(255, 80, 0, 0.5) 50%, transparent 74%), + radial-gradient(20% 60% at 66% 100%, rgba(255, 240, 170, 0.92), rgba(255, 100, 0, 0.52) 46%, transparent 72%), + radial-gradient(24% 76% at 86% 100%, rgba(255, 214, 110, 0.9), rgba(255, 70, 0, 0.5) 50%, transparent 74%); +} +.shalom-flames::after { + background-image: + radial-gradient(17% 70% at 30% 100%, rgba(255, 250, 210, 0.95), rgba(255, 140, 0, 0.55) 44%, transparent 70%), + radial-gradient(21% 84% at 54% 100%, rgba(255, 232, 150, 0.9), rgba(255, 90, 0, 0.5) 48%, transparent 72%), + radial-gradient(17% 66% at 78% 100%, rgba(255, 244, 180, 0.92), rgba(255, 110, 0, 0.52) 44%, transparent 70%); } .shalom-overlay.mode-fire .shalom-flames { - opacity: 1; - animation: flames 0.26s infinite alternate; + opacity: 0.5; /* base morbida: le particelle canvas fanno il grosso */ } -.shalom-overlay.mode-fire .shalom-flames.right { - animation-delay: 0.13s; +.shalom-overlay.mode-fire .shalom-flames::before { + animation: flameA 0.42s infinite alternate; } -@keyframes flames { - 0% { transform: scaleY(1) scaleX(1) skewX(0deg); } - 100% { transform: scaleY(1.14) scaleX(0.92) skewX(4deg); } +.shalom-overlay.mode-fire .shalom-flames::after { + animation: flameB 0.31s infinite alternate; +} +@keyframes flameA { + 0% { transform: scaleY(0.94) scaleX(1.02) translateY(0); opacity: 0.85; } + 100% { transform: scaleY(1.2) scaleX(0.97) translateY(-3%); opacity: 1; } +} +@keyframes flameB { + 0% { transform: scaleY(1.14) translateX(-1.5%); opacity: 0.7; } + 100% { transform: scaleY(0.9) translateX(2%); opacity: 1; } } /* leone che irrompe */ .shalom-lion { position: absolute; - top: 50%; + top: 48%; left: 50%; - width: min(46vw, 520px); - border-radius: 16px; + width: min(52vw, 580px); + z-index: 2; + /* sfuma i bordi dell'immagine così il fuoco si fonde col nero invece di + mostrare un riquadro rettangolare netto */ + -webkit-mask: radial-gradient(ellipse 66% 66% at 50% 45%, #000 44%, transparent 74%); + mask: radial-gradient(ellipse 66% 66% at 50% 45%, #000 44%, transparent 74%); transform: translate(-50%, -50%) scale(0.2); opacity: 0; - filter: drop-shadow(0 0 40px #ff6a00); + filter: drop-shadow(0 0 55px #ff6a00); } .shalom-overlay.mode-fire .shalom-lion { animation: lionSlam 0.6s cubic-bezier(0.2, 1.5, 0.4, 1) forwards,