Framework-agnostic image selection tools — rectangles, ellipses, lasso (free select), color select, magic wand, and bitmap masks.
- Draw rect/ellipse selections by dragging
- Lasso (free select) tool: click to place polygonal nodes, drag to draw freehand curves, mix both freely
- Color select and magic wand tools for pixel-based selection
- Resize and move committed selections via handles
- Bitmap mask mode with Shift (add) / Ctrl (subtract) modifiers
- Shared selection state across all tools (GIMP-like behavior)
- Animated marching-ants canvas overlay
- Automatic cursor management
- Copy selection to clipboard (Ctrl+C / Cmd+C)
- Works with any framework — React, Vue, Svelte, vanilla JS, etc.
npm install selection-toolsWrap an <img> in a container div, then call mount():
<div id="container">
<img src="photo.jpg" />
</div>import { SelectionToolkit } from "selection-tools";
const toolkit = new SelectionToolkit({
onChange(state) {
console.log(state.activeTool, state.mask);
},
});
const cleanup = toolkit.mount(document.getElementById("container"));
toolkit.setTool("magic-wand");
toolkit.setTolerance(20);
toolkit.clearSelection();
// Tear down when done
cleanup();mount() handles everything: finds the <img>, creates and positions the overlay canvas, applies required styles, binds all events, and auto-updates the cursor.
| Option | Type | Default | Description |
|---|---|---|---|
onChange |
(state: ToolkitState) => void |
required | Called after every state mutation |
tool |
ToolType |
"rect" |
Initial tool |
tolerance |
number |
10 |
Color tolerance for pixel tools (0–100) |
zoom |
number |
1 |
Display zoom factor |
getBounds |
() => { w, h } | null |
auto | Drawable area dimensions; defaults to image client size |
| Field | Type | Description |
|---|---|---|
activeTool |
ToolType |
Current tool |
cursorStyle |
string |
CSS cursor value (auto-applied by mount()) |
mask |
SelectionMask | null |
The shared bitmap mask |
maskOutlines |
string[] |
SVG path d strings for the mask boundary |
maskVersion |
number |
Increments on every mask change |
selection |
SelRect | null |
Active rect/ellipse in display coordinates |
dragPhase |
DragPhase |
"idle" / "drawing" / "committed" |
handleLayout |
HandleLayout | null |
Pre-computed resize handle rects |
lassoNodes |
{ x, y }[] |
Lasso polygon vertices |
lassoPhase |
string |
"idle" / "placing" / "committed" |
| Method | Description |
|---|---|
mount(container) |
All-in-one setup. Returns cleanup function. |
setTool(tool) |
Switch to "rect" / "ellipse" / "freehand" / "color-select" / "magic-wand" |
setTolerance(n) |
Set pixel tool tolerance (0–100) |
clearSelection() |
Clear the entire selection and mask |
getState() |
Get the current ToolkitState snapshot |
getSelectedPixels() |
Extract selected pixels as cropped RGBA data |
copySelection() |
Copy selection to clipboard as PNG |
configure(opts) |
Update getBounds, zoom, or tolerance |
setMask(mask) |
Programmatically set the selection mask |
computeOutlines() |
Recompute mask outlines (returns true if changed) |
transformSelection(fn) |
Transform the active rect/ellipse selection |
For cases where mount() doesn't fit (e.g. you manage the canvas yourself), use the individual methods:
const toolkit = new SelectionToolkit({
onChange(state) {
/* ... */
},
});
const cleanups = [
toolkit.attachImage(imgElement),
toolkit.bindEvents(containerElement),
toolkit.attachCanvas(canvasElement),
];
// Tear down
cleanups.forEach((fn) => fn());
toolkit.destroy();Click and drag to draw a selection. After committing, resize via handles or move by dragging inside.
| Action | Effect |
|---|---|
| Click | Place a polygonal anchor node |
| Click + drag | Draw a freehand curve (auto-simplified on release) |
| Click on start node | Close the polygon and commit to mask |
| Double-click | Close with a straight line back to start and commit |
| Enter | Commit the current path to mask |
| Backspace | Undo the last action (node or freehand stroke) |
| Escape | Cancel and clear the path |
| Drag an anchor node | Reposition the node; adjacent freehand curves deform |
Click on a pixel to select all pixels of that color across the entire image.
Click on a pixel to flood-fill select contiguous pixels within the tolerance.
Hold Shift while clicking/drawing to add to the selection, or Ctrl to subtract. Works across all tools.
The toolkit uses two coordinate systems:
- Display coordinates — scaled by the current zoom factor. UI events,
SelRect, and handle positions all use display coordinates. - Native coordinates — 1:1 with image pixels. The bitmap mask and pixel tools (
colorSelect,magicWand) operate in native coordinates.
Use toNativeRect(rect, zoom) to convert a display-coordinate rectangle to native coordinates before applying it to a mask.
import {
createMask,
maskApplyRect,
maskApplyEllipse,
maskApplyPolygon,
toNativeRect,
extractMaskedPixels,
extractOutlines,
} from "selection-tools";
const mask = createMask(imageWidth, imageHeight);
// Convert display rect to native coords before masking
const nativeRect = toNativeRect(displaySelection, zoom);
maskApplyRect(mask, nativeRect, "replace");
const pixels = extractMaskedPixels(mask, ctx.getImageData(0, 0, w, h).data);
const paths = extractOutlines(mask);| Mask mode | Effect |
|---|---|
"replace" |
Clears the mask, then sets the new shape |
"add" |
Union — adds the shape to the existing mask |
"subtract" |
Difference — removes the shape from the existing mask |
For maximum control, use SelectionController and LassoController directly:
import { SelectionController, LassoController } from "selection-tools";
const ctrl = new SelectionController({
getBounds: () => ({ w: canvas.width, h: canvas.height }),
onChange: (state) => render(state),
scheduleFrame: null,
});
canvas.addEventListener("mousedown", (e) => {
ctrl.handlePointerDown({
x: e.offsetX,
y: e.offsetY,
button: e.button,
shiftKey: e.shiftKey,
ctrlKey: e.ctrlKey,
});
});
window.addEventListener("mousemove", (e) => {
ctrl.handlePointerMove({
x: e.offsetX,
y: e.offsetY,
button: e.button,
shiftKey: e.shiftKey,
ctrlKey: e.ctrlKey,
});
});
window.addEventListener("mouseup", () => ctrl.handlePointerUp());Working examples are in examples/. Each is a self-contained Vite project:
cd examples/<framework>
npm install
npm run dev| Framework | Directory | Description |
|---|---|---|
| React | examples/react/ |
All five tools with SelectionToolkit |
| Vanilla JS | examples/vanilla/ |
All five tools, no framework |
| Vue 3 | examples/vue/ |
Composition API with <script setup> |
| Svelte 5 | examples/svelte/ |
Runes ($state) |
All examples use SelectionToolkit.mount() for setup.
MIT
