一个用于演练/演示的桌面时钟工具:两个窗口 —— 控制窗口(普通)与时钟窗口(无边框、 可拖动、可拖边缘改大小,由控制窗口开与关)。 功能 - 两个窗口各自的「始终置顶」开关 - 显示样式:纯数字 / 圆形表盘,切换立即生效 - 实时设定时间(含「填入当前显示时间」) - 倍率:支持正数、负数(倒转)、0(暂停)、小数 - 暂停 / 继续(继续时回到暂停前的倍率,按钮文案会显示是几倍) - 快进五档:+5 秒 / +1 分钟 / +5 分钟 / +10 分钟 / +30 分钟 实现要点 - 走时是「基准时间 + 基准时刻 + 倍率」的锚点式模型(不累加、零漂移); 倍率、暂停、倒转、快进因此是同一套机制的四个用法,互不打架 - 跳转动画做在「呈现时间」这一层:数字滚动与表盘指针扫动共用同一个中间值, 所以永远同步,也不会跨 12 点倒着转一圈 - 无边框窗口的拖动(CSS app-region + 控件 no-drag)与拖边缘改大小 (自己算几何 + IPC setBounds,因为 frame:false 下系统边缘命中不可靠) 测试 152 例:走时模型 / 改大小几何 / 呈现时间 / 两个渲染进程(DOM 桩)
75 lines
3.7 KiB
JavaScript
75 lines
3.7 KiB
JavaScript
'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;
|
||
}
|