<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          從零開發(fā)一款圖片編輯器Mitu-Dooring

          共 14035字,需瀏覽 29分鐘

           ·

          2021-08-31 22:33

          開學(xué)季

          關(guān)注并將「趣談前端」設(shè)為星標(biāo)

          每天定時分享技術(shù)干貨/優(yōu)秀開源/技術(shù)思維

          背景介紹

          我們知道,為了提高企業(yè)研發(fā)效能和對客戶需求的快速響應(yīng),現(xiàn)在很多企業(yè)都在著手?jǐn)?shù)字化轉(zhuǎn)型,不僅僅是大廠(阿里,字節(jié),騰訊,百度)在做低代碼可視化這一塊,很多中小企業(yè)也在做,擁有可視化低代碼相關(guān)技術(shù)背景的程序員也越來受重視。

          我最近一直在做數(shù)據(jù)可視化和lowcode/nocode相關(guān)的項(xiàng)目,針對我自己的工作經(jīng)驗(yàn)和對lowcode/nocode的探索,也寫了一系列低代碼可視化搭建系列文章,今天我們繼續(xù)來分享可視化相關(guān)的內(nèi)容——可視化圖片編輯器

          在分享過程中,我會以最近我寫開源的一個項(xiàng)目Mitu為案例,仔細(xì)拆解它的實(shí)現(xiàn)過程。Mitu主要是輔助H5編輯器 H5-Dooring 做圖像處理用的,大家也可以輕松基于它進(jìn)行二次開發(fā)和擴(kuò)展,變成更強(qiáng)大的圖片編輯器。

          在文章末尾我會附上 github 地址 和 demo 地址,方便大家學(xué)習(xí)和體驗(yàn)。接下來我就來帶大家介紹和剖析一下這款開源圖片編輯器 Mitu

          項(xiàng)目介紹

          mitu-dooring.gif

          以上是圖片編輯器的部分演示效果,我們可以通過拖拽重組的方式快速生成我們想要的圖片,也能將圖片保存為模版,以便后期復(fù)用。在項(xiàng)目開發(fā)之前我也設(shè)計(jì)了一個簡單的原型,保證自己的開發(fā)方向不會跑偏,大家可以參考一下:

          image.png

          按照我一向的寫作風(fēng)格,我先列一下技術(shù)實(shí)現(xiàn)的大綱,以便大家有選擇且高效率的閱讀和學(xué)習(xí):

          • 可視化編輯器項(xiàng)目搭建和技術(shù)選型
          • 圖形庫設(shè)計(jì)
          • 屬性編輯器設(shè)計(jì)
          • 自定義圖元控制器實(shí)現(xiàn)
          • 預(yù)覽功能實(shí)現(xiàn)
          • 保存圖片功能實(shí)現(xiàn)
          • 模版保存實(shí)現(xiàn)
          • 導(dǎo)入模版功能實(shí)現(xiàn)
          • 可視化圖片編輯器后期規(guī)劃

          好了,話不多說,接下來開始我們的技術(shù)實(shí)現(xiàn)。

          技術(shù)實(shí)現(xiàn)

          image.png

          項(xiàng)目搭建和技術(shù)選型

          編輯器的實(shí)現(xiàn)思路和技術(shù)棧無關(guān),這里我采用了 React 來實(shí)現(xiàn),當(dāng)然大家如果更喜歡 Vue 或者 sveltejs,也是沒問題的,項(xiàng)目整體技術(shù)選型如下:

          • umi 可擴(kuò)展的企業(yè)級前端應(yīng)用框架
          • React + Typescript
          • Antd 前端組件庫
          • fabric 一個可以簡化 Canvas 程序編寫的庫
          • localStorage 本地數(shù)據(jù)存儲

          當(dāng)然在項(xiàng)目的實(shí)現(xiàn)過程中還有很多細(xì)節(jié)和思想,接下來我會一一和大家介紹。如果大家對 fabric 這個庫不太熟悉也不用擔(dān)心,我會通過具體功能的實(shí)現(xiàn)來帶大家熟悉這個庫。

          在介紹下面的內(nèi)容之前我們先安裝一下 fabric ,然后初始化一個畫布。

          yarn add fabric

          初始化一個畫布:

          import { fabric } from "fabric";
          import { nanoid } from 'nanoid';
          import { useEffect, useState, useRef } from 'react';

          export default function IndexPage({
              const canvasRef = useRef<any>(null);
              useEffect(() => {
                  canvasRef.current = new fabric.Canvas('canvas');
                  // 創(chuàng)建一個文本元素
                  const shape = new fabric.IText(nanoid(8), {
                       text'H5-Dooring',
                       width : 60,
                       height : 60,
                       fill : '#06c',
                       left30,
                       top30
                   })
                  // 將文本元素插入畫布
                  canvasRef.current.add(shape);
                  // 設(shè)置畫布的背景色
                  canvasRef.current.backgroundColor = 'rgba(255,255,255,1)';
              })
              return <canvas id="canvas" width={600} height={400}></canvas>
          }

          這樣我們就創(chuàng)建好了一個畫布,并在畫布中插入了一段可編輯可拖拽的文本,如下:

          image.png

          圖形庫設(shè)計(jì)

          作為一款圖片編輯器,為了提高使用的靈活性我們還需要提供一些基礎(chǔ)圖形方便我們設(shè)計(jì)圖片,所以我在編輯器里添加了圖形庫:

          image.png

          主要有如文本圖片直線矩形圓形三角形箭頭馬賽克,當(dāng)然大家可以根據(jù)自己的需求添加更多的基本圖元。我們在圖片庫中點(diǎn)擊任意一個元素即可將其插入畫布,這塊是利用 fabric 的 add 方法,當(dāng)然 fabric 也內(nèi)制了很多基本圖形,我們可以在文檔中參考一下。為了讓圖形插入更有封裝性,我定義了圖形的基本 schema 結(jié)構(gòu):

          const baseShapeConfig = {
            IText: {
              text'H5-Dooring',
              width : 60,
              height : 60,
              fill : '#06c'
            },
            Triangle: {
              width100,
              height100,
              fill'#06c'
            },
            Circle: {
              radius50,
              fill'#06c'
            },
            Rect: {
              width : 60,
              height : 60,
              fill : '#06c'
            },
            Line: {
              width100,
              height1,
              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);
          }

          后續(xù)我們添加圖形時只需要定義 schema 即可,但是需要注意的是 fabric 創(chuàng)建圖形的方式并不都都是統(tǒng)一的,我們需要對特定圖片的創(chuàng)建進(jìn)行特殊判斷,比如直線路徑:

          if(type === 'Line') {
                shape = new fabric.Path('M 0 0 L 100 0', {
                  stroke'#ccc'
                  strokeWidth2,
                  objectCachingfalse,
                  left: size[0] / 3,
                  top: size[1] / 3
                })
          }

          當(dāng)然我們也可以用 switch 來對不同情況進(jìn)行不同處理,這樣我們就實(shí)現(xiàn)了一個基本圖片庫。

          chrome-capture (9).gif

          屬性編輯器設(shè)計(jì)

          屬性編輯器主要是用來對圖形屬性進(jìn)行配置的,比如填充顏色描邊顏色描邊寬度,目前我主要定義了這3個維度,大家也可以基于此繼續(xù)擴(kuò)展更多的可編輯屬性,類似于 H5-Dooring 的組件屬性配置面板。

          chrome-capture (10).gif

          我們可以在編輯器右側(cè)的屬性編輯區(qū)控制圖形的屬性,因?yàn)閷傩阅壳爸挥?個,我就直接硬編碼寫上去了,大家也可以用動態(tài)渲染的方式來實(shí)現(xiàn)。需要注意的是我們怎么知道我們選中的是那個組件呢? 好在 fabric 提供了一系列 api 幫助我們更好的控制元素對象,這里我們用 getActiveObject 方法拿到當(dāng)前選中的元素,具體實(shí)現(xiàn)代碼如下:

          // ...
          // 定義基礎(chǔ)屬性
          const [attrs, setAttrs] = useState({
              fill'#0066cc',
              stroke'',
              strokeWidth0,
            })
          // 更新選中的元素
          const updateAttr = (type: 'fill' | 'stroke' | 'strokeWidth' | 'imgUrl', val:string | number) => {
              setAttrs({...attrs, [type]: val})
              // 獲取當(dāng)前選中元素對象
              const obj = canvasRef.current.getActiveObject()
              // 設(shè)置元素屬性
              obj.set({...attrs})
              // 重新渲染
              canvasRef.current.renderAll();
          }

          屬性編輯器的樣式實(shí)現(xiàn)這里我就不一一介紹了,都比較基礎(chǔ),我們來看一下編輯項(xiàng)的基本結(jié)構(gòu):

          <span className={styles.label}>描邊寬度: </span>
          <InputNumber size="small" min={0} value={attrs.strokeWidth}  onChange={(v) => updateAttr('strokeWidth', v)} />

          自定義圖元控制器實(shí)現(xiàn)

          因?yàn)槟J(rèn)情況下 fabric 沒有提供刪除按鈕和邏輯,所以我們需要自己二次擴(kuò)展,恰好 fabric 提供了自定義擴(kuò)展的方法,接下來我們就一起自定義一個刪除按鈕并實(shí)現(xiàn)刪除邏輯。

          image.png

          具體實(shí)現(xiàn)代碼如下:

          // 刪除按鈕
          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({
                x0.5,
                y-0.5,
                offsetY-32// 自定義距元素的偏移距離, 也可以定義offsetX
                cursorStyle'pointer',
                mouseUpHandler: deleteObject,
                render: renderIcon,
                cornerSize24
          });

          這樣我們就實(shí)現(xiàn)了自定義元素控制,我們也可以按照類似的方法實(shí)現(xiàn)自定義的控件。效果如下:

          chrome-capture (11).gif

          預(yù)覽功能實(shí)現(xiàn)

          預(yù)覽功能我主要是利用原生 canvas 的 toDataURL 方法來生成base64的數(shù)據(jù),然后賦值給 img 標(biāo)簽。還有一個細(xì)節(jié)需要注意的是如果我們在預(yù)覽之前畫布仍然有選中狀態(tài)的元素,那么控制點(diǎn)也會被截取出來,如下:

          image.png

          這樣對用戶體驗(yàn)非常不好,我們需要在預(yù)覽時看到一張純粹的圖片,我的方案是在預(yù)覽前取消畫布所有元素的選中狀態(tài),可以用 fabric 實(shí)例的 discardActiveObject() 方法取消激活狀態(tài),然后更新畫布即可,具體實(shí)現(xiàn)邏輯如下:

          // 1. 取消畫布所有元素的選中狀態(tài)
          canvasRef.current.discardActiveObject()
          canvasRef.current.renderAll();

          // 2. 將當(dāng)前畫布轉(zhuǎn)化為圖片的base64地址
          const img = document.getElementById("canvas");
          const src = (img as HTMLCanvasElement).toDataURL("image/png");

          // 3. 設(shè)置元素url,顯示預(yù)覽彈窗
          setImgUrl(src)
          setIsShow(true)

          預(yù)覽效果展示:

          chrome-capture (12).gif

          保存圖片功能實(shí)現(xiàn)

          保存圖片其實(shí)和預(yù)覽功能很像,唯一不同的是我們需要把圖片下載到本地,那么我主要是用純前端的方式實(shí)現(xiàn)圖片下載,大家也可以用自己熟悉的前端下載方案,接下來貼一下我的方案實(shí)現(xiàn):

          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 方法,兩年前我也在我的文章中分享過對應(yīng)的實(shí)現(xiàn),感興趣的可以參考一下。下載的效果如下:

          image.png

          模版保存實(shí)現(xiàn)

          在設(shè)計(jì)圖片編輯器的過程中我們也要考慮保存用戶的資產(chǎn),比如做的比較好的圖片可以保存為模版,以便下次復(fù)用,所以我在編輯器里還實(shí)現(xiàn)的簡單的模版保存和使用的功能。我們先看一下效果:

          chrome-capture (13).gif

          我們在演示中可以看到保存為模版之后會自動同步到左側(cè)的模版列表中,我們下次創(chuàng)作時可以直接導(dǎo)入模版進(jìn)行二次創(chuàng)作。以下是實(shí)現(xiàn)的邏輯圖:

          image.png

          由上圖可以發(fā)現(xiàn)我們保存模版不僅僅是保存圖片,還需要保存圖片對應(yīng)的 json schema 數(shù)據(jù),之所以要保存 json schema 是為了當(dāng)用戶切換到對應(yīng)的模版之后可以保證模版的每個元素都可以還原,類似于我們最熟悉的 PSD 源文件。fabric 提供了序列化畫布的方法 toDatalessJSON(),我們在保存模版的時候只要把序列化后的 json 和圖片一起保存即可,這里方便處理我暫時存在 localStorage 中,大家也可以使用大容量本地化存儲方案 indexedDB,我之前也基于 indexedDB 封裝了開箱即用的緩存庫 xdb,大家可以直接拿來使用。

          • xdb | 基于promise封裝且支持過期時間的開箱即用的indexedDB緩存庫

          保存模版的具體實(shí)現(xiàn)如下:

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

          導(dǎo)入模版功能實(shí)現(xiàn)

          導(dǎo)入模版的本質(zhì)是反序列化 Json Schema,在研究 fabric 的過程中發(fā)現(xiàn)了其可以直接加載 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))

          然后我們就可以根據(jù)保存的模版列表,動態(tài)切換模版了:

          chrome-capture (14).gif

          后期規(guī)劃

          這款圖片編輯器我已經(jīng)在 github 開源了,大家可以基于次開發(fā)更強(qiáng)大的圖片編輯器,對于圖片編輯器的后期規(guī)劃,我也評估了幾個可行的方向,如果大家感興趣也可以聯(lián)系我參與到項(xiàng)目中來。

          后期規(guī)劃如下:

          • [x] 撤銷重做
          • [x] 畫布背景設(shè)置
          • [x] 豐富圖形組件庫
          • [x] 圖片濾鏡配置
          • [x] 模塊化界面
          • [x] 解析PSD

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

          好了,今天的分享就到這里了,如果文章對你有幫助,你也可以點(diǎn)贊 + 轉(zhuǎn)發(fā), 鼓勵作者持續(xù)創(chuàng)作。




          從零搭建全棧可視化大屏制作平臺V6.Dooring

          從零設(shè)計(jì)可視化大屏搭建引擎

          Dooring可視化搭建平臺數(shù)據(jù)源設(shè)計(jì)剖析

          可視化搭建的一些思考和實(shí)踐

          基于Koa + React + TS從零開發(fā)全棧文檔編輯器(進(jìn)階實(shí)戰(zhàn)



          創(chuàng)作不易,加個點(diǎn)贊、在看 支持一下哦!

          瀏覽 90
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久久久精品欧美 | 国产色情在线 | 色情网站在线 | 国产三级视频在线 | 国产美女被操网站 |