preface

I have been out of work for half a month and dawdling in the nuggets every day. Recently, I suddenly became interested in canvas. Seeing the beautiful effect written by the big guy, I couldn’t help wanting to complete one.

It took me a whole working day to be satisfied.

Decomposition of thinking

We can see that this clock is composed of four parts: frame, scale, number and pointer. First of all, the pointer is dynamic, so it must be drawn on canvas.

Secondly border, scale, numbers are static, border is very simple, with HTML + CSS can be achieved, scale and numbers can also be achieved with absolute positioning, but scale and numbers so much, have to write how much CSS absolute positioning ah, it must not be tired into a dog. Therefore, numbers and scales are also drawn using canvas.

Draw the border

The idea is clear: create borders using HTML + CSS.

  1. The background should be set to black, the program ape favorite black background, no problem with it;
  2. You need two border containers, one outer and one inner, using border-radius equal to 50% width and height to make the container round;
  3. We notice that the border here is shaded, so we can use box-shadow to create a border shadow, the outer border with the outer shadow, and the inner border with the inner shadow.

Easy, right? Look at the code.

// 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>Copy the code

Now we have created a nice ring border. Let’s see what it looks like.

Create the canvas and position it

Now comes the key, we need to get the node of canvas and draw our graph on this node. Next, locate the center point of canvas as the origin of coordinates, and draw a solid circle at this origin as the shackle point of the clock (I don’t know what it is called here [face covering]), and directly enter the code.

/** * @desc: Create center * @param {*} * @return {*} */ const createCenterPointer = (CTX) => {// Draw a small circle 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"); Ctx.translate (100, 100); // set the center point so that 100,100 becomes coordinate 0,0 ctx.translate(100, 100); createCenterPointer(ctx); }Copy the code

Now let’s see what happens

Like is not like much a mole 😂!

Draw the scale & number

To be a perfect clock, you need to support both scale and number visual references.

Draw the calibration

So what we see here is that the scale is divided into big and small scales, 12 for the big scale and 60 for the small scale, so it’s easy to imagine that we have to rotate it twice, and then we have the very important deflection function called rotate. The rotate function is used to deflect the value of a small scale with the width of 1.

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();
}
Copy the code

The next step is to draw the large scale, which is a little more difficult because the large scale is an irregular shape, like the one below

Rotate and fill in the color to create a single scale and use the rotate function to rotate the corresponding value.

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); // Draw a circle using arc(center point X, center point Y, radius, start Angle, end Angle) 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(); }Copy the code

So the problem is, when we draw the small scale, the X-axis has been shifted, so we have to make sure that the X-axis is back to its original shape, so that we can make sure that when we draw the large scale, it will not be affected.

Save packs the current state of the CTX onto the stack, restore fetches the top state of the stack and assigns a value to the CTX. Save can be multiple times, but restore must fetch the state as many times as save.

/** * @desc: drawScale * @param {*} * @return {*} */ const drawScale = (CTX) => {// Save the last status ctx.save(); LineWidth = 1; 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(); } // Restore to the last saved state 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); // Draw a circle using arc(center point X, center point Y, radius, start Angle, end Angle) 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(); };Copy the code

Let’s see how it works

Drawing Numbers

How do I round the numbers? Here we want to use canvas to draw text, which involves two core methods.

  • Font – Defines the font
  • fillText(text,x,y) – Draws solid text on canvas

Here’s how:

  1. The digital clock is 1-12, which can be drawn by the for loop. The traversal method is fillText, and the text method is index traversal.
  2. What are the coordinates of x and y? Let’s put our junior high school knowledge to use – trigonometric functions, radius * sin(offset Angle) to find the position of the x axis, radius * cos(offset Angle);
  3. Since the numbers themselves have width and height, the x and Y positions need to subtract width and height / 2 from themselves;
  4. To ensure that the state between functions is not affected, we also add a pair of save and restore functions;

Without further ado, go to the code

@param {*} * @return {*} */ const drawScaleNumber = (CTX) => {ctx.save(); LineWidth = 1; CTX. LineWidth = 1; const textRadius = 80; 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(); };Copy the code

The trigonometric function of this piece needs to be drawn by yourself to understand, and the offset of the text is sometimes inconsistent, so the specific need to adjust ~

Let’s see the effect

Well, still calculate ok! Blow a wave.

Draw a pointer

In my mind, the pointer is a six-sided diamond, like this

So here’s the plan.

  1. Use the Canvas moveTo&lineTo line tool and fill it with color.
  2. Use shadowColor to add a shadow to the pointer to give it a sense of hierarchy.
  3. Obtain the system time, calculate the Angle of pointer deflection, and use rotate to realize the Angle deflection.
  4. Since the X-axis shifts after each drawing of the pointer, you need to use save to save the state after drawing and use restore to retrieve the last state.

(2 * math.pi) / 12) * hour is the current Angle of the hour hand, plus the Angle the minute hand has turned to give it the Angle it should have turned. So we should add (2 * math.pi) / 12) * (min / 60), and since the coordinate system and our field of view are exactly 180° upside down, we should subtract math.pi / 2, so we get

  • The Angle of rotation of the hour hand is zero((2 * Math.PI) / 12) * (hour + min / 60) - Math.PI / 2;
  • Similarly, we have the Angle of rotation of the minute hand is zero((2 * Math.PI) / 60) * (min + sec / 60) - Math.PI / 2;
  • The second hand rotates at an Angle of zero((2 * Math.PI) / 60) * (sec - 1 + milliSec / 1000) - Math.PI / 2;

Take a look at the code:

/** * @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(); // Save the previous state ctx.save(); Rotate (((2 * math.pi) / 12) * (hour + min / 60) - math.pi / 2); rotate(((2 * math.pi) / 12) * (hour + min / 60) - math.pi / 2); ctx.beginPath(); // moveTo sets the starting point of the drawing line ctx.moveTo(-3, 0); // lineTo set the lineTo pass through the point 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(); // Create a black shadow with a blur level of 4 ctx.shadowBlur = 4; ctx.shadowColor = "black"; // fill color ctx.fillStyle = "black"; ctx.fill(); ctx.closePath(); // Restore to the last saved state ctx.restore(); ctx.save(); Rotate (((2 * math.pi) / 60) * (min + SEC / 60) - math.pi / 2); rotate(((2 * math.pi) / 60) * (min + SEC / 60) - math.pi / 2); ctx.beginPath(); // moveTo sets the starting point of the drawing line ctx.moveto (-2, 0); // lineTo set the lineTo pass through the point 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(); // Create a blue shadow with a blur level of 3 ctx.shadowBlur = 3; ctx.shadowColor = "#1e80ff"; // fill color ctx.fillStyle = "#1e80ff"; ctx.fill(); ctx.closePath(); ctx.restore(); ctx.save(); // second hand ctx.rotate(((2 * math.pi) / 40) * (sec-1 + zipgex) - math.pi / 2); ctx.beginPath(); // moveTo sets the starting point of the drawing line 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(); // Create a red shadow with a blur level of 2 ctx.shadowBlur = 2; ctx.shadowColor = "#e9686b"; // Fill color ctx.fillStyle = "#e9686b"; ctx.fill(); ctx.closePath(); ctx.restore(); };Copy the code

After drawing, our static clock is complete, look at the effect ~

The gradient border

At first we used CSS box-shadow to draw the border, but we can also use canvas gradient property to draw a border. Canvas gradient can be filled with rectangles, circles, lines, text, etc. Various shapes can define different colors.

There are two different ways to set the Canvas gradient:

  • createLinearGradient(x,y,x1,y1) – Create a line gradient
  • createRadialGradient(x,y,r,x1,y1,r1) – Create a radial/circular gradient

It is obvious from here that we can use the radial/circle gradient to achieve the shadow effect.

/** * @desc: * @param {*} * @return {*} */ const createRadialGradient = (CTX) => {// Create gradient 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"); // Fill gradient ctx.fillStyle = GRD; ctx.arc(0, 0, 100, 0, 2 * Math.PI); ctx.fill(); };Copy the code

After comparison, it is found that the border effect is not as beautiful as CSS box-shadow, so we still choose CSS to draw the border.

Get the needle moving

When we have finished drawing the clock, the last step is to make it move, that is, to update the view. It should be noted that every time we update the view, we should clear the previous canvas and start drawing a new view, otherwise there will be a thousand hands of Guanyin scene. We can use two methods to update the view:

    1. The timersetInterval, refresh the view at a fixed frequency;
    1. Use Windows. RequestAnimationFrame to browser built-in frequency refresh view;

Used the two kinds of schemes of junior partner should be clear, setInterval is not unified and browser refresh frequency, frequency of updates too wasteful, update too slow can cause visual caton, show the effect is not good, so we choose window. RequestAnimationFrame to update the view. The code is as follows:

Const animloop = () => {drawClock(CTX, canvasTarget); // console.log('canvas'); requestAnimationFrame(animloop); }; animloop();Copy the code

So our clock is in motion, with a GIF to illustrate the effect.

Prevents the view from becoming blurred after zooming in

An obvious disadvantage of drawing with Canvas is that after canvas is enlarged, the image will become blurred. This is because the independent pixel of the device is inconsistent with the physical pixel. The pixel ratio of the device is equal to the physical pixel/independent pixel of the device, and the pixel ratio of most devices is 2. This means that a 100px image needs to be placed at 200px to display properly, so we need to

  1. I’m going to double the canvas image and usectx.scale(2, 2)Can be achieved;
  2. Then I’ll double the size of the Canvas container and use CSSThe transform: scale (0.5, 0.5)It can be done.

Look at the code changes.

// 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: CreateClock * @param {*} * @return {*} */ const createClock = () => {const canvasTarget = document.getElementById("canvasClock"); if (canvasTarget) { const ctx = canvasTarget.getContext("2d"); + ctx.scale(2, 2); DrawClock (CTX, canvasTarget); drawClock(CTX, canvasTarget); // console.log('canvas'); requestAnimationFrame(animloop); }; animloop(); }}};Copy the code

At this point, our clock is complete, and zooming in does not blur.

The final code

Finally, attach the final code, take the browser to use ~

<!--
 * @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>
Copy the code

This is my first article in the nuggets of hydrology, like friends please give a thumbs up oh ~