@coolms/image-editor-angular 2.0.0-alpha.2
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 +391 -0
- package/fesm2022/coolms-image-editor-angular.mjs +5543 -0
- package/fesm2022/coolms-image-editor-angular.mjs.map +1 -0
- package/package.json +47 -0
- package/types/coolms-image-editor-angular.d.ts +1488 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dmitry Popov / CoolMS contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
# @coolms/image-editor-angular
|
|
2
|
+
|
|
3
|
+
Modern image editor for CoolMS DXP. Built on fabric.js v6 via a
|
|
4
|
+
custom `FabricEngineAdapter`, wrapped by a CoolMS-native UI shell.
|
|
5
|
+
|
|
6
|
+
## Status
|
|
7
|
+
|
|
8
|
+
**Feature-complete on the fabric.js v6 engine.** The shell
|
|
9
|
+
(top toolbar, canvas mount, right sidebar, properties panel) drives
|
|
10
|
+
all editing operations through the engine API. The save flow is
|
|
11
|
+
integrated end-to-end with the Media Library and the VFS file
|
|
12
|
+
manager: open an image asset (or any VFS image file), edit, Save
|
|
13
|
+
(overwrite) or Save as new, and the source surface reflects the
|
|
14
|
+
change after thumbnail regeneration.
|
|
15
|
+
|
|
16
|
+
The engine adapter pattern was the foundation that made the engine
|
|
17
|
+
swap from Toast UI to fabric.js v6 viable without touching the UI
|
|
18
|
+
shell or any tool component.
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
### Editing tools
|
|
23
|
+
|
|
24
|
+
**Transform** (4 tools):
|
|
25
|
+
- **Crop** — interactive cropzone overlay built from a fabric `Rect`
|
|
26
|
+
with corner controls; aspect ratio presets (Free, 1:1, 4:3, 16:9,
|
|
27
|
+
3:2). Apply uses the bake-and-replace strategy: the image
|
|
28
|
+
is rendered with its current rotation/flip/filters into a canvas
|
|
29
|
+
at the bounding-box dimensions, the cropped sub-region is
|
|
30
|
+
extracted, and the FabricImage is replaced. The cropped output
|
|
31
|
+
is always axis-aligned regardless of how the image was rotated
|
|
32
|
+
when the user drew the crop overlay.
|
|
33
|
+
- **Rotate** — preset buttons (90° CW, 90° CCW, 180°) for instant
|
|
34
|
+
commits; free-rotation slider live-bound to the image's actual
|
|
35
|
+
angle (drag rotates in real time, single undo entry per drag
|
|
36
|
+
session)
|
|
37
|
+
- **Flip** — horizontal and vertical toggles. Button highlights
|
|
38
|
+
read from `state.imageFlipX/Y` so they stay accurate after
|
|
39
|
+
mouse-driven manipulations and undo/redo
|
|
40
|
+
- **Resize** — width and height numeric inputs with aspect lock,
|
|
41
|
+
live-synced to the visually-current dimensions (mouse-scaling on
|
|
42
|
+
the canvas updates the inputs in real time, until the user starts
|
|
43
|
+
typing). Apply commits a real image-data resize via fabric's
|
|
44
|
+
`toCanvasElement` round-trip (rotation, flip, filters all baked
|
|
45
|
+
into the new pixel data)
|
|
46
|
+
|
|
47
|
+
**Filters** (7 tools):
|
|
48
|
+
- **Brightness, Contrast, Saturation** — sliders, range −1..1
|
|
49
|
+
- **Blur** — slider, range 0..1
|
|
50
|
+
- **Sepia, Grayscale, Sharpen** — toggles, parameter-less
|
|
51
|
+
|
|
52
|
+
Slider filters live-preview during drag with a coalesced
|
|
53
|
+
`undo -> applyFilter` cycle so the undo stack stays at one entry per
|
|
54
|
+
preview session. `clearRedoStack()` on commit prevents stale preview
|
|
55
|
+
entries from leaking into the redo stack.
|
|
56
|
+
|
|
57
|
+
### Top toolbar
|
|
58
|
+
|
|
59
|
+
- **Undo** / **Redo** — bound to engine history events. The custom
|
|
60
|
+
command stack records one entry per logical
|
|
61
|
+
operation; mouse manipulations and sidebar-driven changes both
|
|
62
|
+
push commands through the same path.
|
|
63
|
+
- **Reset** — re-loads the original image and applies fit-to-viewport
|
|
64
|
+
zoom centred. Identity viewport transform is restored so the next
|
|
65
|
+
fit calculation lands the image at the viewport centre regardless
|
|
66
|
+
of where the user had zoomed/panned.
|
|
67
|
+
- **Zoom out** / **Zoom %** / **Zoom in** — viewport zoom only,
|
|
68
|
+
clamped 5%..1000% with a 1.25× step. Click the percentage to jump
|
|
69
|
+
to fit. **Ctrl+wheel** (Cmd+wheel on Mac, also covers trackpad
|
|
70
|
+
pinch) zooms with a 1.1× factor anchored at canvas centre.
|
|
71
|
+
Viewport zoom does not affect saved bytes (pure fabric viewport
|
|
72
|
+
transform).
|
|
73
|
+
- **Hand tool** — pan the canvas with mouse drag. Single click
|
|
74
|
+
toggles on, second click toggles off. While active, fabric's
|
|
75
|
+
selection and hit-testing are disabled so a drag on empty canvas
|
|
76
|
+
always pans rather than drawing a marquee.
|
|
77
|
+
- **Dimensions badge** — current image size in natural pixels.
|
|
78
|
+
|
|
79
|
+
### Direct mouse manipulation
|
|
80
|
+
|
|
81
|
+
Click the image to select it; fabric's corner controls render at
|
|
82
|
+
the image edges. Drag a corner to scale, the rotation handle
|
|
83
|
+
(top-centre) to rotate, or the body to move. On `mouseup` the
|
|
84
|
+
adapter pushes a single transform command spanning the entire
|
|
85
|
+
gesture, so undo reverts the whole drag in one step rather than
|
|
86
|
+
per-pixel. The sidebar tool inputs (Rotate slider, Resize
|
|
87
|
+
width/height) live-sync to the image's transform via the
|
|
88
|
+
`imageTransformed` event — they reflect mouse-driven changes in
|
|
89
|
+
real time, and changes from the sidebar drive the canvas the same
|
|
90
|
+
way. Both paths are authoritative; they cannot drift.
|
|
91
|
+
|
|
92
|
+
### Canvas frame
|
|
93
|
+
|
|
94
|
+
A thin orange rectangle (`rgba(245, 166, 35, 0.5)`) tracks the
|
|
95
|
+
image bounds as a visual hint of the image extent against the dark
|
|
96
|
+
canvas background. The frame mirrors the image's `angle / scaleX /
|
|
97
|
+
scaleY / flipX / flipY` so it follows rotation and flip natively;
|
|
98
|
+
`strokeWidth = 2 / zoom` keeps the visible stroke at a constant 1
|
|
99
|
+
visual pixel regardless of viewport zoom. It is `excludeFromExport:
|
|
100
|
+
true`, hidden during crop mode (so the dashed cropzone is the only
|
|
101
|
+
visible rectangle), and never reaches the saved bytes — the export
|
|
102
|
+
path renders only the FabricImage object.
|
|
103
|
+
|
|
104
|
+
### Save flow
|
|
105
|
+
|
|
106
|
+
- **Save** — overwrite the original asset (with confirmation dialog)
|
|
107
|
+
- **Save as...** — create a new asset with editable filename
|
|
108
|
+
(default `{stem}-edited{ext}`)
|
|
109
|
+
- **Cancel** — close without changes
|
|
110
|
+
|
|
111
|
+
The host calls `shell.deactivateActiveTool()` immediately before
|
|
112
|
+
`engine.export()` so any mid-edit tool (most importantly the
|
|
113
|
+
cropper, which renders a fabric overlay) cleans up before pixels
|
|
114
|
+
are read. Export uses fabric v6's `image.toBlob({format, quality})`
|
|
115
|
+
and `image.toDataURL` directly — they render only the FabricImage
|
|
116
|
+
object, so the canvas frame and any leftover overlay are never in
|
|
117
|
+
the output regardless of `excludeFromExport`.
|
|
118
|
+
|
|
119
|
+
## Architecture
|
|
120
|
+
|
|
121
|
+
Three layers, separated for swappability:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
UI shell --► ImageEditorEngine --► FabricEngineAdapter --► fabric.js v6
|
|
125
|
+
(interface) (this package) (npm peer dep)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
1. **UI shell** — `<coolms-image-editor>` host component with three
|
|
129
|
+
panes (top toolbar, canvas mount, right sidebar). Tools register
|
|
130
|
+
themselves into a `ToolRegistryService`; the right sidebar uses
|
|
131
|
+
`NgComponentOutlet` with `inputs` to render the active tool's
|
|
132
|
+
properties panel.
|
|
133
|
+
2. **Engine adapter** — `FabricEngineAdapter` implements
|
|
134
|
+
`ImageEditorEngine`. Owns canvas dimensions (no vendor CSS
|
|
135
|
+
auto-fit layer), normalises the public API (lowercase filter
|
|
136
|
+
names, `imageTransformed` event for bidirectional sync, single
|
|
137
|
+
`setCanvasDimensions` for viewport-driven resizing), maintains
|
|
138
|
+
a custom undo / redo command stack, and bumps fabric's filter
|
|
139
|
+
`textureSize` config to fit images larger than 4096 px before
|
|
140
|
+
the first WebGL filter pass.
|
|
141
|
+
3. **fabric.js v6** (vendor, MIT) — canvas rendering (`Canvas`),
|
|
142
|
+
image object (`FabricImage`), interactive controls (corner /
|
|
143
|
+
rotation handles), filter pipeline (WebGL backend with Canvas2D
|
|
144
|
+
fallback). Imported via `from 'fabric/es'` for tree-shaking.
|
|
145
|
+
|
|
146
|
+
### Why an adapter
|
|
147
|
+
|
|
148
|
+
- **Lazy loading**: the editor module is split into its own lazy
|
|
149
|
+
chunk and only loaded when the user opens the dialog.
|
|
150
|
+
- **Stable API**: lowercase filter names; `imageTransformed` event
|
|
151
|
+
abstracts mouse vs sidebar origin; viewport zoom semantics
|
|
152
|
+
(clamped, anchored at canvas centre) live in one place. Vendor
|
|
153
|
+
renames or replacement don't leak to the shell.
|
|
154
|
+
- **Engine swap proven viable**: replaced Toast UI with
|
|
155
|
+
fabric v6 with zero changes to the UI shell or tool components.
|
|
156
|
+
The interface is the firewall.
|
|
157
|
+
|
|
158
|
+
### Filter conventions
|
|
159
|
+
|
|
160
|
+
Public API uses lowercase names ('brightness') and normalised
|
|
161
|
+
ranges (see `src/types/filter.types.ts`). The adapter maps them to
|
|
162
|
+
fabric `BaseFilter` classes (`Brightness`, `Contrast`, etc.).
|
|
163
|
+
Sharpen resolves to a stock `Convolute` filter constructed with
|
|
164
|
+
the canonical 3×3 sharpen kernel `[0,-1,0,-1,5,-1,0,-1,0]` —
|
|
165
|
+
identical to Toast UI's old extension, but with no extension
|
|
166
|
+
import needed.
|
|
167
|
+
|
|
168
|
+
The adapter calls `ensureFilterBackendFitsImage(image)` before each
|
|
169
|
+
`applyFilters` invocation. If the image's longest dimension exceeds
|
|
170
|
+
`config.textureSize` (default 4096), `textureSize` is bumped to the
|
|
171
|
+
next power of two (capped at 16384) and the filter backend is
|
|
172
|
+
re-initialised. Without this, fabric's WebGL filter pipeline
|
|
173
|
+
allocates a tile-sized GL canvas that's too small for the image,
|
|
174
|
+
leaving pixels past the tile boundary un-rendered (the right-edge
|
|
175
|
+
crop bug this surfaced). The factory's `WebGLProbe`
|
|
176
|
+
falls back to `Canvas2dFilterBackend` automatically if the GPU
|
|
177
|
+
can't support the new size.
|
|
178
|
+
|
|
179
|
+
## Canvas vs Image semantics
|
|
180
|
+
|
|
181
|
+
The editor uses a **transform-the-image** model rather than the
|
|
182
|
+
Photoshop-style separate-canvas model.
|
|
183
|
+
|
|
184
|
+
- The **image** is the document. Crop, resize, rotate, flip, and
|
|
185
|
+
filters all mutate the image's pixels or its transform state.
|
|
186
|
+
- The **canvas** is a viewport. It is sized to the host container
|
|
187
|
+
via `engine.setCanvasDimensions(viewport)` and only exists to
|
|
188
|
+
display the image; it has no independent dimensions, no
|
|
189
|
+
background colour, no separate save-bounds.
|
|
190
|
+
- **Save** outputs the current image with all transforms applied
|
|
191
|
+
(`image.toBlob` / `image.toDataURL`). What you see is what gets
|
|
192
|
+
saved; nothing outside the image bounds is ever in the output.
|
|
193
|
+
|
|
194
|
+
Practical consequences:
|
|
195
|
+
|
|
196
|
+
- An image cannot be moved relative to a canvas, because the
|
|
197
|
+
canvas is the viewport, not a separate workspace.
|
|
198
|
+
- Crop bakes rotation, flip, and prior crops into the new pixel
|
|
199
|
+
data, then drops the transform state back to identity. The
|
|
200
|
+
cropped output is always axis-aligned regardless of how the
|
|
201
|
+
image was rotated when the user drew the crop overlay.
|
|
202
|
+
- Rotation is reversible (undo / redo) until the next crop, after
|
|
203
|
+
which the rotated pixels become part of the image content.
|
|
204
|
+
- Viewport zoom is purely visual and never affects saved bytes.
|
|
205
|
+
|
|
206
|
+
A true Photoshop canvas model (fixed-size workspace, image as a
|
|
207
|
+
positioned layer, separate Canvas Size / Image Size / Trim tools)
|
|
208
|
+
is deferred to. It is significant scope and only worth
|
|
209
|
+
building if the editor becomes a distributable product. For
|
|
210
|
+
complex multi-layer compositions today, recommend a dedicated
|
|
211
|
+
desktop tool.
|
|
212
|
+
|
|
213
|
+
## Usage
|
|
214
|
+
|
|
215
|
+
### As a CDK Dialog (the primary integration path)
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
import { Dialog } from '@angular/cdk/dialog';
|
|
219
|
+
import { firstValueFrom } from 'rxjs';
|
|
220
|
+
import {
|
|
221
|
+
CoolmsImageEditorHostComponent,
|
|
222
|
+
type CoolmsImageEditorHostData,
|
|
223
|
+
type CoolmsImageEditorHostResult,
|
|
224
|
+
} from '@coolms/image-editor-angular';
|
|
225
|
+
|
|
226
|
+
const data: CoolmsImageEditorHostData = {
|
|
227
|
+
asset: {
|
|
228
|
+
uuid: '...', // MediaAsset.id
|
|
229
|
+
originalUrl: '/media/photo.jpg',
|
|
230
|
+
filename: 'photo.jpg',
|
|
231
|
+
mimeType: 'image/jpeg',
|
|
232
|
+
dimensions: { width: 1920, height: 1080 },
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const dialogRef = dialog.open<CoolmsImageEditorHostResult, CoolmsImageEditorHostData>(
|
|
237
|
+
CoolmsImageEditorHostComponent,
|
|
238
|
+
{
|
|
239
|
+
data,
|
|
240
|
+
backdropClass: 'cdk-overlay-dark-backdrop',
|
|
241
|
+
disableClose: true,
|
|
242
|
+
},
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
const result = await firstValueFrom(dialogRef.closed);
|
|
246
|
+
if (result?.kind === 'saved') {
|
|
247
|
+
// result.mode is 'overwrite' or 'save_as_new'
|
|
248
|
+
// result.assetUuid is the resulting MediaAsset id
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Direct engine use (for embedding outside a CDK Dialog)
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
import { FabricEngineAdapter, type ImageEditorEngine } from '@coolms/image-editor-angular';
|
|
256
|
+
|
|
257
|
+
const engine: ImageEditorEngine = await FabricEngineAdapter.create({
|
|
258
|
+
container: document.querySelector('#editor')!,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Size the canvas backstore + display to your viewport. The adapter
|
|
262
|
+
// owns canvas dimensions; without this call the canvas defaults to
|
|
263
|
+
// the container's `clientWidth/Height` at construction time.
|
|
264
|
+
engine.setCanvasDimensions({ width: 1200, height: 800 });
|
|
265
|
+
|
|
266
|
+
await engine.loadImage('https://example.com/photo.jpg');
|
|
267
|
+
await engine.applyFilter('brightness', { brightness: 0.2 });
|
|
268
|
+
await engine.crop({ x: 100, y: 50, width: 800, height: 600 });
|
|
269
|
+
await engine.rotate(90);
|
|
270
|
+
|
|
271
|
+
// Fit-to-viewport zoom (with a 5 % margin around image edges)
|
|
272
|
+
const fit = engine.getFitZoom({ width: 1200, height: 800 });
|
|
273
|
+
engine.setZoom(fit);
|
|
274
|
+
|
|
275
|
+
const blob = await engine.export('jpeg', 0.92);
|
|
276
|
+
|
|
277
|
+
engine.on('historyStateChanged', ({ canUndo, canRedo }) => {
|
|
278
|
+
// wire your own undo / redo buttons
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
engine.on('imageTransformed', t => {
|
|
282
|
+
// mirror to your own sidebar: angle, scaleX/Y, flipX/Y, left, top
|
|
283
|
+
// t.source is 'mouse' (corner-handle drag) or 'sidebar' (engine.rotate / engine.flip / etc.)
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
engine.destroy();
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Theming
|
|
290
|
+
|
|
291
|
+
Every component in the package styles itself through CMS CSS
|
|
292
|
+
variables (`--cms-surface`, `--cms-text`, `--cms-accent`,
|
|
293
|
+
`--cms-border`, `--cms-canvas-bg`, `--cms-overlay-scrim`, etc.)
|
|
294
|
+
defined in the host project's `src/styles.scss`. There is no
|
|
295
|
+
per-component colour configuration and no theme prop — the editor
|
|
296
|
+
follows whatever the host theme dictates.
|
|
297
|
+
|
|
298
|
+
When the host project ships a CMS-wide light/dark switch later
|
|
299
|
+
(flipping the values of those variables at the `:root`), the editor
|
|
300
|
+
reflects the new theme immediately on the next render. No remount,
|
|
301
|
+
no `theme` config object to keep in sync.
|
|
302
|
+
|
|
303
|
+
Two intentionally hard-coded values exist:
|
|
304
|
+
- `#ffffff` text on `--cms-overlay-scrim`. The scrim is dark by
|
|
305
|
+
design (legibility over the canvas during loading / saving), so
|
|
306
|
+
the contrast pair is fixed.
|
|
307
|
+
- `rgba(245, 166, 35, .15)` focus-ring shadow on the resize tool's
|
|
308
|
+
numeric inputs. Matches the project's existing focus-ring pattern
|
|
309
|
+
(used by `.cms-input`, `.form-control`, etc.); promote together
|
|
310
|
+
if a `--cms-accent-focus-ring` token lands at the project level.
|
|
311
|
+
|
|
312
|
+
## Performance
|
|
313
|
+
|
|
314
|
+
Measured on Chrome for Windows (, NVIDIA GTX
|
|
315
|
+
1650 via ANGLE D3D11, `WebGLFilterBackend` confirmed,
|
|
316
|
+
`MAX_TEXTURE_SIZE=16384`):
|
|
317
|
+
|
|
318
|
+
| Metric | Toast UI baseline | (fabric v6) | Delta |
|
|
319
|
+
|---|---|---|---|
|
|
320
|
+
| Editor lazy chunk (transfer / gzipped) | 167.60 kB | **78.5 kB** | **−53%** |
|
|
321
|
+
| Editor lazy chunk (decoded / raw) | ~722 kB | **290.4 kB** | **−60%** |
|
|
322
|
+
|
|
323
|
+
Heap behaviour over 10 open / load 4608×3456 image / close cycles
|
|
324
|
+
on a 4 GB heap-limit Chrome:
|
|
325
|
+
|
|
326
|
+
| Checkpoint | Heap used | Canvases | Editor host instances |
|
|
327
|
+
|---|---|---|---|
|
|
328
|
+
| Baseline | 15.79 MB | 0 | 0 |
|
|
329
|
+
| After cycle 1 | 15.82 MB | 0 | 0 |
|
|
330
|
+
| After cycle 5 | 15.87 MB | 0 | 0 |
|
|
331
|
+
| After cycle 10 | 15.88 MB | 0 | 0 |
|
|
332
|
+
|
|
333
|
+
Heap delta across 10 cycles: +0.09 MB (~9 kB / cycle, well within
|
|
334
|
+
Angular's per-cycle change-detection noise). A single leaked
|
|
335
|
+
decoded image at 4608×3456 RGBA would be 64 MB; we see 9 kB.
|
|
336
|
+
`engine.destroy()` is fully effective.
|
|
337
|
+
|
|
338
|
+
End-to-end Sharpen (3×3 Convolute) on a 4608×3456 image: ~331 ms
|
|
339
|
+
(click -> image rendered, including DOM dispatch + Angular CD +
|
|
340
|
+
WebGL filter pass + canvas blit). The actual GPU filter time is a
|
|
341
|
+
fraction of that — most is Angular CD + paint.
|
|
342
|
+
|
|
343
|
+
## Browser compatibility
|
|
344
|
+
|
|
345
|
+
- Chrome, Firefox, Edge, Safari (modern, evergreen)
|
|
346
|
+
- WebGL preferred. fabric's `getFilterBackend` factory falls back
|
|
347
|
+
to `Canvas2dFilterBackend` automatically when WebGL is unavailable
|
|
348
|
+
or when the requested `textureSize` exceeds `MAX_TEXTURE_SIZE`.
|
|
349
|
+
- Recommended ≥ 4 GB RAM for images larger than 4096 px on the
|
|
350
|
+
long edge (the WebGL tile bumps to 8192 for those).
|
|
351
|
+
- Tested in Chrome. Firefox / Edge / Safari verified
|
|
352
|
+
conceptually but not in this round; report any browser-specific
|
|
353
|
+
issues against the engine adapter, not the UI shell.
|
|
354
|
+
|
|
355
|
+
## Roadmap
|
|
356
|
+
|
|
357
|
+
### Done
|
|
358
|
+
|
|
359
|
+
- Headless mount, engine adapter, UI shell, 11 tools, theming, save flow
|
|
360
|
+
- `FabricEngineAdapter` rewrite (~640 LOC) and the state-service switch
|
|
361
|
+
- Canvas-space crop (bake-and-replace), Sharpen `textureSize` fix, blob-URL undo
|
|
362
|
+
- Bidirectional sync (`imageTransformed` event), live-bound rotate slider,
|
|
363
|
+
flip / resize signals
|
|
364
|
+
- Canvas frame overlay
|
|
365
|
+
- Toast UI dependency removed (640 LOC adapter deleted)
|
|
366
|
+
- Bundle, memory and functional verification
|
|
367
|
+
|
|
368
|
+
### Planned
|
|
369
|
+
|
|
370
|
+
- Shapes (Free Draw, Rectangle, Circle, Text, Icons, Mask)
|
|
371
|
+
- Color picker / eyedropper
|
|
372
|
+
- WebP / AVIF export options
|
|
373
|
+
- Rich-text inline widget integration
|
|
374
|
+
- Server-side preset crops (Media Library thumbnail / banner sizes)
|
|
375
|
+
- Drag-and-drop image into the editor
|
|
376
|
+
- AI integrations (background removal, smart crop, upscaling)
|
|
377
|
+
- Mobile responsive (touch gestures, pinch-zoom)
|
|
378
|
+
- Keyboard shortcuts (Ctrl+0 fit, Ctrl+1 actual size, etc.)
|
|
379
|
+
|
|
380
|
+
### Further out
|
|
381
|
+
|
|
382
|
+
- **Framework adapters** — React, Vue, Svelte wrappers around the
|
|
383
|
+
same `ImageEditorEngine` interface
|
|
384
|
+
- **Plugin tool registration** — open `ToolRegistryService` to
|
|
385
|
+
consumer-defined tools
|
|
386
|
+
- **Video editor companion** sharing the engine-adapter pattern
|
|
387
|
+
|
|
388
|
+
## License
|
|
389
|
+
|
|
390
|
+
MIT (CoolMS DXP). The runtime peer dependency `fabric` is also MIT.
|
|
391
|
+
See `ATTRIBUTION.md` for the full vendor acknowledgement chain.
|