原生 Canvas 实现画板功能,包含曲线、直线、矩形、原型、箭头、截图、橡皮擦等功能

image

image


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

posted @ 2021-01-14 15:27  Mr.曹  阅读(870)  评论(1编辑  收藏  举报