MiniCPM5-2B-WebGPU-Pi-HTTP / app /src /pi /idle-paws.mjs
Mike0021's picture
Enable browser HTTP commands with CORS-aware errors and bounded requests
39371ea verified
Raw History Blame Contribute Delete
2.98 kB
const IDLE_DELAY = 7_000;
const TRAIL_DURATION = 6_500;
export function createIdlePaws(element, getArea) {
const layer = document.createElement('div');
layer.className = 'terminal-paws'; layer.hidden = true;
layer.setAttribute('aria-hidden', 'true');
for (let i = 0; i < 6; i++) layer.append(document.createElement('i'));
element.append(layer);
const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)');
const listeners = new AbortController();
let enabled = false, visible = false, disposed = false;
let timer = 0, trailTimer = 0, wakeAt = 0, nextAt = 0;
function canRun() {
return enabled && visible && !disposed && !document.hidden && document.hasFocus()
&& !reducedMotion.matches && !document.querySelector('dialog[open]');
}
function hide() {
clearTimeout(trailTimer); trailTimer = 0;
if (!layer.hidden) layer.hidden = true;
}
function schedule() {
if (!canRun()) { clearTimeout(timer); timer = 0; return; }
// Reuse the pending timer when repeated input moves the deadline later.
if (timer && wakeAt <= nextAt) return;
clearTimeout(timer); wakeAt = nextAt;
timer = setTimeout(tick, Math.max(0, nextAt - Date.now()));
}
function reset() {
hide(); nextAt = Date.now() + IDLE_DELAY; schedule();
}
function tick() {
timer = 0;
if (!canRun()) return;
if (Date.now() < nextAt) { schedule(); return; }
const area = getArea();
if (area && area.width >= 220 && area.height >= 180) {
Object.assign(layer.style, {
left: `${area.left}px`, top: `${area.top}px`,
width: `${area.width}px`, height: `${area.height}px`,
});
layer.style.setProperty('--trail-shift', `${Math.random() * 10}%`);
layer.hidden = false;
trailTimer = setTimeout(hide, TRAIL_DURATION);
}
nextAt = Date.now() + TRAIL_DURATION + 45_000 + Math.random() * 15_000;
schedule();
}
const options = { passive: true, capture: true, signal: listeners.signal };
for (const event of ['pointerdown', 'keydown', 'input', 'wheel', 'scroll', 'touchstart', 'touchmove', 'focusin']) {
document.addEventListener(event, reset, options);
}
document.addEventListener('visibilitychange', reset, options);
window.addEventListener('blur', reset, options);
window.addEventListener('focus', reset, options);
reducedMotion.addEventListener('change', reset, options);
const dialogs = new MutationObserver(reset);
for (const dialog of document.querySelectorAll('dialog')) dialogs.observe(dialog, { attributes: true, attributeFilter: ['open'] });
const visibility = new IntersectionObserver(([entry]) => { visible = entry.isIntersecting; reset(); });
visibility.observe(element);
return {
reset,
setEnabled(value) { if (value !== enabled) { enabled = value; reset(); } },
dispose() {
disposed = true; hide(); clearTimeout(timer);
listeners.abort(); dialogs.disconnect(); visibility.disconnect(); layer.remove();
},
};
}