openvibers/openvibe.live Commits

Lite mode for phones / weak GPUs: no shader, canvases, blur filters, blend modes, backdrop-filter or animated shadows (flicker on low-end Android); CSS safety net ≤ 820px

OP OpenVibers committed 11 Sept 2026 · de13bd19bb · 6 files, +75 −12

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMxJm1dB8rdsaMSgNjVa82

Changes

ddiff --git a/public/css/home-fx.css b/public/css/home-fx.css
iindex 815586bf..19279ecd 100644
---- a/public/css/home-fx.css
++++ b/public/css/home-fx.css
@@@ -37,3 +37,7 @@
.hfx-shimmer, .hfx-glow, #page-home .hero-cta .btn-primary, #page-home .section-header h2 .live-dot { animation: none !important; }
.hfx-reveal { opacity: 1; transform: none; transition: none; }
}
++
++.rs-lite .hfx-sparks, .rs-lite .hfx-spot { display: none; }
++.rs-lite .hfx-shimmer, .rs-lite .hfx-glow, .rs-lite #page-home .hero-cta .btn-primary, .rs-lite #page-home .section-header h2 .live-dot { animation: none !important; text-shadow: none; }
++.rs-lite #page-home .stream-card { transition: none; }
ddiff --git a/public/css/i18n-star.css b/public/css/i18n-star.css
iindex f370761c..ac6938a0 100644
---- a/public/css/i18n-star.css
++++ b/public/css/i18n-star.css
@@@ -210,3 +210,11 @@
.chat-msg-hoverable:hover .chat-translate-btn { display: inline-flex; }
.chat-msg-hoverable .chat-translate-btn:hover { color: var(--accent-light); border-color: var(--accent); }
.chat-translation[hidden] { display: none; }
++
++/* Lite mode (phones / weak GPUs): no blur, no filters, no shadow animations */
++.rs-lite .star-card-border::before { animation: none; background: linear-gradient(135deg, #fbbf24, #f472b6, var(--accent)); inset: 0; }
++.rs-lite .star-avatar-glow, .rs-lite .star-petals { display: none; }
++.rs-lite .star-avatar-ring { animation: none; }
++.rs-lite .star-btn-watch::after, .rs-lite .star-kicker i, .rs-lite .star-live-pill { animation: none; }
++.rs-lite .star-btn-watch { box-shadow: none; }
++.rs-lite .ch-captions-dot, .rs-lite .chat-translate-hint { animation: none; box-shadow: none; }
ddiff --git a/public/css/rs-promo.css b/public/css/rs-promo.css
iindex e9dbc181..cd5586d7 100644
---- a/public/css/rs-promo.css
++++ b/public/css/rs-promo.css
@@@ -795,3 +795,36 @@ body.rs-ticker-on .rs-modal, body.rs-ticker-on .rs-takeover { --rs-ticker-h: 0px
.rs-board-row { opacity: 1; transform: none; } .rs-yes { transform: none; }
.rs-money { background: none; -webkit-text-fill-color: var(--rs-gold); color: var(--rs-gold); }
}
++
++/* ═══════════════════════════════════════════════════════════════
++ LITE MODE — html.rs-lite (phones / weak GPUs / Data Saver) and any screen ≤ 820px.
++ No blur filters, blend modes, backdrop-filter, canvases or animated shadows: those are what
++ make low-end Android Chrome flicker. Transform/opacity animations stay.
++ ═══════════════════════════════════════════════════════════════ */
++.rs-lite .rs-tk-aurora, .rs-lite .rs-tk-scan, .rs-lite .rs-tk-gl, .rs-lite .rs-tk-canvas, .rs-lite .rs-tk-burst, .rs-lite .rs-card-foil, .rs-lite .rs-card-glare, .rs-lite .rs-bills, .rs-lite .rs-vip-bubbles, .rs-lite .rs-ticker::before, .rs-lite .rs-btn-gold::after, .rs-lite .rs-glitch::before, .rs-lite .rs-glitch::after { display: none !important; }
++.rs-lite .rs-takeover { background: radial-gradient(120% 80% at 50% 0%, #2e1065 0%, #1e1b4b 40%, #06060a 80%); }
++.rs-lite .rs-tk-grid { animation: none; transform: perspective(600px) rotateX(64deg); opacity: 0.5; }
++.rs-lite .rs-tk-content, .rs-lite .rs-tk-amount, .rs-lite .rs-tk-amount .rs-money { transform: none !important; animation: none !important; }
++.rs-lite .rs-money { animation: none; filter: none; background: none; -webkit-text-fill-color: var(--rs-gold); color: var(--rs-gold); text-shadow: 0 2px 0 rgba(0,0,0,0.35); }
++.rs-lite .rs-tk-amount .rs-money { filter: none; }
++.rs-lite .rs-ticker { animation: rsTickerDrop 0.5s ease both; background: linear-gradient(90deg, #4c1d95, #1d4ed8, #7c3aed); }
++.rs-lite .rs-ticker-item .rs-ticker-cta, .rs-lite .rs-tk-eyebrow, .rs-lite .rs-flow-node.rs-to, .rs-lite .rs-btn-gold, .rs-lite .rs-stream-alert, .rs-lite .rs-sa-siren, .rs-lite .rs-flow2-hub, .rs-lite .rs-chip, .rs-lite .rs-flow2-node, .rs-lite .rs-odo b, .rs-lite .rs-yes, .rs-lite .rs-star-avatar-glow { animation: none !important; box-shadow: none !important; }
++.rs-lite .rs-chip, .rs-lite .rs-flow-node, .rs-lite .rs-flow2-node, .rs-lite .rs-btn-ghost, .rs-lite .rs-tk-close, .rs-lite .rs-modal, .rs-lite .rs-tk-tour { backdrop-filter: none !important; -webkit-backdrop-filter: none !important; }
++.rs-lite .rs-modal { background: rgba(4,4,10,0.9); }
++.rs-lite .rs-card-border::before { animation: none; background: linear-gradient(135deg, var(--rs-gold), var(--accent), var(--rs-blue)); inset: 0; }
++.rs-lite .rs-hero-card { transform: none !important; transition: none; box-shadow: 0 10px 30px rgba(0,0,0,0.4); }
++.rs-lite .rs-coin { animation: none; transform: rotateY(0); } .rs-lite .rs-coin-face { box-shadow: inset 0 0 0 5px rgba(180,83,9,0.55); }
++.rs-lite .rs-vip-star { filter: none; } .rs-lite .rs-vip { box-shadow: 0 10px 30px rgba(0,0,0,0.4); }
++.rs-lite .rs-vip-sparkle { animation: none; opacity: 0.8; }
++.rs-lite .rs-stream-alert { box-shadow: 0 0 0 2px rgba(251,191,36,0.6); }
++.rs-lite .rs-sa-stripe { animation: none; }
++.rs-lite .rs-packet { box-shadow: none; }
++.rs-lite .rs-board::before { animation: none; }
++.rs-lite .rs-tk-title { text-shadow: none; }
++@media (max-width: 820px) {
++ /* CSS-only safety net for the most expensive effects even before JS decides on lite mode. */
++ .rs-tk-aurora { filter: none; opacity: 0.55; } .rs-tk-aurora span { mix-blend-mode: normal; animation: none; }
++ .rs-tk-scan, .rs-card-foil { display: none; }
++ .rs-tk-amount .rs-money { filter: drop-shadow(0 4px 0 rgba(120,53,15,0.6)); }
++ .rs-stream-alert, .rs-sa-siren, .rs-btn-gold { animation: none; }
++}
ddiff --git a/public/index.html b/public/index.html
iindex b644c003..3c290bd4 100644
---- a/public/index.html
++++ b/public/index.html
@@@ -67,9 +67,9 @@
<link rel="stylesheet" href="/css/broadcast.css?v=11">
<link rel="stylesheet" href="/css/cosmetics.css?v=2">
<link rel="stylesheet" href="/css/messenger.css?v=1">
-- <link rel="stylesheet" href="/css/rs-promo.css?v=8">
-- <link rel="stylesheet" href="/css/home-fx.css?v=2">
-- <link rel="stylesheet" href="/css/i18n-star.css?v=2">
++ <link rel="stylesheet" href="/css/rs-promo.css?v=9">
++ <link rel="stylesheet" href="/css/home-fx.css?v=3">
++ <link rel="stylesheet" href="/css/i18n-star.css?v=3">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<!-- JSON-LD Structured Data -->
@@@ -5126,8 +5126,8 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig
<script src="/js/pastes.js?v=15" defer></script>
<script src="/js/draggable-fab.js?v=2" defer></script>
<script src="/js/messenger.js?v=2" defer></script>
--<script src="/js/rs-promo.js?v=11" defer></script>
--<script src="/js/home-fx.js?v=1" defer></script>
++<script src="/js/rs-promo.js?v=12" defer></script>
++<script src="/js/home-fx.js?v=2" defer></script>
<!-- ── OpenVibe shared libs (notifications, account sync, themes) ── -->
<script src="/shared/theme-loader.js" defer></script>
ddiff --git a/public/js/home-fx.js b/public/js/home-fx.js
iindex 1cff4811..a7a5174a 100644
---- a/public/js/home-fx.js
++++ b/public/js/home-fx.js
@@@ -16,6 +16,7 @@
const REDUCED = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const COARSE = window.matchMedia && window.matchMedia('(pointer: coarse)').matches;
if (REDUCED) return;
++ if (COARSE || window.innerWidth <= 820 || (navigator.deviceMemory && navigator.deviceMemory <= 4)) { document.documentElement.classList.add('rs-lite'); return; }
const onHome = () => { const p = document.getElementById('page-home'); return !!(p && p.classList.contains('active')); };
ddiff --git a/public/js/rs-promo.js b/public/js/rs-promo.js
iindex 2cad5487..2b976219 100644
---- a/public/js/rs-promo.js
++++ b/public/js/rs-promo.js
@@@ -21,11 +21,24 @@
(function () {
'use strict';
-- const PROMO_VERSION = 'rs-desperate-v7'; // bump to re-show the takeover to everyone
++ const PROMO_VERSION = 'rs-desperate-v8'; // bump to re-show the takeover to everyone
const SEEN_KEY = 'ov_rs_promo_seen';
const TICKER_KEY = 'ov_rs_ticker_hidden'; // sessionStorage — comes back next visit
const REDUCED = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const COARSE = window.matchMedia && window.matchMedia('(pointer: coarse)').matches;
++ // Lite mode: phones and weak devices get the copy and the cheap transform animations, but no
++ // canvases, shaders, blur filters, blend modes or animated shadows (they flicker and stutter on
++ // low-end Android GPUs). Also honours Data Saver.
++ const LITE = (() => {
++ try {
++ const narrow = window.innerWidth <= 820;
++ const weak = (navigator.hardwareConcurrency && navigator.hardwareConcurrency <= 4) || (navigator.deviceMemory && navigator.deviceMemory <= 4);
++ const saver = navigator.connection && navigator.connection.saveData;
++ const ua = /Android|iPhone|iPad|Mobile/i.test(navigator.userAgent);
++ return !!(saver || (COARSE && (narrow || ua)) || (narrow && weak) || (ua && weak));
++ } catch { return COARSE; }
++ })();
++ if (LITE) document.documentElement.classList.add('rs-lite');
const cfg = { enabled: true, amount: 50, amountMin: 25, referral: 10, vip: null, totals: null, github: 'https://github.com/OpenVibers/OpenVibe.Live', owner: 'admin', discord: 'https://discord.gg/M6MuRUaeJj' };
const STREAM_ALERT_KEY = 'ov_rs_stream_alert_min'; // sessionStorage — collapsed (never hidden) state
@@@ -205,11 +218,15 @@
document.body.style.overflow = 'hidden';
store.set(SEEN_KEY, PROMO_VERSION);
if (!REDUCED) {
-- rainStop = startMoneyRain(takeoverEl.querySelector('.rs-tk-canvas'));
-- glStop = startShader(takeoverEl.querySelector('.rs-tk-gl'));
-- attachParallax(takeoverEl);
++ if (!LITE) {
++ rainStop = startMoneyRain(takeoverEl.querySelector('.rs-tk-canvas'));
++ glStop = startShader(takeoverEl.querySelector('.rs-tk-gl'));
++ attachParallax(takeoverEl);
++ } else {
++ takeoverEl.querySelectorAll('.rs-tk-canvas, .rs-tk-gl, .rs-tk-burst, .rs-tk-scan').forEach(c => c.remove());
++ }
typewriter(takeoverEl.querySelector('.rs-tk-type-text'), PLEAS);
-- countUp(takeoverEl.querySelector('[data-countup]'), cfg.amount, 1500, 500, () => { shake(takeoverEl); coinBurst(takeoverEl.querySelector('.rs-tk-burst')); });
++ countUp(takeoverEl.querySelector('[data-countup]'), cfg.amount, 1500, 500, () => { if (!LITE) { shake(takeoverEl); coinBurst(takeoverEl.querySelector('.rs-tk-burst')); } });
takeoverEl.querySelectorAll('[data-odo]').forEach(el => odometer(el, 900));
} else {
takeoverEl.querySelector('[data-countup]').textContent = money();
@@@ -639,7 +656,7 @@
document.body.appendChild(modalEl);
if (loggedIn) loadCashout().catch(() => { const c = document.getElementById('rs-cashout'); if (c) c.innerHTML = '<p class="rs-modal-fine">Could not load your payouts right now.</p>'; });
document.body.style.overflow = 'hidden';
-- if (!REDUCED) confettiBurst();
++ if (!REDUCED && !LITE) confettiBurst();
setTimeout(() => modalEl?.querySelector('.rs-modal-actions .rs-btn')?.focus({ preventScroll: true }), 400);
}
const _spinner = (t) => `<div class="rs-cashout-loading"><i class="fa-solid fa-circle-notch fa-spin"></i> ${esc(t)}</div>`;
@@@ -675,7 +692,7 @@
const body = Object.fromEntries(new FormData(f).entries());
body.method = modalEl?.querySelector('.rs-toggle')?.dataset.method || 'zelle';
btn.disabled = true; btn.innerHTML = '<i class="fa-solid fa-circle-notch fa-spin"></i> Filing…';
-- try { const r = await api('/promo/claims', { method: 'POST', body }); say(`Claim #${r.claim.id} filed — you'll get a notification when it's approved`, 'success'); if (!REDUCED) confettiBurst(); await loadCashout(); }
++ try { const r = await api('/promo/claims', { method: 'POST', body }); say(`Claim #${r.claim.id} filed — you'll get a notification when it's approved`, 'success'); if (!REDUCED && !LITE) confettiBurst(); await loadCashout(); }
catch (err) { say(err?.message || 'Could not file the claim', 'error'); btn.disabled = false; btn.innerHTML = `<i class="fa-solid fa-sack-dollar"></i> File claim for ${money()}`; }
});
box.querySelectorAll('.rs-payout-form').forEach(f => f.addEventListener('submit', async (e) => {