Recently, WHEN I was refining a function, I found that there were too many configurable items. If they were all coupled together, firstly, it would be difficult to maintain and expand the code, and secondly, it would bring volume redundancy if I did not need this function. Considering the popularity of plug-in, I tried a little.

A simple function that allows you to label an area, usually an image, with a range, and then return the vertex coordinates:

Don’t talk too much, just masturbate.

Plug-in design

The code can be organized in various ways. Functions or classes. Each library or framework may have its own design.

I chose to organize the plug-in code as a function, so a plug-in is a function in its own right.

The first entry to the library is a class:

class Markjs {}
Copy the code

Plug-ins need to be registered first, such as the common vue:

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
Copy the code

In this way, our plugin is also registered:

import Markjs from 'markjs'
import imgPlugin from 'markjs/src/plugins/img'
Markjs.use(imgPlugin)
Copy the code

First of all, let’s analyze what the use does. Since the plug-in is a function, is it ok to call the function directly from the use? Markjs is a class that requires new Markjs to create an instance. Variables and methods that the plug-in needs to access must be instantiated before they can be accessed. Therefore, use only does a simple collection work. If your plugin is just adding mixins or methods to prototypes like vue, it can be called directly:

class Markjs {
    // List of plug-ins
    static pluginList = []

    // Install the plug-in
    static use(plugin, index = -1) {
        if(! plugin) {return Markjs
        }
        if (plugin.used) {
            return Markjs
        }
        plugin.used = true
        if (index === -1) {
            Markjs.pluginList.push(plugin)
        } else {
            Markjs.pluginList.splice(index, 0, plugin)
        }
        return Markjs
    }
}
Copy the code

Code is very simple, defines a static property pluginList used to store the plug-in, use static method is used to collect the plugin, will add an attribute to the plugin is used to determine whether have been added, to avoid repetition and add, secondly also allows to insert the second parameter to control the plug-in to which position, because some plugins may have order requirements. Return Markjs for chain calls.

The plugin function is then iterated while instantiating:

class Markjs {
    constructor(opt = {}) {
        / /...
        // Call the plug-in
        this.usePlugins()
    }
    
    // Call the plug-in
    usePlugins() {
        let index = 0
        let len = Markjs.pluginList.length
        let loopUse = () = > {
            if (index >= len) {
                return
            }
            let cur = Markjs.pluginList[index]
            cur(this, utils).then(() = > {
                index++
                loopUse()
            })
        }
        loopUse()
    }
}
Copy the code

At the end of the instance creation, the plug-in will be called. As you can see, it is not a simple circular call, but a chain call through promise. The reason for this is that the initialization of some plug-ins may be asynchronous, such as the image loading process in this image plug-in. So the corresponding plug-in function must return a promise:

export default function ImgPlugin(instance) {
    let _resolve = null
    let promise = new Promise((resolve) = > {
        _resolve = resolve
    })
    
    // Plugin logic...
    setTimeout(() = > {
        _resolve()
    },1000)
    
    return promise
}
Copy the code

At this point, the simple plug-in system is complete. Instance is an instance object that you create, can access its variables, methods, or listen for events you need, etc.

Markjs

Since plugins have been chosen, the core functionality, which means annotation related functionality, is also considered as a plug-in, so the Markjs class does only variable definition, event listener distribution, and initialization.

The annotation function is realized by using Canvas, so the main logic is to monitor some events of the mouse to call the drawing context of canvas for drawing, and the event distribution uses a simple subscription publishing mode.

class Markjs {
    constructor(opt = {}) {
        // Merge configuration parameters
        // Variable definition
        this.observer = new Observer()// Publish the subscription object
        / / initialization
        // Bind events
        // Call the plug-in}}Copy the code

That’s all the Markjs class does. Initialization does one thing: create a Canvas element and get the drawing context to view the binding events directly. The library functions with mouse click, double click, press, move, release, and so on:

class Markjs {
    bindEvent() {
        this.canvasEle.addEventListener('click'.this.onclick)
        this.canvasEle.addEventListener('mousedown'.this.onmousedown)
        this.canvasEle.addEventListener('mousemove'.this.onmousemove)
        window.addEventListener('mouseup'.this.onmouseup)
        this.canvasEle.addEventListener('mouseenter'.this.onmouseenter)
        this.canvasEle.addEventListener('mouseleave'.this.onmouseleave)
    }
}
Copy the code

Double-click event despite the ondblclick event can listen, but double click on the click event will also trigger, so can’t distinguish between click or double-click, double-click is commonly by the click event simulation, also can monitor click events to simulate the click event of course, no one reason to do so is not clear system of double click time interval, Therefore, the timer interval is not easy to determine:

class Markjs {
    // Click events
    onclick(e) {
        if (this.clickTimer) {
            clearTimeout(this.clickTimer)
            this.clickTimer = null
        }

        // the click event is triggered 200ms later
        this.clickTimer = setTimeout(() = > {
            this.observer.publish('CLICK', e)
        }, 200);

        // if the time between two clicks is less than 200ms, it is considered as a double click
        if (Date.now() - this.lastClickTime <= 200) {
            clearTimeout(this.clickTimer)
            this.clickTimer = null
            this.lastClickTime = 0
            this.observer.publish('DOUBLE-CLICK', e)
        }

        this.lastClickTime = Date.now()// Last click time}}Copy the code

The principle is very simple, delay a certain time to send the click event, compare the time of two clicks is less than a certain interval, if less than a click, select 200 milliseconds, of course, can also be smaller, but 100 milliseconds my hand speed is not good.

Tagging function

Annotations are definitely the core functionality of this library, and as described above it also serves as a plug-in:

export default function EditPlugin(instance) {
    // Call logic...
}
Copy the code

To manage the function, the mouse click to determine the marked area each vertex, double-click on the path to the closed area, after can edit click activate again, editors can only drag the whole or a vertex, is not able to delete or add vertex, the same canvas can exist at the same time more marked area, but only at a certain moment allows click activate one for editing.

Since multiple annotations can exist on the same canvas, and each annotation can be edited, so each annotation must maintain its state, consider using a class to represent annotation objects:

export default class MarkItem {
    constructor(ctx = null, opt = {}) {
        this.pointArr = []// Vertex array
        this.isEditing = false// Whether to edit
        // Other attributes...
    }
    / / method...
}
Copy the code

Then you need to define two variables:

export default function EditPlugin(instance) {
    // List of all annotation objects
    let markItemList = []
    // The annotation object in the current edit
    let curEditingMarkItem = null
    // Whether a new annotation is being created, that is, the current annotation has not yet closed the path
    let isCreateingMark = false
}
Copy the code

Store all annotations and the currently active annotation area, and then listen for mouse events to draw. Click event to do is to check whether there is an active object, if there is, then judge whether it is closed, if not, check whether there is a annotation object at the mouse click position, if there is, activate it.

instance.on('CLICK'.(e) = > {
    let inPathItem = null
    // Creating a new annotation
    if (isCreateingMark) {
        // If there is an active object with an open path, click Add vertex
        if (curEditingMarkItem) {
            curEditingMarkItem.pushPoint(x, y)// This method adds vertices to the array of vertices in the current annotation instance
        } else{// Create a new annotation instance if no active object currently exists
			curEditingMarkItem = createNewMarkItem()This method is used to instantiate a new annotation object
            curEditingMarkItem.enable()// Make the annotation object editable
            curEditingMarkItem.pushPoint(x, y)
            markItemList.push(curEditingMarkItem)// Add to the list of annotation objects}}else if (inPathItem = checkInPathItem(x, y)) {// Check if there is a labeling area in the mouse click position, activate it if there is
        inPathItem.enable()
        curEditingMarkItem = inPathItem
    } else {Otherwise, clear the current state, such as active state, etc
        reset()
    }
    render()
})
Copy the code

There are a lot of new methods and properties, all annotated in detail, the concrete implementation is very simple not to expand, interested in reading the source code, focus on two of them, checkInPathItem and Render.

The checkInPathItem function loops through the markItemList to check if the current location is within the tagged area path:

function checkInPathItem(x, y) {
    for (let i = markItemList.length - 1; i >= 0; i--) {
        let item = markItemList[i]
        if(item.checkInPath(x, y) || item.checkInPoints(x, y) ! = = -1) {
            return item
        }
    }
}
Copy the code

CheckInPath and checkInPoints are two methods on the MarkItem prototype that check whether a location is within the path of the callout area and within the vertices of the callout, respectively:

export default class MarkItem {
    checkInPath(x, y) {
        this.ctx.beginPath()
        for (let i = 0; i < this.pointArr.length; i++) {
            let {x, y} = this.pointArr[i]
            if (i === 0) {
                this.ctx.moveTo(x, y)
            } else {
                this.ctx.lineTo(x, y)
            }
        }
        this.ctx.closePath()
        return this.ctx.isPointInPath(x, y)
    }
}
Copy the code

First draw and close the path according to the current vertex array of the annotation object, and then call the isPointInPath method in canvas interface to judge whether the point is in the path. IsPointInPath method is only for the path and is valid for the current path, so fillRect cannot be used if the vertex is square. To draw, use rect:

export default class MarkItem {
    checkInPoints(_x, _y) {
        let index = -1
        for (let i = 0; i < this.pointArr.length; i++) {
            this.ctx.beginPath()
            let {x, y} = this.pointArr[i]
            this.ctx.rect(x - pointWidth, y - pointWidth, pointWidth * 2, pointWidth * 2)
            if (this.ctx.isPointInPath(_x, _y)) {
                index = i
                break}}return index
    }
}
Copy the code

Render method is also traversing the markItemList, calling the drawing method of the MarkItem instance. The drawing logic is basically the same as the logic of the above path detection, except that the path detection only needs to draw the path and the drawing needs to call the stroke, fill and other methods to stroke and fill, otherwise invisible.

To do this, click Create annotation and Activate annotation. Double-click to close the unclosed path:

instance.on('DOUBLE-CLICK'.(e) = > 
    if (curEditingMarkItem) {
        isCreateingMark = false
        curEditingMarkItem.closePath()
        curEditingMarkItem.disable()
        curEditingMarkItem = null
        render()
    }
})
Copy the code

Now that the core annotation function is complete, let’s move on to a feature that improves the experience: detecting line crossing.

Detection line cross fork by the way, can use vector detail can refer to this article: www.cnblogs.com/tuyang1129/… .

// check whether line AB and CD intersect
// a, b, c, d: {x, y}
function checkLineSegmentCross(a, b, c, d) {
    let cross = false
    / / vector
    let ab = [b.x - a.x, b.y - a.y]
    let ac = [c.x - a.x, c.y - a.y]
    let ad = [d.x - a.x, d.y - a.y]
    // Point c and d are on both sides of line segment AB, condition 1
    let abac = ab[0] * ac[1] - ab[1] * ac[0]
    let abad = ab[0] * ad[1] - ab[1] * ad[0]

    / / vector
    let dc = [c.x - d.x, c.y - d.y]
    let da = [a.x - d.x, a.y - d.y]
    let db = [b.x - d.x, b.y - d.y]
    // Point a and b are on both sides of line segment CD, condition 2
    let dcda = dc[0] * da[1] - dc[1] * da[0]
    let dcdb = dc[0] * db[1] - dc[1] * db[0]

    // If condition 1 is satisfied, then line segments cross
    if (abac * abad < 0 && dcda * dcdb < 0) {
        cross = true
    }
    return cross
}
Copy the code

With the method above to detect the intersection of two line segments, all you need to do is traverse the labeled array of vertices to join the segments, and then compare them in pairs.

The method of dragging and dropping marks and vertices is also very simple. Listen for the mouse press event to determine whether the pressed position is in the path or vertex using the method of whether the above detection point is in the path. If yes, listen for the mouse movement event to update the entire pointArr array or the x and Y coordinates of a vertex.

At this point, all the annotation functions are complete.

Plug-in sample

Next, let’s look at a simple image plugin. This image plugin is to load the image and adjust the width and height of the canvas according to the actual width and height of the image. It is very simple:

export default function ImgPlugin(instance) {
    let _resolve = null
    let promise = new Promise((resolve) = > {
        _resolve = resolve
    })
    
    // Load the image
    utils.loadImage(opt.img)
        .then((img) = > {
            imgActWidth = image.width
            imgActHeight = image.height
            setSize()
        	drawImg()
            _resolve()
        })
        .catch((e) = > {
            _resolve()
        })
    
    // Change the canvas width and height
    function setSize () {
        // The width and height of the container are larger than the actual width and height of the image, no need to zoom
        if (elRectInfo.width >= imgActWidth && elRectInfo.height >= imgActHeight) {
            actEditWidth = imgActWidth
            actEditHeight =imgActHeight
        } else {// The size of the container is smaller than the actual size of the image
            let imgActRatio = imgActWidth / imgActHeight
            let elRatio = elRectInfo.width / elRectInfo.height
            if (elRatio > imgActRatio) {
                // The height is fixed and the width is adaptive
                ratio = imgActHeight / elRectInfo.height
                actEditWidth = imgActWidth / ratio
                actEditHeight = elRectInfo.height
            } else {
                // The width is fixed and the height is adaptive
                ratio = imgActWidth / elRectInfo.width
                actEditWidth = elRectInfo.width
                actEditHeight = imgActHeight / ratio
            }
        }
        
        canvas.width = actEditWidth
        canvas.height = actEditHeight
    }
    
    // Create a new Canvas element to display the image
    function drawImg () {
        let canvasEle = document.createElement('canvas')
        instance.el.appendChild(canvasEle)
        let ctx = canvasEle.getContext('2d')
        ctx.drawImage(image, 0.0, actEditWidth, actEditHeight)
    }
    
    return promise
}
Copy the code

conclusion

This paper uses a simple annotation function to practice plug-in development. There is no doubt that plug-in is a good way to expand, such as VUE, VUE CLi, VuePress, BetterScroll, MarkDown-it, Leaflet and so on, to separate modules and improve functions through plug-in system. But it also requires a good architectural design, I am one of the main problems in the process of practice is not to find a good way to determine whether certain properties, methods and events to expose out, but when writing plug-ins to expose, the three of the main problem is that to develop the plug-in if you need a method to visit no more than a little trouble, Secondly, the functional boundaries of plug-ins have not been clearly considered, and it is impossible to determine which functions can be realized, which still need to be understood and improved in the future.

The source code has been uploaded to github: github.com/wanglin2/ma… .

Blog: lxqnsys.com/, official account: Ideal Youth Lab