Spaces:
Sleeping
Sleeping
| // TinyWorld — stage life: staggered bubbles, typewriter, shockwave, wander + juice. | |
| (function () { | |
| var BUBBLE_LIFETIME_MS = 8000; | |
| var STAGGER_DELAY_MS = 120; | |
| function animateBubbles() { | |
| var bubbles = document.querySelectorAll('.bubble:not([data-animated])'); | |
| var i = 0; | |
| bubbles.forEach(function (b) { | |
| b.dataset.animated = '1'; | |
| b.dataset.spawned = Date.now(); | |
| b.style.animationDelay = (i * STAGGER_DELAY_MS) + 'ms'; | |
| setTimeout(function () { typewriter(b); }, i * STAGGER_DELAY_MS + 120); | |
| i++; | |
| }); | |
| // Juice: trigger roster flash + mood pop for each speaking pawn | |
| setTimeout(function () { | |
| document.querySelectorAll('.pawn.speaking').forEach(function (p) { | |
| var moodEl = p.querySelector('.pawn-mood'); | |
| if (moodEl) { moodEl.classList.remove('pop'); void moodEl.offsetWidth; moodEl.classList.add('pop'); } | |
| }); | |
| document.querySelectorAll('.roster-card').forEach(function (c) { | |
| c.classList.remove('speaking-flash'); void c.offsetWidth; c.classList.add('speaking-flash'); | |
| }); | |
| }, 200); | |
| } | |
| function typewriter(bubble) { | |
| var node = bubble.firstChild; | |
| if (!node || node.nodeType !== 3) return; | |
| var full = node.textContent; | |
| if (full.length < 4) return; | |
| node.textContent = ''; | |
| var k = 0; | |
| var speed = Math.max(9, Math.min(22, 520 / full.length)); | |
| (function step() { | |
| if (k < full.length) { node.textContent += full[k++]; setTimeout(step, speed); } | |
| })(); | |
| } | |
| function dismissOld() { | |
| document.querySelectorAll('.bubble[data-spawned]').forEach(function (b) { | |
| if (Date.now() - parseInt(b.dataset.spawned, 10) > BUBBLE_LIFETIME_MS) { | |
| b.style.transition = 'opacity .4s ease, transform .4s ease'; | |
| b.style.opacity = '0'; | |
| b.style.transform = 'translateX(-50%) translateY(8px) scale(.9)'; | |
| setTimeout(function () { if (b.parentNode) b.remove(); }, 400); | |
| } | |
| }); | |
| } | |
| setInterval(dismissOld, 1000); | |
| function fireShockwave() { | |
| var sw = document.querySelector('.shockwave'); | |
| if (!sw) return; | |
| sw.classList.remove('fire'); void sw.offsetWidth; sw.classList.add('fire'); | |
| setTimeout(function () { sw.classList.remove('fire'); }, 900); | |
| } | |
| // Juice: screen shake on event throw | |
| function fireScreenShake() { | |
| var stage = document.querySelector('.stage'); | |
| if (!stage) return; | |
| stage.classList.remove('shake'); void stage.offsetWidth; stage.classList.add('shake'); | |
| setTimeout(function () { stage.classList.remove('shake'); }, 550); | |
| } | |
| // Juice: button pulse on throw | |
| function fireButtonPulse() { | |
| var btn = document.querySelector('#throw-btn') || document.querySelector('button.primary'); | |
| if (!btn) return; | |
| btn.classList.remove('pulse'); void btn.offsetWidth; btn.classList.add('pulse'); | |
| setTimeout(function () { btn.classList.remove('pulse'); }, 2500); | |
| } | |
| // Juice: meter tick glow when vibe-fill width changes | |
| function watchMeters() { | |
| var fills = document.querySelectorAll('.vibe-fill'); | |
| fills.forEach(function (f) { | |
| if (f.dataset.watched) return; | |
| f.dataset.watched = '1'; | |
| var lastW = f.style.width; | |
| var m = new MutationObserver(function () { | |
| if (f.style.width !== lastW) { | |
| lastW = f.style.width; | |
| f.classList.remove('tick'); void f.offsetWidth; f.classList.add('tick'); | |
| } | |
| }); | |
| m.observe(f, { attributes: true, attributeFilter: ['style'] }); | |
| }); | |
| } | |
| setInterval(watchMeters, 500); | |
| // Juice: day/night tint drift | |
| function startTintDrift() { | |
| var tint = document.querySelector('.stage-tint'); | |
| if (tint && !tint.classList.contains('drift')) tint.classList.add('drift'); | |
| } | |
| // gentle idle wander so the town feels alive between events | |
| function wander() { | |
| document.querySelectorAll('.pawn').forEach(function (p) { | |
| if (p.classList.contains('speaking')) return; | |
| var dx = (Math.random() * 2 - 1) * 0.7; | |
| var dy = (Math.random() * 2 - 1) * 0.4; | |
| var t = p.querySelector('.pawn-token'); | |
| if (t) t.style.transform = 'translate(' + dx.toFixed(2) + 'px,' + dy.toFixed(2) + 'px)'; | |
| }); | |
| } | |
| setInterval(wander, 2600); | |
| // Juice: track mouse on buttons for radial highlight | |
| document.addEventListener('mousemove', function (e) { | |
| var btn = e.target.closest('button.primary, .gr-button-primary, #throw-btn'); | |
| if (!btn) return; | |
| var rect = btn.getBoundingClientRect(); | |
| btn.style.setProperty('--mx', ((e.clientX - rect.left) / rect.width * 100) + '%'); | |
| btn.style.setProperty('--my', ((e.clientY - rect.top) / rect.height * 100) + '%'); | |
| }); | |
| var observer = new MutationObserver(function (muts) { | |
| var fresh = false; | |
| muts.forEach(function (m) { | |
| m.addedNodes && m.addedNodes.forEach(function (n) { | |
| if (n.nodeType === 1 && (n.querySelector ? (n.querySelector('.bubble') || n.classList.contains('bubble')) : false)) fresh = true; | |
| }); | |
| }); | |
| if (fresh) { | |
| setTimeout(function () { | |
| animateBubbles(); | |
| fireShockwave(); | |
| fireScreenShake(); | |
| fireButtonPulse(); | |
| startTintDrift(); | |
| }, 40); | |
| } | |
| }); | |
| function watch() { | |
| var root = document.querySelector('.gradio-container') || document.body; | |
| if (root) { observer.observe(root, { childList: true, subtree: true }); animateBubbles(); startTintDrift(); } | |
| else setTimeout(watch, 300); | |
| } | |
| if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', watch); | |
| else watch(); | |
| })(); | |