找回密码
 会员注册
查看: 14|回复: 0

使用canvas实现一个小小的截图功能

[复制链接]

2万

主题

0

回帖

7万

积分

超级版主

积分
74504
发表于 2024-9-30 15:54:00 | 显示全部楼层 |阅读模式
效果图话不多说,先看下效果 实现思路考虑了一下这个功能,肯定得用 hook,因为是一个有状态的东西,hook 需要返回截图,取消截图的功能函数以及截取图片在图片所在的同一父元素节点下添加两个 canvas,【canvas A】用于展示截图动效(比如,未被截取区域背景置灰,截取区域显示边框);【canvas B】用于展示完整图片,便于截取动作进行以及生成截图数据(记住 canvas A 和 canvas B,后面讲解还会用到)通过在【canvas A】上通过监听mouseup,mousemove,mousedown三个事件计算截取的区域,生成截取动效,生成截取图片等截图动作完毕的时候即时生成截取图片数据返回 难点1. 计算截取区域在截图开始后,在【canvas A】的mousedown事件记录起点坐标 A, 通过mousemove事件实时监听具体的坐标,document 级的mouseup事件记录结束坐标 B(鼠标有可能会跑出截图区域外,所以是在 document 上监听 mouseup),以 A 为起点,B 为终点,AB 两点就能够计算出截取区域//获取截图开始的点【canvasA】.onmousedown=function(e){记录起点坐标A}//获取鼠标坐标【canvasA】.onmousemove=function(坐标数据){1.记录鼠标坐标2.生成截图区域动效}//获取截图结束的点document.addEventListener('mouseup',function(e){1.记录终点坐标2.生成截图()}2 截图动画效果(未被选取部分置灰,截取部分添加边框等)在mousedown事件上把【canvas A】给置灰//设置截图时灰色背景【canvasA】.fillStyle='rgba(0,0,0,0.6)'【canvasA】.strokeStyle='rgba(0,143,255,1)'在mouseup事件上绘制被截取效果第一步:遮罩层:globalCompositeOperation = 'source-over' 表示在目标图像上显示源图像,那么此时进行fillRect(0, 0, 【canvas A】.width, 【canvas A】.height)那就是把我们之前设置好置灰样式在目标图片上层进行绘制,实现了第一步置灰效果第二步:画框:globalCompositeOperation = 'destination-out' 在源图像之外显示目标图像。只有源图像之外的目标图像部分会被显示,源图像是透明的。【canvas A】.fillRect(x, y, w, h)使得截取内部透明第三步:描边:懂的都懂,不细讲了//第一步:遮罩层【canvasA】.globalCompositeOperation='source-over'【canvasA】.fillRect(0,0,【canvasA】.width,【canvasA】.height)//第二步:画框【canvasA】.globalCompositeOperation='destination-out'【canvasA】.fillRect(x,y,w,h)//第三步:描边【canvasA】.globalCompositeOperation='source-over'【canvasA】.moveTo(x,y)【canvasA】.lineTo(x+w,y)【canvasA】.lineTo(x+w,y+h)【canvasA】.lineTo(x,y+h)【canvasA】.lineTo(x,y)【canvasA】.stroke()【canvasA】.closePath()3. 生成&获得截取区域图片鼠标动作停止后就是截图结束,所以需要在moveup事件生成截取图片数据,在这里可以通过 canvas 自带的 canvas.toDataURL 把截图转化为 base64,因为通过mousedown和mousemove我们已经获取用户的截取区域了,并且我们在截图开始的时候,会把原图片绘制到【canvas B】中,所以我们可以直接在【canvas B】上对该区域进行截取然后生成图片~constcanvas=document.createElement('canvas')constcontext=canvas.getContext('2d')constdata=【canvasB】.getImageData(area.x,area.y,area.w,area.h)canvas.width=area.wcanvas.height=area.hcontext.putImageData(data,0,0)returncanvas.toDataURL('image/png',1) 完整代码我已经把截图功能封装成了一个 hook,有需要自取。还比较糙,有问题随时反馈。使用方法这个 hook 会返回三个函数 init, cut, cancelCut,以及截图数据 clipImgData,init:在 init 函数把需要截图区域的父元素传进去cut:开始截图,需要把原图片作为参数传入cancelCut:取消截图功能clipImgData:base64 格式的截图数据1. 截图功能 hookconstclip=()=>{constclipAreaWrap=useRef(null)//截图区域domconstclipCanvas=useRef(null)//用于截图的的canvas,以及截图开始生成截图效果(背景置灰)constdrawCanvas=useRef(null)//把图片绘制到canvas上方便用于生成截取图片的base64数据const[clipImgData,setClipImgData]=useState('')constinit=(wrap)=>{if(!wrap)returnclipAreaWrap.current=wrapclipCanvas.current=document.createElement('canvas')drawCanvas.current=document.createElement('canvas')clipCanvas.current.style='width:100%;height:100%;z-index:2;position:absolute;left:0;top:0;'drawCanvas.current.style='width:100%;height:100%;z-index:1;position:absolute;left:0;top:0;'clipAreaWrap.current.appendChild(clipCanvas.current)clipAreaWrap.current.appendChild(drawCanvas.current)}//截图constcut=(souceImg:string)=>{constdrawCanvasCtx=drawCanvas.current.getContext('2d')constclipCanvasCtx=clipCanvas.current.getContext('2d')constwrapWidth=clipAreaWrap.current.clientWidthconstwrapHeight=clipAreaWrap.current.clientHeightclipCanvas.current.width=wrapWidthclipCanvas.current.height=wrapHeightdrawCanvas.current.width=wrapWidthdrawCanvas.current.height=wrapHeight//设置截图时灰色背景clipCanvasCtx.fillStyle='rgba(0,0,0,0.6)'clipCanvasCtx.strokeStyle='rgba(0,143,255,1)'//生成一个截取区域的img然后把它作为canvas的第一个参数constclipImg=document.createElement('img')clipImg.classList.add('img_anonymous')clipImg.crossOrigin='anonymous'clipImg.src=souceImg//Q:这里为什么需要append到clipAreaWrap里//A:因为直接clipImg.src的引入是没有css样式的(主要是宽高)如果不append直接进行drawCanvasCtx.drawImage,//那其实画的是原始大小的clipImgclipAreaWrap.current.appendChild(clipImg)//绘制截图区域clipImg.onload=()=>{//x,y->计算从drawCanvasCtx的的哪一x,y坐标点进行绘制constx=Math.floor((wrapWidth-clipImg.width)/2)consty=Math.floor((wrapHeight-clipImg.height)/2)//Q:为什么这里要用克隆节点的宽高//A:因为clipImg的宽高是在dom中已经被css修改过的宽高(长/宽)了,而非该图片的真实长和宽//用这个宽高在drawCanvasCtx的绘图只会绘制clipImg的小部分内容(因为假宽高比真宽高小),看起来就像是被放大了constclipImgCopy=clipImg.cloneNode()drawCanvasCtx.drawImage(clipImg,0,0,clipImgCopy.width,clipImgCopy.height,x,y,clipImg.width,clipImg.height)}letstart=null//获取截图开始的点clipCanvas.current.onmousedown=function(e){start={x:e.offsetX,y:e.offsetY}}//绘制截图区域效果clipCanvas.current.onmousemove=function(e){if(start){fill(clipCanvasCtx,wrapWidth,wrapHeight,start.x,start.y,e.offsetX-start.x,e.offsetY-start.y)}}//截图完毕,获取截图图片数据document.addEventListener('mouseup',function(e){if(start){varurl=getClipPicUrl({x:start.x,y:start.y,w:e.offsetX-start.x,h:e.offsetY-start.y},drawCanvasCtx)start=null//生成base64格式的图setClipImgData(url)}})}constcancelCut=()=>{clipCanvas.current.width=clipAreaWrap.current.clientWidthclipCanvas.current.height=clipAreaWrap.current.clientHeightdrawCanvas.current.width=clipAreaWrap.current.clientWidthdrawCanvas.current.height=clipAreaWrap.current.clientHeightconstdrawCanvasCtx=drawCanvas.current.getContext('2d')constclipCanvasCtx=clipCanvas.current.getContext('2d')drawCanvasCtx.clearRect(0,0,drawCanvas.current.clientWidth,drawCanvas.current.clientHeight)clipCanvasCtx.clearRect(0,0,clipCanvas.current.clientWidth,clipCanvas.current.clientHeight)//移除鼠标事件clipCanvas.current.onmousedown=nullclipCanvas.current.onmousemove=null}constgetClipPicUrl=(area,drawCanvasCtx)=>{constcanvas=document.createElement('canvas')constcontext=canvas.getContext('2d')constdata=drawCanvasCtx.getImageData(area.x,area.y,area.w,area.h)canvas.width=area.wcanvas.height=area.hcontext.putImageData(data,0,0)returncanvas.toDataURL('image/png',1)}//绘制出截图的效果constfill=(context,ctxWidth,ctxHeight,x,y,w,h)=>{context.clearRect(0,0,ctxWidth,ctxHeight)context.beginPath()//遮罩层context.globalCompositeOperation='source-over'context.fillRect(0,0,ctxWidth,ctxHeight)//画框context.globalCompositeOperation='destination-out'context.fillRect(x,y,w,h)//描边context.globalCompositeOperation='source-over'context.moveTo(x,y)context.lineTo(x+w,y)context.lineTo(x+w,y+h)context.lineTo(x,y+h)context.lineTo(x,y)//context.stroke()context.closePath()}return{init,cut,cancelCut,clipImgData}}2. html 部分importReact,{ReactElement,useEffect,useRef,useState}from'react'import'./index.less'exportdefault()=>{constclipAreaWrap=useRef(null)//截图区域domconst{init,cut,cancelCut,clipImgData}=clip()return({init(clipAreaWrap.current)cut('https://cdn-tos.baohuaxia.com/obj/static-assets/433ed21f7f4a27a5bde94a8119d618c5.png')}}>截图{cancelCut()}}>取消)}3.CSS.clip-area-wrap{height:450px;position:relative;//图片居中显示img{width:100%;display:block;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);max-width:100%;max-height:100%;}}//回显区域.clip-img-area{width:250px;height:250px;position:relative;margin:0auto;//图片居中显示img{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);max-width:100%;max-height:100%;}} 后续想法后续还想实现一些功能,比如:自动把截图放入剪切板根据需要生成并返回不同格式的截图图片过大时,进行图片压缩(canvas.toDataURL 可以实现)……文章会持续更新,敬请关注参考canvas 实现截图功能——截取图片的一部分[1]加入我们财经前端团队是一支基础技术与业务支持并重的团队,既有扎实过硬的技术底盘,又服务于公司多个核心业务:抖音支付,金融,保险,证券等。目前财经前端团队大部队集中在北京,深圳、杭州也建立有研发中心,各地业务都在蓬勃发展中,团队氛围开放活泼,热切期待志同道合的朋友加入我们!参考资料[1]canvas 实现截图功能——截取图片的一部分: https://blog.csdn.net/HuangsTing/article/details/106141263
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-14 20:47 , Processed in 0.489198 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表