11
头图

(https://juejin.cn/post/6978685539985653767 "https://juejin.cn/post/6978685539985653767")」

foreword

Hello everyone, my name is Lin Sanxin. I recall when I was recruiting at school canvas , but I couldn't. Later, I always wanted to find a chance to learn canvas , but I never had time. canvas in the front end is becoming more and more important. For this reason, I specially wrote 3 small projects, so that you can get started with canvas 10 minutes. Yes, I don’t have her in my heart, only you

image.png

1. Canvas realizes clock rotation

To achieve the following effects, it is divided into several steps:

  • 1. Find the center of the canvas, draw the table center, and the table frame
  • 2. Get the current time of hour hand, minute hand, second hand, and scale according to the time
  • 3, using a timer, every one second acquired new time, and re-drawing, to the clock rotation effect

截屏2021-07-19 下午8.52.15.png

1.1 watch center, watch frame

There are two knowledge points for drawing the center of the watch and the watch frame:

  • 1. Find the center position of
  • 2. Draw a circle

    //html
    
    <canvas id="canvas" width="600" height="600"></canvas>
    
    // js
    
    // 设置中心点,此时300,300变成了坐标的0,0
    ctx.translate(300, 300)
    // 画圆线使用arc(中心点X,中心点Y,半径,起始角度,结束角度)
    ctx.arc(0, 0, 100, 0, 2 * Math.PI)
    ctx.arc(0, 0, 5, 0, 2 * Math.PI)
    // 执行画线段的操作stroke
    ctx.stroke() 

    Let's take a look at the effect and find that it doesn't seem right. We want to draw two independent circle lines of . How can the two circles connected together:

截屏2021-07-19 下午9.10.07.png
The reason is: when the above code draws a circle, it is drawn continuously, so after the large circle is drawn, the line is not cut off, and then the small circle is drawn, which will definitely connect the large circle and the small circle. The solution is: beginPath,closePath

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

// 画大圆
+ ctx.beginPath()
// 画圆线使用arc(中心点X,中心点Y,半径,起始角度,结束角度)
  ctx.arc(0, 0, 100, 0, 2 * Math.PI)
  ctx.stroke() // 执行画线段的操作
+ ctx.closePath()

// 画小圆
+ ctx.beginPath()
  ctx.arc(0, 0, 5, 0, 2 * Math.PI)
  ctx.stroke()
+ ctx.closePath()

1.2 Hour, minute, second

To draw these three pointers, there are two knowledge points:

  • 1. Calculate the angle according to the current , minute and second to
  • hour hand, minute hand, second hand at the calculated angle
    How to draw a line according to the calculated angle, for example 3 o'clock, then the hour hand should start 12 o'clock clockwise rotates 2 * Math.PI / 12 * 3 = 90° , the same is true for the minute hand and the second hand, but different from the hour hand It's ratio of 161ecb6aa65b57, because has 12 copies on the watch, while minute and second hands are both 60 copies

截屏2021-07-19 下午10.07.19.png

At this time, there is a new question. Taking the above example as an example, I calculated 90° , so how do we draw the hour hand? We can use moveTo and lineTo to draw line segments. As for 90°, we only need to x-axis clockwise by 90° , and then draw this line segment, then we get the pointer of the specified angle. But as mentioned above, the starting point is 12 o’clock. Our default x-axis is indeed horizontal, so after we calculate the angle of the hour, minute and second hand, we need to subtract 90° from every time. Maybe this is a bit confusing, let's demonstrate it through the following figure, or take the example of 3 points above:

截屏2021-07-19 下午10.30.23.png

截屏2021-07-19 下午10.31.02.png
In this way, the drawing angle of the 3-point pointer is obtained.

There are new problems again. For example, now that I have finished drawing the hour hand, and then I want to draw the minute hand, the x-axis has been deflected when I drew the hour-hand. At this time, the x-axis must be restored to its original appearance before we can continue to draw. The minute hand, otherwise the drawn minute hand is inaccurate. At this time, save and restore come in handy, save is to pack and push the current state of ctx into the stack, restore is to take out the state at the top of the stack and assign it to ctx, save can be multiple times, but the number of times restore takes the state Must be equal to the number of saves

截屏2021-07-19 下午10.42.06.png

Understand what was said above, the rest of the painting scale, the scale of the initial hours of the second hand with the truth as simple as that, but the scale is dead, need not be calculated, only the rules drawn 60 small scale, and 12 Ge Just big scale

const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')

ctx.translate(300, 300) // 设置中心点,此时300,300变成了坐标的0,0
// 把状态保存起来
+ ctx.save()

// 画大圆
ctx.beginPath()
// 画圆线使用arc(中心点X,中心点Y,半径,起始角度,结束角度)
ctx.arc(0, 0, 100, 0, 2 * Math.PI)
ctx.stroke() // 执行画线段的操作
ctx.closePath()

// 画小圆
ctx.beginPath()
ctx.arc(0, 0, 5, 0, 2 * Math.PI)
ctx.stroke()
ctx.closePath()

----- 新加代码  ------

// 获取当前 时,分,秒
let time = new Date()
let hour = time.getHours() % 12
let min = time.getMinutes()
let sec = time.getSeconds()

// 时针
ctx.rotate(2 * Math.PI / 12 * hour + 2 * Math.PI / 12 * (min / 60) - Math.PI / 2)
ctx.beginPath()
// moveTo设置画线起点
ctx.moveTo(-10, 0)
// lineTo设置画线经过点
ctx.lineTo(40, 0)
// 设置线宽
ctx.lineWidth = 10
ctx.stroke()
ctx.closePath()
// 恢复成上一次save的状态
ctx.restore()
// 恢复完再保存一次
ctx.save()

// 分针
ctx.rotate(2 * Math.PI / 60 * min + 2 * Math.PI / 60 * (sec / 60) - Math.PI / 2)
ctx.beginPath()
ctx.moveTo(-10, 0)
ctx.lineTo(60, 0)
ctx.lineWidth = 5
ctx.strokeStyle = 'blue'
ctx.stroke()
ctx.closePath()
ctx.restore()
ctx.save()

//秒针
ctx.rotate(2 * Math.PI / 60 * sec -  - Math.PI / 2)
ctx.beginPath()
ctx.moveTo(-10, 0)
ctx.lineTo(80, 0)
ctx.strokeStyle = 'red'
ctx.stroke()
ctx.closePath()
ctx.restore()
ctx.save()

// 绘制刻度,也是跟绘制时分秒针一样,只不过刻度是死的
ctx.lineWidth = 1
for (let i = 0; i < 60; i++) {
    ctx.rotate(2 * Math.PI / 60)
    ctx.beginPath()
    ctx.moveTo(90, 0)
    ctx.lineTo(100, 0)
    // ctx.strokeStyle = 'red'
    ctx.stroke()
    ctx.closePath()
}
ctx.restore()
ctx.save()
ctx.lineWidth = 5
for (let i = 0; i < 12; i++) {
    ctx.rotate(2 * Math.PI / 12)
    ctx.beginPath()
    ctx.moveTo(85, 0)
    ctx.lineTo(100, 0)
    ctx.stroke()
    ctx.closePath()
}

ctx.restore()

截屏2021-07-19 下午10.53.53.png

The last step is to update the view to make the clock turn. The first thing that comes to mind must be the timer setInterval , but pay attention to a problem: every time you update the view, you must clear the previous canvas, and then start to draw a new view, otherwise you will There will be a scene of Thousand-handed Avalokitesvara

截屏2021-07-19 下午10.57.05.png

Attach the final code:

const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')

setInterval(() => {
    ctx.save()
    ctx.clearRect(0, 0, 600, 600)
    ctx.translate(300, 300) // 设置中心点,此时300,300变成了坐标的0,0
    ctx.save()

    // 画大圆
    ctx.beginPath()
    // 画圆线使用arc(中心点X,中心点Y,半径,起始角度,结束角度)
    ctx.arc(0, 0, 100, 0, 2 * Math.PI)
    ctx.stroke() // 执行画线段的操作
    ctx.closePath()

    // 画小圆
    ctx.beginPath()
    ctx.arc(0, 0, 5, 0, 2 * Math.PI)
    ctx.stroke()
    ctx.closePath()

    // 获取当前 时,分,秒
    let time = new Date()
    let hour = time.getHours() % 12
    let min = time.getMinutes()
    let sec = time.getSeconds()

    // 时针
    ctx.rotate(2 * Math.PI / 12 * hour + 2 * Math.PI / 12 * (min / 60) - Math.PI / 2)
    ctx.beginPath()
    // moveTo设置画线起点
    ctx.moveTo(-10, 0)
    // lineTo设置画线经过点
    ctx.lineTo(40, 0)
    // 设置线宽
    ctx.lineWidth = 10
    ctx.stroke()
    ctx.closePath()
    ctx.restore()
    ctx.save()

    // 分针
    ctx.rotate(2 * Math.PI / 60 * min + 2 * Math.PI / 60 * (sec / 60) - Math.PI / 2)
    ctx.beginPath()
    ctx.moveTo(-10, 0)
    ctx.lineTo(60, 0)
    ctx.lineWidth = 5
    ctx.strokeStyle = 'blue'
    ctx.stroke()
    ctx.closePath()
    ctx.restore()
    ctx.save()

    //秒针
    ctx.rotate(2 * Math.PI / 60 * sec - Math.PI / 2)
    ctx.beginPath()
    ctx.moveTo(-10, 0)
    ctx.lineTo(80, 0)
    ctx.strokeStyle = 'red'
    ctx.stroke()
    ctx.closePath()
    ctx.restore()
    ctx.save()

    // 绘制刻度,也是跟绘制时分秒针一样,只不过刻度是死的
    ctx.lineWidth = 1
    for (let i = 0; i < 60; i++) {
        ctx.rotate(2 * Math.PI / 60)
        ctx.beginPath()
        ctx.moveTo(90, 0)
        ctx.lineTo(100, 0)
        // ctx.strokeStyle = 'red'
        ctx.stroke()
        ctx.closePath()
    }
    ctx.restore()
    ctx.save()
    ctx.lineWidth = 5
    for (let i = 0; i < 12; i++) {
        ctx.rotate(2 * Math.PI / 12)
        ctx.beginPath()
        ctx.moveTo(85, 0)
        ctx.lineTo(100, 0)
        // ctx.strokeStyle = 'red'
        ctx.stroke()
        ctx.closePath()
    }

    ctx.restore()
    ctx.restore()
}, 1000)

Effect very good ah:

clock的副本.gif

2. Canvas implement scratch card

When I was a child, many people bought a recharge card, and everyone who knows it understands it. Scratch the gray skin with your fingernails, and you can see the answer below.
截屏2021-07-19 下午11.02.09.png

The idea is this:

  • 1. The bottom answer is a div , the top gray skin is a canvas , and canvas initially covers div
  • 2, mouse events, and clicks when you move the mouse through the paths painting round open, and set globalCompositeOperation to destination-out , the path of the mouse through all become transparent, a transparent, naturally, showing answer information below .

Regarding fill , it is actually a reference to stroke , fill is to fill the graphics, and stroke is just to draw a border line

// html
<canvas id="canvas" width="400" height="100"></canvas>
<div class="text">恭喜您获得100w</div>
<style>
        * {
            margin: 0;
            padding: 0;
        }
        .text {
            position: absolute;
            left: 130px;
            top: 35px;
            z-index: -1;
        }
</style>


// js
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')

// 填充的颜色
ctx.fillStyle = 'darkgray'
// 填充矩形 fillRect(起始X,起始Y,终点X,终点Y)
ctx.fillRect(0, 0, 400, 100)
ctx.fillStyle = '#fff'
// 绘制填充文字
ctx.fillText('刮刮卡', 180, 50)

let isDraw = false
canvas.onmousedown = function () {
    isDraw = true
}
canvas.onmousemove = function (e) {
    if (!isDraw) return
    // 计算鼠标在canvas里的位置
    const x = e.pageX - canvas.offsetLeft
    const y = e.pageY - canvas.offsetTop
    // 设置globalCompositeOperation
    ctx.globalCompositeOperation = 'destination-out'
    // 画圆
    ctx.arc(x, y, 10, 0, 2 * Math.PI)
    // 填充圆形
    ctx.fill()
}
canvas.onmouseup = function () {
    isDraw = false
}

The effect is as follows:

guaguaka.gif

3. Canvas implements drawing board and saving

Frame: Use vue + elementUI

It's actually very simple, the difficulties are as follows:

  • 1. Drag and drop the mouse to draw squares and circles
  • 2. After drawing a save canvas, it will be superimposed when drawing again next time
  • 3. Save the picture

The first point is that you only need to calculate the point coordinates of the mouse click and the current coordinates of the mouse. You can calculate the length and width of the rectangle: x - beginX, y - beginY , and the circle uses the Pythagorean theorem: Math.sqrt((x - beginX) * (x - beginX) + (y - beginY) * (y - beginY))

The second point is to use the getImageData and putImageData methods of canvas

The third point, the idea is to canvas and assign it to the a tag with download function, and actively click the a tag to download the

Take a look at the effect:

截屏2021-07-19 下午11.16.24.png

截屏2021-07-19 下午11.17.41.png

I will not explain the specific code too much. It is not difficult to say that it is not difficult. As long as the first two projects are understood, this project is easy to understand:

<template>
  <div>
    <div style="margin-bottom: 10px; display: flex; align-items: center">
      <el-button @click="changeType('huabi')" type="primary">画笔</el-button>
      <el-button @click="changeType('rect')" type="success">正方形</el-button>
      <el-button
        @click="changeType('arc')"
        type="warning"
        style="margin-right: 10px"
        >圆形</el-button
      >
      <div>颜色:</div>
      <el-color-picker v-model="color"></el-color-picker>
      <el-button @click="clear">清空</el-button>
      <el-button @click="saveImg">保存</el-button>
    </div>
    <canvas
      id="canvas"
      width="800"
      height="400"
      @mousedown="canvasDown"
      @mousemove="canvasMove"
      @mouseout="canvasUp"
      @mouseup="canvasUp"
    >
    </canvas>
  </div>
</template>

<script>
export default {
  data() {
    return {
      type: "huabi",
      isDraw: false,
      canvasDom: null,
      ctx: null,
      beginX: 0,
      beginY: 0,
      color: "#000",
      imageData: null,
    };
  },
  mounted() {
    this.canvasDom = document.getElementById("canvas");
    this.ctx = this.canvasDom.getContext("2d");
  },
  methods: {
    changeType(type) {
      this.type = type;
    },
    canvasDown(e) {
      this.isDraw = true;
      const canvas = this.canvasDom;
      this.beginX = e.pageX - canvas.offsetLeft;
      this.beginY = e.pageY - canvas.offsetTop;
    },
    canvasMove(e) {
      if (!this.isDraw) return;
      const canvas = this.canvasDom;
      const ctx = this.ctx;
      const x = e.pageX - canvas.offsetLeft;
      const y = e.pageY - canvas.offsetTop;
      this[`${this.type}Fn`](ctx, x, y);
    },
    canvasUp() {
      this.imageData = this.ctx.getImageData(0, 0, 800, 400);
      this.isDraw = false;
    },
    huabiFn(ctx, x, y) {
      ctx.beginPath();
      ctx.arc(x, y, 5, 0, 2 * Math.PI);
      ctx.fillStyle = this.color;
      ctx.fill();
      ctx.closePath();
    },
    rectFn(ctx, x, y) {
      const beginX = this.beginX;
      const beginY = this.beginY;
      ctx.clearRect(0, 0, 800, 400);
      this.imageData && ctx.putImageData(this.imageData, 0, 0, 0, 0, 800, 400);
      ctx.beginPath();
      ctx.strokeStyle = this.color;
      ctx.rect(beginX, beginY, x - beginX, y - beginY);
      ctx.stroke();
      ctx.closePath();
    },
    arcFn(ctx, x, y) {
      const beginX = this.beginX;
      const beginY = this.beginY;
      this.isDraw && ctx.clearRect(0, 0, 800, 400);
      this.imageData && ctx.putImageData(this.imageData, 0, 0, 0, 0, 800, 400);
      ctx.beginPath();
      ctx.strokeStyle = this.color;
      ctx.arc(
        beginX,
        beginY,
        Math.round(
          Math.sqrt((x - beginX) * (x - beginX) + (y - beginY) * (y - beginY))
        ),
        0,
        2 * Math.PI
      );
      ctx.stroke();
      ctx.closePath();
    },
    saveImg() {
      const url = this.canvasDom.toDataURL();
      const a = document.createElement("a");
      a.download = "sunshine";
      a.href = url;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
    },
    clear() {
        this.imageData = null
        this.ctx.clearRect(0, 0, 800, 400)
    }
  },
};
</script>

<style lang="scss" scoped>
#canvas {
  border: 1px solid black;
}
</style>

Epilogue

I'm Lin Sanxin, an enthusiastic front-end rookie programmer. If you are motivated, like the front-end, and want to learn the front-end, then we can make friends and fish together haha, touch the fish group, add me, please note [Si No]

image.png


Sunshine_Lin
2.1k 声望7.1k 粉丝