⚠️ This article is the first contract article in the nuggets community. It is prohibited to reprint without authorization

background

As we know, in order to increase the efficiency of the enterprise research and development and the rapid response to customer demand, many companies are now underway to digital transition, is not only a giant (byte, ali, tencent, baidu) in the block, do low code visualization is also doing a lot of small and medium-sized enterprises, with low visual code related technical background programmers have to be taken into account.

I’ve recently been working on data visualization and LowCode/Nocode projects. Based on my own experience and exploration of Lowcode/NoCode, I have also written a series of articles on low-code visualization constructs. Today we will continue to share the content related to visualization — visual image editor.

In the process of sharing, I will take a recent open source project Mitu as an example to carefully disassemble its implementation process. Mitu is mainly used to assist H5 editor H5-dooring to do image processing. It can also be easily developed and extended based on it to become a more powerful image editor.

At the end of the article, I will attach github address and demo address for your convenience to learn and experience. Next, I will take you to introduce and analyze the open source image editor Mitu.

Project introduction

The above is part of the image editor to demonstrate the effect, we can quickly generate the image we want by dragging and reassembling, and can also save the image as a template for later reuse. Before the project development, I also designed a simple prototype to ensure that the development direction of my own will not go astray, you can refer to:

In my usual writing style, I will start with an outline of the technical implementation so that you can read and learn selectively and efficiently:

  • Visual editor project construction and technology selection
  • Graphic Library design
  • Property editor design
  • Custom pixel controller implementation
  • Implementation of preview function
  • Save the picture function
  • Templates save implementations
  • Import template function
  • Visual picture editor later planning

All right, without further ado, let’s move on to our technical implementation.

The technical implementation

Project construction and technology selection

The implementation idea of the editor has nothing to do with the technology stack. Here I use React to implement it. Of course, if you prefer Vue or SvelteJS, it is ok.

  • Umi scalable enterprise – level front-end application framework
  • React + Typescript
  • Antd front-end component library
  • fabricOne can simplifyCanvasProgram written library
  • LocalStorage Local data store

Of course, there are many details and ideas in the implementation process of the project, and I will introduce them to you one by one. Don’t worry if you are not familiar with the Fabric library. I will introduce you to the fabric library through the implementation of specific functions.

Before we get to the next section let’s install Fabric and initialize a canvas.

yarn add fabric
Copy the code

Initialize a canvas:

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');
        // Create a text element
        const shape = new fabric.IText(nanoid(8), {
             text: 'H5-Dooring'.width : 60.height : 60.fill : '#06c'.left: 30.top: 30
         })
        // Insert the text element into the canvas
        canvasRef.current.add(shape);
        // Set the background color of the canvas
        canvasRef.current.backgroundColor = 'rgba(255,255,255,1)';
    })
    return <canvas id="canvas" width={600} height={400}></canvas>
}
Copy the code

We have created a canvas and inserted editable and drag-and-drop text into the canvas as follows:

Graphic Library design

As a picture editor, in order to improve the flexibility of use, we also need to provide some basic graphics for us to design pictures, so I added a graphics library in the editor:

The main elements are text, images, lines, rectangles, circles, triangles, arrows, mosaics, and of course you can add more basic primitives according to your needs. Click on any element in the gallery to insert it into the canvas, using fabric’s add method. Fabric also contains many basic shapes, which we can refer to in the documentation. To make graph insertion more encapsulation, I define the basic schema structure of the graph:

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: {}}Copy the code

So our method of inserting the graph would be like this:

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);
}
Copy the code

In the future, we only need to define the schema when adding graphs. However, it is important to note that the way fabric creates graphs is not uniform. We need to make special decisions on the creation of specific images, such as linear paths:

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})}Copy the code

Of course, we can also use switch to do different things for different situations, so we have a basic image library.

Property editor design

The property editor is mainly used to configure graphic properties, such as fill color, stroke color, and stroke width. So far I have defined these three dimensions, and you can expand more edgeable properties based on this, similar to h5-Dooring’s component properties configuration panel.

We can control the properties of the graph in the property edit area on the right side of the editor, because there are only three properties at present, I will directly hardcode it, we can also use dynamic rendering to achieve. Notice how do we know which component we selected? Fabric provides a series of apis to help us better control the element object, here we use getActiveObject method to get the selected element, concrete implementation code is as follows:

// ...
// Define the base properties
const [attrs, setAttrs] = useState({
    fill: '#0066cc'.stroke: ' '.strokeWidth: 0,})// Update the selected element
const updateAttr = (type: 'fill' | 'stroke' | 'strokeWidth' | 'imgUrl', val:string | number) = >{ setAttrs({... attrs, [type]: val})// Gets the currently selected element object
    const obj = canvasRef.current.getActiveObject()
    // Set element attributesobj.set({... attrs})// re-render
    canvasRef.current.renderAll();
}
Copy the code

The style implementation of the property editor is not introduced here, it is more basic, let’s take a look at the basic structure of the edit item:

<span className={styles.label}>Stroke width:</span>
<InputNumber size="small" min={0} value={attrs.strokeWidth}  onChange={(v)= > updateAttr('strokeWidth', v)} />
Copy the code

Custom pixel controller implementation

Since fabric does not provide a delete button or logic by default, we need to do our own secondary extension, and as fabric does provide a custom extension method, we will customize a delete button together and implement the delete logic.

The specific implementation code is as follows:

// Delete button
const deleteIcon = "Data: image/SVG + XML, % 3 c % 3 FXML version = '1.0' encoding =" utf-8 "% 3 f % 3 e % 3 c! DOCTYPE SVG PUBLIC '- / / / / W3C DTD SVG 1.1 / / EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' % 3 e % 3 CSVG 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 =" the 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 0.7071 398.3889-83.3116) "style =" the fill: white; 'width =' 65.544 'height =' 262.179 '/ % 3 e % 3 c/g % 3 e % 3 c/SVG % 3 e";

// Delete method
function deleteObject(eventData, transform) {
    const target = transform.target;
    const canvas = target.canvas;
    canvas.remove(target);
    canvas.requestRenderAll();
}

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

// Global add/remove button
fabric.Object.prototype.controls.deleteControl = new fabric.Control({
      x: 0.5.y: -0.5.offsetY: -32.// Define the offset distance from the element. You can also define offsetX
      cursorStyle: 'pointer'.mouseUpHandler: deleteObject,
      render: renderIcon,
      cornerSize: 24
});
Copy the code

This allows us to implement custom element control, and we can implement custom controls in a similar way. The effect is as follows:

Implementation of preview function

Preview function I mainly use native Canvas toDataURL method to generate base64 data, and then assign value to IMG tag. Another detail to note is that if the canvas still has selected elements before the preview, the control points will also be captured as follows:

This is very bad for the user experience, we need to see a pure image during the preview, my solution is to uncheck all elements of the canvas before the preview, we can use the Fabric instance discardActiveObject() method to deactivate the state, and then update the canvas, the specific implementation logic is as follows:

// 1. Uncheck all elements of the canvas
canvasRef.current.discardActiveObject()
canvasRef.current.renderAll();

// 2. Convert the current canvas to the base64 address of the image
const img = document.getElementById("canvas");
const src = (img as HTMLCanvasElement).toDataURL("image/png");

// 3. Set the element URL to display the preview popup
setImgUrl(src)
setIsShow(true)
Copy the code

Preview effect display:

Save the picture function

Save the picture and preview function is actually very similar, the only difference is that we need to download the picture to the local, so I mainly use pure front-end way to download the picture, we can also use their familiar front-end download scheme, next post my scheme:

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()
  }))
}
Copy the code

The main method is createObjectURL and revokeObjectURL of window URL objects. I shared the corresponding implementation in my article two years ago, if you are interested, please refer to it. The download looks like this:

Templates save implementations

In the process of designing the picture editor, we should also consider saving the user’s assets. For example, the better pictures can be saved as templates for reuse next time, so I also realized the simple template saving and use function in the editor. Let’s take a look at the effect:

We can see in the demo that the template saved as a template will be automatically synchronized to the template list on the left, so we can directly import the template for second creation next time. Here is the implementation logic diagram:

It can be seen from the above figure that we save the template not only to save the image, but also the corresponding JSON Schema data of the image. The reason why we save the JSON Schema is to ensure that every element of the template can be restored after the user switches to the corresponding template, similar to the PSD source file we are most familiar with. Fabric provides a method to serialize the canvas toDatalessJSON(). When we save the template, we only need to save the serialized JSON and the image together. You can also use indexedDB, a massive local storage solution. I also used indexedDB as an out-of-the-box cache library that you can use directly.

  • XDB | based on promise encapsulation and support expiration time out of the box with indexedDB library cache

The specific implementation of saving templates is as follows:

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))
    / / save picture
    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))
    // Update the template list
    setTpls((prev:any) = > [...prev, {id, t: val}])
    setIsTplShow(false)}Copy the code

Import template function

The essence of importing a template is to deserialize the Json Schema. In the process of researching fabric, WE found that it can directly load the Json rendering graph sequence, so we can directly load the Json saved above into the canvas:

// 1. Empty the canvas before loading
canvasRef.current.clear();
// 2. Reset the canvas background color
canvasRef.current.backgroundColor = 'rgba(255,255,255,1)';
// 3. Render JSON
canvasRef.current.loadFromJSON(tpls[id].json, canvasRef.current.renderAll.bind(canvasRef.current))
Copy the code

We can then dynamically switch templates based on the saved template list:

In the late planning

I have opened the source of this picture editor on Github, and we can develop a more powerful picture editor based on the second. As for the later planning of the picture editor, I have also evaluated several feasible directions. If you are interested, you can also contact me to participate in the project.

The later planning is as follows:

  • Cancel the redo
  • Canvas background Settings
  • Enrich the graphics component library
  • Image filter configuration
  • Modular interface
  • Parsing the PSD

If you are interested in visual scaffolding or low code/zero code, please refer to my previous articles or share your thoughts in the comments section, and explore the real technology of the front end.

Making: mitu – editor | lightweight and extensible pictures/graphics editor solution: nuggets technology community author: Xu Xiaoxi column: low code visualization public number: anecdotal stories front end

The articles

  • How to design a visual platform component store?
  • Build engine from zero design visualization large screen
  • Build desktop visual editor Dooring from zero using Electron
  • (low code) Visual construction platform data source design analysis
  • Build a PC page editor pc-dooring from scratch
  • How to build building blocks to quickly develop H5 pages?