ailon / markerjs2

Add image annotation to your web apps.
https://markerjs.com
Other
141 stars 39 forks source link

How to scale markers upon image resize? #194

Open SadmanYasar opened 1 month ago

SadmanYasar commented 1 month ago

I have a Nextjs app using Markerjs for annotating images. I set the width to 500px but on mobile view, its width is full. It works but due to resizing of the image, the canvas seems to resize but the markers are not scaling to the image. I am using a combination of both markerjs2 and markerjs live. So by default, it calls the showMarkers method attached below and when editing, it shows the markerArea. The edit is enabled using a state after clicking a row in a table. All the states are stored in a statemap so the markers are displayed on top of the original image instead of rendered image after adding a marker. Showing rendered image instead fixes this issue as its just showing an image, but I am looking for a way to make the markers responsive.

The code for showing markers:

const showMarkerArea = useCallback((rowId: string) => {
        if (!imgRef.current || !isImageLoaded) return;

        if (markerArea.current) {
            markerArea.current.close();
        }

        if (markerView.current) {
            markerView.current.close();
        }

        markerArea.current = new markerjs2.MarkerArea(imgRef.current);

        markerArea.current.availableMarkerTypes = [
            markerjs2.EllipseMarker
        ];

        markerArea.current.settings.defaultColorSet = ['#EF4444', '#3B82F6', '#10B981', '#F59E0B', '#6366F1'];

        if (el.current) {
            markerArea.current.targetRoot = el.current;
        }

        markerArea.current.addEventListener('markercreate', (event) => {
            if (event.marker) {
                event.marker.notes = rowId;
            }
        });

        markerArea.current.addEventListener('render', (event) => {
            updateStateMap(rowId, event.state);
        });

        markerArea.current.addEventListener('close', (event) => {
            updateStateMap(rowId, event.markerArea.getState());
        });

        markerArea.current.settings.displayMode = "inline";

        markerArea.current.show();

        if (stateMap[rowId]) {
            markerArea.current.restoreState(stateMap[rowId]);
        }

        console.log('stateMap', stateMap);
    }, [stateMap, updateStateMap, isImageLoaded]);

    const showMarkers = useCallback((target: HTMLImageElement) => {
        if (!isImageLoaded) return;

        if (markerView.current) {
            markerView.current.close();
        }

        if (markerArea.current) {
            markerArea.current.close();
        }

        markerView.current = new mjslive.MarkerView(target);

        if (el.current) {
            markerView.current.targetRoot = el.current;
        }

        const allMarkers = Object.values(stateMap).flatMap(state => state.markers);

        markerView.current.addEventListener("load", (mv) => {
            mv.markers.forEach((m) => {
                m.outerContainer.style.opacity = "0.5"; // Set all markers to 0.5 opacity
                if (hoveredItem && m.notes === hoveredItem) {
                    m.outerContainer.style.opacity = "1"; // Set matching markers to 1 opacity
                }
            });
        });

        const viewState: markerjs2.MarkerAreaState = {
            width: target.width,
            height: target.height,
            markers: allMarkers
        };

        console.log('viewState', viewState);

        try {
            markerView.current.show(viewState);
        } catch (error) {
            console.error('Error showing markers:', error);
            console.log('Current viewState:', viewState);
        }
    }, [stateMap, hoveredItem, isImageLoaded]);

The code for displaying the image:

<div className='flex w-full flex-col md:flex-row gap-4'>
                <div className="relative border rounded-lg max-sm:w-full max-w-[500px] max-h-[500px]" ref={el}>
                    <img
                        ref={imgRef}
                        src={src}
                        crossOrigin="anonymous"
                        alt='Image to annotate'
                        className='max-w-[500px] max-h-[400px]'
                        onLoad={handleImageLoad}
                    />
                </div>
</div>

I also using popup instead of inline but the marker positions are inconsistent. Would appreciate any guidance on this. TIA.

ailon commented 1 month ago

I haven't looked deeply enough but what rubs me a bit wrong on the first glance is that you are manually/explicitly setting width/height in the state before opening it with marker.js Live:

        const viewState: markerjs2.MarkerAreaState = {
            width: target.width,
            height: target.height,
            markers: allMarkers
        };

Not sure why you do that but this would mess up the internal scaling as it's done based on the width/height of the whole annotation in relation to the position and dimensions of individual markers.

SadmanYasar commented 1 month ago

I haven't looked deeply enough but what rubs me a bit wrong on the first glance is that you are manually/explicitly setting width/height in the state before opening it with marker.js Live:

        const viewState: markerjs2.MarkerAreaState = {
            width: target.width,
            height: target.height,
            markers: allMarkers
        };

Not sure why you do that but this would mess up the internal scaling as it's done based on the width/height of the whole annotation in relation to the position and dimensions of individual markers.

Thanks! This was definitely the issue. It's fixed after I removed the width and height for the viewState.

I'm just facing one issue which is after resizing the image from mobile to desktop, the markerjs live doesn't scale the markers, only after markerArea is reinitialised it gets updated in the markerjs live. Is this because of the width set for the image? I tried setting width to full in the image but it doesn't seem to fix it.