在这里插入图片描述
在这里插入图片描述

时钟仪表盘设计:圆形坐标系与定时刷新架构

一、设计理念:时间与数据都要「动」

前三个应用都是「静态绘制 + 触摸交互」,本应用引入时间维:时钟要持续走、仪表盘数据要会变。页面结构:

  1. 标题栏:🕐 动态时钟 + 实时日期;
  2. 模拟时钟卡片:深色表盘 + 60 刻度 + 12 数字 + 三根表针(秒针平滑扫秒);
  3. 仪表盘卡片:环形进度 + 动态变色 + 刻度 + 中心数值;
  4. 操作区:🎲 刷新数据(仪表动画)、⏸ 暂停/继续(走时开关)。

技术核心:极坐标绘制(角度 → 坐标)、setInterval 定时刷新、动画插值逼近。三篇前的图表是「一次性投影」,本应用是「持续投影」——同一套绘制函数每帧重跑,数据变了画面就变。

为什么第四个应用选时钟仪表盘

前三个应用(签名、涂鸦、图表)都是「用户操作 → 重绘」的事件驱动模型。时钟应用引入了全新的驱动源:时间。这个转变带来三个新技术课题:

  1. 极坐标绘制:图表的饼图只用了极坐标的一小部分(角度);时钟的表盘、刻度、数字、指针全部依赖 圆心 + 半径 × (cos, sin) 这个公式——它是圆形 UI 的「唯一公式」;
  2. 持续动画:前三个应用的重绘由事件触发(触摸、切 tab),时钟的重绘由计时器主动触发,每 250ms 一次,永不停歇——这是「帧循环」的雏形;
  3. 多频率共存:时钟要 4fps、仪表动画要 40fps,两块画布刷新频率差 10 倍——如何隔离、如何分配资源,是性能架构的新课题。

这三个课题分别对应后续三篇:极坐标(4-2 表盘)、走时动画(4-3)、逼近动画与双画布(4-4)。时钟是「从事件驱动走向时间驱动」的桥梁,也是第 5 个应用(粒子,60fps)的性能预演。

时钟应用的「真实感」设计

本应用的时钟不是「显示数字的表」,而是「尽量像真实时钟的模拟表」——这种真实感来自三个设计细节:

细节 实现 效果
扫秒 秒针含毫秒插值 秒针连续滑动而非每秒跳 6°
三针联动 分针含秒、时针含分 分针/时针随下级单位缓慢移动
深色表盘 深蓝灰底 + 白刻度 经典腕表配色,夜间友好

这三个细节的共同点是「把离散的时间变成连续的角度」——真实时钟的指针是连续运动的,只有数字表才跳变。模拟表的魅力就在于连续,这也是本应用刻意追求的方向。

二、数据模型:时间即数据

时钟没有「数据结构」,它的数据是系统时间本身:

const now = new Date();
const s = now.getSeconds() + now.getMilliseconds() / 1000;  // 含毫秒,平滑扫秒
const m = now.getMinutes() + s / 60;                         // 分针受秒影响
const h = (now.getHours() % 12) + m / 60;                    // 时针受分影响

三个关键设计

  1. 秒针含毫秒s = 秒 + 毫秒/1000——若只取整数秒,秒针每秒「跳」一次;含毫秒后秒针连续滑动(扫秒),观感高级;
  2. 分针含秒:分针位置 = 分 + 秒/60,分针随秒数缓慢连续移动而非整分瞬跳;
  3. 时针含分:时针 = 时%12 + 分/60,时针在整点间平滑过渡——真实时钟的三针联动就是这样层层嵌套。

联动公式的统一:每根针 = (自身单位 + 下级单位/进制) / 总刻度 × 2π。秒针 60 格、分针 60 格、时针 12 格,三针共享同一套「比例 → 角度」逻辑。

三、极坐标:圆上任意位置的唯一答案

圆上的一切元素(刻度、数字、表针)都靠同一个公式定位:

x = cx + r·cos(angle)
y = cy + r·sin(angle)
元素 半径 r 角度 angle
刻度线内端 表盘 r−6 刻度角度
数字 表盘 r−32 数字角度
时针端点 r×0.45 小时比例 × 2π
分针端点 r×0.66 分钟比例 × 2π
秒针端点 r−18 秒比例 × 2π

角度约定:Canvas 的 0° 在 3 点方向、顺时针增加;表盘习惯 12 点在上,所以所有角度都减 π/2 偏移angle - π/2),把「3 点」挪到「12 点」。

角度计算三例

const tickAngle = i * Math.PI / 30 - Math.PI / 2;      // 60 刻度,每格 π/30
const numAngle  = i * Math.PI / 6 - Math.PI / 2;       // 12 数字,每格 π/6
const handAngle = value / max * Math.PI * 2 - Math.PI / 2;  // 表针按比例

三例全是「第 i 格 × 每格角度 − π/2」的变体——极坐标绘制的所有东西都是这一个公式的实例。

四、双画布架构:clockCtx 与 gaugeCtx

时钟和仪表盘是两个独立画布、两套独立上下文:

private clockSettings: RenderingContextSettings = new RenderingContextSettings(true);
private clockCtx: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.clockSettings);
private gaugeSettings: RenderingContextSettings = new RenderingContextSettings(true);
private gaugeCtx: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.gaugeSettings);

为什么分开:两套绘制内容刷新频率不同——时钟每 250ms 重绘,仪表只在动画期每 25ms 重绘。共用一块画布会导致「时钟的每帧都在重绘仪表」,浪费且难以隔离。按刷新频率拆画布是仪表盘类应用的通用架构。

onReady 分别初始化

Canvas(this.clockCtx).width(280).height(280)
  .onReady(() => {
    this.clockSize = this.clockCtx.width;
    this.drawClock();
  })
Canvas(this.gaugeCtx).width(280).height(190)
  .onReady(() => {
    this.gaugeW = this.gaugeCtx.width;
    this.gaugeH = this.gaugeCtx.height;
    this.drawGauge();
  })

五、定时刷新:setInterval 的三种生命周期

本应用有两个计时器,生命周期各异:

计时器 间隔 用途 生命周期
clockTimer 250ms 时钟重绘 + 文案刷新 常驻(aboutToAppear 启动)
gaugeTimer 25ms 仪表逼近动画 一次性(到位即清)
aboutToAppear(): void {
  this.startClock();
}
aboutToDisappear(): void {
  // 两个计时器必须全部清理
  if (this.clockTimer !== -1) {
    clearInterval(this.clockTimer);
    this.clockTimer = -1;
  }
  if (this.gaugeTimer !== -1) {
    clearInterval(this.gaugeTimer);
    this.gaugeTimer = -1;
  }
}

页面销毁清理计时器是硬性纪律:页面退出后计时器若仍存活,回调里操作已销毁画布的上下文,轻则无效重绘、重则崩溃。凡 setInterval,必在 aboutToDisappear 清理

六、UI 文案与画布的关系

@State timeText: string = '';
@State dateText: string = '';
@State gaugeText: string = '65%';

分工:画布画「图形」(表盘、表针、进度环),ArkUI 组件显示「文字」(时间、日期、百分比数值)。为什么时间不用 Canvas 画?

  1. 组件文字更精致:字号、字重、颜色全部组件化控制,还支持系统字体;
  2. 刷新更廉价:@State 更新只重绘 Text 组件,不必重绘整块画布;
  3. 语义更清晰:时间是「信息展示」不是「画面元素」,组件更合适。

**「画图形、组件排文字」**的混合架构再次登场——画布聚焦图形,组件负责排版,各司其职。

六.1 时间驱动的两种「同步」方式

时钟应用里,画布(表针)与组件(时间文字)需要保持同步,本应用用「同一回调里取值」实现。还有两种方案值得对比:

方案 实现 优点 缺点
同回调取值(本应用) drawClock + refreshText 共用同一 new Date() 天然同步、简单
分回调各自取 两个 setInterval 各取各的 独立控制 可能差一帧、需对齐
单向传递 文字由画布状态推导 单一真相源 多一层状态

「同回调取值」是最直观可靠的方案——同步的保证来自「同一时刻取样」,而不是「事后对齐」。

六.2 暂停状态与计时器状态的一致性

@State pausedclockTimer !== -1 之间存在隐含的一致性约束:

  • paused = true → clockTimer 应为 -1(已清);
  • paused = false → clockTimer 应非 -1(在跑)。

如果用户连点暂停/继续,可能出现「paused 已变但计时器状态没跟上」的中间态。本应用的 togglePause 是原子操作(先翻转 paused,再同步增删计时器),中间态不会被 UI 观察到——状态变更要「成对」完成,这是交互状态机的纪律。

七、文章小结

本节完成了时钟仪表盘的设计:需求(动态时钟 + 动态仪表)→ 数据模型(时间三针嵌套联动)→ 极坐标公式(角度 → 坐标,全图一公式)→ 双画布架构(按刷新频率拆分)→ 定时器生命周期(常驻 vs 一次性 + 销毁清理)。核心方法论是「极坐标一公式走天下」与「计时器必清理」。

下一节实现表盘:60 刻度、12 数字与三根表针的完整绘制。

八、补充:FAQ

Q1:为什么用 250ms 而不是 1000ms 刷新时钟?
秒针含毫秒插值,250ms 一帧即每秒 4 帧,扫秒已足够平滑;1000ms 则毫秒信息浪费(秒针每帧同位置)。刷新率越高越平滑,但 4fps 对秒针已是「连续感」下限,再高浪费 CPU。

Q2:为什么仪表用独立计时器,不复用时钟的?
刷新频率差 10 倍(25ms vs 250ms),复用会让仪表动画卡顿感明显,也会让时钟白白高频重绘。按需独立计时器,各管各的频率。

Q3:三针联动公式记不住怎么办?
记一句话:「低一级单位按进制折算进本级」——秒带毫秒、分带秒、时带分,每根针的取值都是「整数 + 下级小数」,再除以总刻度乘 2π。推导一遍就忘不掉。

九、深入:为什么时钟是最适合「极坐标教学」的载体

时钟的绘制几乎覆盖了极坐标的全部应用场景,而且每个元素都是「一个公式的变体」,这是它作为教学载体的独特价值。

9.1 极坐标的完整知识图谱

本应用涉及极坐标的四个层次:

层次 内容 时钟里的例子
点定位 圆心 + 半径 × (cos, sin) 数字位置、刻度端点
线段绘制 两个极坐标点连线 刻度线、表针
弧线绘制 arc 起点角 → 终点角 表盘圆、进度弧
角度换算 值 → 比例 → 弧度 秒/分/时 → 指针角度

这四层从「点」到「线」到「弧」到「映射」,正好是极坐标由浅入深的学习路径。学完时钟,你再看任何圆形 UI(雷达图、环形菜单、表盘组件)都能直接上手。

9.2 「角度偏移 -π/2」的统一性

本应用所有角度公式都带 - π/2,这个偏移值得单独理解:

Canvas 0° = 3 点方向(正 x 轴)
表盘习惯 0° = 12 点方向(正 y 轴负方向)
两者相差 90° = π/2

所以「画表盘」=「把 Canvas 坐标系整体逆时针旋转 90°」。三种实现方式:

  1. 公式里减 π/2(本应用):每个角度计算时手动偏移,直观但容易漏;
  2. ctx.rotate(-π/2):用变换矩阵整体旋转坐标系,一劳永逸但影响后续所有绘制(需 save/restore);
  3. 预置角度表:把 12 个数字角度预先算好存常量,运行时只查表。

本应用选方案 1,因为它最直观且不引入变换状态管理。理解三种方案的取舍,你在不同场景就能选对。

9.3 从「静态图表」到「持续投影」的架构转变

3-1 篇强调图表是「数据域 → 像素域」的投影;本应用把这句话升级为「持续投影」:

静态图表:数据 → 投影一次 → 画面(除非用户操作,否则不变)
动态时钟:时间 → 每 250ms 投影一次 → 画面持续变化

架构上只多了一个「定时重投影」的驱动源(setInterval),绘制函数几乎没变——「画面 = 数据的函数」这个等式,加上时间维度后变成「画面 = 时间(数据)」。这是整个系列从「静态」走向「动态」的分水岭,也是第 5 个应用(粒子,60fps 持续投影)的预告。

十、设计决策复盘:本应用的五个关键选择

把本应用的设计决策集中复盘,每个选择都有明确的理由:

决策点 选择 理由
时钟刷新频率 250ms 秒针含毫秒,4fps 已平滑;再高浪费
仪表刷新频率 25ms 逼近动画需要更细粒度
画布数量 双画布 刷新频率差 10 倍,共用会互相拖累
时间显示 组件文字而非画布 文字刷新廉价、样式精致
暂停实现 清计时器 「时间即数据」,暂停只是停驱动源

其中「按刷新频率拆画布」和「高频文字交给组件」是两个最容易被忽视的架构决策——它们共同保证了「时钟 4fps + 仪表 40fps」共存时互不干扰、资源最优。

十一、动手练习

  1. 改刷新率:把 clockTimer 间隔从 250 改成 1000,观察秒针从「平滑扫秒」变成「每秒跳 6°」,理解刷新率与动画平滑度的关系;
  2. 改扫秒为跳秒:把 getMilliseconds()/1000 删掉,对比跳秒与扫秒的观感差异;
  3. 改表盘配色:把深色表盘(#1E293B)改成白色,同步调整刻度/数字/指针颜色,体会「颜色与逻辑解耦」;
  4. 加秒针尾翼:给分针也加尾翼(反向短针),对比真实时钟的分针设计;
  5. 单画布实验:把双画布合并成一块,观察「时钟每帧重绘仪表」的性能浪费与视觉干扰,验证「按频率拆画布」的必要性。

十二、进阶阅读:从时钟到更多圆形 UI

学完本应用的设计,你已经掌握「极坐标一公式」的完整用法。这里把它推广到更多真实场景,帮助你举一反三:

12.1 圆形 UI 的通用设计模式

任何圆形 UI 都逃不出这个模式:

1. 定义圆心 (cx, cy) 与半径 r
2. 定义「值 → 角度」的映射(比例 × 2π)
3. 用 cos/sin 把角度转成坐标
4. 绘制(点/线/弧/文字)

雷达图(每个维度一个角度)、环形菜单(每个菜单项一个角度)、仪表盘(数值映射角度)、环形进度(比例映射弧长)——全部是「值 → 角度 → 坐标」三步走。本应用的表盘只是这个模式的一个实例

12.2 三种「角度偏移」场景的对比
场景 偏移方式 说明
表盘 12 点起始 角度 - π/2 把 3 点方向挪到 12 点
仪表盘 -90° 起始 弧起点 -π/2 进度从顶部开始
雷达图对称分布 每个维度 angle = i/n×2π - π/2 维度均匀绕圈

三个场景的偏移都是为了「视觉起点对齐」,本质相同——先定起点方向,再按比例分布角度

12.3 时间的另一种表达:毫秒精度与日期

本应用展示了「时:分:秒」与「年-月-日 星期」两种时间粒度。真实应用中还有更多:

  • 毫秒级now.getMilliseconds() 用于秒表/动画精确同步;
  • 时区:世界时钟用 UTC 偏移 换算(如伦敦 = UTC+0,东京 = UTC+9);
  • 国际化:星期/月份的本地化文案(中文「星期一」vs 英文 “Monday”)。

时间的「数据模型」始终是系统时钟 + 格式化规则,画面只是它的投影——这再次印证「时间即数据」的设计。

12.4 本应用给粒子特效(应用 5)的启示

时钟应用的「持续投影」模型是粒子特效的雏形:

概念 时钟 粒子
驱动源 250ms 计时器 60fps 帧循环
数据 时间(s/m/h) 粒子数组(位置/速度)
重绘 整盘重绘 全屏重绘
资源 双计时器 对象池 + 帧循环

唯一的本质差异是数据量:时钟只有 3 个数据点(三针角度),粒子有数万个对象。数据量从「个位」到「万位」的跃迁,正是第 5 个应用要解决的性能问题——现在打好「持续投影」的心智基础,届时只需关注「如何让数万个对象的重绘跑进 16ms」。


表盘绘制:刻度、数字与表针的三角函数

一、目标

把一块「像样的表盘」画出来:深色表盘 + 60 刻度(整点长粗、分钟短细)+ 12 个数字 + 三根表针 + 中心轴。全部元素都靠极坐标公式定位。

1.1 表盘是「极坐标公式」的集大成者

前文说过「极坐标一公式走天下」,本节就是这句话的验证:表盘上的每个元素——刻度、数字、指针、中心轴——都是同一个公式 坐标 = 圆心 + 半径 × (cos角度, sin角度) 的实例,区别只在半径与角度。

元素 半径(决定远近) 角度(决定方向)
刻度内端 小半径(靠内) 刻度角度
刻度外端 大半径(贴边) 同一刻度角度
数字 中半径 数字角度
表针端点 各针专属半径 时间比例角度
中心轴 0(圆心)

绘制表盘 = 准备一堆「半径 + 角度」对,然后逐个套公式。理解这个视角,读代码时就不会迷失在几十行 cos/sin 里。

1.2 表盘的「信息分层」

表盘上的元素不是平铺的,它们有清晰的层次:外圈是参考系(刻度/数字),内圈是信息(指针)。这个分层决定了绘制顺序与半径分配:

最外层:表盘底(深色圆)     ← 背景
第二层:刻度环(60 条线)    ← 参考系之一
第三层:数字环(12 个字)    ← 参考系之二
第四层:三根表针            ← 核心信息
最内层:中心轴              ← 信息锚点

每个元素只在自己的层内绘制,不越层——这就是为什么表盘看起来「不乱」。真实时钟的表盘设计(劳力士、卡西欧)都遵循同样的分层原则,只是细节更多。

二、表盘底座

const size = this.clockSize;
const cx = size / 2;
const cy = size / 2;
const r = size / 2 - 12;   // 表盘半径(留 12vp 边距)
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.fillStyle = '#1E293B';   // 深蓝灰表盘
ctx.fill();
ctx.lineWidth = 2;
ctx.strokeStyle = '#334155';
ctx.stroke();

表盘即圆arc(0~2π) 填充深色 + 描边一圈略亮的同色系边线,产生「表壳内凹」的层次。深色表盘配白色刻度数字,对比清晰、夜间友好。

三、刻度:60 个循环的粗细分级

ctx.lineCap = 'round';
for (let i = 0; i < 60; i++) {
  const angle = i * Math.PI / 30 - Math.PI / 2;   // 每格 π/30(6°)
  const major = i % 5 === 0;                       // 整点刻度(每 5 格)
  const outer = r - 6;                             // 刻度外端
  const inner = outer - (major ? 14 : 6);          // 内端:整点更长
  ctx.beginPath();
  ctx.moveTo(cx + Math.cos(angle) * inner, cy + Math.sin(angle) * inner);
  ctx.lineTo(cx + Math.cos(angle) * outer, cy + Math.sin(angle) * outer);
  ctx.strokeStyle = major ? '#FFFFFF' : '#64748B';
  ctx.lineWidth = major ? 3 : 1.5;
  ctx.stroke();
}

刻度设计三要点

  1. 60 个刻度 = 12 整点 + 48 分钟点i % 5 === 0 判定整点——12 个整点刻度(白色、3vp、长 14),48 个分钟刻度(灰、1.5vp、长 6),主次分明
  2. 刻度是「放射线段」:从内半径到外半径画线段——内端短点、外端贴表盘边缘,看起来像「印在表盘上」而不是「悬在空中」;
  3. lineCap round:刻度两端圆头,精致感立刻提升——尖头刻度显廉价。

极坐标复用:内端/外端只有半径不同(inner/outer),角度相同(angle),cos/sin × 半径 就是两端坐标——一个角度、两个半径,画一条刻度

四、数字:12 个方向的 fillText

ctx.fillStyle = '#FFFFFF';
ctx.font = '15vp sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
for (let i = 1; i <= 12; i++) {
  const angle = i * Math.PI / 6 - Math.PI / 2;   // 每格 π/6(30°)
  const nr = r - 32;                             // 数字半径(比刻度内圈更靠里)
  ctx.fillText(String(i), cx + Math.cos(angle) * nr, cy + Math.sin(angle) * nr);
}

数字定位的关键textAlign='center' + textBaseline='middle'——文字以 (x, y) 为几何中心,配合半径 r−32 让数字环均匀分布在表盘内圈。若用默认 baseline(alphabetic),数字会整体偏上,12 与 6 的垂直感失衡。

数字半径比刻度内端更小(r−32 < r−20),数字环在刻度环内侧——从外到内:刻度 → 数字 → 表针,层次清晰。

五、三根表针:共享 drawHand

this.drawHand(ctx, cx, cy, h / 12 * Math.PI * 2, r * 0.45, '#FFFFFF', 6);  // 时针
this.drawHand(ctx, cx, cy, m / 60 * Math.PI * 2, r * 0.66, '#FFFFFF', 4);  // 分针

private drawHand(ctx: CanvasRenderingContext2D, cx: number, cy: number,
                 angle: number, len: number, color: string, width: number): void {
  ctx.beginPath();
  ctx.moveTo(cx, cy);
  ctx.lineTo(cx + Math.cos(angle) * len, cy + Math.sin(angle) * len);
  ctx.strokeStyle = color;
  ctx.lineWidth = width;
  ctx.lineCap = 'round';
  ctx.stroke();
}

表针 = 从圆心指向角度方向的一条粗线。参数化后三针共用一套代码,差异只有三个参数(角度/长度/粗细):

表针 角度 长度 宽度
时针 h/12 × 2π r×0.45 6
分针 m/60 × 2π r×0.66 4
秒针 s/60 × 2π r−18 1.8(红色)

长度递减原则:时针最短最粗(视觉锚点)、分针居中、秒针最长最细——表针长度与频率成反比,这是所有真实时钟的比例逻辑。

六、秒针与中心轴

// 秒针:红色 + 尾翼(越过圆心反向短针)
const sa = s / 60 * Math.PI * 2;
ctx.beginPath();
ctx.moveTo(cx - Math.cos(sa) * r * 0.12, cy - Math.sin(sa) * r * 0.12);  // 尾翼
ctx.lineTo(cx + Math.cos(sa) * (r - 18), cy + Math.sin(sa) * (r - 18));  // 针尖
ctx.strokeStyle = '#EF4444';
ctx.lineWidth = 1.8;
ctx.stroke();

// 中心轴:红底白芯
ctx.beginPath();
ctx.arc(cx, cy, 5, 0, Math.PI * 2);
ctx.fillStyle = '#EF4444';
ctx.fill();
ctx.beginPath();
ctx.arc(cx, cy, 2, 0, Math.PI * 2);
ctx.fillStyle = '#FFFFFF';
ctx.fill();

两个细节

  1. 秒针尾翼:起点不在圆心而在圆心反向 0.12r 处——秒针「穿过轴心」两端都有,模仿真实秒针的配重尾,指针不再像一根孤零零的短线
  2. 中心轴双层:红底(5vp)+ 白芯(2vp)——三根针的交汇点有「轴承感」,且白芯让红秒针的旋转中心有参照。

七、绘制顺序:后画者在上

表盘底 → 刻度 → 数字 → 时针 → 分针 → 秒针 → 中心轴

顺序即层级:后绘制的盖住先绘制的。秒针最后画(盖住分针),中心轴最后画(盖住秒针根部)——「针压针、轴压针」的物理遮挡关系用绘制顺序自然实现,无需任何 z-index 概念。

7.1 绘制顺序的「幂等性」要求

时钟每 250ms 全量重绘一次,这要求 drawClock 是幂等的:无论调用多少次、在什么时刻调用,画布结果都等于「当前时间的完整投影」。实现幂等的关键:

  1. 每次重绘先 clearRect:表盘、刻度、指针都要重画,不清屏会叠加残影;
  2. 所有元素从头画到尾:不能「上次画过这次就不画」——每帧都是完整的一幅表盘;
  3. 颜色/线宽在元素内设置:不依赖上一次循环遗留的状态(延续 3-2 篇的「状态显式复位」纪律)。

幂等性是「持续投影」架构的前提——因为计时器可能在任意时刻触发重绘,只有「每次重绘都完整」才能保证画面永远正确。

7.2 秒针的「压轴」与绘制开销

秒针是最后一根针(压住分针),意味着每次重绘都要把前面所有元素(表盘、60 刻度、12 数字、时针、分针)全部重画一遍。这笔开销大吗?

  • 60 条刻度线 + 12 个数字 + 5 个圆/线 ≈ 80 个绘制命令;
  • 每个命令都是几十纳秒级的路径构建,80 个 ≈ 微秒级;
  • 250ms 一帧,CPU 占用可以忽略。

「全量重绘」在元素量小时永远是最优解——只有元素量级上万(粒子特效)才需要考虑增量优化。时钟的表盘就是「全量重绘成本极低」的典型证明。

八、文章小结

本节实现了完整表盘:底座(深色圆)→ 刻度(60 循环 + 整点分级)→ 数字(center/middle 对齐 + 独立半径)→ 三针(参数化 drawHand + 长度频率反比)→ 秒针尾翼与中心轴(后画覆盖)。核心公式贯穿始终:坐标 = 圆心 + 半径 × (cos角度, sin角度)

下一节实现走时动画:setInterval 驱动、毫秒平滑扫秒与暂停恢复。

九、补充:FAQ

Q1:为什么数字从 1 开始而不是 0?
表盘只有 1~12,for i in 1..12 直接对应表盘数字。角度公式 i × π/6 在 i=12 时等于 2π(同 0°),12 点位置正确——i 即数字,角度自动对位

Q2:刻度为什么从表盘边缘 r−6 开始向内画?
外端固定贴边(r−6),内端按主次伸缩——「贴边」保证刻度环与表盘同心,视觉稳定;若外端也浮动,刻度环会出现「歪」的错觉。

Q3:秒针尾翼会不会超出表盘?
尾翼长 0.12r ≈ 15vp,远小于表盘半径,安全。真实时钟尾翼更长,本应用从简,视觉已足够。

十、深入:表盘绘制的三个「为什么」

10.1 为什么刻度要「放射状」而不是「垂直竖线」

刻度线的两种画法对比:

画法 实现 效果
放射状(本应用) 沿半径方向从内到外画线段 刻度线「指向圆心」,像真实表盘
垂直竖线 在刻度位置画垂直短竖线 像「卷尺」,缺乏表盘的向心感

放射状的几何本质是「刻度线段的两端在同一条半径上」——内端与外端的角度相同,只有半径不同。这种「同心放射」结构是表盘的视觉骨架,让人一眼识别「这是表盘」。

10.2 为什么用 i % 5 === 0 判定整点

60 个刻度中,第 0、5、10…55 是整点(对应 12、1、2…11),间隔正好 5 格。i % 5 === 0 是「60 刻度 + 12 整点」的最小公倍数关系:

60 / 12 = 5    → 每 5 格一个整点

这个取模判定的优点是零额外数组——不需要专门记录「哪些是整点」,数学关系天然表达。若改成 24 小时制(24 个整点),只需把取模从 5 改成 2.5(60/24),但 2.5 非整数,所以实际要换数据结构——取模只在「整除」时优雅

10.3 数字与刻度的「半径分层」

表盘元素从外到内的半径:

元素 半径 说明
刻度外端 r − 6 贴表盘边缘
刻度内端(分钟) r − 12 短刻度
刻度内端(整点) r − 20 长刻度
数字 r − 32 数字环
时针端点 r × 0.45 最短指针
分针端点 r × 0.66 中指针
秒针端点 r − 18 最长指针

半径分层 = 视觉层级:外层是「参考系」(刻度/数字),内层是「信息」(指针)。若把数字半径调大盖住刻度,表盘就「糊」了——每个元素各占一层,互不重叠,这是表盘美观的几何基础。

十一、动手练习

  1. 刻度密度实验:把 60 刻度改成 120(每格 π/60),观察表盘变得更密,思考「太密会怎样」(视觉噪音);
  2. 整点判定实验:把 i % 5 改成 i % 10,观察整点刻度变成每 10 格一个(6 个整点),理解取模与密度的关系;
  3. 数字半径实验:把数字半径从 r−32 改成 r−10(贴近刻度),观察数字与刻度重叠的混乱效果;
  4. 指针长度实验:把时针长度从 r×0.45 改成 r×0.8(比分针还长),观察「长度与频率反比」被打破后的视觉失衡;
  5. 线帽实验:去掉 lineCap = 'round',观察刻度从圆头变成平头的廉价感差异。

十二、本篇知识点串联

表盘绘制是「极坐标公式」的集中演练,它把 4-1 篇的「一公式走天下」变成了可运行的代码

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐