前端:從零實現一款視覺化圖片編輯器

背景介紹

我們知道,為了提高企業研發效能和對客戶需求的快速響應,現在很多企業都在著手數字化轉型,不僅僅是大廠(阿里,位元組,騰訊,百度)在做低程式碼視覺化這一塊,很多中小企業也在做,擁有視覺化低程式碼相關技術背景的程式設計師也越來受重視。

我最近一直在做資料視覺化和

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();}

屬性編輯器的樣式實現這裡我就不一一介紹了,都比較基礎,我們來看一下編輯項的基本結構:

描邊寬度: 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();}// 渲染iconfunction 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。 渲染jsoncanvasRef。current。loadFromJSON(tpls[id]。json, canvasRef。current。renderAll。bind(canvasRef。current))

然後我們就可以根據儲存的模版列表,動態切換模版了:

前端:從零實現一款視覺化圖片編輯器

後期規劃

這款圖片編輯器我已經在

github

開源了,大家可以基於次開發更強大的圖片編輯器,對於圖片編輯器的後期規劃,我也評估了幾個可行的方向,如果大家感興趣也可以聯絡我參與到專案中來。

後期規劃如下:

[x] 撤銷重做

[x] 畫布背景設定

[x] 豐富圖形元件庫

[x] 圖片濾鏡配置

[x] 模組化介面

[x] 解析PSD

如果大家對視覺化搭建或者低程式碼/零程式碼感興趣,也可以參考我往期的文章或者在評論區交流你的想法和心得,歡迎一起探索前端真正的技術。

github: mitu-editor | 輕量級且可擴充套件的圖片/圖形編輯器解決方案

作者:徐小夕

專欄:低程式碼視覺化