react-image-annotate-mngo
v1.0.102
Published
A high-performance, responsive, canvas and DOM-hybrid image annotation library built for React. It allows users to place custom icon/text elements, free-hand draw with a responsive pencil tool, manipulate elements (drag, resize, rotate, copy/paste), and u
Readme
react-image-annotate-mngo
A high-performance, responsive, canvas and DOM-hybrid image annotation library built for React. It allows users to place custom icon/text elements, free-hand draw with a responsive pencil tool, manipulate elements (drag, resize, rotate, copy/paste), and undo/redo operations with boundary clamping.
Features
- Interactive & Read-Only Modes: Supports a clean
readonlyviewer state that disables interaction handlers, toolbars, and selection overlays for view-only display. - Extensible Tool Registry: Supports custom tool definitions and SVG stamps via the
toolsprop for specialized diagramming workflows. - Resolution-Independent Coordinates: Stored annotations use normalized base coordinates, ensuring saved data renders identically across different screen resolutions and aspect ratios.
- Boundary Clamping: All drag, resize, stamp, paste, and draw gestures are strictly bounded to the image canvas, preventing annotations from getting pushed off-screen.
- Undo & Redo Stack: Complete immutable snapshot history with dedicated toolbar controls and hotkeys.
- Keyboard Shortcuts: Built-in hotkeys for Undo (Ctrl/Cmd+Z), Redo (Ctrl/Cmd+Shift+Z / Ctrl+Y), Copy/Paste (Ctrl/Cmd+C, Ctrl/Cmd+V), and Delete (Delete / Backspace), with smart input focus suppression.
- Universal Pointer Event Handling: Utilizes
pointerdown,pointermove, andpointerupto deliver uniform gesture interactions across mouse, touchscreens, and styluses. - GPU-Accelerated CSS Transforms: Elements use
transform: translate3d(...) rotate(...)instead oftop/leftlayout properties, leveraging GPU compositing for 60fps rendering without layout reflows. - Direct DOM Manipulation with Commit-on-Release: Dragging, rotating, and resizing modify CSS variables directly on the element's DOM node in real-time, bypassing React re-renders during interactions and committing to the store only on pointer release (
pointerup). - Frame-Synced Updates with
requestAnimationFrame: Interactive gesture pipelines throttle visual DOM updates to the display's refresh rate viarequestAnimationFrame. - Atomic State Management & Granular Subscriptions: Each annotation element subscribes strictly to its own ID via
useSyncExternalStore, ensuring modifying or selecting one item never triggers re-renders on other items. - CSS Variable-Driven Container Scaling: Scale factors (
--scale-x,--scale-y) are stored as CSS custom properties on the root container, allowing all annotations and strokes to scale instantly on window or container resize without iterating over individual elements. - Zero-Config CSS Injection: Component styles are bundled and injected automatically via JavaScript runtime — no external CSS imports required.
- Fully Typed: Written in TypeScript with complete declarations (
.d.ts) exported out of the box.
📦 Installation
npm install react-image-annotate-mngo
# or
yarn add react-image-annotate-mngo
# or
pnpm add react-image-annotate-mngoPeer Dependencies
react>= 18.0.0 (Supports React 18 & 19)react-dom>= 18.0.0
🚀 Quick Start
import { useState } from 'react';
import MNgoImageAnnotate, { type AnnotationData } from 'react-image-annotate-mngo';
export default function App() {
const [annotationData, setAnnotationData] = useState<AnnotationData>({
annotationIds: [],
annotations: {},
highestZIndex: 0,
});
return (
<div style={{ width: '100%', height: '80vh' }}>
<MNgoImageAnnotate
imgSrc="https://images.unsplash.com/photo-1579783902614-a3fb3927b675?auto=format&fit=crop&w=1200&q=80"
annotationData={annotationData}
onAnnotationDataChange={setAnnotationData}
/>
</div>
);
}Note: Give the parent container an explicit height (e.g.
height: 80vhorh-screen) so the component can expand smoothly to fit your layout.
🛠️ Adding Custom Tools / Shapes
You can extend the toolbar with your own custom tools or icon stamps using the tools prop:
import MNgoImageAnnotate, { type Tool } from 'react-image-annotate-mngo';
const CUSTOM_TOOLS: Tool[] = [
{
name: 'server',
btnIcon: <ServerIcon className="w-5 h-5" />, // Displayed in the toolbar
elementIcon: <ServerIcon className="w-full h-full text-indigo-600" />, // Displayed when placed on the canvas
},
{
name: 'database',
btnIcon: <DatabaseIcon className="w-5 h-5" />,
elementIcon: <DatabaseIcon className="w-full h-full text-emerald-600" />,
},
];
export default function CustomToolsExample() {
return (
<div style={{ height: '100vh' }}>
<MNgoImageAnnotate
imgSrc="/architecture.png"
tools={CUSTOM_TOOLS}
/>
</div>
);
}📖 Component Props
| Prop | Type | Default | Description |
|---|---|---|---|
| imgSrc | string | undefined | Image URL or base64 data string to annotate. If omitted, renders an interactive grid fallback canvas. |
| readonly | boolean | false | When set to true, disables interaction, dragging, tools, and shortcuts (view-only mode). |
| tools | Tool[] | undefined | Custom tools/stamps added to the right side of the toolbar. |
| annotationData | AnnotationData | undefined | Controlled annotation data state. |
| onAnnotationDataChange | (data: AnnotationData) => void | undefined | Callback invoked whenever annotations are added, modified, moved, rotated, resized, or deleted. |
⌨️ Keyboard Shortcuts
| Shortcut | Action | Notes | |---|---|---| | Ctrl / Cmd + Z | Undo | Reverts previous annotation modification | | Ctrl / Cmd + Shift + Z | Redo | Also supports Ctrl + Y | | Ctrl / Cmd + C | Copy | Copies the currently selected element | | Ctrl / Cmd + V | Paste | Pastes the copied element with offset | | Delete / Backspace | Delete | Removes the currently selected element |
Shortcuts are automatically suppressed when typing inside <input>, <textarea>, or content-editable elements.
📐 TypeScript Interfaces
export interface Tool {
name: string;
btnIcon: ReactNode;
elementIcon?: ReactNode;
}
export interface AnnotationData {
annotationIds: string[];
annotations: Record<string, Annotation>;
highestZIndex: number;
}
export interface Annotation {
id: string;
name: string;
zIndex: number;
pos?: { x: number; y: number };
rotation?: number;
dimensions?: { width: number; height: number };
text?: string;
points?: number[][]; // Pencil stroke coordinates
}
export interface MNgoImageAnnotateProps {
readonly?: boolean;
imgSrc?: string;
tools?: Tool[];
annotationData?: AnnotationData;
onAnnotationDataChange?: (newData: AnnotationData) => void;
}🏛️ Architecture, Design Decisions & Trade-offs
- Semi-Controlled Hybrid Component: Annotation data lives inside an external library store, synchronized bidirectionally with the consumer app via
annotationDataandonAnnotationDataChangeprops. SupportsimgSrc,tools, andreadonlymodes. - Context Layering: Static configuration, props, global data, and refs (
compRootRef,bgRef) live in context to eliminate prop drilling across deeply nested canvas elements. - Dynamic Scaling Subsystem: A custom hook (
useContainerScaling) computes the scale factor (ratio between rendered and natural image dimensions:currDimn / baseDimn), driving global CSS variables (--scale-x,--scale-y) via aResizeObserveron the background element. - Resolution-Independent Base Coordinates: Element positions, dimensions, rotation, and freehand pencil paths are stored normalized to the background's natural dimensions. Visual positions are dynamically computed as
baseVal * scaleFactor. - Hybrid DOM + Canvas Rendering: HTML5 2D Canvas is used for continuous, high-frequency freehand drawing, while native DOM elements are used for selectable, editable shape/icon annotations and text boxes.
- Universal Pointer Engine: Unified gesture handling across mouse, touchscreen, and stylus using
pointerdown,pointermove, andpointerup. - 60/120 FPS Direct DOM Mutation: During active drag, resize, or rotate gestures, updates mutate CSS transform variables directly on the DOM node inside
requestAnimationFrame(bypassing React reconciliation and leveraging GPU compositing to avoid relayout/reflow and repaint cycles). State is committed to the store only on release (pointerup). - External Pub/Sub Store: Implemented an external reactive store integrated into React components via
useSyncExternalStore. - Fine-Grained Subscriptions: Each element component subscribes strictly to its individual annotation ID, preventing whole-stage re-renders when a single element moves.
- Memento-Pattern History: Implemented a
StateHistoryclass managing immutable snapshots (past, future) of annotation state for $O(1)$ undo and redo.
- Hybrid DOM + Canvas vs. Pure Canvas: Implementing selectable, rotatable, and editable annotations on pure Canvas requires custom hit-testing matrices, custom transform handles, synthetic text carets, and event management. The DOM handles accessibility, text input, and transforms natively.
- CSS Variables for Transforms: Direct CSS variable manipulation updates transforms directly on the GPU compositing layer without dirtying the React fiber tree.
- Element-Level
ResizeObservervs. Window Resize: The annotation canvas can resize due to sidebar toggles, flexbox shifts, or tab switches without a window resize event ever firing.ResizeObserveron the background element targets the exact source of truth. - Commit-on-Release vs. Continuous Store Dispatch: Decouples visual frame updates (120Hz) from the state persistence engine, keeping the history stack clean and eliminating store dispatch lag.
- Custom External Store vs. Redux/Zustand:
- Pros: Zero runtime dependencies; ultra-lightweight bundle with zero risk of peer-dependency conflicts.
- Cons: Required writing custom subscription, selector, and notification boilerplate.
- Hybrid DOM + Canvas vs. Pure Canvas:
- Pros: Native DOM accessibility, event management, and crisp SVG scaling.
- Cons: Higher memory footprint per element than canvas pixels; requires synchronizing two distinct rendering layers (canvas & annotation shapes DOM) on resize.
- Full Snapshot Memento vs. Delta/Diff Actions:
- Pros: Robust $O(1)$ time restoration without risk of state drift, missed actions, or complex inverse-matrix calculations.
- Cons: Higher memory usage due to object duplication (mitigated by capping the history stack to 30 snapshots).
- Direct DOM Mutation vs. Pure React Declarative Flow:
- Pros: Eliminates layout thrashing, virtual DOM diffing, and dropped frames during rapid pointer gestures.
- Cons: Temporarily deviates from React’s declarative single-source-of-truth model during active gestures until settled on
pointerup.
📄 License
MIT © Aditya Suman
