Skip to content

时钟指针

在时钟盘面和数字都绘制完成后,最后一步是绘制时钟指针:时针、分针和秒针。我们将创建 JavaScript 函数来根据当前时间计算它们的位置并在 canvas 上渲染它们。

现在,主要的 drawClock 函数将协调绘制盘面、数字和时间。drawTime 函数将获取当前时间,并在计算出每个指针的角度后,调用 drawHand 函数来绘制它们。drawHand 函数是一个辅助函数,用于绘制具有特定长度和宽度的线条,代表时钟指针。

// 假设 drawFace(ctx, radius) 和 drawNumbers(ctx, radius) 已在前面的部分中定义。
function drawClock(ctx, radius) {
// 清除 canvas 以进行重绘(对于动画很重要,对于静态绘制也是很好的做法)
ctx.clearRect(-radius, -radius, canvas.width, canvas.height);
drawFace(ctx, radius);
drawNumbers(ctx, radius);
drawTime(ctx, radius);
}
function drawTime(context, clockRadius) {
const now = new Date();
let hour = now.getHours();
let minute = now.getMinutes();
let second = now.getSeconds();
// 计算时针的角度
hour = hour % 12; // 转换为 12 小时制
// 时针会根据分钟和秒钟按比例移动
const hourAngle = (hour * Math.PI / 6) +
(minute * Math.PI / (6 * 60)) +
(second * Math.PI / (360 * 60));
drawHand(context, hourAngle, clockRadius * 0.5, clockRadius * 0.07);
// 计算分针的角度
// 分针会根据秒钟按比例移动
const minuteAngle = (minute * Math.PI / 30) +
(second * Math.PI / (30 * 60));
drawHand(context, minuteAngle, clockRadius * 0.8, clockRadius * 0.07);
// 计算秒针的角度
const secondAngle = (second * Math.PI / 30);
drawHand(context, secondAngle, clockRadius * 0.9, clockRadius * 0.02);
}
function drawHand(context, positionAngle, length, lineWidth) {
context.beginPath();
context.lineWidth = lineWidth;
context.lineCap = "round"; // 使指针尖端变圆润
context.moveTo(0, 0); // 从时钟中心开始绘制
// 将 canvas 旋转到指针的角度
// 注意:角度 0 是 3 点钟方向。要使角度 0 为 12 点钟方向,我们通常会减去 Math.PI / 2。
// 然而,我们的计算是直接映射的:角度 0 对应 12 点,PI/2 对应 3 点,PI 对应 6 点,3PI/2 对应 9 点。
// 并且 lineTo(0, -length) 在旋转后的上下文中向上绘制。
// 因此,正角度表示逆时针旋转。lineTo(0, -length) 在新方向上“向上”绘制。
context.rotate(positionAngle);
context.lineTo(0, -length); // 在旋转后的上下文中向上绘制指针
context.stroke(); // 渲染指针
// 将 canvas 旋转回原始方向
context.rotate(-positionAngle);
// 使用 save() 和 restore() 的替代方法:
// context.save();
// context.rotate(positionAngle);
// context.beginPath();
// context.moveTo(0,0);
// context.lineTo(0, -length);
// context.lineWidth = lineWidth;
// context.lineCap = "round";
// context.stroke();
// context.restore();
}
  1. drawClock(ctx, radius):此函数现在是主要的绘图程序。在绘制之前,可以使用 ctx.clearRect() 清除前一帧,这对于动画至关重要。对于静态时钟,如果多次调用此函数,它能确保每次绘制都是干净的。

  2. drawTime(context, clockRadius):

  • 使用 new Date() 获取当前时间。
  • 将小时转换为 12 小时制(hour % 12)。
  • 计算每个指针的角度。角度以弧度为单位:
  • 一个整圆是 2 * Math.PI 弧度。
  • 时针:(hour * Math.PI / 6):有 12 个小时,因此每个小时是 (2 * Math.PI) / 12 = Math.PI / 6 弧度。为了确保平滑移动,会加上分钟和秒钟产生的微小角度。
  • 分针:(minute * Math.PI / 30):有 60 分钟,因此每分钟是 (2 * Math.PI) / 60 = Math.PI / 30 弧度。会加上秒钟产生的微小角度。
  • 秒针:(second * Math.PI / 30):与分针类似,每秒是 Math.PI / 30 弧度。
  • 为每个指针调用 drawHand(),提供计算出的角度、长度(相对于 clockRadius)和宽度(也相对于 clockRadius)。
const now = new Date();
let hour = now.getHours();
let minute = now.getMinutes();
let second = now.getSeconds();
hour = hour % 12;
// 示例:时针计算包含分钟和秒钟产生的微小角度,以实现平滑移动
const hourAngle = (hour * Math.PI / 6) +
(minute * Math.PI / (6 * 60)) +
(second * Math.PI / (360 * 60));
drawHand(ctx, hourAngle, radius * 0.5, radius * 0.07);
  1. drawHand(context, positionAngle, length, lineWidth):
  • context.beginPath():为指针开始一个新的绘图路径。
  • context.lineWidth 和 context.lineCap = "round":设置指针线的粗细和样式。
  • context.moveTo(0, 0):将线的起点设置为时钟的中心(在我们平移后的坐标系中,中心是 (0,0))。
  • context.rotate(positionAngle):将整个 canvas 坐标系旋转 positionAngle 度。随后的绘图操作将相对于这个新的方向进行。
  • context.lineTo(0, -length):从当前点 (0,0) 绘制一条线到 (0, -length)。在旋转后的系统中,这相当于沿着新的 y 轴向上绘制指针。
  • context.stroke():在 canvas 上渲染这条线。
  • context.rotate(-positionAngle):将 canvas 旋转回相同的角度。这非常重要,以确保绘制下一个指针(或其他绘图操作)时,从原始的、未旋转的方向开始。隔离变换的一种替代且通常更受欢迎的方法是在变换之前使用 context.save(),之后使用 context.restore()。

这就完成了模拟时钟的静态绘制。要使其成为一个实时时钟,您通常会使用 setInterval() 或 requestAnimationFrame() 每秒调用 drawClock() 函数。您可以在此处了解更多关于 JavaScript 动画的信息。

// 此部分的完整示例脚本:
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let radius = canvas.height / 2; // 将原点移动到中心
ctx.translate(radius, radius); // 应用内边距
radius *= 0.90;
// 假设 drawFace 和 drawNumbers 已定义(为方便独立示例,从前面的部分复制而来)
function drawFace(context, r) {
context.beginPath(); // 为主圆开始一个新路径
context.arc(0, 0, r, 0, 2 * Math.PI); // 绘制时钟盘面的外圆
context.fillStyle = 'white';
context.fill();
// 时钟边框
context.strokeStyle = '#333'; // 边框颜色
context.lineWidth = r * 0.1; // 边框宽度相对于半径
context.stroke();
// 中心圆点
context.beginPath(); // 为中心圆点开始一个新路径
context.arc(0, 0, r * 0.05, 0, 2 * Math.PI);
context.fillStyle = '#333'; // 中心圆点颜色
context.fill();
}
function drawNumbers(context, r) {
context.font = `${r * 0.15}px Arial`;
context.textBaseline = "middle";
context.textAlign = "center";
for (let num = 1; num <= 12; num++) {
const angle = num * Math.PI / 6;
context.rotate(angle);
context.translate(0, -r * 0.85);
context.rotate(-angle); // 旋转回以竖直绘制数字
context.fillText(num.toString(), 0, 0);
context.rotate(angle); // 旋转到原始位置以绘制下一个数字
context.translate(0, r * 0.85); // 平移回中心
context.rotate(-angle); // 将系统旋转回 0 以绘制下一个数字
}
}
// 本课的函数
function drawTime(context, clockRadius) {
const now = new Date();
let hour = now.getHours();
let minute = now.getMinutes();
let second = now.getSeconds();
hour = hour % 12;
const hourAngle = (hour * Math.PI / 6) +
(minute * Math.PI / (6 * 60)) +
(second * Math.PI / (360 * 60));
drawHand(context, hourAngle, clockRadius * 0.5, clockRadius * 0.07);
const minuteAngle = (minute * Math.PI / 30) +
(second * Math.PI / (30 * 60));
drawHand(context, minuteAngle, clockRadius * 0.8, clockRadius * 0.07);
const secondAngle = (second * Math.PI / 30);
drawHand(context, secondAngle, clockRadius * 0.9, clockRadius * 0.02, 'red'); // 可选:秒针颜色
}
function drawHand(context, positionAngle, length, lineWidth, color = '#333') {
context.beginPath();
context.lineWidth = lineWidth;
context.lineCap = "round";
context.strokeStyle = color; // 允许自定义指针颜色
context.moveTo(0, 0);
context.rotate(positionAngle);
context.lineTo(0, -length);
context.stroke();
context.rotate(-positionAngle);
context.strokeStyle = '#333'; // 如果改变了 strokeStyle,则重置
}
// 主绘图函数
function drawClock() {
// 清除前一帧(对于静态可选,对于动画必需)
// ctx.clearRect(-radius - ctx.lineWidth, -radius - ctx.lineWidth, canvas.width, canvas.height);
// 考虑到 canvas 已被平移,进行完整的清除:
ctx.save();
ctx.setTransform(1, 0, 0, 1, 0, 0); // 重置变换为默认值
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.restore(); // 恢复变换,原点在中心
drawFace(ctx, radius);
drawNumbers(ctx, radius);
drawTime(ctx, radius);
}
drawClock(); // 绘制时钟一次
// 对于实时时钟,每秒更新:
// setInterval(() => drawClock(), 1000);
// 或者使用 requestAnimationFrame 实现更平滑的浏览器动画:
// function runClock() {
// drawClock();
// requestAnimationFrame(runClock);
// }
// requestAnimationFrame(runClock);