组件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,
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()
} // 将需要存储的数据根据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,
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

如上就能实现本地画形状了,但以上的逻辑是本地画完就保存到远端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,
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' }),
] // 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, {
width: '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为`{width: 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

react项目中canvas之画形状(圆形,椭圆形,方形)的更多相关文章

  1. 如何在非 React 项目中使用 Redux

    本文作者:胡子大哈 原文链接:https://scriptoj.com/topic/178/如何在非-react-项目中使用-redux 转载请注明出处,保留原文链接和作者信息. 目录 1.前言 2. ...

  2. 如何优雅地在React项目中使用Redux

    前言 或许你当前的项目还没有到应用Redux的程度,但提前了解一下也没有坏处,本文不会安利大家使用Redux 概念 首先我们会用到哪些框架和工具呢? React UI框架 Redux 状态管理工具,与 ...

  3. react项目中实现元素的拖动和缩放实例

    在react项目中实现此功能可借助 react-rnd 库,文档地址:https://github.com/bokuweb/react-rnd#Screenshot .下面是实例运用: import ...

  4. React项目中实现右键自定义菜单

    最近在react项目中需要实现一个,右键自定义菜单功能.找了找发现纯react项目里没有什么工具可以实现这样的功能,所以在网上搜了搜相关资料.下面我会附上完整的组件代码. (注:以下代码非本人原创,具 ...

  5. React项目中使用Mobx状态管理(二)

    并上一节使用的是普通的数据状态管理,不过官方推荐使用装饰器模式,而在默认的react项目中是不支持装饰器的,需要手动启用. 官方参考 一.添加配置 官方提供了四种方法, 方法一.使用TypeScrip ...

  6. 在react项目中使用ECharts

    这里我们要在自己搭建的react项目中使用ECharts,我们可以在ECharts官网上看到有一种方式是在 webpack 中使用 ECharts,我们需要的就是这种方法. 我们在使用ECharts之 ...

  7. 优雅的在React项目中使用Redux

    概念 首先我们会用到哪些框架和工具呢? React UI框架 Redux 状态管理工具,与React没有任何关系,其他UI框架也可以使用Redux react-redux React插件,作用:方便在 ...

  8. 深入浅出TypeScript(5)- 在React项目中使用TypeScript

    前言 在第二小节中,我们讨论了利用TypeScript创建Web项目的实现,在本下节,我们讨论一下如何结合React创建一个具备TypeScript类型的应用项目. 准备 Webpack配置在第二小节 ...

  9. redux在react项目中的应用

    今天想跟大家分享一下redux在react项目中的简单使用 1 1.redux使用相关的安装 yarn add redux yarn add react-redux(连接react和redux) 2. ...

随机推荐

  1. Ubuntu 16.04安装完重启后黑屏,光标一直闪

    原文:https://blog.csdn.net/weixin_38533896/article/details/81023690 版权声明:本文为博主原创文章,转载请附上博文链接! 安装教程网址:h ...

  2. ArcGIS超级工具1.6升级说明

    ArcGIS超级工具1.6升级说明:多了:7个工具,总82工具,只支持ArcGIS10.2以上的版本 1.6 Excel转点支持Excel的xls和xlsx,自动识别工作表和字段名,生成点 5.6 M ...

  3. keras多层感知机MLP

    肯定有人要说什么多层感知机,不就是几个隐藏层连接在一起的吗.话是这么说,但是我觉得我们首先要自己承认自己高级,不然怎么去说服(hu nong)别人呢 from keras.models import ...

  4. 小程序下载canvas生成图片

    save_share_img:function(img){ var that = this; let { result } = that.data; getData.getData( "sa ...

  5. JQuery.BlockUI使用方法举例

    JQuery.BlockUI是众多JQuery插件弹出层中的一个,它小巧(原版16k,压缩后10左右),容易使用, 功能齐全,支持Iframe,支持Modal,可定制性高也意味他默认谦虚的外表. jQ ...

  6. C/C++语言,自学资源,滚动更新中……

    以下教学视频中,缺少对“字符串”技术的讨论,大家注意看书. 一维数组,及其举例:(第四版)P77~P85,书上这部分内容写的很好,很详细,尤其是这里列出来的每一个例子都要仔细去看一看,对你会很有帮助. ...

  7. react做的简单的购物车

    ###第一步 :首先电脑上已经安装react的脚手架 cnpm  install    create-react-app   -g ###第二步 :创建项目 creact-react-app   项目 ...

  8. cisco 各类子网的划分

    C类地址子网划分 /25 子网掩码为128: 1位的取值为1,其他7位的取值为0(10000000): 块大小128: (256-128): 2个子网,每个子网最多包含126台主机: /26 子网掩码 ...

  9. 已经安装了VRay但3dmax的材质编辑器里没有VRay材质的解决过程

    已经安装了VRay但3dmax的材质编辑器里没有VRay材质怎么办? 众所周知,vray是一款很好用的渲染器,但是安装过程和使用当中总会出现各种问题.昨天我就遇到了,捣鼓半天终于解决,分享给大家自己的 ...

  10. 日后成为人工智能领域的小大佬,立个flag~

    时间过的很快,仿佛就在昨天我还是一个机械在校硕士.但现在我已经开始在互联网企业中上班了,第一周的小任务进行简单的数据处理,自己搞的很不好,路还很长,但来日方长! 这周开始学习爬虫,最近会很忙,希望能改 ...