广告位联系
返回顶部
分享到

html5 Canvas实现图片标记、缩放、移动和保存历史状态功能(附转换公式)

html5 来源:互联网搜集 作者:秩名 发布时间:2020-03-18 20:17:04 人浏览
摘要

前言 因为也是大三了,最近俺也在找实习,之前有一个自己的小项目: https://github.com/zhcxk1998/School-Partners 面试官说可以往深层次思考一下,或许加一些新的功能来增加项目的难度,他提了几个建议,其中一个就是 试卷在线批阅,老师可以在上面对作业进

前言

因为也是大三了,最近俺也在找实习,之前有一个自己的小项目:

面试官说可以往深层次思考一下,或许加一些新的功能来增加项目的难度,他提了几个建议,其中一个就是 试卷在线批阅,老师可以在上面对作业进行批注,圈圈点点等 俺当天晚上就开始研究这个东东哈哈哈,终于被我研究出来啦!

采用的是 canvas 绘制画笔,由css3的 transform 属性来进行平移与缩放,之后再详细介绍介绍

(希望大家可以留下宝贵的赞与star嘻嘻)

效果预览

-[-/a>

 

动图是放cdn的,如果访问不了,可以登录在线尝试尝试:

公式推导 如果不想看公式如何推导,可以直接跳过看后面的具体实现~ 1. 坐标转换公式 转换公式介绍

其实一开始也是想在网上找一下有没有相关的资料,但是可惜找不到,所以就自己慢慢的推出来了。我就举一下横坐标的例子吧!

通用公式

这个公式是表示,通过公式来将鼠标按下的坐标转换为画布中的相对坐标,这一点尤为重要

(transformOrigin - downX) / scale * (scale-1) + downX - translateX = pointX

参数解释

transformOrigin: transform变化的基点(通过这个属性来控制元素以哪里进行变化)
downX: 鼠标按下的坐标(注意,用的时候需要减去容器左偏移距离,因为我们要的是相对于容器的坐标)
scale: 缩放倍数,默认为1
translateX: 平移的距离

推导过程

这个公式的话,其实就比较通用,可以用在别的利用到 transform 属性的场景,至于怎么推导的话,我是用的笨办法

具体的测试代码,放在文末,需要自取~

1. 先做出两个相同的元素,然后标记上坐标,并且设置容器属性 overflow:hidden 来隐藏溢出内容

 

 

 

ok,现在就有两个一样的矩阵啦,我们为他标记上一些红点,然后我们对左边的进行css3的样式变化 transform

矩形的宽高是 360px * 360px 的,我们定义一下他的变化属性,变化基点选择正中心,放大3倍

得到如下结果

 

 

 

ok,我们现在对比一下上面的结果,就会发现,放大3倍的时候,恰好是中间黑色方块占据了全部宽度。接下来我们就可以对这些点与原先没有进行变化(右边)的矩形进行对比就可以得到他们坐标的关系啦

2. 开始对两个坐标进行对比,然后推出公式

现在举一个简单的例子吧,例如我们算一下左上角的坐标(现在已经标记为黄色了)

 

 

 

其实我们其实就可以直接心算出来坐标的关系啦

这里左边计算坐标的值是我们鼠标按下的坐标

这里左边计算坐标的值是我们鼠标按下的坐标

这里左边计算坐标的值是我们鼠标按下的坐标

  • 因为宽高是 360px ,所以分成3等份,每份宽度是 120px
  • 因为变化之后容器的宽高是不变的,变化的只有矩形本身
  • 我们可以得出左边的黄色标记坐标是 x:120 y:0 ,右边的黄色标记为 x:160 y:120 (这个其实肉眼看应该就能看出来了,实在不行可以用纸笔算一算)

这个坐标可能有点特殊,我们再换几个来计算计算(根据特殊推一般)

 

 

 

蓝色标记:左边: x:120 y:120 ,右边: x: 160 y:160 绿色标记:左边: x: 240 y:240 ,右边: x: 200: y:200

好了,我们差不多已经可以拿到坐标之间的关系了,我们可以列一个表

 

 

 

还觉得不放心?我们可以换一下,缩放倍数与容器宽高等进行计算

 

 

 

不知道大家有没有感觉呢,然后我们就可以慢慢根据坐标推出通用的公式啦

(transformOrigin - downX) / scale * (scale-1) + down - translateX = point

当然,我们或许还有这个 translateX 没有尝试,这个就比较简单一点了,脑内模拟一下,就知道我们可以减去位移的距离就ok啦。我们测试一下

我们先修改一下样式,新增一下位移的距离

还是我们上面的状态,ok,我们现在蓝色跟绿色的标记还是一一对应的,那我们看看现在的坐标情况

  • 蓝色:左边: x:0 y:0 ,右边: x:160 y:160
  • 绿色:左边: x:120 y:120 ,右边: x:200 y:200

我们分别运用公式算一下出来的坐标是怎么样的 (以下为经过坐标换算)

蓝色:左边: x:120 y:120 ,右边: x:160 y:160 绿色:左边: x:160 y:160 ,右边: x:200 y:200

不难发现,我们其实就相差了与位移距离 translateX/translateY 的差值,所以,我们只需要减去位移的距离就可以完美的进行坐标转换啦

测试公式

根据上面的公式,我们可以简单测试一下!这个公式到底能不能生效!!!

我们直接沿用上面的demo,测试一下如果元素进行了变化,我们鼠标点下的地方生成一个标记,位置是否显示正确。看起来很ok啊(手动滑稽)

可能有人会问,为什么要减去这个 offsetLeftoffsetTop 呢,因为我们上面反复强调,我们计算的是鼠标点击的坐标,而这个坐标还是相对于我们展示容器的坐标,所以我们要减去容器本身的偏移量才行。

组件设计

既然demo啥的都已经测试了ok了,我们接下来就逐一分析一下这个组件应该咋设计好呢(目前仍为低配版,之后再进行优化完善)

1. 基本的画布构成

 

 

 

我们先简单分析一下这个构成吧,其实主要就是一个画布的容器,右边一个工具栏,仅此而已

 

 

 

大体就这样子啦!

我们唯一需要的一点就是,容器需要设置属性 overflow: hidden 用来隐藏内部canvas画布溢出的内容,也就是说,我们要控制我们可视的区域。同时我们需要动态获取容器宽高来为canvas设置尺寸

2. 初始化canvas画布与填充图片

我们可以弄个方法来初始化并且填充画布,以下截取主要部分,其实就是为canvas画布设置尺寸与填充我们的图片

3. 监听canvas画布的各种鼠标事件

这个控制移动的话,我们首先可以弄一个方法来监听画布鼠标的各种事件,可以区分不同的模式来进行不同的事件处理

4. 实现画布移动

这个就比较好办啦,我们只需要利用鼠标按下的坐标,和我们拖动的距离就可以实现画布的移动啦,因为涉及到每次移动都需要计算最新的位移距离,我们可以定义几个变量来进行计算。

这里监听的是容器的鼠标事件,而不是canvas画布的事件,因为这样子我们可以再移动超过边界的时候也可以进行移动操作

 

 

 

简单的总结一下:

  • 传入鼠标按下的坐标
  • 计算当前位移距离,并更新css变化效果
  • 鼠标抬起时更新最新的位移状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 定义一些变量,来保存当前/最新的移动状态
// 当前位移的距离
const translatePointXRef: MutableRefObject<number> = useRef(0)
const translatePointYRef: MutableRefObject<number> = useRef(0)
// 上一次位移结束的位移距离
const fillStartPointXRef: MutableRefObject<number> = useRef(0)
const fillStartPointYRef: MutableRefObject<number> = useRef(0)
 
// 移动时候的监听函数
const handleMoveMode = (downX: number, downY: number) => {
  const { current: canvas } = canvasRef
  const { current: wrap } = wrapRef
  const { current: fillStartPointX } = fillStartPointXRef
  const { current: fillStartPointY } = fillStartPointYRef
  if (!canvas || !wrap || mouseMode !== 0) return
 
  // 为容器添加移动事件,可以在空白处移动图片
  wrap.onmousemove = (event: MouseEvent) => {
    const moveX: number = event.pageX
    const moveY: number = event.pageY
 
    // 更新现在的位移距离,值为:上一次位移结束的坐标+移动的距离
    translatePointXRef.current = fillStartPointX + (moveX - downX)
    translatePointYRef.current = fillStartPointY + (moveY - downY)
 
    // 更新画布的css变化
    canvas.style.transform = `scale(${canvasScale},${canvasScale}) translate(${translatePointXRef.current}px,${translatePointYRef.current}px)`
  }
  
  wrap.onmouseup = (event: MouseEvent) => {
    const upX: number = event.pageX
    const upY: number = event.pageY
    
    // 取消事件监听
    wrap.onmousemove = null
    wrap.onmouseup = null;
 
    // 鼠标抬起时候,更新“上一次唯一结束的坐标”
    fillStartPointXRef.current = fillStartPointX + (upX - downX)
    fillStartPointYRef.current = fillStartPointY + (upY - downY)
  }
}

5. 实现画布缩放

画布缩放我主要通过右侧的滑动条以及鼠标滚轮来实现,首先我们再监听画布鼠标事件的函数中加一下监听滚轮的事件

总结一下:

  • 监听鼠标滚轮的变化
  • 更新缩放倍数,并改变样式

接着我们使用hooks的副作用函数,依赖于画布缩放倍数来进行样式的更新

6. 实现画笔绘制

这个就需要用到我们之前推导出来的公式啦!因为呢,仔细想一下,如果我们缩放位移之后,我们鼠标按下的位置,他的坐标可能就相对于画布来说会有变化, 所以我们需要转换一下才能进行鼠标按下的位置与画布的位置一一对应的效果

稍微总结一下:

  • 传入鼠标按下的坐标
  • 通过公式转换,开始在对应坐标下绘制
  • 鼠标抬起时,取消事件监听
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 利用公式转换一下坐标
const generateLinePoint = (x: number, y: number) => {
  const { current: wrap } = wrapRef
  const { current: translatePointX } = translatePointXRef
  const { current: translatePointY } = translatePointYRef
  const wrapWidth: number = wrap?.offsetWidth || 0
  const wrapHeight: number = wrap?.offsetHeight || 0
  // 缩放位移坐标变化规律
  // (transformOrigin - downX) / scale * (scale-1) + downX - translateX = pointX
  const pointX: number = ((wrapWidth / 2) - x) / canvasScale * (canvasScale - 1) + x - translatePointX
  const pointY: number = ((wrapHeight / 2) - y) / canvasScale * (canvasScale - 1) + y - translatePointY
 
  return {
    pointX,
    pointY
  }
}
 
// 监听鼠标画笔事件
const handleLineMode = (downX: number, downY: number) => {
  const { current: canvas } = canvasRef
  const { current: wrap } = wrapRef
  const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d')
  if (!canvas || !wrap || !context) return
 
  const offsetLeft: number = canvas.offsetLeft
  const offsetTop: number = canvas.offsetTop
  // 减去画布偏移的距离(以画布为基准进行计算坐标)
  downX = downX - offsetLeft
  downY = downY - offsetTop
 
  const { pointX, pointY } = generateLinePoint(downX, downY)
  context.globalCompositeOperation = "source-over"
  context.beginPath()
  // 设置画笔起点
  context.moveTo(pointX, pointY)
 
  canvas.onmousemove = null
  canvas.onmousemove = (event: MouseEvent) => {
    const moveX: number = event.pageX - offsetLeft
    const moveY: number = event.pageY - offsetTop
    const { pointX, pointY } = generateLinePoint(moveX, moveY)
    // 开始绘制画笔线条~
    context.lineTo(pointX, pointY)
    context.stroke()
  }
  canvas.onmouseup = () => {
    context.closePath()
    canvas.onmousemove = null
    canvas.onmouseup = null
  }
}

7. 橡皮擦的实现

橡皮擦目前还有点问题,现在的话是通过将 canvas 画布的背景图片 + globalCompositeOperation 这个属性来模拟橡皮擦的实现,不过,这时候图片生成出来之后,橡皮擦的痕迹会变成白色,而不是透明

此步骤与画笔实现差不多,只有一点点小变动

设置属性 context.globalCompositeOperation = "destination-out"

8. 撤销与恢复的功能实现

这个的话,我们首先需要了解常见的撤销与恢复的功能的逻辑 分几种情况吧

  • 若当前状态处于第一个位置,则不允许撤销
  • 若当前状态处于最后一个位置,则不允许恢复
  • 如果当前撤销了,然而更新了状态,则取当前状态为最新的状态(也就是说不允许恢复了,这个刚更新的状态就是最新的)

画布状态的更新

所以我们需要设置一些变量来存,状态列表,与当前画笔的状态下标

我们还需要在初始化canvas的时候,我们就添加入当前的状态存入列表中,作为最先开始的空画布状态

然后我们就实现一下,画笔更新时候,我们也需要将当前的状态添加入 画笔状态列表 ,并且更新当前状态对应的下标,还需要处理一下一些细节

总结一下:

  • 鼠标抬起时,获取当前canvas画布状态
  • 添加进状态列表中,并且更新状态下标
  • 如果当前处于撤销状态,若使用画笔更新状态,则将当前的最为最新的状态,原先位置之后的状态全部清空

画布状态的撤销与恢复

ok,其实现在关于画布状态的更新,我们已经完成了。接下来我们需要处理一下状态的撤销与恢复的功能啦

我们先定义一下这个工具栏吧

 

 

 

然后我们设置对应的事件,分别是撤销,恢复,与清空,其实都很容易看懂,最多就是处理一下边界情况。

事件设置好之后,我们就可以开始监听一下这个 canvasCurrentHistory 当前状态下标,使用副作用函数进行处理

为canvas画布填充图像信息!

这样就大功告成啦!!!

9. 实现鼠标图标的变化

我们简单的处理一下,画笔模式则是画笔的图标,橡皮擦模式下鼠标是橡皮擦,移动模式下就是普通的移动图标

切换模式时候,设置一下不同的图标

10. 切换图片

现在的话只是一个demo状态,通过点击选择框,切换不同的图片

 

 

 

注意事项

注意容器的偏移量

我们需要注意一下,因为公式中的 downX 是相对容器的坐标,也就是说,我们需要减去容器的偏移量,这种情况会出现在使用了 margin 等参数,或者说上方或者左侧有别的元素的情况

我们输出一下我们红色的元素的 offsetLeft 等属性,会发现他是已经本身就有50的偏移量了,我们计算鼠标点击的坐标的时候就要减去这一部分的偏移量

 

 

 

注意父组件使用relative相对布局的情况

假如我们现在有一种这种的布局,打印红色元素的偏移量,看起来都挺正常的

 

 

 

但是如果我们目标元素的父元素(也就是黄色部分)设置 relative 相对布局

这时候我们打印出来的偏移量会是多少呢

 

 

 

两次答案不一样啊,因为我们的偏移量是根据相对位置来计算的,如果父容器使用相对布局,则会影响我们子元素的偏移量

组件代码(低配版)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
import React, { FC, ComponentType, useEffect, useRef, RefObject, useState, MutableRefObject } from 'react'
import { CustomBreadcrumb } from '@/admin/components'
import { RouteComponentProps } from 'react-router-dom';
import { FormComponentProps } from 'antd/lib/form';
import {
  Slider, Radio, Button, Tooltip, Icon, Select, Spin, message, Popconfirm
} from 'antd';
 
import './index.scss'
import { RadioChangeEvent } from 'antd/lib/radio';
import { getURLBase64 } from '@/admin/utils/getURLBase64'
 
const { Option, OptGroup } = Select;
 
type MarkPaperProps = RouteComponentProps & FormComponentProps
 
const MarkPaper: FC<MarkPaperProps> = (props: MarkPaperProps) => {
  const MOVE_MODE: number = 0
  const LINE_MODE: number = 1
  const ERASER_MODE: number = 2
  const canvasRef: RefObject<HTMLCanvasElement> = useRef(null)
  const containerRef: RefObject<HTMLDivElement> = useRef(null)
  const wrapRef: RefObject<HTMLDivElement> = useRef(null)
  const translatePointXRef: MutableRefObject<number> = useRef(0)
  const translatePointYRef: MutableRefObject<number> = useRef(0)
  const fillStartPointXRef: MutableRefObject<number> = useRef(0)
  const fillStartPointYRef: MutableRefObject<number> = useRef(0)
  const canvasHistroyListRef: MutableRefObject<ImageData[]> = useRef([])
  const [lineColor, setLineColor] = useState<string>('#fa4b2a')
  const [fillImageSrc, setFillImageSrc] = useState<string>('')
  const [mouseMode, setmouseMode] = useState<number>(MOVE_MODE)
  const [lineWidth, setLineWidth] = useState<number>(5)
  const [canvasScale, setCanvasScale] = useState<number>(1)
  const [isLoading, setIsLoading] = useState<boolean>(false)
  const [canvasCurrentHistory, setCanvasCurrentHistory] = useState<number>(0)
 
  useEffect(() => {
    setFillImageSrc('')
  }, [])
 
  // 重置变换参数,重新绘制图片
  useEffect(() => {
    setIsLoading(true)
    translatePointXRef.current = 0
    translatePointYRef.current = 0
    fillStartPointXRef.current = 0
    fillStartPointYRef.current = 0
    setCanvasScale(1)
    fillImage()
  }, [fillImageSrc])
 
  // 画布参数变动时,重新监听canvas
  useEffect(() => {
    handleCanvas()
  }, [mouseMode, canvasScale, canvasCurrentHistory])
 
  // 监听画笔颜色变化
  useEffect(() => {
    const { current: canvas } = canvasRef
    const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d')
    if (!context) return
 
    context.strokeStyle = lineColor
    context.lineWidth = lineWidth
    context.lineJoin = 'round'
    context.lineCap = 'round'
  }, [lineWidth, lineColor])
 
  //监听缩放画布
  useEffect(() => {
    const { current: canvas } = canvasRef
    const { current: translatePointX } = translatePointXRef
    const { current: translatePointY } = translatePointYRef
    canvas && (canvas.style.transform = `scale(${canvasScale},${canvasScale}) translate(${translatePointX}px,${translatePointY}px)`)
  }, [canvasScale])
 
  useEffect(() => {
    const { current: canvas } = canvasRef
    const { current: canvasHistroyList } = canvasHistroyListRef
    const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d')
    if (!canvas || !context || canvasCurrentHistory === 0) return
    context?.putImageData(canvasHistroyList[canvasCurrentHistory - 1], 0, 0)
  }, [canvasCurrentHistory])
 
  const fillImage = async () => {
    const { current: canvas } = canvasRef
    const { current: wrap } = wrapRef
    const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d')
    const img: HTMLImageElement = new Image()
 
    if (!canvas || !wrap || !context) return
 
    img.src = await getURLBase64(fillImageSrc)
    img.onload = () => {
      // 取中间渲染图片
      // const centerX: number = canvas && canvas.width / 2 - img.width / 2 || 0
      // const centerY: number = canvas && canvas.height / 2 - img.height / 2 || 0
      canvas.width = img.width
      canvas.height = img.height
 
      // 背景设置为图片,橡皮擦的效果才能出来
      canvas.style.background = `url(${img.src})`
      context.drawImage(img, 0, 0)
      context.strokeStyle = lineColor
      context.lineWidth = lineWidth
      context.lineJoin = 'round'
      context.lineCap = 'round'
 
      // 设置变化基点,为画布容器中央
      canvas.style.transformOrigin = `${wrap?.offsetWidth / 2}px ${wrap?.offsetHeight / 2}px`
      // 清除上一次变化的效果
      canvas.style.transform = ''
      const imageData: ImageData = context.getImageData(0, 0, canvas.width, canvas.height)
      canvasHistroyListRef.current = []
      canvasHistroyListRef.current.push(imageData)
      // canvasCurrentHistoryRef.current = 1
      setCanvasCurrentHistory(1)
      setTimeout(() => { setIsLoading(false) }, 500)
    }
  }
 
  const generateLinePoint = (x: number, y: number) => {
    const { current: wrap } = wrapRef
    const { current: translatePointX } = translatePointXRef
    const { current: translatePointY } = translatePointYRef
    const wrapWidth: number = wrap?.offsetWidth || 0
    const wrapHeight: number = wrap?.offsetHeight || 0
    // 缩放位移坐标变化规律
    // (transformOrigin - downX) / scale * (scale-1) + downX - translateX = pointX
    const pointX: number = ((wrapWidth / 2) - x) / canvasScale * (canvasScale - 1) + x - translatePointX
    const pointY: number = ((wrapHeight / 2) - y) / canvasScale * (canvasScale - 1) + y - translatePointY
 
    return {
      pointX,
      pointY
    }
  }
 
  const handleLineMode = (downX: number, downY: number) => {
    const { current: canvas } = canvasRef
    const { current: wrap } = wrapRef
    const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d')
    if (!canvas || !wrap || !context) return
 
    const offsetLeft: number = canvas.offsetLeft
    const offsetTop: number = canvas.offsetTop
    // 减去画布偏移的距离(以画布为基准进行计算坐标)
    downX = downX - offsetLeft
    downY = downY - offsetTop
 
    const { pointX, pointY } = generateLinePoint(downX, downY)
    context.globalCompositeOperation = "source-over"
    context.beginPath()
    context.moveTo(pointX, pointY)
 
    canvas.onmousemove = null
    canvas.onmousemove = (event: MouseEvent) => {
      const moveX: number = event.pageX - offsetLeft
      const moveY: number = event.pageY - offsetTop
      const { pointX, pointY } = generateLinePoint(moveX, moveY)
      context.lineTo(pointX, pointY)
      context.stroke()
    }
    canvas.onmouseup = () => {
      const imageData: ImageData = context.getImageData(0, 0, canvas.width, canvas.height)
 
      // 如果此时处于撤销状态,此时再使用画笔,则将之后的状态清空,以刚画的作为最新的画布状态
      if (canvasCurrentHistory < canvasHistroyListRef.current.length) {
        canvasHistroyListRef.current = canvasHistroyListRef.current.slice(0, canvasCurrentHistory)
      }
      canvasHistroyListRef.current.push(imageData)
      setCanvasCurrentHistory(canvasCurrentHistory + 1)
      context.closePath()
      canvas.onmousemove = null
      canvas.onmouseup = null
    }
  }
 
  const handleMoveMode = (downX: number, downY: number) => {
    const { current: canvas } = canvasRef
    const { current: wrap } = wrapRef
    const { current: fillStartPointX } = fillStartPointXRef
    const { current: fillStartPointY } = fillStartPointYRef
    if (!canvas || !wrap || mouseMode !== 0) return
 
    // 为容器添加移动事件,可以在空白处移动图片
    wrap.onmousemove = (event: MouseEvent) => {
      const moveX: number = event.pageX
      const moveY: number = event.pageY
 
      translatePointXRef.current = fillStartPointX + (moveX - downX)
      translatePointYRef.current = fillStartPointY + (moveY - downY)
 
      canvas.style.transform = `scale(${canvasScale},${canvasScale}) translate(${translatePointXRef.current}px,${translatePointYRef.current}px)`
    }
 
    wrap.onmouseup = (event: MouseEvent) => {
      const upX: number = event.pageX
      const upY: number = event.pageY
 
      wrap.onmousemove = null
      wrap.onmouseup = null;
 
      fillStartPointXRef.current = fillStartPointX + (upX - downX)
      fillStartPointYRef.current = fillStartPointY + (upY - downY)
    }
  }
 
  // 目前橡皮擦还有点问题,前端显示正常,保存图片下来,擦除的痕迹会变成白色
  const handleEraserMode = (downX: number, downY: number) => {
    const { current: canvas } = canvasRef
    const { current: wrap } = wrapRef
    const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d')
    if (!canvas || !wrap || !context) return
 
    const offsetLeft: number = canvas.offsetLeft
    const offsetTop: number = canvas.offsetTop
    downX = downX - offsetLeft
    downY = downY - offsetTop
 
    const { pointX, pointY } = generateLinePoint(downX, downY)
 
    context.beginPath()
    context.moveTo(pointX, pointY)
 
    canvas.onmousemove = null
    canvas.onmousemove = (event: MouseEvent) => {
      const moveX: number = event.pageX - offsetLeft
      const moveY: number = event.pageY - offsetTop
      const { pointX, pointY } = generateLinePoint(moveX, moveY)
      context.globalCompositeOperation = "destination-out"
      context.lineWidth = lineWidth
      context.lineTo(pointX, pointY)
      context.stroke()
    }
    canvas.onmouseup = () => {
      const imageData: ImageData = context.getImageData(0, 0, canvas.width, canvas.height)
      if (canvasCurrentHistory < canvasHistroyListRef.current.length) {
        canvasHistroyListRef.current = canvasHistroyListRef.current.slice(0, canvasCurrentHistory)
      }
      canvasHistroyListRef.current.push(imageData)
      setCanvasCurrentHistory(canvasCurrentHistory + 1)
      context.closePath()
      canvas.onmousemove = null
      canvas.onmouseup = null
    }
  }
 
  const handleCanvas = () => {
    const { current: canvas } = canvasRef
    const { current: wrap } = wrapRef
    const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d')
    if (!context || !wrap) return
 
    // 清除上一次设置的监听,以防获取参数错误
    wrap.onmousedown = null
    wrap.onmousedown = function (event: MouseEvent) {
      const downX: number = event.pageX
      const downY: number = event.pageY
 
      switch (mouseMode) {
        case MOVE_MODE:
          handleMoveMode(downX, downY)
          break
        case LINE_MODE:
          handleLineMode(downX, downY)
          break
        case ERASER_MODE:
          handleEraserMode(downX, downY)
          break
        default:
          break
      }
    }
 
    wrap.onwheel = null
    wrap.onwheel = (e: MouseWheelEvent) => {
      const { deltaY } = e
      const newScale: number = deltaY > 0
        ? (canvasScale * 10 - 0.1 * 10) / 10
        : (canvasScale * 10 + 0.1 * 10) / 10
      if (newScale < 0.1 || newScale > 2) return
      setCanvasScale(newScale)
    }
  }
 
  const handleScaleChange = (value: number) => {
    setCanvasScale(value)
  }
 
  const handleLineWidthChange = (value: number) => {
    setLineWidth(value)
  }
 
  const handleColorChange = (color: string) => {
    setLineColor(color)
  }
 
  const handleMouseModeChange = (event: RadioChangeEvent) => {
    const { target: { value } } = event
    const { current: canvas } = canvasRef
    const { current: wrap } = wrapRef
 
    setmouseMode(value)
 
    if (!canvas || !wrap) return
    switch (value) {
      case MOVE_MODE:
        canvas.style.cursor = 'move'
        wrap.style.cursor = 'move'
        break
      case LINE_MODE:
        wrap.style.cursor = 'default'
        break
      case ERASER_MODE:
        message.warning('橡皮擦功能尚未完善,保存图片会出现错误')
        wrap.style.cursor = 'default'
        break
      default:
        canvas.style.cursor = 'default'
        wrap.style.cursor = 'default'
        break
    }
  }
 
  const handleSaveClick = () => {
    const { current: canvas } = canvasRef
    // 可存入数据库或是直接生成图片
    console.log(canvas?.toDataURL())
  }
 
  const handlePaperChange = (value: string) => {
    const fillImageList = {
    }
    setFillImageSrc(fillImageList[value])
  }
 
  const handleRollBack = () => {
    const isFirstHistory: boolean = canvasCurrentHistory === 1
    if (isFirstHistory) return
    setCanvasCurrentHistory(canvasCurrentHistory - 1)
  }
 
  const handleRollForward = () => {
    const { current: canvasHistroyList } = canvasHistroyListRef
    const isLastHistory: boolean = canvasCurrentHistory === canvasHistroyList.length
    if (isLastHistory) return
    setCanvasCurrentHistory(canvasCurrentHistory + 1)
  }
 
  const handleClearCanvasClick = () => {
    const { current: canvas } = canvasRef
    const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d')
    if (!canvas || !context || canvasCurrentHistory === 0) return
 
    // 清空画布历史
    canvasHistroyListRef.current = [canvasHistroyListRef.current[0]]
    setCanvasCurrentHistory(1)
 
    message.success('画布清除成功!')
  }
 
  return (
    <div>
      <CustomBreadcrumb list={['内容管理', '批阅作业']} />
      <div className="mark-paper__container" ref={containerRef}>
        <div className="mark-paper__wrap" ref={wrapRef}>
          <div
            className="mark-paper__mask"
            style={{ display: isLoading ? 'flex' : 'none' }}
          >
            <Spin
              tip="图片加载中..."
              indicator={<Icon type="loading" style={{ fontSize: 36 }} spin
              />}
            />
          </div>
          <canvas
            ref={canvasRef}
            className="mark-paper__canvas">
            <p>很可惜,这个东东与您的电脑不搭!</p>
          </canvas>
        </div>
        <div className="mark-paper__sider">
          <div>
            选择作业:
            <Select
              defaultValue="xueshengjia"
              style={{
                width: '100%', margin: '10px 0 20px 0'
              }}
              onChange={handlePaperChange} >
              <OptGroup label="17软件一班">
                <Option value="xueshengjia">学生甲</Option>
                <Option value="xueshengyi">学生乙</Option>
              </OptGroup>
              <OptGroup label="17软件二班">
                <Option value="xueshengbing">学生丙</Option>
              </OptGroup>
            </Select>
          </div>
          <div>
            画布操作:<br />
            <div className="mark-paper__action">
              <Tooltip title="撤销">
                <i
                  className={`icon iconfont icon-chexiao ${canvasCurrentHistory <= 1 && 'disable'}`}
                  onClick={handleRollBack} />
              </Tooltip>
              <Tooltip title="恢复">
                <i
                  className={`icon iconfont icon-fanhui ${canvasCurrentHistory >= canvasHistroyListRef.current.length && 'disable'}`}
                  onClick={handleRollForward} />
              </Tooltip>
              <Popconfirm
                title="确定清空画布吗?"
                onConfirm={handleClearCanvasClick}
                okText="确定"
                cancelText="取消"
              >
                <Tooltip title="清空">
                  <i className="icon iconfont icon-qingchu" />
                </Tooltip>
              </Popconfirm>
            </div>
          </div>
          <div>
            画布缩放:
            <Tooltip placement="top" title='可用鼠标滚轮进行缩放'>
              <Icon type="question-circle" />
            </Tooltip>
            <Slider
              min={0.1}
              max={2.01}
              step={0.1}
              value={canvasScale}
              tipFormatter={(value) => `${(value).toFixed(2)}x`}
              onChange={handleScaleChange} />
          </div>
          <div>
            画笔大小:
            <Slider
              min={1}
              max={9}
              value={lineWidth}
              tipFormatter={(value) => `${value}px`}
              onChange={handleLineWidthChange} />
          </div>
          <div>
            模式选择:
            <Radio.Group
              className="radio-group"
              onChange={handleMouseModeChange}
              value={mouseMode}>
              <Radio value={0}>移动</Radio>
              <Radio value={1}>画笔</Radio>
              <Radio value={2}>橡皮擦</Radio>
            </Radio.Group>
          </div>
          <div>
            颜色选择:
            <div className="color-picker__container">
              {['#fa4b2a', '#ffff00', '#ee00ee', '#1890ff', '#333333', '#ffffff'].map(color => {
                return (
                  <Tooltip placement="top" title={color} key={color}>
                    <div
                      role="button"
                      className={`color-picker__wrap ${color === lineColor && 'color-picker__wrap--active'}`}
                      style={{ background: color }}
                      onClick={() => handleColorChange(color)}
                    />
                  </Tooltip>
                )
              })}
            </div>
          </div>
          <Button onClick={handleSaveClick}>保存图片</Button>
        </div>
      </div>
    </div >
  )
}
 
export default MarkPaper as ComponentType


版权声明 : 本文内容来源于互联网或用户自行发布贡献,该文观点仅代表原作者本人。本站仅提供信息存储空间服务和不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权, 违法违规的内容, 请发送邮件至2530232025#qq.cn(#换@)举报,一经查实,本站将立刻删除。
原文链接 : https://www.jb51.net/html5/716686.html
相关文章
  • html5+实现plus.io进行拍照和图片等获取
    使用Hbuilder开发工具开发:实现可对Android机进行控制和获取资源 说明:IO模块管理本地文件系统,用于对文件系统的目录浏览、文件的读取
  • HTML标签实现带动画的抖音LOGO效果

    HTML标签实现带动画的抖音LOGO效果
    先上结果,最终实现效果如下: 还原度应该还可以吧? 抖音Logo结构 想要用CSS来画抖音的Logo,前提要先了解它的构造,一定是一些几何图形
  • video.js支持m3u8格式直播的实现教程

    video.js支持m3u8格式直播的实现教程
    为什么要使用video.js? 1. PC端浏览器并不支持video直接播放m3u8格式的视频 2. 手机端各式各样的浏览器定制的video界面风格不统一,直接写原生
  • html5调用摄像头截图功能的介绍
    关于html5调用音视频等多媒体硬件的API已经很成熟,不过一直找不到机会把这些硬件转化为实际的应用场景,不过近年来随着iot和AI的浪潮,
  • 前端监听websocket消息并实时弹出的代码实例介绍

    前端监听websocket消息并实时弹出的代码实例介绍
    本文默认您已掌握react生态开发的相关技术,并熟练应用umiJS的原则上,请继续! 项目需求: 1、服务侧推送给消息给前端,前端需要展示在
  • HTML5新增内容和API介绍

    HTML5新增内容和API介绍
    classList 属性 该属性用于在元素中添加,移除及切换 CSS 类。 classList属性返回元素的类名,作为 DOMTokenList 对象: div id=div class=test1 test2clas
  • 使用canvas仿Echarts实现金字塔图的教程

    使用canvas仿Echarts实现金字塔图的教程
    最近公司项目都偏向于数字化大屏展示????,而这次发给我的项目原型中出现了一个金字塔图?????, 好巧不巧,由于我们的图表都是使用Ech
  • 基于HTML十秒做出淘宝页面的方法

    基于HTML十秒做出淘宝页面的方法
    十秒钟做出一个网页,可能大家看完会觉得有点标题党,其实不然,把别人的抠过来还是可以的 比如我这次先把淘宝的网页扣过来为例 第一
  • HTML5+CSS+JavaScript实现捉虫小游戏的代码

    HTML5+CSS+JavaScript实现捉虫小游戏的代码
    捉虫小游戏 首页展示: 选择昆虫: 效果展示: 有密集恐惧症的别玩哟、游戏永远不会停止 一直玩 项目源码结构: 图片和js以及css等基础
  • Canvas绘制像素风图片的代码

    Canvas绘制像素风图片的代码
    童年玩红白机。尤其国内的小霸王那段时光还记得么。那个马里奥大叔还记得么。 因为特别喜欢像素风的游戏从小到大一直都是,像素风本
  • 本站所有内容来源于互联网或用户自行发布,本站仅提供信息存储空间服务,不拥有版权,不承担法律责任。如有侵犯您的权益,请您联系站长处理!
  • Copyright © 2017-2022 F11.CN All Rights Reserved. F11站长开发者网 版权所有 | 苏ICP备2022031554号-1 | 51LA统计