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:
AskaEth committed 2026-09-15 01:31:07 +08:00
1 parent 892b9190aa
commit 9c7081b21d
27 files changed
+5467 -2

No files matched your search

+82
View File
@@ -0,0 +1,82 @@
'use strict';
// 拖边缘改大小的几何用例(纯计算,不需要 Electron):
// node test/resize.test.js
// 无边框窗口里这段代码只有真鼠标拖拽才能触发,先用纯函数把它钉死,Windows 上就只剩「手感」要试。
const { computeBounds, MIN_SIZE } = require('../renderer/resize');
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 start = { x: 400, y: 300, width: 520, height: 280 };
const same = (a, b) => a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
console.log('--- 单边 ---');
check('东边 +120:只有宽度变,左上角不动',
same(computeBounds(start, 'e', 120, 0), { x: 400, y: 300, width: 640, height: 280 }));
check('南边 +60:只有高度变',
same(computeBounds(start, 's', 0, 60), { x: 400, y: 300, width: 520, height: 340 }));
check('西边 -80:宽度增加 80,左边界左移 80',
same(computeBounds(start, 'w', -80, 0), { x: 320, y: 300, width: 600, height: 280 }));
check('北边 -50:高度增加 50,上边界上移 50',
same(computeBounds(start, 'n', 0, -50), { x: 400, y: 250, width: 520, height: 330 }));
check('东边往回拖 -100:窗口变窄,左上角不动',
same(computeBounds(start, 'e', -100, 0), { x: 400, y: 300, width: 420, height: 280 }));
console.log('--- 四角 ---');
check('右下角 +40/+30:宽高同时增加',
same(computeBounds(start, 'se', 40, 30), { x: 400, y: 300, width: 560, height: 310 }));
check('左上角 -40/-30:宽高增加且整体左上移',
same(computeBounds(start, 'nw', -40, -30), { x: 360, y: 270, width: 560, height: 310 }));
check('右上角 +40/-30:宽度增加、上边界上移',
same(computeBounds(start, 'ne', 40, -30), { x: 400, y: 270, width: 560, height: 310 }));
check('左下角 -40/+30:左边界左移、高度增加',
same(computeBounds(start, 'sw', -40, 30), { x: 360, y: 300, width: 560, height: 310 }));
console.log('--- 最小尺寸 ---');
check('东边往死里拖:宽度停在 260', computeBounds(start, 'e', -9999, 0).width === MIN_SIZE.width);
check('南边往死里拖:高度停在 150', computeBounds(start, 's', 0, -9999).height === MIN_SIZE.height);
{
const r = computeBounds(start, 'w', 9999, 0);
check('西边往死里拖:宽度停在 260,且右边界不动(窗口不会被推走)',
r.width === MIN_SIZE.width && r.x + r.width === start.x + start.width, JSON.stringify(r));
}
{
const r = computeBounds(start, 'n', 0, 9999);
check('北边往死里拖:高度停在 150,且下边界不动',
r.height === MIN_SIZE.height && r.y + r.height === start.y + start.height, JSON.stringify(r));
}
{
const r = computeBounds(start, 'nw', 9999, 9999);
check('左上角往死里拖:宽高都停在最小值,右下角不动',
r.width === MIN_SIZE.width && r.height === MIN_SIZE.height &&
r.x + r.width === start.x + start.width && r.y + r.height === start.y + start.height, JSON.stringify(r));
}
console.log('--- 边界 ---');
check('位移为 0 时原样返回', same(computeBounds(start, 'e', 0, 0), start));
check('结果都是整数(避免 setBounds 收到小数后抖动)',
Number.isInteger(computeBounds(start, 'e', 12.4, 0).width));
check('负坐标(窗口在主屏左侧)也能算',
same(computeBounds({ x: -900, y: -200, width: 520, height: 280 }, 'w', -100, 0),
{ x: -1000, y: -200, width: 620, height: 280 }));
console.log('');
if (failures.length === 0) {
console.log(`全部通过:${passed}/${passed}`);
process.exit(0);
} else {
console.log(`通过 ${passed},失败 ${failures.length}:${failures.join(';')}`);
process.exit(1);
}