// src/renderer/secondTick.ts // 统一的「每显示秒重画一次」调度器,替代各面板自己的 rAF / setTimeout(250) 循环。 // // 为什么不是简单的 setInterval(1000): // 重新导出浮窗需要的日期工具,避免浮窗再绕到 shared/constants 拉(仅模块内部使用)。 export { localDateKey } from '../shared/constants'; // 倒计时显示的是 Math.ceil((endsAt - now) / 1000),它的变化时刻锁在 endsAt 的相位上, // 跟墙上时钟的整秒边界没有关系。用整秒对齐的定时器驱动会产生 0~999ms 的固定滞后 // (最坏情况下 00:00 会比铃声晚将近一秒才出现)——比它要替换掉的 240ms 闸门更差。 // 所以这里睡到「显示值下一次真正改变的那一刻」:唤醒次数是 1 次/秒,而重画延迟 // 反而从 ≤240ms 降到 ≤1 帧。 // // 为什么必须处理 visibilitychange: // rAF 在窗口隐藏时天然停摆、显示时立即恢复;setTimeout 不是 —— 隐藏页面的定时器会被 // Chromium 对齐到 1s,约 5 分钟后进一步降到 1 次/分钟。若不管,从托盘恢复主窗时会看到 // 最长一分钟的陈旧读数。这里隐藏时直接停表、显示时立刻重画并重排, // 既复刻了 rAF 原本的语义,也比它更省(隐藏期间零唤醒)。 export interface SecondTicker { /** 状态变化后重排下一次唤醒(endsAt 变了、开始/暂停/重置了)。 */ restart(): void; stop(): void; } /** * @param draw 重画回调 * @param nextAt 返回「显示值下一次改变」的绝对时间戳;返回 null 表示当前状态下 * 显示值不会自己变化(idle / paused),此时不装定时器。 */ export function createSecondTicker(draw: () => void, nextAt: () => number | null): SecondTicker { let timer = 0; const clear = (): void => { if (timer) { window.clearTimeout(timer); timer = 0; } }; const schedule = (): void => { clear(); if (document.hidden) return; // 隐藏期间不唤醒,show 时由 onVisibility 补画 const at = nextAt(); if (at === null) return; // clamp 到 [4, 1000]:即使 nextAt() 的相位算错,最坏也退化成 1Hz 轮询, // 不会比「每秒刷新一次」更差。 const delay = Math.min(1000, Math.max(4, at - Date.now())); timer = window.setTimeout(() => { timer = 0; draw(); schedule(); }, delay); }; const onVisibility = (): void => { if (document.hidden) clear(); else { draw(); schedule(); } }; document.addEventListener('visibilitychange', onVisibility); schedule(); return { restart: schedule, stop: () => { clear(); document.removeEventListener('visibilitychange', onVisibility); } }; } /** * 倒数显示 Math.ceil(remain/1000) 下一次变化的绝对时刻。 * 例:endsAt = now+5500 → 当前显示 6,将在 now+504 变成 5。 * +4ms 让 ceil 稳定跨过边界,避免在临界点上重复算出同一个时刻。 */ export function nextCeilBoundary(endsAt: number, now: number = Date.now()): number { const remain = Math.max(0, endsAt - now); const shown = Math.ceil(remain / 1000); return shown <= 0 ? now + 1000 : endsAt - (shown - 1) * 1000 + 4; }