当前位置: 首页 > news >正文

大型的营销型网站南京建设网

大型的营销型网站,南京建设网,南京移动网站建设哪里强,嵌入式软件开发有哪些(给前端大学加星标#xff0c;提升前端技能.)作者#xff1a;zhcxk1998https://juejin.im/user/5d4304bdf265da03d15531dc哈哈哈俺又来啦#xff0c;这次带来的是canvas实现一些画布功能的文章#xff0c;希望大家喜欢#xff01;这个css3变化公式可以适用于平常我们使用的… (给前端大学加星标提升前端技能.)作者zhcxk1998https://juejin.im/user/5d4304bdf265da03d15531dc哈哈哈俺又来啦这次带来的是canvas实现一些画布功能的文章希望大家喜欢这个css3变化公式可以适用于平常我们使用的transform属性或者是移动端我们缩放地图啊之类的都可以哟前言因为也是大三了最近俺也在找实习之前有一个自己的小项目:https://github.com/zhcxk1998/School-Partners面试官说可以往深层次思考一下或许加一些新的功能来增加项目的难度他提了几个建议其中一个就是试卷在线批阅老师可以在上面对作业进行批注圈圈点点等俺当天晚上就开始研究这个东东哈哈哈终于被我研究出来啦采用的是canvas绘制画笔由css3的transform属性来进行平移与缩放因为呢考虑到如果用canvas的drawImage或者scale等属性进行变化生成出来的图片也会有影响想着直接css3变化canvas用来做画笔等功能。大佬们有何妙招在评论区指点指点(希望大家可以留下宝贵的赞与star嘻嘻)效果预览动图是放cdn的如果访问不了可以登录在线尝试尝试:http://test.algbb.cn/#/admin/content/mark-paper公式推导如果不想看公式如何推导可以直接跳过看后面的具体实现~1.坐标转换公式转换公式介绍其实一开始也是想在网上找一下有没有相关的资料但是可惜找不到所以就自己慢慢的推出来了。我就举一下横坐标的例子吧通用公式这个公式是表示通过公式来将鼠标按下的坐标转换为画布中的相对坐标这一点尤为重要(transformOrigin - downX) / scale * (scale-1) downX - translateX pointX参数解释transformOrigin: transform变化的基点(通过这个属性来控制元素以哪里进行变化)downX: 鼠标按下的坐标(注意用的时候需要减去容器左偏移距离因为我们要的是相对于容器的坐标)scale: 缩放倍数默认为1translateX: 平移的距离推导过程这个公式的话其实就比较通用可以用在别的利用到transform属性的场景至于怎么推导的话我是用的笨办法具体的测试代码放在文末需要自取~1. 先做出两个相同的元素然后标记上坐标并且设置容器属性overflow:hidden来隐藏溢出内容ok现在就有两个一样的矩阵啦我们为他标记上一些红点然后我们对左边的进行css3的样式变化transform矩形的宽高是360px * 360px的我们定义一下他的变化属性变化基点选择正中心放大3倍// csstransform-origin: 180px 180px;transform: scale(3, 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啦。我们测试一下我们先修改一下样式新增一下位移的距离transform-origin: 180px 180px;transform: scale(3, 3) translate(-40px,-40px);还是我们上面的状态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啊(手动滑稽)const wrap document.getElementById(wrap)wrap.onmousedown function (e) { const downX e.pageX - wrap.offsetLeft const downY e.pageY - wrap.offsetTop const scale 3 const translateX -40 const translateY -40 const transformOriginX 180 const transformOriginY 180 const dot document.getElementById(dot) dot.style.left (transformOriginX - downX) / scale * (scale - 1) downX - translateX px dot.style.top (transformOriginY - downY) / scale * (scale - 1) downY - translateY px}可能有人会问为什么要减去这个offsetLeft跟offsetTop呢因为我们上面反复强调我们计算的是鼠标点击的坐标而这个坐标还是相对于我们展示容器的坐标所以我们要减去容器本身的偏移量才行。组件设计既然demo啥的都已经测试了ok了我们接下来就逐一分析一下这个组件应该咋设计好呢(目前仍为低配版之后再进行优化完善)1. 基本的画布构成我们先简单分析一下这个构成吧其实主要就是一个画布的容器右边一个工具栏仅此而已大体就这样子啦 ref{canvasRef} classNamemark-paper__canvas 很可惜这个东东与您的电脑不搭 我们唯一需要的一点就是容器需要设置属性overflow: hidden用来隐藏内部canvas画布溢出的内容也就是说我们要控制我们可视的区域。同时我们需要动态获取容器宽高来为canvas设置尺寸2. 初始化canvas画布与填充图片我们可以弄个方法来初始化并且填充画布以下截取主要部分其实就是为canvas画布设置尺寸与填充我们的图片const fillImage async () { // 此处省略... const img: HTMLImageElement new Image() img.src await getURLBase64(fillImageSrc) img.onload () { canvas.width img.width canvas.height img.height context.drawImage(img, 0, 0) // 设置变化基点为画布容器中央 canvas.style.transformOrigin ${wrap?.offsetWidth / 2}px ${wrap?.offsetHeight / 2}px // 清除上一次变化的效果 canvas.style.transform }}3. 监听canvas画布的各种鼠标事件这个控制移动的话我们首先可以弄一个方法来监听画布鼠标的各种事件可以区分不同的模式来进行不同的事件处理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 } }4. 实现画布移动这个就比较好办啦我们只需要利用鼠标按下的坐标和我们拖动的距离就可以实现画布的移动啦因为涉及到每次移动都需要计算最新的位移距离我们可以定义几个变量来进行计算。这里监听的是容器的鼠标事件而不是canvas画布的事件因为这样子我们可以再移动超过边界的时候也可以进行移动操作简单的总结一下传入鼠标按下的坐标计算当前位移距离并更新css变化效果鼠标抬起时更新最新的位移状态// 定义一些变量来保存当前/最新的移动状态// 当前位移的距离const translatePointXRef: MutableRefObject useRef(0)const translatePointYRef: MutableRefObject useRef(0)// 上一次位移结束的位移距离const fillStartPointXRef: MutableRefObject useRef(0)const fillStartPointYRef: MutableRefObject 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. 实现画布缩放画布缩放我主要通过右侧的滑动条以及鼠标滚轮来实现首先我们再监听画布鼠标事件的函数中加一下监听滚轮的事件总结一下监听鼠标滚轮的变化更新缩放倍数并改变样式// 监听鼠标滚轮更新画布缩放倍数const handleCanvas () { const { current: wrap } wrapRef // 省略一万字... wrap.onwheel null wrap.onwheel (e: MouseWheelEvent) { const { deltaY } e // 这里要注意一下我是0.1来递增递减但是因为JS使用IEEE 754来计算所以精度有问题我们自己处理一下 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) }}// 监听滑动条来控制缩放 min{0.1} max{2.01} step{0.1} value{canvasScale} tipFormatter{(value) ${(value).toFixed(2)}x} onChange{handleScaleChange} /const handleScaleChange (value: number) { setCanvasScale(value)}接着我们使用hooks的副作用函数依赖于画布缩放倍数来进行样式的更新//监听缩放画布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])6. 实现画笔绘制这个就需要用到我们之前推导出来的公式啦因为呢仔细想一下如果我们缩放位移之后我们鼠标按下的位置他的坐标可能就相对于画布来说会有变化所以我们需要转换一下才能进行鼠标按下的位置与画布的位置一一对应的效果稍微总结一下传入鼠标按下的坐标通过公式转换开始在对应坐标下绘制鼠标抬起时取消事件监听// 利用公式转换一下坐标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// 目前橡皮擦还有点问题前端显示正常保存图片下来擦除的痕迹会变成白色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 () { context.closePath() canvas.onmousemove null canvas.onmouseup null }}8. 撤销与恢复的功能实现这个的话我们首先需要了解常见的撤销与恢复的功能的逻辑分几种情况吧若当前状态处于第一个位置则不允许撤销若当前状态处于最后一个位置则不允许恢复如果当前撤销了然而更新了状态则取当前状态为最新的状态(也就是说不允许恢复了这个刚更新的状态就是最新的)画布状态的更新所以我们需要设置一些变量来存状态列表与当前画笔的状态下标// 定义参数存东东const canvasHistroyListRef: MutableRefObject useRef([])const [canvasCurrentHistory, setCanvasCurrentHistory] useState(0)我们还需要在初始化canvas的时候我们就添加入当前的状态存入列表中作为最先开始的空画布状态const fillImage async () { // 省略一万字... img.src await getURLBase64(fillImageSrc) img.onload () { const imageData: ImageData context.getImageData(0, 0, canvas.width, canvas.height) canvasHistroyListRef.current [] canvasHistroyListRef.current.push(imageData) setCanvasCurrentHistory(1) }}然后我们就实现一下画笔更新时候我们也需要将当前的状态添加入画笔状态列表并且更新当前状态对应的下标还需要处理一下一些细节总结一下鼠标抬起时获取当前canvas画布状态添加进状态列表中并且更新状态下标如果当前处于撤销状态若使用画笔更新状态则将当前的最为最新的状态原先位置之后的状态全部清空const handleLineMode (downX: number, downY: number) { // 省略一万字... 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 }}画布状态的撤销与恢复ok其实现在关于画布状态的更新我们已经完成了。接下来我们需要处理一下状态的撤销与恢复的功能啦我们先定义一下这个工具栏吧然后我们设置对应的事件分别是撤销恢复与清空其实都很容易看懂最多就是处理一下边界情况。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(画布清除成功)}事件设置好之后我们就可以开始监听一下这个canvasCurrentHistory当前状态下标使用副作用函数进行处理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])为canvas画布填充图像信息这样就大功告成啦9. 实现鼠标图标的变化我们简单的处理一下画笔模式则是画笔的图标橡皮擦模式下鼠标是橡皮擦移动模式下就是普通的移动图标切换模式时候设置一下不同的图标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: canvas.style.cursor url(http://cdn.algbb.cn/pencil.ico) 6 26, pointer wrap.style.cursor default break case ERASER_MODE: message.warning(橡皮擦功能尚未完善保存图片会出现错误) canvas.style.cursor url(http://cdn.algbb.cn/eraser.ico) 6 26, pointer wrap.style.cursor default break default: canvas.style.cursor default wrap.style.cursor default break }}10. 切换图片现在的话只是一个demo状态通过点击选择框切换不同的图片// 重置变换参数重新绘制图片useEffect(() { setIsLoading(true) translatePointXRef.current 0 translatePointYRef.current 0 fillStartPointXRef.current 0 fillStartPointYRef.current 0 setCanvasScale(1) fillImage()}, [fillImageSrc])const handlePaperChange (value: string) { const fillImageList { xueshengjia: http://cdn.algbb.cn/test/canvasTest.jpg, xueshengyi: http://cdn.algbb.cn/test/canvasTest2.png, xueshengbing: http://cdn.algbb.cn/emoji/30.png, } setFillImageSrc(fillImageList[value])}注意事项注意容器的偏移量我们需要注意一下因为公式中的downX是相对容器的坐标也就是说我们需要减去容器的偏移量这种情况会出现在使用了margin等参数或者说上方或者左侧有别的元素的情况我们输出一下我们红色的元素的offsetLeft等属性会发现他是已经本身就有50的偏移量了我们计算鼠标点击的坐标的时候就要减去这一部分的偏移量window.onload function () { const test document.getElementById(test) console.log(offsetLeft: ${test.offsetLeft}, offsetHeight: ${test.offsetTop})}html,body { margin: 0; padding: 0;}#test { width: 50px; height: 50px; margin-left: 50px; background: red;}注意父组件使用relative相对布局的情况假如我们现在有一种这种的布局打印红色元素的偏移量看起来都挺正常的但是如果我们目标元素的父元素(也就是黄色部分)设置relative相对布局.wrap { position: relative; width: 400px; height: 300px; background: yellow;}这时候我们打印出来的偏移量会是多少呢两次答案不一样啊因为我们的偏移量是根据相对位置来计算的如果父容器使用相对布局则会影响我们子元素的偏移量组件代码(低配版)import React, { FC, ComponentType, useEffect, useRef, RefObject, useState, MutableRefObject } from reactimport { CustomBreadcrumb } from /admin/componentsimport { 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.scssimport { RadioChangeEvent } from antd/lib/radio;import { getURLBase64 } from /admin/utils/getURLBase64const { Option, OptGroup } Select;type MarkPaperProps RouteComponentProps FormComponentPropsconst MarkPaper: FC (props: MarkPaperProps) { const MOVE_MODE: number 0 const LINE_MODE: number 1 const ERASER_MODE: number 2 const canvasRef: RefObject useRef(null) const containerRef: RefObject useRef(null) const wrapRef: RefObject useRef(null) const translatePointXRef: MutableRefObject useRef(0) const translatePointYRef: MutableRefObject useRef(0) const fillStartPointXRef: MutableRefObject useRef(0) const fillStartPointYRef: MutableRefObject useRef(0) const canvasHistroyListRef: MutableRefObject useRef([]) const [lineColor, setLineColor] useState(#fa4b2a) const [fillImageSrc, setFillImageSrc] useState() const [mouseMode, setmouseMode] useState(MOVE_MODE) const [lineWidth, setLineWidth] useState(5) const [canvasScale, setCanvasScale] useState(1) const [isLoading, setIsLoading] useState(false) const [canvasCurrentHistory, setCanvasCurrentHistory] useState(0) useEffect(() { setFillImageSrc(http://cdn.algbb.cn/test/canvasTest.jpg) }, []) // 重置变换参数重新绘制图片 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: canvas.style.cursor url(http://cdn.algbb.cn/pencil.ico) 6 26, pointer wrap.style.cursor default break case ERASER_MODE: message.warning(橡皮擦功能尚未完善保存图片会出现错误) canvas.style.cursor url(http://cdn.algbb.cn/eraser.ico) 6 26, pointer 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 { xueshengjia: http://cdn.algbb.cn/test/canvasTest.jpg, xueshengyi: http://cdn.algbb.cn/test/canvasTest2.png, xueshengbing: http://cdn.algbb.cn/emoji/30.png, } 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 ( classNamemark-paper__mask style{{ display: isLoading ? flex : none }} tip图片加载中... indicator{ /} / ref{canvasRef} classNamemark-paper__canvas 很可惜这个东东与您的电脑不搭 选择作业 defaultValuexueshengjia style{{ width: 100%, margin: 10px 0 20px 0 }} onChange{handlePaperChange} 学生甲 学生乙 学生丙 画布操作 className{icon iconfont icon-chexiao ${canvasCurrentHistory 1 disable}} onClick{handleRollBack} / className{icon iconfont icon-fanhui ${canvasCurrentHistory canvasHistroyListRef.current.length disable}} onClick{handleRollForward} / onConfirm{handleClearCanvasClick} okText确定 cancelText取消 画布缩放 min{0.1} max{2.01} step{0.1} value{canvasScale} tipFormatter{(value) ${(value).toFixed(2)}x} onChange{handleScaleChange} / 画笔大小 min{1} max{9} value{lineWidth} tipFormatter{(value) ${value}px} onChange{handleLineWidthChange} / 模式选择 classNameradio-group onChange{handleMouseModeChange} value{mouseMode} 移动 画笔 橡皮擦 颜色选择 {[#fa4b2a, #ffff00, #ee00ee, #1890ff, #333333, #ffffff].map(color { return ( rolebutton className{color-picker__wrap ${color lineColor color-picker__wrap--active}} style{{ background: color }} onClick{() handleColorChange(color)} / ) })} 保存图片 )}export default MarkPaper as ComponentType结语如果这篇东东对大家有所帮助希望大家可以给我点赞一下鼓励一下或者给俺的项目点个star支持支持吧github.com/zhcxk1998/School-Partners菜鸡分析的不到位还请各位大佬指出俺的不足阿里嘎多~分享前端好文点亮 在看
http://www.huolong8.cn/news/383571/

相关文章:

  • “设计网站”备案的网站每年都要备案么
  • 网站建设就业方向搜索引擎优化策略不包括
  • 济宁网站建设 悍诺苏州网络推广优化
  • 平顶山哪里有做网站的公司番禺微网站建设
  • 做网站需要哪些知识网页浏览器主要通过什么协议
  • html5 国外网站html网页设计模板免费下载
  • 企业网站建设合同方案wordpress博文模板
  • 用php做企业网站的可行性Wordpress 大数据量优化
  • 网站分站系遵化建设局网站
  • 保山网站建设优化wap网站psd
  • 网站在线支付接口清河网站建设设计费用
  • 响应式网站开发方法分成型网站建设
  • 邢台集团网站建设价格seo wordpress主题
  • 如何把自己电脑做网站服务器吗平面设计广告设计
  • 网站建设完成后怎么上传服务器广东学校网站建设公司
  • 做美团网站需要多少钱召开网站建设培训会
  • 上传了网站程序后成立一个网站
  • 哪个网站看电视剧最全还免费大学生网站建设方案
  • 做网站页面一般用什么软件wordpress文章中标签
  • 徐州网站建设找哪家怎么样推广自己的公司
  • 企业网站建设用什么云主机费用
  • html5国内网站北京网站开发公司有哪些
  • 做餐饮在环保局网站备案模板网站难做seo
  • 网站建设补充合同范本国家住房和城乡建设部官方网站
  • wordpress 用户注册 插件南京做网站优化公司
  • 宁波营销型网站建设首选宠物网站建设
  • 西安网站优化维护网页设计颜色代码表
  • 做网站找人网站首页做后台链接
  • 线上注册公司是在哪个网站想做一个网站怎么做的
  • 焦作建设企业网站公司用自己的名字设计头像