@gradio/imageeditor 0.14.4 → 0.15.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/CHANGELOG.md +21 -0
- package/InteractiveImageEditor.svelte +4 -7
- package/dist/InteractiveImageEditor.svelte +4 -3
- package/dist/InteractiveImageEditor.svelte.d.ts +1 -1
- package/dist/shared/Controls.svelte +32 -1
- package/dist/shared/Controls.svelte.d.ts +2 -0
- package/dist/shared/ImageEditor.svelte +23 -1
- package/dist/shared/ImageEditor.svelte.d.ts +2 -1
- package/dist/shared/brush/brush-cursor.js +0 -42
- package/dist/shared/brush/brush-textures.d.ts +19 -2
- package/dist/shared/brush/brush-textures.js +120 -82
- package/dist/shared/brush/brush.js +0 -52
- package/dist/shared/{utils → core}/commands.d.ts +19 -58
- package/dist/shared/core/commands.js +70 -0
- package/dist/shared/core/editor.d.ts +4 -91
- package/dist/shared/core/editor.js +18 -708
- package/dist/shared/core/layers.d.ts +143 -0
- package/dist/shared/core/layers.js +774 -0
- package/dist/shared/image/image.d.ts +14 -4
- package/dist/shared/image/image.js +91 -18
- package/dist/shared/resize/resize.d.ts +31 -0
- package/dist/shared/resize/resize.js +127 -290
- package/dist/shared/zoom/zoom.js +0 -51
- package/package.json +7 -7
- package/shared/Controls.svelte +33 -1
- package/shared/ImageEditor.svelte +25 -9
- package/shared/Toolbar.svelte +1 -4
- package/shared/brush/ColorSwatch.svelte +0 -3
- package/shared/brush/brush-cursor.ts +0 -42
- package/shared/brush/brush-textures.ts +166 -83
- package/shared/brush/brush.ts +0 -53
- package/shared/core/commands.ts +118 -0
- package/shared/core/editor.ts +33 -957
- package/shared/core/layers.ts +1068 -0
- package/shared/image/image.ts +139 -21
- package/shared/resize/resize.ts +160 -299
- package/shared/zoom/zoom.ts +0 -51
- package/dist/shared/utils/commands.js +0 -73
- package/shared/utils/commands.ts +0 -174
package/shared/core/editor.ts
CHANGED
|
@@ -1,37 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Application,
|
|
3
|
-
Container,
|
|
4
|
-
Graphics,
|
|
5
|
-
Sprite,
|
|
6
|
-
Filter,
|
|
7
|
-
RenderTexture,
|
|
8
|
-
GlProgram,
|
|
9
|
-
defaultFilterVert,
|
|
10
|
-
SCALE_MODES,
|
|
11
|
-
Texture,
|
|
12
|
-
type ALPHA_MODES,
|
|
13
|
-
Assets
|
|
14
|
-
} from "pixi.js";
|
|
1
|
+
import { Application, Container, Graphics, Sprite, Assets } from "pixi.js";
|
|
15
2
|
|
|
16
3
|
import { DropShadowFilter as BlurFilter } from "pixi-filters/drop-shadow";
|
|
17
4
|
|
|
18
5
|
import { ImageTool } from "../image/image";
|
|
19
|
-
import { AddImageCommand } from "../image/image";
|
|
20
6
|
|
|
21
7
|
import { ZoomTool } from "../zoom/zoom";
|
|
22
8
|
import type { Subtool, Tool as ToolbarTool } from "../Toolbar.svelte";
|
|
23
9
|
import type { Readable, Writable } from "svelte/store";
|
|
24
10
|
import { spring, type Spring } from "svelte/motion";
|
|
25
|
-
import { writable
|
|
26
|
-
import { Rectangle } from "pixi.js";
|
|
11
|
+
import { writable } from "svelte/store";
|
|
27
12
|
import { type ImageBlobs, type LayerOptions } from "../types";
|
|
28
|
-
import { get_canvas_blob } from "../utils/pixi";
|
|
29
13
|
import type { BrushTool } from "../brush/brush";
|
|
30
14
|
import type { CropTool } from "../crop/crop";
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
15
|
+
import { CommandManager, type Command } from "./commands";
|
|
16
|
+
import {
|
|
17
|
+
LayerManager,
|
|
18
|
+
AddLayerCommand,
|
|
19
|
+
RemoveLayerCommand,
|
|
20
|
+
ReorderLayerCommand
|
|
21
|
+
} from "./layers";
|
|
35
22
|
|
|
36
23
|
export interface Tool {
|
|
37
24
|
name: string;
|
|
@@ -46,895 +33,6 @@ export interface Tool {
|
|
|
46
33
|
off?: (event: string, callback: () => void) => void;
|
|
47
34
|
}
|
|
48
35
|
|
|
49
|
-
export class CommandManager {
|
|
50
|
-
private undo_stack: Command[] = [];
|
|
51
|
-
private redo_stack: Command[] = [];
|
|
52
|
-
|
|
53
|
-
async execute(command: Command): Promise<void> {
|
|
54
|
-
await command.execute();
|
|
55
|
-
this.undo_stack.push(command);
|
|
56
|
-
this.redo_stack = []; // Clear redo stack when new command is executed
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async undo(): Promise<void> {
|
|
60
|
-
const command = this.undo_stack.pop();
|
|
61
|
-
if (command) {
|
|
62
|
-
await command.undo();
|
|
63
|
-
this.redo_stack.push(command);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async redo(): Promise<void> {
|
|
68
|
-
const command = this.redo_stack.pop();
|
|
69
|
-
if (command) {
|
|
70
|
-
await command.execute();
|
|
71
|
-
this.undo_stack.push(command);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export class LayerManager {
|
|
77
|
-
private layers: {
|
|
78
|
-
name: string;
|
|
79
|
-
id: string;
|
|
80
|
-
container: Container;
|
|
81
|
-
user_created: boolean;
|
|
82
|
-
visible: boolean;
|
|
83
|
-
}[] = [];
|
|
84
|
-
private active_layer: Container | null = null;
|
|
85
|
-
private active_layer_id: string | null = null;
|
|
86
|
-
private draw_textures: Map<Container, RenderTexture> = new Map();
|
|
87
|
-
layer_store: Writable<{
|
|
88
|
-
active_layer: string;
|
|
89
|
-
layers: {
|
|
90
|
-
name: string;
|
|
91
|
-
id: string;
|
|
92
|
-
user_created: boolean;
|
|
93
|
-
visible: boolean;
|
|
94
|
-
}[];
|
|
95
|
-
}> = writable({
|
|
96
|
-
active_layer: "",
|
|
97
|
-
|
|
98
|
-
layers: []
|
|
99
|
-
});
|
|
100
|
-
private background_layer: Container | null = null;
|
|
101
|
-
private image_container: Container;
|
|
102
|
-
private app: Application;
|
|
103
|
-
private fixed_canvas: boolean;
|
|
104
|
-
private dark: boolean;
|
|
105
|
-
private border_region: number;
|
|
106
|
-
private layer_options: LayerOptions;
|
|
107
|
-
constructor(
|
|
108
|
-
image_container: Container,
|
|
109
|
-
app: Application,
|
|
110
|
-
fixed_canvas: boolean,
|
|
111
|
-
dark: boolean,
|
|
112
|
-
border_region: number,
|
|
113
|
-
layer_options: LayerOptions
|
|
114
|
-
) {
|
|
115
|
-
this.image_container = image_container;
|
|
116
|
-
this.app = app;
|
|
117
|
-
this.fixed_canvas = fixed_canvas;
|
|
118
|
-
this.dark = dark;
|
|
119
|
-
this.border_region = border_region;
|
|
120
|
-
this.layer_options = layer_options;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
toggle_layer_visibility(id: string): void {
|
|
124
|
-
const layer = this.layers.find((l) => l.id === id);
|
|
125
|
-
if (layer) {
|
|
126
|
-
layer.container.visible = !layer.container.visible;
|
|
127
|
-
layer.visible = layer.container.visible;
|
|
128
|
-
this.layer_store.update((state) => ({
|
|
129
|
-
active_layer: state.active_layer,
|
|
130
|
-
layers: this.layers
|
|
131
|
-
}));
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
create_background_layer(width: number, height: number): Container {
|
|
136
|
-
if (this.background_layer) {
|
|
137
|
-
this.background_layer.destroy();
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Create new background layer
|
|
141
|
-
const layer = new Container();
|
|
142
|
-
|
|
143
|
-
this.background_layer = layer;
|
|
144
|
-
|
|
145
|
-
// Create a render texture for the background with transparency
|
|
146
|
-
const bg_texture = RenderTexture.create({
|
|
147
|
-
width,
|
|
148
|
-
height,
|
|
149
|
-
resolution: window.devicePixelRatio,
|
|
150
|
-
antialias: true,
|
|
151
|
-
scaleMode: SCALE_MODES.NEAREST
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
// Create sprite to display the texture with transparency
|
|
155
|
-
const bg_sprite = new Sprite(bg_texture);
|
|
156
|
-
layer.addChild(bg_sprite);
|
|
157
|
-
|
|
158
|
-
// Clear the texture with transparency
|
|
159
|
-
const clear_graphics = new Graphics();
|
|
160
|
-
clear_graphics.clear();
|
|
161
|
-
|
|
162
|
-
clear_graphics
|
|
163
|
-
.rect(0, 0, width, height)
|
|
164
|
-
.fill({ color: this.dark ? 0x333333 : 0xffffff, alpha: 1 });
|
|
165
|
-
|
|
166
|
-
// Render with transparency
|
|
167
|
-
this.app.renderer.render({
|
|
168
|
-
container: clear_graphics,
|
|
169
|
-
target: bg_texture,
|
|
170
|
-
clear: true
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
this.image_container.addChild(layer);
|
|
174
|
-
|
|
175
|
-
layer.zIndex = -1;
|
|
176
|
-
|
|
177
|
-
this.update_layer_order();
|
|
178
|
-
return layer;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
set_layer_options(
|
|
182
|
-
layer_options: LayerOptions,
|
|
183
|
-
width: number,
|
|
184
|
-
height: number
|
|
185
|
-
): void {
|
|
186
|
-
this.layer_options = layer_options;
|
|
187
|
-
this.reset_layers(width, height);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Creates a background layer from an image URL
|
|
192
|
-
* @param url The URL of the image to use as background
|
|
193
|
-
* @param width The width of the layer (if fixed_canvas is true)
|
|
194
|
-
* @param height The height of the layer (if fixed_canvas is true)
|
|
195
|
-
* @param borderRegion Minimum border region to add around the image for outpainting (in pixels)
|
|
196
|
-
* @returns A Promise that resolves to the background Container when the image is loaded
|
|
197
|
-
*/
|
|
198
|
-
async create_background_layer_from_url(
|
|
199
|
-
url: string,
|
|
200
|
-
width?: number,
|
|
201
|
-
height?: number
|
|
202
|
-
): Promise<Container> {
|
|
203
|
-
const layer = this.create_background_layer(
|
|
204
|
-
width || this.image_container.width,
|
|
205
|
-
height || this.image_container.height
|
|
206
|
-
);
|
|
207
|
-
|
|
208
|
-
try {
|
|
209
|
-
const texture = await Texture.from(url);
|
|
210
|
-
const sprite = new Sprite(texture);
|
|
211
|
-
|
|
212
|
-
const imageWidth = sprite.texture.width;
|
|
213
|
-
const imageHeight = sprite.texture.height;
|
|
214
|
-
|
|
215
|
-
// Calculate dimensions based on whether canvas size is fixed
|
|
216
|
-
const containerWidth = width || this.image_container.width;
|
|
217
|
-
const containerHeight = height || this.image_container.height;
|
|
218
|
-
|
|
219
|
-
if (this.fixed_canvas) {
|
|
220
|
-
// If fixed canvas, fit the image within the specified dimensions minus border region
|
|
221
|
-
// Calculate aspect-preserving dimensions
|
|
222
|
-
const effectiveContainerWidth = Math.max(
|
|
223
|
-
containerWidth - this.border_region * 2,
|
|
224
|
-
10
|
|
225
|
-
);
|
|
226
|
-
const effectiveContainerHeight = Math.max(
|
|
227
|
-
containerHeight - this.border_region * 2,
|
|
228
|
-
10
|
|
229
|
-
);
|
|
230
|
-
|
|
231
|
-
const imageAspectRatio = imageWidth / imageHeight;
|
|
232
|
-
const containerAspectRatio =
|
|
233
|
-
effectiveContainerWidth / effectiveContainerHeight;
|
|
234
|
-
|
|
235
|
-
let finalWidth, finalHeight;
|
|
236
|
-
let posX = this.border_region, // Start with border offset
|
|
237
|
-
posY = this.border_region;
|
|
238
|
-
|
|
239
|
-
if (
|
|
240
|
-
imageWidth <= effectiveContainerWidth &&
|
|
241
|
-
imageHeight <= effectiveContainerHeight
|
|
242
|
-
) {
|
|
243
|
-
// Image fits within container without scaling
|
|
244
|
-
finalWidth = imageWidth;
|
|
245
|
-
finalHeight = imageHeight;
|
|
246
|
-
} else {
|
|
247
|
-
if (imageAspectRatio > containerAspectRatio) {
|
|
248
|
-
// Width is the limiting factor
|
|
249
|
-
finalWidth = effectiveContainerWidth;
|
|
250
|
-
finalHeight = effectiveContainerWidth / imageAspectRatio;
|
|
251
|
-
} else {
|
|
252
|
-
// Height is the limiting factor
|
|
253
|
-
finalHeight = effectiveContainerHeight;
|
|
254
|
-
finalWidth = effectiveContainerHeight * imageAspectRatio;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Center image within the effective container area (which already accounts for border)
|
|
259
|
-
posX += Math.round((effectiveContainerWidth - finalWidth) / 2);
|
|
260
|
-
posY += Math.round((effectiveContainerHeight - finalHeight) / 2);
|
|
261
|
-
sprite.width = finalWidth;
|
|
262
|
-
sprite.height = finalHeight;
|
|
263
|
-
sprite.position.set(posX, posY);
|
|
264
|
-
} else {
|
|
265
|
-
// If not fixed canvas, use the natural dimensions of the image plus border
|
|
266
|
-
// Position starts at the border offset
|
|
267
|
-
sprite.position.set(this.border_region, this.border_region);
|
|
268
|
-
|
|
269
|
-
// We need to re-create the background layer with the actual image dimensions plus border
|
|
270
|
-
if (this.background_layer) {
|
|
271
|
-
this.background_layer.destroy();
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Create new layer with image dimensions plus border on all sides
|
|
275
|
-
const totalWidth = imageWidth + this.border_region * 2;
|
|
276
|
-
const totalHeight = imageHeight + this.border_region * 2;
|
|
277
|
-
const newLayer = this.create_background_layer(totalWidth, totalHeight);
|
|
278
|
-
|
|
279
|
-
sprite.width = imageWidth;
|
|
280
|
-
sprite.height = imageHeight;
|
|
281
|
-
newLayer.addChild(sprite);
|
|
282
|
-
|
|
283
|
-
return newLayer;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
layer.addChild(sprite);
|
|
287
|
-
|
|
288
|
-
return layer;
|
|
289
|
-
} catch (error) {
|
|
290
|
-
console.error("Error loading image from URL:", error);
|
|
291
|
-
return layer;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
create_layer({
|
|
296
|
-
width,
|
|
297
|
-
height,
|
|
298
|
-
layer_name,
|
|
299
|
-
user_created,
|
|
300
|
-
layer_id = undefined,
|
|
301
|
-
make_active = false
|
|
302
|
-
}: {
|
|
303
|
-
width: number;
|
|
304
|
-
height: number;
|
|
305
|
-
layer_name?: string;
|
|
306
|
-
user_created: boolean;
|
|
307
|
-
layer_id?: string;
|
|
308
|
-
make_active?: boolean;
|
|
309
|
-
}): Container {
|
|
310
|
-
const layer = new Container();
|
|
311
|
-
const _layer_id = layer_id || Math.random().toString(36).substring(2, 15);
|
|
312
|
-
const _layer_name = layer_name || `Layer ${this.layers.length + 1}`;
|
|
313
|
-
|
|
314
|
-
this.layers.push({
|
|
315
|
-
name: _layer_name,
|
|
316
|
-
id: _layer_id,
|
|
317
|
-
container: layer,
|
|
318
|
-
user_created,
|
|
319
|
-
visible: true
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
this.image_container.addChild(layer);
|
|
323
|
-
|
|
324
|
-
// Create a single render texture for drawing with transparency
|
|
325
|
-
const draw_texture = RenderTexture.create({
|
|
326
|
-
width,
|
|
327
|
-
height,
|
|
328
|
-
resolution: window.devicePixelRatio,
|
|
329
|
-
antialias: true,
|
|
330
|
-
scaleMode: SCALE_MODES.NEAREST
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
const canvas_sprite = new Sprite(draw_texture);
|
|
334
|
-
layer.addChild(canvas_sprite);
|
|
335
|
-
|
|
336
|
-
const clear_graphics = new Graphics();
|
|
337
|
-
clear_graphics.clear();
|
|
338
|
-
clear_graphics.beginFill(0, 0);
|
|
339
|
-
clear_graphics.drawRect(0, 0, width, height);
|
|
340
|
-
clear_graphics.endFill();
|
|
341
|
-
|
|
342
|
-
this.app.renderer.render({
|
|
343
|
-
container: clear_graphics,
|
|
344
|
-
target: draw_texture,
|
|
345
|
-
clear: true
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
// Store texture for drawing
|
|
349
|
-
this.draw_textures.set(layer, draw_texture);
|
|
350
|
-
|
|
351
|
-
this.update_layer_order();
|
|
352
|
-
if (make_active) {
|
|
353
|
-
this.set_active_layer(_layer_id);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
this.layer_store.set({
|
|
357
|
-
active_layer: this.active_layer_id || "",
|
|
358
|
-
layers: this.layers
|
|
359
|
-
});
|
|
360
|
-
return layer;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* Creates a new layer with an image loaded from a URL
|
|
365
|
-
* @param url The URL of the image to load
|
|
366
|
-
* @param borderRegion Minimum border region to add around the image for outpainting (in pixels)
|
|
367
|
-
* @returns A Promise that resolves to the layer ID
|
|
368
|
-
*/
|
|
369
|
-
async add_layer_from_url(url: string): Promise<string> {
|
|
370
|
-
const { width, height } = this.image_container.getLocalBounds();
|
|
371
|
-
const layer = this.create_layer({
|
|
372
|
-
width,
|
|
373
|
-
height,
|
|
374
|
-
layer_name: "Layer 1",
|
|
375
|
-
user_created: true
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
const layerIndex = this.layers.findIndex((l) => l.container === layer);
|
|
379
|
-
if (layerIndex === -1) {
|
|
380
|
-
console.error("Could not find newly created layer");
|
|
381
|
-
return "";
|
|
382
|
-
}
|
|
383
|
-
const layerId = this.layers[layerIndex].id;
|
|
384
|
-
|
|
385
|
-
try {
|
|
386
|
-
const texture = await Assets.load(url);
|
|
387
|
-
|
|
388
|
-
const drawTexture = this.draw_textures.get(layer);
|
|
389
|
-
if (!drawTexture) {
|
|
390
|
-
console.error("No draw texture found for layer");
|
|
391
|
-
return layerId;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
const sprite = new Sprite(texture);
|
|
395
|
-
const imageWidth = sprite.width;
|
|
396
|
-
const imageHeight = sprite.height;
|
|
397
|
-
|
|
398
|
-
let posX = this.border_region;
|
|
399
|
-
let posY = this.border_region;
|
|
400
|
-
|
|
401
|
-
// Calculate effective dimensions (accounting for border)
|
|
402
|
-
const effectiveWidth = this.fixed_canvas
|
|
403
|
-
? width - this.border_region * 2
|
|
404
|
-
: width;
|
|
405
|
-
const effectiveHeight = this.fixed_canvas
|
|
406
|
-
? height - this.border_region * 2
|
|
407
|
-
: height;
|
|
408
|
-
|
|
409
|
-
// If the image is smaller than the effective layer area, center it within that area
|
|
410
|
-
if (imageWidth < effectiveWidth || imageHeight < effectiveHeight) {
|
|
411
|
-
posX = Math.floor((effectiveWidth - imageWidth) / 2);
|
|
412
|
-
posY = Math.floor((effectiveHeight - imageHeight) / 2);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
sprite.position.set(posX, posY);
|
|
416
|
-
|
|
417
|
-
// If the image is larger than the effective layer area, scale it down to fit
|
|
418
|
-
if (imageWidth > effectiveWidth || imageHeight > effectiveHeight) {
|
|
419
|
-
const imageAspectRatio = imageWidth / imageHeight;
|
|
420
|
-
const areaAspectRatio = effectiveWidth / effectiveHeight;
|
|
421
|
-
|
|
422
|
-
let finalWidth, finalHeight;
|
|
423
|
-
|
|
424
|
-
if (imageAspectRatio > areaAspectRatio) {
|
|
425
|
-
// Width is the limiting factor
|
|
426
|
-
finalWidth = effectiveWidth;
|
|
427
|
-
finalHeight = effectiveWidth / imageAspectRatio;
|
|
428
|
-
} else {
|
|
429
|
-
// Height is the limiting factor
|
|
430
|
-
finalHeight = effectiveHeight;
|
|
431
|
-
finalWidth = effectiveHeight * imageAspectRatio;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
sprite.width = finalWidth;
|
|
435
|
-
sprite.height = finalHeight;
|
|
436
|
-
|
|
437
|
-
// Recalculate position to center within effective area
|
|
438
|
-
posX =
|
|
439
|
-
this.border_region + Math.floor((effectiveWidth - finalWidth) / 2);
|
|
440
|
-
posY =
|
|
441
|
-
this.border_region + Math.floor((effectiveHeight - finalHeight) / 2);
|
|
442
|
-
sprite.position.set(posX, posY);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
this.app.renderer.render(sprite, { renderTexture: drawTexture });
|
|
446
|
-
|
|
447
|
-
this.set_active_layer(layerId);
|
|
448
|
-
|
|
449
|
-
return layerId;
|
|
450
|
-
} catch (error) {
|
|
451
|
-
console.error("Error loading image from URL:", error);
|
|
452
|
-
return layerId;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
get_active_layer(): typeof this.active_layer {
|
|
457
|
-
return this.active_layer;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
set_active_layer(id: string): void {
|
|
461
|
-
if (this.layers.some((l) => l.id === id)) {
|
|
462
|
-
this.active_layer =
|
|
463
|
-
this.layers.find((l) => l.id === id)?.container ||
|
|
464
|
-
this.layers[0]?.container ||
|
|
465
|
-
null;
|
|
466
|
-
this.active_layer_id = id;
|
|
467
|
-
this.layer_store.set({
|
|
468
|
-
active_layer: id,
|
|
469
|
-
layers: this.layers
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
get_layers(): typeof this.layers {
|
|
475
|
-
return this.layers;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
get_layer_textures(id: string): { draw: RenderTexture } | null {
|
|
479
|
-
const layer = this.layers.find((l) => l.id === id);
|
|
480
|
-
if (layer) {
|
|
481
|
-
const draw = this.draw_textures.get(layer.container);
|
|
482
|
-
if (draw) {
|
|
483
|
-
return { draw };
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
return null;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
delete_layer(id: string): void {
|
|
490
|
-
const index = this.layers.findIndex((l) => l.id === id);
|
|
491
|
-
if (index > -1) {
|
|
492
|
-
// Clean up texture
|
|
493
|
-
const draw_texture = this.draw_textures.get(this.layers[index].container);
|
|
494
|
-
if (draw_texture) {
|
|
495
|
-
draw_texture.destroy();
|
|
496
|
-
this.draw_textures.delete(this.layers[index].container);
|
|
497
|
-
}
|
|
498
|
-
this.layers[index].container.destroy();
|
|
499
|
-
if (this.active_layer === this.layers[index].container) {
|
|
500
|
-
const new_active_layer = this.layers[Math.max(0, index - 1)] || null;
|
|
501
|
-
this.active_layer = new_active_layer?.container || null;
|
|
502
|
-
this.active_layer_id = new_active_layer?.id || null;
|
|
503
|
-
}
|
|
504
|
-
this.layers = this.layers.filter((l) => l.id !== id);
|
|
505
|
-
|
|
506
|
-
this.layer_store.update((_layers) => ({
|
|
507
|
-
active_layer:
|
|
508
|
-
_layers.active_layer === id
|
|
509
|
-
? this.layers[this.layers.length - 1]?.id
|
|
510
|
-
: _layers.active_layer,
|
|
511
|
-
layers: this.layers
|
|
512
|
-
}));
|
|
513
|
-
|
|
514
|
-
this.update_layer_order();
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
private update_layer_order(): void {
|
|
519
|
-
// Ensure background layer always has lowest z-index
|
|
520
|
-
if (this.background_layer) {
|
|
521
|
-
this.background_layer.zIndex = -1;
|
|
522
|
-
}
|
|
523
|
-
// Update other layers starting from 0
|
|
524
|
-
this.layers.forEach((layer, index) => {
|
|
525
|
-
layer.container.zIndex = index;
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
move_layer(id: string, direction: "up" | "down"): void {
|
|
530
|
-
const index = this.layers.findIndex((l) => l.id === id);
|
|
531
|
-
if (index > -1) {
|
|
532
|
-
const new_index = direction === "up" ? index - 1 : index + 1;
|
|
533
|
-
|
|
534
|
-
this.layers = this.layers.map((l, i) => {
|
|
535
|
-
if (i === index) {
|
|
536
|
-
return this.layers[new_index];
|
|
537
|
-
}
|
|
538
|
-
if (i === new_index) {
|
|
539
|
-
return this.layers[index];
|
|
540
|
-
}
|
|
541
|
-
return l;
|
|
542
|
-
});
|
|
543
|
-
this.update_layer_order();
|
|
544
|
-
this.layer_store.update((_layers) => ({
|
|
545
|
-
active_layer: id,
|
|
546
|
-
layers: this.layers
|
|
547
|
-
}));
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
/**
|
|
552
|
-
* Resizes all layers to a new width and height
|
|
553
|
-
* @param newWidth The new width of the layers
|
|
554
|
-
* @param newHeight The new height of the layers
|
|
555
|
-
* @param scale If true, scales the layer content to fit the new dimensions. If false, keeps the content size unchanged.
|
|
556
|
-
* @param anchor The anchor point to position the content relative to the new size
|
|
557
|
-
*/
|
|
558
|
-
resize_all_layers(
|
|
559
|
-
newWidth: number,
|
|
560
|
-
newHeight: number,
|
|
561
|
-
scale: boolean,
|
|
562
|
-
anchor:
|
|
563
|
-
| "top-left"
|
|
564
|
-
| "top"
|
|
565
|
-
| "top-right"
|
|
566
|
-
| "left"
|
|
567
|
-
| "center"
|
|
568
|
-
| "right"
|
|
569
|
-
| "bottom-left"
|
|
570
|
-
| "bottom"
|
|
571
|
-
| "bottom-right",
|
|
572
|
-
// Add old canvas dimensions for anchor calculations when not scaling
|
|
573
|
-
oldCanvasWidth: number,
|
|
574
|
-
oldCanvasHeight: number
|
|
575
|
-
): void {
|
|
576
|
-
// Create a map of the old layers by ID for reference
|
|
577
|
-
const oldLayersById = new Map(this.layers.map((l) => [l.id, l]));
|
|
578
|
-
const oldBackgroundLayer = this.background_layer;
|
|
579
|
-
|
|
580
|
-
// Calculate offset for non-scaling resize based on anchor and dimension change
|
|
581
|
-
const calculateOffset = (): { offsetX: number; offsetY: number } => {
|
|
582
|
-
let offsetX = 0;
|
|
583
|
-
let offsetY = 0;
|
|
584
|
-
const deltaWidth = newWidth - oldCanvasWidth;
|
|
585
|
-
const deltaHeight = newHeight - oldCanvasHeight;
|
|
586
|
-
|
|
587
|
-
// Determine X offset based on anchor point
|
|
588
|
-
if (anchor.includes("left")) {
|
|
589
|
-
offsetX = 0;
|
|
590
|
-
} else if (anchor.includes("right")) {
|
|
591
|
-
offsetX = deltaWidth;
|
|
592
|
-
} else {
|
|
593
|
-
// center
|
|
594
|
-
offsetX = Math.floor(deltaWidth / 2);
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
// Determine Y offset based on anchor point
|
|
598
|
-
if (anchor.includes("top")) {
|
|
599
|
-
offsetY = 0;
|
|
600
|
-
} else if (anchor.includes("bottom")) {
|
|
601
|
-
offsetY = deltaHeight;
|
|
602
|
-
} else {
|
|
603
|
-
// center
|
|
604
|
-
offsetY = Math.floor(deltaHeight / 2);
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
return { offsetX, offsetY };
|
|
608
|
-
};
|
|
609
|
-
|
|
610
|
-
// First, handle the background layer using the helper method
|
|
611
|
-
// Note: create_background_layer called inside the helper might destroy the old one
|
|
612
|
-
this.background_layer = this._resize_background_layer(
|
|
613
|
-
oldBackgroundLayer, // Pass the potentially null old layer
|
|
614
|
-
newWidth,
|
|
615
|
-
newHeight,
|
|
616
|
-
scale,
|
|
617
|
-
calculateOffset
|
|
618
|
-
);
|
|
619
|
-
|
|
620
|
-
// Now process regular layers
|
|
621
|
-
const processedLayers: {
|
|
622
|
-
name: string;
|
|
623
|
-
id: string;
|
|
624
|
-
container: Container;
|
|
625
|
-
user_created: boolean;
|
|
626
|
-
}[] = [];
|
|
627
|
-
|
|
628
|
-
// Store old layer data before clearing the main arrays
|
|
629
|
-
const oldLayerData = this.layers.map((oldLayer) => ({
|
|
630
|
-
id: oldLayer.id,
|
|
631
|
-
name: oldLayer.name,
|
|
632
|
-
user_created: oldLayer.user_created,
|
|
633
|
-
texture: this.draw_textures.get(oldLayer.container),
|
|
634
|
-
container: oldLayer.container
|
|
635
|
-
}));
|
|
636
|
-
|
|
637
|
-
// Clear the current layers array and textures map. New layers will be added by the helper.
|
|
638
|
-
this.layers = [];
|
|
639
|
-
this.draw_textures.clear(); // Ensure map is cleared before helpers add to it
|
|
640
|
-
|
|
641
|
-
// Rebuild layers in the original order using the helper method
|
|
642
|
-
for (const oldData of oldLayerData) {
|
|
643
|
-
// The helper _resize_single_layer handles creating the new layer,
|
|
644
|
-
// drawing content, cleanup, and adding the new layer to this.layers.
|
|
645
|
-
// It returns null if resizing failed for that layer.
|
|
646
|
-
const newLayer = this._resize_single_layer(
|
|
647
|
-
oldData,
|
|
648
|
-
newWidth,
|
|
649
|
-
newHeight,
|
|
650
|
-
scale,
|
|
651
|
-
calculateOffset
|
|
652
|
-
);
|
|
653
|
-
if (newLayer) {
|
|
654
|
-
processedLayers.push(newLayer);
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
// Ensure the active layer still exists, otherwise select the top one
|
|
659
|
-
const currentActiveId = get(this.layer_store).active_layer;
|
|
660
|
-
const activeLayerExists = processedLayers.some(
|
|
661
|
-
(l) => l.id === currentActiveId
|
|
662
|
-
);
|
|
663
|
-
|
|
664
|
-
if (!activeLayerExists && processedLayers.length > 0) {
|
|
665
|
-
this.set_active_layer(processedLayers[0].id); // Set to top layer (index 0 is bottom visually)
|
|
666
|
-
} else if (processedLayers.length === 0) {
|
|
667
|
-
this.layer_store.update((s) => ({ ...s, active_layer: "" })); // No layers left
|
|
668
|
-
} else {
|
|
669
|
-
// If the active layer still exists, just trigger an update to ensure consistency
|
|
670
|
-
this.layer_store.update((s) => ({ ...s }));
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
// Update Z-indices
|
|
674
|
-
this.update_layer_order();
|
|
675
|
-
|
|
676
|
-
// Final cleanup attempt - textures might need explicit destroy calls
|
|
677
|
-
// Using setTimeout allows the current execution stack to clear
|
|
678
|
-
setTimeout(() => {
|
|
679
|
-
// Attempt to force GC or resource cleanup if needed, though Pixi often handles this
|
|
680
|
-
Assets.cache.reset(); // Be careful with this, might clear too much
|
|
681
|
-
this.app.renderer.textureGC.run(); // Force texture garbage collection
|
|
682
|
-
}, 100);
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
/**
|
|
686
|
-
* Helper method to resize the background layer.
|
|
687
|
-
*/
|
|
688
|
-
private _resize_background_layer(
|
|
689
|
-
oldBackgroundLayer: Container | null,
|
|
690
|
-
newWidth: number,
|
|
691
|
-
newHeight: number,
|
|
692
|
-
scale: boolean,
|
|
693
|
-
calculateOffset: () => { offsetX: number; offsetY: number }
|
|
694
|
-
): Container | null {
|
|
695
|
-
if (!oldBackgroundLayer) {
|
|
696
|
-
// If no old background layer existed, create a new one
|
|
697
|
-
return this.create_background_layer(newWidth, newHeight);
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
// Identify the actual image sprite within the background container
|
|
701
|
-
let backgroundImage: Sprite | null = oldBackgroundLayer.children.find(
|
|
702
|
-
(child) =>
|
|
703
|
-
child instanceof Sprite &&
|
|
704
|
-
child.texture !== (oldBackgroundLayer.children[0] as Sprite)?.texture
|
|
705
|
-
) as Sprite | null;
|
|
706
|
-
|
|
707
|
-
// Create the new background layer first (this also destroys the old one implicitly if it exists)
|
|
708
|
-
const newBackgroundLayer = this.create_background_layer(
|
|
709
|
-
newWidth,
|
|
710
|
-
newHeight
|
|
711
|
-
);
|
|
712
|
-
|
|
713
|
-
if (backgroundImage) {
|
|
714
|
-
const newBgImage = new Sprite(backgroundImage.texture);
|
|
715
|
-
newBgImage.width = backgroundImage.width; // Preserve original size unless scaling
|
|
716
|
-
newBgImage.height = backgroundImage.height;
|
|
717
|
-
|
|
718
|
-
if (scale) {
|
|
719
|
-
newBgImage.width = newWidth;
|
|
720
|
-
newBgImage.height = newHeight;
|
|
721
|
-
newBgImage.position.set(0, 0);
|
|
722
|
-
} else {
|
|
723
|
-
const { offsetX, offsetY } = calculateOffset();
|
|
724
|
-
newBgImage.position.set(
|
|
725
|
-
backgroundImage.x + offsetX,
|
|
726
|
-
backgroundImage.y + offsetY
|
|
727
|
-
);
|
|
728
|
-
}
|
|
729
|
-
newBackgroundLayer.addChild(newBgImage);
|
|
730
|
-
}
|
|
731
|
-
// If no background image, the new layer created above is already blank and correctly sized.
|
|
732
|
-
|
|
733
|
-
return newBackgroundLayer;
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
/**
|
|
737
|
-
* Helper method to resize a single regular layer.
|
|
738
|
-
*/
|
|
739
|
-
private _resize_single_layer(
|
|
740
|
-
oldData: {
|
|
741
|
-
id: string;
|
|
742
|
-
name: string;
|
|
743
|
-
user_created: boolean;
|
|
744
|
-
texture: RenderTexture | undefined;
|
|
745
|
-
container: Container;
|
|
746
|
-
},
|
|
747
|
-
newWidth: number,
|
|
748
|
-
newHeight: number,
|
|
749
|
-
scale: boolean,
|
|
750
|
-
calculateOffset: () => { offsetX: number; offsetY: number }
|
|
751
|
-
): {
|
|
752
|
-
name: string;
|
|
753
|
-
id: string;
|
|
754
|
-
container: Container;
|
|
755
|
-
user_created: boolean;
|
|
756
|
-
} | null {
|
|
757
|
-
if (!oldData.texture) {
|
|
758
|
-
console.warn(
|
|
759
|
-
`No texture found for layer ${oldData.id}, skipping cleanup.`
|
|
760
|
-
);
|
|
761
|
-
// Destroy the old container if it exists and is not destroyed
|
|
762
|
-
if (oldData.container && !oldData.container.destroyed) {
|
|
763
|
-
if (this.image_container.children.includes(oldData.container)) {
|
|
764
|
-
this.image_container.removeChild(oldData.container);
|
|
765
|
-
}
|
|
766
|
-
oldData.container.destroy({ children: true });
|
|
767
|
-
}
|
|
768
|
-
return null;
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
// Create a new layer container and associated render texture
|
|
772
|
-
const newContainer = this.create_layer({
|
|
773
|
-
// This adds the layer to this.layers and draw_textures
|
|
774
|
-
width: newWidth,
|
|
775
|
-
height: newHeight,
|
|
776
|
-
layer_name: oldData.name,
|
|
777
|
-
user_created: oldData.user_created
|
|
778
|
-
});
|
|
779
|
-
|
|
780
|
-
// Retrieve the newly created layer data (it's the last one added by create_layer)
|
|
781
|
-
const newLayer = this.layers[this.layers.length - 1];
|
|
782
|
-
newLayer.id = oldData.id; // Restore the original ID
|
|
783
|
-
|
|
784
|
-
// Get the render texture for the new layer
|
|
785
|
-
const newTexture = this.draw_textures.get(newContainer);
|
|
786
|
-
|
|
787
|
-
if (!newTexture) {
|
|
788
|
-
console.error(
|
|
789
|
-
`Failed to get texture for newly created layer ${newLayer.id}. Cleaning up.`
|
|
790
|
-
);
|
|
791
|
-
// Clean up the partially created new layer resources
|
|
792
|
-
if (newContainer && !newContainer.destroyed) {
|
|
793
|
-
if (this.image_container.children.includes(newContainer)) {
|
|
794
|
-
this.image_container.removeChild(newContainer);
|
|
795
|
-
}
|
|
796
|
-
newContainer.destroy({ children: true });
|
|
797
|
-
const idx = this.layers.findIndex((l) => l.container === newContainer);
|
|
798
|
-
if (idx > -1) this.layers.splice(idx, 1);
|
|
799
|
-
this.draw_textures.delete(newContainer);
|
|
800
|
-
}
|
|
801
|
-
// Clean up old resources
|
|
802
|
-
if (oldData.texture && !oldData.texture.destroyed)
|
|
803
|
-
oldData.texture.destroy(true);
|
|
804
|
-
if (oldData.container && !oldData.container.destroyed)
|
|
805
|
-
oldData.container.destroy({ children: true });
|
|
806
|
-
return null;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
// Explicitly clear the new render texture (create_layer might already do this, but explicit is safer)
|
|
810
|
-
this.app.renderer.clear({ target: newTexture, clearColor: [0, 0, 0, 0] });
|
|
811
|
-
|
|
812
|
-
// Create a sprite with the old texture content
|
|
813
|
-
const sprite = new Sprite(oldData.texture);
|
|
814
|
-
|
|
815
|
-
if (scale) {
|
|
816
|
-
sprite.width = newWidth;
|
|
817
|
-
sprite.height = newHeight;
|
|
818
|
-
sprite.position.set(0, 0);
|
|
819
|
-
} else {
|
|
820
|
-
const { offsetX, offsetY } = calculateOffset();
|
|
821
|
-
sprite.position.set(offsetX, offsetY);
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
// Render the sprite onto the new texture
|
|
825
|
-
this.app.renderer.render(sprite, { renderTexture: newTexture });
|
|
826
|
-
|
|
827
|
-
// Clean up temporary sprite and old resources
|
|
828
|
-
sprite.destroy();
|
|
829
|
-
if (oldData.texture && !oldData.texture.destroyed) {
|
|
830
|
-
oldData.texture.destroy(true);
|
|
831
|
-
}
|
|
832
|
-
if (oldData.container && !oldData.container.destroyed) {
|
|
833
|
-
if (this.image_container.children.includes(oldData.container)) {
|
|
834
|
-
this.image_container.removeChild(oldData.container);
|
|
835
|
-
}
|
|
836
|
-
oldData.container.destroy({ children: true });
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
// Return the data for the successfully resized layer
|
|
840
|
-
return newLayer;
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
async get_blobs(width: number, height: number): Promise<ImageBlobs> {
|
|
844
|
-
const blobs = {
|
|
845
|
-
background: await get_canvas_blob(
|
|
846
|
-
this.app.renderer,
|
|
847
|
-
this.background_layer,
|
|
848
|
-
{
|
|
849
|
-
width,
|
|
850
|
-
height,
|
|
851
|
-
x: 0,
|
|
852
|
-
y: 0
|
|
853
|
-
}
|
|
854
|
-
),
|
|
855
|
-
layers: await Promise.all(
|
|
856
|
-
this.layers.map(async (layer) => {
|
|
857
|
-
const blob = await get_canvas_blob(
|
|
858
|
-
this.app.renderer,
|
|
859
|
-
layer.container,
|
|
860
|
-
{
|
|
861
|
-
width,
|
|
862
|
-
height,
|
|
863
|
-
x: 0,
|
|
864
|
-
y: 0
|
|
865
|
-
}
|
|
866
|
-
);
|
|
867
|
-
if (blob) {
|
|
868
|
-
return blob;
|
|
869
|
-
}
|
|
870
|
-
return null;
|
|
871
|
-
})
|
|
872
|
-
),
|
|
873
|
-
composite: await get_canvas_blob(this.app.renderer, this.image_container)
|
|
874
|
-
};
|
|
875
|
-
|
|
876
|
-
return blobs;
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
reset_layers(width: number, height: number, persist = false): void {
|
|
880
|
-
const _layers_to_recreate = persist
|
|
881
|
-
? this.layers.map((layer) => [layer.name, layer.id])
|
|
882
|
-
: this.layer_options.layers.map((layer) => [layer, undefined]);
|
|
883
|
-
|
|
884
|
-
this.layers.forEach((layer) => {
|
|
885
|
-
this.delete_layer(layer.id);
|
|
886
|
-
});
|
|
887
|
-
|
|
888
|
-
for (const [layer_name, layer_id] of _layers_to_recreate) {
|
|
889
|
-
this.create_layer({
|
|
890
|
-
width,
|
|
891
|
-
height,
|
|
892
|
-
layer_name: layer_name,
|
|
893
|
-
user_created: this.layer_options.layers.find((l) => l === layer_name)
|
|
894
|
-
? false
|
|
895
|
-
: true,
|
|
896
|
-
layer_id: layer_id
|
|
897
|
-
});
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
if (!persist) {
|
|
901
|
-
this.active_layer = this.layers[0].container;
|
|
902
|
-
this.active_layer_id = this.layers[0].id;
|
|
903
|
-
} else {
|
|
904
|
-
// the id is the same but the layer is new, we need to find the new layer
|
|
905
|
-
this.active_layer =
|
|
906
|
-
this.layers.find((l) => l.id === this.active_layer_id)?.container ||
|
|
907
|
-
this.layers[0].container;
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
this.layer_store.update((state) => ({
|
|
911
|
-
active_layer: this.active_layer_id || this.layers[0].id,
|
|
912
|
-
layers: this.layers
|
|
913
|
-
}));
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
init_layers(width: number, height: number): void {
|
|
917
|
-
for (const layer of this.layers) {
|
|
918
|
-
this.delete_layer(layer.id);
|
|
919
|
-
}
|
|
920
|
-
for (const layer of this.layer_options.layers) {
|
|
921
|
-
this.create_layer({
|
|
922
|
-
width,
|
|
923
|
-
height,
|
|
924
|
-
layer_name: layer,
|
|
925
|
-
user_created: false
|
|
926
|
-
});
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
this.active_layer = this.layers[0].container;
|
|
930
|
-
this.active_layer_id = this.layers[0].id;
|
|
931
|
-
this.layer_store.update((_layers) => ({
|
|
932
|
-
active_layer: this.layers[0].id,
|
|
933
|
-
layers: this.layers
|
|
934
|
-
}));
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
|
|
938
36
|
const core_tools = ["image", "zoom"] as const;
|
|
939
37
|
|
|
940
38
|
interface ImageEditorOptions {
|
|
@@ -1002,7 +100,6 @@ class EditorState {
|
|
|
1002
100
|
throw new Error("EditorState must be created by ImageEditor");
|
|
1003
101
|
}
|
|
1004
102
|
|
|
1005
|
-
// Store private data
|
|
1006
103
|
this.scale = 1.0;
|
|
1007
104
|
this.position = { x: 0, y: 0 };
|
|
1008
105
|
this.subscribers = new Set();
|
|
@@ -1024,7 +121,6 @@ class EditorState {
|
|
|
1024
121
|
});
|
|
1025
122
|
}
|
|
1026
123
|
|
|
1027
|
-
// Internal methods - only accessible to EditorState class
|
|
1028
124
|
_set_position(x: number, y: number): void {
|
|
1029
125
|
const oldPosition = { ...this.position };
|
|
1030
126
|
this.position = { x, y };
|
|
@@ -1048,7 +144,6 @@ class EditorState {
|
|
|
1048
144
|
});
|
|
1049
145
|
}
|
|
1050
146
|
|
|
1051
|
-
// Public method but only affects notifications
|
|
1052
147
|
subscribe(callback: (value: any) => void): () => void {
|
|
1053
148
|
this.subscribers.add(callback);
|
|
1054
149
|
return () => this.subscribers.delete(callback);
|
|
@@ -1063,7 +158,8 @@ export class ImageEditor {
|
|
|
1063
158
|
private app!: Application;
|
|
1064
159
|
private ui_container!: Container;
|
|
1065
160
|
private image_container!: Container;
|
|
1066
|
-
|
|
161
|
+
command_manager: CommandManager;
|
|
162
|
+
|
|
1067
163
|
private layer_manager!: LayerManager;
|
|
1068
164
|
private tools: Map<string, Tool> = new Map<string, Tool>();
|
|
1069
165
|
private current_tool!: ToolbarTool;
|
|
@@ -1114,6 +210,7 @@ export class ImageEditor {
|
|
|
1114
210
|
this.width = options.width;
|
|
1115
211
|
this.height = options.height;
|
|
1116
212
|
this.command_manager = new CommandManager();
|
|
213
|
+
|
|
1117
214
|
this.ready = new Promise((resolve) => {
|
|
1118
215
|
this.ready_resolve = resolve;
|
|
1119
216
|
});
|
|
@@ -1204,16 +301,13 @@ export class ImageEditor {
|
|
|
1204
301
|
powerPreference: "high-performance"
|
|
1205
302
|
});
|
|
1206
303
|
|
|
1207
|
-
// Set canvas properties
|
|
1208
304
|
const canvas = this.app.canvas as HTMLCanvasElement;
|
|
1209
305
|
canvas.style.background = "transparent";
|
|
1210
306
|
|
|
1211
307
|
await this.setup_containers();
|
|
1212
308
|
|
|
1213
|
-
//create background layer
|
|
1214
309
|
this.layer_manager.create_background_layer(this.width, this.height);
|
|
1215
310
|
|
|
1216
|
-
// Create initial layer
|
|
1217
311
|
this.layer_manager.init_layers(this.width, this.height);
|
|
1218
312
|
|
|
1219
313
|
for (const tool of this.tools.values()) {
|
|
@@ -1229,7 +323,6 @@ export class ImageEditor {
|
|
|
1229
323
|
|
|
1230
324
|
this.target_element.appendChild(canvas);
|
|
1231
325
|
|
|
1232
|
-
// Ensure parent element is transparent
|
|
1233
326
|
this.target_element.style.background = "transparent";
|
|
1234
327
|
|
|
1235
328
|
this.dimensions.subscribe((dimensions) => {
|
|
@@ -1256,7 +349,6 @@ export class ImageEditor {
|
|
|
1256
349
|
const effective_width = this.dimensions_value.width * this.scale_value;
|
|
1257
350
|
const effective_height = this.dimensions_value.height * this.scale_value;
|
|
1258
351
|
|
|
1259
|
-
// Convert image_container's position (in stage space) to outline_container local coordinates.
|
|
1260
352
|
const local_x = Math.round(
|
|
1261
353
|
this.image_container.position.x - this.outline_container.position.x
|
|
1262
354
|
);
|
|
@@ -1264,7 +356,6 @@ export class ImageEditor {
|
|
|
1264
356
|
this.image_container.position.y - this.outline_container.position.y
|
|
1265
357
|
);
|
|
1266
358
|
|
|
1267
|
-
// Make sure overlay container follows the same position as outline container
|
|
1268
359
|
this.overlay_container.position.set(
|
|
1269
360
|
this.outline_container.position.x,
|
|
1270
361
|
this.outline_container.position.y
|
|
@@ -1278,7 +369,6 @@ export class ImageEditor {
|
|
|
1278
369
|
alpha: 1
|
|
1279
370
|
});
|
|
1280
371
|
|
|
1281
|
-
// Draw border region indicator if border_region > 0
|
|
1282
372
|
if (this.border_region > 0) {
|
|
1283
373
|
const scaled_border = this.border_region * this.scale_value;
|
|
1284
374
|
const border_x = local_x + scaled_border - 1;
|
|
@@ -1286,10 +376,8 @@ export class ImageEditor {
|
|
|
1286
376
|
const border_width = effective_width - scaled_border * 2 + 1;
|
|
1287
377
|
const border_height = effective_height - scaled_border * 2 + 1;
|
|
1288
378
|
|
|
1289
|
-
// Clear previous overlay graphics
|
|
1290
379
|
this.overlay_graphics.clear();
|
|
1291
380
|
|
|
1292
|
-
// Draw border rectangle using pixelLine for crisp 1px outline
|
|
1293
381
|
this.overlay_graphics
|
|
1294
382
|
.rect(border_x, border_y, border_width, border_height)
|
|
1295
383
|
.stroke({
|
|
@@ -1299,15 +387,12 @@ export class ImageEditor {
|
|
|
1299
387
|
pixelLine: true
|
|
1300
388
|
});
|
|
1301
389
|
|
|
1302
|
-
// Create dashed line effect by drawing small rects along the border
|
|
1303
390
|
const dashLength = 5;
|
|
1304
391
|
const gapLength = 5;
|
|
1305
392
|
const totalLength = dashLength + gapLength;
|
|
1306
393
|
const lineColor = 0x999999;
|
|
1307
394
|
|
|
1308
|
-
// Draw dashed horizontal lines (top and bottom)
|
|
1309
395
|
for (let x = border_x; x < border_x + border_width; x += totalLength) {
|
|
1310
|
-
// Top dash
|
|
1311
396
|
this.overlay_graphics
|
|
1312
397
|
.rect(
|
|
1313
398
|
x,
|
|
@@ -1317,7 +402,6 @@ export class ImageEditor {
|
|
|
1317
402
|
)
|
|
1318
403
|
.fill({ color: lineColor, alpha: 0.7 });
|
|
1319
404
|
|
|
1320
|
-
// Bottom dash
|
|
1321
405
|
this.overlay_graphics
|
|
1322
406
|
.rect(
|
|
1323
407
|
x,
|
|
@@ -1328,9 +412,7 @@ export class ImageEditor {
|
|
|
1328
412
|
.fill({ color: lineColor, alpha: 0.7 });
|
|
1329
413
|
}
|
|
1330
414
|
|
|
1331
|
-
// Draw dashed vertical lines (left and right)
|
|
1332
415
|
for (let y = border_y; y < border_y + border_height; y += totalLength) {
|
|
1333
|
-
// Left dash
|
|
1334
416
|
this.overlay_graphics
|
|
1335
417
|
.rect(
|
|
1336
418
|
border_x,
|
|
@@ -1340,7 +422,6 @@ export class ImageEditor {
|
|
|
1340
422
|
)
|
|
1341
423
|
.fill({ color: lineColor, alpha: 0.7 });
|
|
1342
424
|
|
|
1343
|
-
// Right dash
|
|
1344
425
|
this.overlay_graphics
|
|
1345
426
|
.rect(
|
|
1346
427
|
border_x + border_width,
|
|
@@ -1351,7 +432,6 @@ export class ImageEditor {
|
|
|
1351
432
|
.fill({ color: lineColor, alpha: 0.7 });
|
|
1352
433
|
}
|
|
1353
434
|
} else {
|
|
1354
|
-
// Clear overlay graphics if no border region
|
|
1355
435
|
this.overlay_graphics.clear();
|
|
1356
436
|
}
|
|
1357
437
|
});
|
|
@@ -1445,7 +525,6 @@ export class ImageEditor {
|
|
|
1445
525
|
this.app.renderer.resize(width, height);
|
|
1446
526
|
}
|
|
1447
527
|
|
|
1448
|
-
// Instead, update the container position to stay centered
|
|
1449
528
|
const app_center_x = width / 2;
|
|
1450
529
|
const app_center_y = height / 2;
|
|
1451
530
|
|
|
@@ -1464,6 +543,12 @@ export class ImageEditor {
|
|
|
1464
543
|
zoom.cleanup();
|
|
1465
544
|
zoom.setup(this.context, this.current_tool, this.current_subtool);
|
|
1466
545
|
}
|
|
546
|
+
|
|
547
|
+
const brush = this.tools.get("brush") as BrushTool;
|
|
548
|
+
|
|
549
|
+
if (brush) {
|
|
550
|
+
this.set_tool("draw");
|
|
551
|
+
}
|
|
1467
552
|
}
|
|
1468
553
|
|
|
1469
554
|
async set_image_properties(properties: {
|
|
@@ -1534,6 +619,7 @@ export class ImageEditor {
|
|
|
1534
619
|
* @param url The URL of the image to add
|
|
1535
620
|
*/
|
|
1536
621
|
async add_image_from_url(url: string): Promise<void> {
|
|
622
|
+
this.command_manager.reset();
|
|
1537
623
|
const image_tool = this.tools.get("image") as ImageTool;
|
|
1538
624
|
const texture = await Assets.load(url);
|
|
1539
625
|
await image_tool.add_image({
|
|
@@ -1542,14 +628,10 @@ export class ImageEditor {
|
|
|
1542
628
|
border_region: this.border_region
|
|
1543
629
|
});
|
|
1544
630
|
|
|
1545
|
-
// Update resize tool if present
|
|
1546
631
|
const resize_tool = this.tools.get("resize") as any;
|
|
1547
632
|
if (resize_tool && typeof resize_tool.set_border_region === "function") {
|
|
1548
633
|
resize_tool.set_border_region(this.border_region);
|
|
1549
634
|
}
|
|
1550
|
-
|
|
1551
|
-
this.notify("change");
|
|
1552
|
-
this.notify("input");
|
|
1553
635
|
}
|
|
1554
636
|
|
|
1555
637
|
set_tool(tool: ToolbarTool): void {
|
|
@@ -1574,11 +656,9 @@ export class ImageEditor {
|
|
|
1574
656
|
|
|
1575
657
|
async reset_canvas(): Promise<void> {
|
|
1576
658
|
this.layer_manager.reset_layers(this.width, this.height);
|
|
1577
|
-
// Clear background image
|
|
1578
659
|
this.background_image = undefined;
|
|
1579
660
|
this.background_image_present.set(false);
|
|
1580
661
|
|
|
1581
|
-
// Reset position, scale and dimensions to default values
|
|
1582
662
|
await this.set_image_properties({
|
|
1583
663
|
width: this.width,
|
|
1584
664
|
height: this.height,
|
|
@@ -1587,13 +667,10 @@ export class ImageEditor {
|
|
|
1587
667
|
animate: false
|
|
1588
668
|
});
|
|
1589
669
|
|
|
1590
|
-
// Reset command stacks
|
|
1591
670
|
this.command_manager = new CommandManager();
|
|
1592
671
|
|
|
1593
|
-
//create background layer
|
|
1594
672
|
this.layer_manager.create_background_layer(this.width, this.height);
|
|
1595
673
|
|
|
1596
|
-
// Reset tools
|
|
1597
674
|
for (const tool of this.tools.values()) {
|
|
1598
675
|
tool.cleanup();
|
|
1599
676
|
tool.setup(this.context, this.current_tool, this.current_subtool);
|
|
@@ -1610,14 +687,14 @@ export class ImageEditor {
|
|
|
1610
687
|
}
|
|
1611
688
|
|
|
1612
689
|
add_layer(): void {
|
|
1613
|
-
this.
|
|
690
|
+
const add_layer_command = new AddLayerCommand(this.context, {
|
|
1614
691
|
width: this.width,
|
|
1615
692
|
height: this.height,
|
|
1616
|
-
layer_name: undefined,
|
|
1617
693
|
user_created: true,
|
|
1618
|
-
layer_id: undefined,
|
|
1619
694
|
make_active: true
|
|
1620
695
|
});
|
|
696
|
+
|
|
697
|
+
this.command_manager.execute(add_layer_command);
|
|
1621
698
|
this.notify("change");
|
|
1622
699
|
}
|
|
1623
700
|
|
|
@@ -1627,6 +704,7 @@ export class ImageEditor {
|
|
|
1627
704
|
* @returns A Promise that resolves when all layers are added
|
|
1628
705
|
*/
|
|
1629
706
|
async add_layers_from_url(layer_urls: string[] | undefined): Promise<void> {
|
|
707
|
+
this.command_manager.reset();
|
|
1630
708
|
const _layers = this.layer_manager.get_layers();
|
|
1631
709
|
_layers.forEach((l) => this.layer_manager.delete_layer(l.id));
|
|
1632
710
|
if (layer_urls === undefined || layer_urls.length === 0) {
|
|
@@ -1645,7 +723,6 @@ export class ImageEditor {
|
|
|
1645
723
|
|
|
1646
724
|
this.layer_manager.set_active_layer(_layers[0].id);
|
|
1647
725
|
|
|
1648
|
-
// Update resize tool if present with border region
|
|
1649
726
|
const resize_tool = this.tools.get("resize") as any;
|
|
1650
727
|
if (resize_tool && typeof resize_tool.set_border_region === "function") {
|
|
1651
728
|
resize_tool.set_border_region(this.border_region);
|
|
@@ -1661,12 +738,18 @@ export class ImageEditor {
|
|
|
1661
738
|
}
|
|
1662
739
|
|
|
1663
740
|
move_layer(id: string, direction: "up" | "down"): void {
|
|
1664
|
-
|
|
741
|
+
const reorder_layer_command = new ReorderLayerCommand(
|
|
742
|
+
this.context,
|
|
743
|
+
id,
|
|
744
|
+
direction
|
|
745
|
+
);
|
|
746
|
+
this.command_manager.execute(reorder_layer_command);
|
|
1665
747
|
this.notify("change");
|
|
1666
748
|
}
|
|
1667
749
|
|
|
1668
750
|
delete_layer(id: string): void {
|
|
1669
|
-
this.
|
|
751
|
+
const remove_layer_command = new RemoveLayerCommand(this.context, id);
|
|
752
|
+
this.command_manager.execute(remove_layer_command);
|
|
1670
753
|
this.notify("change");
|
|
1671
754
|
}
|
|
1672
755
|
|
|
@@ -1685,11 +768,9 @@ export class ImageEditor {
|
|
|
1685
768
|
| "bottom-right",
|
|
1686
769
|
scale: boolean
|
|
1687
770
|
): void {
|
|
1688
|
-
// Store old dimensions before updating
|
|
1689
771
|
const oldWidth = this.width;
|
|
1690
772
|
const oldHeight = this.height;
|
|
1691
773
|
|
|
1692
|
-
// Resize layers first, passing old dimensions for anchor calculation
|
|
1693
774
|
this.layer_manager.resize_all_layers(
|
|
1694
775
|
width,
|
|
1695
776
|
height,
|
|
@@ -1699,16 +780,14 @@ export class ImageEditor {
|
|
|
1699
780
|
oldHeight
|
|
1700
781
|
);
|
|
1701
782
|
|
|
1702
|
-
// Update the editor's main dimension tracking *after* resizing layers
|
|
1703
783
|
this.width = width;
|
|
1704
784
|
this.height = height;
|
|
1705
785
|
|
|
1706
|
-
// Update the dimensions store and reset view scale/position
|
|
1707
786
|
this.set_image_properties({
|
|
1708
787
|
width,
|
|
1709
788
|
height,
|
|
1710
|
-
scale: 1,
|
|
1711
|
-
position: { x: 0, y: 0 },
|
|
789
|
+
scale: 1,
|
|
790
|
+
position: { x: 0, y: 0 },
|
|
1712
791
|
animate: false
|
|
1713
792
|
});
|
|
1714
793
|
this.notify("change");
|
|
@@ -1741,9 +820,6 @@ export class ImageEditor {
|
|
|
1741
820
|
|
|
1742
821
|
destroy(): void {
|
|
1743
822
|
if (!this.app) return;
|
|
1744
|
-
// for (const tool of this.tools) {
|
|
1745
|
-
// tool[1]?.cleanup();
|
|
1746
|
-
// }
|
|
1747
823
|
|
|
1748
824
|
this.app?.destroy();
|
|
1749
825
|
}
|