Make writing a habit together! This is my first day to participate in the “Gold Digging Day New Plan · April More text challenge”, click to see the details of the activity.

Implementation effect

In addition to the pikachu background, the hour, minute and second hand scales are drawn on Canvas.

The implementation process

Set the basic size and background

  1. Full face footage of Pikachu

  1. The basic structure
  • Set the HTML structure as follows:
<div class="canvas-box">
    <canvas width="300" height="300"></canvas>
</div>
Copy the code

Question: Why embed a div box here?

Later, we set background-image to div to make the clock’s background pikachu. The reason for not setting the background image directly to the canvas is that the background image will overwrite the canvas clock we draw behind.

  • Set the style style as follows:
.canvas-box {
    width: 300px;
    height: 300px;
    background-image: url("img/pikachu.webp");
    /* Stretch the image to see the panorama */
    background-size: contain;
    /* Horizontal center + vertical move up */
    background-position: 50% 25%;
    /* If the image is symmetric, it can be set as */
    /* background-position: center; * /
    / * * / it become
    border-radius: 50%;
}
Copy the code

Basic API (in order of basic use)

  1. GetContext: Returns the context of the Canvas object, which can be interpreted as a brush.
  2. ClearRect: Sets the pixel to transparent to erase a rectangular area, avoiding repeated drawing.
  3. Save: Press to save the canvas state. (corresponding to restore)
  4. Translate: If you add a translation to the current grid, x, y will change the relative translation position.
  5. Rotate: Adds a rotation to the transformation matrix, which changes the relative rotation Angle.
  6. BeginPath: method of starting a new path by clearing a list of empty subpaths, indicating the start of a brush drawing.
  7. Arc: Draws an arc path
  8. MoveTo: Moves the starting point of a new subpath to (x, y) coordinates
  9. LineTo: use a straight lineTo connect the end of a child path to the x, y coordinates
  10. Stroke: A method of drawing current or existing paths based on the current drawing style using a non-zero wrap rule. Basically, stroke.
  11. fillTextIn:(x, y)Position fill text. Similarly, fillRect, fill.
  12. ClosePath: Method of returning the pen point to the start of the current subpath. It tries to draw a line from the current point to the starting point. If the graph is already closed or has only one point, this method does nothing.
  13. Restore: The stack restores the brush state.

Of course, there are also some brush properties, such as lineWidth for lineWidth, lineCap for line end display style, fillStyle for fill color and strokeStyle for line color.

With this knowledge in mind, we’ll start drawing the clock

Draw scales and numbers

General content: After obtaining the basic information of the clock, first move the brush to the center of the circle according to the routine of save-draw-restore, then draw an arc as the border.

How to set the angles of the numbers and scales: Divide the angles evenly throughout the circle (2* math.pi), because there are 12 numbers, so they are divided into 12 parts, and because there are 60 scales, so the scales need to be divided into 60 parts.

How to set the coordinates of the number and scale: the unit circle’s sine and cosine functions can be used to find the relative x and y coordinates of the center of the circle. Of course, since the clock is not a unit circle, we multiply it by one more radius.

// Get basic data
let canvas = document.querySelector("canvas")
let ctx = canvas.getContext('2d')
let width = ctx.canvas.width
let height = ctx.canvas.height
let r = width / 2; / / radius

// Draw the background
function drawBackground() {
    // Basic routine stack - draw - stack
    ctx.save(); // Save the original context information to the stack
    ctx.translate(r, r); // Move the brush to the center
    ctx.beginPath() // Start drawing
    ctx.lineWidth = 2 // Set the line width

    // Draw the border of the clock
    // Prototype: void ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise);
    ctx.arc(0.0, r - ctx.lineWidth / 2.0.2 * Math.PI) // draw a circle with a radius of r- half the line width relative to 0,0
    ctx.closePath() // Finish drawing
    ctx.strokeStyle = '# 222' // Set the fill color
    ctx.stroke() / / fill

    // Scale number: since the circle starts in the positive x direction, it starts with 3
    var hourNum = [3.4.5.6.7.8.9.10.11.12.1.2]
    hourNum.map(function (num, i) {
        var rad = 2 * Math.PI / 12 * i   // 2 PI is a circle, broken into 12 parts
        var x = Math.cos(rad) * (r * 10 / 12)  
        var y = Math.sin(rad) * (r * 10 / 12)
        // Set the text style to be filled next
        ctx.font = '13px Arial'
        ctx.textAlign = 'center'
        ctx.textBaseline = 'middle'
        ctx.fillStyle = '# 000'
        ctx.fillText(num, x, y)
    })

    // Draw the minute scale
    for (var i = 0; i < 60; i++) {
        var rad = 2 * Math.PI / 60 * i
        var x = Math.cos(rad) * (r * 95 / 100)
        var y = Math.sin(rad) * (r * 95 / 100)
        ctx.beginPath()
        if (i % 5= =0) {
            ctx.fillStyle = '# 000'
            ctx.arc(x, y, 1.0.2 * Math.PI, false)}else {
            ctx.fillStyle = '# 808080'
            ctx.arc(x, y, 1.0.2 * Math.PI, false)
        }
        ctx.fill()
    }
    ctx.restore(); // Restore the original context information
}
Copy the code

Draw hour hand, minute hand, second hand

Drawing an hour hand is actually like pulling a straight line from the center of a circle. But we need to set the Angle it points to according to an Hour. We still split the angles of the 12 squares (2 * math.pi / 12) to see how many angles there are for hour.

Minor detail: If we follow hour, the hour hand will only point to the hour, so following this idea, we need to slice the Angle even more finely, and then add the offset between the two hours according to minutes.

// Draw an hour hand
function drawHour(hour, minute) {
    ctx.save();
    ctx.translate(r, r);
    var rad = 2 * Math.PI / 12 * hour; //2 π is a circle divided into 12 cells
    var mad = 2 * Math.PI / 12 / 60 * minute; // The ratio of the corresponding minutes
    ctx.rotate(rad + mad);
    ctx.beginPath();
    ctx.lineWidth = 3;
    ctx.lineCap = 'round';
    ctx.moveTo(0.0);
    ctx.lineTo(0, -r / 3);
    ctx.stroke();
    ctx.restore();
}
Copy the code

Minute hand and second hand are more simple, just need to change the line thickness and length of the corresponding clockwise routine.

// Draw the minute hand
function drawMinute(minute) {
    ctx.save();
    ctx.translate(r, r);
    let rad = 2 * Math.PI / 60 * minute; //2 π is a circle divided into 60 cells
    ctx.rotate(rad);
    ctx.beginPath();
    ctx.lineWidth = 2;
    ctx.lineCap = 'round';
    ctx.moveTo(0.0);
    ctx.lineTo(0, -r / 2);
    ctx.stroke();
    ctx.restore();
}

// Draw the second hand
function drawSecond(second) {
    ctx.save()
    ctx.translate(r, r);
    var rad = 2 * Math.PI / 60 * second
    ctx.rotate(rad)
    ctx.beginPath()
    ctx.lineWidth = 1
    ctx.lineCap = 'round'
    ctx.strokeStyle = 'red'
    ctx.moveTo(0.0)
    ctx.lineTo(0, -r * 9 / 10);
    ctx.stroke()
    ctx.restore()
}
Copy the code

Regular update time

Finally, we only need to constantly update the time to achieve dynamic clock.

function drawMyClock() {
    ctx.clearRect(0.0, width, height) // Empty, otherwise the drawing will be repeated
    let now = new Date(a);let hour = now.getHours();
    let minute = now.getMinutes();
    let second = now.getSeconds();
    drawBackground();
    drawHour(hour, minute);
    drawMinute(minute);
    drawSecond(second);
}
drawMyClock(); // called once directly when the page is loaded
setInterval(function () { // Set timer directly
    drawMyClock();
}, 1000);
Copy the code

The effect is as follows:

Finally, we tried to encapsulate the clock more elegantly with class, pumping the same CTX state, such as translate(r, R), into refresh, a continuously updated function. The final code is as follows:

class MyClock {
    constructor(canvas,isNeedBorder = true,isNeedNumberDot = true,isNeedMinuteDot = true) {
        this.canvas = canvas;
        this.ctx = canvas.getContext("2d");
        this.width = this.ctx.canvas.width
        this.height = this.ctx.canvas.height
        this.r = this.width / 2; / / radius

        this.isNeedBorder = isNeedBorder;
        this.isNeedNumberDot = isNeedNumberDot;
        this.isNeedMinuteDot = isNeedMinuteDot;
        
        this.timer = null;
    }
    setTime(hour, minute, second) {// Set the clock time
        if (this.isValid(hour, minute, second)) {
            this.hour = hour;
            this.minute = minute;
            this.second = second; }}isValid(hour, minute, second) {// Check whether the time is valid
        if (typeof hour == 'number' && typeof minute == 'number' && typeof second == 'number' && hour >=
            0 && hour < 24 && minute >= 0 && minute < 60 && second >= 0 && second < 60) {
            return true;
        }
        return false;
    }
    start(hour, minute, second) {// Start running the clock
        let now = new Date(a);this.setTime(hour || now.getHours(), minute || now.getMinutes(), second || now.getSeconds());
        console.log(this.hour, this.minute, this.second);
        this.timer = setInterval(() = > {
            this.refresh();
            this.addTime();
        }, 1000);
    }
    end(){// End the clock
        clearInterval(this.timer);
    }
    addTime() {// Update time
        this.second++;
        if (this.second >= 60) {
            this.second -= 60;
            this.minute++;
            if (this.minute >= 60) {
                this.minute -= 60;
                this.hour++;
                if (this.hour >= 24) {
                    this.hour -= 24; }}}}refresh() {// Refresh the clock
        
        this.ctx.save(); // Adjust the public CTX context here
        this.ctx.clearRect(0.0.this.width, this.height) // Empty, otherwise the drawing will be repeated
        this.ctx.translate(this.r, this.r);
        this.ctx.lineCap = 'round'

        if (this.hour == undefined) return; // No initialization time
        this.drawBackground(this.isNeedBorder,this.isNeedNumberDot,this.isNeedMinuteDot);
        this.drawHour(this.hour, this.minute);
        this.drawMinute(this.minute);
        this.drawSecond(this.second);
        this.ctx.restore();
    }
    drawBackground() {// Draw background (border + scale)
        let ctx = this.ctx;
        if (this.isNeedBorder) {
            this.drawBorder();
        }
        if (this.isNeedNumberDot) {
            this.drawNumberDot();
        }
        if (this.isNeedMinuteDot) {
            this.drawMinuteDot(); }}drawBorder() {// Draw the border
        let ctx = this.ctx;
        let r = this.r;
        ctx.save(); // Save the original context information to the stack
        ctx.beginPath() // Start drawing
        ctx.lineWidth = 2 // Set the line width
        // Prototype: void ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise);
        ctx.arc(0.0, r - ctx.lineWidth / 2.0.2 * Math.PI) // draw a circle with a radius of r- half the line width relative to 0,0
        ctx.closePath() // Finish drawing
        ctx.strokeStyle = '# 222' // Set the fill color
        ctx.stroke() / / fill
        ctx.restore(); // Restore the original context information
    }
    drawNumberDot() { // Draw a numeric scale
        let ctx = this.ctx;
        let r = this.r;
        ctx.save();
        // Scale number: since the circle starts in the positive x direction, it starts with 3
        var hourNum = [3.4.5.6.7.8.9.10.11.12.1.2]
        hourNum.map(function (num, i) {
            var rad = 2 * Math.PI / 12 * i // 2 PI is a circle, broken into 12 parts
            var x = Math.cos(rad) * (r * 10 / 12)
            var y = Math.sin(rad) * (r * 10 / 12)
            // Set the text style to be filled next
            ctx.font = '13px Arial'
            ctx.textAlign = 'center'
            ctx.textBaseline = 'middle'
            ctx.fillStyle = '# 000'
            ctx.fillText(num, x, y)
        })
        ctx.restore();
    }
    drawMinuteDot() { // Draw the minute scale
        let ctx = this.ctx;
        let r = this.r;
        ctx.save();
        for (var i = 0; i < 60; i++) {
            var rad = 2 * Math.PI / 60 * i
            var x = Math.cos(rad) * (r * 95 / 100)
            var y = Math.sin(rad) * (r * 95 / 100)
            ctx.beginPath()
            if (i % 5= =0) {
                ctx.fillStyle = '# 000'
                ctx.arc(x, y, 1.0.2 * Math.PI, false)}else {
                ctx.fillStyle = '# 808080'
                ctx.arc(x, y, 1.0.2 * Math.PI, false)
            }
            ctx.fill()
            ctx.closePath();
        }
        ctx.restore();
    }
    drawHour(hour, minute) {// Draw an hour hand
        let ctx = this.ctx;
        let r = this.r;
        ctx.save();
        var rad = 2 * Math.PI / 12 * hour; //2 π is a circle divided into 12 cells
        var mad = 2 * Math.PI / 12 / 60 * minute; // The ratio of the corresponding minutes
        ctx.rotate(rad + mad);
        ctx.beginPath();
        ctx.lineWidth = 3;
        ctx.moveTo(0.0);
        ctx.lineTo(0, -r / 3);
        ctx.stroke();
        ctx.restore();
    }
    drawMinute(minute) {// Draw the minute hand
        let r = this.r;
        let ctx = this.ctx;
        ctx.save();
        let rad = 2 * Math.PI / 60 * minute; //2 π is a circle divided into 60 cells
        ctx.rotate(rad);
        ctx.beginPath();
        ctx.lineWidth = 2;
        ctx.moveTo(0.0);
        ctx.lineTo(0, -r / 2);
        ctx.stroke();
        ctx.restore();
    }
    drawSecond(second) {// Draw the second hand
        let ctx = this.ctx;
        let r = this.r;
        ctx.save()
        var rad = 2 * Math.PI / 60 * second
        ctx.rotate(rad)
        ctx.beginPath()
        ctx.lineWidth = 1
        ctx.strokeStyle = 'red'
        ctx.moveTo(0.0)
        ctx.lineTo(0, -r * 9 / 10);
        ctx.stroke()
        ctx.restore()
    }
}
// Get basic data
let canvas = document.querySelector("canvas")
let yyClock = new MyClock(canvas);
yyClock.start();
Copy the code