// Auto Scroll — official WebToApp sample. // // Demonstrates: // - requestAnimationFrame-based smooth scrolling (not setInterval + // scrollBy, which produces visible stuttering on devices) // - Pause-on-touch and pause-on-tab-hidden // - Speed control through a config item // - Floating control panel with start/stop + speed slider // - State persistence with sessionStorage so navigating to a new page on // the same site continues scrolling (function () { var STATE_KEY = 'wta-auto-scroll-active'; var SPEED_KEY = 'wta-auto-scroll-speed'; // Speed: pixels-per-second (independent of frame rate). We map the // 1..10 user-visible level to 30..300 px/s; level 5 is a leisurely // article-reading pace. function levelToPxPerSec(level) { var n = Math.max(1, Math.min(10, level | 0)); return 30 * n; } // Read a possibly-overridden speed from sessionStorage first, else the // user's saved config. Lets the floating panel +/- control do live // tuning without polluting the persisted value. function getSpeedLevel() { var override = sessionStorage.getItem(SPEED_KEY); if (override) return parseInt(override, 10) || 5; return parseInt(getConfig('speedLevel', '5'), 10) || 5; } // ── core scroll engine ──────────────────────────────────────────── var rafId = null; var lastTs = 0; var paused = false; var stopOnUser = String(getConfig('pauseOnInteract', 'true')) === 'true'; var reverse = String(getConfig('reverse', 'false')) === 'true'; var useShortcuts = String(getConfig('keyboardShortcuts', 'true')) === 'true'; function step(ts) { if (paused) { rafId = requestAnimationFrame(step); lastTs = ts; return; } if (!lastTs) lastTs = ts; var dt = (ts - lastTs) / 1000; // seconds lastTs = ts; var px = levelToPxPerSec(getSpeedLevel()) * dt; if (reverse) px = -px; var before = window.scrollY; window.scrollBy(0, px); // If we didn't move (page bottom or scroll-locked), stop — it's // pointless to keep burning frames. if (Math.abs(window.scrollY - before) < 0.1) { stop(); return; } rafId = requestAnimationFrame(step); } function start() { if (rafId !== null) return; if (reverse) window.scrollTo(0, document.documentElement.scrollHeight); sessionStorage.setItem(STATE_KEY, '1'); paused = false; lastTs = 0; rafId = requestAnimationFrame(step); refreshPanel(); } function stop() { if (rafId !== null) cancelAnimationFrame(rafId); rafId = null; sessionStorage.removeItem(STATE_KEY); refreshPanel(); } function toggle() { if (rafId === null) start(); else stop(); } // ── pause-on-interaction ────────────────────────────────────────── // We pause briefly when the user touches the page (so manual scroll // works), then resume after a short idle period. var resumeTimer = null; function onUserScroll() { if (rafId === null || !stopOnUser) return; paused = true; if (resumeTimer) clearTimeout(resumeTimer); resumeTimer = setTimeout(function () { paused = false; lastTs = 0; }, 1500); } window.addEventListener('wheel', onUserScroll, { passive: true }); window.addEventListener('touchstart', onUserScroll, { passive: true }); window.addEventListener( 'keydown', function (e) { // Space / arrows / page down — assume user wants control back. if ( e.key === ' ' || e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'PageDown' || e.key === 'PageUp' ) { onUserScroll(); } }, { passive: true } ); // Pause when the tab is backgrounded — saves battery and stops the // scroll position drifting while the user does something else. // ── keyboard shortcuts (J toggle, [ slower, ] faster, R reverse) ── if (useShortcuts) { window.addEventListener('keydown', function (e) { if (e.ctrlKey || e.metaKey || e.altKey) return; if (e.target && /^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName)) return; if (e.key === 'j' || e.key === 'J') { e.preventDefault(); toggle(); } else if (e.key === '[') { e.preventDefault(); sessionStorage.setItem(SPEED_KEY, String(Math.max(1, getSpeedLevel() - 1))); refreshPanel(); } else if (e.key === ']') { e.preventDefault(); sessionStorage.setItem(SPEED_KEY, String(Math.min(10, getSpeedLevel() + 1))); refreshPanel(); } else if (e.key === 'r' || e.key === 'R') { e.preventDefault(); reverse = !reverse; if (rafId !== null) { stop(); start(); } } }); } document.addEventListener('visibilitychange', function () { if (document.hidden) paused = true; else if (rafId !== null) { paused = false; lastTs = 0; } }); // ── floating panel ──────────────────────────────────────────────── // A tiny remote control: play/pause toggle and speed +/-. Built with // plain DOM so the code reads end-to-end with no framework knowledge. var panel = null; function ensurePanel() { if (panel) return panel; panel = document.createElement('div'); panel.id = 'wta-as-panel'; panel.innerHTML = '' + '' + '' + ''; panel.addEventListener('click', function (e) { var btn = e.target.closest('button[data-action]'); if (!btn) return; if (btn.dataset.action === 'toggle') toggle(); if (btn.dataset.action === 'slower') { sessionStorage.setItem(SPEED_KEY, String(Math.max(1, getSpeedLevel() - 1))); refreshPanel(); } if (btn.dataset.action === 'faster') { sessionStorage.setItem(SPEED_KEY, String(Math.min(10, getSpeedLevel() + 1))); refreshPanel(); } }); document.body.appendChild(panel); return panel; } function refreshPanel() { if (!panel) return; var main = panel.querySelector('.wta-as-main'); var speed = panel.querySelector('.wta-as-speed'); if (rafId === null) { main.textContent = '▶'; panel.classList.remove('wta-as-running'); } else { main.textContent = '⏸'; panel.classList.add('wta-as-running'); } speed.textContent = getSpeedLevel() + '×'; } // ── panel button registration ───────────────────────────────────── if (typeof __WTA_MODULE_UI__ !== 'undefined' && __WTA_MODULE_UI__.register) { __WTA_MODULE_UI__.register({ id: __MODULE_INFO__.id, name: __MODULE_INFO__.name, icon: __MODULE_INFO__.icon, uiConfig: __MODULE_UI_CONFIG__, runMode: __MODULE_RUN_MODE__, onClick: function () { ensurePanel(); refreshPanel(); toggle(); }, }); } // ── resume after navigation ─────────────────────────────────────── if (sessionStorage.getItem(STATE_KEY) === '1') { ensurePanel(); refreshPanel(); start(); } })();