# A kind of Canvas realizes drawing, zooming, moving and saving historical state on the picture, which is pure dry goods (with css3 conversion formula attached)

Hahaha, I'm here again. This time, I'll bring you some articles about canvas functions. I hope you like them! This css3 change formula can be applied to the transform attribute we usually use or the map zooming on the mobile terminal!

# Preface

Because I am a junior, I am looking for an internship recently. Before that, I had a small project of my own:

github.com/zhcxk1998/S...

The interviewer said that he could think deeply, maybe add some new functions to increase the difficulty of the project. He put forward several suggestions, one of which is online marking of the test paper, on which the teacher can annotate the homework, circle and wait for me to start to study this subject that night, and finally I have found out!

The canvas painting brush is used, which is translated and zoomed by the transform attribute of css3. Considering that if the attributes of canvas such as drawImage or scale are used to change, the generated image will also have an impact, thinking about the direct change of css3, and canvas is used to do the functions such as brush. What are the tricks of the big guys? Give me some tips in the comment area!

(I hope you can leave your precious praise and star hee hee)

# Formula deduction

If you don't want to see how the formula is derived, you can skip to see the specific implementation later~

## 1. Coordinate conversion formula

### Introduction to conversion formula

In fact, at the beginning, I wanted to find out if there was any relevant information on the Internet, but unfortunately I couldn't find it, so I pushed it out slowly. Let me give you an example of abscissa!

General formula

This formula means that the coordinates pressed by the mouse can be converted into relative coordinates in the canvas through the formula, which is particularly important

(transformOrigin - downX) / scale * (scale-1) + downX - translateX = pointX

Parameter interpretation

```Transformrigin: the base point of the transform change (this attribute controls where the element changes)
downX: the coordinates of the mouse down (note that the left offset distance of the container needs to be subtracted when using, because we want the coordinates relative to the container)
Scale: scale multiple, default is 1
translateX: distance to translate
Copy code```

### Derivation process

This formula, in fact, is more general. It can be used in other scenarios where the transform attribute is used. As for how to deduce it, I use a stupid method

The specific test code, placed at the end of the article, needs to be taken by itself~

1. First make two identical elements, mark the coordinates, and set the container property overflow:hidden to hide the overflow content

ok, now there are two same matrices. Let's mark them with some red dots, and then we will transform the css3 style on the left

The width and height of the rectangle is 360px * 360px. Let's define its change attribute. Select the positive center of the change base point, and zoom in three times

```// css
transform-origin: 180px 180px;
transform: scale(3, 3);
Copy code```

The results are as follows

ok, let's compare the above results and find that when we zoom in three times, the black square in the middle occupies the whole width. Next, we can compare these points with the original unchanged rectangle (on the right) to get their coordinates

2. Start to compare the two coordinates, and then deduce the formula

Now let's give a simple example, for example, let's calculate the coordinates of the upper left corner (now it's marked yellow)

In fact, we can calculate the relationship of coordinates directly
(the value of the coordinate on the left here is the coordinate we press)
(the value of the coordinate on the left here is the coordinate we press)
(the value of the coordinate on the left here is the coordinate we press)

• Because the width and height are 360px, they are divided into three equal parts, each with a width of 120px
• Because the width and height of the container will not change after the change, only the rectangle itself will change
• We can see that the yellow mark coordinate on the left is x:120 y:0, and the yellow mark on the right is x:160 y:120

This coordinate may be a little special. Let's change it a few times to calculate it

• Blue mark: left: x:120 y:120, right: x: 160 y:160
• Green mark: left: x: 240 y:240, right: x: 200: y:200

Well, we can almost get the relationship between coordinates. We can make a table

Still feel uneasy? Instead, we can calculate the zoom ratio and the width and height of the container

I don't know if you feel it. Then we can slowly deduce the general formula according to the coordinates
(transformOrigin - downX) / scale * (scale-1) + down - translateX = point

Of course, we may not try to translate x, which is a little simpler. If we simulate it in the brain, we will know that we can subtract the displacement distance. Let's test it

Let's change the style and add the displacement distance

``````transform-origin: 180px 180px;
transform: scale(3, 3) translate(-40px,-40px);
Copy code``````

It's still the state above us. ok, now the blue and green marks correspond to each other. Let's take a look at the current coordinates

• Blue: left: x:0 y:0, right: x:160 y:160
• Green: left: x:120 y:120, right: x:200 y:200

Let's use the formula to figure out how the coordinates are (after coordinate conversion)

• Blue: left: x:120 y:120, right: x:160 y:160
• Green: left: x:160 y:160, right: x:200 y:200

It's not hard to find that we are actually different from the displacement distance translateX/translateY, so we only need to subtract the displacement distance to complete the coordinate conversion

# Test formula

According to the above formula, we can simply test it! Can this formula work in the end!!!

Let's use the demo above to test whether the location of a tag is displayed correctly if the element changes. It looks ok

``````const wrap = document.getElementById('wrap')
wrap.onmousedown = function (e) {
const downX = e.pageX - wrap.offsetLeft
const downY = e.pageY - wrap.offsetTop

const scale = 3
const translateX = -40
const translateY = -40
const transformOriginX = 180
const transformOriginY = 180

const dot = document.getElementById('dot')
dot.style.left = (transformOriginX - downX) / scale * (scale - 1) + downX - translateX + 'px'
dot.style.top = (transformOriginY - downY) / scale * (scale - 1) + downY - translateY + 'px'
}
Copy code``````

It may be asked why offset left and offset top should be subtracted. As we have repeatedly stressed above, we calculate the coordinates of the mouse click, and the coordinates are still relative to the coordinates of the display container, so we need to subtract the offset of the container itself.

# Component design

Now that demo and other components have been tested, let's analyze how to design this component one by one (it's still low configuration version at present, and then optimize it)

## 1. Basic canvas composition

Let's start with a brief analysis of this structure. In fact, it's mainly a container of canvas and a toolbar on the right. That's all

``````<div className="mark-paper__wrap" ref={wrapRef}>
<canvas
ref={canvasRef}
className="mark-paper__canvas">
<p>Unfortunately, this thing doesn't work with your computer!</p>
</canvas>
<div className="mark-paper__sider" />
</div>
Copy code``````

The only thing we need is that the container needs to set the property overflow: hidden to hide the overflow content of the internal canvas, that is, we need to control the visible area. At the same time, we need to get the width and height of the container dynamically to set the size of canvas

## 2. Initialize canvas and fill image

We can get a method to initialize and fill the canvas. The following is the main part of the screenshot, which is actually to set the size of canvas and fill our pictures

``````const fillImage = async () => {
// Omit here

const img: HTMLImageElement = new Image()

img.src = await getURLBase64(fillImageSrc)
canvas.width = img.width
canvas.height = img.height
context.drawImage(img, 0, 0)

// Set the change base point to the center of the canvas container
canvas.style.transformOrigin = `\${wrap?.offsetWidth / 2}px \${wrap?.offsetHeight / 2}px`
// Remove the effect of the last change
canvas.style.transform = ''
}
}
Copy code``````

## 3. Monitor various mouse events on canvas

To control the movement, we can first find a way to listen to various events of the canvas mouse, and distinguish different modes to handle different events

``````const handleCanvas = () => {
const { current: canvas } = canvasRef
const { current: wrap } = wrapRef
const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d')
if (!context || !wrap) return

// Clear the last set listening to prevent getting parameter errors
wrap.onmousedown = null
wrap.onmousedown = function (event: MouseEvent) {
const downX: number = event.pageX
const downY: number = event.pageY

// Distinguish the mouse mode we choose now: move, brush, eraser
switch (mouseMode) {
case MOVE_MODE:
handleMoveMode(downX, downY)
break
case LINE_MODE:
handleLineMode(downX, downY)
break
case ERASER_MODE:
handleEraserMode(downX, downY)
break
default:
break
}
}
Copy code``````

## 4. Move the canvas

This is easier to do. We only need to use the coordinates pressed by the mouse and the distance we drag to move the canvas. Because the latest displacement distance needs to be calculated for each move, we can define several variables for calculation.

This is listening to the mouse events of the container, not the events of the canvas, because in this way, we can move beyond the boundary again

To summarize briefly:

• Incoming mouse down coordinates
• Calculate the current displacement distance and update the effect of css change
• Update the latest displacement state when the mouse is raised
``````// Define some variables to save the current / latest movement status
// Distance of current displacement
const translatePointXRef: MutableRefObject<number> = useRef(0)
const translatePointYRef: MutableRefObject<number> = useRef(0)
// Displacement distance at the end of last displacement
const fillStartPointXRef: MutableRefObject<number> = useRef(0)
const fillStartPointYRef: MutableRefObject<number> = useRef(0)

// Monitor function when moving
const handleMoveMode = (downX: number, downY: number) => {
const { current: canvas } = canvasRef
const { current: wrap } = wrapRef
const { current: fillStartPointX } = fillStartPointXRef
const { current: fillStartPointY } = fillStartPointYRef
if (!canvas || !wrap || mouseMode !== 0) return

// Add a move event to the container to move the picture in the space
wrap.onmousemove = (event: MouseEvent) => {
const moveX: number = event.pageX
const moveY: number = event.pageY

// Update the current displacement distance, the value is: the coordinate at the end of the last displacement + the distance moved
translatePointXRef.current = fillStartPointX + (moveX - downX)
translatePointYRef.current = fillStartPointY + (moveY - downY)

// Update css changes on canvas
canvas.style.transform = `scale(\${canvasScale},\${canvasScale}) translate(\${translatePointXRef.current}px,\${translatePointYRef.current}px)`
}

wrap.onmouseup = (event: MouseEvent) => {
const upX: number = event.pageX
const upY: number = event.pageY

// Cancel event listening
wrap.onmousemove = null
wrap.onmouseup = null;

// When the mouse is raised, update the "last unique ending coordinate"
fillStartPointXRef.current = fillStartPointX + (upX - downX)
fillStartPointYRef.current = fillStartPointY + (upY - downY)
}
}

Copy code``````

## 5. Zoom the canvas

To zoom the canvas, I mainly use the slider on the right and the mouse wheel. First of all, we can add the event of monitoring the wheel to the function of monitoring the canvas mouse event

To summarize:

• Monitor the change of mouse wheel
• Update zoom factor and change style
``````// Monitor the mouse wheel and update the zoom ratio of canvas
const handleCanvas = () => {
const { current: wrap } = wrapRef

// Omit ten thousand words

wrap.onwheel = null
wrap.onwheel = (e: MouseWheelEvent) => {
const { deltaY } = e
// Note here that I use 0.1 to increase and decrease, but because JS uses IEEE 754 to calculate, there is a problem with precision. Let's deal with it ourselves
const newScale: number = deltaY > 0
? (canvasScale * 10 - 0.1 * 10) / 10
: (canvasScale * 10 + 0.1 * 10) / 10
if (newScale < 0.1 || newScale > 2) return
setCanvasScale(newScale)
}
}

// Monitor slider to control zoom
<Slider
min={0.1}
max={2.01}
step={0.1}
value={canvasScale}
tipFormatter={(value) => `\${(value).toFixed(2)}x`}
onChange={handleScaleChange} />

const handleScaleChange = (value: number) => {
setCanvasScale(value)
}
Copy code``````

Then we use the side effect function of hooks to update the style depending on the canvas zoom ratio

``````//Monitor zoom canvas
useEffect(() => {
const { current: canvas } = canvasRef
const { current: translatePointX } = translatePointXRef
const { current: translatePointY } = translatePointYRef
canvas && (canvas.style.transform = `scale(\${canvasScale},\${canvasScale}) translate(\${translatePointX}px,\${translatePointY}px)`)
}, [canvasScale])
Copy code``````

## 6. Realize brush drawing

This needs to use the formula we deduced before! Because, think about it carefully, if we zoom in and out of the displacement, the coordinates of the mouse button may change with respect to the canvas, so we need to convert it to achieve the effect of one-to-one correspondence between the mouse button position and the canvas position

To summarize:

• Incoming mouse down coordinates
• Start drawing in corresponding coordinates through formula conversion
• Cancel event monitoring when the mouse is raised
``````// Use the formula to change the coordinates
const generateLinePoint = (x: number, y: number) => {
const { current: wrap } = wrapRef
const { current: translatePointX } = translatePointXRef
const { current: translatePointY } = translatePointYRef
const wrapWidth: number = wrap?.offsetWidth || 0
const wrapHeight: number = wrap?.offsetHeight || 0
// Scaling displacement coordinate change law
// (transformOrigin - downX) / scale * (scale-1) + downX - translateX = pointX
const pointX: number = ((wrapWidth / 2) - x) / canvasScale * (canvasScale - 1) + x - translatePointX
const pointY: number = ((wrapHeight / 2) - y) / canvasScale * (canvasScale - 1) + y - translatePointY

return {
pointX,
pointY
}
}

// Listen to mouse brush events
const handleLineMode = (downX: number, downY: number) => {
const { current: canvas } = canvasRef
const { current: wrap } = wrapRef
const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d')
if (!canvas || !wrap || !context) return

const offsetLeft: number = canvas.offsetLeft
const offsetTop: number = canvas.offsetTop
// Subtract the offset distance of the canvas (calculate the coordinates based on the canvas)
downX = downX - offsetLeft
downY = downY - offsetTop

const { pointX, pointY } = generateLinePoint(downX, downY)
context.globalCompositeOperation = "source-over"
context.beginPath()
// Set brush start point
context.moveTo(pointX, pointY)

canvas.onmousemove = null
canvas.onmousemove = (event: MouseEvent) => {
const moveX: number = event.pageX - offsetLeft
const moveY: number = event.pageY - offsetTop
const { pointX, pointY } = generateLinePoint(moveX, moveY)
// Start drawing brush lines~
context.lineTo(pointX, pointY)
context.stroke()
}
canvas.onmouseup = () => {
context.closePath()
canvas.onmousemove = null
canvas.onmouseup = null
}
}
Copy code``````

## 7. Realization of eraser

At present, there are still some problems with eraser. Now, we use the canvas background image + globalCompositeOperation property to simulate the implementation of eraser. However, after the image is generated, the trace of eraser will turn white instead of transparent

This step is similar to the brush implementation, with only a little change

• Set the property context.globalcompositeoperation = "destination out"
``````// At present, there are still some problems with the eraser. The front display is normal. Save the picture, and the erasing trace will turn white
const handleEraserMode = (downX: number, downY: number) => {
const { current: canvas } = canvasRef
const { current: wrap } = wrapRef
const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d')
if (!canvas || !wrap || !context) return

const offsetLeft: number = canvas.offsetLeft
const offsetTop: number = canvas.offsetTop
downX = downX - offsetLeft
downY = downY - offsetTop

const { pointX, pointY } = generateLinePoint(downX, downY)

context.beginPath()
context.moveTo(pointX, pointY)

canvas.onmousemove = null
canvas.onmousemove = (event: MouseEvent) => {
const moveX: number = event.pageX - offsetLeft
const moveY: number = event.pageY - offsetTop
const { pointX, pointY } = generateLinePoint(moveX, moveY)
context.globalCompositeOperation = "destination-out"
context.lineWidth = lineWidth
context.lineTo(pointX, pointY)
context.stroke()
}
canvas.onmouseup = () => {
context.closePath()
canvas.onmousemove = null
canvas.onmouseup = null
}
}
Copy code``````

## 8. Function realization of cancellation and recovery

In this case, we first need to understand the logic of common undo and restore functions

• Undo is not allowed if the current state is in the first position
• If the current state is in the last position, recovery is not allowed
• If the current status is revoked but updated, the current status is taken as the latest status (that is, recovery is not allowed, and the newly updated status is the latest)

### Update of canvas status

So we need to set some variables to save the status list and the status subscript of the current brush

``````// Definition parameters stored in Dongdong
const canvasHistroyListRef: MutableRefObject<ImageData[]> = useRef([])
const [canvasCurrentHistory, setCanvasCurrentHistory] = useState<number>(0)
Copy code``````

We also need to add the current state to the list when initializing canvas as the first empty canvas state

``````const fillImage = async () => {
// Omit ten thousand words

img.src = await getURLBase64(fillImageSrc)
const imageData: ImageData = context.getImageData(0, 0, canvas.width, canvas.height)
canvasHistroyListRef.current = []
canvasHistroyListRef.current.push(imageData)
setCanvasCurrentHistory(1)
}
}
Copy code``````

Then we will implement it. When the brush is updated, we also need to add the current state to the brush state list, update the subscript corresponding to the current state, and deal with some details

To summarize:

• When the mouse is raised, get the current canvas state
• Add to status list and update status subscript
• If it is currently in the undo state, if the brush is used to update the state, the current latest state will be cleared, and the state after the original position will be cleared
``````const handleLineMode = (downX: number, downY: number) => {
// Omit ten thousand words
canvas.onmouseup = () => {
const imageData: ImageData = context.getImageData(0, 0, canvas.width, canvas.height)

// If it is in the undo state and the brush is used at this time, the later state will be cleared and the newly drawn one will be the latest canvas state
if (canvasCurrentHistory < canvasHistroyListRef.current.length) {
canvasHistroyListRef.current = canvasHistroyListRef.current.slice(0, canvasCurrentHistory)
}
canvasHistroyListRef.current.push(imageData)
setCanvasCurrentHistory(canvasCurrentHistory + 1)
context.closePath()
canvas.onmousemove = null
canvas.onmouseup = null
}
}
Copy code``````

### Revocation and restoration of canvas state

ok, in fact, we have finished the update of canvas status. Next, we need to deal with the function of state revocation and recovery

Let's define this toolbar first

Then we set the corresponding events, which are undo, restore and clear, which are easy to understand. The most important thing is to deal with the boundary situation.

``````const handleRollBack = () => {
const isFirstHistory: boolean = canvasCurrentHistory === 1
if (isFirstHistory) return
setCanvasCurrentHistory(canvasCurrentHistory - 1)
}

const handleRollForward = () => {
const { current: canvasHistroyList } = canvasHistroyListRef
const isLastHistory: boolean = canvasCurrentHistory === canvasHistroyList.length
if (isLastHistory) return
setCanvasCurrentHistory(canvasCurrentHistory + 1)
}

const handleClearCanvasClick = () => {
const { current: canvas } = canvasRef
const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d')
if (!canvas || !context || canvasCurrentHistory === 0) return

// Empty canvas history
canvasHistroyListRef.current = [canvasHistroyListRef.current[0]]
setCanvasCurrentHistory(1)

message.success('Canvas cleared successfully!')
}
Copy code``````

After the event is set, we can start to listen to the current status subscript of the canvas current history and use the side effect function to process it

``````useEffect(() => {
const { current: canvas } = canvasRef
const { current: canvasHistroyList } = canvasHistroyListRef
const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d')
if (!canvas || !context || canvasCurrentHistory === 0) return
context?.putImageData(canvasHistroyList[canvasCurrentHistory - 1], 0, 0)
}, [canvasCurrentHistory])
Copy code``````

Fill canvas with image information!

So it's done!!!

## 9. Change mouse icon

Let's deal with it briefly. The brush mode is the icon of the brush. The mouse in the eraser mode is the eraser, and the move mode is the ordinary move icon

When switching modes, set different icons

``````const handleMouseModeChange = (event: RadioChangeEvent) => {
const { target: { value } } = event
const { current: canvas } = canvasRef
const { current: wrap } = wrapRef

setmouseMode(value)

if (!canvas || !wrap) return
switch (value) {
case MOVE_MODE:
canvas.style.cursor = 'move'
wrap.style.cursor = 'move'
break
case LINE_MODE:
canvas.style.cursor = `url('http://cdn.algbb.cn/pencil.ico') 6 26, pointer`
wrap.style.cursor = 'default'
break
case ERASER_MODE:
message.warning('Eraser function is not perfect, there will be errors when saving pictures')
canvas.style.cursor = `url('http://cdn.algbb.cn/eraser.ico') 6 26, pointer`
wrap.style.cursor = 'default'
break
default:
canvas.style.cursor = 'default'
wrap.style.cursor = 'default'
break
}
}
Copy code``````

# 10. Switch pictures

Now it's just a demo state. Click the selection box to switch between different pictures

``````// Reset transform parameters, redraw picture
useEffect(() => {
translatePointXRef.current = 0
translatePointYRef.current = 0
fillStartPointXRef.current = 0
fillStartPointYRef.current = 0
setCanvasScale(1)
fillImage()
}, [fillImageSrc])

const handlePaperChange = (value: string) => {
const fillImageList = {
'xueshengjia': 'http://cdn.algbb.cn/test/canvasTest.jpg',
'xueshengyi': 'http://cdn.algbb.cn/test/canvasTest2.png',
'xueshengbing': 'http://cdn.algbb.cn/emoji/30.png',
}
setFillImageSrc(fillImageList[value])
}
Copy code``````

# Matters needing attention

## Note the offset of the container

We need to pay attention to that, because the downX in the formula is the coordinate relative to the container, that is to say, we need to subtract the offset of the container, which will occur when the parameter such as margin is used, or there are other elements above or on the left

When we output the offsetLeft and other attributes of our red element, we will find that it has an offset of 50. When we calculate the mouse click coordinates, we need to subtract the offset of this part

``````window.onload = function () {
const test = document.getElementById('test')
console.log(`offsetLeft: \${test.offsetLeft}, offsetHeight: \${test.offsetTop}`)
}

html,
body {
margin: 0;
}

#test {
width: 50px;
height: 50px;
margin-left: 50px;
background: red;
}

<div class="container">
<div id="test"></div>
</div>
Copy code``````

## Pay attention to the relative layout of parent components

If we have a layout like this now, it looks normal to print the offset of red elements

But if we set the relative layout of the parent element (that is, the yellow part) of the target element

``````.wrap {
position: relative;
width: 400px;
height: 300px;
background: yellow;
}

<div class="container">
<div class="sider"></div>
<div class="wrap">
<div id="test"></div>
</div>
</div>
Copy code``````

What offset will we print out at this time

The two answers are different, because our offset is calculated according to the relative position. If the parent container uses the relative layout, it will affect the offset of our child elements

# Component code (low configuration version)

``````import React, { FC, ComponentType, useEffect, useRef, RefObject, useState, MutableRefObject } from 'react'
import { RouteComponentProps } from 'react-router-dom';
import { FormComponentProps } from 'antd/lib/form';
import {
Slider, Radio, Button, Tooltip, Icon, Select, Spin, message, Popconfirm
} from 'antd';

import './index.scss'
import { getURLBase64 } from '@/admin/utils/getURLBase64'

const { Option, OptGroup } = Select;

type MarkPaperProps = RouteComponentProps & FormComponentProps

const MarkPaper: FC<MarkPaperProps> = (props: MarkPaperProps) => {
const MOVE_MODE: number = 0
const LINE_MODE: number = 1
const ERASER_MODE: number = 2
const canvasRef: RefObject<HTMLCanvasElement> = useRef(null)
const containerRef: RefObject<HTMLDivElement> = useRef(null)
const wrapRef: RefObject<HTMLDivElement> = useRef(null)
const translatePointXRef: MutableRefObject<number> = useRef(0)
const translatePointYRef: MutableRefObject<number> = useRef(0)
const fillStartPointXRef: MutableRefObject<number> = useRef(0)
const fillStartPointYRef: MutableRefObject<number> = useRef(0)
const canvasHistroyListRef: MutableRefObject<ImageData[]> = useRef([])
const [lineColor, setLineColor] = useState<string>('#fa4b2a')
const [fillImageSrc, setFillImageSrc] = useState<string>('')
const [mouseMode, setmouseMode] = useState<number>(MOVE_MODE)
const [lineWidth, setLineWidth] = useState<number>(5)
const [canvasScale, setCanvasScale] = useState<number>(1)
const [canvasCurrentHistory, setCanvasCurrentHistory] = useState<number>(0)

useEffect(() => {
setFillImageSrc('http://cdn.algbb.cn/test/canvasTest.jpg')
}, [])

// Reset transform parameters, redraw picture
useEffect(() => {
translatePointXRef.current = 0
translatePointYRef.current = 0
fillStartPointXRef.current = 0
fillStartPointYRef.current = 0
setCanvasScale(1)
fillImage()
}, [fillImageSrc])

// When canvas parameters change, listen to canvas again
useEffect(() => {
handleCanvas()
}, [mouseMode, canvasScale, canvasCurrentHistory])

// Monitor brush color change
useEffect(() => {
const { current: canvas } = canvasRef
const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d')
if (!context) return

context.strokeStyle = lineColor
context.lineWidth = lineWidth
context.lineJoin = 'round'
context.lineCap = 'round'
}, [lineWidth, lineColor])

//Monitor zoom canvas
useEffect(() => {
const { current: canvas } = canvasRef
const { current: translatePointX } = translatePointXRef
const { current: translatePointY } = translatePointYRef
canvas && (canvas.style.transform = `scale(\${canvasScale},\${canvasScale}) translate(\${translatePointX}px,\${translatePointY}px)`)
}, [canvasScale])

useEffect(() => {
const { current: canvas } = canvasRef
const { current: canvasHistroyList } = canvasHistroyListRef
const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d')
if (!canvas || !context || canvasCurrentHistory === 0) return
context?.putImageData(canvasHistroyList[canvasCurrentHistory - 1], 0, 0)
}, [canvasCurrentHistory])

const fillImage = async () => {
const { current: canvas } = canvasRef
const { current: wrap } = wrapRef
const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d')
const img: HTMLImageElement = new Image()

if (!canvas || !wrap || !context) return

img.src = await getURLBase64(fillImageSrc)
// Take the middle rendered picture
// const centerX: number = canvas && canvas.width / 2 - img.width / 2 || 0
// const centerY: number = canvas && canvas.height / 2 - img.height / 2 || 0
canvas.width = img.width
canvas.height = img.height

// The background is set as a picture, so the effect of the eraser can come out
canvas.style.background = `url(\${img.src})`
context.drawImage(img, 0, 0)
context.strokeStyle = lineColor
context.lineWidth = lineWidth
context.lineJoin = 'round'
context.lineCap = 'round'

// Set the change base point to the center of the canvas container
canvas.style.transformOrigin = `\${wrap?.offsetWidth / 2}px \${wrap?.offsetHeight / 2}px`
// Remove the effect of the last change
canvas.style.transform = ''
const imageData: ImageData = context.getImageData(0, 0, canvas.width, canvas.height)
canvasHistroyListRef.current = []
canvasHistroyListRef.current.push(imageData)
// canvasCurrentHistoryRef.current = 1
setCanvasCurrentHistory(1)
}
}

const generateLinePoint = (x: number, y: number) => {
const { current: wrap } = wrapRef
const { current: translatePointX } = translatePointXRef
const { current: translatePointY } = translatePointYRef
const wrapWidth: number = wrap?.offsetWidth || 0
const wrapHeight: number = wrap?.offsetHeight || 0
// Scaling displacement coordinate change law
// (transformOrigin - downX) / scale * (scale-1) + downX - translateX = pointX
const pointX: number = ((wrapWidth / 2) - x) / canvasScale * (canvasScale - 1) + x - translatePointX
const pointY: number = ((wrapHeight / 2) - y) / canvasScale * (canvasScale - 1) + y - translatePointY

return {
pointX,
pointY
}
}

const handleLineMode = (downX: number, downY: number) => {
const { current: canvas } = canvasRef
const { current: wrap } = wrapRef
const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d')
if (!canvas || !wrap || !context) return

const offsetLeft: number = canvas.offsetLeft
const offsetTop: number = canvas.offsetTop
// Subtract the offset distance of the canvas (calculate the coordinates based on the canvas)
downX = downX - offsetLeft
downY = downY - offsetTop

const { pointX, pointY } = generateLinePoint(downX, downY)
context.globalCompositeOperation = "source-over"
context.beginPath()
context.moveTo(pointX, pointY)

canvas.onmousemove = null
canvas.onmousemove = (event: MouseEvent) => {
const moveX: number = event.pageX - offsetLeft
const moveY: number = event.pageY - offsetTop
const { pointX, pointY } = generateLinePoint(moveX, moveY)
context.lineTo(pointX, pointY)
context.stroke()
}
canvas.onmouseup = () => {
const imageData: ImageData = context.getImageData(0, 0, canvas.width, canvas.height)

// If it is in the undo state and the brush is used at this time, the later state will be cleared and the newly drawn one will be the latest canvas state
if (canvasCurrentHistory < canvasHistroyListRef.current.length) {
canvasHistroyListRef.current = canvasHistroyListRef.current.slice(0, canvasCurrentHistory)
}
canvasHistroyListRef.current.push(imageData)
setCanvasCurrentHistory(canvasCurrentHistory + 1)
context.closePath()
canvas.onmousemove = null
canvas.onmouseup = null
}
}

const handleMoveMode = (downX: number, downY: number) => {
const { current: canvas } = canvasRef
const { current: wrap } = wrapRef
const { current: fillStartPointX } = fillStartPointXRef
const { current: fillStartPointY } = fillStartPointYRef
if (!canvas || !wrap || mouseMode !== 0) return

// Add a move event to the container to move the picture in the space
wrap.onmousemove = (event: MouseEvent) => {
const moveX: number = event.pageX
const moveY: number = event.pageY

translatePointXRef.current = fillStartPointX + (moveX - downX)
translatePointYRef.current = fillStartPointY + (moveY - downY)

canvas.style.transform = `scale(\${canvasScale},\${canvasScale}) translate(\${translatePointXRef.current}px,\${translatePointYRef.current}px)`
}

wrap.onmouseup = (event: MouseEvent) => {
const upX: number = event.pageX
const upY: number = event.pageY

wrap.onmousemove = null
wrap.onmouseup = null;

fillStartPointXRef.current = fillStartPointX + (upX - downX)
fillStartPointYRef.current = fillStartPointY + (upY - downY)
}
}

// At present, there are still some problems with the eraser. The front display is normal. Save the picture, and the erasing trace will turn white
const handleEraserMode = (downX: number, downY: number) => {
const { current: canvas } = canvasRef
const { current: wrap } = wrapRef
const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d')
if (!canvas || !wrap || !context) return

const offsetLeft: number = canvas.offsetLeft
const offsetTop: number = canvas.offsetTop
downX = downX - offsetLeft
downY = downY - offsetTop

const { pointX, pointY } = generateLinePoint(downX, downY)

context.beginPath()
context.moveTo(pointX, pointY)

canvas.onmousemove = null
canvas.onmousemove = (event: MouseEvent) => {
const moveX: number = event.pageX - offsetLeft
const moveY: number = event.pageY - offsetTop
const { pointX, pointY } = generateLinePoint(moveX, moveY)
context.globalCompositeOperation = "destination-out"
context.lineWidth = lineWidth
context.lineTo(pointX, pointY)
context.stroke()
}
canvas.onmouseup = () => {
const imageData: ImageData = context.getImageData(0, 0, canvas.width, canvas.height)
if (canvasCurrentHistory < canvasHistroyListRef.current.length) {
canvasHistroyListRef.current = canvasHistroyListRef.current.slice(0, canvasCurrentHistory)
}
canvasHistroyListRef.current.push(imageData)
setCanvasCurrentHistory(canvasCurrentHistory + 1)
context.closePath()
canvas.onmousemove = null
canvas.onmouseup = null
}
}

const handleCanvas = () => {
const { current: canvas } = canvasRef
const { current: wrap } = wrapRef
const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d')
if (!context || !wrap) return

// Clear the last set listening to prevent getting parameter errors
wrap.onmousedown = null
wrap.onmousedown = function (event: MouseEvent) {
const downX: number = event.pageX
const downY: number = event.pageY

switch (mouseMode) {
case MOVE_MODE:
handleMoveMode(downX, downY)
break
case LINE_MODE:
handleLineMode(downX, downY)
break
case ERASER_MODE:
handleEraserMode(downX, downY)
break
default:
break
}
}

wrap.onwheel = null
wrap.onwheel = (e: MouseWheelEvent) => {
const { deltaY } = e
const newScale: number = deltaY > 0
? (canvasScale * 10 - 0.1 * 10) / 10
: (canvasScale * 10 + 0.1 * 10) / 10
if (newScale < 0.1 || newScale > 2) return
setCanvasScale(newScale)
}
}

const handleScaleChange = (value: number) => {
setCanvasScale(value)
}

const handleLineWidthChange = (value: number) => {
setLineWidth(value)
}

const handleColorChange = (color: string) => {
setLineColor(color)
}

const handleMouseModeChange = (event: RadioChangeEvent) => {
const { target: { value } } = event
const { current: canvas } = canvasRef
const { current: wrap } = wrapRef

setmouseMode(value)

if (!canvas || !wrap) return
switch (value) {
case MOVE_MODE:
canvas.style.cursor = 'move'
wrap.style.cursor = 'move'
break
case LINE_MODE:
canvas.style.cursor = `url('http://cdn.algbb.cn/pencil.ico') 6 26, pointer`
wrap.style.cursor = 'default'
break
case ERASER_MODE:
message.warning('Eraser function is not perfect, there will be errors when saving pictures')
canvas.style.cursor = `url('http://cdn.algbb.cn/eraser.ico') 6 26, pointer`
wrap.style.cursor = 'default'
break
default:
canvas.style.cursor = 'default'
wrap.style.cursor = 'default'
break
}
}

const handleSaveClick = () => {
const { current: canvas } = canvasRef
// It can be stored in database or directly generated pictures
console.log(canvas?.toDataURL())
}

const handlePaperChange = (value: string) => {
const fillImageList = {
'xueshengjia': 'http://cdn.algbb.cn/test/canvasTest.jpg',
'xueshengyi': 'http://cdn.algbb.cn/test/canvasTest2.png',
'xueshengbing': 'http://cdn.algbb.cn/emoji/30.png',
}
setFillImageSrc(fillImageList[value])
}

const handleRollBack = () => {
const isFirstHistory: boolean = canvasCurrentHistory === 1
if (isFirstHistory) return
setCanvasCurrentHistory(canvasCurrentHistory - 1)
}

const handleRollForward = () => {
const { current: canvasHistroyList } = canvasHistroyListRef
const isLastHistory: boolean = canvasCurrentHistory === canvasHistroyList.length
if (isLastHistory) return
setCanvasCurrentHistory(canvasCurrentHistory + 1)
}

const handleClearCanvasClick = () => {
const { current: canvas } = canvasRef
const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d')
if (!canvas || !context || canvasCurrentHistory === 0) return

// Empty canvas history
canvasHistroyListRef.current = [canvasHistroyListRef.current[0]]
setCanvasCurrentHistory(1)

message.success('Canvas cleared successfully!')
}

return (
<div>
<CustomBreadcrumb list={['Content management', 'Marking homework']} />
<div className="mark-paper__container" ref={containerRef}>
<div className="mark-paper__wrap" ref={wrapRef}>
<div
>
<Spin
/>}
/>
</div>
<canvas
ref={canvasRef}
className="mark-paper__canvas">
<p>Unfortunately, this thing doesn't work with your computer!</p>
</canvas>
</div>
<div className="mark-paper__sider">
<div>
//Select job:
<Select
defaultValue="xueshengjia"
style={{
width: '100%', margin: '10px 0 20px 0'
}}
onChange={handlePaperChange} >
<OptGroup label="17 Software class one">
<Option value="xueshengjia">Student armor</Option>
<Option value="xueshengyi">Student B</Option>
</OptGroup>
<OptGroup label="17 Software class two">
<Option value="xueshengbing">Student C</Option>
</OptGroup>
</Select>
</div>
<div>
//Canvas operation: < br / >
<div className="mark-paper__action">
<Tooltip title="Revoke">
<i
className={`icon iconfont icon-chexiao \${canvasCurrentHistory <= 1 && 'disable'}`}
onClick={handleRollBack} />
</Tooltip>
<Tooltip title="recovery">
<i
className={`icon iconfont icon-fanhui \${canvasCurrentHistory >= canvasHistroyListRef.current.length && 'disable'}`}
onClick={handleRollForward} />
</Tooltip>
<Popconfirm
title="Are you sure you want to empty the canvas?"
onConfirm={handleClearCanvasClick}
okText="Determine"
cancelText="cancel"
>
<Tooltip title="empty">
<i className="icon iconfont icon-qingchu" />
</Tooltip>
</Popconfirm>
</div>
</div>
<div>
//Canvas zoom:
<Tooltip placement="top" title='Zoom with mouse wheel'>
<Icon type="question-circle" />
</Tooltip>
<Slider
min={0.1}
max={2.01}
step={0.1}
value={canvasScale}
tipFormatter={(value) => `\${(value).toFixed(2)}x`}
onChange={handleScaleChange} />
</div>
<div>
//Brush size:
<Slider
min={1}
max={9}
value={lineWidth}
tipFormatter={(value) => `\${value}px`}
onChange={handleLineWidthChange} />
</div>
<div>
//Mode selection:
onChange={handleMouseModeChange}
value={mouseMode}>
</div>
<div>
//Color selection:
<div className="color-picker__container">
{['#fa4b2a', '#ffff00', '#ee00ee', '#1890ff', '#333333', '#ffffff'].map(color => {
return (
<Tooltip placement="top" title={color} key={color}>
<div
role="button"
className={`color-picker__wrap \${color === lineColor && 'color-picker__wrap--active'}`}
style={{ background: color }}
onClick={() => handleColorChange(color)}
/>
</Tooltip>
)
})}
</div>
</div>
<Button onClick={handleSaveClick}>Save pictures</Button>
</div>
</div>
</div >
)
}

export default MarkPaper as ComponentType
Copy code``````

# epilogue

If this article helps you, I hope you can give me some praise and encouragement!

Or give my project a star support!

github.com/zhcxk1998/S...

Vegetable chicken analysis is not in place, please point out my shortcomings! Arigado~

Posted on Mon, 23 Mar 2020 09:36:09 -0700 by jack_indigo