Skip to content

3rChuss/react-canvas-masker

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

87 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

react-canvas-masker

๐Ÿ–Œ๏ธ A lightweight, flexible React component and hook for drawing and extracting masks from images using canvas. Perfect for AI workflows, in-browser image editing tools, and selective manipulation.


๐Ÿง  What is react-canvas-masker?

react-canvas-masker is a modern and actively maintained React library that allows users to draw freeform masks over images, extract those masked regions, and integrate with AI-powered image processing workflows or any kind of canvas-based editing tool.

Itโ€™s built as an enhanced fork of react-mask-editor, rewritten with:

  • โœ… Hook-based architecture
  • ๐Ÿ” Undo/redo support
  • ๐Ÿ”ง Flexible API
  • ๐Ÿงผ Clean and modern codebase

๐Ÿš€ Features

  • โœ… Draw 1-bit (black/white) masks over any image using a brush tool
  • ๐Ÿ” Undo/redo and clear support
  • ๐ŸŽจ Customizable brush: size, color, opacity, blend mode
  • ๐Ÿ” Zoom and pan capabilities for precise mask editing
  • ๐Ÿ–ฑ๏ธ Intuitive controls: mouse wheel zoom, space+drag panning
  • ๐Ÿ“ฆ Use as a component, hook, or via React context
  • โšก Imperative API via ref
  • ๐Ÿ“ฑ Responsive design that adapts to container size
  • ๐Ÿงช Local demo/example app included

๐Ÿ“† Installation

npm install react-canvas-masker
# or
yarn add react-canvas-masker

๐Ÿ‘จโ€๐Ÿ’ผ Basic Usage โ€“ React Component

import React from 'react';
import { MaskEditor, toMask } from 'react-canvas-masker';

const MyComponent = () => {
  const canvas = React.useRef(null);
  return (
    <>
      <MaskEditor src="https://placekitten.com/256/256" canvasRef={canvas} />
      <button
        onClick={() => {
          if (canvas.current?.maskCanvas) {
            console.log(toMask(canvas.current.maskCanvas));
          }
        }}
      >
        Get Mask
      </button>
    </>
  );
};

Pre-loading an Existing Mask

You can resume editing from a previously saved mask by passing it as the initialMask prop:

import React from 'react';
import { MaskEditor, toMask } from 'react-canvas-masker';

const MyComponent = () => {
  const canvas = React.useRef(null);
  const [savedMask, setSavedMask] = React.useState(null);

  return (
    <>
      <MaskEditor 
        src="https://placekitten.com/256/256" 
        canvasRef={canvas}
        initialMask={savedMask} // Load previously saved mask
        onMaskChange={(mask) => {
          // Auto-save mask on changes
          localStorage.setItem('myMask', mask);
        }}
      />
      <button
        onClick={() => {
          if (canvas.current?.maskCanvas) {
            const mask = toMask(canvas.current.maskCanvas);
            setSavedMask(mask);
            localStorage.setItem('myMask', mask);
          }
        }}
      >
        Save Mask
      </button>
      <button
        onClick={() => {
          const loadedMask = localStorage.getItem('myMask');
          if (loadedMask) {
            setSavedMask(loadedMask);
          }
        }}
      >
        Load Saved Mask
      </button>
    </>
  );
};

โš™๏ธ Component Props

Prop Type Required Default Description
src string Yes โ€” Source URL of the image to edit.
cursorSize number No 10 Radius (in pixels) of the brush for editing the mask.
onCursorSizeChange (size: number) => void No โ€” Callback when the user changes the brush size via mouse wheel.
maskOpacity number No 0.4 CSS opacity, decimal between 0โ€“1.
maskColor string No #ffffff Hex color (with or without leading '#') for the mask.
maskBlendMode `"normal" "multiply" "screen" "overlay" "darken" "lighten" "color-dodge" "color-burn" "hard-light" "soft-light" "difference" "exclusion" "hue" "saturation" "color" "luminosity"` No normal CSS blending mode for the mask layer.
onDrawingChange (isDrawing: boolean) => void Yes โ€” Called when the user starts or stops drawing.
maxWidth number No 1240 Maximum width for loaded images. Images larger than this will be scaled down automatically.
maxHeight number No 1240 Maximum height for loaded images. Images larger than this will be scaled down automatically.
crossOrigin string No โ€” Value for the crossOrigin attribute on the underlying <img>. Useful for CORS images.
onUndoRequest () => void No โ€” Called when the user requests an undo action.
onRedoRequest () => void No โ€” Called when the user requests a redo action.
onMaskChange (mask: string) => void No โ€” Called with the current mask (as a dataURL) when the mask changes. Debounced while drawing.
initialMask string No โ€” Pre-load an existing mask as base64 data URL. Useful for resuming editing from a saved state.
scale number No 1 Initial zoom scale for the image editor.
minScale number No 0.8 Minimum allowed zoom scale.
maxScale number No 4 Maximum allowed zoom scale.
onScaleChange (scale: number) => void No โ€” Callback when the zoom scale changes.
enableWheelZoom boolean No true Enable/disable zooming with the mouse wheel.
onPanChange (x: number, y: number) => void No โ€” Callback when the pan position changes.
constrainPan boolean No true Enable/disable constraints that keep the image in view while panning.

๐Ÿงฉ Ref API (MaskEditorCanvasRef)

The MaskEditor component exposes useful methods via ref:

Name Type Description
maskCanvas `HTMLCanvasElement null` The mask canvas element.
undo() () => void Undo the last mask change.
redo() () => void Redo the last undone mask change.
clear() () => void Clear the mask.
resetZoom() () => void Reset zoom to initial scale and center the image.
setPan() (x: number, y: number) => void Set the pan position manually.
zoomIn() () => void Zoom in by one step (0.1 scale increment).
zoomOut() () => void Zoom out by one step (0.1 scale decrement).

๐Ÿงช Advanced Usage

Using the useMaskEditor hook

You can manage the full mask editing flow yourself:

const CustomMaskEditor = () => {
  const {
    canvasRef,
    clear,
    cursorCanvasRef,
    handleMouseDown,
    handleMouseUp,
    key,
    maskBlendMode,
    maskCanvasRef,
    maskOpacity,
    redo,
    transform,
    effectiveScale,
    size,
    undo,
    containerRef,
    resetZoom,
    isPanning,
    setPan,
  } = useMaskEditor({
    src: 'https://placekitten.com/256/256',
    maskColor: '#00ff00',
    maxWidth: 1024, // Optional: limit image width
    maxHeight: 1024, // Optional: limit image height
    onDrawingChange: (drawing) => console.log(drawing),
    // Zoom and pan options
    scale: 1, // Initial scale
    minScale: 0.5, // Minimum zoom allowed
    maxScale: 5, // Maximum zoom allowed
    enableWheelZoom: true, // Enable mouse wheel zoom
    constrainPan: true, // Keep image in view while panning
    onScaleChange: (newScale) => console.log(`Zoom level: ${newScale}`),
    onPanChange: (x, y) => console.log(`Pan position: ${x}, ${y}`),
  });

  const transformStyle = React.useMemo(() => {
    return {
      position: 'absolute' as const,
      top: '50%',
      left: '50%',
      transform: `translate(-50%, -50%) scale(${effectiveScale}) translate(${transform.translateX}px, ${transform.translateY}px)`,
      transformOrigin: 'center',
      transition: isPanning ? 'none' : 'transform 0.15s ease-out',
      width: size.x + 'px',
      height: size.y + 'px',
      display: 'block',
    };
  }, [transform, effectiveScale, isPanning, size]);

  return (
    <div
      className="react-mask-editor-outer"
      style={{
        maxWidth: `${1024}px`,
        maxHeight: `${1024}px`,
        minHeight: '300px',
        width: '100%',
        height: '100%',
      }}
      tabIndex={0}
    >
      <div className="controls">
        <button onClick={undo}>Undo</button>
        <button onClick={redo}>Redo</button>
        <button onClick={clear}>Clear</button>
        <button onClick={resetZoom}>Reset Zoom</button>
        <button onClick={() => setPan(0, 0)}>Center Image</button>
      </div>
      <div
        className="react-mask-editor-inner"
        ref={containerRef}
        style={{
          width: '100%',
          height: '100%',
          overflow: 'hidden',
          position: 'relative',
          display: 'flex',
          justifyContent: 'center',
          alignItems: 'center',
        }}
      >
        <div
          className="canvas-container"
          style={{
            position: 'relative',
            maxWidth: '100%',
            maxHeight: '100%',
            width: '100%',
            height: '100%',
            minHeight: '200px',
            overflow: 'hidden',
          }}
        >
          <div className="all-canvases" style={transformStyle}>
            <canvas
              key={key}
              ref={canvasRef}
              style={{
                width: size.x,
                height: size.y,
              }}
              width={size.x}
              height={size.y}
              className="react-mask-editor-base-canvas"
            />
            <canvas
              ref={maskCanvasRef}
              width={size.x}
              height={size.y}
              style={{
                width: size.x,
                height: size.y,
                opacity: maskOpacity,
                mixBlendMode: maskBlendMode as any,
              }}
              className="react-mask-editor-mask-canvas"
            />
            <canvas
              ref={cursorCanvasRef}
              width={size.x}
              height={size.y}
              onMouseUp={handleMouseUp}
              onMouseDown={handleMouseDown}
              style={{
                width: size.x,
                height: size.y,
                cursor: isPanning ? 'grabbing' : 'default',
              }}
              className="react-mask-editor-cursor-canvas"
            />
          </div>
        </div>
      </div>
    </div>
  );
};

Using MaskEditorProvider context

Ideal if you want to split canvas and controls across components:

import { MaskEditorProvider, useMaskEditorContext } from 'react-canvas-masker';

const MaskEditorCanvas = () => {
  const {
    canvasRef,
    maskCanvasRef,
    cursorCanvasRef,
    containerRef,
    size,
    transform,
    isPanning,
    handleMouseDown,
    handleMouseUp,
  } = useMaskEditorContext();

  return (
    <div
      ref={containerRef}
      style={{ width: '100%', height: '500px', position: 'relative' }}
    >
      <div
        style={{
          position: 'absolute',
          top: '50%',
          left: '50%',
          transform: `translate(-50%, -50%) scale(${transform.scale}) translate(${transform.translateX}px, ${transform.translateY}px)`,
          transition: isPanning ? 'none' : 'transform 0.15s ease-out',
        }}
      >
        <canvas ref={canvasRef} width={size.x} height={size.y} />
        <canvas ref={maskCanvasRef} width={size.x} height={size.y} />
        <canvas
          ref={cursorCanvasRef}
          width={size.x}
          height={size.y}
          onMouseDown={handleMouseDown}
          onMouseUp={handleMouseUp}
        />
      </div>
    </div>
  );
};

const MaskEditorControls = () => {
  const { undo, redo, clear, resetZoom, setPan, scale, zoomIn, zoomOut } =
    useMaskEditorContext();

  return (
    <div className="controls">
      <button onClick={undo}>Undo</button>
      <button onClick={redo}>Redo</button>
      <button onClick={clear}>Clear</button>
      <button onClick={zoomIn}>Zoom In</button>
      <button onClick={zoomOut}>Zoom Out</button>
      <button onClick={resetZoom}>Reset Zoom</button>
      <button onClick={() => setPan(0, 0)}>Center Image</button>
      <div>Current Zoom: {Math.round(scale * 100)}%</div>
    </div>
  );
};

const App = () => (
  <MaskEditorProvider
    src="https://placekitten.com/256/256"
    maxWidth={1024} // Optional: limit image width
    maxHeight={1024} // Optional: limit image height
    crossOrigin="anonymous" // Optional: set crossOrigin for CORS
    onDrawingChange={() => {}}
    // Zoom and pan options
    scale={1}
    minScale={0.5}
    maxScale={5}
    enableWheelZoom={true}
    constrainPan={true}
    onScaleChange={(scale) => console.log(`Zoom: ${scale}`)}
    onPanChange={(x, y) => console.log(`Pan: ${x}, ${y}`)}
  >
    <MaskEditorCanvas />
    <MaskEditorControls />
  </MaskEditorProvider>
);

๐Ÿ” Zoom and Pan Features

The editor includes sophisticated zoom and pan capabilities to enable precise mask editing:

User Interactions

  • Zoom: Use Ctrl/Cmd + Mouse Wheel to zoom in/out centered on image
  • Pan: Hold Space and drag to pan the image, or use middle mouse button
  • Resize Brush: Use Mouse Wheel (without modifier keys) to adjust brush size

Zoom Control API

The editor now provides explicit zoom control methods through the imperative API:

  • zoomIn(): Increases zoom by 0.1 scale increment (respects maxScale limit)
  • zoomOut(): Decreases zoom by 0.1 scale decrement (respects minScale limit)
  • resetZoom(): Resets zoom to scale 1 and centers the image
  • setPan(x, y): Manually sets the pan position

These methods can be accessed through:

  • Component ref: maskEditorRef.current.zoomIn()
  • Context: const { zoomIn } = useMaskEditorContext()
  • Hook: const { zoomIn } = useMaskEditor(props)

Perfect for implementing custom toolbar zoom controls with buttons or sliders!

Automatic Behaviors

  • Responsive Scaling: Images automatically scale to fit their container
  • Smooth Transitions: Gentle animations when zooming (disabled during active panning)
  • Position Constraints: Optional boundaries prevent the image from being panned too far out of view
  • Centered Reset: resetZoom() function centers the image and resets scale to 1

Programmatic Control

// Example of programmatically controlling zoom and pan
const CustomZoomControls = () => {
  const maskEditorRef = React.useRef(null);

  return (
    <>
      <button onClick={() => maskEditorRef.current?.zoomIn()}>Zoom In</button>
      <button onClick={() => maskEditorRef.current?.zoomOut()}>Zoom Out</button>
      <button onClick={() => maskEditorRef.current?.resetZoom()}>
        Reset Zoom & Center
      </button>
      <button onClick={() => maskEditorRef.current?.setPan(50, 20)}>
        Move to Position
      </button>
    </>
  );
};

๐Ÿ’ก Use Cases

react-canvas-masker is great for:

  • โœจ AI image editing apps (e.g. Stable Diffusion, DALLยทE, Sora, etc.)
  • ๐Ÿ”ง Web-based design tools (like Figma clones or mockup tools)
  • ๐Ÿ“ Educational tools where users interact with images
  • ๐Ÿ”ฎ Selective filtering or redacting images (blur, crop, etc.)
  • ๐Ÿš€ Creative playgrounds or generative UIs

๐Ÿ“œ Notes

  • All mask operations are done on a separate canvas for performance
  • The mask is returned as a black-and-white PNG (base64)
  • Supports up to 50 undo/redo steps
  • Forked and modernized from react-mask-editor

๐Ÿ“– License

MIT


๐Ÿ™Œ About This Fork

This is a cleaned-up and improved version of an unmaintained package, refactored into a hook-first, React 18+ friendly library with a focus on AI tooling and performance. Key enhancements include:

  • Advanced zoom and pan capabilities for precise editing
  • Optimized event handling and rendering
  • Responsive design that adapts to container dimensions
  • Improved coordinate calculations for pixel-perfect precision
  • Enhanced user controls with intuitive keyboard and mouse interactions

About

A modern, flexible React component and hook for image mask editing.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors