Features

The whole list, straight from the repo's own docs — 48 of them. The engine ones run on your machine; the interface ones are what you touch. Pick a group, or just scroll.

Image Processing (Rust/WASM)

Clone Stamp

Alt+Click source, paint to clone with adjustable size, hardness, opacity, spacing

Red Stamps

REJECTED / APPROVED / DRAFT / CONFIDENTIAL / UNDER REVIEW presets; JS renders label to OffscreenCanvas, Rust scales to brush size via bilinear resize and composites with "Red Stamp" history entry

Edit group (Crop · Transform · Color Picker · Resize Layer · Canvas Size · Guides)

each is its own sub-tool with its own panel section and its own URL, split out of the single combined tool they used to share. Crop: interactive SVG overlay with rule-of-thirds guides and 8 draggable resize handles, committed through Rust. Transform: flip horizontal/vertical, rotate 90° CW/CCW. Align: six buttons snap a selected text/shape's bounding box to any canvas edge or center, computed in Rust (align_annotation) as a single undo step. Selection Marker (above Align): a magic-wand flood-select — click a region to select pixels within a colour tolerance and delete them; the flood-fill, mask, and delete are all Rust (magic_wand_select / delete_selection), with Alt+A select-all and Alt+D deselect

Text Shadow

soft drop shadow on the text glyphs and/or the background box, in the Text panel below the colour swatch: shared colour / opacity / offset / blur, rendered in Rust (offset + box-blurred silhouette, tile grown to fit so Align sees it) and persisted with the edit

Security tab

EXIF keep/strip moved out of Compress into Settings → Security as a saved preference (applies to every export path); GPS / capture-time / lens stays or goes by your choice

Responsive / snapped windows

one shared useBreakpoint hook: below ~1000px the top bar goes icon-only, below ~900px the side panels float as overlay drawers (scrim, one at a time) instead of crushing the canvas, below ~600px a "wider window" notice. Reduce Motion suppresses the layout slides

Keyboard accessibility

Skip-to-canvas link, landmark roles + labels (toolbar / panels / canvas), accessible names on the tool buttons, and Escape-to-close + role="dialog" semantics on the modals; on top of the existing tool / number / Alt shortcuts

Resize & Compress

one tile, one panel. Both halves move the same two numbers (Web Performance Gain and PageSpeed Insights Score read dimensions AND format/quality together), so they share a panel the way Squoosh does: the scores on top, then Resize, then Compress. Resampling is fully in WASM with no canvas round-trip. One Apply button, named for what is pending — "Apply Resize" when only the dimensions moved, "Apply Compression" when only quality/format/method moved, "Apply Compression & Resize" when both did. Dimensions-only re-saves in the photo's own format at full quality, so the quality slider is left alone

Levels

Brightness (−100% to +100%), contrast (0% to 300%); each adjustment is a separate undo snapshot

Histogram

live RGB / Luma scope in the Review panel, computed in Rust (calculate_histogram) straight from the composite buffer — no per-frame offscreen-canvas sampling

Fast integer compositing

all alpha blending (blend_pixel, blend_over) uses integer source-over math instead of per-pixel float ÷255.0, verified identical to the old result within ±1

Color Picker

its own sub-tool under Edit → Color Picker; hovering the canvas shows a floating 11×11 magnifier (sourced from Rust get_pixel_region); clicking picks the pixel color and sets it as both brush and text color

Blur Brush

Box-blur with stroke-based region masking; configurable radius and intensity; its own sub-tool under Create → Blur Brush

Arrows

Anti-aliased arrows with arrowhead (single or double), drawn directly on the pixel buffer; Create → Arrow

Shapes

Rectangles, circles, hand-drawn circles, and lines rendered in WASM; Create → Shapes, with Arrow and Pins as their own sub-tools beside it

Paint / Brush

Freehand painting via WASM paint_dab + paint_stroke_to; configurable brush size, color, and opacity; Create → Brush, with Pen and Blur Brush as sibling sub-tools

Pen (Vector Paths)

Photoshop-style Bézier pen (Create → Pen): click to drop corner anchors, click-drag to pull smooth curve handles, and grab any anchor or handle to reshape the path. Enter closes it, Esc finishes it open. An optional solid background fills the closed interior (Rust scanline polygon fill, under the stroke). Committed paths stay re-editable — click one to re-open its anchors/handles — and round-trip through history, photo-switch, reload, and cloud sync as a kind-7 ShapeAnnotation (cubic control sequence flattened with de Casteljau)

Text

Click-to-place text with configurable font family (12 browser-safe options), size, weight, and color; up to 8 recent texts that re-open the canvas text box at the last used position, restoring all text settings

Emoji Stamp

Browser renders emoji to OffscreenCanvas, pixels sent to WASM stamp_pixels() for alpha compositing; Create → Emoji

Export

Lossless PNG via Rust encoder, JPEG/WebP/AVIF via browser

Blank Canvas

start a new document from the upload dialog: dimensions + a page-size preset (FHD / Square / Story / 4×6 / 5×7 / 8×10) and a White / Black / hex / Transparent background; the fill is generated in Rust (blank_png → codec::export_png) with no canvas round-trip

Thumbnails

256px WebP thumbnails built from the already-decoded working pixels via Rust resize_pixels (uploads decode once, not twice); working canvas stays at ≤2048px

Copy/Paste Regions

Cross-photo pixel compositing with alpha blending; paste from clipboard supported

History

50-step undo/redo with labeled snapshots (including dimensions for crop/resize/rotate correctness), jump-to, delete entry

Per-photo Edit Persistence

Switching photos saves the full WASM canvas + undo/redo stack to IndexedDB (PNG-encoded per snapshot). Switching back restores the exact edit session — same canvas state, same undo history, all redo steps intact

UI (React)

Animated Panels

Staggered entrance: TopBar → Sidebar → Gallery (Framer Motion springs)

Tool Grid

10 tools with gradient icons: Clone Stamp, Resize & Compress, Crop, Paint, Text, Arrows (FileText — coming soon), Shapes, Effects (Sparkles), Batch Image Editor (bulk logo stamp + grid mosaic view), Eraser (relabeled from AI)

Tab Switchers

Stamp (Clone / Stamps / Emojis), Shapes (Shapes / Arrows), Paint (Paint / Blur Brush / Pen), Effects (Levels / Color Picker) via shared TabGroup component

Spacebar Pan

Hold Space for grab-to-pan; all tool handlers bypassed during pan

A/B Compare Slider

Squoosh-style draggable divider; overlay is positioned exactly over the canvas bounding box (tracks zoom/pan via ResizeObserver) so before/after layers are always pixel-aligned

Review Panel

Right-side panel (Alt+H) with a header toggle group that opens up to three stacked sections sharing the body height (1 full / 2 halves / 3 thirds, each scrollable):

Upload

Drag-and-drop modal with file browser and paste-from-clipboard (Ctrl+V / paste button)

Export Dropdown

PNG, JPEG, WebP, AVIF format selector in the top bar

Keyboard Shortcut Modal

Alt+/ opens a full reference overlay grouped by category (two columns per group)

Hidden Dev Tools

three clicks on a blank status-bar button unlock the Diagnostics Log (Alt+Delete) and the user/tier selector (Alt+L) in production, and append a Dev Tools section to the Alt+/ modal

Eraser Panel

the AI tool relabeled (tool id ai unchanged for shortcut/persistence/routing): Brush Eraser (drag on canvas to scrub the active layer to transparent, local, free, no sign-in) plus three removal modes in one panel — Magic Eraser (local PatchMatch object removal, coming soon), Background Removal (rembg) and Object Removal (SD Inpaint), both Replicate-backed and Paid-tier. 4× Upscale moved to the Effects tool; OCR/text extraction moved to the Text tool; the old Smart Crop and Auto-Enhance placeholders were retired

Light / Dark / System theme

full light & dark palettes (warm earth-tone dark, warm-paper light) driven by CSS-variable tokens in styles.css (:root light, .dark dark, @custom-variant dark); "System" follows the OS via matchMedia and updates live. Pre-paint guard in index.html (no FOUC). Set in Settings → Appearance, persisted to localStorage + Convex. JetBrains Mono + DM Sans throughout

Rulers & Grids

toggleable top/left pixel rulers (tick labels track zoom) + a configurable non-destructive grid overlay: square px spacing, golden-ratio lines, or N×M divisions, with color + opacity. Set in Settings → Rulers & Grids, persisted. The grid-layout geometry is computed in Rust (grid_lines WASM export) as the single source, projected to an SVG overlay on the canvas

State management (Zustand)

the editor's UI, tool, and gallery state lives in three Zustand stores (useUIStore / useToolStore / useGalleryStore) instead of AppShell local state; components subscribe with atomic selectors for minimal re-renders, and durable prefs (last master-bar tab, tool sub-modes) persist to IndexedDB via a write-deduped StateStorage adapter. Behaviour-preserving groundwork for splitting AppShell into feature modules. See State Management

Five-group toolbar

the rail is five groups (Enhance · Select · Create · Edit · Batch) owning 34 sub-tools, replacing eleven flat tools. One registry (features/tools/toolGroups.ts) defines them, and the rail, the sub-tool row, the digits 1–5, the shortcut sheet, the canvas dispatch and the cursor all derive from it — there is no second table to fall out of step. Legacy tool ids are untouched underneath, so persistence and old links keep working

Sub-tool routing

every sub-tool has its own address: #/create/brush, #/edit/color-picker, #/select/ellipse. Thirty-five older URL shapes redirect to the sub-tool they meant, each pinned by a test. The three Edit sub-tools that share a tool underneath used to collapse onto one URL; they no longer do

Sub-tool canvas dispatch

the lit sub-tool decides what a drag does and which cursor you get, instead of the whole parent tool sharing one. A sub-tool with no canvas gesture idles by default rather than inheriting whatever the clone stamp was doing

Colour picker history

eyedroppered colours land in a Recent Colors list under Edit → Color Picker: click to re-apply to brush and text, ✕ to forget, Clear to empty. Same list UI as the guides list. Per-session, not persisted

OCR

reading text out of an image is its own sub-tool (Create → OCR) rather than a mode buried inside the Text panel

Directional duplicate pad

a rectangle or circle picked out of Review → Reselect gets a d-pad on its row; pressing it puts four ⊕ around the shape on the canvas. Each press lays another same-sized copy, clear of the original, in that direction, and each side counts independently — ← twice gives two marching left, and a later ↑ goes above the *original* rather than above the last copy. Built for laying out a diagram from repeated boxes. Rectangles and circles only; every other shape kind keeps a disabled placeholder so the row shape stays constant down the list. Offsets are computed from the shape's own bounding box (lib/duplicatePadGeometry.ts) and the copy itself is an engine-side struct clone (duplicate_shape_annotation)

Duplicate from the Reselect row

any placed text or shape copies from its own row, offset down-right. The engine clones the annotation struct rather than re-adding it from a property list, so a shadow, rotation, background, corner radius or perspective warp travels with the copy; a test walks every field the engine emits rather than a hand-listed set, so a field added later is covered the moment it exists

Stroke Stabilizer, everywhere

the pulled-string "lazy mouse" leash reaches the Paint brush, the Eraser, the blur brush, pixelate, redact and the clone stamp from a single setting. One leash implementation (src/stabilizer.rs) with its level table exported to JS as stabilizer_leash, so no consumer re-derives the numbers. On the clone stamp the source offset is preserved exactly — a lagging tip samples a source lagging by the same vector — and a move inside the leash costs zero recomposites, because every stroke engine returns whether it actually stamped