时钟指针
现代 Canvas 时钟:绘制指针
Section titled “现代 Canvas 时钟:绘制指针”第四部分:绘制时钟指针
Section titled “第四部分:绘制时钟指针”在时钟盘面和数字都绘制完成后,最后一步是绘制时钟指针:时针、分针和秒针。我们将创建 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();}-
drawClock(ctx, radius):此函数现在是主要的绘图程序。在绘制之前,可以使用ctx.clearRect()清除前一帧,这对于动画至关重要。对于静态时钟,如果多次调用此函数,它能确保每次绘制都是干净的。 -
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);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);