DrawShape.jsx components as follows:
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, width: PropTypes.number, height: PropTypes.number, onAddShape: PropTypes.func, type: PropTypes.string, shapeWidth: PropTypes.number, color: PropTypes.string, } static defaultProps = { style: {}, width: 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, width: 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() } // data to be stored is compressed according to the resolution to a value between the 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 // abscissa at mousedown mouseDownY = 0 // when mousedown ordinate positionX // stores shape data X positionY // stores shape data Y dataX // wide shape data stored DATAY // stores shape data height of 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
Corresponding components DrawShape.jsx less follows:
.draw-shape-canvas-component-wrap { width: 100%; cursor: url('~ROOT/shared/assets/image/vn-shape-cursor-35-35.png') 22 22, nw-resize; }
The corresponding higher order components DrawShape.jsx DrawShape.js following components:
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, width: 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
Videos can be realized as local shape, but the above logic is saved to the local unfinished distal end remote data, the local shape of the painting removed. This applies to teachers and students end side of the scene. Then the remote components, we have to traverse the data remoteStore in turn show. code show as below:
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, width: 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 数组 width: PropTypes.number, })), PropTypes.arrayOf (PropTypes.shape ({ type: PropTypes.string, content: PropTypes.string, color: PropTypes.string, position: PropTypes.string, fontSize: PropTypes.number, })), ]), } static defaultProps = { style: {}, width: 1000, height: 1000, remoteWritings: [], } componentDidMount() { this.writingCtx = this.canvasElem.getContext('2d') this.cancelAutoRuns = [ autorun(this.drawWritingsAutoRun, { name: 'auto draw writings' }), ] // After recovery scribing resize the this .resizeObserver = new new ResizeObserver (() => { the this .drawWritingsAutoRun () }) this.resizeObserver.observe(this.canvasElem) } componentWillUnmount() { this.resizeObserver.unobserve(this.canvasElem) _.forEach(this.cancelAutoRuns, f => f()) } canvasElem writingCtx drawWritingsAutoRun = () => { // TODO performance optimization, has been filtered Videos crossed the this .writingCtx.clearRect (0, 0, the this .props.width, the 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, }) } }) } // the coordinate point [0,1] scaled according to the resolution 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, { width: '100%', }) return ( <canvas className="remote-writing-canvas-component-wrap" width={width} height={height} style={wrapStyles} ref={(r) => { this.canvasElem = r }} /> ) } } export default RemoteWritingCanvas
Wherein the drawing of utility functions used from drawWritings: Internal code is as follows:
/** * Draw a whole line * @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() } } /** * Draw a point, do optimization based on previously existing line * @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) } The else IF (prevPointsLength === 2) { // begin scribing ctx.beginPath () ctx.moveTo(...point) } The else { // continue scribing ctx.lineTo (... point) } ctx.stroke() } /** * Draw a set of lines, crossed translucent support, each update will remove all the repaint it crossed * @param ctx * @Param lines two-dimensional array, the array elements are crossed dots, 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() } } } /** * Erase path * @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' } } /** * Write * @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) } /** * Write beyond the right edge of the canvas wrap * @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' // character to separate array 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) } /** * Painting shape * @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() } }
There are two canvas width and height is provided:
1. properties height, width, the canvas is set resolution, i.e. the canvas coordinate range. If `canvasElem.height = 200; canvasElem.width = 400;` which corresponds to the bottom right coordinates (200, 400).
2. style style inside height and width, set the actual display size. If the same is canvasElem mentioned above, style is `{width: 100px; height: 100px}`, the monitor canvasElem mouseDown, bottom right click event acquired in the mouse position coordinates `(event.offsetX, event.offsetY) `` should be (100, 100) `.
The mouse click position drawn on a canvas coordinate conversion is required that trans `trans ([100, 100]) == [200, 400]` `trans` make the following coordinate conversion and then returns - x * canvas maximum transverse coordinate / display width - y * canvas coordinate maximum longitudinal / display height reference code trans = ([x, y]) => {const scaleX = canvasElem.width / canvasElem.clientWidth const scaleY = canvasElem.height / canvasElem.clientHeight return [x * scaleX, y * scaleY]} in general, we courseware display area of a fixed size (the 4: 3 or 16: 9), the size and proportions shown courseware is not fixed, the display width of the scribe line canvas filled courseware display area, which the resolution is based on the resolution of the picture courseware load calculations come, so we usually need to coordinate conversion performed when crossed.
Summary: If you feel the above too much trouble, just want to achieve a simple drawing a straight line, shape, etc. locally, you can refer to this article: https://blog.csdn.net/qq_31164127/article/details/72929871