HTML & CSS

  1. <div class="clock">
  2. <canvas id="clock" width="500" height="500" />
  3. </div>
.clock {
  width: 500px;
  height: 500px;
  background-color: #202121;
  margin: 50px auto;
  border-radius: 100px;
}

JS 结构 和 获取Canvas 渲染环境

Canvas.getContext(contextID) 获取 canvas 渲染上下文

  • 参数 contextID
    • 目前只支持 ‘2d’
    • 返回 CanvasRenderingContext2D 对象 ```javascript (function () { let ctx = clock.getContext(“2d”); class Clock { init() { this.draw(); } draw() { } } window.Clock = Clock; })();

new Clock().init();

<a name="rm4xH"></a>
# 准备
Clock 类的 draw() 增加绘制面板的能力
```javascript
class Clock {
  init() {
    this.draw();
  }   
  draw() {
    this.drawPanel();
  }
  drawPanel() { 
  }
  window.Clock = Clock;
})();

画布宽高

通过上下文的 canvas 属性来获取
cWidth = ctx.canvas.width
cHeight = ctx.canvas.height

确定半径

半径为画布一半小 20px

class Clock{
  constructor() {
    this.r = cWidth / 2 - 20;
  }

  // ...
}

开始一个新路径

CanvasRenderingContext2D.beginPath()

擦除一个矩形区域

CanvasRenderingContext2D.clearRect(x, y, width, height)

画线

两点确定一条线
CanvasRenderingContext2D.moveTo(x, y)
CanvasRenderingContext2D.lineTo(x, y)

画圆弧

CanvasRenderingContext2D.arc()
void ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise)

  • 参数

    • x 圆弧中心(圆心)的 x 轴坐标。
    • y 圆弧中心(圆心)的 y 轴坐标。
    • radius 圆弧的半径。
    • startAngle 圆弧的起始点,x轴方向开始计算,单位以弧度表示。
    • endAngle 圆弧的终点,单位以弧度表示。
    • anticlockwise 可选的Boolean值 ,如果为 true,逆时针绘制圆弧,反之,顺时针绘制。

      图形绘画

      stroke 描边
      CanvasRenderingContext2D.strokeStyle 描边样式
      CanvasRenderingContext2D.stroke() 描边
      fill 填充
      CanvasRenderingContext2D.fillStyle 填充样式
      CanvasRenderingContext2D.fill() 填充

      画文字

      CanvasRenderingContext2D.strokeText() 描边文字
      CanvasRenderingContext2D.fillText() 填充文字
      void ctx.fillText(text, x, y, [maxWidth]);
  • 参数

    • text 使用当前的 font, textAlign, textBaseline 和 direction 值对文本进行渲染。
    • x 文本起点的 x 轴坐标。
    • y 文本起点的 y 轴坐标。
    • maxWidth 可选 绘制的最大宽度。如果指定了值,并且经过计算字符串的值比最大宽度还要宽,字体为了适应会水平缩放(如果通过水平缩放当前字体,可以进行有效的或者合理可读的处理)或者使用小号的字体。

      文字大小 / 字体

      CanvasRenderingContext2D.font = “40px sans-serif”
      CanvasRenderingContext2D.textAlign = “center”
      CanvasRenderingContext2D.textBaseline = “middle”

画布平移

CanvasRenderingContext2D.translate(x, y)

画布旋转

CanvasRenderingContext2D.rotate(angle)

保存 / 恢复 canvas 全部状态

CanvasRenderingContext2D.save()
CanvasRenderingContext2D.restore()


绘制

底板

this.drawPanel()
ctx.translate 平移画布,把时钟的中心设置为画布中心
使用画圆弧的方法,以填充方式在画布的中心点画一个圆

class Clock {
  constructor() {
    this.r = cWidth / 2 - 20;
  }

  init() {
    this.draw();
  }

  draw() {
    this.drawPanel();
  }

  drawPanel() {
    ctx.beginPath();
    ctx.translate(cWidth / 2, cWidth / 2);
    ctx.fillStyle = "#fff";
    ctx.arc(0, 0, this.r, 0, 2 * Math.PI);
    ctx.fill();
  }
}

面板数字

this.drawHourNums
使用填充文字的方法把小时数字绘制于画布上
但先要确定先个数字位置,把一个圆分为 12 等份,求出每个等份的圆弧的点的坐标
弧度角 k 为每一等份

hours = Array(12)
  .fill(undefined)
  .map((_, index) => index + 1); // [1,2,3...12] 的数组

drawHourNums() {
  ctx.font = "40px sans-serif";
  ctx.textAlign = "center";
  ctx.textBaseline = "middle";
  ctx.fillStyle = "#000";

  hours.forEach((text, k) => {
    let rad = ((2 * Math.PI) / 12) * (k - 2); 
    // 因为在 x 轴开始(3点的位置) k-2 让开始的 1 回到时钟的位置
    let x = Math.cos(rad) * (this.r - 40); 
    let y = Math.sin(rad) * (this.r - 40);
    ctx.beginPath();
    ctx.fillText(text, x, y);
  }); 
}

面板中点

drawCentralPointer() {
  ctx.beginPath();
  ctx.fillStyle = "#666";
  ctx.arc(0, 0, 13, 0, 2 * Math.PI);
  ctx.fill();

  ctx.beginPath();
  ctx.fillStyle = "#333";
  ctx.arc(0, 0, 6, 0, 2 * Math.PI);
  ctx.fill();
}

时分秒针

利用 moveTo lineTo 画出来

drawHourIndicator() {
  ctx.beginPath();
  ctx.lineWidth = 5;
  ctx.lineCap = "round";
  ctx.moveTo(0, 0);
  ctx.lineTo(0, -this.r / 1.8);
  ctx.stroke();
}
drawMinuteIndicator() {
  ctx.beginPath();
  ctx.lineWidth = 5;
  ctx.lineCap = "round";
  ctx.moveTo(0, 0);
  ctx.lineTo(0, -this.r / 1.5);
  ctx.stroke();
}
drawSecondIndicator() {
  ctx.beginPath();
  ctx.strokeStyle = "orange";
  ctx.lineWidth = 5;
  ctx.lineCap = "round";
  ctx.moveTo(0, 0);
  ctx.lineTo(0, -this.r / 1.2);
  ctx.stroke();
}

动起来

显示当前时间

封装一个获取当前时间函数 getTime(),返回一个带有时分秒属性的对象

function getTime() {
  let d = new Date();
  return {
    hours: d.getHours(),
    minutes: d.getMinutes(),
    seconds: d.getSeconds()
  };
}

然后在绘制时分秒时,把对应的值传入相应的绘制函数

draw() {
  const { hours, minutes, seconds } = getTime();
  this.drawPanel();
  this.drawHourNums();
  this.drawHourIndicator(hours, minutes);
  this.drawMinuteIndicator(minutes);
  this.drawSecondIndicator(seconds);
  this.drawCentralPointer();
}

修改,能按传入的参数绘制指针。通过 rotate 旋转其角度

drawHourIndicator(hours, minutes) { // 时针是受, 时和分影响
  const rad = ((2 * Math.PI) / 12) * hours,
        mRad = ((2 * Math.PI) / 12 / 60) * minutes;
  ctx.save();
  ctx.beginPath();
  ctx.lineWidth = 5;
  ctx.lineCap = "round";
  ctx.rotate(rad + mRad);
  ctx.moveTo(0, 0);
  ctx.lineTo(0, -this.r / 1.8);
  ctx.stroke();
  ctx.restore();
}
drawMinuteIndicator(minutes) {
  const rad = ((2 * Math.PI) / 60) * minutes;
  ctx.save(); // 先保存 canvas 的状态
  ctx.beginPath();
  ctx.lineWidth = 5;
  ctx.lineCap = "round";
  ctx.rotate(rad); // 要在绘制之前先旋转
  ctx.moveTo(0, 0);
  ctx.lineTo(0, -this.r / 1.5);
  ctx.stroke();
  ctx.restore(); // 完成后恢复
}
drawSecondIndicator(seconds) {
  const rad = ((2 * Math.PI) / 60) * seconds;
  ctx.save();
  ctx.beginPath();
  ctx.strokeStyle = "orange";
  ctx.lineWidth = 5;
  ctx.lineCap = "round";
  ctx.rotate(rad);
  ctx.moveTo(0, 0);
  ctx.lineTo(0, -this.r / 1.2);
  ctx.stroke();
  ctx.restore();
}

动起来

设置 setInterval 使其每秒重绘一次,
重绘前用 clearRect 画布透明,也要结合 save 和 restore 对画布的状态做处理

let ctx = clock.getContext("2d"),
    cWidth = ctx.canvas.width,
    cHeight = ctx.canvas.height,
    hours = Array(12)
.fill(undefined)
.map((_, index) => index + 1),
    t;

init() {
  this.draw();
  t = setInterval(this.draw.bind(this), 1000);
}

draw() {
  ctx.clearRect(0, 0, cWidth, cHeight);
  const { hours, minutes, seconds } = getTime();

  ctx.save();
  this.drawPanel();
  this.drawHourNums();
  this.drawHourIndicator(hours, minutes);
  this.drawMinuteIndicator(minutes);
  this.drawSecondIndicator(seconds);
  this.drawCentralPointer();
  ctx.restore();
}

最终效果

点击查看【codepen】