原生 Canvas 实现画板功能,包含曲线、直线、矩形、原型、箭头、截图、橡皮擦等功能
import React, { Component, Fragment } from 'react'
import { Form, Button, Input, InputNumber } from 'antd'
import DrawerArrow from './DrawerArrow'
import styles from './index.less'
const LIST = [
{
key: 'pen',
name: '铅笔',
},
{
key: 'line',
name: '直线',
},
{
key: 'rect',
name: '矩形',
},
{
key: 'arc',
name: '圆形',
},
{
key: 'arrow',
name: '箭头',
},
{
key: 'robber',
name: '橡皮檫',
},
{
key: 'screenshot',
name: '截图',
},
]
class Main extends Component {
constructor(props) {
super(props)
this.state = {
bgUrlList: [], // 背景图 历史记录列表
isDraw: false,
brushType: null, // 默认为铅笔
originX: undefined,
originY: undefined,
hisImgList: [], // 用于存储历史记录的数组
step: -1, // 记录的步数
areaSize: [600, 600], // 画板的尺寸
screenshotArea: {
x0: 0,
y0: 0,
x1: 600,
y1: 600,
}, // 截图区域的大小
showSreenshoot: false, // 是否正在截图
}
this.cavasDom = null
this.ctx = null
this.originBgUrl =
'https://data.znds.com/attachment/forum/201606/09/175354uvk8ck3wmxk5zv3k.jpg' // 背景图
this.snapImg = new Image() // 用于动态实时记录上次绘制的 canvas 快照(不包含背景图片)
}
componentDidMount() {
this.initCanvas()
}
initCanvas = () => {
if (document.getElementById('cavasDom')) {
this.cavasDom = document.getElementById('cavasDom')
this.ctx = this.cavasDom.getContext('2d')
this.bindHistory()
}
}
// 选择画笔类型
selectType = brushType => {
this.setState({
brushType,
})
}
onMouseDown = event => {
event.stopPropagation()
const {
form: { getFieldValue },
} = this.props
const { brushType, areaSize } = this.state
if (!brushType) {
return
}
const color = getFieldValue('color')
const lineWidth = getFieldValue('lineWidth')
const originX =
event.pageX - document.getElementById('canvasWrapper').offsetLeft // 原点x坐标
const originY =
event.pageY - document.getElementById('canvasWrapper').offsetTop // 原点y坐标
this.setState({
originX,
originY,
isDraw: true,
})
if (brushType !== 'robber') {
this.snapImg.src = this.cavasDom.toDataURL('image/png')
}
this.ctx.beginPath()
if (brushType === 'screenshot') {
this.ctx.strokeStyle = '#000'
this.ctx.lineWidth = 1
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'
this.drawRect(0, 0, areaSize[0], areaSize[1], true)
this.setState({
screenshotArea: {
x0: 0,
y0: 0,
x1: areaSize[0],
y1: areaSize[1],
},
})
} else {
this.ctx.strokeStyle = color
this.ctx.lineWidth = lineWidth
}
this.ctx.moveTo(originX, originY)
}
onMouseMove = event => {
event.stopPropagation()
const { originX, originY, isDraw, brushType, areaSize } = this.state
if (isDraw && brushType) {
let x = event.pageX - document.getElementById('canvasWrapper').offsetLeft
let y = event.pageY - document.getElementById('canvasWrapper').offsetTop
if (brushType === 'pen') {
this.ctx.lineTo(x, y)
this.ctx.stroke()
} else if (brushType === 'robber') {
this.ctx.strokeStyle = '#fff'
this.ctx.clearRect(x - 10, y - 10, 20, 20)
} else if (brushType === 'line') {
this.drawLine(originX, originY, x, y)
} else if (brushType === 'rect') {
this.drawRect(originX, originY, x, y)
} else if (brushType === 'arc') {
this.drawCircle(originX, originY, x, y)
} else if (brushType === 'arrow') {
this.ctx.clearRect(0, 0, areaSize[0], areaSize[1])
this.ctx.drawImage(this.snapImg, 0, 0, areaSize[0], areaSize[1])
DrawerArrow(
this.ctx,
originX,
originY,
x,
y,
this.props.form.getFieldValue('color'),
)
} else if (brushType === 'screenshot') {
this.drawRect(originX, originY, x, y, true)
this.setState({
screenshotArea: {
x0: originX,
y0: originY,
x1: x,
y1: y,
},
})
}
}
}
onMouseLeave = event => {
event.stopPropagation()
const { isDraw, brushType } = this.state
if (!brushType) {
return
}
if (isDraw) {
this.setState({
isDraw: false,
showSreenshoot: brushType === 'screenshot',
})
this.ctx.closePath()
}
}
onMouseUp = event => {
const { brushType } = this.state
event.stopPropagation()
if (!brushType) {
return
}
this.setState({
isDraw: false,
showSreenshoot: brushType === 'screenshot',
})
if (brushType !== 'screenshot') {
this.bindHistory() // 当绘画结束时,调用history,保存这一步的历史记录
}
}
// 取消截图
cancelShoot = () => {
const { hisImgList, bgUrlList, areaSize } = this.state
this.ctx.clearRect(0, 0, areaSize[0], areaSize[1]) // 清空画布
let tempImg = new Image()
tempImg.src = hisImgList[hisImgList.length - 1]
this.originBgUrl = bgUrlList[bgUrlList.length - 1]
// 从数组中调取历史记录的最后一条,进行重绘
tempImg.onload = () => {
this.ctx.drawImage(tempImg, 0, 0, areaSize[0], areaSize[1])
}
this.setState({
showSreenshoot: false,
brushType: null,
})
}
// 截图
toShoot = () => {
const { screenshotArea: SA, bgUrlList, hisImgList, areaSize } = this.state
let newOriginX = SA.x0
let newOriginY = SA.y0
if (SA.x1 < SA.x0) {
newOriginX = SA.x1
}
if (SA.y1 < SA.y0) {
newOriginY = SA.y1
}
this.snapImg.src = hisImgList[hisImgList.length - 1]
let bgImg = new Image()
bgImg.src = this.originBgUrl
bgImg.onload = () => {
this.ctx.drawImage(bgImg, 0, 0, areaSize[0], areaSize[1])
this.ctx.drawImage(this.snapImg, 0, 0, areaSize[0], areaSize[1])
let img2 = new Image()
img2.src = this.cavasDom.toDataURL('image/png')
img2.onload = () => {
this.ctx.drawImage(
img2,
newOriginX,
newOriginY,
Math.abs(SA.x0 - SA.x1),
Math.abs(SA.y0 - SA.y1),
0,
0,
areaSize[0],
areaSize[1],
)
this.setState(
{
bgUrlList: [...bgUrlList, this.cavasDom.toDataURL('image/png')],
hisImgList: [...hisImgList, this.cavasDom.toDataURL('image/png')],
},
() => {
this.cancelShoot()
},
)
}
}
}
// 画直线
drawLine = (originX, originY, x, y) => {
const { areaSize } = this.state
this.ctx.clearRect(0, 0, areaSize[0], areaSize[1])
this.ctx.drawImage(this.snapImg, 0, 0, areaSize[0], areaSize[1])
this.ctx.beginPath()
this.ctx.moveTo(originX, originY)
this.ctx.lineTo(x, y)
this.ctx.stroke()
this.ctx.closePath()
}
// 画矩形
drawRect = (originX, originY, x, y, screenshot = false) => {
const { areaSize } = this.state
this.ctx.clearRect(0, 0, areaSize[0], areaSize[1])
this.ctx.drawImage(this.snapImg, 0, 0, areaSize[0], areaSize[1])
this.ctx.beginPath()
let newOriginX = originX
let newOriginY = originY
if (x < originX) {
newOriginX = x
}
if (y < originY) {
newOriginY = y
}
this.ctx.rect(
newOriginX,
newOriginY,
Math.abs(x - originX),
Math.abs(y - originY),
)
this.ctx.stroke()
if (screenshot) {
this.ctx.fill()
}
this.ctx.closePath()
}
// 画圆形
drawCircle = (originX, originY, x, y) => {
const { areaSize } = this.state
this.ctx.clearRect(0, 0, areaSize[0], areaSize[1])
this.ctx.drawImage(this.snapImg, 0, 0, areaSize[0], areaSize[1])
this.ctx.beginPath()
let newOriginX = originX
let newOriginY = originY
if (x < originX) {
newOriginX = x
}
if (y < originY) {
newOriginY = y
}
let r = Math.sqrt(
Math.abs(x - originX) * Math.abs(x - originX) +
Math.abs(y - originY) * Math.abs(y - originY),
)
this.ctx.arc(
Math.abs(x - originX) + newOriginX,
Math.abs(y - originY) + newOriginY,
r,
0,
2 * Math.PI,
)
this.ctx.stroke()
this.ctx.closePath()
}
// 上一步
toPrev = () => {
const { step, hisImgList, bgUrlList, areaSize } = this.state
let copyStep = step
if (copyStep >= 0) {
copyStep--
this.ctx.clearRect(0, 0, areaSize[0], areaSize[1]) // 清空画布
let tempImg = new Image()
tempImg.src = hisImgList[copyStep]
if (copyStep > 0) {
this.originBgUrl = bgUrlList[copyStep]
}
tempImg.onload = () => {
this.ctx.drawImage(tempImg, 0, 0, areaSize[0], areaSize[1])
} // 从数组中调取历史记录,进行重绘
} else {
alert('没有上一步了')
}
this.setState({
step: copyStep,
})
}
// 下一步
toNext = () => {
const { step, hisImgList, bgUrlList, areaSize } = this.state
let copyStep = step
if (copyStep < hisImgList.length - 1) {
copyStep++
this.ctx.clearRect(0, 0, areaSize[0], areaSize[1]) // 清空画布
let tempImg = new Image()
tempImg.src = hisImgList[copyStep]
this.originBgUrl = bgUrlList[copyStep]
tempImg.onload = () => {
this.ctx.drawImage(tempImg, 0, 0, areaSize[0], areaSize[1])
} // 从数组中调取历史记录,进行重绘
} else {
alert('没有下一步了')
}
this.setState({
step: copyStep,
})
}
// 保存历史记录
bindHistory = () => {
const { step, hisImgList, bgUrlList } = this.state
let copyStep = step
copyStep += 1
if (copyStep < hisImgList.length) {
hisImgList.length = copyStep
bgUrlList.length = copyStep
}
hisImgList.push(this.cavasDom.toDataURL('image/png'))
bgUrlList.push(this.originBgUrl)
this.setState({
step: copyStep,
hisImgList: [...hisImgList],
bgUrlList: [...bgUrlList],
})
}
// 最终保存
toSave = () => {
this.props.form.validateFields(err => {
if (!err) {
const { areaSize } = this.state
this.snapImg.src = this.cavasDom.toDataURL('image/png')
let bgImg = new Image()
bgImg.src = this.originBgUrl
bgImg.onload = () => {
this.ctx.drawImage(bgImg, 0, 0, areaSize[0], areaSize[1])
this.ctx.drawImage(this.snapImg, 0, 0, areaSize[0], areaSize[1])
console.log(bgImg)
console.log(bgImg.width)
console.log(bgImg.height)
console.log(this.cavasDom.toDataURL('image/png'), 2222)
}
}
})
}
render() {
const {
form: { getFieldDecorator },
} = this.props
const { areaSize, brushType, showSreenshoot } = this.state
return (
<Fragment>
<div id="canvasWrapper" className={styles.canvasWrapper}>
<canvas
className={styles.canvasStyle}
id="cavasDom"
width={areaSize[0]}
height={areaSize[1]}
onMouseDown={this.onMouseDown}
onMouseMove={this.onMouseMove}
onMouseLeave={this.onMouseLeave}
onMouseUp={this.onMouseUp}
/>
<img
src={this.originBgUrl}
alt=""
className={styles.largeBgImg}
style={{
width: areaSize[0],
height: areaSize[1],
}}
/>
</div>
<br />
<div id="select">
{LIST.map(item => (
<Button
key={item.key}
type={item.key === brushType ? 'primary' : 'default'}
onClick={() => this.selectType(item.key)}>
{item.name}
</Button>
))}
<Button onClick={this.toPrev}>上一步</Button>
<Button onClick={this.toNext}>下一步</Button>
<Button onClick={this.toSave}>保存</Button>
<Form layout="inline">
<Form.Item>
{getFieldDecorator('color', {
initialValue: '#f31212',
})(<Input type="color" style={{ width: 60 }} />)}
</Form.Item>
<Form.Item>
{getFieldDecorator('lineWidth', {
initialValue: 2,
})(<InputNumber style={{ width: 60 }} />)}
</Form.Item>
</Form>
{showSreenshoot && (
<div className={styles.shootWrapper}>
<div className={styles.btns}>
<Button onClick={this.cancelShoot}>取消截图</Button>
<Button onClick={this.toShoot}>确认截图</Button>
</div>
</div>
)}
</div>
</Fragment>
)
}
}
export default Form.create()(Main)
画箭头
let beginPoint = {}
let stopPoint = {}
let polygonVertex = []
let CONST = {
edgeLen: 50, // 箭头的头部长度
angle: 30, // 箭头的头部角度
}
function paraDef(edgeLen, angle) {
CONST.edgeLen = edgeLen
CONST.angle = angle
}
// 封装的作图对象
let Plot = {
angle: '',
dynArrowSize() {
let x = stopPoint.x - beginPoint.x
let y = stopPoint.y - beginPoint.y
let length = Math.sqrt(x ** 2 + y ** 2)
if (length < 50) {
CONST.edgeLen = length / 2
} else if (length < 250) {
CONST.edgeLen /= 2
} else if (length < 500) {
CONST.edgeLen = (CONST.edgeLen * length) / 500
}
},
// getRadian 返回以起点与X轴之间的夹角角度值
getRadian(beginPoint, stopPoint) {
Plot.angle =
(Math.atan2(stopPoint.y - beginPoint.y, stopPoint.x - beginPoint.x) /
Math.PI) *
180
paraDef(50, 30)
Plot.dynArrowSize()
},
// /获得箭头底边两个点
arrowCoord(beginPoint, stopPoint) {
polygonVertex[0] = beginPoint.x
polygonVertex[1] = beginPoint.y
polygonVertex[6] = stopPoint.x
polygonVertex[7] = stopPoint.y
Plot.getRadian(beginPoint, stopPoint)
polygonVertex[8] =
stopPoint.x -
CONST.edgeLen * Math.cos((Math.PI / 180) * (Plot.angle + CONST.angle))
polygonVertex[9] =
stopPoint.y -
CONST.edgeLen * Math.sin((Math.PI / 180) * (Plot.angle + CONST.angle))
polygonVertex[4] =
stopPoint.x -
CONST.edgeLen * Math.cos((Math.PI / 180) * (Plot.angle - CONST.angle))
polygonVertex[5] =
stopPoint.y -
CONST.edgeLen * Math.sin((Math.PI / 180) * (Plot.angle - CONST.angle))
},
// 获取另两个底边侧面点
sideCoord() {
let midpoint = {}
midpoint.x = (polygonVertex[4] + polygonVertex[8]) / 2
midpoint.y = (polygonVertex[5] + polygonVertex[9]) / 2
polygonVertex[2] = (polygonVertex[4] + midpoint.x) / 2
polygonVertex[3] = (polygonVertex[5] + midpoint.y) / 2
polygonVertex[10] = (polygonVertex[8] + midpoint.x) / 2
polygonVertex[11] = (polygonVertex[9] + midpoint.y) / 2
},
// 画箭头
drawArrow(ctx, color) {
ctx.fillStyle = color
ctx.beginPath()
ctx.moveTo(polygonVertex[0], polygonVertex[1])
ctx.lineTo(polygonVertex[2], polygonVertex[3])
ctx.lineTo(polygonVertex[4], polygonVertex[5])
ctx.lineTo(polygonVertex[6], polygonVertex[7])
ctx.lineTo(polygonVertex[8], polygonVertex[9])
ctx.lineTo(polygonVertex[10], polygonVertex[11])
ctx.closePath()
ctx.fill()
},
}
const todo = (ctx, originX, originY, x, y, color) => {
beginPoint.x = originX
beginPoint.y = originY
stopPoint.x = x
stopPoint.y = y
Plot.arrowCoord(beginPoint, stopPoint)
Plot.sideCoord()
Plot.drawArrow(ctx, color)
}
export default todo