Files
AskaEth 9c7081b21d feat: ClockLab —— 可调速桌面时钟(数字/表盘,支持倍率、暂停、倒转、快进)
一个用于演练/演示的桌面时钟工具:两个窗口 —— 控制窗口(普通)与时钟窗口(无边框、
可拖动、可拖边缘改大小,由控制窗口开与关)。

功能
- 两个窗口各自的「始终置顶」开关
- 显示样式:纯数字 / 圆形表盘,切换立即生效
- 实时设定时间(含「填入当前显示时间」)
- 倍率:支持正数、负数(倒转)、0(暂停)、小数
- 暂停 / 继续(继续时回到暂停前的倍率,按钮文案会显示是几倍)
- 快进五档:+5 秒 / +1 分钟 / +5 分钟 / +10 分钟 / +30 分钟

实现要点
- 走时是「基准时间 + 基准时刻 + 倍率」的锚点式模型(不累加、零漂移);
  倍率、暂停、倒转、快进因此是同一套机制的四个用法,互不打架
- 跳转动画做在「呈现时间」这一层:数字滚动与表盘指针扫动共用同一个中间值,
  所以永远同步,也不会跨 12 点倒着转一圈
- 无边框窗口的拖动(CSS app-region + 控件 no-drag)与拖边缘改大小
  (自己算几何 + IPC setBounds,因为 frame:false 下系统边缘命中不可靠)

测试 152 例:走时模型 / 改大小几何 / 呈现时间 / 两个渲染进程(DOM 桩)
2026-09-15 01:31:07 +08:00

75 lines
3.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
// 呈现时间层:主进程给的是「目标时间」,画面用的是「呈现时间」。
// 平时两者相等;只有在发生一次性跳变(快进、设定时间)时,才用一小段时间把呈现时间缓动到新目标,
// 于是数字会自己「滚」过去、指针会「扫」过去 —— 数字和表盘都只从这一个值推导,不必各写一套动画。
//
// 之所以动画「时间」而不是动画「字符 / 角度」:跨 12 点、跨 0 点时角度公式照旧,
// 不会出现 359°→1° 倒着转一圈;老式里程表那种数字观感也只是同一个值的副产品。
//
// 拆成独立文件是为了能在 node 里直接跑用例(同 renderer/resize.js 的做法)。
const JUMP_THRESHOLD_MS = 1500; // 一步小于这个幅度就直接跳:正常走时每秒都在变,不该每秒动一次
const EASE_MS = 350; // 缓动时长,够看清「滚」的过程,又不至于拖到下一次操作
function targetAt(snapshot, now) {
return snapshot.baseMs + (now - snapshot.baseRealMs) * snapshot.rate;
}
// 两次参数之间的目标断点:在同一个真实时刻上,分别用新旧参数算时间再相减。
// 取新参数的基准时刻来比,是因为改倍率时模型先把当前显示时间固化进了基准,
// 两个公式在这一刻必然相等 —— 「改变速度」不会被误判成「跳变」。
function jumpBetween(previous, next) {
if (!previous) return 0;
const at = next.baseRealMs;
return targetAt(next, at) - targetAt(previous, at);
}
function create() {
return { snapshot: null, offsetMs: 0, startMs: 0 };
}
// 尚未消化的偏移:缓动起点处的差值按三次缓出衰减到 0,越小说明越接近目标
function pendingMs(layer, now) {
const t = (now - layer.startMs) / EASE_MS;
if (t >= 1) return 0; // 到点就彻底归零,不给浮点留残余(否则数字永远差几百毫秒)
if (t <= 0) return layer.offsetMs;
const left = 1 - t;
return layer.offsetMs * left * left * left;
}
// 收到新的走时参数时调用:决定这一次是「直接跳」还是「缓动过去」
function accept(layer, now, snapshot) {
// 只在「目标时间确实断了一截」时才缓动,所以:
// · 幅度太小(正常走时、+1 秒)直跳 —— 否则每秒都在滚,反而看不清时间;
// · rate 0(暂停)直跳 —— 暂停画面必须停在暂停那一刻的准确时间上,
// 让缓动挂在那里的话,暂停期间显示的就是一个走不完的中间值(看着像时间不对)。
const jump = layer.snapshot ? jumpBetween(layer.snapshot, snapshot) : 0;
const animate = layer.snapshot !== null && snapshot.rate !== 0 && Math.abs(jump) > JUMP_THRESHOLD_MS;
if (animate) {
// 从「当前呈现值」重新出发:缓动没结束又来一次跳变时,用旧目标减去尚未消化的偏移反推回去,
// 不能从跳变前的旧值重来,否则画面会先弹回去再滚一次。
const presentedNow = targetAt(layer.snapshot, now) - pendingMs(layer, now);
layer.offsetMs = targetAt(snapshot, now) - presentedNow;
} else {
layer.offsetMs = 0;
}
layer.snapshot = snapshot;
layer.startMs = now;
}
// 每帧调用:这一刻该显示的时间
function valueAt(layer, now) {
return targetAt(layer.snapshot, now) - pendingMs(layer, now);
}
// 渲染进程里这几个函数要能被 clock.js 直接用,所以除了 CommonJS 导出,还挂成一个命名空间
// (同 resize.js 的双向可用做法:浏览器读全局,node 测试读 require)
const presentedTime = { create, accept, valueAt, targetAt, jumpBetween, JUMP_THRESHOLD_MS, EASE_MS };
if (typeof module !== 'undefined' && module.exports) {
module.exports = presentedTime;
}