组件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. android studio3.4打jar包

    第一步在build.gradle文件里的android{}里面加入下面内容 //生成jar包 task makeJar(type:Copy) { delete 'build/outputs/netwo ...

  2. 标准6轴机器人正反解(1)-坐标系和MDH参数表

    刚来新公司不久,部门给安排了新人作业,我被分到的任务是求标准6轴机器人的正反解,以及利用就近原则选择最优解.从今天开始,逐步将这部分内容总结出来: 本文以及后续文章均使用改进DH法: 连杆坐标系: 坐 ...

  3. Xgboost建模

    xgboost参数 选择较高的学习速率(learning rate).一般情况下,学习速率的值为0.1.但是,对于不同的问题,理想的学习速率有时候会在0.05到0.3之间波动.选择对应于此学习速率的理 ...

  4. memcache安装与简单介绍

    本文参考自菜鸟教程中的内容. 安装 安装memcache的时候,请切换为root用户 root@centos # wget http://www.memcached.org/files/memcach ...

  5. Qt编写自定义控件40-导航进度条

    一.前言 导航进度条控件,其实就是支付宝.京东.淘宝订单页面的进度控件,提示当前第几步,总共有几步,然后当前进度特殊颜色显示,每个进度带有时间文字等信息,本控件特意将三种样式风格都集成进去了,京东订单 ...

  6. PAT 甲级 1047 Student List for Course (25 分)(cout超时,string scanf printf注意点,字符串哈希反哈希)

    1047 Student List for Course (25 分)   Zhejiang University has 40,000 students and provides 2,500 cou ...

  7. IDEA的查询引用、调用关系图的功能(转)

    转自: http://www.cnblogs.com/ghj1976/p/5382455.html Eclipse的"Call Hierarchy"可以查看一个Java方法或类成员 ...

  8. ubuntu 18.04下greenplum安装笔记(二)安装Greenplum的失败的尝试

    之前对Linux环境进行了搭建,现在开始进行Greenplum的正式安装. 下载 进Greenplum的官网:https://greenplum.org/download/ 可以发现,对于ubuntu ...

  9. ADFS RelayState

    https://blogs.technet.microsoft.com/askds/2012/09/27/ad-fs-2-0-relaystate/ 什么是RelayState,我为什么要关心?有两种 ...

  10. Docker镜像的构建(五)

    目录 构建镜像 1.使用 commit 命令构建 1.1 运行一个要进行修改的容器 1.2 安装 Apache 软件包 1.3 提交定制容器 2.使用 Dockerfile 构建 2.1 我们的第一个 ...