react-image-mask
Version:
A React component for creating image masks with drawing tools
583 lines (483 loc) • 16.7 kB
Markdown
# React Image Mask
A React component for creating image masks with drawing tools. Features include freehand drawing, box selection, polygon selection, eraser tools, and more.
## Installation
```bash
npm install react-image-mask
```
## Dependencies
This package has the following peer dependencies:
- `react` >= 16.8.0
- `react-dom` >= 16.8.0
## Usage
### CSS Import
**Important**: You need to import the CSS file for the component to display correctly:
```tsx
import 'react-image-mask/dist/index.css';
```
### Basic Usage
```tsx
import React from 'react';
import { ImageMask } from 'react-image-mask';
import 'react-image-mask/dist/index.css';
function App() {
return (
<div>
{/* Uses default placeholder image */}
<ImageMask />
</div>
);
}
```
### With Custom Image and Mask Callback
```tsx
import React, { useState } from 'react';
import { ImageMask } from 'react-image-mask';
import 'react-image-mask/dist/index.css';
function App() {
const [maskData, setMaskData] = useState<string | null>(null);
const handleMaskChange = (newMaskData: string | null) => {
setMaskData(newMaskData);
console.log('Mask updated:', newMaskData);
};
return (
<div>
<ImageMask
src="https://example.com/your-image.jpg"
onMaskChange={handleMaskChange}
maskColor="rgba(255, 0, 0, 1)"
opacity={0.7}
brushSize={15}
/>
{maskData && (
<div>
<h3>Mask Preview:</h3>
<img src={maskData} alt="Generated mask" style={{ maxWidth: '200px' }} />
</div>
)}
</div>
);
}
```
### Using Ref to Control the Component
```tsx
import React, { useRef } from 'react';
import { ImageMask, ImageMaskRef } from 'react-image-mask';
import 'react-image-mask/dist/index.css';
function App() {
const maskRef = useRef<ImageMaskRef>(null);
const handleDownload = () => {
const maskData = maskRef.current?.getMaskData();
if (maskData) {
const link = document.createElement('a');
link.href = maskData;
link.download = 'mask.png';
link.click();
}
};
const handleClear = () => {
maskRef.current?.clearMask();
};
return (
<div>
<ImageMask
ref={maskRef}
src="https://example.com/image.jpg"
onMaskChange={(maskData) => console.log('Mask changed:', maskData)}
onZoomChange={(zoom) => console.log('Zoom:', zoom)}
onHistoryChange={(canUndo, canRedo) => console.log('History:', { canUndo, canRedo })}
/>
<div>
<button onClick={handleDownload}>Download Mask</button>
<button onClick={handleClear}>Clear Mask</button>
<button onClick={() => maskRef.current?.undo()}>Undo</button>
<button onClick={() => maskRef.current?.redo()}>Redo</button>
</div>
</div>
);
}
```
### With Controls Configuration
```tsx
import React from 'react';
import { ImageMask, ControlsConfig } from 'react-image-mask';
import 'react-image-mask/dist/index.css';
function App() {
// Configure which controls to show
const controlsConfig: ControlsConfig = {
showDownloadButton: false, // Hide download button in controls
showClearButton: true,
showUndoRedo: true,
showToolButtons: true,
showBrushControls: true,
showColorControls: true,
showOpacityControls: true,
showZoomControls: true
};
return (
<div>
<ImageMask
src="https://example.com/image.jpg"
controlsConfig={controlsConfig}
maskColor="rgba(0, 255, 0, 1)" // Green mask
opacity={0.6}
brushSize={20}
/>
</div>
);
}
```
## Components
### ImageMask
The main component that includes both the canvas and controls. This is the primary component you'll use in most cases.
#### Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `src` | `string` | `"https://picsum.photos/1024/1024"` | Image source URL |
| `maskColor` | `string` | `"rgba(0, 0, 0, 1)"` | Initial mask color (RGBA format) |
| `opacity` | `number` | `0.5` | Initial opacity (0-1) |
| `brushSize` | `number` | `10` | Initial brush size in pixels |
| `controlsConfig` | `ControlsConfig` | See below | Configuration for which controls to show |
| `onMaskChange` | `(maskData: string \| null) => void` | `undefined` | Callback when mask changes |
| `onZoomChange` | `(zoom: number) => void` | `undefined` | Callback when zoom changes |
| `onHistoryChange` | `(canUndo: boolean, canRedo: boolean) => void` | `undefined` | Callback when history changes |
| `className` | `string` | `"tool-mode"` | Custom CSS class for the container |
| `ref` | `React.Ref<ImageMaskRef>` | `undefined` | Ref to control the component programmatically |
#### Default ControlsConfig
```typescript
{
showDownloadButton: true,
showClearButton: true,
showUndoRedo: true,
showToolButtons: true,
showBrushControls: true,
showColorControls: true,
showOpacityControls: true,
showZoomControls: true
}
```
#### Example Usage
```tsx
import React, { useRef, useState } from 'react';
import { ImageMask, ImageMaskRef, ControlsConfig } from 'react-image-mask';
import 'react-image-mask/dist/index.css';
function App() {
const maskRef = useRef<ImageMaskRef>(null);
const [maskData, setMaskData] = useState<string | null>(null);
const controlsConfig: ControlsConfig = {
showDownloadButton: false, // We'll handle download ourselves
showClearButton: true,
showUndoRedo: true,
showToolButtons: true,
showBrushControls: true,
showColorControls: true,
showOpacityControls: true,
showZoomControls: true
};
const handleDownload = () => {
const data = maskRef.current?.getMaskData();
if (data) {
const link = document.createElement('a');
link.href = data;
link.download = 'mask.png';
link.click();
}
};
return (
<div>
<ImageMask
ref={maskRef}
src="https://example.com/image.jpg"
maskColor="rgba(255, 0, 0, 1)"
opacity={0.7}
brushSize={15}
controlsConfig={controlsConfig}
onMaskChange={setMaskData}
onZoomChange={(zoom) => console.log('Zoom:', zoom)}
onHistoryChange={(canUndo, canRedo) => console.log('History:', { canUndo, canRedo })}
className="custom-image-mask"
/>
<button onClick={handleDownload}>Download Mask</button>
{maskData && (
<div>
<h3>Current Mask:</h3>
<img src={maskData} alt="Mask preview" style={{ maxWidth: '200px' }} />
</div>
)}
</div>
);
}
```
### ImageMaskCanvas
The canvas component for image masking. This is the low-level component that handles the actual drawing and image display.
#### Props
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `src` | `string` | Yes | Image source URL |
| `toolMode` | `ToolMode` | Yes | Current tool mode |
| `maskColor` | `string` | No | Mask color (RGBA format) |
| `width` | `number` | No | Canvas width |
| `height` | `number` | No | Canvas height |
| `opacity` | `number` | No | Mask opacity (0-1) |
| `onZoomChange` | `(zoom: number) => void` | No | Zoom change callback |
| `onHistoryChange` | `(canUndo: boolean, canRedo: boolean) => void` | No | History change callback |
| `ref` | `React.Ref<ImageMaskCanvasRef>` | No | Ref to control the canvas programmatically |
#### Example Usage
```tsx
import React, { useRef, useState } from 'react';
import { ImageMaskCanvas, ImageMaskCanvasRef, ToolMode } from 'react-image-mask';
import 'react-image-mask/dist/index.css';
function App() {
const canvasRef = useRef<ImageMaskCanvasRef>(null);
const [toolMode, setToolMode] = useState<ToolMode>('mask-freehand');
const [maskColor, setMaskColor] = useState('rgba(0, 0, 0, 1)');
const [opacity, setOpacity] = useState(0.5);
const [brushSize, setBrushSize] = useState(10);
const handleDownload = () => {
const maskData = canvasRef.current?.getMaskData();
if (maskData) {
const link = document.createElement('a');
link.href = maskData;
link.download = 'mask.png';
link.click();
}
};
return (
<div>
<div style={{ marginBottom: '10px' }}>
<button onClick={() => setToolMode('mask-freehand')}>Freehand</button>
<button onClick={() => setToolMode('mask-box')}>Box</button>
<button onClick={() => setToolMode('eraser-freehand')}>Eraser</button>
<button onClick={() => canvasRef.current?.clearMask()}>Clear</button>
<button onClick={handleDownload}>Download</button>
</div>
<div style={{ marginBottom: '10px' }}>
<label>
Color:
<input
type="color"
value={maskColor}
onChange={(e) => setMaskColor(e.target.value)}
/>
</label>
<label>
Opacity:
<input
type="range"
min="0"
max="1"
step="0.1"
value={opacity}
onChange={(e) => setOpacity(Number(e.target.value))}
/>
</label>
<label>
Brush Size:
<input
type="range"
min="1"
max="50"
value={brushSize}
onChange={(e) => setBrushSize(Number(e.target.value))}
/>
</label>
</div>
<ImageMaskCanvas
ref={canvasRef}
src="https://example.com/image.jpg"
toolMode={toolMode}
maskColor={maskColor}
opacity={opacity}
onZoomChange={(zoom) => console.log('Zoom:', zoom)}
onHistoryChange={(canUndo, canRedo) => console.log('History:', { canUndo, canRedo })}
/>
</div>
);
}
```
### ImageMaskControls
The controls component for tool selection and settings. This provides the UI for all the drawing tools and settings.
#### Props
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `setToolMode` | `(toolMode: ToolMode) => void` | Yes | Function to set the current tool mode |
| `toolMode` | `ToolMode` | Yes | Current tool mode |
| `clearCanvas` | `() => void` | No | Function to clear the canvas |
| `currentZoom` | `number` | No | Current zoom level |
| `undo` | `() => void` | No | Function to undo last action |
| `redo` | `() => void` | No | Function to redo last action |
| `canUndo` | `boolean` | No | Whether undo is available |
| `canRedo` | `boolean` | No | Whether redo is available |
| `onDownloadMask` | `() => void` | No | Function to download the mask |
| `setMaskColor` | `(color: string) => void` | No | Function to set mask color |
| `currentMaskColor` | `string` | No | Current mask color |
| `setOpacity` | `(opacity: number) => void` | No | Function to set opacity |
| `currentOpacity` | `number` | No | Current opacity |
| `setBrushSize` | `(size: number) => void` | No | Function to set brush size |
| `currentBrushSize` | `number` | No | Current brush size |
| `setZoom` | `(zoom: number) => void` | No | Function to set zoom |
| `controlsConfig` | `ControlsConfig` | No | Configuration for which controls to show |
#### Example Usage
```tsx
import React, { useState } from 'react';
import { ImageMaskControls, ToolMode, ControlsConfig } from 'react-image-mask';
import 'react-image-mask/dist/index.css';
function App() {
const [toolMode, setToolMode] = useState<ToolMode>('mask-freehand');
const [maskColor, setMaskColor] = useState('rgba(0, 0, 0, 1)');
const [opacity, setOpacity] = useState(0.5);
const [brushSize, setBrushSize] = useState(10);
const [zoom, setZoom] = useState(100);
const [canUndo, setCanUndo] = useState(false);
const [canRedo, setCanRedo] = useState(false);
const controlsConfig: ControlsConfig = {
showDownloadButton: true,
showClearButton: true,
showUndoRedo: true,
showToolButtons: true,
showBrushControls: true,
showColorControls: true,
showOpacityControls: true,
showZoomControls: true
};
const handleClearCanvas = () => {
// Implement clear canvas logic
console.log('Clear canvas');
};
const handleUndo = () => {
// Implement undo logic
console.log('Undo');
};
const handleRedo = () => {
// Implement redo logic
console.log('Redo');
};
const handleDownloadMask = () => {
// Implement download logic
console.log('Download mask');
};
return (
<div>
<ImageMaskControls
setToolMode={setToolMode}
toolMode={toolMode}
clearCanvas={handleClearCanvas}
currentZoom={zoom}
undo={handleUndo}
redo={handleRedo}
canUndo={canUndo}
canRedo={canRedo}
onDownloadMask={handleDownloadMask}
setMaskColor={setMaskColor}
currentMaskColor={maskColor}
setOpacity={setOpacity}
currentOpacity={opacity}
setBrushSize={setBrushSize}
currentBrushSize={brushSize}
setZoom={setZoom}
controlsConfig={controlsConfig}
/>
<div style={{ marginTop: '20px' }}>
<p>Current Tool: {toolMode}</p>
<p>Current Color: {maskColor}</p>
<p>Current Opacity: {opacity}</p>
<p>Current Brush Size: {brushSize}px</p>
<p>Current Zoom: {zoom}%</p>
</div>
</div>
);
}
```
## Types
### ToolMode
```typescript
type ToolMode = 'move' | 'mask-freehand' | 'mask-box' | 'mask-polygon' | 'eraser-freehand' | 'eraser-box' | 'clear';
```
### ControlsConfig
```typescript
interface ControlsConfig {
showDownloadButton?: boolean;
showClearButton?: boolean;
showUndoRedo?: boolean;
showToolButtons?: boolean;
showBrushControls?: boolean;
showColorControls?: boolean;
showOpacityControls?: boolean;
showZoomControls?: boolean;
}
```
### ImageMaskRef
```typescript
interface ImageMaskRef {
getMaskData: () => string | null;
clearMask: () => void;
undo: () => void;
redo: () => void;
}
```
### ImageMaskCanvasRef
```typescript
interface ImageMaskCanvasRef {
getMaskData: () => string | null;
clearMask: () => void;
undo: () => void;
redo: () => void;
setToolMode: (mode: ToolMode) => void;
setMaskColor: (color: string) => void;
setOpacity: (opacity: number) => void;
setBrushSize: (size: number) => void;
canUndo: boolean;
canRedo: boolean;
setZoom: (zoomPercentage: number) => void;
}
```
## Features
- **Multiple Drawing Tools**: Freehand, box selection, polygon selection
- **Eraser Tools**: Freehand and box eraser
- **Zoom Controls**: Zoom in/out with mouse wheel or controls
- **Touch Gestures**: Full iPad/mobile support with pinch-to-zoom and pan gestures
- **Apple Pencil Support**: Works seamlessly with Apple Pencil on iPad
- **History**: Undo/redo functionality
- **Customizable**: Adjustable brush size, opacity, and colors
- **Download**: Export mask as PNG
- **TypeScript**: Full TypeScript support
- **Flexible Controls**: Show/hide specific control sections
- **Responsive**: Automatically scales images to fit container
## Touch & Mobile Support
The component provides full touch gesture support for iPad and mobile devices:
### 📱 **Supported Gestures**
- **Single Touch Drawing**: Use finger or Apple Pencil for all drawing tools
- **Two-Finger Zoom**: Pinch gesture for zooming in/out (100%-1000%)
- **Two-Finger Pan**: Drag with two fingers to pan around zoomed images (works in any tool mode)
- **Apple Pencil**: Full precision support for Apple Pencil on iPad
### 🎯 **How to Use on Mobile**
1. **Drawing**: Use any drawing tool and draw with finger or stylus
2. **Zooming**: Use two fingers to pinch zoom in/out on the canvas
3. **Panning**: Use two fingers to drag and pan around (no need to switch tools)
4. **Combined**: You can zoom and pan simultaneously with two fingers
5. **Tools**: All tools work with touch - freehand, box, polygon, eraser
### ⚙️ **Technical Details**
- **Touch Conflict Prevention**: Drawing only occurs with single touch to avoid accidental marks during navigation
- **Native iOS Feel**: Two-finger zoom and pan gestures work like standard iOS apps (Photos, Maps, etc.)
- **Simultaneous Gestures**: Can zoom and pan at the same time with two fingers
- **Browser Zoom Disabled**: `touchAction: 'none'` prevents browser zoom interference
- **Smart Gesture Detection**: Distinguishes between single-touch drawing and two-finger navigation
### 📋 **Browser Support**
- ✅ Safari on iPad/iPhone
- ✅ Chrome on Android
- ✅ Edge on Surface devices
- ✅ Any modern mobile browser with touch support
## Development
To run the development environment:
```bash
npm run dev
```
To build the library:
```bash
npm run build
```
To run Storybook:
```bash
npm run storybook
```
## License
MIT