⚠️ 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 simplify
Canvas
Program 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?