ailon / markerjs2

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

How to scale markers upon image resize? #194

Open SadmanYasar opened 1 day ago

SadmanYasar commented 1 day 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 21 hours 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.