My Canvas entry record, using Canvas to achieve picture annotation

General requirements

The server returns the image address, the front end loads the image and annotates the region on it, and then returns the array of dots to the back end. The flow effect of a picture is as follows:

The implementation process

Draw the reproduction

First of all, the image address returned by the back end needs to be rendered in the Canvas (there are two Canvas tags, one is for drawing the base image and the other is for drawing the annotation layer, the width and height of the two Canvas should be the same, and CSS should be used to overlay the annotation layer on the base image). Use React useEffect to listen for SRC changes in the props to render the base image. Since we do not know the size of the picture, in order to render the picture with the same proportion, we need to calculate the zoom ratio according to the width and height of Canvas and the width and height of picture (it is stored with useRef to facilitate the calculation of coordinate points later).

 useEffect(() = > {
        if(! src)return;
        const ctx = baseCanvas.current.getContext("2d");
        setLoading(true);
        // After the image is loaded, draw it on canvas
        const img = new Image();
        img.src = src;
        img.onload = function () {
            ctx.clearRect(0.0, width, height); // Clear the base map
            let _width = img.width, _height = img.height;
            imgInfo.current = {
                width: _width,
                height: _height
            } // Store image width and height to prevent errors caused by calculation
            if (img.width > width || img.height > height) {
                scale.current = img.width > img.height ? img.width / width : img.height / height;
                _width = img.width / scale.current;
                _height = img.height / scale.current;
            } // Scale equally
            baseCanvas.current.width = topCanvas.current.width = _width;
            baseCanvas.current.height = topCanvas.current.height = _height;
            ctx.drawImage(this.0.0, _width, _height);
            setLoading(false);
            handleClear(); // Clear the annotations
        }
        img.onerror = () = > {
            warn_barry('Image load failed');
            setLoading(false);
        }
    }, [src]);
Copy the code

Okay, so at this point, we’re pretty much done. Can fish for 2 days, the rest of Friday, manual dog head.

Trace points and lines

Defines a point array to store the point information for each new annotation. Bind the click event on the Canvas, then trace the point on the annotation layer, connect the point with the last point information in the point array, and then store the information in the Point array.

let points: Array<point> = [];
const clickFn = (e: any) = > {
                if(brush ! = ='add') return;
                clearTimeout(timer);
                timer = setTimeout(() = > {
                    let x = e.offsetX, y = e.offsetY;
                    const index = points.findIndex(item= > Math.abs(equalScale(item[0].false) - x) < 10 && Math.abs(equalScale(item[1].false) - y) < 10);
                    if (index > 0) { console.error('Please connect to starting point'); return; }
                    if (index === 0) { x = equalScale(points[0] [0].false); y = equalScale(points[0] [1].false); }
                    const ctx = topCanvas.current.getContext("2d");
                    ctx.fillStyle = "rgba(24, 144, 255, 1)";
                    ctx.fillRect(x - 3, y - 3.6.6);
                    let prevPoint;
                    if (prevPoint = points[points.length - 1]) {
                        ctx.strokeStyle = "rgba(24, 144, 255, 1)";
                        ctx.beginPath();
                        ctx.moveTo(equalScale(prevPoint[0].false), equalScale(prevPoint[1].false));
                        ctx.lineTo(x, y);
                        ctx.stroke();
                        ctx.closePath();
                    }
                    points.push([equalScale(x), equalScale(y)]);
                }, 200);
            } // Click draw coordinate points and line
 topCanvas.current.addEventListener('click', clickFn); // Bind the click event to the annotation layer
Copy the code

Double-click to exit and draw the annotation area

After the above range is drawn, save operation is provided (after saving, tracing points and lines are not allowed again, only nodes can be dragged and changed to change shape) to draw the region.

// Draw the annotation method
 const draw = () = > {
        const ctx = topCanvas.current.getContext("2d");
        ctx.clearRect(0.0, width, height);
        markArr.current.forEach(mark= > { // Iterate over the annotation array to draw all annotation areas
            const { points } = mark;
            ctx.beginPath();
            ctx.fillStyle = "rgba(24, 144, 255, 1)";
            points.forEach((item, index) = > {
                const x = equalScale(item[0].false), y = equalScale(item[1].false);
                ctx.fillRect(x - 3, y - 3.6.6);
                index === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
            });
            ctx.stroke();
            ctx.fillStyle = 'rgba(24, 144, 255, .5)';
            ctx.fill();
            ctx.closePath();
        });
    }
 const dbClickFn = () = > {
                clearTimeout(timer);
                if(points.length < 3) {
                    setBrush(' '); 
                    return; // If the plane is not formed, it will not be saved
                }
                if (JSON.stringify(points[0])! = =JSON.stringify(points[points.length - 1])) points.push(points[0]); // The default connection starting point
                markArr.current.push({
                    key: new Date().getTime(),
                    points
                }); // Store the temporary point array
                handleChange(); // Triggers the onChange event passed to props
                draw(); // Draw the annotation area
                setBrush(' '); // Exit the new state
            } // Double-click to save the current annotation
Copy the code

Node drag changes the shape of the label

The above operation has successfully drawn out the scope of the labeled area, the following needs to fine tune the area, need to achieve drag nodes to draw again. Defines a current object to store the current moving point.

  • Monitor mouse movement events, change the point information in current, and redraw the point and region
  • Monitor the mouse-down event to judge whether the current position overlaps with the marked point. If so, store the point information into current
  • Listen for the mouse lift event, clear the current object, trigger the onChange event in the props, and pass the final point information
            let current: null | point | undefined = null;
            const moveFn = throttle((e: any) = > {
                topCanvas.current.style.cursor = 'default';
                const x = e.offsetX, y = e.offsetY;
                if (current) {
                    current[0] = equalScale(x);
                    current[1] = equalScale(y); draw(); }},10);
            const mousedownFn = (e: any) = > {
                const x = equalScale(e.offsetX), y = equalScale(e.offsetY);
                for (let i = 0; i < markArr.current.length; i++) {
                    const { points } = markArr.current[i];
                    current = points.find(item= > Math.abs(item[0] - x) < 20 && Math.abs(item[1] - y) < 20);
                    if(current) break; }}const mouseupFn = () = > {
                current = null;
                handleChange();
            }
            topCanvas.current.addEventListener('mousemove', moveFn);
            topCanvas.current.addEventListener('mousedown', mousedownFn);
            topCanvas.current.addEventListener('mouseup', mouseupFn);
Copy the code

At this point, we’re almost done.

Json data bidirectional binding

Now that the general requirements have been met, the customer has come up with the need to visualize json data and make changes.

The user does not want to trace the point, but changes the shape by directly changing the point information. So you have this string of JSON data on the right, and the user can change the point information in the JSON to achieve the effect of drawing the region. Since I have encapsulated the annotation component into an effect similar to ANTD Input, it has no state of its own, and all the data is changed through value and onChange in props. The only thing to do is to cache the Canvas element to avoid unnecessary rendering. Trigger onChange in props every time a new annotation is complete or a drag is complete. Don’t call onChange during the drag, you may find that the page is stuck.

Refer to the case

Wanglin2. Making. IO/useful/markjs / #… Wanglin2. Making. IO/useful/markjs / #…

See you in the next article.