feat: ClockLab —— 可调速桌面时钟(数字/表盘,支持倍率、暂停、倒转、快进)
一个用于演练/演示的桌面时钟工具:两个窗口 —— 控制窗口(普通)与时钟窗口(无边框、 可拖动、可拖边缘改大小,由控制窗口开与关)。 功能 - 两个窗口各自的「始终置顶」开关 - 显示样式:纯数字 / 圆形表盘,切换立即生效 - 实时设定时间(含「填入当前显示时间」) - 倍率:支持正数、负数(倒转)、0(暂停)、小数 - 暂停 / 继续(继续时回到暂停前的倍率,按钮文案会显示是几倍) - 快进五档:+5 秒 / +1 分钟 / +5 分钟 / +10 分钟 / +30 分钟 实现要点 - 走时是「基准时间 + 基准时刻 + 倍率」的锚点式模型(不累加、零漂移); 倍率、暂停、倒转、快进因此是同一套机制的四个用法,互不打架 - 跳转动画做在「呈现时间」这一层:数字滚动与表盘指针扫动共用同一个中间值, 所以永远同步,也不会跨 12 点倒着转一圈 - 无边框窗口的拖动(CSS app-region + 控件 no-drag)与拖边缘改大小 (自己算几何 + IPC setBounds,因为 frame:false 下系统边缘命中不可靠) 测试 152 例:走时模型 / 改大小几何 / 呈现时间 / 两个渲染进程(DOM 桩)
This commit is contained in:
1 parent
892b9190aa
commit
9c7081b21d
27 files changed
+5467
-2
No files matched your search
@@ -0,0 +1,167 @@
|
||||
'use strict';
|
||||
|
||||
// 纯逻辑自测(不需要 Electron、不需要显示器):
|
||||
// node test/clock-model.test.js
|
||||
// 走时模型是所有功能的地基,这里把它单独钉死,GUI 的问题就不会和算法问题混在一起。
|
||||
|
||||
const clockModel = require('../src/clock-model');
|
||||
|
||||
let passed = 0;
|
||||
const failures = [];
|
||||
|
||||
function check(name, ok, detail) {
|
||||
if (ok) {
|
||||
passed += 1;
|
||||
console.log(`PASS ${name}${detail ? ` [${detail}]` : ''}`);
|
||||
} else {
|
||||
failures.push(name);
|
||||
console.log(`FAIL ${name}${detail ? ` [${detail}]` : ''}`);
|
||||
}
|
||||
}
|
||||
|
||||
const T0 = 1_700_000_000_000; // 固定基准时刻,测试不依赖真实时钟
|
||||
const SECOND = 1000;
|
||||
|
||||
console.log('--- 正常走时 ---');
|
||||
{
|
||||
const c = clockModel.create(T0);
|
||||
check('初始倍率为 1', c.rate === 1);
|
||||
check('走 1 秒真实时间 = 1 秒显示时间', clockModel.displayMs(c, T0 + SECOND) === T0 + SECOND);
|
||||
check('走 1 小时真实时间 = 1 小时显示时间', clockModel.displayMs(c, T0 + 3600 * SECOND) === T0 + 3600 * SECOND);
|
||||
}
|
||||
|
||||
{
|
||||
// 分 3600 次推进,验证「基准 + 经过时间 × 倍率」不会像逐帧累加那样漂移
|
||||
const c = clockModel.create(T0);
|
||||
let now = T0;
|
||||
for (let i = 0; i < 3600; i += 1) {
|
||||
now += SECOND;
|
||||
clockModel.displayMs(c, now);
|
||||
}
|
||||
check('一小时逐秒推进后零漂移', clockModel.displayMs(c, now) === T0 + 3600 * SECOND);
|
||||
}
|
||||
|
||||
console.log('--- 倍率 ---');
|
||||
{
|
||||
const c = clockModel.create(T0);
|
||||
clockModel.setRate(c, T0, 2);
|
||||
check('倍率 2:1 秒真实时间走 2 秒', clockModel.displayMs(c, T0 + SECOND) === T0 + 2 * SECOND);
|
||||
}
|
||||
{
|
||||
const c = clockModel.create(T0);
|
||||
clockModel.setRate(c, T0, 0.5);
|
||||
check('倍率 0.5:半速', clockModel.displayMs(c, T0 + SECOND) === T0 + 500);
|
||||
}
|
||||
{
|
||||
const c = clockModel.create(T0);
|
||||
clockModel.setRate(c, T0, -1);
|
||||
check('倍率 -1:走 1 秒真实时间倒退 1 秒', clockModel.displayMs(c, T0 + SECOND) === T0 - SECOND);
|
||||
check('倍率 -1:再走 5 秒仍在继续倒退(不是卡住)',
|
||||
clockModel.displayMs(c, T0 + 6 * SECOND) === T0 - 6 * SECOND);
|
||||
}
|
||||
{
|
||||
const c = clockModel.create(T0);
|
||||
clockModel.setRate(c, T0, -2.5);
|
||||
check('倍率 -2.5:小数 + 负数同时成立', clockModel.displayMs(c, T0 + 4 * SECOND) === T0 - 10 * SECOND);
|
||||
}
|
||||
|
||||
console.log('--- 切换倍率不跳变 ---');
|
||||
{
|
||||
const c = clockModel.create(T0);
|
||||
const before = clockModel.displayMs(c, T0 + 3000);
|
||||
clockModel.setRate(c, T0 + 3000, 7);
|
||||
const after = clockModel.displayMs(c, T0 + 3000);
|
||||
check('切倍率的瞬间显示时间连续(同一时刻前后相等)', before === after, `${before} → ${after}`);
|
||||
check('切换后按新倍率走', clockModel.displayMs(c, T0 + 4000) === before + 7 * SECOND);
|
||||
}
|
||||
|
||||
console.log('--- 暂停 / 继续 ---');
|
||||
{
|
||||
const c = clockModel.create(T0);
|
||||
clockModel.setRate(c, T0, 4);
|
||||
clockModel.setRate(c, T0 + SECOND, 0);
|
||||
const frozenAt = clockModel.displayMs(c, T0 + SECOND);
|
||||
check('倍率 0:显示时间冻结', clockModel.displayMs(c, T0 + 60 * SECOND) === frozenAt);
|
||||
check('暂停时记住了暂停前的倍率 4', c.lastRate === 4);
|
||||
|
||||
clockModel.setRate(c, T0 + 60 * SECOND, c.lastRate);
|
||||
check('继续:从暂停处按原倍率接着走',
|
||||
clockModel.displayMs(c, T0 + 61 * SECOND) === frozenAt + 4 * SECOND);
|
||||
}
|
||||
{
|
||||
const c = clockModel.create(T0);
|
||||
clockModel.setRate(c, T0, 0);
|
||||
check('倍率一直是 0 时 lastRate 保持默认 1', c.lastRate === 1);
|
||||
}
|
||||
|
||||
console.log('--- 快进 / 快退 ---');
|
||||
{
|
||||
const c = clockModel.create(T0);
|
||||
const before = clockModel.displayMs(c, T0 + 2000);
|
||||
clockModel.shift(c, T0 + 2000, 5 * SECOND);
|
||||
check('+5 秒:显示时间立刻加 5 秒', clockModel.displayMs(c, T0 + 2000) === before + 5 * SECOND);
|
||||
check('快进后基准确实变了', c.baseMs === before + 5 * SECOND && c.baseRealMs === T0 + 2000);
|
||||
check('快进后继续按倍率走', clockModel.displayMs(c, T0 + 3000) === before + 6 * SECOND);
|
||||
}
|
||||
{
|
||||
const c = clockModel.create(T0);
|
||||
clockModel.setRate(c, T0, -1);
|
||||
const before = clockModel.displayMs(c, T0 + 10 * SECOND);
|
||||
clockModel.shift(c, T0 + 10 * SECOND, 30 * 60 * SECOND);
|
||||
check('负倍率下 +30 分钟:先固化显示时间再平移(不与倒转打架)',
|
||||
clockModel.displayMs(c, T0 + 10 * SECOND) === before + 30 * 60 * SECOND);
|
||||
check('平移后负倍率继续生效',
|
||||
clockModel.displayMs(c, T0 + 11 * SECOND) === before + 30 * 60 * SECOND - SECOND);
|
||||
}
|
||||
{
|
||||
const c = clockModel.create(T0);
|
||||
clockModel.setRate(c, T0, 0);
|
||||
const before = clockModel.displayMs(c, T0 + 1000);
|
||||
clockModel.shift(c, T0 + 1000, 10 * 60 * SECOND);
|
||||
check('暂停状态下快进仍然生效,且之后继续冻结',
|
||||
clockModel.displayMs(c, T0 + 60 * SECOND) === before + 10 * 60 * SECOND);
|
||||
}
|
||||
{
|
||||
// 五个快进按钮的档位
|
||||
const deltas = [5 * SECOND, 60 * SECOND, 5 * 60 * SECOND, 10 * 60 * SECOND, 30 * 60 * SECOND];
|
||||
const c = clockModel.create(T0);
|
||||
const before = clockModel.displayMs(c, T0);
|
||||
for (const d of deltas) clockModel.shift(c, T0, d);
|
||||
const total = deltas.reduce((a, b) => a + b, 0);
|
||||
check('五档快进累加正确(+5s/+1m/+5m/+10m/+30m)',
|
||||
clockModel.displayMs(c, T0) === before + total, `总计 ${total / 1000} 秒`);
|
||||
}
|
||||
|
||||
console.log('--- 设定时间 ---');
|
||||
{
|
||||
const c = clockModel.create(T0);
|
||||
clockModel.setRate(c, T0, 60);
|
||||
clockModel.setTimeOfDay(c, T0 + 5000, 8, 30, 15);
|
||||
const d = new Date(clockModel.displayMs(c, T0 + 5000));
|
||||
check('设定为 08:30:15 且毫秒归零',
|
||||
d.getHours() === 8 && d.getMinutes() === 30 && d.getSeconds() === 15 && d.getMilliseconds() === 0,
|
||||
`${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}.${d.getMilliseconds()}`);
|
||||
check('设定后高倍率继续生效', clockModel.displayMs(c, T0 + 6000) === d.getTime() + 60 * SECOND);
|
||||
}
|
||||
{
|
||||
const c = clockModel.create(T0);
|
||||
clockModel.setTimeOfDay(c, T0, 0, 0, 0);
|
||||
const d = new Date(clockModel.displayMs(c, T0));
|
||||
check('设定为 00:00:00(边界)', d.getHours() === 0 && d.getMinutes() === 0 && d.getSeconds() === 0);
|
||||
}
|
||||
{
|
||||
const c = clockModel.create(T0);
|
||||
clockModel.setTimeOfDay(c, T0, 23, 59, 59);
|
||||
const d = new Date(clockModel.displayMs(c, T0 + 2000));
|
||||
check('23:59:59 后跨零点进位', d.getHours() === 0 && d.getMinutes() === 0 && d.getSeconds() === 1,
|
||||
`${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}`);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
if (failures.length === 0) {
|
||||
console.log(`全部通过:${passed}/${passed}`);
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log(`通过 ${passed},失败 ${failures.length}:${failures.join(';')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in new issue
Block a user