前端:从零实现一款可视化图片编辑器
myzbx 2025-01-11 15:34 18 浏览
背景介绍
我们知道,为了提高企业研发效能和对客户需求的快速响应,现在很多企业都在着手数字化转型,不仅仅是大厂(阿里,字节,腾讯,百度)在做低代码可视化这一块,很多中小企业也在做,拥有可视化低代码相关技术背景的程序员也越来受重视。
我最近一直在做数据可视化和lowcode/nocode相关的项目,针对我自己的工作经验和对lowcode/nocode的探索,也写了一系列低代码可视化搭建系列文章,今天我们继续来分享可视化相关的内容——可视化图片编辑器。
在分享过程中,我会以最近我写开源的一个项目Mitu为案例,仔细拆解它的实现过程。Mitu主要是辅助H5编辑器 H5-Dooring 做图像处理用的,大家也可以轻松基于它进行二次开发和扩展,变成更强大的图片编辑器。
在文章末尾我会附上 github 地址 和 demo 地址,方便大家学习和体验。接下来我就来带大家介绍和剖析一下这款开源图片编辑器 Mitu。
项目介绍
以上是图片编辑器的部分演示效果,我们可以通过拖拽重组的方式快速生成我们想要的图片,也能将图片保存为模版,以便后期复用。在项目开发之前我也设计了一个简单的原型,保证自己的开发方向不会跑偏,大家可以参考一下:
按照我一向的写作风格,我先列一下技术实现的大纲,以便大家有选择且高效率的阅读和学习:
- 可视化编辑器项目搭建和技术选型
- 图形库设计
- 属性编辑器设计
- 自定义图元控制器实现
- 预览功能实现
- 保存图片功能实现
- 模版保存实现
- 导入模版功能实现
- 可视化图片编辑器后期规划
好了,话不多说,接下来开始我们的技术实现。
技术实现
项目搭建和技术选型
编辑器的实现思路和技术栈无关,这里我采用了 React 来实现,当然大家如果更喜欢 Vue 或者 sveltejs,也是没问题的,项目整体技术选型如下:
- umi 可扩展的企业级前端应用框架
- React + Typescript
- Antd 前端组件库
- fabric 一个可以简化 Canvas 程序编写的库
- localStorage 本地数据存储
当然在项目的实现过程中还有很多细节和思想,接下来我会一一和大家介绍。如果大家对 fabric 这个库不太熟悉也不用担心,我会通过具体功能的实现来带大家熟悉这个库。
在介绍下面的内容之前我们先安装一下 fabric ,然后初始化一个画布。
yarn add fabric
初始化一个画布:
``` js import { fabric } from "fabric"; import { nanoid } from 'nanoid'; import { useEffect, useState, useRef } from 'react';
export default function IndexPage() { const canvasRef = useRef(null); useEffect(() => { canvasRef.current = new fabric.Canvas('canvas'); // 创建一个文本元素 const shape = new fabric.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 } ```
这样我们就创建好了一个画布,并在画布中插入了一段可编辑可拖拽的文本,如下:
图形库设计
作为一款图片编辑器,为了提高使用的灵活性我们还需要提供一些基础图形方便我们设计图片,所以我在编辑器里添加了图形库:
主要有如文本,图片,直线,矩形,圆形,三角形,箭头,马赛克,当然大家可以根据自己的需求添加更多的基本图元。我们在图片库中点击任意一个元素即可将其插入画布,这块是利用 fabric 的 add 方法,当然 fabric 也内制了很多基本图形,我们可以在文档中参考一下。为了让图形插入更有封装性,我定义了图形的基本 schema 结构:
const baseShapeConfig = {
IText: {
text: 'H5-Dooring',
width : 60,
height : 60,
fill : '#06c'
},
Triangle: {
width: 100,
height: 100,
fill: '#06c'
},
Circle: {
radius: 50,
fill: '#06c'
},
Rect: {
width : 60,
height : 60,
fill : '#06c'
},
Line: {
width: 100,
height: 1,
fill: '#06c'
},
Arrow: {},
Image: {},
Mask: {}
}
这样我们插入图形的方法就可以这样写:
type ElementType = 'IText' | 'Triangle' | 'Circle' | 'Rect' | 'Line' | 'Image' | 'Arrow' | 'Mask'
const insertShape = (type:ElementType) => {
shape = new fabric[type]({
...baseShapeConfig[type],
left: size[0] / 3,
top: size[1] / 3
})
canvasRef.current.add(shape);
}
后续我们添加图形时只需要定义 schema 即可,但是需要注意的是 fabric 创建图形的方式并不都都是统一的,我们需要对特定图片的创建进行特殊判断,比如直线路径:
if(type === 'Line') {
shape = new fabric.Path('M 0 0 L 100 0', {
stroke: '#ccc',
strokeWidth: 2,
objectCaching: false,
left: size[0] / 3,
top: size[1] / 3
})
}
当然我们也可以用 switch 来对不同情况进行不同处理,这样我们就实现了一个基本图片库。
属性编辑器设计
属性编辑器主要是用来对图形属性进行配置的,比如填充颜色,描边颜色,描边宽度,目前我主要定义了这3个维度,大家也可以基于此继续扩展更多的可编辑属性,类似于 H5-Dooring 的组件属性配置面板。
我们可以在编辑器右侧的属性编辑区控制图形的属性,因为属性目前只有3个,我就直接硬编码写上去了,大家也可以用动态渲染的方式来实现。需要注意的是我们怎么知道我们选中的是那个组件呢? 好在 fabric 提供了一系列 api 帮助我们更好的控制元素对象,这里我们用 getActiveObject 方法拿到当前选中的元素,具体实现代码如下:
// ...
// 定义基础属性
const [attrs, setAttrs] = useState({
fill: '#0066cc',
stroke: '',
strokeWidth: 0,
})
// 更新选中的元素
const updateAttr = (type: 'fill' | 'stroke' | 'strokeWidth' | 'imgUrl', val:string | number) => {
setAttrs({...attrs, [type]: val})
// 获取当前选中元素对象
const obj = canvasRef.current.getActiveObject()
// 设置元素属性
obj.set({...attrs})
// 重新渲染
canvasRef.current.renderAll();
}
属性编辑器的样式实现这里我就不一一介绍了,都比较基础,我们来看一下编辑项的基本结构:
<span className={styles.label}>描边宽度: </span>
<InputNumber size="small" min={0} value={attrs.strokeWidth} onChange={(v) => updateAttr('strokeWidth', v)} />
自定义图元控制器实现
因为默认情况下 fabric 没有提供删除按钮和逻辑,所以我们需要自己二次扩展,恰好 fabric 提供了自定义扩展的方法,接下来我们就一起自定义一个删除按钮并实现删除逻辑。
具体实现代码如下:
// 删除按钮
const deleteIcon = "data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3C!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3E%3Csvg version='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='200 215 230 470' xml:space='preserve'%3E%3Ccircle style='fill:%23F44336;' cx='299.76' cy='439.067' r='218.516'/%3E%3Cg%3E%3Crect x='267.162' y='307.978' transform='matrix(0.7071 -0.7071 0.7071 0.7071 -222.6202 340.6915)' style='fill:white;' width='65.545' height='262.18'/%3E%3Crect x='266.988' y='308.153' transform='matrix(0.7071 0.7071 -0.7071 0.7071 398.3889 -83.3116)' style='fill:white;' width='65.544' height='262.179'/%3E%3C/g%3E%3C/svg%3E";
// 删除方法
function deleteObject(eventData, transform) {
const target = transform.target;
const canvas = target.canvas;
canvas.remove(target);
canvas.requestRenderAll();
}
// 渲染icon
function renderIcon(ctx, left, top, styleOverride, fabricObject) {
const size = this.cornerSize;
ctx.save();
ctx.translate(left, top);
ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
ctx.drawImage(img, -size/2, -size/2, size, size);
ctx.restore();
}
// 全局添加删除按钮
fabric.Object.prototype.controls.deleteControl = new fabric.Control({
x: 0.5,
y: -0.5,
offsetY: -32, // 自定义距元素的偏移距离, 也可以定义offsetX
cursorStyle: 'pointer',
mouseUpHandler: deleteObject,
render: renderIcon,
cornerSize: 24
});
这样我们就实现了自定义元素控制,我们也可以按照类似的方法实现自定义的控件。效果如下:
预览功能实现
预览功能我主要是利用原生 canvas 的 toDataURL 方法来生成base64的数据,然后赋值给 img 标签。还有一个细节需要注意的是如果我们在预览之前画布仍然有选中状态的元素,那么控制点也会被截取出来,如下:
这样对用户体验非常不好,我们需要在预览时看到一张纯粹的图片,我的方案是在预览前取消画布所有元素的选中状态,可以用 fabric 实例的 discardActiveObject() 方法取消激活状态,然后更新画布即可,具体实现逻辑如下:
// 1. 取消画布所有元素的选中状态
canvasRef.current.discardActiveObject()
canvasRef.current.renderAll();
// 2. 将当前画布转化为图片的base64地址
const img = document.getElementById("canvas");
const src = (img as HTMLCanvasElement).toDataURL("image/png");
// 3. 设置元素url,显示预览弹窗
setImgUrl(src)
setIsShow(true)
预览效果展示:
保存图片功能实现
保存图片其实和预览功能很像,唯一不同的是我们需要把图片下载到本地,那么我主要是用纯前端的方式实现图片下载,大家也可以用自己熟悉的前端下载方案,接下来贴一下我的方案实现:
function download(url:string, filename:string, cb?:Function) {
return fetch(url).then(res => res.blob().then(blob => {
let a = document.createElement('a');
let url = window.URL.createObjectURL(blob);
a.href = url;
a.download = filename;
a.click();
window.URL.revokeObjectURL(url);
cb && cb()
}))
}
主要是用的window 的 URL 对象的 createObjectURL 和 revokeObjectURL 方法,两年前我也在我的文章中分享过对应的实现,感兴趣的可以参考一下。下载的效果如下:
模版保存实现
在设计图片编辑器的过程中我们也要考虑保存用户的资产,比如做的比较好的图片可以保存为模版,以便下次复用,所以我在编辑器里还实现的简单的模版保存和使用的功能。我们先看一下效果:
我们在演示中可以看到保存为模版之后会自动同步到左侧的模版列表中,我们下次创作时可以直接导入模版进行二次创作。以下是实现的逻辑图:
由上图可以发现我们保存模版不仅仅是保存图片,还需要保存图片对应的 json schema 数据,之所以要保存 json schema 是为了当用户切换到对应的模版之后可以保证模版的每个元素都可以还原,类似于我们最熟悉的 PSD 源文件。fabric 提供了序列化画布的方法 toDatalessJSON(),我们在保存模版的时候只要把序列化后的 json 和图片一起保存即可,这里方便处理我暂时存在 localStorage 中,大家也可以使用大容量本地化存储方案 indexedDB,我之前也基于 indexedDB 封装了开箱即用的缓存库 xdb,大家可以直接拿来使用。
- xdb | 基于promise封装且支持过期时间的开箱即用的indexedDB缓存库
保存模版的具体实现如下:
const handleSaveTpl = () => {
const val = tplNameRef.current.state.value
const json = canvasRef.current.toDatalessJSON()
const id = nanoid(8)
// 存json
const tpls = JSON.parse(localStorage.getItem('tpls') || "{}")
tpls[id] = {json, t: val};
localStorage.setItem('tpls', JSON.stringify(tpls))
// 存图片
canvasRef.current.discardActiveObject()
canvasRef.current.renderAll()
const imgUrl = getImgUrl()
const tplImgs = JSON.parse(localStorage.getItem('tplImgs') || "{}")
tplImgs[id] = imgUrl
localStorage.setItem('tplImgs', JSON.stringify(tplImgs))
// 更新模版列表
setTpls((prev:any) => [...prev, {id, t: val}])
setIsTplShow(false)
}
导入模版功能实现
导入模版的本质是反序列化 Json Schema,在研究 fabric 的过程中发现了其可以直接加载 json 渲染图形序列,所以我们可以直接将上文保存的 json 直接加载到画布:
// 1.加载前清空画布
canvasRef.current.clear();
// 2.重置画布背景色
canvasRef.current.backgroundColor = 'rgba(255,255,255,1)';
// 3. 渲染json
canvasRef.current.loadFromJSON(tpls[id].json, canvasRef.current.renderAll.bind(canvasRef.current))
然后我们就可以根据保存的模版列表,动态切换模版了:
后期规划
这款图片编辑器我已经在 github 开源了,大家可以基于次开发更强大的图片编辑器,对于图片编辑器的后期规划,我也评估了几个可行的方向,如果大家感兴趣也可以联系我参与到项目中来。
后期规划如下:
- [x] 撤销重做
- [x] 画布背景设置
- [x] 丰富图形组件库
- [x] 图片滤镜配置
- [x] 模块化界面
- [x] 解析PSD
如果大家对可视化搭建或者低代码/零代码感兴趣,也可以参考我往期的文章或者在评论区交流你的想法和心得,欢迎一起探索前端真正的技术。
github: mitu-editor | 轻量级且可扩展的图片/图形编辑器解决方案
作者:徐小夕
专栏:低代码可视化
相关推荐
- STM学习笔记--STM32F10X时钟
-
一:系统(SYSCLK)时钟3种(注:时钟频率较高)本文引用地址:http://www.eepw.com.cn/article/201609/296750.htmHSI振荡器时钟8MHZHSE振荡...
- 澜起科技领先启动DDR5时钟驱动芯片试产,助力提升数据速度与稳定性
-
添加我为微信好友<<<点击左侧,每日精选三只热门板块金股免费领。【澜起科技开启DDR5时钟驱动器芯片试产】澜起科技已启动DDR5第一子代时钟驱动器芯片的生产试验。该产品将用于新...
- STM32F030 Nucleo-让MCU全速点灯,其中隐藏的含义(一)
-
只要是有点基础的骚友,点个灯就是个小KS!但是,我却发现一些猫腻!!!先看程序:从牛卡板卡的硬件原理图得知LD1为ST-Link的RGB指示灯,LD3为牛客板卡的电源指示灯,那么只剩下LD2了,LD2...
- 西门子-CPU模块的参数设定
-
右键单击CPU模块所在的行,并选中“对象属性(ObjectProperties…)”选项,可以打开CPU的参数设定页面。通过设定页面不同的标签,可以打开不同的参数设定对象。1.基本参数(Genera...
- 海湾GST5000主机报时钟电源故障
-
主板时钟电源主要记录系统的时间,也就是维持系统时钟的准确性。还有记录启动时要用的硬件信息,也就是BⅠOS信息。有好多厂家工控板有独立纽扣电池,如下图一般工控板电池没电,主机开机后系统时间不是准确的时间...
- S7-1200 CPU 时钟与 CP 时钟的同步
-
CPU同步CP时钟按如下步骤组态实现CPU时钟同步CP时钟:1.在CPU属性中激活“通过NTP服务器启动同步时间”,同时激活“CPU与该设备中的模块进行同步”,即实现CPU...
- 开机需先按F1,开机后时间不准?手把手教你更换纽扣电池解决
-
台式机开机必按F1?手把手教你更换CR2032纽扣电池,彻底解决BIOS重置问题台式机主板上的CR2032纽扣电池(CMOS电池)是电脑的“记忆电源”,其核心作用是为BIOS芯片持续供电,确保在完全断...
- 空气钟摆着不动会自己上链,这种神秘装置是如何实现走时的?
-
我们都知道,积家在钟表领域发明了两个伟大的时计,一个是积家表的翻转表(Reverso),另一个则是积家的空气钟(Atmos),tmos空气钟在其发明近一个世纪后仍然是一项独特而有趣的发明,这种钟是在空...
- 2013款新帕萨特一键启动不能正常启动一步搞定
-
2013年购买2013款新帕萨特2.0御尊,一直使用正常,除了正常保养外,2019年更换过火花塞,别的都没更换。2022年2月10日启动时可点火,不能正常启动发动机。还提示驻车制动器故障等,长时间按压...
- 老板喊你设计一个高效的定时任务系统
-
【51CTO.com原创稿件】今天想跟大家一起探讨一个听起来很简单的话题:定时任务机制。无非就是一个计时器,到了指定时间就开始跑呗。tooyoung,要是这么简单我还说啥呢,干不就完了。那如果是几千...
- 三星S6 edge+不一样 双曲面屏新玩儿法
-
三星GalaxyS6edge+最为耀眼的特点就是它的双曲面侧屏,这是对此前三星S6edge的又一次升级。当然,这一特色为你带来的不仅是看起来爽而已,其实它用起来更爽。今天就为大家详细介绍一下三星...
- 深度分析:雄鹿队季后赛首轮被淘汰后,字母哥交易时钟正式启动
-
欢迎关注、点赞、评论、收藏、转发!##【字母哥踩到香蕉皮?再见印第安纳噩梦夜】周二那场加时赛宰雄鹿绝不手软,步行者最后四十秒轰出8-0冲击波。119-118比分活活憋死雄鹿最后一口气——又是五场解决...
- 用爆火的DeepSeek做了个桌面时钟
-
最近这DeepSeek是相当火爆啊。大家都在各种宣传它有多厉害,我也很好奇,就学者体验一下。我是个完全不会写代码的小白,没学过没用过,然后看别人都在用AI来替自己完成编程,很简单的样子。先打开浏览器。...
- 西门子1200PLC基础篇——定时器的具体应用
-
西门子1200PLC基础篇——定时器的具体应用上一篇我们一起学习了定时器的操作。今天我们用一个案例一起来学习定时器的具体应用。案例要求:Q0.0端停三秒,运行五秒,再停三秒,再运行五秒,依次循环闪烁。...
- 效率工具:推荐一款极简桌面待办工具
-
对于职场办公人员来说,每天有各种各样的事项需要处理,如何高效的管理待办事项非常关键。这个时候有一款好用的待办管理软件就可以帮助大家解决这个问题。今天给大家推荐Kite待办这款极简的桌面待办工具,希望...
- 一周热门
- 最近发表
- 标签列表
-
- HTML 简介 (30)
- HTML 响应式设计 (31)
- HTML URL 编码 (32)
- HTML Web 服务器 (31)
- HTML 表单属性 (32)
- HTML 音频 (31)
- HTML5 支持 (33)
- HTML API (36)
- HTML 总结 (32)
- HTML 全局属性 (32)
- HTML 事件 (31)
- HTML 画布 (32)
- HTTP 方法 (30)
- 键盘快捷键 (30)
- CSS 语法 (35)
- CSS 选择器 (30)
- CSS 轮廓 (30)
- CSS 轮廓宽度 (31)
- CSS 谷歌字体 (33)
- CSS 链接 (31)
- CSS 定位 (31)
- CSS 图片库 (32)
- CSS 图像精灵 (31)
- SVG 文本 (32)
- 时钟启动 (33)