上班没事儿干?用 canvas 写个时钟

1,159 阅读6分钟

前言

已经半个月没干活了,每天都在掘金里混日子,最近对 canvas 突然来了兴趣,看到大佬写的那种优美的效果,我也忍不住想整一个。

没想到花了我整整一个工作日的时间,才令我满意。

src=http___mmbiz.qpic.cn_mmbiz_kcQ1DgficQbXcs1ZIvmQDS402LC92QGWgjHInpEBQKlJ2cSA2vTfrBBBznk4b3Lg5UnGe4XKVHQz8CjANDaXmRA_0_wx_fmt=gif&refer=http___mmbiz.qpic.gif

思路分解

我们看到这个时钟拥有边框,刻度,数字,指针四部分组成,首先指针是动态的,那必然得用 canvas 来绘制,毋庸置疑的吧;

其次边框,刻度,数字都是静止的,边框很简单,用 html + css 就能实现,刻度和数字也能用绝对定位实现,但刻度和数字那么多,得写多少 css 的绝对定位啊,那不得累成狗。因此,数字和刻度也使用 canvas 来绘制。

绘制边框

思路很明确了,创建边框使用 html + css

  1. 背景要设置成黑色,程序猿最爱的黑底,没毛病吧;
  2. 需要两个边框容器,一个外边框,一个内边框,使用 border-radius 等于 50% 的宽高让容器成为圆形;
  3. 我们注意到这里的边框是有阴影的,可以使用 box-shadow 创建一个边框阴影,外边框就用外阴影,内边框就用内阴影。 很简单吧,那看看代码。
// css
    <style>
      html,
      body {
        overflow: hidden;
      }
     .canvasPlay {
        width: 100vw;
        height: 100vh;
        display: flex;
        align-items: center;
        justify-content: center;
        flex-direction: column;
        background-color: black;
        overflow: hidden;
      }
      .canvasPlayWrapper {
        transform: scale(0.5, 0.5);
        width: 440px;
        height: 440px;
        min-width: 440px;
        min-height: 440px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        border-radius: 50%;
        background-color: white;
        box-shadow: 0px 0px 24px gray;
      }
      .canvasTarget {
        box-shadow: 0px 0px 24px gray inset; 
        background-color: white;
        border-radius: 50%;
      }
    </style>
    
// html
    <div class="canvasPlay">
      <div class="canvasPlayWrapper">
        <canvas
          id="canvasClock"
          width="400px"
          height="400px"
          class="canvasTarget"
        ></canvas>
      </div>
    </div>

至此,我们就创建了一个好看的环形边框了,来看看效果。

D0E6386F-C3E9-482D-AEAC-8E8322EAE0B5.png

创建 canvas 并定位

接下来是关键了,我们要获取到 canvas 的节点,然后在这个节点上绘制我们的图形。接下来定位到 canvas 中心点作为坐标原点,并在这个原点绘制一个实心圆,作为钟表的束缚点(这里我不知道叫啥[捂脸]),直接上代码。

/**
   * @desc: 创建中心点
   * @param {*}
   * @return {*}
   */
  const createCenterPointer = (ctx) => {
    // 画中间的小圆
    ctx.beginPath();
    ctx.arc(0, 0, 3, 0, 2 * Math.PI);
    ctx.stroke();
    ctx.fillStyle = "black";
    ctx.fill();
    ctx.closePath();
  };
      
const canvasTarget = document.getElementById("canvasClock");
if (canvasTarget) {
  const ctx = canvasTarget.getContext("2d");
  // 设置中心点,此时100,100变成了坐标的0,0
  ctx.translate(100, 100);
  createCenterPointer(ctx);
}

现在看看效果咋样啦~

C976EAB8-0D53-46D7-9CB5-70BC1196DA36.png

像不像是多了一颗痣 😂!

绘制 刻度 & 数字

作为一个完美的时钟,当然要支持刻度和数字两种视觉参考啦。

绘制刻度

这里我们看到,刻度的话分成大小两种刻度,大刻度分成12份,小刻度分成60份,这样我们很容易想到,得分两次遍历绘制才行,然后用到一个很重要的偏转角度的函数 - rotate。 小刻度宽度为1,我们使用 canvas 的画线函数便很容易实现,配合 rotate 函数偏转对应的值就好了,看看代码:

ctx.lineWidth = 1;
for (let i = 0; i < 60; i += 1) {
  ctx.rotate((2 * Math.PI) / 60);
  ctx.beginPath();
  ctx.moveTo(90, 0);
  ctx.lineTo(100, 0);
  ctx.stroke();
  ctx.closePath();
}

接下来绘制大刻度,会稍微难一点,因为大刻度是一个不规则形状,就像下面这个图一样

A373A1B3-63D9-4117-AB52-5090DEC9A38B.png

把这个形状画好之后再给它填充一下颜色,就得到了单个刻度,然后使用 rotate 函数偏转对应的值就好了,看看代码:

ctx.lineWidth = 1;
for (let i = 0; i < 12; i += 1) {
  ctx.rotate((2 * Math.PI) / 12);
  ctx.beginPath();
  ctx.lineTo(89, 0);
  ctx.lineTo(90, 2);
  ctx.lineTo(100, 2);
  // ctx.lineTo(100, 0);
  // 画圆线使用arc(中心点X,中心点Y,半径,起始角度,结束角度)
  ctx.arc(0, 0, 100, 0, (2 * Math.PI) / 250);
  ctx.lineTo(100, -2);
  ctx.lineTo(90, -2);
  ctx.lineTo(89, 0);
  ctx.stroke();
  ctx.fillStyle = "black";
  ctx.fill();
  ctx.closePath();
}

那么问题来了,当我们画完小刻度之后,这时候x轴已经发生了偏移,这时候肯定要让x轴恢复到原来的模样,我们才能保证接下来绘制大刻度的时候不受影响。

那么save和restore就派上用场了,save是把ctx当前的状态打包压入栈中,restore是取出栈顶的状态并赋值给ctxsave可多次,但是restore取状态的次数必须等于save次数,那么看看完整的函数吧。

/**
   * @desc:  绘制刻度
   * @param {*}
   * @return {*}
   */
  const drawScale = (ctx) => {
    // 保存上一次的状态
    ctx.save();
    // 绘制刻度,也是跟绘制时分秒针一样,只不过刻度是死的
    ctx.lineWidth = 1;
    for (let i = 0; i < 60; i += 1) {
      ctx.rotate((2 * Math.PI) / 60);
      ctx.beginPath();
      ctx.moveTo(90, 0);
      ctx.lineTo(100, 0);
      ctx.stroke();
      ctx.closePath();
    }
    // 恢复成上一次保存的状态
    ctx.restore();
    ctx.save();

    ctx.lineWidth = 1;
    for (let i = 0; i < 12; i += 1) {
      ctx.rotate((2 * Math.PI) / 12);
      ctx.beginPath();
      ctx.lineTo(89, 0);
      ctx.lineTo(90, 2);
      ctx.lineTo(100, 2);
      // ctx.lineTo(100, 0);
      // 画圆线使用arc(中心点X,中心点Y,半径,起始角度,结束角度)
      ctx.arc(0, 0, 100, 0, (2 * Math.PI) / 250);
      ctx.lineTo(100, -2);
      ctx.lineTo(90, -2);
      ctx.lineTo(89, 0);
      ctx.stroke();
      ctx.fillStyle = "black";
      ctx.fill();
      ctx.closePath();
    }
    ctx.restore();
  };

看看效果咋样~

6C8F8864-4005-4E53-B803-85EE578226C9.png

绘制数字

数字怎么整上去?这里我们要用 canvas 绘制文本,就涉及到两个核心的方法。

  • font - 定义字体
  • fillText(text,x,y) - 在 canvas 上绘制实心的文本 这就简单了,方法如下:
  1. 数字时钟为 1-12,很明显用 for 循环绘制即可,遍历使用 fillText 的方法绘制,text 就用遍历 index 即可;
  2. x,y 的坐标怎么定位?把我们的初中知识用起来吧 - 三角函数,使用 radius * sin(偏移角度) 就得到了 x 轴的位置,使用 radius * cos(偏移角度);
  3. 由于数字本身具有宽高,因此,x 和 y 的位置需要在本身的基础上减去 数字宽高 / 2
  4. 为了保证函数之间的状态不受影响,所以我们同样要为函数加入一对 save和restore;

话不多说,上代码

/**
   * @desc:  绘制数字
   * @param {*}
   * @return {*}
   */
  const drawScaleNumber = (ctx) => {
    ctx.save();
    // 绘制刻度,也是跟绘制时分秒针一样,只不过刻度是死的
    ctx.lineWidth = 1;
    const textRadius = 80; // 设置文字半径为80,与边界相差20个像素
    for (let i = 0; i < 12; i += 1) {
      ctx.font = "16px Arial";
      ctx.fillText(
        i + 1,
        textRadius * Math.sin((Math.PI * (i + 1)) / 6) -
          (Math.ceil(i / 8) * 8) / 2,
        -(textRadius * Math.cos((Math.PI * (i + 1)) / 6) - 12 / 2)
      );
    }
    ctx.restore();
  };

这块的三角函数需要自己画图才能理解,还有就是文字偏移量有时候不一定准,所以具体需要调~

还是看看效果吧~

6FED3911-7833-467A-9BC2-96EF1581C405.png

嗯,还算可以吧!自吹一波。

绘制指针

在我的心中,指针是个六边菱形,就像下面这样

4E01E308-AF0C-43F3-B6DB-A108DA08EE85.png

因此,方案来了。

  1. 使用 canvas 的 moveTo&lineTo 的画线工具,然后填充一下颜色;
  2. 使用 shadowColor 为指针添加阴影,这样显得有层级感;
  3. 获取系统时间,计算指针偏转的角度,使用 rotate 实现角度偏转;
  4. 由于每次绘制指针之后 x 轴都会发生偏移,所以在绘制完成之后都需要使用 save 保存状态,并使用 restore 取出上一次的状态。

很明显,计算指针角度是最复杂的,我们拿时针举例,时针总共被分成12份,那么 (2 * Math.PI) / 12) * hour 就是当前时针转的角度,当然,还得加上分针所转过的角度才能是时针整体应该转过的角度,所以应该再加上 (2 * Math.PI) / 12) * (min / 60) ,由于坐标系和我们的视野正好颠倒了 180°,所以还应该再减去 Math.PI / 2,因此我们就得到了

  • 时针转过的角度为 ((2 * Math.PI) / 12) * (hour + min / 60) - Math.PI / 2;
  • 同理,我们得到了分针转过的角度为((2 * Math.PI) / 60) * (min + sec / 60) - Math.PI / 2;
  • 秒针转过的角度为((2 * Math.PI) / 60) * (sec - 1 + milliSec / 1000) - Math.PI / 2;

看看代码吧:

/**
   * @desc: 获取当前时间
   * @param {*}
   * @return {*}
   */
  const getCurrentTime = () => {
    // 获取当前时,分,秒,毫秒
    const time = new Date();
    const hour = time.getHours() % 12;
    const min = time.getMinutes();
    const sec = time.getSeconds();
    const milliSec = time.getMilliseconds();
    return {
      hour,
      min,
      sec,
      milliSec,
    };
  };
/**
   * @desc: 创建指针
   * @param {*}
   * @return {*}
   */
  const createPointer = (ctx) => {
    const { hour, min, sec, milliSec } = getCurrentTime();
    // 保存上一次的状态
    ctx.save();
    // 时针
    ctx.rotate(((2 * Math.PI) / 12) * (hour + min / 60) - Math.PI / 2);
    ctx.beginPath();
    // moveTo设置画线起点
    ctx.moveTo(-3, 0);
    // lineTo设置画线经过点
    ctx.lineTo(0, 3);
    ctx.lineTo(45, 3);
    ctx.lineTo(50, 0);
    ctx.lineTo(45, -3);
    ctx.lineTo(0, -3);
    ctx.lineTo(-3, 0);
    // 设置线宽
    ctx.lineWidth = 1;
    ctx.strokeStyle = "black";
    ctx.stroke();

    // 创建黑色阴影,模糊级数是 4
    ctx.shadowBlur = 4;
    ctx.shadowColor = "black";
    // 填充颜色
    ctx.fillStyle = "black";
    ctx.fill();

    ctx.closePath();
    // 恢复成上一次保存的状态
    ctx.restore();
    ctx.save();

    // 分针
    ctx.rotate(((2 * Math.PI) / 60) * (min + sec / 60) - Math.PI / 2);
    ctx.beginPath();
    // moveTo设置画线起点
    ctx.moveTo(-2, 0);
    // lineTo设置画线经过点
    ctx.lineTo(0, 2);
    ctx.lineTo(52, 2);
    ctx.lineTo(60, 0);
    ctx.lineTo(52, -2);
    ctx.lineTo(0, -2);
    ctx.lineTo(-2, 0);
    ctx.lineWidth = 1;
    ctx.strokeStyle = "#1e80ff";
    ctx.stroke();
    // 创建蓝色阴影,模糊级数是 3
    ctx.shadowBlur = 3;
    ctx.shadowColor = "#1e80ff";
    // 填充颜色
    ctx.fillStyle = "#1e80ff";
    ctx.fill();
    ctx.closePath();
    ctx.restore();
    ctx.save();

    // 秒针
    ctx.rotate(
      ((2 * Math.PI) / 60) * (sec - 1 + milliSec / 1000) - Math.PI / 2
    );
    ctx.beginPath();
    // moveTo设置画线起点
    ctx.moveTo(-1, 0);
    ctx.lineTo(0, 1);
    ctx.lineTo(60, 1);
    ctx.lineTo(70, 0);
    ctx.lineTo(60, -1);
    ctx.lineTo(0, -1);
    ctx.lineTo(-1, 0);
    ctx.strokeStyle = "#e9686b";
    ctx.stroke();
    // 创建红色阴影,模糊级数是 2
    ctx.shadowBlur = 2;
    ctx.shadowColor = "#e9686b";
    // 填充颜色
    ctx.fillStyle = "#e9686b";
    ctx.fill();
    ctx.closePath();

    ctx.restore();
  };

绘制完成之后,我们的静态时钟也就完成了,看看效果~

image.png

渐变边框

最开始我们使用了 css 的 box-shadow 绘制了边框,但我们也可以使用 canvas 的渐变属性绘制一个边框,canvas 渐变可以填充在矩形, 圆形, 线条, 文本等等, 各种形状可以自己定义不同的颜色。

以下有两种不同的方式来设置Canvas渐变:

  • createLinearGradient(x,y,x1,y1) - 创建线条渐变
  • createRadialGradient(x,y,r,x1,y1,r1) - 创建一个径向/圆渐变

到这里很明显,我们能使用径向/圆渐变实现阴影的效果,上代码:

/**
   * @desc: 创建渐变内环
   * @param {*}
   * @return {*}
   */
  const createRadialGradient = (ctx) => {
    // 创建渐变
    const grd = ctx.createRadialGradient(0, 0, 0, 0, 0, 100);
    grd.addColorStop(0, "white");
    grd.addColorStop(0.92, "white");
    grd.addColorStop(0.99, "#BDBDBD");
    grd.addColorStop(1, "#D8D8D8");

    // 填充渐变
    ctx.fillStyle = grd;
    ctx.arc(0, 0, 100, 0, 2 * Math.PI);
    ctx.fill();
  };

经过对比发现,这样的边框效果并没有 css 的 box-shadow 美观,因此我们依然选择 css 来绘制边框。

让指针运动起来

当我们把时钟都绘制完成了,最后一步就是让它动起来,也就是要更新视图,需要注意的是,每次更新视图的时候都要把上一次的画布清除,再开始画新的视图,不然就会出现千手观音的景象, 更新视图我们可以使用两种方案:

    1. 定时器setInterval,以固定频率刷新视图;
    1. 使用window.requestAnimationFrame,以浏览器内置频率刷新视图;

用过这两种方案的小伙伴应该很清楚,setInterval 并不能和浏览器刷新频率统一,更新频率过快造成浪费,更新过慢会导致视觉卡顿,呈现效果并不好,因此我们选择 window.requestAnimationFrame 来更新视图。 代码如下:

// 渲染函数
const animloop = () => {
  drawClock(ctx, canvasTarget);
  // console.log('canvas');
  requestAnimationFrame(animloop);
};
animloop();

这样我们的时钟就动起来了,配上一张 gif 来表示一下效果吧。

防止视图放大之后模糊

用 canvas 绘图有个很明显的缺点就是 canvas 被放大之后,图像会变得模糊起来,这是由于设备独立像素和物理像素不一致,设备像素比=物理像素/设备独立像素,大部分设备像素比为2,这意味着100px的图像要放在200px中才可以正常显示,因此,我们需要

  1. 先把 canvas 图像放大两倍,用 ctx.scale(2, 2) 便可以实现;
  2. 然后将canvas 外部容器缩小两倍,用 css 的 transform: scale(0.5, 0.5) 便可以实现。 看看代码修改吧。
// css
 .canvasPlayWrapper {
      + transform: scale(0.5, 0.5);
        width: 440px;
        height: 440px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        border-radius: 50%;
        background-color: white;
        box-shadow: 0px 0px 24px gray;
      }
// js
 /**
   * @desc: 创建时钟
   * @param {*}
   * @return {*}
   */
  const createClock = () => {
    const canvasTarget = document.getElementById("canvasClock");
    if (canvasTarget) {
      const ctx = canvasTarget.getContext("2d");
      // 放大两倍,容器再缩小两倍,防止放大的时候模糊
    + ctx.scale(2, 2);
      if (ctx) {
        // 渲染函数
        const animloop = () => {
          drawClock(ctx, canvasTarget);
          // console.log('canvas');
          requestAnimationFrame(animloop);
        };
        animloop();
      }
    }
  };

至此,我们的时钟就完成啦,并且放大还不会模糊。

最终代码

最后附上一下最终代码,拿到浏览器里就能用~

image.png

<!--
 * @Descripttion:  canvas 时钟
 * @version: 1.0.0
 * @Author: jiaxiantao
 * @Date: 2021-08-20 14:12:03
 * @LastEditors: jiaxiantao
 * @LastEditTime: 2021-08-23 16:29:26
-->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>这是一个时钟</title>
    <style>
      html,
      body {
        overflow: hidden;
      }
      .canvasPlay {
        width: 100vw;
        height: 100vh;
        display: flex;
        align-items: center;
        justify-content: center;
        flex-direction: column;
        background-color: black;
        overflow: hidden;
      }
      .canvasPlayWrapper {
        transform: scale(0.5, 0.5);
        width: 440px;
        height: 440px;
        min-width: 440px;
        min-height: 440px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        border-radius: 50%;
        background-color: white;
        box-shadow: 0px 0px 24px gray;
      }
      .canvasTarget {
        box-shadow: 0px 0px 24px gray inset;
        background-color: white;
        border-radius: 50%;
      }
    </style>
  </head>
  <body>
    <div class="canvasPlay">
      <div class="canvasPlayWrapper">
        <canvas
          id="canvasClock"
          width="400px"
          height="400px"
          class="canvasTarget"
        ></canvas>
      </div>
    </div>

    <script>
      /**
       * @desc: 获取当前时间
       * @param {*}
       * @return {*}
       */
      const getCurrentTime = () => {
        // 获取当前时,分,秒,毫秒
        const time = new Date();
        const hour = time.getHours() % 12;
        const min = time.getMinutes();
        const sec = time.getSeconds();
        const milliSec = time.getMilliseconds();
        return {
          hour,
          min,
          sec,
          milliSec,
        };
      };

      /**
       * @desc: 创建中心点
       * @param {*}
       * @return {*}
       */
      const createCenterPointer = (ctx) => {
        // 画中间的小圆
        ctx.beginPath();
        ctx.arc(0, 0, 3, 0, 2 * Math.PI);
        ctx.stroke();
        ctx.fillStyle = "black";
        ctx.fill();
        ctx.closePath();
      };

      /**
       * @desc: 创建渐变内环
       * @param {*}
       * @return {*}
       */
      const createRadialGradient = (ctx) => {
        // 创建渐变
        const grd = ctx.createRadialGradient(0, 0, 0, 0, 0, 100);
        grd.addColorStop(0, "white");
        grd.addColorStop(0.92, "white");
        grd.addColorStop(0.99, "#BDBDBD");
        grd.addColorStop(1, "#D8D8D8");

        // 填充渐变
        ctx.fillStyle = grd;
        ctx.arc(0, 0, 100, 0, 2 * Math.PI);
        ctx.fill();
      };

      /**
       * @desc:  绘制刻度
       * @param {*}
       * @return {*}
       */
      const drawScale = (ctx) => {
        // 保存上一次的状态
        ctx.save();
        // 绘制刻度,也是跟绘制时分秒针一样,只不过刻度是死的
        ctx.lineWidth = 1;
        for (let i = 0; i < 60; i += 1) {
          ctx.rotate((2 * Math.PI) / 60);
          ctx.beginPath();
          ctx.moveTo(90, 0);
          ctx.lineTo(100, 0);
          ctx.stroke();
          ctx.closePath();
        }
        // 恢复成上一次保存的状态
        ctx.restore();
        ctx.save();

        ctx.lineWidth = 1;
        for (let i = 0; i < 12; i += 1) {
          ctx.rotate((2 * Math.PI) / 12);
          ctx.beginPath();
          ctx.lineTo(89, 0);
          ctx.lineTo(90, 2);
          ctx.lineTo(100, 2);
          // ctx.lineTo(100, 0);
          // 画圆线使用arc(中心点X,中心点Y,半径,起始角度,结束角度)
          ctx.arc(0, 0, 100, 0, (2 * Math.PI) / 250);
          ctx.lineTo(100, -2);
          ctx.lineTo(90, -2);
          ctx.lineTo(89, 0);
          ctx.stroke();
          ctx.fillStyle = "black";
          ctx.fill();
          ctx.closePath();
        }
        ctx.restore();
      };

      /**
       * @desc:  绘制数字
       * @param {*}
       * @return {*}
       */
      const drawScaleNumber = (ctx) => {
        ctx.save();
        // 绘制刻度,也是跟绘制时分秒针一样,只不过刻度是死的
        ctx.lineWidth = 1;
        const textRadius = 80; // 设置文字半径为80,与边界相差20个像素
        for (let i = 0; i < 12; i += 1) {
          ctx.font = "16px Arial";
          ctx.fillText(
            i + 1,
            textRadius * Math.sin((Math.PI * (i + 1)) / 6) -
              (Math.ceil(i / 8) * 8) / 2,
            -(textRadius * Math.cos((Math.PI * (i + 1)) / 6) - 12 / 2)
          );
        }
        ctx.restore();
      };

      /**
       * @desc: 创建指针
       * @param {*}
       * @return {*}
       */
      const createPointer = (ctx) => {
        const { hour, min, sec, milliSec } = getCurrentTime();
        // 保存上一次的状态
        ctx.save();
        // 时针
        ctx.rotate(((2 * Math.PI) / 12) * (hour + min / 60) - Math.PI / 2);
        ctx.beginPath();
        // moveTo设置画线起点
        ctx.moveTo(-3, 0);
        // lineTo设置画线经过点
        ctx.lineTo(0, 3);
        ctx.lineTo(45, 3);
        ctx.lineTo(50, 0);
        ctx.lineTo(45, -3);
        ctx.lineTo(0, -3);
        ctx.lineTo(-3, 0);
        // 设置线宽
        ctx.lineWidth = 1;
        ctx.strokeStyle = "black";
        ctx.stroke();

        // 创建黑色阴影,模糊级数是 4
        ctx.shadowBlur = 4;
        ctx.shadowColor = "black";
        // 填充颜色
        ctx.fillStyle = "black";
        ctx.fill();

        ctx.closePath();
        // 恢复成上一次保存的状态
        ctx.restore();
        ctx.save();

        // 分针
        ctx.rotate(((2 * Math.PI) / 60) * (min + sec / 60) - Math.PI / 2);
        ctx.beginPath();
        // moveTo设置画线起点
        ctx.moveTo(-2, 0);
        // lineTo设置画线经过点
        ctx.lineTo(0, 2);
        ctx.lineTo(52, 2);
        ctx.lineTo(60, 0);
        ctx.lineTo(52, -2);
        ctx.lineTo(0, -2);
        ctx.lineTo(-2, 0);
        ctx.lineWidth = 1;
        ctx.strokeStyle = "#1e80ff";
        ctx.stroke();
        // 创建蓝色阴影,模糊级数是 3
        ctx.shadowBlur = 3;
        ctx.shadowColor = "#1e80ff";
        // 填充颜色
        ctx.fillStyle = "#1e80ff";
        ctx.fill();
        ctx.closePath();
        ctx.restore();
        ctx.save();

        // 秒针
        ctx.rotate(
          ((2 * Math.PI) / 60) * (sec - 1 + milliSec / 1000) - Math.PI / 2
        );
        ctx.beginPath();
        // moveTo设置画线起点
        ctx.moveTo(-1, 0);
        ctx.lineTo(0, 1);
        ctx.lineTo(60, 1);
        ctx.lineTo(70, 0);
        ctx.lineTo(60, -1);
        ctx.lineTo(0, -1);
        ctx.lineTo(-1, 0);
        ctx.strokeStyle = "#e9686b";
        ctx.stroke();
        // 创建红色阴影,模糊级数是 2
        ctx.shadowBlur = 2;
        ctx.shadowColor = "#e9686b";
        // 填充颜色
        ctx.fillStyle = "#e9686b";
        ctx.fill();
        ctx.closePath();

        ctx.restore();
      };

      /**
       * @desc: 绘制时钟
       * @param {*} ctx
       * @return {*}
       */
      const drawClock = (ctx, target) => {
        if (ctx) {
          ctx.save();
          // 保存清除状态
          ctx.clearRect(0, 0, 200, 200);

          // 设置中心点,此时100,100变成了坐标的0,0
          ctx.translate(100, 100);

          // 创建中心点
          createCenterPointer(ctx);

          // 创建渐变内环
          // createRadialGradient(ctx);

          // 绘制刻度
          drawScale(ctx);
          // 绘制数字
          drawScaleNumber(ctx);

          // 创建指针
          createPointer(ctx);

          ctx.restore();
        }
      };

      /**
       * @desc: 创建时钟
       * @param {*}
       * @return {*}
       */
      const createClock = () => {
        const canvasTarget = document.getElementById("canvasClock");
        if (canvasTarget) {
          const ctx = canvasTarget.getContext("2d");
          // 放大两倍,容器再缩小两倍,防止放大的时候模糊
          ctx.scale(2, 2);
          if (ctx) {
            // 渲染函数
            const animloop = () => {
              drawClock(ctx, canvasTarget);
              // console.log('canvas');
              requestAnimationFrame(animloop);
            };
            animloop();
          }
        }
      };

      createClock();
    </script>
  </body>
</html>

这是我在掘金里的第一篇水文,喜欢的小伙伴请点个赞哦~