npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

react-canvas-masker

v1.1.1

Published

General-purpose mask editor for React image manipulation apps

Readme

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>
    </>
  );
};

⚙️ 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. | | | | | | | | | | | | | | | | | 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