Recently, I was working on a web version of SVG editor, so I learned about editors. This article is some of my superficial learning summary, hope can give beginners some ideas.

My in-development SVG editor personal project address: github.com/F-star/svg-… Welcome to Star.

In front of the word

With the rapid development of front-end technology in recent years, people tend to put application development on the web browser, that is, B/S architecture. Compared to the traditional C/S model, it is more compatible, less expensive to develop, and requires no installation, just open a page in the browser.

The Web graphics editor mainly uses THE HTML5 Canvas technology and SVG technology. Canvas draws using JavaScript programs, SVG draws using XML document descriptions. SVG is vector based, zoom in and out without distortion. Canvas is bitmap-based, suitable for pixel processing, but also suitable for HTML5 small games. They each have advantages and disadvantages, the specific use of which program development, need to choose according to their own needs.

I was going to make an SVG editor, so I definitely chose the SVG solution. In addition, the svG.js library is used to make it easier to manipulate SVG and make the code more readable. Svg.js provides a readable chain-writing method, which is also useful for learning SVG (you can generate an SVG with simple code). I will put comments next to the code related to SVG.js, so you can read my code without svG.js.

Functional description

Undo: Returns to the state before the last operation.

Redo: If excessive undo is found during the undo process, you can “redo” to enter the state after a certain operation.

In general, slightly more complex editors have undo/redo capabilities. Undo redo is a basic feature of an editor that allows the user to roll back the editor to the state before the error.

Choosing an implementation

Undo/Redo based on object serialization

Implement the undo/redo function. One of the methods is undo/redo based on object serialization.

With each operation, all previous objects are serialized (that is, the current view state is stored in a variable) and pushed into a stack called undoStack. When you need to undo, undoStack out of the stack, the data out of the stack is parsed, restored to the UI layer, and the serialized data out of the stack is pushed into the redoStack.

The advantage of this pattern is that the code is easy to implement and the complexity is low, but the disadvantage is that the more objects there are, the more memory is used to save the state each time, so it is not the preferred solution for the editor.

Undo/Redo based on command mode

In command mode, a command object is created for each operation, which records the specific execution method (execute) and an inverse execution method (undo). Each time the editor performs an operation, the corresponding command object is created, the execute method of the command object is executed, and the object is pushed onto the undo stack.

When the user undoes, if the undo stack is not empty, pop up the command object at the top of the undo stack, execute its execute method, and push the object to the redo stack.

Redo is similar to the above. If the redo stack is not empty, pop the top object, execute the execute method, and push the object onto the undo stack.

Each time you perform an operation and create a new command, clear the redo stack if it is not empty.

Some operations may be combinations of multiple operations, and you need to use the “combination pattern” of design patterns to wrap multiple operations into a single combination. Each execute and redo operation traverses the sub-operations under the combination operation.

Since this pattern records only forward and reverse operations, the natural memory footprint is independent of the number of objects. But because you need to derive the reverse of each operation, the code implementation is more complex than the previous pattern and is not reusable.

The undo redo function of the sample editor uses this pattern.

implementation

Tutorial sample source code address: github.com/F-star/web-…

F-star.github. IO/Web-Editor -…

The code section is a reference to the implementation of SVG-Edit, an open source Web-based, Javascript driven SVG drawing editor.

The preparatory work

First we create an index. HTML file with a div#drawing element to hold our SVG elements.

To make the code more readable, I used the modularity of ES6 and compiled it with Babel.

If you want to develop more complex editors, modularity is necessary to make your code less coupled and easier to unit test. Consider introducing typescript to provide static typing as well, since there are undoubtedly a lot of methods involved in developing an editor, and passing in parameters that are not typed correctly can lead to unexpected errors.

Let’s start writing the code.

First we introduced the svg.js library, then our entry file index.js, and set the type of this script to Module to get native ES6 modularity support. So make sure the browser running the following HTML supports ES6 modularity.

The < body > < div id = "drawing" > < / div > < script SRC = "https://cdnjs.cloudflare.com/ajax/libs/svg.js/2.6.6/svg.js" > < / script > <script src="./index.js" type="module"></script> </body>Copy the code

Then we start writing the code for the history.js file. I’ve used the ES6 class syntax here because it’s significantly more readable than the “prototype inheritance” approach. Of course, you can write it as “prototype inheritance,” and class is just the syntactic sugar.

The command class

First we create a command base class.

Class Command {constructor() {} execute() {throw new Error(' constructor is not overwritten! '); // If this method is not overridden during inheritance, an error will be reported. In this way, an inherited subcommand class is guaranteed to override the method. } undo() {console.error(' undo method not overridden! '); }}Copy the code

We can then wrap a subcommand class based on the business logic and instantiate it as needed. The InsertElementCommand class below is used to create new elements.

// history.js
// Create a collection of methods for different elements
const InsertElement = {
    // Create a text element in [x, y] with a width of size and a height of size;
    // And returns a reference to this object (the SVGJS wrapped object).
    text(x, y, size, content=' ') {
        return draw.text(content).move(x, y).size(size);
    }
    // We can also write rect, circle and other methods.
}

// Insert element command class
export class InsertElementCommand extends Command {

    // Specify the element type and the state to be saved.
    constructor(type, ... args) {
        super(a);this.el = null;
        this.type = type;
        this.args = args;
    }

    execute() {
        // Here write the method of creation
        console.log('exec')
        this.el = InsertElement[this.type](... this.args); }undo() {
        console.log('undo')
        // Remove the element
        this.el.remove(); }}Copy the code

Here, for better generality, we create an InsertElement object that holds various methods to create different types. This object is actually the policy object in the “Policy Pattern” of the design pattern. Here, our code for creating the text type is written in the text method of our InsertElement object.

CommandManager object

So we have a concrete command class. Next, we need to write a command Management object (CommandManager) to manage all the commands we create.

// history.js

// Commands manage objects
export const cmdManager = (() = > {
    let redoStack = [];        / / redo stack
    let undoStack = [];        / / undo stack
    
    return {
        execute(cmd) {
            cmd.execute();                  / / implement the execute
            undoStack.push(cmd);       / / into the stack
            redoStack = [];            / / empty redoStack
        },
    
        undo() {
            if (undoStack.length == 0) {
                alert('can not undo more')
                return;
            }
            const cmd = undoStack.pop();
            cmd.undo();
            redoStack.push(cmd);
        }, 
        
        redo() {
            if (redoStack.length == 0) {
                alert('can not redo more')
                return;
            }
            constcmd = redoStack.pop(); cmd.execute(); undoStack.push(cmd); }}}) ();Copy the code

Every time we create a Command object, we call the cmdManager.execute(CMD) method, which executes the execute method of the Command object and pushes the Command object into the undoStack.

Redo /undo stacks can be implemented in many ways, but to make the code more intuitive, use two arrays to hold two stacks.

In SVG-Edit, a bidirectional linked list is used: an array is used and a pointer is given to a Command object. The pointer is undoStack on the left and redoStack on the right. This way, each time you undo the redo, you only need to change the pointer position, but you don’t need to change the operation on the array, so the time complexity is much lower.

Further packing

With the following code, we can execute and save each step.

let cmd = new InsertElementCommand('text', x, y, 20.'good');
cmdManager.execute(cmd);
Copy the code

But if you have to write code like this for every operation, it’s a bit cumbersome. Taking inspiration from the js native method document.execCommand, I added an executeCommand globally.

// commondAction.js

import {
    InsertElementCommand,
    cmdManager,
} from './history.js'


const commondAction = {
    drawText(. args) {
        let cmd = new InsertElementCommand('text'. args); cmdManager.execute(cmd); },undo() {
        cmdManager.undo();
    },

    redo(){ cmdManager.redo(); }}// Set executeCommond to a global method
window.executeCommond = (cmdName, ... args) = >{ commondAction[cmdName](... args); }Copy the code

We can then create a Command object anywhere and execute its execute command in the following way.

ExecuteCommond ('drawText', x, y, 20, 'ok '); executeCommond('undo'); executeCommond('redo');Copy the code

With the extension of the command, we can parse the first argument cmdName, determine whether to create an element, or modify some parameters of an element (e.g., ‘create rect’, ‘update text’), and then call the corresponding methods.

Finally, we bind these commands to the event response events in the entry index.js file.

Practice after class

You can download the source code I provided on Github and try adding the “Create Rect” feature.

If you want to challenge this, you can also write a function to move elements. If you want to consider the interaction, which involves mousedown, mousemove, and mouseup events, it can be a little complicated, but you can ignore the interaction and move the element by passing in the element ID and coordinates.

Related articles

  1. Web-based SVG Editor (2) — Hierarchical Design (DOM structure)
  2. For more articles on the SVG editor, visit the SVG Editor section of my personal blog

reference

  • Three undo/Redo implementations
  • Talk about command mode from Undo,Redo
  • Implementation of Undo/Redo without number of operations
  • What are the advantages of SVG and HTML5 Canvas and which one is more promising?
  • Use execCommands to edit HTML content in your browser
  • JavaScript design patterns and development practices command patterns, combination patterns
  • Blog.csdn.net/lhrhi/artic…
  • www.haorooms.com/post/js_fwb…