@ascentsparksoftware/react-image-editor 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +411 -0
- package/dist/index.d.ts +1023 -0
- package/dist/index.js +7715 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +1766 -0
- package/package.json +72 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,1023 @@
|
|
|
1
|
+
import { ReactElement } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Loader contracts for the editor's optional, heavyweight features — AI
|
|
5
|
+
* background removal (`@imgly/background-removal` + `onnxruntime-web`) and HEIC
|
|
6
|
+
* decoding (`heic2any`).
|
|
7
|
+
*
|
|
8
|
+
* The core editor deliberately does NOT `import()` these packages. They ship
|
|
9
|
+
* WASM, web workers and deep CJS graphs that the dev-server bundlers (Vite's
|
|
10
|
+
* optimizer in particular) cannot pre-bundle reliably; having them in the core's
|
|
11
|
+
* import graph breaks the editor for EVERY consumer, whether or not they use the
|
|
12
|
+
* feature. Instead the consumer installs the package it wants and injects a
|
|
13
|
+
* loader (see `providers.ts`). The heavy `import()` then lives in the consumer's
|
|
14
|
+
* own code, where they own the bundler config and only they pay the cost.
|
|
15
|
+
*
|
|
16
|
+
* These are intentionally MINIMAL structural types describing only the call
|
|
17
|
+
* surface the editor uses, so this file has zero dependency on the optional
|
|
18
|
+
* packages' own type declarations.
|
|
19
|
+
*/
|
|
20
|
+
/** The `heic2any` call surface the decoder uses. */
|
|
21
|
+
type Heic2AnyFn = (opts: {
|
|
22
|
+
blob: Blob;
|
|
23
|
+
toType?: string;
|
|
24
|
+
quality?: number;
|
|
25
|
+
}) => Promise<Blob | Blob[]>;
|
|
26
|
+
/** Whatever `() => import('heic2any')` resolves to — a module namespace or the bare fn. */
|
|
27
|
+
type AspHeicDecoderModule = {
|
|
28
|
+
readonly default?: Heic2AnyFn;
|
|
29
|
+
} | Heic2AnyFn;
|
|
30
|
+
/**
|
|
31
|
+
* A loader that resolves the `heic2any` decoder on demand. Register it with
|
|
32
|
+
* `provideAspHeicDecoder(() => import('heic2any'))` to enable HEIC/HEIF import.
|
|
33
|
+
*/
|
|
34
|
+
type AspHeicDecoderLoader = () => Promise<AspHeicDecoderModule>;
|
|
35
|
+
/** The `@imgly/background-removal` call surface the engine uses. */
|
|
36
|
+
type RemoveBackgroundFn = (source: Blob | string, config?: {
|
|
37
|
+
progress?: (key: string, current: number, total: number) => void;
|
|
38
|
+
}) => Promise<Blob>;
|
|
39
|
+
/** Whatever `() => import('@imgly/background-removal')` resolves to. */
|
|
40
|
+
interface AspBackgroundRemovalModule {
|
|
41
|
+
readonly removeBackground: RemoveBackgroundFn;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* A loader that resolves the background-removal engine on demand. Register it
|
|
45
|
+
* with `provideAspBackgroundRemoval(() => import('@imgly/background-removal'))`
|
|
46
|
+
* to enable the Remove-background / Cut-out-subject tools.
|
|
47
|
+
*/
|
|
48
|
+
type AspBackgroundRemovalLoader = () => Promise<AspBackgroundRemovalModule>;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The editor's scoped CSS custom-property contract.
|
|
52
|
+
*
|
|
53
|
+
* Every visual style in the library references one of these `--asp-*` variables,
|
|
54
|
+
* and {@link deriveTheme} produces a value for each from the three theming inputs.
|
|
55
|
+
* Hosts may override any individual variable in their own CSS for fine control.
|
|
56
|
+
*/
|
|
57
|
+
/** Color tokens derived at runtime from `baseColor` + `accentColor` + `themeMode`. */
|
|
58
|
+
declare const COLOR_TOKEN_NAMES: readonly ["--asp-bg", "--asp-surface", "--asp-surface-2", "--asp-surface-sunk", "--asp-ink", "--asp-ink-700", "--asp-ink-muted", "--asp-ink-faint", "--asp-line", "--asp-line-strong", "--asp-accent", "--asp-accent-ink", "--asp-accent-hover", "--asp-accent-soft", "--asp-accent-soft-ink", "--asp-ring", "--asp-scrim", "--asp-success", "--asp-warning", "--asp-error"];
|
|
59
|
+
/** Non-color tokens: fixed dimensional/typographic values, the same in every theme. */
|
|
60
|
+
declare const STATIC_TOKEN_NAMES: readonly ["--asp-radius-sm", "--asp-radius-md", "--asp-radius-lg", "--asp-radius-pill", "--asp-ctl-h", "--asp-ctl-h-sm", "--asp-font-mono"];
|
|
61
|
+
/** Every token name the editor sets on its root element. */
|
|
62
|
+
declare const THEME_TOKEN_NAMES: readonly ["--asp-bg", "--asp-surface", "--asp-surface-2", "--asp-surface-sunk", "--asp-ink", "--asp-ink-700", "--asp-ink-muted", "--asp-ink-faint", "--asp-line", "--asp-line-strong", "--asp-accent", "--asp-accent-ink", "--asp-accent-hover", "--asp-accent-soft", "--asp-accent-soft-ink", "--asp-ring", "--asp-scrim", "--asp-success", "--asp-warning", "--asp-error", "--asp-radius-sm", "--asp-radius-md", "--asp-radius-lg", "--asp-radius-pill", "--asp-ctl-h", "--asp-ctl-h-sm", "--asp-font-mono"];
|
|
63
|
+
type ThemeTokenName = (typeof THEME_TOKEN_NAMES)[number];
|
|
64
|
+
/** A fully-resolved theme: every token name mapped to a CSS value string. */
|
|
65
|
+
type AspThemeTokens = Record<ThemeTokenName, string>;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Derive the full `--asp-*` token set from the three theming inputs.
|
|
69
|
+
*
|
|
70
|
+
* Strategy: surfaces, ink, and lines are generated as a perceptually-even
|
|
71
|
+
* lightness scale tinted toward the BASE hue (so the editor blends with the
|
|
72
|
+
* host's neutral palette); interactive tokens are generated from the ACCENT.
|
|
73
|
+
* Text tokens are then run through {@link ensureContrastAA} against their actual
|
|
74
|
+
* background so WCAG AA (AAA for primary ink) is GUARANTEED for any input pair,
|
|
75
|
+
* in both light and dark — the 3 inputs alone always yield a legible result.
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
/** Light or dark derivation. */
|
|
79
|
+
type AspThemeMode = 'light' | 'dark';
|
|
80
|
+
/**
|
|
81
|
+
* Build the complete theme token map.
|
|
82
|
+
*
|
|
83
|
+
* @param baseColor neutral anchor (hex) — surfaces/ink/lines tint toward its hue.
|
|
84
|
+
* @param accentColor interactive accent (hex) — kept exactly as the brand color.
|
|
85
|
+
* @param mode `'light'` or `'dark'`.
|
|
86
|
+
* @throws {Error} if either color is not valid hex.
|
|
87
|
+
*/
|
|
88
|
+
declare function deriveTheme(baseColor: string, accentColor: string, mode: AspThemeMode): AspThemeTokens;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Public type contract for the editor.
|
|
92
|
+
*
|
|
93
|
+
* The tool and filter unions are derived from `as const` source arrays so the
|
|
94
|
+
* registry (and any exhaustiveness check) is structurally guaranteed to cover
|
|
95
|
+
* every member — there is one source of truth, not a union plus a parallel list
|
|
96
|
+
* that can drift.
|
|
97
|
+
*/
|
|
98
|
+
/** Layout/baseline preset — sets the chrome and a default tool set. */
|
|
99
|
+
type AspMode = 'viewer' | 'basic' | 'advanced' | 'full';
|
|
100
|
+
/**
|
|
101
|
+
* A host-controllable size: a number (interpreted as `px`) or any CSS length —
|
|
102
|
+
* `'600px'`, `'70%'`, `'80vh'`, `'calc(100vh - 120px)'`. Used for the editor's
|
|
103
|
+
* `width`/`height` inputs. A per-mode minimum is always enforced on top so the
|
|
104
|
+
* toolbars and panels keep enough room to render.
|
|
105
|
+
*/
|
|
106
|
+
type AspSize = number | string;
|
|
107
|
+
/** Every tool the editor can expose, grouped by capability. Source of truth for {@link AspTool}. */
|
|
108
|
+
declare const ALL_TOOLS: readonly ["crop", "rotate", "straighten", "flip", "resize", "pen", "highlighter", "eraser", "shapes", "arrow", "line", "text", "sticker", "redact", "magicwand", "removebg", "selectsubject", "adjust", "filters", "select", "layers", "duplicate", "delete", "opacity", "align", "group", "background", "frame"];
|
|
109
|
+
type AspTool = (typeof ALL_TOOLS)[number];
|
|
110
|
+
/** Fabric.js built-in filters exposed by the editor. Source of truth for {@link AspFilter}. */
|
|
111
|
+
declare const ALL_FILTERS: readonly ["brightness", "contrast", "saturation", "vibrance", "hue", "blur", "sharpen", "grayscale", "sepia", "invert", "pixelate", "noise", "gamma", "blendColor"];
|
|
112
|
+
type AspFilter = (typeof ALL_FILTERS)[number];
|
|
113
|
+
/** Export formats. `json` serializes the re-editable Fabric scene; `pdf` embeds a raster. */
|
|
114
|
+
type AspExportFormat = 'png' | 'jpeg' | 'webp' | 'svg' | 'json' | 'pdf';
|
|
115
|
+
/** A structured error surfaced via the `errorOccurred` output. */
|
|
116
|
+
interface AspEditorError {
|
|
117
|
+
/** Stable machine code, e.g. `'load-failed'`, `'export-failed'`, `'engine-init-failed'`. */
|
|
118
|
+
readonly code: string;
|
|
119
|
+
readonly message: string;
|
|
120
|
+
}
|
|
121
|
+
/** Crop aspect-ratio presets. */
|
|
122
|
+
type AspAspectPreset = 'free' | '1:1' | '4:3' | '16:9' | '3:2' | 'original';
|
|
123
|
+
/**
|
|
124
|
+
* A host-defined crop aspect option — e.g. a CMS target like a 1200×630 social
|
|
125
|
+
* image. `ratio` is width / height; build it from explicit dimensions with
|
|
126
|
+
* {@link aspectOption}.
|
|
127
|
+
*/
|
|
128
|
+
interface AspAspectOption {
|
|
129
|
+
readonly label: string;
|
|
130
|
+
readonly ratio: number;
|
|
131
|
+
}
|
|
132
|
+
/** Build an {@link AspAspectOption} from explicit pixel dimensions. */
|
|
133
|
+
declare function aspectOption(width: number, height: number, label?: string): AspAspectOption;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Lazy web-font loading for the text tool.
|
|
137
|
+
*
|
|
138
|
+
* A chosen family is loaded by injecting the Google Fonts stylesheet once and
|
|
139
|
+
* awaiting the CSS Font Loading API, so text renders in the correct font rather
|
|
140
|
+
* than a fallback. System/generic stacks need no loading. SSR-safe (no-op when
|
|
141
|
+
* `document` is absent), idempotent, and resolves even if loading fails (the
|
|
142
|
+
* fallback simply renders).
|
|
143
|
+
*/
|
|
144
|
+
interface FontOption {
|
|
145
|
+
readonly label: string;
|
|
146
|
+
/** CSS `font-family` value applied to the text object. */
|
|
147
|
+
readonly value: string;
|
|
148
|
+
}
|
|
149
|
+
/** Default font choices: a system stack plus popular Google families. */
|
|
150
|
+
declare const DEFAULT_FONTS: readonly FontOption[];
|
|
151
|
+
|
|
152
|
+
interface ImageEditorProps {
|
|
153
|
+
readonly src?: string | Blob | null;
|
|
154
|
+
readonly mode?: AspMode;
|
|
155
|
+
/**
|
|
156
|
+
* Editor width — a number (px) or any CSS length (`'70%'`, `'80vh'`,
|
|
157
|
+
* `'calc(100vw - 320px)'`). Defaults to filling the host's container. A
|
|
158
|
+
* per-mode minimum is always enforced so the chrome stays usable.
|
|
159
|
+
*/
|
|
160
|
+
readonly width?: AspSize | null;
|
|
161
|
+
/** Editor height — same shape as {@link ImageEditorProps.width}. */
|
|
162
|
+
readonly height?: AspSize | null;
|
|
163
|
+
readonly tools?: readonly AspTool[] | null;
|
|
164
|
+
readonly disabledTools?: readonly AspTool[];
|
|
165
|
+
readonly filters?: readonly AspFilter[] | 'all' | null;
|
|
166
|
+
readonly aspectPresets?: readonly AspAspectPreset[];
|
|
167
|
+
/**
|
|
168
|
+
* Aspect the crop starts at. When set, the editor opens already constrained to
|
|
169
|
+
* it (in basic mode the crop frame is live immediately; in advanced/full it is
|
|
170
|
+
* the crop tool's starting aspect). When unset, a sole non-`free` preset is
|
|
171
|
+
* auto-selected, otherwise the crop opens unconstrained (`free`).
|
|
172
|
+
*/
|
|
173
|
+
readonly initialAspect?: AspAspectPreset | null;
|
|
174
|
+
/** Host-defined crop aspect targets (e.g. CMS sizes); shown after the presets. */
|
|
175
|
+
readonly aspectRatios?: readonly AspAspectOption[];
|
|
176
|
+
readonly exportFormats?: readonly AspExportFormat[];
|
|
177
|
+
readonly exportQuality?: number;
|
|
178
|
+
readonly baseColor?: string;
|
|
179
|
+
readonly accentColor?: string;
|
|
180
|
+
readonly themeMode?: AspThemeMode;
|
|
181
|
+
/** Heading shown by the `basic` modal layout. */
|
|
182
|
+
readonly heading?: string;
|
|
183
|
+
/** Show the edit-history panel in the workspace (hosts that don't want it set false). */
|
|
184
|
+
readonly showHistory?: boolean;
|
|
185
|
+
/** Enable keyboard shortcuts while the pointer is over the editor. */
|
|
186
|
+
readonly keyboardEnabled?: boolean;
|
|
187
|
+
/** Available text fonts (host-overridable). */
|
|
188
|
+
readonly fonts?: readonly FontOption[];
|
|
189
|
+
/**
|
|
190
|
+
* Enable AI background removal / subject cut-out. Install
|
|
191
|
+
* `@imgly/background-removal` in the consuming app and pass its dynamic import:
|
|
192
|
+
* `backgroundRemovalLoader={() => import('@imgly/background-removal')}`.
|
|
193
|
+
* The heavy WASM/worker `import()` then lives in the consumer's bundle, not the
|
|
194
|
+
* library's. Without it the Remove-background / Cut-out tools are hidden.
|
|
195
|
+
*/
|
|
196
|
+
readonly backgroundRemovalLoader?: AspBackgroundRemovalLoader | null;
|
|
197
|
+
/**
|
|
198
|
+
* Enable HEIC/HEIF image import. Install `heic2any` in the consuming app and
|
|
199
|
+
* pass its dynamic import: `heicDecoderLoader={() => import('heic2any')}`.
|
|
200
|
+
* Without it, importing a HEIC/HEIF file throws a descriptive error.
|
|
201
|
+
*/
|
|
202
|
+
readonly heicDecoderLoader?: AspHeicDecoderLoader | null;
|
|
203
|
+
readonly onSaved?: (blob: Blob) => void;
|
|
204
|
+
readonly onCanceled?: () => void;
|
|
205
|
+
/** Fired after an image successfully loads (initial, picker, or upload). */
|
|
206
|
+
readonly onImageLoaded?: () => void;
|
|
207
|
+
/** Fired with the exported Blob when the user downloads from the Export menu. */
|
|
208
|
+
readonly onExported?: (blob: Blob) => void;
|
|
209
|
+
/** Fired on a recoverable error (load/export/engine init) instead of throwing. */
|
|
210
|
+
readonly onError?: (error: AspEditorError) => void;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* `<ImageEditor>` — the editor's root/container component.
|
|
214
|
+
*
|
|
215
|
+
* Owns the {@link EditorController} (and through it the Fabric engine), the
|
|
216
|
+
* resolved tool/filter sets, theming, and all workspace state. Presentational
|
|
217
|
+
* children (rail, options panel, history, layers) render data and emit intent;
|
|
218
|
+
* this container is the only place that drives the engine.
|
|
219
|
+
*/
|
|
220
|
+
declare function ImageEditor({ src, mode, width, height, tools, disabledTools, filters, aspectPresets, initialAspect, aspectRatios, exportFormats, exportQuality, baseColor, accentColor, themeMode, heading, showHistory, keyboardEnabled, fonts, backgroundRemovalLoader, heicDecoderLoader, onSaved, onCanceled, onImageLoaded, onExported, onError, }: ImageEditorProps): ReactElement;
|
|
221
|
+
|
|
222
|
+
/** Options for {@link openImageEditorDialog}. */
|
|
223
|
+
interface OpenImageEditorConfig {
|
|
224
|
+
readonly src?: string | Blob | null;
|
|
225
|
+
readonly heading?: string;
|
|
226
|
+
readonly baseColor?: string;
|
|
227
|
+
readonly accentColor?: string;
|
|
228
|
+
readonly themeMode?: AspThemeMode;
|
|
229
|
+
readonly aspectPresets?: readonly AspAspectPreset[];
|
|
230
|
+
/** Aspect the crop opens constrained to (e.g. `'1:1'` for an avatar). */
|
|
231
|
+
readonly initialAspect?: AspAspectPreset;
|
|
232
|
+
readonly exportFormats?: readonly AspExportFormat[];
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Opens the editor's `basic` layout in a modal overlay and resolves with the
|
|
236
|
+
* saved image Blob, or `null` if the user cancels (close button, scrim click,
|
|
237
|
+
* or Escape). Implemented with a dedicated React root on a detached scrim so
|
|
238
|
+
* it works from any event handler without a host component.
|
|
239
|
+
*/
|
|
240
|
+
declare function openImageEditorDialog(config?: OpenImageEditorConfig): Promise<Blob | null>;
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Data-driven tool and filter catalog.
|
|
244
|
+
*
|
|
245
|
+
* The rail, options panels, and resolution all read from these tables — there
|
|
246
|
+
* is no per-tool branching anywhere else. `mode`/`tools`/`disabledTools`/
|
|
247
|
+
* `filters` resolve against this metadata (see `resolve-tools.ts`).
|
|
248
|
+
*/
|
|
249
|
+
|
|
250
|
+
type AspToolGroup = 'transform' | 'annotate' | 'color' | 'object' | 'canvas';
|
|
251
|
+
interface ToolMeta {
|
|
252
|
+
readonly key: AspTool;
|
|
253
|
+
readonly label: string;
|
|
254
|
+
/** Iconify/Lucide icon id. */
|
|
255
|
+
readonly icon: string;
|
|
256
|
+
readonly group: AspToolGroup;
|
|
257
|
+
}
|
|
258
|
+
/** A filter is either a continuous "adjustment" (slider) or a one-tap "look". */
|
|
259
|
+
type FilterKind = 'adjustment' | 'look';
|
|
260
|
+
interface FilterMeta {
|
|
261
|
+
readonly key: AspFilter;
|
|
262
|
+
readonly label: string;
|
|
263
|
+
readonly kind: FilterKind;
|
|
264
|
+
/** Slider bounds + default for adjustments (omitted for looks). */
|
|
265
|
+
readonly min?: number;
|
|
266
|
+
readonly max?: number;
|
|
267
|
+
readonly defaultValue?: number;
|
|
268
|
+
readonly unit?: string;
|
|
269
|
+
}
|
|
270
|
+
declare const TOOL_REGISTRY: Record<AspTool, ToolMeta>;
|
|
271
|
+
declare const FILTER_REGISTRY: Record<AspFilter, FilterMeta>;
|
|
272
|
+
declare const DEFAULT_TOOLS: Record<AspMode, readonly AspTool[]>;
|
|
273
|
+
/** Default filter set per mode. `'all'` means every Fabric filter. */
|
|
274
|
+
declare const DEFAULT_FILTERS: Record<AspMode, readonly AspFilter[] | 'all'>;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Resolve the visible tool and filter sets from the public inputs.
|
|
278
|
+
*
|
|
279
|
+
* Tools: `tools` (explicit allowlist) ?? default-for-`mode`, then minus `disabledTools`.
|
|
280
|
+
* Filters: `filters` (`'all'` | explicit list) ?? default-for-`mode`.
|
|
281
|
+
*
|
|
282
|
+
* In all cases the output is de-duplicated and restricted to known catalog
|
|
283
|
+
* members, so a stray or misspelled entry can never crash the UI or render a
|
|
284
|
+
* phantom control. Explicit lists preserve their given order; an explicit empty
|
|
285
|
+
* list is honored as "show nothing" (it is an override, not a fall-through).
|
|
286
|
+
*/
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Resolve the ordered set of enabled tools.
|
|
290
|
+
*
|
|
291
|
+
* @param mode baseline preset.
|
|
292
|
+
* @param tools explicit allowlist, or `null` to use the mode default.
|
|
293
|
+
* @param disabledTools tools to subtract from the resolved set.
|
|
294
|
+
*/
|
|
295
|
+
declare function resolveTools(mode: AspMode, tools: readonly AspTool[] | null, disabledTools: readonly AspTool[]): AspTool[];
|
|
296
|
+
/**
|
|
297
|
+
* Resolve the ordered set of enabled filters.
|
|
298
|
+
*
|
|
299
|
+
* @param mode baseline preset.
|
|
300
|
+
* @param filters explicit list, the literal `'all'`, or `null` for the mode default.
|
|
301
|
+
*/
|
|
302
|
+
declare function resolveFilters(mode: AspMode, filters: readonly AspFilter[] | 'all' | null): AspFilter[];
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* A bounded linear undo/redo history.
|
|
306
|
+
*
|
|
307
|
+
* Each entry pairs a human label (for the History panel) with an opaque state
|
|
308
|
+
* snapshot (the editor stores serialized Fabric scenes here). Pure and fully
|
|
309
|
+
* unit-testable — it holds no Fabric references.
|
|
310
|
+
*/
|
|
311
|
+
interface HistoryEntry<T> {
|
|
312
|
+
readonly label: string;
|
|
313
|
+
readonly state: T;
|
|
314
|
+
}
|
|
315
|
+
declare class EditHistory<T> {
|
|
316
|
+
private readonly stack;
|
|
317
|
+
private cursor;
|
|
318
|
+
private readonly maxEntries;
|
|
319
|
+
constructor(initialLabel: string, initialState: T, maxEntries?: number);
|
|
320
|
+
/** All retained entries, oldest first. */
|
|
321
|
+
get entries(): readonly HistoryEntry<T>[];
|
|
322
|
+
/** Index of the current entry within {@link entries}. */
|
|
323
|
+
get index(): number;
|
|
324
|
+
get length(): number;
|
|
325
|
+
get current(): HistoryEntry<T>;
|
|
326
|
+
get canUndo(): boolean;
|
|
327
|
+
get canRedo(): boolean;
|
|
328
|
+
/**
|
|
329
|
+
* Record a new state. Any redo branch ahead of the cursor is discarded, and
|
|
330
|
+
* the oldest entries are dropped once the cap is exceeded.
|
|
331
|
+
*/
|
|
332
|
+
push(label: string, state: T): void;
|
|
333
|
+
/** Step back one entry, or return `null` if already at the start. */
|
|
334
|
+
undo(): HistoryEntry<T> | null;
|
|
335
|
+
/** Step forward one entry, or return `null` if already at the end. */
|
|
336
|
+
redo(): HistoryEntry<T> | null;
|
|
337
|
+
/** Replace the entire history with a single fresh entry. */
|
|
338
|
+
reset(label: string, state: T): void;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* A bounded linear undo/redo history that stores **diffs**, not full snapshots.
|
|
343
|
+
*
|
|
344
|
+
* The editor serializes its whole scene (often hundreds of KB once an image is
|
|
345
|
+
* loaded) on every edit. Keeping 50 full snapshots would cost tens of MB. This
|
|
346
|
+
* history keeps one full base plus a chain of jsondiffpatch deltas — each
|
|
347
|
+
* incremental edit (move a shape, tweak a color) is a sub-KB delta — and
|
|
348
|
+
* reconstructs any state on demand by replaying deltas from the base.
|
|
349
|
+
*
|
|
350
|
+
* Behavior is identical to a full-snapshot stack: `current`/`undo`/`redo`
|
|
351
|
+
* return the exact serialized state, branches truncate on push, and the oldest
|
|
352
|
+
* entries drop once the cap is exceeded (the new oldest is re-materialized into
|
|
353
|
+
* the base so the chain stays valid). Pure and unit-testable — no Fabric refs.
|
|
354
|
+
*/
|
|
355
|
+
|
|
356
|
+
/** Minimal projection of an entry for the History panel (labels only). */
|
|
357
|
+
interface HistoryStep {
|
|
358
|
+
readonly label: string;
|
|
359
|
+
}
|
|
360
|
+
declare class DeltaHistory {
|
|
361
|
+
private readonly differ;
|
|
362
|
+
private readonly stack;
|
|
363
|
+
private cursor;
|
|
364
|
+
private readonly maxEntries;
|
|
365
|
+
constructor(initialLabel: string, initialState: string, maxEntries?: number);
|
|
366
|
+
/** Labels for every retained entry, oldest first (for the History panel). */
|
|
367
|
+
get entries(): readonly HistoryStep[];
|
|
368
|
+
get index(): number;
|
|
369
|
+
get length(): number;
|
|
370
|
+
get current(): HistoryEntry<string>;
|
|
371
|
+
/** The base entry (index 0) — used by the engine's full "reset edits" action. */
|
|
372
|
+
get first(): HistoryEntry<string>;
|
|
373
|
+
get canUndo(): boolean;
|
|
374
|
+
get canRedo(): boolean;
|
|
375
|
+
/**
|
|
376
|
+
* Record a new state. The redo branch ahead of the cursor is discarded, and
|
|
377
|
+
* the oldest entries drop once the cap is exceeded.
|
|
378
|
+
*/
|
|
379
|
+
push(label: string, state: string): void;
|
|
380
|
+
/** Step back one entry, or return `null` if already at the start. */
|
|
381
|
+
undo(): HistoryEntry<string> | null;
|
|
382
|
+
/** Step forward one entry, or return `null` if already at the end. */
|
|
383
|
+
redo(): HistoryEntry<string> | null;
|
|
384
|
+
/** Replace the entire history with a single fresh base entry. */
|
|
385
|
+
reset(label: string, state: string): void;
|
|
386
|
+
/** Serialized byte length of the delta retained for an entry (for tests/metrics). */
|
|
387
|
+
retainedDeltaBytes(index: number): number;
|
|
388
|
+
/** Reconstruct the serialized state at an index by replaying deltas from the base. */
|
|
389
|
+
private entryAt;
|
|
390
|
+
/**
|
|
391
|
+
* Replay the delta chain from the base (index 0) up to `targetIndex`. The
|
|
392
|
+
* base is deep-cloned first so patching never mutates retained state.
|
|
393
|
+
*/
|
|
394
|
+
private reconstruct;
|
|
395
|
+
/**
|
|
396
|
+
* Drop the base entry and promote the next one to a fresh base by
|
|
397
|
+
* materializing its full state (its delta no longer has a predecessor).
|
|
398
|
+
*/
|
|
399
|
+
private dropOldest;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* EditorEngine — the only component that talks to Fabric.js directly.
|
|
404
|
+
*
|
|
405
|
+
* It owns a Fabric `Canvas` holding one base `FabricImage` (the photo) plus
|
|
406
|
+
* annotation objects on top, and exposes high-level editor operations: load,
|
|
407
|
+
* zoom/pan, rotate/flip/straighten, crop, adjustments/filters, shapes/text/
|
|
408
|
+
* free-draw, undo/redo (serialized-scene snapshots), and export. UI components
|
|
409
|
+
* call these methods and never import Fabric themselves.
|
|
410
|
+
*
|
|
411
|
+
* Not unit-tested in jsdom (Fabric needs a real canvas/WebGL); verified through
|
|
412
|
+
* the demo with Playwright screenshots, per the project's visual-truth rule.
|
|
413
|
+
*/
|
|
414
|
+
|
|
415
|
+
interface EngineOptions {
|
|
416
|
+
readonly width: number;
|
|
417
|
+
readonly height: number;
|
|
418
|
+
/**
|
|
419
|
+
* Loader for AI background removal (`@imgly/background-removal`). When absent,
|
|
420
|
+
* the Remove-background / Cut-out tools are unavailable. Kept out of the core
|
|
421
|
+
* import graph on purpose — see `engine/loaders.ts`.
|
|
422
|
+
*/
|
|
423
|
+
readonly backgroundRemovalLoader?: AspBackgroundRemovalLoader | null;
|
|
424
|
+
/**
|
|
425
|
+
* Loader for the HEIC/HEIF decoder (`heic2any`). When absent, importing a
|
|
426
|
+
* HEIC/HEIF file throws a descriptive error.
|
|
427
|
+
*/
|
|
428
|
+
readonly heicDecoderLoader?: AspHeicDecoderLoader | null;
|
|
429
|
+
}
|
|
430
|
+
type ShapeKind = 'rect' | 'ellipse' | 'line' | 'arrow' | 'triangle' | 'diamond' | 'pentagon' | 'hexagon' | 'star';
|
|
431
|
+
type RedactMode = 'blur' | 'pixelate' | 'solid';
|
|
432
|
+
interface AnnotationStyle {
|
|
433
|
+
readonly color: string;
|
|
434
|
+
readonly strokeWidth: number;
|
|
435
|
+
/** Rectangle corner radius (px, in object coordinates). `0`/absent = sharp corners. */
|
|
436
|
+
readonly cornerRadius?: number;
|
|
437
|
+
}
|
|
438
|
+
interface TextStyle {
|
|
439
|
+
readonly color: string;
|
|
440
|
+
readonly fontSize: number;
|
|
441
|
+
readonly fontFamily?: string;
|
|
442
|
+
}
|
|
443
|
+
/** Rich-text attributes of the selected text, for the Text panel toggles. */
|
|
444
|
+
interface TextStyleInfo {
|
|
445
|
+
readonly bold: boolean;
|
|
446
|
+
readonly italic: boolean;
|
|
447
|
+
readonly underline: boolean;
|
|
448
|
+
readonly strike: boolean;
|
|
449
|
+
readonly align: string;
|
|
450
|
+
/** The text's current font family, so the panel can reflect it on selection. */
|
|
451
|
+
readonly fontFamily: string;
|
|
452
|
+
}
|
|
453
|
+
/** Editable style of the current selection, surfaced to the host UI. */
|
|
454
|
+
interface SelectionStyleInfo {
|
|
455
|
+
/** `'text'` → color is fill + size is fontSize; `'stroke'` → color is stroke + size is strokeWidth. */
|
|
456
|
+
readonly kind: 'text' | 'stroke';
|
|
457
|
+
readonly color: string;
|
|
458
|
+
readonly size: number;
|
|
459
|
+
/** Present when a single text object is selected. */
|
|
460
|
+
readonly textStyle?: TextStyleInfo;
|
|
461
|
+
/** Present when a single rectangle is selected: its current corner radius. */
|
|
462
|
+
readonly cornerRadius?: number;
|
|
463
|
+
/** Present when a single rectangle is selected: the pill-cap radius for its size. */
|
|
464
|
+
readonly cornerRadiusMax?: number;
|
|
465
|
+
}
|
|
466
|
+
/** One entry in the layers panel (top of the z-stack first). */
|
|
467
|
+
interface LayerInfo {
|
|
468
|
+
readonly id: string;
|
|
469
|
+
readonly label: string;
|
|
470
|
+
readonly locked: boolean;
|
|
471
|
+
readonly visible: boolean;
|
|
472
|
+
readonly selected: boolean;
|
|
473
|
+
/** Object opacity, 0–1. */
|
|
474
|
+
readonly opacity: number;
|
|
475
|
+
/** False for the base image, which should not be deletable. */
|
|
476
|
+
readonly removable: boolean;
|
|
477
|
+
}
|
|
478
|
+
/** A target output size in pixels for the artboard / export region. */
|
|
479
|
+
interface ArtboardSize {
|
|
480
|
+
readonly width: number;
|
|
481
|
+
readonly height: number;
|
|
482
|
+
}
|
|
483
|
+
/** Progress of an in-browser AI operation (model fetch + inference). */
|
|
484
|
+
interface AiProgress {
|
|
485
|
+
/** `'loading'` while fetching the model, `'processing'` during inference, `'done'`. */
|
|
486
|
+
readonly stage: 'loading' | 'processing' | 'done';
|
|
487
|
+
/** 0–1 within the current stage (best-effort; some stages report no total). */
|
|
488
|
+
readonly progress: number;
|
|
489
|
+
}
|
|
490
|
+
/** A user-placed guide line at a fixed scene coordinate. */
|
|
491
|
+
interface ManualGuide {
|
|
492
|
+
readonly id: string;
|
|
493
|
+
/** `h` = horizontal line at scene-y `pos`; `v` = vertical line at scene-x `pos`. */
|
|
494
|
+
readonly orientation: 'h' | 'v';
|
|
495
|
+
readonly pos: number;
|
|
496
|
+
}
|
|
497
|
+
/** The current view transform plus canvas size — everything a ruler needs. */
|
|
498
|
+
interface Viewport {
|
|
499
|
+
readonly zoom: number;
|
|
500
|
+
readonly panX: number;
|
|
501
|
+
readonly panY: number;
|
|
502
|
+
readonly width: number;
|
|
503
|
+
readonly height: number;
|
|
504
|
+
}
|
|
505
|
+
declare class EditorEngine {
|
|
506
|
+
private readonly canvas;
|
|
507
|
+
private readonly fabric;
|
|
508
|
+
private readonly history;
|
|
509
|
+
private baseImage;
|
|
510
|
+
private rotation;
|
|
511
|
+
private straighten;
|
|
512
|
+
private zoomPct;
|
|
513
|
+
private adjustments;
|
|
514
|
+
private looks;
|
|
515
|
+
private frame;
|
|
516
|
+
private idCounter;
|
|
517
|
+
private clipboard;
|
|
518
|
+
private panMode;
|
|
519
|
+
private panLast;
|
|
520
|
+
private snapEnabled;
|
|
521
|
+
private activeGuides;
|
|
522
|
+
private artboard;
|
|
523
|
+
/** Committed crop region (scene coords) — drives the dim mask and raster/PDF export. */
|
|
524
|
+
private cropRegion;
|
|
525
|
+
/** The interactive crop frame while the Crop tool is active, else null. */
|
|
526
|
+
private cropFrame;
|
|
527
|
+
/** Aspect ratio (w/h) constraining the crop frame, or null for free crop. */
|
|
528
|
+
private cropRatio;
|
|
529
|
+
/** Per-object interactivity saved while a crop session locks the rest of the scene. */
|
|
530
|
+
private cropPrevState;
|
|
531
|
+
private cropPrevUniformScaling;
|
|
532
|
+
/**
|
|
533
|
+
* Avatar-style crop (basic mode): a FIXED crop frame, with the base image
|
|
534
|
+
* panned and zoomed underneath it. Distinct from the movable `cropFrame` of the
|
|
535
|
+
* advanced workspace.
|
|
536
|
+
*/
|
|
537
|
+
private imageCropActive;
|
|
538
|
+
/** The "cover" scale at which the image exactly fills the fixed crop frame (zoom floor). */
|
|
539
|
+
private imageCropFitScale;
|
|
540
|
+
private rulersEnabled;
|
|
541
|
+
private manualGuides;
|
|
542
|
+
/** Live preview of a guide being dragged from a ruler (not yet committed). */
|
|
543
|
+
private guideDraft;
|
|
544
|
+
/** Id of an existing manual guide being dragged on the canvas, if any. */
|
|
545
|
+
private draggingGuideId;
|
|
546
|
+
private guideIdCounter;
|
|
547
|
+
private selectionListener;
|
|
548
|
+
private layersListener;
|
|
549
|
+
private viewportListener;
|
|
550
|
+
private guidesListener;
|
|
551
|
+
private textMode;
|
|
552
|
+
private textPlacementListener;
|
|
553
|
+
private textFinishListener;
|
|
554
|
+
private pendingFinishText;
|
|
555
|
+
private redactPlacement;
|
|
556
|
+
private magicMode;
|
|
557
|
+
private magicListener;
|
|
558
|
+
private aiProgressListener;
|
|
559
|
+
private onFontsLoaded;
|
|
560
|
+
private lastViewportKey;
|
|
561
|
+
/** Consumer-injected loaders for the optional heavy features (may be null). */
|
|
562
|
+
private readonly bgRemovalLoader;
|
|
563
|
+
private readonly heicLoader;
|
|
564
|
+
/** Snap distance in *screen* pixels; divided by zoom to get a scene threshold. */
|
|
565
|
+
private static readonly SNAP_PX;
|
|
566
|
+
/** Pointer proximity (screen px) for grabbing a manual guide on the canvas. */
|
|
567
|
+
private static readonly GUIDE_GRAB_PX;
|
|
568
|
+
private constructor();
|
|
569
|
+
private nextId;
|
|
570
|
+
/** Register a callback fired when the active selection (and its style) changes. */
|
|
571
|
+
setSelectionListener(cb: (info: SelectionStyleInfo | null) => void): void;
|
|
572
|
+
/** Register a callback fired when the layer set or its state changes. */
|
|
573
|
+
setLayersListener(cb: () => void): void;
|
|
574
|
+
private notifyLayers;
|
|
575
|
+
private notifySelection;
|
|
576
|
+
private describeSelection;
|
|
577
|
+
/**
|
|
578
|
+
* Set the corner radius of the currently selected rectangle. The value is in
|
|
579
|
+
* on-screen pixels and is clamped to the rectangle's pill cap. No-op (returns
|
|
580
|
+
* false) when the selection is not a single rectangle. Pass `commit: false`
|
|
581
|
+
* for live slider drags; commit once on release.
|
|
582
|
+
*/
|
|
583
|
+
setSelectedCornerRadius(radius: number, commit?: boolean): boolean;
|
|
584
|
+
/**
|
|
585
|
+
* Apply a color and/or size to the currently selected object(s), routing by
|
|
586
|
+
* object type (text → fill/fontSize, shapes & paths → stroke/strokeWidth, and
|
|
587
|
+
* recursing into groups such as arrows). Returns false if nothing is selected.
|
|
588
|
+
* Pass `commit: false` for live slider drags; commit once on release.
|
|
589
|
+
*/
|
|
590
|
+
styleActiveObject(style: {
|
|
591
|
+
color?: string;
|
|
592
|
+
size?: number;
|
|
593
|
+
fontFamily?: string;
|
|
594
|
+
}, commit?: boolean): boolean;
|
|
595
|
+
private styleOne;
|
|
596
|
+
/** Enable/disable edge & center snapping. Clears any visible guides when off. */
|
|
597
|
+
setSnapping(enabled: boolean): void;
|
|
598
|
+
/** Scene-space bounding box of an object from its absolute corner coords. */
|
|
599
|
+
private sceneBox;
|
|
600
|
+
/**
|
|
601
|
+
* Nudge a dragged object so a near edge/center aligns to the canvas or another
|
|
602
|
+
* object, and record the guide lines to draw. Snap threshold is constant in
|
|
603
|
+
* *screen* pixels (zoom-aware) so it feels the same at any zoom.
|
|
604
|
+
*/
|
|
605
|
+
private applySnap;
|
|
606
|
+
/** Pick the smallest within-threshold offset from any anchor to any line. */
|
|
607
|
+
private bestSnap;
|
|
608
|
+
/** Paint active guides onto the overlay context, mapped through the viewport. */
|
|
609
|
+
private drawGuides;
|
|
610
|
+
/** Drop the guides and repaint so the overlay is clean. */
|
|
611
|
+
private clearGuides;
|
|
612
|
+
/**
|
|
613
|
+
* Erase the overlay (top) context. `contextTop` can be momentarily undefined
|
|
614
|
+
* outside a render cycle, so this guards before touching it; clearing by the
|
|
615
|
+
* backing canvas's pixel size is correct regardless of retina scaling.
|
|
616
|
+
*/
|
|
617
|
+
private clearOverlay;
|
|
618
|
+
/** Register a callback fired when the view transform or canvas size changes. */
|
|
619
|
+
setViewportListener(cb: () => void): void;
|
|
620
|
+
/** Register a callback fired when manual guides (or the live draft) change. */
|
|
621
|
+
setGuidesListener(cb: () => void): void;
|
|
622
|
+
private notifyGuides;
|
|
623
|
+
/** Current view transform + canvas size, in CSS pixels / scene units. */
|
|
624
|
+
getViewport(): Viewport;
|
|
625
|
+
private notifyViewportIfChanged;
|
|
626
|
+
/** Show/hide rulers; when off, an in-progress guide draft is dropped. */
|
|
627
|
+
setRulersEnabled(enabled: boolean): void;
|
|
628
|
+
isRulersEnabled(): boolean;
|
|
629
|
+
getManualGuides(): readonly ManualGuide[];
|
|
630
|
+
/** The live guide preview during a ruler drag, or null. */
|
|
631
|
+
getGuideDraft(): ManualGuide | null;
|
|
632
|
+
/** Map a viewport (screen, CSS-px) point to scene coordinates. */
|
|
633
|
+
viewportToScene(vx: number, vy: number): {
|
|
634
|
+
x: number;
|
|
635
|
+
y: number;
|
|
636
|
+
};
|
|
637
|
+
/** Map a client (page) point to canvas viewport (CSS-px) coordinates. */
|
|
638
|
+
clientToViewport(clientX: number, clientY: number): {
|
|
639
|
+
x: number;
|
|
640
|
+
y: number;
|
|
641
|
+
};
|
|
642
|
+
/**
|
|
643
|
+
* Show a live guide preview at a scene position (during a ruler drag).
|
|
644
|
+
* Pass `null` to clear the preview. Does not touch history.
|
|
645
|
+
*/
|
|
646
|
+
setGuideDraft(orientation: 'h' | 'v', scenePos: number | null): void;
|
|
647
|
+
/** Commit a new manual guide at a scene position and record it in history. */
|
|
648
|
+
addManualGuide(orientation: 'h' | 'v', scenePos: number): void;
|
|
649
|
+
/** Remove every manual guide and record it in history (no-op if already empty). */
|
|
650
|
+
clearManualGuides(): void;
|
|
651
|
+
/** The manual guide whose line is within grab range of a screen point, or null. */
|
|
652
|
+
private guideAtViewport;
|
|
653
|
+
/** Live-move the guide being dragged to the scene coordinate under the pointer. */
|
|
654
|
+
private dragGuideTo;
|
|
655
|
+
/**
|
|
656
|
+
* Finish a guide drag. Dropping the line outside the canvas removes it;
|
|
657
|
+
* otherwise the move is committed to history. Restores object selection.
|
|
658
|
+
*/
|
|
659
|
+
private endGuideDrag;
|
|
660
|
+
/**
|
|
661
|
+
* Set (or clear) the artboard — a fixed output region. Content outside the
|
|
662
|
+
* region is dimmed on screen and excluded from raster/PDF export, which is
|
|
663
|
+
* rendered at exactly the artboard's pixel dimensions. `null` exports the
|
|
664
|
+
* whole canvas (the default).
|
|
665
|
+
*/
|
|
666
|
+
setArtboard(size: ArtboardSize | null): void;
|
|
667
|
+
getArtboard(): ArtboardSize | null;
|
|
668
|
+
/** True while the interactive crop frame is on the canvas. */
|
|
669
|
+
isCropping(): boolean;
|
|
670
|
+
/** True once a crop region has been applied (drives the dim mask + export). */
|
|
671
|
+
hasCropRegion(): boolean;
|
|
672
|
+
/**
|
|
673
|
+
* Begin an interactive crop. Drops a draggable, resizable frame over the canvas
|
|
674
|
+
* (rule-of-thirds + dimmed surroundings), starting from any existing crop region
|
|
675
|
+
* or a centered default for the given aspect `ratio` (null = free). The rest of
|
|
676
|
+
* the scene is made non-interactive until {@link applyCropRegion} or
|
|
677
|
+
* {@link cancelCrop}.
|
|
678
|
+
*/
|
|
679
|
+
beginCrop(ratio: number | null): void;
|
|
680
|
+
/** Reshape the active crop frame to a new aspect `ratio` (null = free). */
|
|
681
|
+
setCropRatio(ratio: number | null): void;
|
|
682
|
+
/** Only corner handles when an aspect ratio is locked; all handles when free. */
|
|
683
|
+
private applyCropControls;
|
|
684
|
+
/** Keep the dragged/resized crop frame within the canvas (and on-ratio). */
|
|
685
|
+
private constrainCropFrame;
|
|
686
|
+
/** Commit the crop frame as the output region and end the session. */
|
|
687
|
+
applyCropRegion(): void;
|
|
688
|
+
/** End the crop session without applying (the previous region is kept). */
|
|
689
|
+
cancelCrop(): void;
|
|
690
|
+
/** Clear any committed crop region (back to the full canvas). */
|
|
691
|
+
clearCropRegion(): void;
|
|
692
|
+
/** True while the fixed-frame, pan-and-zoom-the-image crop is active. */
|
|
693
|
+
isImageCropping(): boolean;
|
|
694
|
+
/**
|
|
695
|
+
* Begin an avatar-style crop: a FIXED crop frame of `ratio`, with the base
|
|
696
|
+
* image grabbable to pan and zoomable (via {@link zoomImageCrop}) underneath
|
|
697
|
+
* it. The image always covers the frame — there are no gaps. `null` ratio ends
|
|
698
|
+
* the image crop (full image).
|
|
699
|
+
*/
|
|
700
|
+
beginImageCrop(ratio: number | null): void;
|
|
701
|
+
/** Switch the fixed crop frame to a new aspect (recenters + re-covers the image). */
|
|
702
|
+
setImageCropRatio(ratio: number | null): void;
|
|
703
|
+
/**
|
|
704
|
+
* Zoom the image within the fixed crop frame. `pct` is 100..400 where 100% is
|
|
705
|
+
* the "cover" floor (image exactly fills the frame). Zooms about the frame
|
|
706
|
+
* centre and keeps the frame covered.
|
|
707
|
+
*/
|
|
708
|
+
zoomImageCrop(pct: number): void;
|
|
709
|
+
/** Commit the avatar crop: keep the fixed region as the export region, end the mode. */
|
|
710
|
+
applyImageCrop(): void;
|
|
711
|
+
/** End the avatar crop and clear the region (full image, no crop). */
|
|
712
|
+
cancelImageCrop(): void;
|
|
713
|
+
private endImageCrop;
|
|
714
|
+
/** Place the fixed crop frame for `ratio` and cover it with the image, centred. */
|
|
715
|
+
private setImageCropFrame;
|
|
716
|
+
/** The smallest image scale at which its (rotated) bounds still cover `frame`. */
|
|
717
|
+
private coverScaleFor;
|
|
718
|
+
/** Keep the panned/zoomed image fully covering the fixed crop frame (no gaps). */
|
|
719
|
+
private clampImageToCover;
|
|
720
|
+
/** Remove the crop frame and restore the rest of the scene's interactivity. */
|
|
721
|
+
private endCropSession;
|
|
722
|
+
/**
|
|
723
|
+
* The artboard's on-canvas rectangle in scene coordinates: the largest
|
|
724
|
+
* centered rectangle of the artboard's aspect ratio that fits the canvas.
|
|
725
|
+
* Returns null when no artboard is set.
|
|
726
|
+
*/
|
|
727
|
+
private artboardRect;
|
|
728
|
+
/** Render the output region to a data URL at its target pixel size. */
|
|
729
|
+
private artboardDataUrl;
|
|
730
|
+
/** Output pixel size of the current region (crop region in scene px, else artboard preset). */
|
|
731
|
+
private outputSize;
|
|
732
|
+
/**
|
|
733
|
+
* Dim the canvas outside the current output region and outline it, on the
|
|
734
|
+
* overlay context. During an interactive crop the region is the live crop
|
|
735
|
+
* frame (with a rule-of-thirds grid and no extra outline, since Fabric draws
|
|
736
|
+
* the frame and its handles); otherwise it is the committed crop/artboard.
|
|
737
|
+
*/
|
|
738
|
+
private drawArtboardMask;
|
|
739
|
+
/** The current output region (committed crop, else centered artboard), scene coords. */
|
|
740
|
+
private outputRect;
|
|
741
|
+
/** The live crop frame's rectangle in scene coordinates (bakes its scale). */
|
|
742
|
+
private cropFrameRect;
|
|
743
|
+
/** Create an engine bound to a `<canvas>` element. */
|
|
744
|
+
static create(canvasEl: HTMLCanvasElement, options: EngineOptions): Promise<EditorEngine>;
|
|
745
|
+
/**
|
|
746
|
+
* Resolve an import source into a canvas-ready URL.
|
|
747
|
+
*
|
|
748
|
+
* - SVG → rasterized at high resolution (Fabric's vector parser mangles complex
|
|
749
|
+
* SVGs and a raw `<img>` misreads their intrinsic size).
|
|
750
|
+
* - Blob / data URL → decoded with EXIF orientation and downscaled, converting
|
|
751
|
+
* formats the browser can't decode natively (HEIC/HEIF). Throws a descriptive
|
|
752
|
+
* error on undecodable input rather than silently yielding nothing.
|
|
753
|
+
* - Remote URL → returned as-is and left to Fabric, since canvas-resampling a
|
|
754
|
+
* cross-origin image would taint it.
|
|
755
|
+
*/
|
|
756
|
+
private resolveImportUrl;
|
|
757
|
+
/**
|
|
758
|
+
* Load an image from a URL or Blob, fit it to the canvas, and reset history.
|
|
759
|
+
*
|
|
760
|
+
* A Blob is first decoded into a data URL so the image's serialized `src`
|
|
761
|
+
* survives in history snapshots (a transient object URL would be revoked and
|
|
762
|
+
* break undo).
|
|
763
|
+
*/
|
|
764
|
+
loadImage(src: string | Blob): Promise<void>;
|
|
765
|
+
get hasImage(): boolean;
|
|
766
|
+
private fitBaseImage;
|
|
767
|
+
/** Rotate the image by a signed multiple of 90°. */
|
|
768
|
+
rotateBy(deg: number): void;
|
|
769
|
+
/** Fine straighten angle, −45..45°. */
|
|
770
|
+
setStraighten(deg: number, commit?: boolean): void;
|
|
771
|
+
private applyAngle;
|
|
772
|
+
/** Image-crop zoom as a percentage where 100% is the cover floor (for the slider). */
|
|
773
|
+
get imageCropZoomPct(): number;
|
|
774
|
+
/** Flip the image horizontally or vertically. */
|
|
775
|
+
flip(axis: 'h' | 'v'): void;
|
|
776
|
+
get zoom(): number;
|
|
777
|
+
setZoom(pct: number): void;
|
|
778
|
+
zoomBy(deltaPct: number): void;
|
|
779
|
+
resetView(): void;
|
|
780
|
+
/** Crop the base image to a centered rectangle of the given aspect preset. */
|
|
781
|
+
applyCrop(preset: AspAspectPreset): void;
|
|
782
|
+
/**
|
|
783
|
+
* Crop the base image to a centered rectangle of an arbitrary width/height
|
|
784
|
+
* ratio (`null` = full image). Use this for host-defined / CMS aspect targets.
|
|
785
|
+
*/
|
|
786
|
+
applyCropRatio(ratio: number | null): void;
|
|
787
|
+
/** Merge adjustment values and re-render. Pass `commit` on slider release. */
|
|
788
|
+
setAdjustments(values: Partial<Record<AspFilter, number>>, commit?: boolean): void;
|
|
789
|
+
/** Toggle a one-tap look filter (grayscale/sepia/invert/sharpen). */
|
|
790
|
+
toggleLook(look: AspFilter): void;
|
|
791
|
+
isLookActive(look: AspFilter): boolean;
|
|
792
|
+
getAdjustment(key: AspFilter): number;
|
|
793
|
+
private rebuildFilters;
|
|
794
|
+
/** Add a shape at the canvas center. */
|
|
795
|
+
addShape(kind: ShapeKind, style: AnnotationStyle): void;
|
|
796
|
+
private buildArrow;
|
|
797
|
+
/** Add an editable text box at the canvas center. */
|
|
798
|
+
addText(text: string, style: TextStyle): void;
|
|
799
|
+
/**
|
|
800
|
+
* Enable/disable the "click to place text" mode (the Text tool). While on, the
|
|
801
|
+
* cursor is a text caret and clicking empty canvas drops an editable box.
|
|
802
|
+
*/
|
|
803
|
+
setTextMode(enabled: boolean): void;
|
|
804
|
+
/** Register the callback fired with a scene point when text mode is clicked. */
|
|
805
|
+
setTextPlacementListener(cb: (point: {
|
|
806
|
+
x: number;
|
|
807
|
+
y: number;
|
|
808
|
+
}) => void): void;
|
|
809
|
+
/** Enable/disable magic-wand mode (a canvas click flood-fill erases a region). */
|
|
810
|
+
setMagicMode(enabled: boolean): void;
|
|
811
|
+
/** Register the callback fired with a scene point when magic mode is clicked. */
|
|
812
|
+
setMagicListener(cb: (point: {
|
|
813
|
+
x: number;
|
|
814
|
+
y: number;
|
|
815
|
+
}) => void): void;
|
|
816
|
+
/**
|
|
817
|
+
* Register the callback fired when an empty-canvas click in text mode finishes
|
|
818
|
+
* an already-placed text (so the host can switch back to the Select tool).
|
|
819
|
+
*/
|
|
820
|
+
setTextFinishListener(cb: () => void): void;
|
|
821
|
+
/**
|
|
822
|
+
* Add a text box at a scene point and immediately enter in-place editing with
|
|
823
|
+
* the placeholder pre-selected, so the user just starts typing (Photoshop-style).
|
|
824
|
+
*/
|
|
825
|
+
addTextAt(x: number, y: number, style: TextStyle): void;
|
|
826
|
+
private placeText;
|
|
827
|
+
/**
|
|
828
|
+
* Add a movable/resizable redaction marquee centered on the canvas. The user
|
|
829
|
+
* positions it over the area to conceal; transient until {@link applyRedaction}
|
|
830
|
+
* bakes it.
|
|
831
|
+
*/
|
|
832
|
+
addRedactionMarquee(): void;
|
|
833
|
+
/** Add a redaction marquee centered at a scene point (used by click-to-place). */
|
|
834
|
+
addRedactionMarqueeAt(cx: number, cy: number): void;
|
|
835
|
+
/**
|
|
836
|
+
* Enable/disable click-to-place for redaction: while on, clicking empty canvas
|
|
837
|
+
* drops a new marquee there (but only when one isn't already present, so you
|
|
838
|
+
* reposition the existing box rather than spawning duplicates).
|
|
839
|
+
*/
|
|
840
|
+
setRedactPlacement(enabled: boolean): void;
|
|
841
|
+
/** Remove the redaction marquee without applying it. */
|
|
842
|
+
cancelRedaction(): void;
|
|
843
|
+
/**
|
|
844
|
+
* Bake the redaction: conceal the COMPOSITED content under the marquee. `solid`
|
|
845
|
+
* lays an opaque box; `blur`/`pixelate` sample the rendered pixels in the region
|
|
846
|
+
* (so all underlying layers are hidden) and place a filtered, opaque patch.
|
|
847
|
+
*/
|
|
848
|
+
applyRedaction(mode: RedactMode): Promise<void>;
|
|
849
|
+
private findByRole;
|
|
850
|
+
/**
|
|
851
|
+
* Apply a decorative frame around the image. Each style maps to a distinct,
|
|
852
|
+
* real border rendering (`none` clears it). The frame is a non-interactive
|
|
853
|
+
* rectangle tagged with `aspRole: 'frame'` so it can be re-found and replaced
|
|
854
|
+
* even after a history restore rebuilds every canvas object.
|
|
855
|
+
*/
|
|
856
|
+
applyFrame(style: string, color: string): void;
|
|
857
|
+
/**
|
|
858
|
+
* Set the canvas background to a solid color (`'transparent'` clears it, showing
|
|
859
|
+
* the checkerboard). Fabric serializes `backgroundColor`, so it survives undo.
|
|
860
|
+
*/
|
|
861
|
+
setBackground(color: string): void;
|
|
862
|
+
/** Set the canvas background to a linear gradient of the given color stops. */
|
|
863
|
+
setBackgroundGradient(colors: readonly string[]): void;
|
|
864
|
+
/** Set an uploaded image as the canvas background, scaled to cover. */
|
|
865
|
+
setBackgroundImage(src: string | Blob): Promise<void>;
|
|
866
|
+
/** Remove every canvas object tagged with the given `aspRole`. */
|
|
867
|
+
private removeTagged;
|
|
868
|
+
/**
|
|
869
|
+
* Enable or disable freehand drawing. When `highlighter` is set, the brush is
|
|
870
|
+
* translucent and wider so strokes read as a highlight over the underlying
|
|
871
|
+
* content rather than an opaque line.
|
|
872
|
+
*/
|
|
873
|
+
setFreeDraw(enabled: boolean, style: AnnotationStyle, highlighter?: boolean): void;
|
|
874
|
+
/** Apply rich-text attributes (weight/style/underline/align/spacing/bg) to the active text. */
|
|
875
|
+
applyTextStyle(props: Record<string, string | number | boolean>, commit?: boolean): boolean;
|
|
876
|
+
/** Set the fill of the active non-text object(s) (`'transparent'` clears it). */
|
|
877
|
+
setActiveFill(color: string, commit?: boolean): boolean;
|
|
878
|
+
/** Set opacity (0–1) on the active object(s). Returns false if nothing selected. */
|
|
879
|
+
setOpacity(value: number, commit?: boolean): boolean;
|
|
880
|
+
/** Set opacity (0–1) on a specific layer. */
|
|
881
|
+
setLayerOpacity(id: string, value: number, commit?: boolean): void;
|
|
882
|
+
/** Delete the currently selected object(s). */
|
|
883
|
+
deleteActive(): void;
|
|
884
|
+
/** Clear the active selection. */
|
|
885
|
+
discardSelection(): void;
|
|
886
|
+
/** Select all editable (unlocked, non-base) objects. */
|
|
887
|
+
selectAll(): void;
|
|
888
|
+
/** Copy the current selection to the internal clipboard. */
|
|
889
|
+
copy(): Promise<void>;
|
|
890
|
+
/** Paste the clipboard contents, offset and selected. */
|
|
891
|
+
paste(): Promise<void>;
|
|
892
|
+
/** Add an image (e.g. pasted from the OS clipboard) as a movable object. */
|
|
893
|
+
addImageObject(src: string | Blob): Promise<void>;
|
|
894
|
+
/**
|
|
895
|
+
* Magic-wand erase: from a scene point, flood-fill the image under the pointer
|
|
896
|
+
* and clear (make transparent) every contiguous pixel within `tolerance`
|
|
897
|
+
* (0–100) of the clicked color. Great for knocking out a solid background.
|
|
898
|
+
* Operates on the base image, or a selected image layer if one is active.
|
|
899
|
+
*/
|
|
900
|
+
magicErase(scenePoint: {
|
|
901
|
+
x: number;
|
|
902
|
+
y: number;
|
|
903
|
+
}, tolerance: number): Promise<boolean>;
|
|
904
|
+
/** Register a callback for AI-operation progress (model fetch + inference). */
|
|
905
|
+
setAiProgressListener(cb: (info: AiProgress) => void): void;
|
|
906
|
+
/** Whether AI background removal has a loader wired (drives feature gating). */
|
|
907
|
+
get backgroundRemovalAvailable(): boolean;
|
|
908
|
+
/**
|
|
909
|
+
* In-browser AI background removal. Segments the target image (the base image,
|
|
910
|
+
* or a selected image layer) and either replaces it with the cut-out subject
|
|
911
|
+
* (`mode: 'replace'`) or adds the cut-out as a new layer (`mode: 'subject'`).
|
|
912
|
+
* The model is fetched and cached on first use; progress is reported via the AI
|
|
913
|
+
* progress listener. Returns false if there's no image to process.
|
|
914
|
+
*/
|
|
915
|
+
removeImageBackground(mode: 'replace' | 'subject'): Promise<boolean>;
|
|
916
|
+
/** Draw an image object's source bitmap to a fresh canvas and return a PNG blob. */
|
|
917
|
+
private imageToBlob;
|
|
918
|
+
/** Duplicate the current selection in place (offset). */
|
|
919
|
+
duplicateActive(): Promise<void>;
|
|
920
|
+
private addClones;
|
|
921
|
+
private setActive;
|
|
922
|
+
/** Group the current multi-selection into a single object. */
|
|
923
|
+
groupActive(): void;
|
|
924
|
+
/** Ungroup the selected group back into individual objects. */
|
|
925
|
+
ungroupActive(): void;
|
|
926
|
+
/** Align the active object/selection to an edge or center of the canvas. */
|
|
927
|
+
alignActive(mode: 'left' | 'center-h' | 'right' | 'top' | 'center-v' | 'bottom'): void;
|
|
928
|
+
/** Enable/disable space-drag panning (disables selection while active). */
|
|
929
|
+
setPanMode(enabled: boolean): void;
|
|
930
|
+
get canUndo(): boolean;
|
|
931
|
+
get canRedo(): boolean;
|
|
932
|
+
get historyEntries(): readonly HistoryStep[];
|
|
933
|
+
get historyIndex(): number;
|
|
934
|
+
undo(): Promise<void>;
|
|
935
|
+
redo(): Promise<void>;
|
|
936
|
+
/**
|
|
937
|
+
* Serialize the whole editor — scene objects, transform/adjustment/look/frame
|
|
938
|
+
* state, and the artboard — into a portable, versioned template string. The
|
|
939
|
+
* inverse of {@link loadScene}.
|
|
940
|
+
*/
|
|
941
|
+
exportScene(): string;
|
|
942
|
+
/**
|
|
943
|
+
* Restore a template produced by {@link exportScene}. Resets the history to
|
|
944
|
+
* the loaded state so it becomes the new undo baseline. Throws on malformed
|
|
945
|
+
* input rather than loading a partial scene.
|
|
946
|
+
*/
|
|
947
|
+
loadScene(json: string): Promise<void>;
|
|
948
|
+
/**
|
|
949
|
+
* Serialize the full editor state — the Fabric scene PLUS the engine's own
|
|
950
|
+
* transform/adjustment/look/frame state — so a restore puts both back in sync
|
|
951
|
+
* (the canvas alone does not capture rotation buckets, slider values, etc).
|
|
952
|
+
*/
|
|
953
|
+
private snapshot;
|
|
954
|
+
private commit;
|
|
955
|
+
private restore;
|
|
956
|
+
private findBaseImage;
|
|
957
|
+
/** Reset all edits back to the freshly-loaded image. */
|
|
958
|
+
reset(): Promise<void>;
|
|
959
|
+
get rotationAngle(): number;
|
|
960
|
+
get straightenAngle(): number;
|
|
961
|
+
/** A copy of the current adjustment values. */
|
|
962
|
+
getAdjustments(): Record<AspFilter, number>;
|
|
963
|
+
/** The single active look (UI is single-select), or null. */
|
|
964
|
+
get activeLook(): AspFilter | null;
|
|
965
|
+
get activeFrame(): string;
|
|
966
|
+
/** The layer stack, top of the z-order first (matching visual stacking). */
|
|
967
|
+
getLayers(): LayerInfo[];
|
|
968
|
+
private findById;
|
|
969
|
+
/**
|
|
970
|
+
* Select a layer by id (no-op if it is locked or missing). When `additive`
|
|
971
|
+
* (shift/cmd/ctrl-click in the panel), toggle the layer in/out of a
|
|
972
|
+
* multi-selection instead of replacing it.
|
|
973
|
+
*/
|
|
974
|
+
selectLayer(id: string, additive?: boolean): void;
|
|
975
|
+
/**
|
|
976
|
+
* Reorder the whole z-stack to match a display order (front-most first, as the
|
|
977
|
+
* Layers panel shows it). Used by drag-and-drop reordering.
|
|
978
|
+
*/
|
|
979
|
+
reorderLayers(displayOrderIds: readonly string[]): void;
|
|
980
|
+
/** Rename a layer; an empty/blank name clears back to the auto label. */
|
|
981
|
+
renameLayer(id: string, name: string): void;
|
|
982
|
+
/** Lock/unlock a layer. Locked layers are not selectable, so clicks pass through. */
|
|
983
|
+
toggleLayerLock(id: string): void;
|
|
984
|
+
private setLocked;
|
|
985
|
+
/** Show/hide a layer. */
|
|
986
|
+
toggleLayerVisible(id: string): void;
|
|
987
|
+
/** Move a layer up (forward) or down (backward) in the z-order. */
|
|
988
|
+
moveLayer(id: string, direction: 'up' | 'down'): void;
|
|
989
|
+
/** Delete a layer (the base image is protected). */
|
|
990
|
+
deleteLayer(id: string): void;
|
|
991
|
+
/** Export the current scene to a Blob in the requested format. */
|
|
992
|
+
exportImage(format: AspExportFormat, qualityPct: number, allowedFormats: readonly AspExportFormat[]): Promise<Blob>;
|
|
993
|
+
/**
|
|
994
|
+
* Inline the web fonts used by the scene's text into the SVG so it is
|
|
995
|
+
* self-contained. Network failures fall back to the original SVG (text keeps
|
|
996
|
+
* its font-family). No-op outside a browser (no `fetch`).
|
|
997
|
+
*/
|
|
998
|
+
private embedSvgFonts;
|
|
999
|
+
/** Distinct `fontFamily` values across all text objects (recursing into groups). */
|
|
1000
|
+
private collectTextFontFamilies;
|
|
1001
|
+
/**
|
|
1002
|
+
* Render into a single-page PDF. The page is sized to the artboard (when set)
|
|
1003
|
+
* or the full canvas, and the image is the matching region. jsPDF is lazy-loaded.
|
|
1004
|
+
*/
|
|
1005
|
+
private exportPdf;
|
|
1006
|
+
/** Pixel data of the current canvas, or null if no rendering context. */
|
|
1007
|
+
getImageData(): ImageData | null;
|
|
1008
|
+
/** Resize the editing surface. */
|
|
1009
|
+
setSize(width: number, height: number): void;
|
|
1010
|
+
/** Tear down the Fabric canvas and release resources. */
|
|
1011
|
+
destroy(): Promise<void>;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* Apply a derived theme to an element as scoped CSS custom properties.
|
|
1016
|
+
*
|
|
1017
|
+
* Tokens are set on the element's inline `style`, so they cascade only to the
|
|
1018
|
+
* editor's own subtree and never leak into (or clash with) the host page. Hosts
|
|
1019
|
+
* can still override any individual `--asp-*` variable with higher specificity.
|
|
1020
|
+
*/
|
|
1021
|
+
declare function applyTheme(element: HTMLElement, tokens: AspThemeTokens): void;
|
|
1022
|
+
|
|
1023
|
+
export { ALL_FILTERS, ALL_TOOLS, type AiProgress, type AnnotationStyle, type ArtboardSize, type AspAspectOption, type AspAspectPreset, type AspBackgroundRemovalLoader, type AspBackgroundRemovalModule, type AspEditorError, type AspExportFormat, type AspFilter, type AspHeicDecoderLoader, type AspHeicDecoderModule, type AspMode, type AspSize, type AspThemeMode, type AspThemeTokens, type AspTool, type AspToolGroup, COLOR_TOKEN_NAMES, DEFAULT_FILTERS, DEFAULT_FONTS, DEFAULT_TOOLS, DeltaHistory, EditHistory, EditorEngine, type EngineOptions, FILTER_REGISTRY, type FilterKind, type FilterMeta, type FontOption, type Heic2AnyFn, type HistoryEntry, type HistoryStep, ImageEditor, type ImageEditorProps, type LayerInfo, type ManualGuide, type OpenImageEditorConfig, type RedactMode, type RemoveBackgroundFn, STATIC_TOKEN_NAMES, type SelectionStyleInfo, type ShapeKind, THEME_TOKEN_NAMES, TOOL_REGISTRY, type TextStyle, type ThemeTokenName, type ToolMeta, type Viewport, applyTheme, aspectOption, deriveTheme, openImageEditorDialog, resolveFilters, resolveTools };
|