watch plugin: analog clock overlay with SVG watchface
Simple watchface with second-hand wobble animation, 5-min idle auto-show, W key toggle, light/dark theme colors, backdrop blur from theme settings. Grace period prevents instant dismiss. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
00d14b3015
commit
b9345a410f
11 files changed
+374
-2
No files matched your search
@@ -0,0 +1,169 @@
|
||||
// ── Watch plugin — analog clock overlay ──
|
||||
|
||||
const $ = id => document.getElementById(id);
|
||||
|
||||
const LAYER_MAP = {
|
||||
digits: 'watch-layer-digits',
|
||||
hour: 'watch-layer-hour',
|
||||
minute: 'watch-layer-minute',
|
||||
second: 'watch-second-wobble',
|
||||
};
|
||||
|
||||
let visible = false;
|
||||
let tickTimer = null;
|
||||
let idleTimer = null;
|
||||
let lastActivity = Date.now();
|
||||
let showedAt = 0;
|
||||
let wobbleKick = null, wobbleRebound = null;
|
||||
let svgCache = {};
|
||||
let loaded = false;
|
||||
|
||||
const IDLE_MINUTES = 5;
|
||||
|
||||
// ── SVG loading ──
|
||||
|
||||
async function loadSvg(layer) {
|
||||
if (svgCache[layer]) return svgCache[layer];
|
||||
try {
|
||||
const res = await fetch(`plugins/watch/faces/simple/${layer}.svg`);
|
||||
if (!res.ok) return '';
|
||||
let svg = await res.text();
|
||||
svg = svg.replace(/<\?xml[\s\S]*?\?>/gi, '').replace(/<!DOCTYPE[\s\S]*?>/gi, '').trim();
|
||||
svgCache[layer] = svg;
|
||||
return svg;
|
||||
} catch { return ''; }
|
||||
}
|
||||
|
||||
async function loadFace() {
|
||||
if (loaded) return;
|
||||
for (const [layer, elId] of Object.entries(LAYER_MAP)) {
|
||||
const el = $(elId);
|
||||
if (el) el.innerHTML = await loadSvg(layer);
|
||||
}
|
||||
loaded = true;
|
||||
}
|
||||
|
||||
// ── Clock logic ──
|
||||
|
||||
function tick() {
|
||||
const now = new Date();
|
||||
const h12 = now.getHours() % 12;
|
||||
const m = now.getMinutes();
|
||||
const s = now.getSeconds();
|
||||
|
||||
const hourEl = $('watch-layer-hour');
|
||||
const minEl = $('watch-layer-minute');
|
||||
const secEl = $('watch-layer-second');
|
||||
|
||||
if (hourEl) hourEl.style.transform = `rotate(${(((h12 + m / 60) / 12) * 360).toFixed(3)}deg)`;
|
||||
if (minEl) minEl.style.transform = `rotate(${(((m + s / 60) / 60) * 360).toFixed(3)}deg)`;
|
||||
if (secEl) secEl.style.transform = `rotate(${((s / 60) * 360).toFixed(3)}deg)`;
|
||||
|
||||
triggerWobble();
|
||||
}
|
||||
|
||||
function triggerWobble() {
|
||||
const wobEl = $('watch-second-wobble');
|
||||
if (!wobEl) return;
|
||||
clearTimeout(wobbleKick);
|
||||
clearTimeout(wobbleRebound);
|
||||
wobEl.style.transform = 'rotate(0deg)';
|
||||
requestAnimationFrame(() => {
|
||||
wobEl.style.transform = 'rotate(0.78deg)';
|
||||
wobbleKick = setTimeout(() => { wobEl.style.transform = 'rotate(-0.25deg)'; }, 95);
|
||||
wobbleRebound = setTimeout(() => { wobEl.style.transform = 'rotate(0deg)'; }, 205);
|
||||
});
|
||||
}
|
||||
|
||||
function startTick() {
|
||||
if (tickTimer) return;
|
||||
tick();
|
||||
tickTimer = setInterval(tick, 1000);
|
||||
}
|
||||
|
||||
function stopTick() {
|
||||
if (tickTimer) { clearInterval(tickTimer); tickTimer = null; }
|
||||
clearTimeout(wobbleKick);
|
||||
clearTimeout(wobbleRebound);
|
||||
}
|
||||
|
||||
// ── Show / hide ──
|
||||
|
||||
async function show() {
|
||||
if (visible) return;
|
||||
visible = true;
|
||||
const overlay = $('watch-overlay');
|
||||
if (!overlay) return;
|
||||
showedAt = Date.now();
|
||||
await loadFace();
|
||||
overlay.classList.remove('hidden');
|
||||
startTick();
|
||||
requestAnimationFrame(() => overlay.classList.add('visible'));
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (!visible) return;
|
||||
visible = false;
|
||||
const overlay = $('watch-overlay');
|
||||
if (!overlay) return;
|
||||
overlay.classList.remove('visible');
|
||||
stopTick();
|
||||
setTimeout(() => { if (!visible) overlay.classList.add('hidden'); }, 250);
|
||||
}
|
||||
|
||||
function toggle() { visible ? hide() : show(); }
|
||||
|
||||
// ── Idle auto-show ──
|
||||
|
||||
function resetIdleTimer() {
|
||||
clearTimeout(idleTimer);
|
||||
if (!IDLE_MINUTES) return;
|
||||
idleTimer = setTimeout(() => {
|
||||
if (visible) return;
|
||||
if (document.visibilityState !== 'visible') return;
|
||||
if (document.querySelector('.overlay.open')) return;
|
||||
show();
|
||||
}, IDLE_MINUTES * 60000);
|
||||
}
|
||||
|
||||
function onActivity() {
|
||||
const now = Date.now();
|
||||
if (now - lastActivity < 300) return;
|
||||
lastActivity = now;
|
||||
if (visible && now - showedAt > 500) hide();
|
||||
resetIdleTimer();
|
||||
}
|
||||
|
||||
// ── Init ──
|
||||
|
||||
function init() {
|
||||
const overlay = $('watch-overlay');
|
||||
if (!overlay) return;
|
||||
|
||||
overlay.addEventListener('click', () => { hide(); onActivity(); });
|
||||
|
||||
window.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape' && visible) {
|
||||
e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
|
||||
hide(); onActivity();
|
||||
}
|
||||
}, { capture: true });
|
||||
|
||||
for (const ev of ['pointermove', 'pointerdown', 'keydown', 'wheel', 'touchstart']) {
|
||||
window.addEventListener(ev, onActivity, { passive: true });
|
||||
}
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') onActivity();
|
||||
});
|
||||
|
||||
resetIdleTimer();
|
||||
window.hyprcrd?.registerPlugin({ name: 'watch', version: '1.0.0' });
|
||||
}
|
||||
|
||||
window._watchToggle = toggle;
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
Reference in new issue
Block a user