zoukankan      html  css  js  c++  java
  • react项目中canvas之画形状(圆形,椭圆形,方形)

    组件DrawShape.jsx如下:

    import React, { Component } from 'react'
    // import ClassNames from 'classnames'
    import PropTypes from 'prop-types'
    import _ from 'lodash'
    import './index.less'
    
    class DrawShape extends Component {
      static propTypes = {
        style: PropTypes.object,
         PropTypes.number,
        height: PropTypes.number,
        onAddShape: PropTypes.func,
        type: PropTypes.string,
        shapeWidth: PropTypes.number,
        color: PropTypes.string,
      }
    
      static defaultProps = {
        style: {},
         1000,
        height: 1000,
        onAddShape: _.noop,
        type: 'square',
        shapeWidth: 2,
        color: '#ee4f4f',
      }
    
      state = {
      }
    
      componentDidMount() {
        const { canvasElem } = this
        this.writingCtx = canvasElem.getContext('2d')
    
        if (canvasElem) {
          canvasElem.addEventListener('mousedown', this.handleMouseDown)
          canvasElem.addEventListener('mousemove', this.handleMouseMove)
          canvasElem.addEventListener('mouseup', this.handleMouseUp)
          canvasElem.addEventListener('mouseout', this.handleMouseOut)
        }
      }
    
      componentWillUnmount() {
        const { canvasElem } = this
        if (canvasElem) {
          canvasElem.removeEventListener('mousedown', this.handleMouseDown)
          canvasElem.removeEventListener('mousemove', this.handleMouseMove)
          canvasElem.removeEventListener('mouseup', this.handleMouseUp)
          canvasElem.removeEventListener('mouseout', this.handleMouseOut)
        }
      }
    
      handleMouseDown = (e) => {
        this.isDrawingShape = true
        if (this.canvasElem !== undefined) {
          this.coordinateScaleX = this.canvasElem.clientWidth / this.props.width
          this.coordinateScaleY = this.canvasElem.clientHeight / this.props.height
        }
        this.writingCtx.lineWidth = this.props.shapeWidth / this.coordinateScaleX
        this.writingCtx.strokeStyle = this.props.color
        const {
          offsetX,
          offsetY,
        } = e
        this.mouseDownX = offsetX
        this.mouseDownY = offsetY
      }
    
      handleMouseMove = (e) => {
        if (this.isDrawingShape === true) {
          switch (this.props.type) {
            case 'square':
              this.drawRect(e)
              break
            case 'circle':
              this.drawEllipse(e)
              break
          }
        }
      }
    
      handleMouseUp = () => {
        this.isDrawingShape = false
        this.props.onAddShape({
          type: this.props.type,
          color: this.props.color,
           this.squeezePathX(this.props.shapeWidth / this.coordinateScaleX),
          positionX: this.squeezePathX(this.positionX),
          positionY: this.squeezePathY(this.positionY),
          dataX: this.squeezePathX(this.dataX),
          dataY: this.squeezePathY(this.dataY),
        })
        this.writingCtx.clearRect(0, 0, this.props.width, this.props.height)
      }
    
      handleMouseOut = (e) => {
        this.handleMouseUp(e)
      }
    
      drawRect = (e) => {
        const {
          offsetX,
          offsetY,
        } = e
        this.positionX = this.mouseDownX / this.coordinateScaleX
        this.positionY = this.mouseDownY / this.coordinateScaleY
        this.dataX = (offsetX - this.mouseDownX) / this.coordinateScaleX
        this.dataY = (offsetY - this.mouseDownY) / this.coordinateScaleY
        this.writingCtx.clearRect(0, 0, this.props.width, this.props.height)
        this.writingCtx.beginPath()
        this.writingCtx.strokeRect(this.positionX, this.positionY, this.dataX, this.dataY)
      }
    
      drawCircle = (e) => {
        const {
          offsetX,
          offsetY,
        } = e
        const rx = (offsetX - this.mouseDownX) / 2
        const ry = (offsetY - this.mouseDownY) / 2
        const radius = Math.sqrt(rx * rx + ry * ry)
        const centreX = rx + this.mouseDownX
        const centreY = ry + this.mouseDownY
        this.writingCtx.clearRect(0, 0, this.props.width, this.props.height)
        this.writingCtx.beginPath()
        this.writingCtx.arc(centreX / this.coordinateScaleX, centreY / this.coordinateScaleY, radius, 0, Math.PI * 2)
        this.writingCtx.stroke()
      }
    
      drawEllipse = (e) => {
        const {
          offsetX,
          offsetY,
        } = e
        const radiusX = Math.abs(offsetX - this.mouseDownX) / 2
        const radiusY = Math.abs(offsetY - this.mouseDownY) / 2
        const centreX = offsetX >= this.mouseDownX ? (radiusX + this.mouseDownX) : (radiusX + offsetX)
        const centreY = offsetY >= this.mouseDownY ? (radiusY + this.mouseDownY) : (radiusY + offsetY)
        this.positionX = centreX / this.coordinateScaleX
        this.positionY = centreY / this.coordinateScaleY
        this.dataX = radiusX / this.coordinateScaleX
        this.dataY = radiusY / this.coordinateScaleY
        this.writingCtx.clearRect(0, 0, this.props.width, this.props.height)
        this.writingCtx.beginPath()
        this.writingCtx.ellipse(this.positionX, this.positionY, this.dataX, this.dataY, 0, 0, Math.PI * 2)
        this.writingCtx.stroke()
      }
    
      // 将需要存储的数据根据canvas分辨率压缩至[0,1]之间的数值
      squeezePathX(value) {
        const {
          width,
        } = this.props
        return value / width
      }
    
      squeezePathY(value) {
        const {
          height,
        } = this.props
        return value / height
      }
    
      canvasElem
    
      writingCtx
    
      isDrawingShape = false
    
      coordinateScaleX
    
      coordinateScaleY
    
      mouseDownX = 0 // mousedown时的横坐标
    
      mouseDownY = 0 // mousedown时的纵坐标
    
      positionX // 存储形状数据的x
    
      positionY // 存储形状数据的y
    
      dataX // 存储形状数据的宽
    
      dataY // 存储形状数据的高
    
      render() {
        const {
          width,
          height,
          style,
        } = this.props
    
        return (
          <canvas
            width={width}
            height={height}
            style={style}
            className="draw-shape-canvas-component-wrap"
            ref={(r) => { this.canvasElem = r }}
          />
        )
      }
    }
    
    export default DrawShape

    组件DrawShape.jsx对应的less如下:

    .draw-shape-canvas-component-wrap {
      width: 100%;
      cursor: url('~ROOT/shared/assets/image/vn-shape-cursor-35-35.png') 22 22, nw-resize;
    }

    组件DrawShape.jsx对应的高阶组件DrawShape.js如下:

    import React, { Component } from 'react'
    import PropTypes from 'prop-types'
    import { observer } from 'mobx-react'
    
    import { DrawShape } from '@dby-h5-clients/pc-1vn-components'
    
    import localStore from '../../store/localStore'
    import remoteStore from '../../store/remoteStore'
    
    @observer
    class DrawShapeWrapper extends Component {
      static propTypes = {
        id: PropTypes.string.isRequired,
        style: PropTypes.object,
      }
    
      static defaultProps = {
        style: {},
      }
    
      handleAddShape = (shapeInfo) => {
        remoteStore.getMediaResourceById(this.props.id).state.addShape({
          type: shapeInfo.type,
          color: shapeInfo.color,
           shapeInfo.width,
          position: JSON.stringify([shapeInfo.positionX, shapeInfo.positionY]),
          data: JSON.stringify([shapeInfo.dataX, shapeInfo.dataY]),
        })
      }
    
      render() {
        const {
          slideRenderWidth,
          slideRenderHeight,
        } = remoteStore.getMediaResourceById(this.props.id).state
    
        const {
          currentTask,
          drawShapeConfig,
        } = localStore.pencilBoxInfo
    
        if (currentTask !== 'drawShape') {
          return null
        }
    
        return (
          <DrawShape
            style={this.props.style}
            onAddShape={this.handleAddShape}
            height={slideRenderHeight}
            width={slideRenderWidth}
            type={drawShapeConfig.type}
            shapeWidth={drawShapeConfig.width}
            color={drawShapeConfig.color}
          />
        )
      }
    }
    
    export default DrawShapeWrapper

    如上就能实现本地画形状了,但以上的逻辑是本地画完就保存到远端remote数据里,本地画的形状清除了。此适用于老师端和学生端的场景。那么在remote组件中我们要遍历remoteStore中的数据进而展示。代码如下:

    import React, { Component } from 'react'
    import PropTypes from 'prop-types'
    import assign from 'object-assign'
    import { autorun } from 'mobx'
    import _ from 'lodash'
    import { observer } from 'mobx-react'
    
    import {
      drawLine,
      clearPath,
      drawWrapText,
      drawShape,
    } from '~/shared/utils/drawWritings'
    
    @observer
    class RemoteWritingCanvas extends Component {
      static propTypes = {
        style: PropTypes.object,
         PropTypes.number,
        height: PropTypes.number,
        remoteWritings: PropTypes.oneOfType([
          PropTypes.arrayOf(PropTypes.shape({
            type: PropTypes.string,
            color: PropTypes.string,
            lineCap: PropTypes.string,
            lineJoin: PropTypes.string,
            points: PropTypes.string, // JSON 数组
             PropTypes.number,
          })),
          PropTypes.arrayOf(PropTypes.shape({
            type: PropTypes.string,
            content: PropTypes.string,
            color: PropTypes.string,
            position: PropTypes.string,
            fontSize: PropTypes.number,
          })),
        ]),
      }
    
      static defaultProps = {
        style: {},
         1000,
        height: 1000,
        remoteWritings: [],
      }
    
    
      componentDidMount() {
        this.writingCtx = this.canvasElem.getContext('2d')
    
        this.cancelAutoRuns = [
          autorun(this.drawWritingsAutoRun, { name: 'auto draw writings' }),
        ]
    
        // resize 后 恢复划线
        this.resizeObserver = new ResizeObserver(() => {
          this.drawWritingsAutoRun()
        })
    
        this.resizeObserver.observe(this.canvasElem)
      }
    
      componentWillUnmount() {
        this.resizeObserver.unobserve(this.canvasElem)
        _.forEach(this.cancelAutoRuns, f => f())
      }
    
      canvasElem
    
      writingCtx
    
      drawWritingsAutoRun = () => {
        // todo 性能优化,过滤已画划线
        this.writingCtx.clearRect(0, 0, this.props.width, this.props.height)
        _.map(this.props.remoteWritings, (writing) => {
          if (['markPen', 'eraser', 'pencil'].indexOf(writing.type) > -1) {
            const {
              type,
              color,
              lineCap,
              lineJoin,
              points,
              width,
            } = writing
    
            const canvasWidth = this.props.width
            switch (type) {
              case 'eraser':
                clearPath(this.writingCtx, this.recoverPath(JSON.parse(points)), width * canvasWidth)
                break
              case 'pencil': // 同 markPen
              case 'markPen':
                drawLine(this.writingCtx, this.recoverPath(JSON.parse(points)), color, width * canvasWidth, lineJoin, lineCap)
                break
            }
          }
          if (writing.type === 'text') {
            const {
              color,
              content,
              fontSize,
              position,
            } = writing
    
            const [x, y] = this.recoverPath(JSON.parse(position))
    
            drawWrapText({
              canvasContext: this.writingCtx,
              text: content,
              color,
              fontSize: fontSize * this.props.width,
              x,
              y,
            })
          }
          if (['square', 'circle'].indexOf(writing.type) > -1) {
            const {
              type,
              color,
              position,
              data,
            } = writing
            const width = this.recoverPathX(writing.width)
            let [positionX, positionY] = JSON.parse(position)
            let [dataX, dataY] = JSON.parse(data)
            positionX = this.recoverPathX(positionX)
            positionY = this.recoverPathY(positionY)
            dataX = this.recoverPathX(dataX)
            dataY = this.recoverPathY(dataY)
            drawShape({
              writingCtx: this.writingCtx,
              type,
              color,
              width,
              positionX,
              positionY,
              dataX,
              dataY,
            })
          }
        })
      }
    
      // 将[0,1]之间的坐标点根据canvas分辨率进行缩放
      recoverPath(path) {
        const {
          width,
          height,
        } = this.props
        return _.map(path, (val, i) => (i % 2 === 0 ? val * width : val * height))
      }
    
      recoverPathX(value) {
        const {
          width,
        } = this.props
        return value * width
      }
    
      recoverPathY(value) {
        const {
          height,
        } = this.props
        return value * height
      }
    
      render() {
        const {
          width,
          height,
          style,
        } = this.props
        const wrapStyles = assign({}, style, {
           '100%',
        })
    
        return (
          <canvas
            className="remote-writing-canvas-component-wrap"
            width={width}
            height={height}
            style={wrapStyles}
            ref={(r) => { this.canvasElem = r }}
          />
        )
      }
    }
    
    export default RemoteWritingCanvas

    其中用到的画图的工具函数来自于drawWritings:内部代码如下:

    /**
     * 画一整条线
     * @param ctx
     * @param points
     * @param color
     * @param width
     * @param lineJoin
     * @param lineCap
     */
    export function drawLine(ctx, points, color, width, lineJoin = 'miter', lineCap = 'round') {
      if (points.length >= 2) {
        ctx.lineWidth = width
        ctx.strokeStyle = color
        ctx.lineCap = lineCap
        ctx.lineJoin = lineJoin
        ctx.beginPath()
        if (points.length === 2) {
          ctx.arc(points[0], points[1], width, 0, Math.PI * 2)
        } else {
          if (points.length > 4) {
            ctx.moveTo(points[0], points[1])
            for (let i = 2; i < points.length - 4; i += 2) {
              ctx.quadraticCurveTo(points[i], points[i + 1], (points[i] + points[i + 2]) / 2, (points[i + 1] + points[i + 3]) / 2)
            }
            ctx.lineTo(points[points.length - 2], points[points.length - 1])
          } else {
            ctx.moveTo(points[0], points[1])
            ctx.lineTo(points[2], points[3])
          }
        }
        ctx.stroke()
        ctx.closePath()
      }
    }
    
    /**
     * 画一个点,根据之前已经存在的线做优化
     * @param ctx
     * @param point
     * @param prevPoints
     * @param color
     * @param width
     * @param lineJoin
     * @param lineCap
     */
    export function drawPoint(ctx, point, prevPoints, color, width, lineJoin = 'miter', lineCap = 'round') {
      ctx.lineWidth = width
      ctx.strokeStyle = color
      ctx.lineCap = lineCap
      ctx.lineJoin = lineJoin
      const prevPointsLength = prevPoints.length
      if (prevPointsLength === 0) { // 画一个点
        ctx.arc(point[0], point[1], width, 0, Math.PI * 2)
      } else if (prevPointsLength === 2) { // 开始划线
        ctx.beginPath()
        ctx.moveTo(...point)
      } else { // 继续划线
        ctx.lineTo(...point)
      }
      ctx.stroke()
    }
    
    /**
     * 画一组线,支持半透明划线,每次更新会清除所有划线后重画一下
     * @param ctx
     * @param lines 二维数组,元素是划线点组成的数组, eg [[1,2,3,4],[1,2,3,4,5,6],...]
     * @param color
     * @param width
     * @param lineJoin
     * @param lineCap
     * @param canvasWith
     * @param canvasHeight
     */
    export function drawOpacityLines(ctx, lines, canvasWith = 10000, canvasHeight = 10000) {
      ctx.clearRect(0, 0, canvasWith, canvasHeight)
    
      for (let i = 0; i < lines.length; i += 1) {
        const {
          points,
          color,
          width,
          lineJoin,
          lineCap,
        } = lines[i]
        const pointsLength = points.length
    
        if (pointsLength > 2) {
          ctx.strokeStyle = color
          ctx.lineCap = lineCap
          ctx.lineJoin = lineJoin
          ctx.lineWidth = width
          ctx.beginPath()
    
          if (pointsLength > 4) {
            ctx.moveTo(points[0], points[1])
            for (let j = 2; j < pointsLength - 4; j += 2) {
              ctx.quadraticCurveTo(points[j], points[j + 1], (points[j] + points[j + 2]) / 2, (points[j + 1] + points[j + 3]) / 2)
            }
            ctx.lineTo(points[pointsLength - 2], points[pointsLength - 1])
          } else {
            ctx.moveTo(points[0], points[1])
            ctx.lineTo(points[2], points[3])
          }
    
          ctx.stroke()
          ctx.closePath()
        }
      }
    }
    
    /**
     * 擦除路径
     * @param ctx
     * @param {Array} points
     * @param width
     */
    export function clearPath(ctx, points, width) {
      const pointsLength = points.length
      if (pointsLength > 0) {
        ctx.beginPath()
        ctx.globalCompositeOperation = 'destination-out'
    
        if (pointsLength === 2) { // 一个点
          ctx.arc(points[0], points[1], width / 2, 0, 2 * Math.PI)
          ctx.fill()
        } else if (pointsLength >= 4) {
          ctx.lineWidth = width
          ctx.lineJoin = 'round'
          ctx.lineCap = 'round'
          ctx.moveTo(points[0], points[1])
          for (let j = 2; j <= pointsLength - 2; j += 2) {
            ctx.lineTo(points[j], points[j + 1])
          }
          ctx.stroke()
        }
        ctx.closePath()
        ctx.globalCompositeOperation = 'source-over'
      }
    }
    
    /**
     * 写字
     * @param {object} textInfo
     * @param textInfo.canvasContext
     * @param textInfo.text
     * @param textInfo.color
     * @param textInfo.fontSize
     * @param textInfo.x
     * @param textInfo.y
     */
    export function drawText(
      {
        canvasContext,
        text,
        color,
        fontSize,
        x,
        y,
      },
    ) {
      canvasContext.font = `normal normal ${fontSize}px Airal`
      canvasContext.fillStyle = color
      canvasContext.textBaseline = 'middle'
      canvasContext.fillText(text, x, y)
    }
    
    /**
     * 写字,超出canvas右侧边缘自动换行
     * @param {object} textInfo
     * @param textInfo.canvasContext
     * @param textInfo.text
     * @param textInfo.color
     * @param textInfo.fontSize
     * @param textInfo.x
     * @param textInfo.y
     */
    export function drawWrapText(
      {
        canvasContext,
        text,
        color,
        fontSize,
        x,
        y,
      },
    ) {
      if (typeof text !== 'string' || typeof x !== 'number' || typeof y !== 'number') {
        return
      }
      const canvasWidth = canvasContext.canvas.width
      canvasContext.font = `normal normal ${fontSize}px sans-serif`
      canvasContext.fillStyle = color
      canvasContext.textBaseline = 'middle'
    
      // 字符分隔为数组
      const arrText = text.split('')
      let line = ''
    
      let calcY = y
      for (let n = 0; n < arrText.length; n += 1) {
        const testLine = line + arrText[n]
        const metrics = canvasContext.measureText(testLine)
        const testWidth = metrics.width
        if (testWidth > canvasWidth - x && n > 0) {
          canvasContext.fillText(line, x, calcY)
          line = arrText[n]
          calcY += fontSize
        } else {
          line = testLine
        }
      }
      canvasContext.fillText(line, x, calcY)
    }
    
    /**
     * 画形状
     * @param {object} shapeInfo
     * @param shapeInfo.writingCtx
     * @param shapeInfo.type
     * @param shapeInfo.color
     * @param shapeInfo.width
     * @param shapeInfo.positionX
     * @param shapeInfo.positionY
     * @param shapeInfo.dataX
     * @param shapeInfo.dataY
     */
    export function drawShape(
      {
        writingCtx,
        type,
        color,
        width,
        positionX,
        positionY,
        dataX,
        dataY,
      },
    ) {
      writingCtx.lineWidth = width
      writingCtx.strokeStyle = color
      if (type === 'square') {
        writingCtx.beginPath()
        writingCtx.strokeRect(positionX, positionY, dataX, dataY)
      }
      if (type === 'circle') {
        writingCtx.beginPath()
        writingCtx.ellipse(positionX, positionY, dataX, dataY, 0, 0, Math.PI * 2)
        writingCtx.stroke()
      }
    }

    canvas 有两种宽高设置 :

    1. 属性height、width,设置的是canvas的分辨率,即画布的坐标范围。如果 `canvasElem.height = 200; canvasElem.width = 400;` 其右下角对应坐标是(200, 400) 。

    2. 样式style里面的height 和width,设置实际显示大小。如果同样是上面提到的canvasElem,style为`{ 100px; height: 100px}`, 监听canvasElem 的 mouseDown,点击右下角在event中获取到的鼠标位置坐标`(event.offsetX, event.offsetY)` 应该是`(100, 100)`。

    将鼠标点击位置画到画布上需要进行一个坐标转换trans 使得`trans([100, 100]) == [200, 400]` `trans`对坐标做以下转换然后返回 - x * canvas横向最大坐标 / 显示宽度 - y * canvas纵向最大坐标 / 显示高度 参考代码 trans = ([x, y]) => { const scaleX = canvasElem.width / canvasElem.clientWidth const scaleY = canvasElem.height / canvasElem.clientHeight return [x * scaleX, y * scaleY] } 通常我们课件显示区域是固定大小的(4:3 或16:9),显示的课件大小和比例是不固定的,显示划线的canvas宽度占满课件显示区域,其分辨率是根据加载的课件图片的分辨率计算得来的,所以我们通常需要在划线时对坐标进行的转换。

    小结:如果觉得以上太麻烦,只是想在本地实现画简单的直线、形状等等,可以参考这篇文章:https://blog.csdn.net/qq_31164127/article/details/72929871

  • 相关阅读:
    用 .Net WebBrowser 控件获取POST数据
    yield再理解--绝对够透彻
    Keras 和 PyTorch 的对比选择
    Keras -Python编写的开源人工神经网络库
    Python 加密之 生成pyd文件
    FPN全解-特征金字塔网络
    RetinaNet(Focal Loss)
    Focal Loss for Dense Object Detection(Retina Net)
    ImageNet Classification-darknet
    Darknet
  • 原文地址:https://www.cnblogs.com/chenbeibei520/p/11096319.html
Copyright © 2011-2022 走看看