当前位置: 首页 > 科技观察

从零开始开发图片编辑器Mitu-Dooring_0

时间:2023-03-17 00:33:35 科技观察

背景介绍我们知道,为了提高企业研发效率,快速响应客户需求,现在很多企业都在进行数字化转型,不仅仅是大厂(阿里、wordFestival、腾讯、百度)都在做低代码可视化,很多中小企业也在做。具有视觉低代码相关技术背景的程序员也越来越受到关注。最近一直在做数据可视化和lowcode/nocode相关的项目。结合自己的工作经验和对lowcode/nocode的探索,我也写了一系列低代码可视化构建文章。今天继续分享可视化相关的内容——可视化图像编辑器。在分享的过程中,我会以我最近写的一个开源项目米兔为例,仔细拆解它的实现过程。米兔主要用于辅助H5编辑器H5-Dooring进行图片处理。您还可以轻松地基于它进行二次开发和扩展,成为功能更强大的图片编辑器。接下来,我将介绍和分析这款开源照片编辑器Mitu。项目介绍以上是图片编辑器的部分演示效果。我们可以通过拖拽重组的方式快速生成我们想要的图片,也可以将图片保存为模板,方便以后重复使用。在项目开发之前,我也设计了一个简单的原型,保证自己的开发方向不会偏离。高效阅读学习:可视化编辑器工程搭建与技术选型图库设计属性编辑器设计自定义图元控制器实现预览功能实现保存图片功能实现模板保存实现导入模板功能实现可视化图片编辑器后期策划现在废话不多说,下面我们就来了解一下。开始我们的技术实现。技术实现项目建设和技术选型小编的实现思路与技术栈无关。这里我使用React来实现。当然,如果你更喜欢Vue或者sveltejs,也没问题。项目整体技术选型如下:umi可扩展的企业级前端应用框架React+Typescriptantd前端组件库fabric一个可以简化Canvas编程的库localStorage本地数据存储当然还有很多细节以及项目实施过程中的思路,接下来会一一介绍。如果你对fabric库还不熟悉,别着急,我会通过具体功能的实现来给大家介绍一下这个库。在介绍下面的内容之前,我们先来安装fabric并初始化一个canvas。yarnaddfabric初始化画布:import{fabric}from"fabric";从'nanoid'导入{nanoid};从“反应”导入{useEffect,useState,useRef};导出默认函数IndexPage(){constcanvasRef=useRef(null);useEffect(()=>{canvasRef.current=newfabric.Canvas('canvas');//创建一个文本元素constshape=newfabric.IText(nanoid(8),{text:'H5-Dooring',width:60,height:60,fill:'#06c',left:30,top:30})//将文本元素插入画布canvasRef.current.add(shape);//设置画布的背景颜色canvasRef.current.backgroundColor='rgba(255,255,255,1)';})return}这样我们就创建了一个Canvas,插入一段可编辑和画布中可拖动文字,如下:图库设计作为图片编辑,为了提高使用的灵活性,我们还需要提供一些基本的图形供我们设计图片,所以我在图库中添加了编辑器:主要是文字、图片、直线、矩形、圆形、三角形、箭头、马赛克,当然也可以根据需要添加更多的基本图元。我们可以单击图片库中的任何元素将其插入到画布中。这是使用fabric的add方法。当然,fabric也内置了很多基础图形,我们可以在文档中参考。为了让图形插入更加封装,我定义了图形的基本schema结构:constbaseShapeConfig={IText:{text:'H5-Dooring',width:60,height:60,fill:'#06c'},三角形:{宽度:100,高度:100,填充:'#06c'},圆形:{半径:50,填充:'#06c'},矩形:{宽度:60,高度:60,填充:'#06c'},Line:{width:100,height:1,fill:'#06c'},Arrow:{},Image:{},Mask:{}}这样插入图形的方法就可以写了如下:typeElementType='IText'|'三角形'|'圆圈'|'矩形'|'线'|'图像'|'箭头'|'Mask'constinsertShape=(type:ElementType)=>{shape=newfabric[type]({...baseShapeConfig[type],left:size[0]/3,top:size[1]/3})canvasRef.current.add(shape);}后面我们添加图形的时候只需要定义schema即可,但是需要注意的是fabric创建图形的方式并不都是统一的。我们需要对具体图片的创建进行特殊判断,比如直线路径:if(type==='Line'){shape=newfabric.Path('M00L1000',{stroke:'#ccc',strokeWidth:2,objectCaching:false,left:size[0]/3,top:size[1]/3})}当然我们也可以使用switch对不同的情况进行不同的处理,这样我们就实现了一个基本的图片库属性编辑器。设计属性编辑器主要用于配置图形属性,如填充颜色、描边颜色、描边宽度。目前我主要定义这三个维度。也可以在此基础上继续扩展更多可编辑的属性,类似H5-Dooring组件属性配置面板。我们可以在编辑器右侧的属性编辑区控制图形的属性。由于只有3个属性,我只是对它们进行了硬编码。您也可以使用动态渲染来实现它。需要注意的是,我们怎么知道我们选择了哪个组件呢?幸运的是,fabric提供了一系列的API来帮助我们更好的控制元素对象。这里我们使用getActiveObject方法获取当前选中的元素。具体实现代码如下://...//定义基本属性const[attrs,setAttrs]=useState({fill:'#0066cc',stroke:'',strokeWidth:0,})//更新选中elementsconstupdateAttr=(type:'fill'|'stroke'|'strokeWidth'|'imgUrl',val:string|number)=>{setAttrs({...attrs,[type]:val})//获取当前选中的元素对象constobj=canvasRef.current.getActiveObject()//设置元素属性obj.set({...attrs})//重新渲染canvasRef.current.renderAll();}我就不介绍了此处属性编辑器的样式实现。它们都比较基础,我们来看看编辑项的基本结构:strokewidth:updateAttr('strokeWidth',v)}/>自定义图元控制器实现因为fabric默认不提供删除按钮和逻辑,所以我们需要扩展两次,正好由fabric提供,我们定义了自定义扩展的方法,接下来我们一起自定义一个删除按钮,实现删除逻辑。具体实现代码如下://删除按钮constdeleteIcon="data:image/svg+xml,%3C%3Fxmlversion='1.0'encoding='utf-8'%3F%3E%3C!DOCTYPEsvgPUBLIC'-//W3C//DTDSVG1.1//EN''http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3E%3Csvgversion='1.1'id='Ebene_1'xmlns='http://www.w3.org/2000/svg'xmlns:xlink='http://www.w3.org/1999/xlink'x='0px'y='0px'width='595.275px'height='595.275px'viewBox='200215230470'xml:space='preserve'%3E%3Ccirclestyle='fill:%23F44336;'cx='299.76'cy='439.067'r='218.516'/%3E%3Cg%3E%3Crectx='267.162'y='307.978'transform='matrix(0.7071-0.70710.70710.7071-222.6202340.6915)'样式='填充:白色;'width='65.545'2.188'2'/%3E%3Crectx='266.988'y='308.153'transform='matrix(0.70710.7071-0.70710.7071398.3889-83.3116)'style='fill:white;'width='65.544'height='262.179'%3E%3C/g%3E%3C/svg%3E";//删除方法functiondeleteObject(eventData,transform){consttarget=transform.target;constcanvas=target.canvas;画布.re移动(目标);canvas.requestRenderAll();}//渲染iconfunctionrenderIcon(ctx,left,top,styleOverride,fabricObject){constsize=this.cornerSize;ctx.save();ctx.translate(左,上);ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));ctx.drawImage(img,-size/2,-size/2,大小,大小);ctx.restore();}//全局增删Buttonfabric.Object.prototype.controls.deleteControl=newfabric.Control({x:0.5,y:-0.5,offsetY:-32,//自定义偏移距离从元素中,您还可以定义offsetXcursorStyle:'pointer',mouseUpHandler:deleteObject,render:renderIcon,cornerSize:24});这样我们就实现了自定义元素的控件,我们也可以用类似的方式实现自定义控件。效果如下:预览功能实现预览功能,我主要是使用原生canvas的toDataURL方法生成base64数据,然后赋值给img标签。还有一个需要注意的细节就是如果我们在预览之前画布上还有选中的元素,控制点也会被截取,如下:这对用户体验非常不好,我们需要看一个纯粹的我的解决方法是取消选中预览前画布所有元素的状态。您可以使用fabric实例的discardActiveObject()方法来停用状态,然后更新画布。具体实现逻辑如下://1.取消画布所有元素选中状态canvasRef.current.discardActiveObject()canvasRef.current.renderAll();//2.将当前画布转换为base64地址imageconstimg=document.getElementById("画布");constsrc=(imgasHTMLCanvasElement).toDataURL("image/png");//3.设置元素url,显示预览弹窗setImgUrl(src)setIsShow(true)预览效果展示:保存图片的功能为其实和预览功能很像,唯一不同的是我们需要把图片下载到本地,所以我主要用纯前端的方式来实现图片下载。也可以使用自己熟悉的前端下载方案。接下来,我将发布我的方案实现:a=document.createElement('a');leturl=window.URL.createObjectURL(blob);a.href=url;a.download=filename;a.click();window.URL.revokeObjectURL(url);cb&&cb()}))}主要是用于windowURL对象的createObjectURL和revokeObjectURL方法,我在两年前的文章中也分享过相应的实现,有兴趣的可以参考。下载后的效果如下:模板保存实现在图片编辑器的设计过程中,我们还需要考虑保存用户的资源。比如比较好的图片可以保存为模板,下次再用,所以我还是实现了简单的模板保存和使用功能。先来看一下效果:我们在demo中可以看到,保存为模板后,会自动同步到左侧的模板列表中,下次创建时可以直接导入模板进行二次创建.下面是实现逻辑图:从上图我们可以发现,我们保存模板不仅仅是为了保存图片,还需要保存图片对应的jsonschema数据。之所以保存jsonschema,是为了保证用户切换到对应模板时的模板。每一个元素都可以还原,类似于我们最熟悉的PSD源文件。Fabric提供了方法toDatalessJSON()来序列化画布。在保存模板的时候,我们只需要将序列化后的json和图片一起保存即可。这样便于加工。我暂时存放在localStorage中,你也可以使用大容量的本地存储方案indexedDB,我之前也封装了基于indexedDB的开箱即用的缓存库xdb,你可以直接使用。xdb|基于promise封装并支持过期时间的开箱即用的indexedDB缓存库保存模板具体实现如下:consthandleSaveTpl=()=>{constval=tplNameRef.current.state.valueconstjson=canvasRef.current。toDatalessJSON()constid=nanoid(8)//保存jsonconsttpls=JSON.parse(localStorage.getItem('tpls')||"{}")tpls[id]={json,t:val};localStorage.setItem('tpls',JSON.stringify(tpls))//保存图像'tplImgs')||"{}")tplImgs[id]=imgUrllocalStorage.setItem('tplImgs',JSON.stringify(tplImgs))//更新模板列表setTpls((prev:any)=>[...prev,{id,t:val}])setIsTplShow(false)}导入模板函数实现了导入模板反序列化JsonSchema的本质。在研究fabric的过程中,我们发现它可以直接加载json来渲染图形序列,所以我们可以直接将上面保存的json加载到canvas中://1.加载前清空canvascanvasRef.current.clear();//2.重置画布背景色canvasRef.current.backgroundColor='rgba(255,255,255,1)';//3.渲染jsoncanvasRef.current.loadFromJSON(tpls[id].json,canvasRef.current.renderAll.bind(canvasRef.current))然后我们可以根据保存的模板列表动态切换模板: