2

Recently, when refining a function, I found that there are too many configurable items. If they are all coupled together, firstly the code is not well maintained and the scalability is not good, and secondly, if I don’t need the function, it will bring about volume redundancy. , Considering the popularity of plug-in, so I tried a little bit.

First introduce the function of this library, a simple function that allows you to mark an area in an area, usually a picture, and then return the vertex coordinates:

Not much to say, Kai Lu.

Plug-in design

I understand that a plug-in is a functional fragment. The code can have various organization methods, functions or classes. Each library or framework may have its own design. Generally, you need to expose a prescribed interface and then inject some when calling the plug-in. Interface or state, expand the functions you need on this basis.

I chose to organize the plug-in code in a functional manner, so a plug-in is an independent function.

First of all, the entrance of the library is a class:

class Markjs {}

The plug-in first needs to be registered, such as the common vue :

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

Refer to this method, our plug-in is also registered like this:

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

First, let’s analyze what this use going to do. Because the plug-in is a function, is it use to call the function directly in 0610e1a682a41d? In fact, it is not possible here, because Markjs is a class. When using it, you need new Markjs to create an instance. The variables and methods that the plug-in needs to access must be instantiated before you can access it, so use only do a simple collection. Yes, the plugin function is called at the same time as it is instantiated. Of course, if your plugin just adds some mixin vue , then it can be called directly:

class Markjs {
    // 插件列表
    static pluginList = []

    // 安装插件
    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
    }
}

The code is very simple. A static property pluginList to store the plug-in. The static method use used to collect plug-ins. It will add a property to the plug-in to determine whether it has been added, avoiding repeated additions. Secondly, it is allowed to pass the second parameter. Control where the plug-in should be inserted, because some plug-ins may have order requirements. Return Markjs for chain call.

After instantiating, iterate and call the plug-in function:

class Markjs {
    constructor(opt = {}) {
        //...
        // 调用插件
        this.usePlugins()
    }
    
    // 调用插件
    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()
    }
}

At the end of the creation of the instance, the plugin will be called. You can see that this is not a simple loop call, but promise . The reason for this is that the initialization of some plugins may be asynchronous, such as this picture. The image loading in the plug-in is an asynchronous process, 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
    })
    
    // 插件逻辑...
    setTimeout(() => {
        _resolve()
    },1000)
    
    return promise
}

At this point, this simple plug-in system is complete, instance is the created instance object, you can access its variables, methods, or listen to the events you need, etc.

Markjs

Markjs has been selected, the core function, here refers to the related function of the annotation, is also considered as a plug-in, so this class of 0610e1a682a510 only does some variable definition, event monitoring, dispatch and initialization.

The annotation function canvas , so the main logic is to monitor some events of the mouse to call canvas for drawing, and the dispatch of events uses a simple subscription publishing model.

class Markjs {
    constructor(opt = {}) {
        // 配置参数合并处理
        // 变量定义
        this.observer = new Observer()// 发布订阅对象
        // 初始化
        // 绑定事件
        // 调用插件
    }
}

The above is all the work done by the Markjs Initialization does one thing, create a canvas element and then get the drawing context, look directly at the binding event, the function of this library needs to use the mouse click, double click, press, move, release and other events:

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

ondblclick events that can be monitored for double-click events click event will also be triggered when double-clicking, so it is impossible to distinguish whether it is a single-click or a double-click. Generally, double-clicks are click event. Of course, you can also monitor the double-click event to simulate a single-click event. One of the reasons for not doing this is that the system's double-click interval is not clear, so the timer interval is difficult to determine:

class Markjs {
    // 单击事件
    onclick(e) {
        if (this.clickTimer) {
            clearTimeout(this.clickTimer)
            this.clickTimer = null
        }

        // 单击事件延迟200ms触发
        this.clickTimer = setTimeout(() => {
            this.observer.publish('CLICK', e)
        }, 200);

        // 两次单击时间小于200ms则认为是双击
        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()// 上一次的单击时间
    }
}

The principle is very simple. The click event is dispatched after a certain time delay. Compare whether the time between two clicks is less than a certain time interval. If it is less than a certain time interval, it is considered to be a click. Here, 200 milliseconds is selected. Of course, it can be smaller. My hand speed in 100 milliseconds is no longer good.

Annotation function

Annotation is undoubtedly the core function of this library, which is also used as a plug-in as described above:

export default function EditPlugin(instance) {
    // 标注逻辑...
}

First, let’s sort out the functions. Click the mouse to confirm the vertices of the labeled area, double-click to close the path of the area, and you can click to activate it again for editing. Editing can only drag and drop the whole or a vertex, and you cannot delete or add vertices. The same canvas There can be multiple labeling areas on the screen at the same time, but only one of them can be activated for editing by clicking one at a time.

Because multiple annotations can exist on the same canvas, and each annotation can also be edited, each annotation has to maintain its state, so you can consider using a class to represent the annotation object:

export default class MarkItem {
    constructor(ctx = null, opt = {}) {
        this.pointArr = []// 顶点数组
        this.isEditing = false// 是否是编辑状态
        // 其他属性...
    }
    // 方法...
}

Then you need to define two variables:

export default function EditPlugin(instance) {
    // 全部的标注对象列表
    let markItemList = []
    // 当前编辑中的标注对象
    let curEditingMarkItem = null
    // 是否正在创建新标注中,即当前标注仍未闭合路径
    let isCreateingMark = false
}

Store all the annotations and the currently active annotation area, the next step is to listen for mouse events to draw. What the click event needs to do is to check whether there is an active object currently, and then judge whether it is closed if it exists, if it does not exist, check whether there is a label object at the position of the mouse click, and activate it if it exists.

instance.on('CLICK', (e) => {
    let inPathItem = null
    // 正在创建新标注中
    if (isCreateingMark) {
        // 当前存在未闭合路径的激活对象,点击新增顶点
        if (curEditingMarkItem) {
            curEditingMarkItem.pushPoint(x, y)// 这个方法往当前标注实例的顶点数组里添加顶点
        } else{// 当前不存在激活对象则创建一个新标注实例
            curEditingMarkItem = createNewMarkItem()// 这个方法用来实例化一个新标注对象
            curEditingMarkItem.enable()// 将标注对象设为可编辑状态
            curEditingMarkItem.pushPoint(x, y)
            markItemList.push(curEditingMarkItem)// 添加到标注对象列表
        }
    } else if (inPathItem = checkInPathItem(x, y)) {// 检测鼠标点击的位置是否存在标注区域,存在则激活它
        inPathItem.enable()
        curEditingMarkItem = inPathItem
    } else {// 否则清除当前状态,比如激活状态等
        reset()
    }
    render()
})

There are many new methods and properties above, and they are all annotated in detail. The specific implementation is very simple and will not be expanded. If you are interested in reading the source code, focus on two of them, checkInPathItem and render .

checkInPathItem function loops through markItemList to detect whether the current position is within the path of the labeling area:

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

checkInPath and checkInPoints are MarkItem , which are used to detect whether a certain position is within the path of the labeling area and each vertex of the label:

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

First draw and close the path according to the current vertex array of the label object, and then call isPointInPath method in the canvas interface to determine whether the point is within the path. The isPointInPath method is only valid for the path and the current path, so it cannot be used if the vertices are square. fillRect ; 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
    }
}

render method also traverses markItemList and calls MarkItem instance. The drawing logic is basically the same as the logic of the detection path above, but when the path is detected, only the path is drawn and the drawing needs to call stroke , fill and other methods to stroke and fill, otherwise Invisible.

At this point, click to create a new label and activate the label to complete, double-click to do it, just close the unclosed path:

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

At this point, the core annotation function is complete. Next, let's look at a function to enhance the experience: detecting line segment crossing.

Vector cross multiplication can be used to detect the intersection of line segments. For details, please refer to this article: https://www.cnblogs.com/tuyang1129/p/9390376.html .

// 检测线段AB、CD是否相交
// a、b、c、d:{x, y}
function checkLineSegmentCross(a, b, c, d) {
    let cross = false
    // 向量
    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]
    // 向量叉乘,判断点c,d分别在线段ab两侧,条件1
    let abac = ab[0] * ac[1] - ab[1] * ac[0]
    let abad = ab[0] * ad[1] - ab[1] * ad[0]

    // 向量
    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]
    // 向量叉乘,判断点a,b分别在线段cd两侧,条件2
    let dcda = dc[0] * da[1] - dc[1] * da[0]
    let dcdb = dc[0] * db[1] - dc[1] * db[0]

    // 同时满足条件1,条件2则线段交叉
    if (abac * abad < 0 && dcda * dcdb < 0) {
        cross = true
    }
    return cross
}

With the above method of detecting the intersection of two line segments, all you have to do is to traverse the labeled vertex array to connect the line segments, and then compare them in pairs.

The method of dragging annotations and vertices is also very simple. Listen to the mouse press event. Use the above method to detect whether the point is in the path to determine whether the pressed position is in the path or vertex. If yes, monitor the mouse movement event to update the whole The pointArr array or the x,y coordinates of a vertex.

At this point, all the annotation functions are completed.

Plugin example

canvas ’s look at a simple picture plug-in. This picture plug-in is to load the picture, and then adjust the width and height of 0610e1a682a802 according to the actual width and height of the picture. It is very simple:

export default function ImgPlugin(instance) {
    let _resolve = null
    let promise = new Promise((resolve) => {
        _resolve = resolve
    })
    
    // 加载图片
    utils.loadImage(opt.img)
        .then((img) => {
            imgActWidth = image.width
            imgActHeight = image.height
            setSize()
            drawImg()
            _resolve()
        })
        .catch((e) => {
            _resolve()
        })
    
    // 修改canvas的宽高
    function setSize () {
        // 容器宽高都大于图片实际宽高,不需要缩放
        if (elRectInfo.width >= imgActWidth && elRectInfo.height >= imgActHeight) {
            actEditWidth = imgActWidth
            actEditHeight =imgActHeight
        } else {// 容器宽高有一个小于图片实际宽高,需要缩放
            let imgActRatio = imgActWidth / imgActHeight
            let elRatio = elRectInfo.width / elRectInfo.height
            if (elRatio > imgActRatio) {
                // 高度固定,宽度自适应
                ratio = imgActHeight / elRectInfo.height
                actEditWidth = imgActWidth / ratio
                actEditHeight = elRectInfo.height
            } else {
                // 宽度固定,高度自适应
                ratio = imgActWidth / elRectInfo.width
                actEditWidth = elRectInfo.width
                actEditHeight = imgActHeight / ratio
            }
        }
        
        canvas.width = actEditWidth
        canvas.height = actEditHeight
    }
    
    // 创建一个新canvas元素来显示图片
    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
}

Summarize

This paper to practice through a simple plug-annotation capabilities a bit of development, no doubt, of the plug-in is a good way to expand, such as vue , Vue CLi , VuePress , BetterScroll , markdown-it , Leaflet etc. are separated by plug-in system Modules, perfect functions, but this also requires a good architecture design. The main problem I encountered in practice was that I didn’t find a good way to judge whether certain properties, methods, and events should be exposed, but Expose it when you are writing a plug-in. The main problem of this is that it is a bit troublesome to develop a plug-in by three parties if a certain method is not accessible. Secondly, the function boundary of the plug-in is not considered clearly, and it is impossible to determine whether the functions are not. If it can be realized, these still need to be understood and improved in the future.

The source code has been uploaded to github: https://github.com/wanglin2/markjs .

Blog: http://lxqnsys.com/ , public account: Ideal Youth Lab

街角小林
897 声望778 粉丝