AspectGlyph
Tiny rectangle showing an aspect ratio (or a circle).
function AspectGlyph({ aspect, round }: {
aspect: number | null;
round?: boolean;
}): react.JSX.Element;Generated from the published TypeScript declarations, so it always matches the package. Most apps only need @image-ultra/react; it re-exports the parts of @image-ultra/core you're likely to use.
@image-ultra/reactAspectGlyphTiny rectangle showing an aspect ratio (or a circle).
function AspectGlyph({ aspect, round }: {
aspect: number | null;
round?: boolean;
}): react.JSX.Element;ColorButtonRound colour button that shows the current colour (or "none").
const ColorButton: react.ForwardRefExoticComponent<ButtonHTMLAttributes<HTMLButtonElement> & {
swatch: string | null;
label: string;
ring?: boolean;
} & react.RefAttributes<HTMLButtonElement>>;ColorStripColours laid out inline, like the filter strip: one tap picks a swatch; the last button opens the full picker (HSV, hex, eyedropper) for any other colour. Scrolls sideways when narrow.
function ColorStrip({ label, value, onChange, swatches, }: ColorStripProps): react.JSX.Element;HsvPickerSaturation/brightness area, hue slider, hex input and eyedropper.
function HsvPicker({ value, onChange }: {
value: string;
onChange: (hex: string) => void;
}): react.JSX.Element;IconButtonA square icon button with a tooltip, styled like the editor's own.
const IconButton: react.ForwardRefExoticComponent<IconButtonProps & react.RefAttributes<HTMLButtonElement>>;ImageEditorThe image editor. Give it a photo (src); get the edited image and the edits as JSON (onSave).
const ImageEditor: react.ForwardRefExoticComponent<ImageEditorProps & react.RefAttributes<ImageEditorHandle>>;MaskBrushOverlayA paintable mask over the stage (for tools like the future AI object eraser). Strokes are in oriented image space — rasterise them with rasterizeMask from @image-ultra/core. Render it as (part of) a tool's StageOverlay.
function MaskBrushOverlay({ strokes, onChange, size, mode, color }: MaskBrushOverlayProps): react.JSX.Element | null;NumberFieldCompact numeric input. Commits on Enter/blur (so typing "1" on the way to "1080" doesn't change the image); ↑/↓ step by 1, Shift by 10.
function NumberField({ label, value, min, max, unit, onChange }: NumberFieldProps): react.JSX.Element;PopoverFloating panel for secondary controls (colours, widths, fonts). Radix handles focus, Escape and outside clicks; it renders inside the editor root so it keeps the theme.
function Popover({ trigger, label, children, side, open, onOpenChange, }: PopoverProps): react.JSX.Element;PresetStripHorizontally scrolling row of choice chips (UI_VISION §5 PresetStrip, compact variant).
function PresetStrip<T extends string>({ label, presets, value, onSelect, variant, onRemove, removeLabel, }: PresetStripProps<T>): react.JSX.Element;RulerSliderPintura-style ruler: a tick scale scrolls under a fixed centre marker (UI_VISION §5). Drag it, use the arrow keys (Shift = ×10), or double-click to reset.
function RulerSlider({ label, value, min, max, step, tickEvery, defaultValue, unitWidth, majorEvery, snapTo, format, onChange, onChangeStart, onChangeEnd, disabled, }: RulerSliderProps): react.JSX.Element;SegmentedControl2–5 mutually exclusive options (UI_VISION §5). Arrow keys move the selection.
function SegmentedControl<T extends string>({ label, options, value, onChange, }: SegmentedControlProps<T>): react.JSX.Element;SwatchPickerSwatches + a compact HSV picker with hex input and eyedropper (UI_VISION §5).
function SwatchPicker({ value, onChange, allowNone, swatches, }: SwatchPickerProps): react.JSX.Element;useEditorStateSubscribe to a slice of editor state; re-renders only when that slice changes.
function useEditorState<T>(selector: (state: EditorStoreState) => T): T;useEditorStoreThe editor's store, inside a custom tool: read with getState(), change edits with update / beginChange.
function useEditorStore(): EditorStore;useFontsThe fonts offered for text (the fonts prop, or the defaults), inside a custom tool.
function useFonts(): readonly FontOption[];useImageEditorA typed ref for controlling the editor from your own UI:
const editor = useImageEditor();
<ImageEditor ref={editor} … />
<button onClick={() => editor.current?.undo()}>Undo</button>function useImageEditor(): RefObject<ImageEditorHandle | null>;useLabelsThe editor's labels (after your labels overrides), inside a custom tool.
function useLabels(): Labels;useLooksThe saved looks and a setter, inside a custom tool.
function useLooks(): [readonly Look[], (looks: Look[]) => void];useStickersYour stickers (the stickers prop), inside a custom tool.
function useStickers(): readonly StickerOption[];useToolStateTransient UI state shared by a tool's Controls and StageOverlay (e.g. current drawing tool, selection). Lives in the store, outside the edit history.
function useToolState<T>(initial: T): [T, (next: T | ((prev: T) => T)) => void];useWatermarkLockedWhether the watermark is locked by the app (lockWatermark), inside a custom tool.
function useWatermarkLocked(): boolean;defineToolIdentity helper that gives you type-checking and autocompletion for a tool definition.
function defineTool<T extends ToolDefinition>(tool: T): T;formatCountFills a CountLabel for count.
function formatCount(label: CountLabel, count: number): string;BUILT_IN_TOOLSThe built-in tools by id, in their default order.
const BUILT_IN_TOOLS: Record<ToolId, ToolDefinition>;DEFAULT_FONTSDefault text fonts: system stacks only, so they render without downloads on every OS.
const DEFAULT_FONTS: readonly (FontOption & {
id: keyof Labels['fontNames'];
})[];DEFAULT_SWATCHESDefault annotation palette (the last one is the brand plum).
const DEFAULT_SWATCHES: readonly ["#ffffff", "#000000", "#ff3b30", "#ff9500", "#ffcc00", "#34c759", "#00c7be", "#0a84ff", "#5856d6", "#af52de", "#ff2d55", "#4d194d"];defaultLabelsThe English labels: every string the editor shows. A template for your own translations.
const defaultLabels: Labels;SIZE_PRESETSCommon social/web sizes. Picking one crops to its aspect ratio (as large as possible) and resizes to exactly this size.
const SIZE_PRESETS: readonly SizePreset[];ColorStripPropsProps for ColorStrip.
interface ColorStripProps {
/** Accessible name of the group, e.g. "Fill colour". */
label: string;
/** Hex colour. */
value: string;
onChange: (value: string) => void;
swatches?: readonly string[];
}CountLabelA label with a number in it: a template with {count}, or a function for plural rules.
type CountLabel = string | ((count: number) => string);FontOptionA font offered for text shapes and the watermark (fonts prop).
interface FontOption {
/** Shown in the font menu. */
label: string;
/** CSS font-family value. Web fonts must be loaded by your app (e.g. next/font). */
family: string;
}IconButtonPropsProps for IconButton.
interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
/** Accessible name, also shown as the hover tooltip. */
label: string;
icon: ReactNode;
/** Show the label next to the icon (TopBar text buttons). */
showLabel?: boolean;
variant?: 'ghost' | 'primary';
/** `sm` = 24px, for secondary actions inside dense panels. Default `md` (40px touch target). */
size?: 'md' | 'sm';
}IconPropsProps every icon accepts (custom tool icons too).
interface IconProps extends Omit<SVGProps<SVGSVGElement>, 'children'> {
/** Rendered size in CSS px. Icons are drawn on a 24px grid. */
size?: number;
}ImageEditorHandleControl the editor from your own code: const editor = useImageEditor() + ref={editor}.
interface ImageEditorHandle {
/** The underlying store, for advanced use (subscribe, custom tools). */
readonly store: EditorStore;
/** Current edits. Serializable — `JSON.stringify` it to save. */
getState(): EditState;
/** Replace all edits (e.g. restore saved JSON). Clears undo history. */
setState(state: EditState | unknown): void;
/** One undoable change: `update('Rotate', (s) => { s.geometry.rotation = 90; })`. */
update(label: string, recipe: EditRecipe): void;
undo(): void;
redo(): void;
reset(): void;
/** Render and encode without triggering `onSave`. */
exportImage(options?: ExportOptions): Promise<ExportResult>;
/** Same as pressing Done: exports with `exportOptions` and calls `onSave`. */
save(): Promise<ExportResult | null>;
}ImageEditorPropsProps for <ImageEditor>.
interface ImageEditorProps {
/** Image to edit. Omit to show the drop zone. */
src?: ImageSource | undefined;
/**
* Edits to restore when the image opens — an `EditState` or its JSON (e.g. from `onSave`'s
* `result.state`). Read whenever `src` changes.
*/
initialState?: EditState | unknown;
/** `'dark'` (default), `'light'`, or `'auto'` to follow the OS setting. */
theme?: ThemeMode;
/**
* Text direction. Default: the page's (inherited). `rtl` mirrors the layout (tool rail on the
* right, rows reversed); the photo, sliders and curves stay left-to-right.
*/
dir?: 'ltr' | 'rtl';
/** Typed shortcuts for common design tokens, e.g. `{ accent: '#ff5a1f' }`. */
themeOverrides?: ThemeOverrides;
/**
* Tools shown in the ToolRail, in this order: built-in ids and/or your own `defineTool(...)`
* definitions. Defaults to all built-in tools.
*/
tools?: readonly ToolInput[];
/** Tool selected when the editor opens. Defaults to the first of `tools`. */
defaultTool?: ToolId | (string & {});
/** Translate or rename any text. */
labels?: LabelOverrides;
/** Output format, quality and size used when the user presses Done. */
exportOptions?: ExportOptions;
/** Called with the exported image when the user presses Done. */
onSave?: (result: ExportResult) => void | Promise<void>;
/** Called after every committed edit (not on every frame of a slider drag). */
onChange?: (state: EditState) => void;
/**
* Saved colour looks shown in the Filter tool. Pass with `onLooksChange` to store them yourself
* (e.g. per user in your database); otherwise they're kept in this browser's localStorage.
*/
looks?: Look[];
onLooksChange?: (looks: Look[]) => void;
/** localStorage key for uncontrolled looks; `false` keeps them in memory only. */
persistLooks?: boolean | string;
/** Fonts offered for text annotations (default: system font stacks). */
fonts?: FontOption[];
/**
* Looks in the Filter tool (default: `FILTER_PRESETS`). A built-in id shows its name from
* `labels.filterNames`; your own presets show their `name`.
*/
filterPresets?: readonly FilterPreset[];
/**
* Output sizes in Resize (default: `SIZE_PRESETS`). A built-in id shows its name from
* `labels.sizePresetNames`; your own sizes show their `label`.
*/
sizePresets?: readonly SizePreset[];
/**
* A watermark to start with — text, or a logo via `logo` (image URL). With `lockWatermark` the
* user can't change or remove it, and every export applies it.
*/
watermark?: WatermarkInput;
lockWatermark?: boolean;
/** Your own stickers, shown first in the Sticker tool. */
stickers?: StickerOption[];
/**
* The 3D sticker library (Microsoft Fluent Emoji 3D, MIT), loaded on demand. Default: the pinned
* jsDelivr copy. Pass your own base URL to self-host the `assets/` folder, or `false` to turn it off.
*/
stickerLibrary?: string | false;
/** Shows a Cancel button in the TopBar when provided. */
onCancel?: () => void;
/**
* Load, export or `initialState` errors. The editor already shows load errors on screen, so
* without this they're only logged with `console.warn`.
*/
onError?: (error: Error) => void;
className?: string;
style?: CSSProperties;
}LabelOverridesAny subset of Labels, also inside groups: pass to labels to translate or rename text.
type LabelOverrides = Partial<Omit<Labels, NestedKey>> & {
[K in NestedKey]?: Partial<Labels[K]>;
};LabelsEvery user-facing string. Pass a partial object to labels to translate or rename.
interface Labels {
/** Built-in filter names, by preset id (`FILTER_PRESETS`). */
filterNames: Record<string, string>;
/** Built-in Resize size names, by id (`SIZE_PRESETS`). */
sizePresetNames: Record<string, string>;
/** Built-in sticker names, by id. */
stickerNames: Record<string, string>;
/** The default text fonts (when the app passes no `fonts`). */
fontNames: Record<'sans' | 'serif' | 'mono' | 'rounded' | 'hand', string>;
/** Layer names for elements the user hasn't named. `{text}` = a text box's first words. */
shapeNames: Record<'rectangle' | 'ellipse' | 'line' | 'arrow' | 'drawing' | 'polygon' | 'text' | 'image', string>;
/** Undo-history step names (History panel, "Undone: …"). */
steps: Record<'crop' | 'moveCrop' | 'resizeCrop' | 'rotate' | 'flip' | 'aspectRatio' | 'resetAdjust' | 'autoEnhance' | 'resetFinetune' | 'resize', string>;
/** Key names in the shortcuts panel (⌘ ⇧ ⌫ ↵ are symbols and stay). */
keys: Record<'ctrl' | 'space' | 'drag' | 'click' | 'tab' | 'esc', string>;
/** Pixel unit after a number field. */
unitPx: string;
cancel: string;
reset: string;
undo: string;
redo: string;
done: string;
saving: string;
zoomIn: string;
zoomOut: string;
zoomFit: string;
zoomLevel: string;
zoomActual: string;
/** Compare button tooltip ("hold" = press and hold). */
compare: string;
showOriginal: string;
before: string;
after: string;
/** Accessible name of the split-view divider. */
compareDivider: string;
history: string;
historyOriginal: string;
/** Screen-reader announcements (7.5c). `{step}` = a history step label. */
announceUndo: string;
announceRedo: string;
announceHistory: string;
/** `{percent}` */
announceZoom: string;
/** `{width}`, `{height}` */
announceCrop: string;
/** `{name}` = the element's name */
announceSelected: string;
announceNothingSelected: string;
announceSaved: string;
saveFailed: string;
/** The photo on the stage: `{width}`, `{height}` = the result's size. */
photoLabel: string;
/** The photo in Annotate / Sticker / Redact (a Tab stop), and how to use it. */
elementsLabel: string;
elementsHint: string;
/** Curve point value: `{in}`, `{out}` (0–255). */
curvePointValue: string;
shortcuts: string;
shortcutsGeneral: string;
shortcutsShow: string;
nudge: string;
finishOrEdit: string;
/** Shortcuts panel: Tab / Shift+Tab on the photo. */
nextElement: string;
deselect: string;
/** Built-in tool names. Custom tools pass their own `label`. */
tools: Record<ToolId, string>;
toolbarLabel: string;
loading: string;
loadError: string;
/** `{format}` is replaced with the file type, e.g. `HEIC`. */
loadErrorUnsupported: string;
loadErrorDamaged: string;
loadErrorNotImage: string;
loadErrorNetwork: string;
emptyTitle: string;
emptyHint: string;
browse: string;
comingSoon: string;
rotateLeft: string;
flipHorizontal: string;
flipVertical: string;
straighten: string;
tiltVertical: string;
tiltHorizontal: string;
resetTool: string;
aspectRatio: string;
aspectFree: string;
aspectOriginal: string;
aspectCircle: string;
cropArea: string;
/** Screen-reader hint on the crop area. */
cropAreaHint: string;
width: string;
height: string;
keepAspect: string;
sizePresets: string;
originalSize: string;
upscaleWarning: string;
/** Names of the 16 adjustments. */
finetune: Record<keyof FinetuneState, string>;
modeAdjust: string;
modeCurves: string;
modeLevels: string;
adjustments: string;
auto: string;
autoHint: string;
curveChannels: Record<CurveChannel, string>;
curvePoint: string;
curveHint: string;
levelsBlack: string;
levelsMid: string;
levelsWhite: string;
saveLook: string;
/** `{name}` is replaced with the look's name. */
lookSaved: string;
viewInFilters: string;
lookName: string;
save: string;
cancelEdit: string;
annotateTools: string;
annotateModes: Record<'select' | 'pen' | 'line' | 'arrow' | 'rect' | 'ellipse' | 'polygon' | 'text', string>;
insertImage: string;
strokeColor: string;
fillColor: string;
textColor: string;
textBackground: string;
strokeWidth: string;
fontSize: string;
opacity: string;
cornerRadius: string;
font: string;
bold: string;
alignLeft: string;
alignCenter: string;
alignRight: string;
arrowStart: string;
arrowEnd: string;
duplicate: string;
copy: string;
zoomCrop: string;
resizeModeSize: string;
resizeModeCanvas: string;
/** Aspect chips for the canvas; `canvasOriginal` = no shape change, padding only. */
canvasShape: string;
canvasOriginal: string;
canvasPadding: string;
canvasAnchor: string;
canvasReset: string;
canvasHint: string;
selectAll: string;
/** Shift-click hint in the shortcuts panel. */
addToSelection: string;
panPhoto: string;
/**
* `{count}` = number of selected shapes. A function gets the number, for languages whose wording
* changes with it (plurals): `(n) => …`.
*/
selectedCount: CountLabel;
resizeSelection: string;
arrange: string;
alignEdges: Record<'left' | 'centerX' | 'right' | 'top' | 'centerY' | 'bottom', string>;
distributeX: string;
distributeY: string;
renameLayer: string;
groupMenu: string;
editInRedact: string;
editInWatermark: string;
lockAll: string;
unlockAll: string;
hideAll: string;
reorderLayer: string;
showAllLayers: string;
cut: string;
paste: string;
deleteShape: string;
layers: string;
noLayers: string;
bringForward: string;
sendBackward: string;
bringToFront: string;
sendToBack: string;
showInLayers: string;
/** Accessible name of the "⋯" menu button on each layer and of the canvas context menu. */
shapeMenu: string;
showLayer: string;
hideLayer: string;
lockLayer: string;
unlockLayer: string;
sizeSmall: string;
sizeMedium: string;
sizeLarge: string;
sizeHuge: string;
annotateHint: string;
polygonHint: string;
textPlaceholder: string;
/** Content of a new text box (selected, so typing replaces it). */
textDefault: string;
editText: string;
rotate: string;
redactTools: string;
redactBox: string;
redactBrush: string;
redactStyle: string;
redactStyles: Record<RedactStyle, string>;
redactStrength: string;
brushSize: string;
redactColor: string;
redactClear: string;
redactDelete: string;
/** History label for a new area. */
redactArea: string;
redactMove: string;
redactHint: string;
redactBlurHint: string;
stickers: string;
stickerTabs: Record<'stickers' | 'emoji', string>;
stickerUpload: string;
stickerBasic: string;
stickerSearch: string;
stickerCategories: string;
emojiGroups: Record<EmojiGroup, string>;
stickerNoResults: string;
/** History label for placing a sticker. */
stickerAdd: string;
watermarkKind: string;
watermarkKinds: Record<'none' | 'text' | 'logo', string>;
watermarkText: string;
watermarkColor: string;
watermarkChooseLogo: string;
watermarkPosition: string;
watermarkPositions: Record<WatermarkPosition, string>;
watermarkSize: string;
watermarkBold: string;
watermarkLocked: string;
/** Accessible name of the draggable watermark on the photo. */
watermarkDrag: string;
frames: string;
frameNone: string;
frameStyles: Record<FrameStyle, string>;
frameSize: string;
frameColor: string;
fillKind: string;
fillKinds: Record<'none' | 'color' | 'image' | 'blur', string>;
backgroundColor: string;
fillChooseImage: string;
fillHint: string;
color: string;
colorNone: string;
colorCustom: string;
colorSaturation: string;
colorHue: string;
colorPick: string;
filters: string;
filterNone: string;
intensity: string;
myLooks: string;
removeLook: string;
}MaskBrushOverlayPropsProps for MaskBrushOverlay.
interface MaskBrushOverlayProps {
strokes: readonly MaskStroke[];
onChange: (strokes: MaskStroke[]) => void;
/** Brush diameter in image (oriented) pixels. */
size: number;
mode: 'paint' | 'erase';
/** Any CSS colour for the mask preview. Default: accent at 45%. */
color?: string;
}NumberFieldPropsProps for NumberField.
interface NumberFieldProps {
label: string;
value: number;
min: number;
max: number;
/** Shown after the number, e.g. `px`. */
unit?: string;
onChange: (value: number) => void;
}PopoverPropsProps for Popover.
interface PopoverProps {
/** The element that opens the popover (usually an `IconButton` or `ColorButton`). */
trigger: ReactNode;
/** Accessible name of the popover panel. */
label: string;
children: ReactNode;
side?: 'top' | 'bottom';
open?: boolean;
onOpenChange?: (open: boolean) => void;
}PresetOne chip in a PresetStrip.
interface Preset<T extends string> {
value: T;
label: string;
/** Optional small visual before the label, e.g. an aspect-ratio glyph. */
glyph?: ReactNode;
/** Extra text for screen readers, e.g. "1080 by 1350 pixels". */
description?: string;
/** Shows a remove badge (and Delete key removes it) — needs `onRemove` on the strip. */
removable?: boolean;
/** Draw a thin divider before this item (to separate groups). */
separatorBefore?: boolean;
/** Small marker in the corner, e.g. a bookmark for the user's own looks. */
badge?: ReactNode;
}PresetStripPropsProps for PresetStrip.
interface PresetStripProps<T extends string> {
label: string;
presets: readonly Preset<T>[];
/** `null` when no preset matches the current state. */
value: T | null;
onSelect: (value: T) => void;
/** `thumbs`: large glyph (e.g. a preview image) above the label. */
variant?: 'chips' | 'thumbs';
onRemove?: (value: T) => void;
/** Accessible name of the remove badge, e.g. "Remove look". */
removeLabel?: string;
}RulerSliderPropsProps for RulerSlider.
interface RulerSliderProps {
/** Accessible name. */
label: string;
value: number;
min: number;
max: number;
/** Precision of the value when dragging. Default 1. */
step?: number;
/** Distance between ticks, also the arrow-key step (Shift = ×10). Default `step`. */
tickEvery?: number;
/** Double-click / Enter resets to this. Default 0. */
defaultValue?: number;
/** Screen pixels per unit of value. Default 6. */
unitWidth?: number;
/** Draw a taller tick every N units. Default 5. */
majorEvery?: number;
/** Values that attract the ruler when dragged close (e.g. 0). Default `[defaultValue]`. */
snapTo?: number[];
format?: (value: number) => string;
onChange: (value: number) => void;
/**
* A drag or key-press burst begins — use it to open one undo step. Separate key presses each
* start one; `undoStep()` merges them in the store.
*/
onChangeStart?: (source: ChangeSource) => void;
/** …and ends. */
onChangeEnd?: () => void;
disabled?: boolean;
}SegmentedControlPropsProps for SegmentedControl.
interface SegmentedControlProps<T extends string> {
/** Accessible name of the group. */
label: string;
options: readonly SegmentedOption<T>[];
value: T;
onChange: (value: T) => void;
}SizePresetBuilt-in output sizes for the Resize tool (data only, so the labels can list their names).
interface SizePreset {
id: string;
/** Chip text. */
label: string;
width: number;
height: number;
}StickerOptionOne of your own stickers (stickers prop).
interface StickerOption {
id: string;
/** Accessible name / tooltip. */
label: string;
/** Image URL (same-origin or CORS-enabled so exports can read it) or data URL. */
src: string;
}SwatchPickerPropsProps for SwatchPicker.
interface SwatchPickerProps {
/** Hex colour or `null` (none). */
value: string | null;
onChange: (value: string | null) => void;
/** Offer "none" (e.g. for fills). */
allowNone?: boolean;
swatches?: readonly string[];
}ThemeModedark (default), light, or auto to follow the OS setting.
type ThemeMode = 'dark' | 'light' | 'auto';ThemeOverridesTyped shortcuts for the most common CSS variables. Anything else can be overridden in CSS: .iu-root { --iu-surface-1: #000; } — see docs/internal/THEMING.md for the full token list.
interface ThemeOverrides {
accent?: string;
accentHover?: string;
accentContrast?: string;
/** Accent-coloured text and icons on surfaces (e.g. selected tool label). Defaults to `accent`. */
accentText?: string;
bg?: string;
stage?: string;
surface1?: string;
surface2?: string;
surface3?: string;
text?: string;
textMuted?: string;
border?: string;
fontFamily?: string;
/** Base control radius, e.g. `'10px'`. Small and large radii scale from it. */
radius?: string;
}ToolDefinitionA tool in the ToolRail. Built-in tools use exactly this API, so custom tools are first-class:
const stamp = defineTool({
id: 'stamp',
label: 'Stamp',
icon: MyStampIcon,
Controls: () => <button onClick={…}>Add stamp</button>,
});
<ImageEditor tools={['adjust', 'finetune', stamp]} /> Inside Controls / StageOverlay, use useEditorStore(), useEditorState() and useLabels().
interface ToolDefinition {
/** Unique id. Built-in ids: see `ToolId`. */
id: string;
/** Text under the icon. Built-in tools read theirs from `labels.tools`. */
label?: string;
icon: ComponentType<IconProps>;
/** Rendered in the ControlBar while the tool is active. */
Controls: ComponentType;
/** Rendered on top of the stage while the tool is active (e.g. the crop box). */
StageOverlay?: ComponentType;
}ToolInputAn entry of the tools prop: a built-in tool id or your own defineTool(...).
type ToolInput = ToolId | ToolDefinition;WatermarkInputThe watermark prop: any watermark fields, plus logo (an image URL) for a logo watermark. Missing fields use the defaults (bottom-right, 5 % high, 70 % opacity).
type WatermarkInput = Partial<Omit<WatermarkState, 'assetId' | 'kind'>> & {
logo?: string;
};@image-ultra/coreapplyLookstate with the look's colour settings (geometry, resize and assets untouched).
function applyLook(state: EditState, look: Look): EditState;autoEnhanceAuto-enhance for the current image and geometry.
function autoEnhance(image: LoadedImage, state: EditState): Promise<AutoEnhanceResult>;createEditorStoreCreates the editor's state store (image, edits, undo history, viewport, tools) with no UI attached.
function createEditorStore(options?: CreateEditorStoreOptions): EditorStore;createEditStateA fresh state with no edits.
function createEditState(): EditState;createLookSaves the colour part of state (finetune, levels, curves, filter) as a named look.
function createLook(state: EditState, name: string, id?: string): Look;detectImageFormatReads a file's header to tell what kind of image it really is (ignores the extension).
function detectImageFormat(blob: Blob): Promise<ImageFormat | null>;exportImageRenders the edited image at full quality and encodes it. Browser only.
function exportImage(image: LoadedImage, state: EditState, options?: ExportOptions): Promise<ExportResult>;filterFromPresetTurns a preset into the self-contained EditState.filter value.
function filterFromPreset(preset: FilterPreset, intensity?: number): FilterState;getBeforeStateThe "before" side of compare: the same framing (geometry + resize) with every look and overlay removed, so it lines up exactly with the edited result.
function getBeforeState(state: EditState): EditState;loadImageDecodes any supported source into an ImageBitmap, applying EXIF orientation. Browser only — call it from effects/event handlers, never during SSR. Every failure is an ImageLoadError (or the abort reason when signal fires).
function loadImage(source: ImageSource, options?: LoadImageOptions): Promise<LoadedImage>;lookMatchestrue when the state's colour settings are exactly the look's.
function lookMatches(state: EditState, look: Look): boolean;parseEditStateTurns untrusted input (e.g. JSON from your database) into a valid EditState. Missing fields get defaults and numbers are clamped to their ranges. Throws EditStateError if the input isn't an edit state at all or comes from a newer version.
function parseEditState(input: unknown): EditState;parseLooksValidates untrusted looks (e.g. from localStorage); invalid entries are dropped.
function parseLooks(input: unknown): Look[];renderImageHeadless rendering: apply a saved EditState to an image without any UI. state may be an EditState or untrusted JSON (it is validated).
function renderImage(source: ImageSource | LoadedImage, state?: EditState | unknown, options?: ExportOptions): Promise<ExportResult>;renderToCanvasRenders the edited image onto a 2D canvas (WebGL2 first, Canvas2D fallback). background flattens transparency (used for JPEG). Browser only.
function renderToCanvas(image: LoadedImage, state: EditState, options?: Pick<ExportOptions, 'maxWidth' | 'maxHeight' | 'renderer'> & {
background?: string;
}): Promise<RenderedCanvas>;selectCanRedoStore selector: is there a step to redo?
const selectCanRedo: (s: EditorState) => boolean;selectCanUndoSelectors for common derived values.
const selectCanUndo: (s: EditorState) => boolean;selectIsDirtyStore selector: do the edits differ from the ones the photo opened with?
const selectIsDirty: (s: EditorState) => boolean;EditStateErrorThrown by parseEditState when the input can't be read as edits.
class EditStateError extends Error {
readonly name = "EditStateError";
}ImageLoadErrorWhy a photo didn't open (code): unsupported format, damaged file, not an image, or a download error.
class ImageLoadError extends Error {
readonly name = "ImageLoadError";
readonly code: ImageLoadErrorCode;
/** Detected format (e.g. `ico`, `heic`), when known. */
readonly format: ImageFormat | null;
constructor(message: string, code: ImageLoadErrorCode, options?: {
format?: ImageFormat | null;
cause?: unknown;
});
}DEFAULT_WATERMARKThe watermark a new one starts from.
const DEFAULT_WATERMARK: WatermarkState;EDIT_STATE_VERSIONEditState — the single, serializable description of every edit. The source image is never modified; rendering = source image + EditState. Save it with JSON.stringify, restore it with parseEditState.
const EDIT_STATE_VERSION = 1;FILTER_PRESETSThe built-in filter looks.
const FILTER_PRESETS: readonly FilterPreset[];FINETUNE_KEYSDisplay order of the adjustments.
const FINETUNE_KEYS: readonly ["brightness", "contrast", "saturation", "vibrance", "exposure", "highlights", "shadows", "temperature", "tint", "hue", "gamma", "clarity", "sharpen", "blur", "grain", "vignette"];FINETUNE_RANGESAllowed range of each adjustment.
const FINETUNE_RANGES: Record<keyof FinetuneState, readonly [min: number, max: number]>;FRAME_STYLESDisplay order in the Frame tool.
const FRAME_STYLES: readonly ["border", "rounded", "bevel", "line", "double", "inset", "plus", "lumber", "corners", "polaroid"];MAX_OUTPUT_SIDELargest output side we accept (also the practical GPU limit on most devices).
const MAX_OUTPUT_SIDE = 16384;REDACT_STYLESRedaction styles: pixelate, blur or solid.
const REDACT_STYLES: readonly ["pixelate", "blur", "solid"];TOOL_IDSBuilt-in tool ids, in their default ToolRail order.
const TOOL_IDS: readonly ["adjust", "finetune", "filter", "annotate", "redact", "sticker", "frame", "fill", "resize", "watermark"];WATERMARK_ELEMENT_IDId of the watermark's element (marker, and its selection on the photo).
const WATERMARK_ELEMENT_ID = "watermark";WATERMARK_POSITIONSWhere a watermark can go: 9 anchors, custom and tile.
const WATERMARK_POSITIONS: readonly ["top-left", "top", "top-right", "left", "center", "right", "bottom-left", "bottom", "bottom-right", "custom", "tile"];AutoEnhanceResultSuggested settings from Auto-enhance. Only the listed finetune keys are touched.
interface AutoEnhanceResult {
finetune: Pick<FinetuneState, 'temperature' | 'tint' | 'vibrance' | 'contrast' | 'shadows' | 'highlights'>;
levels: LevelsState;
}BackgroundStateWhat shows through transparent parts of the result (PNGs, round crops, rounded frames). image refers to an entry in EditState.assets; blur is a blurred copy of the result.
type BackgroundState = {
kind: 'color';
color: string;
} | {
kind: 'image';
assetId: string;
} | {
kind: 'blur';
};CanvasStateExtra space around the photo (DECISIONS #83), e.g. to make a landscape photo square for a post without cutting anything off. The added area is transparent, so the Fill shows there, and annotations can sit on it. Measured around the crop; the output covers crop + space.
interface CanvasState {
/** Width ÷ height of the whole canvas, or `null` to only add `padding`. Only ever adds space. */
aspect: number | null;
/** Space on every side, as a share of the photo's short side (0…1). */
padding: number;
/** Where the photo sits in the extra space: 0 = left / top, 0.5 = centre, 1 = right / bottom. */
anchor: {
x: number;
y: number;
};
}ChangeOptionsOptions for update / beginChange.
interface ChangeOptions {
/**
* Merge with the previous step when it had the same label, was also coalesced, and ended less
* than `coalesceMs` ago with nothing recorded since — e.g. repeated arrow-key presses on one
* control become one undo step.
*/
coalesce?: boolean;
}CreateEditorStoreOptionsOptions for createEditorStore.
interface CreateEditorStoreOptions {
defaultTool?: string;
viewport?: ViewportOptions;
/** Maximum undo steps kept. Default 250. */
historyLimit?: number;
/** How long after a coalesced step the next one still merges with it. Default 600 ms. */
coalesceMs?: number;
}CropShapeCrop outline: rect or ellipse (a round crop).
type CropShape = 'rect' | 'ellipse';CropViewTransform used while cropping: stage px = oriented px × scale + (x, y). The crop rectangle is fitted and centred on the stage; the rest of the image extends around it.
interface CropView {
scale: number;
x: number;
y: number;
}CurveChannelA tone curve: rgb (all) or one colour channel.
type CurveChannel = keyof CurvesState;CurvePointA tone-curve control point [input, output], both 0…1.
type CurvePoint = [x: number, y: number];CurvesStateTone curves. Each list is sorted by x and always includes x = 0 and x = 1.
interface CurvesState {
rgb: CurvePoint[];
red: CurvePoint[];
green: CurvePoint[];
blue: CurvePoint[];
}DrawnShapeShapes you draw and style (everything except redaction areas and the watermark marker).
type DrawnShape = RectShape | EllipseShape | LineShape | PathShape | TextShape | ImageShape;EditAssetAn image stored in the edits (stickers, logos, pasted images, a Fill image).
type EditAsset = RasterAsset;EditorActionsEverything the store can do: load, change edits, undo / redo, zoom, export.
interface EditorActions {
/** Opens an image. `state` restores previously saved edits. Clears history. */
load(source: ImageSource, options?: {
state?: EditState;
}): Promise<void>;
/** Show the error screen, e.g. when the preview can't be drawn. */
fail(error: Error): void;
setActiveTool(tool: string): void;
setCropView(view: CropView | null): void;
/** Replace one tool's transient UI state. */
setToolState(toolId: string, value: unknown): void;
setStageSize(size: Size): void;
setViewport(viewport: Viewport, options?: Pick<ViewportChangeOptions, 'animate'>): void;
zoomTo(scale: number, options?: ViewportChangeOptions): void;
zoomBy(factor: number, options?: ViewportChangeOptions): void;
fit(options?: Pick<ViewportChangeOptions, 'animate'>): void;
setAnimationMs(ms: number): void;
/** Applies one undoable change. During `beginChange`/`endChange` it applies live instead. */
update(label: string, recipe: EditRecipe, options?: ChangeOptions): void;
/** Start a continuous change (e.g. slider drag). All `update` calls merge into one undo step. */
beginChange(label: string, options?: ChangeOptions): void;
/** Finish the continuous change and record it (if anything changed). */
endChange(): void;
/** Abort the continuous change and restore the state from `beginChange`. */
cancelChange(): void;
undo(): void;
redo(): void;
/** Jump through history: negative = back, positive = forward (one change, one `onChange`). */
jump(steps: number): void;
setCompare(value: number | null): void;
/** Back to `initialEdit`, as an undoable step. `label`: the step's name (UI language). */
reset(label?: string): void;
/** Replace the whole edit state, e.g. from saved JSON. Clears history. */
replaceEdit(state: EditState): void;
/** Renders and encodes the current result. */
exportImage(options?: ExportOptions): Promise<ExportResult>;
/** Runs a cancellable job and tracks its progress in `tasks`. */
runTask<T>(label: string, run: (context: TaskContext) => Promise<T>): Promise<T>;
cancelTask(id: string): void;
/** Frees the decoded image, cancels tasks and animations. Call when unmounting. */
destroy(): void;
}EditorStateThe store's data: status, image, edits, history, viewport, active tool.
interface EditorState {
status: EditorStatus;
image: LoadedImage | null;
error: Error | null;
/** Id of the selected tool: a built-in `ToolId` or a custom tool's id. */
activeTool: string;
/**
* Set while a crop-style tool is active: the stage then shows the whole image framed around the
* crop instead of the edited result. `null` = normal result view.
*/
cropView: CropView | null;
/** Size of the stage element in CSS px. */
stageSize: Size;
/** Where the edited result sits on the stage. */
viewport: Viewport;
/** True while the result is shown at "fit" — it then re-fits when the stage resizes. */
isFitted: boolean;
/** Duration used for animated zooms. The UI sets 0 for `prefers-reduced-motion`. */
animationMs: number;
/** All current edits. Immutable — replace it through the actions below. */
edit: EditState;
/** The state the image was opened with; "Reset" returns here. */
initialEdit: EditState;
history: History<EditState>;
/** An open continuous change (slider drag) that becomes one history step on `endChange`. */
pendingChange: {
base: EditState;
label: string;
coalesce: boolean;
/** Set when the change re-opened the previous step: what `cancelChange` goes back to. */
reopened?: {
edit: EditState;
history: History<EditState>;
};
} | null;
tasks: EditorTask[];
/**
* Transient UI state owned by tools (e.g. Annotate's current drawing tool and selection),
* shared between a tool's Controls and StageOverlay. Not part of the edit or history.
*/
toolState: Record<string, unknown>;
/**
* Before/after compare (UI only, never part of the edit): `null` = off, `1` = the whole image
* shows "before", `0…1` = split view with the divider at that fraction of the stage width.
*/
compare: number | null;
}EditorStatusidle (no photo), loading, ready or error.
type EditorStatus = 'idle' | 'loading' | 'ready' | 'error';EditorStoreThe editor's store (a zustand store): getState(), subscribe().
type EditorStore = StoreApi<EditorStoreState>;EditorStoreStateThe store's data and actions together (EditorState & EditorActions).
type EditorStoreState = EditorState & EditorActions;EditorTaskA long-running job (export, AI…) the UI can show progress for and cancel.
interface EditorTask {
id: string;
label: string;
/** 0…1, or `null` when the duration is unknown. */
progress: number | null;
}EditRecipeChanges an EditState. Mutate the draft (Immer) or return a whole new state: store.update('Rotate', (s) => { s.geometry.rotation = 90; })
type EditRecipe = (draft: Draft<EditState>) => void | EditState;EditStateEvery edit, as serializable JSON: geometry, colour, filter, elements, frame, fill, watermark, output size and the images they use. The source photo is never changed.
interface EditState {
version: typeof EDIT_STATE_VERSION;
geometry: GeometryState;
finetune: FinetuneState;
levels: LevelsState;
curves: CurvesState;
/** Applied before finetune, so adjustments tweak the filtered look. */
filter: FilterState | null;
/** Vector shapes on top of the photo, bottom → top (see `annotations.ts`). */
annotations: Shape[];
/** Decorative frame over the photo's edges (see `frame.ts`). */
frame: FrameState | null;
/** What shows through transparent parts (colour, image or a blurred copy). */
background: BackgroundState | null;
/** Text or logo on top of everything (see `watermark.ts`). */
watermark: WatermarkState | null;
/** Space added around the photo, or `null` for none (see `CanvasState`). */
canvas: CanvasState | null;
/** Output size in pixels, or `null` to keep the canvas's (crop + added space) size. */
resize: ResizeState | null;
assets: Record<string, EditAsset>;
}EllipseShapeAn ellipse element.
interface EllipseShape extends ShapeBase {
type: 'ellipse';
/** Bounding box. */
x: number;
y: number;
width: number;
height: number;
fill: Paint;
stroke: Paint;
strokeWidth: number;
}ExportMimeTypeOutput formats: JPEG, PNG or WebP.
type ExportMimeType = 'image/png' | 'image/jpeg' | 'image/webp';ExportOptionsHow an export is encoded and sized.
interface ExportOptions {
/** Default: the source type when it's PNG/JPEG/WebP, otherwise PNG. */
mimeType?: ExportMimeType;
/** 0…1 for JPEG/WebP. Default 0.92 (JPEG) / 0.9 (WebP). */
quality?: number;
/** Scale the result down (never up) to fit these bounds. */
maxWidth?: number;
maxHeight?: number;
/** File name without extension. Default: the source name, else `image`. */
fileName?: string;
/** Fills transparent areas for formats without alpha (JPEG). Default `#ffffff`. */
background?: string;
/** Force a renderer; default tries WebGL2 then Canvas2D. */
renderer?: RendererKind | 'auto';
/**
* Keep the photo's EXIF (camera, lens, date, copyright…) — JPEG to JPEG only. Default `false`:
* exports carry no metadata. GPS location is removed even when `true`; pass
* `{ location: true }` to keep it too. The embedded thumbnail and maker notes are never kept,
* orientation is reset (the pixels are already turned) and the size fields are updated.
*/
keepMetadata?: boolean | {
location?: boolean;
};
}ExportResultThe exported image, its size and type, and the edits that produced it.
interface ExportResult {
blob: Blob;
width: number;
height: number;
/** The type actually produced (browsers may fall back to PNG for unsupported types). */
mimeType: string;
/** File name with extension, e.g. `photo-edited.jpg`. */
fileName: string;
/** The edits that produced this image — store it to re-open the editor later. */
state: EditState;
renderer: RendererKind;
/**
* The result is smaller than asked for: the browser can't hold a canvas that big (e.g. ~16 MP
* on iPhones, ~268 MP on desktop browsers), so it was scaled down to fit.
*/
downscaled: boolean;
}FilterCategoryFilter groups in the Filter tool.
type FilterCategory = 'color' | 'film' | 'mono';FilterPresetBuilt-in filter looks. Each is a colour matrix and/or tone curves — no bitmaps, so they're tiny, resolution-independent and stored in full inside EditState.filter.
interface FilterPreset {
id: string;
name: string;
category: FilterCategory;
matrix?: number[];
curves?: Partial<CurvesState>;
}FilterStateA filter look. The full definition is stored (not just an id) so a saved EditState renders the same anywhere, even without the preset list that created it.
interface FilterState {
/** Preset id, e.g. `chrome` — used to highlight the chip. */
id: string;
/** Display name at the time it was applied. */
name: string;
/** 0…1 blend between the original and the filtered colours. */
intensity: number;
/**
* Row-major 3×4 colour matrix on 0…1 RGB: `r' = m0·r + m1·g + m2·b + m3`, etc.
* Omit for "curves only".
*/
matrix?: number[];
/** Tone curves applied after the matrix. */
curves?: Partial<CurvesState>;
}FinetuneStateColour and detail adjustments. 0 always means "unchanged". Most values are −1…1; sharpen, blur and grain are 0…1 (see FINETUNE_RANGES).
interface FinetuneState {
brightness: number;
contrast: number;
saturation: number;
/** Boosts muted colours more than already-saturated ones. */
vibrance: number;
/** ±2 photographic stops at the extremes. */
exposure: number;
/** Brightens (positive) or recovers (negative) the brightest tones. */
highlights: number;
/** Lifts (positive) or deepens (negative) the darkest tones. */
shadows: number;
/** Negative = cooler (blue), positive = warmer (amber). */
temperature: number;
/** Negative = green, positive = magenta. */
tint: number;
/** Rotates all hues, ±180° at the extremes. */
hue: number;
/** Positive brightens mid-tones, negative darkens them. */
gamma: number;
/** Local contrast (mid-tone detail). Negative softens. */
clarity: number;
sharpen: number;
blur: number;
/** Film grain amount. */
grain: number;
/** Positive darkens the edges, negative lightens them. */
vignette: number;
}FrameStateA frame around the photo.
interface FrameState {
style: FrameStyle;
/** Thickness as a fraction of the output's short side (0.005…0.2). */
size: number;
color: string;
}FrameStyleFrame styles, drawn over the photo's edges (the output size never changes).
type FrameStyle = 'border' | 'rounded' | 'bevel' | 'line' | 'double' | 'inset' | 'plus' | 'lumber' | 'corners' | 'polaroid';GeometryStateRotation, flips, straighten, perspective and crop.
interface GeometryState {
/** Clockwise rotation in 90° steps, applied first. */
rotation: QuarterTurn;
/** Mirror horizontally / vertically, as seen after `rotation`. */
flipX: boolean;
flipY: boolean;
/** Fine rotation in degrees (−45…45, clockwise), around the image centre. */
straighten: number;
/**
* Perspective correction: `x` tilts around the vertical axis (left/right edges),
* `y` around the horizontal axis (top/bottom edges). −1…1, 0 = none.
*/
perspective: {
x: number;
y: number;
};
/**
* Crop in "oriented" space: the image after rotation, flip, straighten and perspective, with the
* origin at the top-left of the rotated-but-not-straightened frame. `null` keeps the whole image.
*/
crop: Rect | null;
/** Locked crop aspect ratio (width ÷ height), or `null` for free-form. */
cropAspect: number | null;
/** `ellipse` makes the result round (transparent corners, or `background` for JPEG). */
cropShape: CropShape;
}HistoryUndo / redo steps.
interface History<T> {
past: HistoryEntry<T>[];
future: HistoryEntry<T>[];
}HistoryEntrySnapshot history. Each entry stores a full immutable state; Immer's structural sharing makes that cheap (unchanged parts are shared between snapshots). See DECISIONS.md #20.
interface HistoryEntry<T> {
state: T;
/** What the change *after* this snapshot did, e.g. "Rotate". Shown in the history panel. */
label: string;
}ImageFormatImage formats recognised from a file's first bytes.
type ImageFormat = 'jpeg' | 'png' | 'gif' | 'webp' | 'avif' | 'heic' | 'bmp' | 'ico' | 'tiff' | 'svg' | 'psd';ImageLoadErrorCodeWhy an image didn't open: - unsupported — a real image, but in a format this browser can't show (e.g. HEIC in Chrome). - damaged — looks like a supported image but can't be decoded (truncated/corrupt file). - not-image — the file isn't an image at all (PDF, text…). - network — the URL couldn't be downloaded (404, offline, blocked by CORS).
type ImageLoadErrorCode = 'unsupported' | 'damaged' | 'not-image' | 'network';ImageShapeAn image element (sticker, logo, pasted image).
interface ImageShape extends ShapeBase {
type: 'image';
x: number;
y: number;
width: number;
height: number;
/** Key into `EditState.assets`. */
assetId: string;
}ImageSourceAnything the editor can open. - string: an http(s) URL, data: URL or blob: URL - Blob / File: e.g. from an <input type="file"> - DOM image sources you already have in memory
type ImageSource = string | Blob | HTMLImageElement | HTMLCanvasElement | ImageBitmap;LevelsStateInput levels: black/white points (0…1) and a mid-tone shift (−1…1, positive = brighter).
interface LevelsState {
black: number;
white: number;
mid: number;
}LineCapLine ends: none, arrow or circle.
type LineCap = 'none' | 'arrow' | 'circle';LineShapeA line or arrow element.
interface LineShape extends ShapeBase {
type: 'line';
/** Start and end. */
points: [Point, Point];
stroke: string;
strokeWidth: number;
startCap: LineCap;
endCap: LineCap;
}LoadedImageThe decoded source image. Never mutated — every edit is described separately.
interface LoadedImage {
/** Decoded pixels with EXIF orientation already applied. */
bitmap: ImageBitmap;
width: number;
height: number;
/** e.g. `image/jpeg`; `null` when unknown (DOM sources). */
mimeType: string | null;
/** Original file name without extension, when known. */
name: string | null;
/**
* The source JPEG's EXIF block (TIFF bytes), read only for `ExportOptions.keepMetadata`.
* Missing for other formats and DOM sources.
*/
exif?: Uint8Array;
}LoadImageOptionsOptions for loadImage.
interface LoadImageOptions {
/** Abort a slow load, e.g. when the user picks another image. */
signal?: AbortSignal;
/** Sent with URL requests. Defaults to `anonymous` so the canvas stays exportable. */
crossOrigin?: 'anonymous' | 'use-credentials';
}LookA saved colour "look": finetune + levels + curves + filter. Geometry is not part of a look, so it can be applied to any photo.
interface Look {
id: string;
name: string;
finetune: FinetuneState;
levels: LevelsState;
curves: CurvesState;
filter: FilterState | null;
}PaintAny CSS colour, or null for none.
type Paint = string | null;PathShapeFreehand pen stroke or polygon.
interface PathShape extends ShapeBase {
type: 'path';
points: Point[];
closed: boolean;
/** Draw with smooth curves through the points (pen) instead of straight segments (polygon). */
smooth: boolean;
fill: Paint;
stroke: Paint;
strokeWidth: number;
}PointA point in CSS pixels.
interface Point {
x: number;
y: number;
}QuarterTurnClockwise quarter turns, in degrees.
type QuarterTurn = 0 | 90 | 180 | 270;RasterAssetA raster produced during editing (e.g. by an AI plugin): a background mask, an erased patch, an upscaled base image. Referenced from other parts of the state by its id in assets.
interface RasterAsset {
kind: 'raster';
/** `data:` URL, `blob:` URL or remote URL. */
src: string;
width: number;
height: number;
mimeType: string;
/** Who made it, e.g. `plugin-ai/remove-background@1`. */
createdBy?: string;
/** Hash of the inputs, so a result can be reused or re-generated. */
inputHash?: string;
}RectAxis-aligned rectangle in pixels.
interface Rect {
x: number;
y: number;
width: number;
height: number;
}RectShapeA rectangle element.
interface RectShape extends ShapeBase {
type: 'rect';
x: number;
y: number;
width: number;
height: number;
fill: Paint;
stroke: Paint;
strokeWidth: number;
cornerRadius: number;
}RedactBoxA rectangular area.
interface RedactBox extends RedactionBase {
kind: 'box';
x: number;
y: number;
width: number;
height: number;
}RedactBrushA painted area: a round brush along points, size = diameter (oriented px).
interface RedactBrush extends RedactionBase {
kind: 'brush';
points: Point[];
size: number;
}RedactionA redaction area (box or brush stroke).
type Redaction = RedactBox | RedactBrush;RedactShapeA redaction area (pixelate / blur / solid) as an element (DECISIONS #88): it hides the photo and every element below it in the list. Box or brush stroke — the fields of RedactBox / RedactBrush.
type RedactShape = (RedactBox | RedactBrush) & Omit<ShapeBase, 'id' | 'rotation' | 'type'> & {
type: 'redact';
};RedactStyleRedactions hide parts of the photo (faces, plates, names). They live in oriented space, like annotations, and are drawn after the colour pipeline and before annotations (DECISIONS #68).
type RedactStyle = 'pixelate' | 'blur' | 'solid';RenderedCanvasA rendered result on a 2D canvas, before encoding.
interface RenderedCanvas {
canvas: AnyCanvas;
width: number;
height: number;
renderer: RendererKind;
/** Smaller than asked for (see `ExportResult.downscaled`). */
downscaled: boolean;
}RendererKindwebgl2, or canvas2d where WebGL2 isn't available.
type RendererKind = 'webgl2' | 'canvas2d';ResizeStateFinal pixel size. Aspect may differ from the crop, which stretches the result.
interface ResizeState {
width: number;
height: number;
}ShapeAny element on the photo: shapes, text, images, redaction areas, the watermark's place.
type Shape = RectShape | EllipseShape | LineShape | PathShape | TextShape | ImageShape | RedactShape | WatermarkShape;ShapeTypeElement types.
type ShapeType = 'rect' | 'ellipse' | 'line' | 'path' | 'text' | 'image' | 'redact' | 'watermark';SizeA width/height pair in pixels.
interface Size {
width: number;
height: number;
}TaskContextPassed to runTask jobs: report progress, check for cancellation.
interface TaskContext {
/** Aborted when the user cancels or the editor closes. Pass it to fetch/models. */
signal: AbortSignal;
progress(value: number | null): void;
}TextAlignText alignment in a text element.
type TextAlign = 'left' | 'center' | 'right';TextShapeA text element.
interface TextShape extends ShapeBase {
type: 'text';
/** Top-left of the text box; the height follows the wrapped text. */
x: number;
y: number;
/** Wrap width. */
width: number;
text: string;
fontFamily: string;
fontSize: number;
fontWeight: 400 | 700;
fontStyle: 'normal' | 'italic';
align: TextAlign;
/** Line height as a multiple of the font size. */
lineHeight: number;
color: string;
/** Optional box behind the text (label / highlight style). */
background: Paint;
}ToolIdIds of the built-in tools.
type ToolId = (typeof TOOL_IDS)[number];ViewportWhere the image sits on the stage. x/y are the screen position (CSS px, relative to the stage) of the image's top-left corner, scale is CSS px per image px.
interface Viewport {
scale: number;
x: number;
y: number;
}ViewportChangeOptionsOptions for zoom / pan changes.
interface ViewportChangeOptions {
/** Stage point that stays fixed while zooming. Defaults to the stage centre. */
anchor?: Point;
/** Animate the change using `animationMs`. */
animate?: boolean;
}WatermarkPositionNine spots on the photo, custom = wherever the user dragged it (x, y), or tile = a repeating, tilted pattern across all of it.
type WatermarkPosition = 'top-left' | 'top' | 'top-right' | 'left' | 'center' | 'right' | 'bottom-left' | 'bottom' | 'bottom-right' | 'custom' | 'tile';WatermarkShapeWhere the watermark is drawn in the element order. Its look and layout stay in EditState.watermark; with no marker in the list it's drawn on top of everything. Only one; ignored when there's no watermark (and for an app-locked one, which is always on top).
interface WatermarkShape extends ShapeBase {
type: 'watermark';
}WatermarkStateA text or logo watermark: position, size, opacity, colour.
interface WatermarkState {
kind: 'text' | 'image';
/** For `text`. */
text: string;
fontFamily: string;
fontWeight: 400 | 700;
color: string;
/** For `image`: an entry in `EditState.assets`. */
assetId: string | null;
position: WatermarkPosition;
/** Centre of the mark for `custom`, as fractions of the output's width / height. */
x: number;
y: number;
/** Degrees clockwise around the mark's centre (not for `tile`, which has its own tilt). */
rotation: number;
/**
* 0.01…1: share of the largest size that fits — 1 = the mark fills the photo's width (or height)
* inside the margin. For `tile`, 1 = marks half that big.
*/
size: number;
opacity: number;
/** Distance from the edges as a fraction of the output's short side (0…0.25). */
margin: number;
}