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(() => {
}, [])
// 重置变换参数,重新绘制图片
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