百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

前端:从零实现一款可视化图片编辑器

zhezhongyun 2025-03-05 23:53 41 浏览

背景介绍

我们知道,为了提高企业研发效能和对客户需求的快速响应,现在很多企业都在着手数字化转型,不仅仅是大厂(阿里,字节,腾讯,百度)在做低代码可视化这一块,很多中小企业也在做,拥有可视化低代码相关技术背景的程序员也越来受重视。

我最近一直在做数据可视化和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 } ```

这样我们就创建好了一个画布,并在画布中插入了一段可编辑可拖拽的文本,如下:

图形库设计

作为一款图片编辑器,为了提高使用的灵活性我们还需要提供一些基础图形方便我们设计图片,所以我在编辑器里添加了图形库:

主要有如文本图片直线矩形圆形三角形箭头马赛克,当然大家可以根据自己的需求添加更多的基本图元。我们在图片库中点击任意一个元素即可将其插入画布,这块是利用 fabricadd 方法,当然 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();
}

属性编辑器的样式实现这里我就不一一介绍了,都比较基础,我们来看一下编辑项的基本结构:

描边宽度: 
 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
});

这样我们就实现了自定义元素控制,我们也可以按照类似的方法实现自定义的控件。效果如下:

预览功能实现

预览功能我主要是利用原生 canvastoDataURL 方法来生成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()
  }))
}

主要是用的windowURL 对象的 createObjectURLrevokeObjectURL 方法,两年前我也在我的文章中分享过对应的实现,感兴趣的可以参考一下。下载的效果如下:

模版保存实现

在设计图片编辑器的过程中我们也要考虑保存用户的资产,比如做的比较好的图片可以保存为模版,以便下次复用,所以我在编辑器里还实现的简单的模版保存和使用的功能。我们先看一下效果:

我们在演示中可以看到保存为模版之后会自动同步到左侧的模版列表中,我们下次创作时可以直接导入模版进行二次创作。以下是实现的逻辑图:

由上图可以发现我们保存模版不仅仅是保存图片,还需要保存图片对应的 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 | 轻量级且可扩展的图片/图形编辑器解决方案
作者:徐小夕
专栏:低代码可视化

相关推荐

Win10全新版本速览 全新图标设计焕然一新

来源:太平洋电脑网[PConline资讯]原定于今年下半年发布的Win1021H2(SunValley),再有几个月就要与我们见面了。之前我们已经陆续介绍过新版在开始菜单、窗口动效、通知中心等方...

第四周B组最佳选手Icon:陨落天团的小将,还是重生星球的偶像?

B组本来被认为是死亡之组,但在上周的异组对抗赛中,实力较强的几只队伍却先后被A组击沉,反而是之前一分难求的OMG2:1战胜了IG,其中Icon功不可没,两次秀翻全场的妖姬和与Juejue中野联动的实力...

系统小技巧:不花一分钱 让声音再大些

有时,笔记本扬声器的音量可能无法满足我们对声音播放的需求。点击系统托盘的小喇叭图标,音量调节滑动条调整到最大也无效,而添置大功率扬声器又不是我们所愿。这时,可通过调整系统本身的设置或利用第三方软件来解...

DNF手游:70级毕业搭配曝光!需集齐四类装备,别指望无形升级了

随着dnf手游7.16版本的临近,各种70级装备的消息满天飞,真真假假难以辨别,但只要以“发布会爆料”为核心,主播十四的爆料为辅,就能确定70版本的毕业标准搭配,查理策划是打算“集百家之长”于一身,完...

学会这5个电脑设置 可解决99%的故障

现阶段复工大家都是宅家办公吧,所以电脑可谓是大救星,因为无论是“停课不停学”的网课,还是在家办公的穷社畜,都离不开电脑。所以今天小编就来教大家几招电脑的自救方法,让你可以在电脑出问题时从容面对,足不出...

LOL:上单腕豪的取胜之道——掌控自身优势,融合装备特效

01前言腕豪这个英雄,在11.9版本之前都没有太多的出场机会,即使出现也大多是在辅助位置上,充当开团、先手控制的角色。但是在11.9版本以后,这个英雄突然之间仿佛飞升了一般,直接冲上T1级别上单,之前...

英雄联盟手游:一篇文章读懂所有龙buff属性效果,还不赶紧收藏

英雄联盟手游采用也是经典峡谷地图,看起来和端游并没有太大差异,但实际上为了适应手游节奏,官方也是进行了适当调整,所以包括红蓝buff,大龙小龙buff效果会有一定的差异,现在就来看看小龙和大龙具体属性...

双击打不开怎么办?双击文件夹显示属性的解决办法

今天小编在双击文件夹的时候,就是打不开,还弹出“属性”对话框,不知道大家有没有出现过,为避免出现这种情况时大家束手无策,今天小编就来为大家分享一下解决方法。1.检查键盘,看看“Alt”键是否卡住。由于...

柜子布局好超省空间,1㎡当10㎡用,收纳涨5倍!户型图直接抄

柜子不嫌多,就怕你家放不下!▼每个空间几乎都需要柜子客厅要有电视柜,卧室要能放下全家四季的衣柜,厨房还需要大容量的橱柜空间就这么小,想要榨出放柜子的地方,还要活得不拥挤简直太难了!其实全是你找错地方打...

制作幻灯片的另类方法(然后制作幻灯片)

如果提到制作幻灯片,很多人都会想到PowerPoint。PowerPoint提供了很多模板,但这些模板实用性并不太高;PowerPoint的交互还算比较方便,但不可否认,交互显得有些呆板,而且要想修改...

《心灵杀手重制版》画面晃动怎么办?画面晃动解决办法

针对《心灵杀手重制版》画面晃动的问题,以下是一些解决办法:修改游戏启动路径:打开游戏所在的文件夹,找到游戏图标的属性并打开。在属性中修改启动路径,加入“-noblur”参数。例如,如果游戏安装在“X:...

win10网络图标是小地球怎么办(excel表格怎样选定区域打印)

win10系统右下角的网络图标突然变成地球图标,而且无法上网,提示无法连接到Internet。出现这个问题多数是因为win10正式版更新了或者是网络中断后遗症。那么win10网络图标变成地球怎么办呢?...

重看电脑任务栏的使用方法:windows 10小贴士

【环球科技综合报道】据日本Livedoor新闻网2月18日报道,windows电脑的屏幕下方配置的任务栏由于总是在电脑桌面上显示,可以说是一个映入眼帘的机会格外多的区域。像是把经常使用的软件图标设置在...

《地狱之门》攻略:四大元素属性介绍

《地狱之门》是近期比较看好的一款卡牌手游,绚丽的3D游戏画面以及创新的即时战斗系统给人耳目一新的感觉,非常值得一试。玩家不仅可以体验到卡牌收集的乐趣,更主要的是能体验到酣畅淋漓的战斗体验。下面小编就来...

一脸懵?DNF希洛克词条赋予属性触发条件解析

DNF希洛克已经开放两天,但是还是看到不少小伙伴对于希洛克词条赋予系统充满疑惑。今天就来简单易懂的给大家科普一下希洛克词条赋予系统里的弯弯绕绕。◎希洛克装备融合在希洛克获取了专属史诗之后,即可通过歌兰...