## Learning the realization of canvas and simple drawing

Coke loves to stay at home 2021-08-08 17:58:45
learning realization canvas simple drawing

Study canvas On and off for a few days , Except look API, I don't know how to learn . therefore , Started trying to simulate some gadgets , Of course, the function is simple , After all, it's still in the beginning stage , It's too complicated to do , And undermine confidence , reasoning , Want to simulate a drawing tool , But I didn't expect to draw a difficult horse , I was badly shut down .

Let's take a look at the rendering ：

My current strength is too good , It feels like a lot of energy has been spent to do it here , It's really hard to draw .

## Knowledge point

Except for the ones I just learned and used `API` outside , When drawing , I have come into contact with `strokeRect`,`arc`,`toDataURL` etc. `API` Usage of , Of course, it's also relatively simple ：

Here are two ways to draw graphics `API` Well ：

1. `strokeRect`： Is to draw a rectangle in the form of lines ;

• Parameters ：
`x`： Start abscissa position ;
`y`： Start ordinate position ;
`width`： The width of the rectangle drawn ;
`height`： The height of the rectangle drawn ;
2. `arc`： Is to draw a rectangle in the form of lines ;

• Parameters ：
`x`： Abscissa position of the center of the circle ;
`y`： The ordinate position of the center of the circle ;
`r`： The radius of the circle ; `sAngle`： Starting angle , In radians ;
`eAngle`： End angle , In radians ;
`counterclockwise`： Optional . Specify whether the drawing should be counter clockwise or clockwise .False = Clockwise ,true = Anti-clockwise ;

Let's look at a simple example ：

``````ctx.beginPath()
ctx.strokeStyle = 'red'
ctx.strokeRect(10, 10, 100, 100)
ctx.beginPath()
ctx.strokeStyle = 'green'
ctx.arc(60, 60, 50, 0, 2 * Math.PI)
ctx.stroke()
Copy code ``````

You can see `arc` and `strokeRect` There are still some differences in the use of ,`arc` It just describes the path , And drawing still needs `stroke` To achieve , You can see from the two names .

## Implement drawing

It is not difficult to implement a simple rendering function , Of course, if it's more complicated , Now I think ： That's too hard, too hard , There are so many things to consider and do .

### reflection

Implement a simple drawing function , There are three elements to consider ：

• Writing point ;
• Move pen path ;
• Close the pen ;

These three elements , They can be separated by `mousedown`,`mousemove`,`mouseup` To achieve ：

API Concept
mousedown Trigger when the mouse is pressed
mousemove Trigger when the mouse moves
mouseup Trigger when the mouse pops up

here , because `addEventListener` You can listen repeatedly to bind events , When switching drawing graphics, it will cause `Bug`, There is no good way to solve ; So I didn't use this `API` To bind events , It's the old `onmousedown` This form .

### Start

good , Now that you have the idea , Then start to realize , Let's write the event monitoring of the mouse first, and then talk about the next step ：

``````cvs.onmousedown = function (e) {
isStart = true
console.log('mousedown')
cvs.onmousemove = function (e) {
isStart && console.log('onmousemove')
}
}
cvs.onmouseup = function (e) {
isStart = false
console.log('onmouseup')
}
Copy code ``````

Here we put `mousemove` Events should be bound to `mousedown` Inside , Because when we expect ： When the pen falls and moves , Drawing .
I looked at the execution effect of the browser , A problem has been identified , Namely `mousemove` The frequency of execution is simply too fast ：

You can see , I just slide a few times , Hundreds of events on execution , If you add the code that needs to operate the canvas later , Efficiency must be slow , So I added throttling here ：

``````const move = throttle(function (e) {
console.log('mousemove')
})
cvs.onmousedown = function (e) {
isStart = true
console.log('mousedown')
cvs.onmousemove = function (e) {
isStart && move.call(this, e)
}
}
function throttle (callback, delay = 10) {
let timer = null
return function () {
const ctx = this
if (timer) {
return
}
timer = setTimeout(() => {
callback.apply(ctx, arguments)
timer = null
}, delay)
}
}
Copy code ``````

The optimization effect ：

After using throttling , It is obvious that the frequency of execution is not as happy as before , So it's a little acceptable .

### draw

After listening to the event , We have to think about how to draw graphics on the drawing board .

#### Draw directly

A painting drawn directly , It's very simple , All we need to do is `mousemove` When , Use `context` Object to draw, each pixel can achieve the goal ：

``````const move = throttle(function (e) {
ctx.lineTo(e.offsetX, e.offsetY)
ctx.stroke()
})
cvs.onmousedown = function (e) {
isStart = true
ctx.beginPath()
ctx.strokeStyle = '#000'
cvs.onmousemove = function (e) {
isStart && move.call(this, e)
}
}
Copy code ``````

The point is to `mousedown` When `beginPath`, Reopen the drawing path , To avoid connecting with the last drawing ; And then through `mousemove` Events continue to draw on OK 了 .

#### Draw a straight line / Draw a rectangle / Draw a circle

It's easy to draw directly , But if you draw a straight line , Just a little bit of trouble ： Because a straight line is made up of two points , And we `mousemove` But there will be many points , After I think , That's how it works ：

Record `mouseup` The first position , As starting point ; Then record each `mousemove` As the end point , And then connect the city in a straight line . that `mousemove` How to get the last point of ？

I pass here every time `mousemove` when ,`celarRect` Canvas information , And then again `lineTo` The first point , In this way, the effect can be achieved ！

``````const move = throttle(function (e) {
ctx.clearRect(0, 0, 600, 600)
ctx.moveTo(start.x, start.y)
ctx.lineTo(e.offsetX, e.offsetY)
ctx.stroke()
})
cvs.onmousedown = function (e) {
isStart = true
ctx.beginPath()
ctx.strokeStyle = '#000'
start.x = e.offsetX
start.y = e.offsetY
cvs.onmousemove = function (e) {
isStart && move.call(this, e)
}
}
Copy code ``````

This seems good . however , But something strange happened ：

You can see , The line drawn each time is not `clearRect` Get rid of , Thought for a long time and didn't find the reason , Later, it was found that when cleared , Re open the drawing path and there will be no problem ：

``````const move = throttle(function (e) {
ctx.clearRect(0, 0, 600, 600)
ctx.beginPath()
ctx.moveTo(start.x, start.y)
ctx.lineTo(e.offsetX, e.offsetY)
ctx.stroke()
})
cvs.onmousedown = function (e) {
isStart = true
ctx.beginPath()
ctx.strokeStyle = '#000'
start.x = e.offsetX
start.y = e.offsetY
cvs.onmousemove = function (e) {
isStart && move.call(this, e)
}
}
Copy code ``````

In this case , The effect is normal . Drawing a rectangle is the same idea as drawing a circle , It's called `API` When , There are some differences in processing . such , It seems to have achieved the effect ？
The answer is NO, We found another problem , Through constant `clearRect` After clearing the canvas , The previous painting will also be removed , That such , How to draw a picture ！

#### Keep records

After thinking for a while , My idea is to introduce a method to save drawing records `record` An array of objects , Use this array to save each drawing operation , And then again `clearRect` After emptying the canvas , Then draw the path in the record again （ If it's more complicated later , Through this record, you can even realize the function of revocation ）.

``````const move = throttle(function (e) {
ctx.clearRect(0, 0, 600, 600)
record.forEach(item => {
ctx.beginPath()
ctx.moveTo(item.x1, item.y1)
ctx.lineTo(item.x2, item.y2)
ctx.stroke()
})
ctx.beginPath()
ctx.moveTo(start.x, start.y)
ctx.lineTo(e.offsetX, e.offsetY)
ctx.stroke()
})
cvs.onmousedown = function (e) {
isStart = true
ctx.beginPath()
ctx.strokeStyle = '#000'
start.x = e.offsetX
start.y = e.offsetY
cvs.onmousemove = function (e) {
isStart && move.call(this, e)
}
}
cvs.onmouseup = function (e) {
isStart = false
record.push({ x1: start.x, y1: start.y, x2: e.offsetX, y2: e.offsetY})
}
Copy code ``````

such , You can keep the effect you painted before . In this way , Draw the circle 、 rectangular 、 And direct drawing should be recorded , It's just a little different .

#### Save the picture

Drawing graphics, we're done , After that, you should save the picture . Save the picture , There's a saying before `API` yes `toDataURL`, This `API` No `context` Contextual , It is `canvas` Methods ：

``````const canvas = document.querySelector('canvas')
const url = canvas.toDataURL()
Copy code ``````

This `API` There are two parameters ： The first is type , The default is ：`image/png`; The second is quality ： The default is ：0.92;

Let's see toDataURL MDN file .

Know this `API` After how to use , Next, we can finish the operation of saving the picture ：

``````function saveImage () {
}
Copy code ``````

Of course , adopt `toDataURL` After getting the data , It can also be assigned to pictures , Achieve preview effect , It should be noted that ：

1. Here I pass `createElemennt` Created images , direct `src` Assignment is invalid , But through `new Image` Object to implement ;
2. The picture is actually through `base64` The way to show .

## summary

Then paste the final effect picture ：

I thought drawing was simple , But I didn't expect to encounter many difficulties in realizing the most basic rendering .
and , If you want to implement more complex functions in the future , The difficulty is getting bigger and bigger , But also good , Knock out a Demo Come on , It's barely qualified .
Back up , Let's evaluate the implementation difficulty first , Let's learn from the simple .

https://qdmana.com/2021/08/20210808175448003d.html