selection-tools
v0.2.0
Published
Selection tools — rect, ellipse, lasso, color select, magic wand, and bitmap masks. Framework-agnostic.
Maintainers
Readme
selection-tools
Framework-agnostic image selection tools — rectangles, ellipses, lasso (free select), color select, magic wand, and bitmap masks.

Features
- 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.
Install
npm install selection-toolsQuick start — mount()
Wrap 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.
SelectionToolkitOptions
| 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 |
ToolkitState
| 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" |
SelectionToolkit methods
| 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 |
Advanced setup
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();Tools
Rectangle & Ellipse
Click and drag to draw a selection. After committing, resize via handles or move by dragging inside.
Lasso (free select)
| 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 |
Color Select
Click on a pixel to select all pixels of that color across the entire image.
Magic Wand
Click on a pixel to flood-fill select contiguous pixels within the tolerance.
Modifiers
Hold Shift while clicking/drawing to add to the selection, or Ctrl to subtract. Works across all tools.
Coordinate systems
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.
Bitmap mask utilities
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 |
Low-level controllers
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());Examples
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.
License
MIT
