@gradio/imageeditor 0.12.13 → 0.13.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 +23 -0
- package/Example.svelte +1 -1
- package/IMAGE_EDITOR_OVERVIEW.md +224 -0
- package/ImageEditor.stories.svelte +4 -4
- package/Index.svelte +35 -46
- package/{shared/InteractiveImageEditor.svelte → InteractiveImageEditor.svelte} +56 -103
- package/dist/Example.svelte.d.ts +1 -1
- package/dist/Index.svelte +29 -33
- package/dist/Index.svelte.d.ts +17 -16
- package/dist/{shared/InteractiveImageEditor.svelte → InteractiveImageEditor.svelte} +47 -88
- package/dist/{shared/InteractiveImageEditor.svelte.d.ts → InteractiveImageEditor.svelte.d.ts} +13 -15
- package/dist/shared/Anchor.svelte +311 -0
- package/dist/shared/Anchor.svelte.d.ts +18 -0
- package/dist/shared/Controls.svelte +162 -23
- package/dist/shared/Controls.svelte.d.ts +7 -2
- package/dist/shared/{tools/IconButton.svelte → IconButton.svelte} +19 -3
- package/dist/shared/{tools/IconButton.svelte.d.ts → IconButton.svelte.d.ts} +4 -1
- package/dist/shared/ImageEditor.svelte +546 -289
- package/dist/shared/ImageEditor.svelte.d.ts +51 -56
- package/dist/shared/Layers.svelte +233 -0
- package/dist/shared/Layers.svelte.d.ts +35 -0
- package/dist/shared/Resize.svelte +218 -0
- package/dist/shared/Resize.svelte.d.ts +29 -0
- package/dist/shared/SecondaryToolbar.svelte +54 -0
- package/dist/shared/SecondaryToolbar.svelte.d.ts +35 -0
- package/dist/shared/Toolbar.svelte +337 -0
- package/dist/shared/Toolbar.svelte.d.ts +42 -0
- package/dist/shared/{tools → brush}/BrushOptions.svelte +68 -64
- package/dist/shared/{tools → brush}/BrushOptions.svelte.d.ts +9 -9
- package/dist/shared/{tools → brush}/BrushSize.svelte +2 -27
- package/dist/shared/{tools → brush}/ColorField.svelte +2 -1
- package/dist/shared/{tools → brush}/ColorField.svelte.d.ts +1 -0
- package/dist/shared/{tools → brush}/ColorPicker.svelte +81 -9
- package/dist/shared/{tools → brush}/ColorPicker.svelte.d.ts +1 -0
- package/dist/shared/{tools → brush}/ColorSwatch.svelte +78 -38
- package/dist/shared/{tools → brush}/ColorSwatch.svelte.d.ts +6 -2
- package/dist/shared/brush/brush-cursor.d.ts +101 -0
- package/dist/shared/brush/brush-cursor.js +327 -0
- package/dist/shared/brush/brush-textures.d.ts +71 -0
- package/dist/shared/brush/brush-textures.js +404 -0
- package/dist/shared/brush/brush-utils.d.ts +32 -0
- package/dist/shared/brush/brush-utils.js +59 -0
- package/dist/shared/brush/brush.d.ts +152 -0
- package/dist/shared/brush/brush.js +396 -0
- package/dist/shared/brush/types.d.ts +37 -0
- package/dist/shared/brush/types.js +1 -0
- package/dist/shared/core/editor.d.ts +252 -0
- package/dist/shared/core/editor.js +1221 -0
- package/dist/shared/crop/crop.d.ts +201 -0
- package/dist/shared/crop/crop.js +727 -0
- package/dist/shared/image/image.d.ts +89 -0
- package/dist/shared/image/image.js +185 -0
- package/dist/shared/resize/resize.d.ts +270 -0
- package/dist/shared/resize/resize.js +1413 -0
- package/dist/shared/types.d.ts +12 -0
- package/dist/shared/utils/pixi.d.ts +16 -57
- package/dist/shared/utils/pixi.js +15 -113
- package/dist/shared/zoom/zoom.d.ts +136 -0
- package/dist/shared/zoom/zoom.js +548 -0
- package/package.json +11 -10
- package/shared/Anchor.svelte +384 -0
- package/shared/Controls.svelte +194 -27
- package/shared/{tools/IconButton.svelte → IconButton.svelte} +21 -4
- package/shared/ImageEditor.svelte +682 -349
- package/shared/Layers.svelte +249 -0
- package/shared/Resize.svelte +239 -0
- package/shared/SecondaryToolbar.svelte +61 -0
- package/shared/Toolbar.svelte +386 -0
- package/shared/brush/BRUSH_TOOL.md +134 -0
- package/shared/brush/BrushOptions.svelte +200 -0
- package/shared/{tools → brush}/BrushSize.svelte +2 -30
- package/shared/{tools → brush}/ColorField.svelte +3 -1
- package/shared/{tools → brush}/ColorPicker.svelte +86 -9
- package/shared/{tools → brush}/ColorSwatch.svelte +95 -42
- package/shared/brush/brush-cursor.ts +408 -0
- package/shared/brush/brush-textures.ts +508 -0
- package/shared/brush/brush-utils.ts +76 -0
- package/shared/brush/brush.ts +506 -0
- package/shared/brush/types.ts +41 -0
- package/shared/core/EDITOR.md +182 -0
- package/shared/core/editor.ts +1730 -0
- package/shared/crop/CROP.md +139 -0
- package/shared/crop/crop.ts +1027 -0
- package/shared/image/IMAGE.md +135 -0
- package/shared/image/image.ts +300 -0
- package/shared/resize/resize.ts +1894 -0
- package/shared/types.ts +14 -0
- package/shared/utils/pixi.ts +19 -246
- package/shared/zoom/zoom.ts +729 -0
- package/dist/shared/layers/Layers.svelte +0 -231
- package/dist/shared/layers/Layers.svelte.d.ts +0 -18
- package/dist/shared/layers/utils.d.ts +0 -73
- package/dist/shared/layers/utils.js +0 -156
- package/dist/shared/tools/Brush.svelte +0 -215
- package/dist/shared/tools/Brush.svelte.d.ts +0 -29
- package/dist/shared/tools/Crop.svelte +0 -155
- package/dist/shared/tools/Crop.svelte.d.ts +0 -16
- package/dist/shared/tools/Cropper.svelte +0 -271
- package/dist/shared/tools/Cropper.svelte.d.ts +0 -41
- package/dist/shared/tools/Handle.svelte +0 -260
- package/dist/shared/tools/Handle.svelte.d.ts +0 -29
- package/dist/shared/tools/Sources.svelte +0 -257
- package/dist/shared/tools/Sources.svelte.d.ts +0 -34
- package/dist/shared/tools/Tools.svelte +0 -102
- package/dist/shared/tools/Tools.svelte.d.ts +0 -48
- package/dist/shared/tools/brush.d.ts +0 -62
- package/dist/shared/tools/brush.js +0 -133
- package/dist/shared/tools/crop.d.ts +0 -15
- package/dist/shared/tools/crop.js +0 -154
- package/dist/shared/tools/index.d.ts +0 -5
- package/dist/shared/tools/index.js +0 -5
- package/dist/shared/tools/sources.d.ts +0 -46
- package/dist/shared/tools/sources.js +0 -121
- package/dist/shared/tools/types.d.ts +0 -1
- package/shared/layers/Layers.svelte +0 -250
- package/shared/layers/utils.ts +0 -294
- package/shared/tools/Brush.svelte +0 -280
- package/shared/tools/BrushOptions.svelte +0 -180
- package/shared/tools/Crop.svelte +0 -189
- package/shared/tools/Cropper.svelte +0 -346
- package/shared/tools/Handle.svelte +0 -289
- package/shared/tools/Sources.svelte +0 -283
- package/shared/tools/Tools.svelte +0 -162
- package/shared/tools/brush.ts +0 -247
- package/shared/tools/crop.ts +0 -213
- package/shared/tools/index.ts +0 -5
- package/shared/tools/sources.ts +0 -195
- package/shared/tools/types.ts +0 -1
- /package/dist/shared/{tools → brush}/BrushSize.svelte.d.ts +0 -0
- /package/dist/shared/{tools/types.js → types.js} +0 -0
|
@@ -0,0 +1,1221 @@
|
|
|
1
|
+
import { Application, Container, Graphics, Sprite, Filter, RenderTexture, GlProgram, defaultFilterVert, SCALE_MODES, Texture, Assets } from "pixi.js";
|
|
2
|
+
import { DropShadowFilter as BlurFilter } from "pixi-filters/drop-shadow";
|
|
3
|
+
import { ImageTool } from "../image/image";
|
|
4
|
+
import { AddImageCommand } from "../image/image";
|
|
5
|
+
import { ZoomTool } from "../zoom/zoom";
|
|
6
|
+
import { spring } from "svelte/motion";
|
|
7
|
+
import { writable, get } from "svelte/store";
|
|
8
|
+
import { Rectangle } from "pixi.js";
|
|
9
|
+
import {} from "../types";
|
|
10
|
+
import { get_canvas_blob } from "../utils/pixi";
|
|
11
|
+
export class CommandManager {
|
|
12
|
+
undo_stack = [];
|
|
13
|
+
redo_stack = [];
|
|
14
|
+
async execute(command) {
|
|
15
|
+
await command.execute();
|
|
16
|
+
this.undo_stack.push(command);
|
|
17
|
+
this.redo_stack = []; // Clear redo stack when new command is executed
|
|
18
|
+
}
|
|
19
|
+
async undo() {
|
|
20
|
+
const command = this.undo_stack.pop();
|
|
21
|
+
if (command) {
|
|
22
|
+
await command.undo();
|
|
23
|
+
this.redo_stack.push(command);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async redo() {
|
|
27
|
+
const command = this.redo_stack.pop();
|
|
28
|
+
if (command) {
|
|
29
|
+
await command.execute();
|
|
30
|
+
this.undo_stack.push(command);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export class LayerManager {
|
|
35
|
+
layers = [];
|
|
36
|
+
active_layer = null;
|
|
37
|
+
draw_textures = new Map();
|
|
38
|
+
layer_store = writable({
|
|
39
|
+
active_layer: "",
|
|
40
|
+
layers: []
|
|
41
|
+
});
|
|
42
|
+
background_layer = null;
|
|
43
|
+
image_container;
|
|
44
|
+
app;
|
|
45
|
+
fixed_canvas;
|
|
46
|
+
dark;
|
|
47
|
+
border_region;
|
|
48
|
+
layer_options;
|
|
49
|
+
constructor(image_container, app, fixed_canvas, dark, border_region, layer_options) {
|
|
50
|
+
this.image_container = image_container;
|
|
51
|
+
this.app = app;
|
|
52
|
+
this.fixed_canvas = fixed_canvas;
|
|
53
|
+
this.dark = dark;
|
|
54
|
+
this.border_region = border_region;
|
|
55
|
+
this.layer_options = layer_options;
|
|
56
|
+
}
|
|
57
|
+
create_background_layer(width, height) {
|
|
58
|
+
if (this.background_layer) {
|
|
59
|
+
this.background_layer.destroy();
|
|
60
|
+
}
|
|
61
|
+
// Create new background layer
|
|
62
|
+
const layer = new Container();
|
|
63
|
+
this.background_layer = layer;
|
|
64
|
+
// Create a render texture for the background with transparency
|
|
65
|
+
const bg_texture = RenderTexture.create({
|
|
66
|
+
width,
|
|
67
|
+
height,
|
|
68
|
+
resolution: window.devicePixelRatio,
|
|
69
|
+
antialias: true,
|
|
70
|
+
scaleMode: SCALE_MODES.NEAREST
|
|
71
|
+
});
|
|
72
|
+
// Create sprite to display the texture with transparency
|
|
73
|
+
const bg_sprite = new Sprite(bg_texture);
|
|
74
|
+
layer.addChild(bg_sprite);
|
|
75
|
+
// Clear the texture with transparency
|
|
76
|
+
const clear_graphics = new Graphics();
|
|
77
|
+
clear_graphics.clear();
|
|
78
|
+
clear_graphics
|
|
79
|
+
.rect(0, 0, width, height)
|
|
80
|
+
.fill({ color: this.dark ? 0x333333 : 0xffffff, alpha: 1 });
|
|
81
|
+
// Render with transparency
|
|
82
|
+
this.app.renderer.render({
|
|
83
|
+
container: clear_graphics,
|
|
84
|
+
target: bg_texture,
|
|
85
|
+
clear: true
|
|
86
|
+
});
|
|
87
|
+
this.image_container.addChild(layer);
|
|
88
|
+
layer.zIndex = -1;
|
|
89
|
+
this.update_layer_order();
|
|
90
|
+
return layer;
|
|
91
|
+
}
|
|
92
|
+
set_layer_options(layer_options, width, height) {
|
|
93
|
+
this.layer_options = layer_options;
|
|
94
|
+
this.reset_layers(width, height);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Creates a background layer from an image URL
|
|
98
|
+
* @param url The URL of the image to use as background
|
|
99
|
+
* @param width The width of the layer (if fixed_canvas is true)
|
|
100
|
+
* @param height The height of the layer (if fixed_canvas is true)
|
|
101
|
+
* @param borderRegion Minimum border region to add around the image for outpainting (in pixels)
|
|
102
|
+
* @returns A Promise that resolves to the background Container when the image is loaded
|
|
103
|
+
*/
|
|
104
|
+
async create_background_layer_from_url(url, width, height) {
|
|
105
|
+
const layer = this.create_background_layer(width || this.image_container.width, height || this.image_container.height);
|
|
106
|
+
try {
|
|
107
|
+
const texture = await Texture.from(url);
|
|
108
|
+
const sprite = new Sprite(texture);
|
|
109
|
+
const imageWidth = sprite.texture.width;
|
|
110
|
+
const imageHeight = sprite.texture.height;
|
|
111
|
+
// Calculate dimensions based on whether canvas size is fixed
|
|
112
|
+
const containerWidth = width || this.image_container.width;
|
|
113
|
+
const containerHeight = height || this.image_container.height;
|
|
114
|
+
if (this.fixed_canvas) {
|
|
115
|
+
// If fixed canvas, fit the image within the specified dimensions minus border region
|
|
116
|
+
// Calculate aspect-preserving dimensions
|
|
117
|
+
const effectiveContainerWidth = Math.max(containerWidth - this.border_region * 2, 10);
|
|
118
|
+
const effectiveContainerHeight = Math.max(containerHeight - this.border_region * 2, 10);
|
|
119
|
+
const imageAspectRatio = imageWidth / imageHeight;
|
|
120
|
+
const containerAspectRatio = effectiveContainerWidth / effectiveContainerHeight;
|
|
121
|
+
let finalWidth, finalHeight;
|
|
122
|
+
let posX = this.border_region, // Start with border offset
|
|
123
|
+
posY = this.border_region;
|
|
124
|
+
if (imageWidth <= effectiveContainerWidth &&
|
|
125
|
+
imageHeight <= effectiveContainerHeight) {
|
|
126
|
+
// Image fits within container without scaling
|
|
127
|
+
finalWidth = imageWidth;
|
|
128
|
+
finalHeight = imageHeight;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
if (imageAspectRatio > containerAspectRatio) {
|
|
132
|
+
// Width is the limiting factor
|
|
133
|
+
finalWidth = effectiveContainerWidth;
|
|
134
|
+
finalHeight = effectiveContainerWidth / imageAspectRatio;
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
// Height is the limiting factor
|
|
138
|
+
finalHeight = effectiveContainerHeight;
|
|
139
|
+
finalWidth = effectiveContainerHeight * imageAspectRatio;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Center image within the effective container area (which already accounts for border)
|
|
143
|
+
posX += Math.round((effectiveContainerWidth - finalWidth) / 2);
|
|
144
|
+
posY += Math.round((effectiveContainerHeight - finalHeight) / 2);
|
|
145
|
+
sprite.width = finalWidth;
|
|
146
|
+
sprite.height = finalHeight;
|
|
147
|
+
sprite.position.set(posX, posY);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
// If not fixed canvas, use the natural dimensions of the image plus border
|
|
151
|
+
// Position starts at the border offset
|
|
152
|
+
sprite.position.set(this.border_region, this.border_region);
|
|
153
|
+
// We need to re-create the background layer with the actual image dimensions plus border
|
|
154
|
+
if (this.background_layer) {
|
|
155
|
+
this.background_layer.destroy();
|
|
156
|
+
}
|
|
157
|
+
// Create new layer with image dimensions plus border on all sides
|
|
158
|
+
const totalWidth = imageWidth + this.border_region * 2;
|
|
159
|
+
const totalHeight = imageHeight + this.border_region * 2;
|
|
160
|
+
const newLayer = this.create_background_layer(totalWidth, totalHeight);
|
|
161
|
+
sprite.width = imageWidth;
|
|
162
|
+
sprite.height = imageHeight;
|
|
163
|
+
newLayer.addChild(sprite);
|
|
164
|
+
return newLayer;
|
|
165
|
+
}
|
|
166
|
+
layer.addChild(sprite);
|
|
167
|
+
return layer;
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
console.error("Error loading image from URL:", error);
|
|
171
|
+
return layer;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
create_layer({ width, height, layer_name, user_created }) {
|
|
175
|
+
const layer = new Container();
|
|
176
|
+
const layer_id = Math.random().toString(36).substring(2, 15);
|
|
177
|
+
const _layer_name = layer_name || `Layer ${this.layers.length + 1}`;
|
|
178
|
+
this.layers.push({
|
|
179
|
+
name: _layer_name,
|
|
180
|
+
id: layer_id,
|
|
181
|
+
container: layer,
|
|
182
|
+
user_created
|
|
183
|
+
});
|
|
184
|
+
this.image_container.addChild(layer);
|
|
185
|
+
// Create a single render texture for drawing with transparency
|
|
186
|
+
const draw_texture = RenderTexture.create({
|
|
187
|
+
width,
|
|
188
|
+
height,
|
|
189
|
+
resolution: window.devicePixelRatio,
|
|
190
|
+
antialias: true,
|
|
191
|
+
scaleMode: SCALE_MODES.NEAREST
|
|
192
|
+
});
|
|
193
|
+
const canvas_sprite = new Sprite(draw_texture);
|
|
194
|
+
layer.addChild(canvas_sprite);
|
|
195
|
+
const clear_graphics = new Graphics();
|
|
196
|
+
clear_graphics.clear();
|
|
197
|
+
clear_graphics.beginFill(0, 0);
|
|
198
|
+
clear_graphics.drawRect(0, 0, width, height);
|
|
199
|
+
clear_graphics.endFill();
|
|
200
|
+
this.app.renderer.render({
|
|
201
|
+
container: clear_graphics,
|
|
202
|
+
target: draw_texture,
|
|
203
|
+
clear: true
|
|
204
|
+
});
|
|
205
|
+
// Store texture for drawing
|
|
206
|
+
this.draw_textures.set(layer, draw_texture);
|
|
207
|
+
this.update_layer_order();
|
|
208
|
+
this.active_layer = layer;
|
|
209
|
+
this.layer_store.set({
|
|
210
|
+
active_layer: layer_id,
|
|
211
|
+
layers: this.layers
|
|
212
|
+
});
|
|
213
|
+
return layer;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Creates a new layer with an image loaded from a URL
|
|
217
|
+
* @param url The URL of the image to load
|
|
218
|
+
* @param borderRegion Minimum border region to add around the image for outpainting (in pixels)
|
|
219
|
+
* @returns A Promise that resolves to the layer ID
|
|
220
|
+
*/
|
|
221
|
+
async add_layer_from_url(url) {
|
|
222
|
+
const { width, height } = this.image_container.getLocalBounds();
|
|
223
|
+
const layer = this.create_layer({
|
|
224
|
+
width,
|
|
225
|
+
height,
|
|
226
|
+
layer_name: "Layer 1",
|
|
227
|
+
user_created: true
|
|
228
|
+
});
|
|
229
|
+
const layerIndex = this.layers.findIndex((l) => l.container === layer);
|
|
230
|
+
if (layerIndex === -1) {
|
|
231
|
+
console.error("Could not find newly created layer");
|
|
232
|
+
return "";
|
|
233
|
+
}
|
|
234
|
+
const layerId = this.layers[layerIndex].id;
|
|
235
|
+
try {
|
|
236
|
+
const texture = await Assets.load(url);
|
|
237
|
+
const drawTexture = this.draw_textures.get(layer);
|
|
238
|
+
if (!drawTexture) {
|
|
239
|
+
console.error("No draw texture found for layer");
|
|
240
|
+
return layerId;
|
|
241
|
+
}
|
|
242
|
+
const sprite = new Sprite(texture);
|
|
243
|
+
const imageWidth = sprite.width;
|
|
244
|
+
const imageHeight = sprite.height;
|
|
245
|
+
let posX = this.border_region;
|
|
246
|
+
let posY = this.border_region;
|
|
247
|
+
// Calculate effective dimensions (accounting for border)
|
|
248
|
+
const effectiveWidth = this.fixed_canvas
|
|
249
|
+
? width - this.border_region * 2
|
|
250
|
+
: width;
|
|
251
|
+
const effectiveHeight = this.fixed_canvas
|
|
252
|
+
? height - this.border_region * 2
|
|
253
|
+
: height;
|
|
254
|
+
// If the image is smaller than the effective layer area, center it within that area
|
|
255
|
+
if (imageWidth < effectiveWidth || imageHeight < effectiveHeight) {
|
|
256
|
+
posX = Math.floor((effectiveWidth - imageWidth) / 2);
|
|
257
|
+
posY = Math.floor((effectiveHeight - imageHeight) / 2);
|
|
258
|
+
}
|
|
259
|
+
sprite.position.set(posX, posY);
|
|
260
|
+
// If the image is larger than the effective layer area, scale it down to fit
|
|
261
|
+
if (imageWidth > effectiveWidth || imageHeight > effectiveHeight) {
|
|
262
|
+
const imageAspectRatio = imageWidth / imageHeight;
|
|
263
|
+
const areaAspectRatio = effectiveWidth / effectiveHeight;
|
|
264
|
+
let finalWidth, finalHeight;
|
|
265
|
+
if (imageAspectRatio > areaAspectRatio) {
|
|
266
|
+
// Width is the limiting factor
|
|
267
|
+
finalWidth = effectiveWidth;
|
|
268
|
+
finalHeight = effectiveWidth / imageAspectRatio;
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
// Height is the limiting factor
|
|
272
|
+
finalHeight = effectiveHeight;
|
|
273
|
+
finalWidth = effectiveHeight * imageAspectRatio;
|
|
274
|
+
}
|
|
275
|
+
sprite.width = finalWidth;
|
|
276
|
+
sprite.height = finalHeight;
|
|
277
|
+
// Recalculate position to center within effective area
|
|
278
|
+
posX =
|
|
279
|
+
this.border_region + Math.floor((effectiveWidth - finalWidth) / 2);
|
|
280
|
+
posY =
|
|
281
|
+
this.border_region + Math.floor((effectiveHeight - finalHeight) / 2);
|
|
282
|
+
sprite.position.set(posX, posY);
|
|
283
|
+
}
|
|
284
|
+
this.app.renderer.render(sprite, { renderTexture: drawTexture });
|
|
285
|
+
this.set_active_layer(layerId);
|
|
286
|
+
return layerId;
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
console.error("Error loading image from URL:", error);
|
|
290
|
+
return layerId;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
get_active_layer() {
|
|
294
|
+
return this.active_layer;
|
|
295
|
+
}
|
|
296
|
+
set_active_layer(id) {
|
|
297
|
+
if (this.layers.some((l) => l.id === id)) {
|
|
298
|
+
this.active_layer =
|
|
299
|
+
this.layers.find((l) => l.id === id)?.container ||
|
|
300
|
+
this.layers[0]?.container ||
|
|
301
|
+
null;
|
|
302
|
+
this.layer_store.set({
|
|
303
|
+
active_layer: id,
|
|
304
|
+
layers: this.layers
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
get_layers() {
|
|
309
|
+
return this.layers;
|
|
310
|
+
}
|
|
311
|
+
get_layer_textures(id) {
|
|
312
|
+
const layer = this.layers.find((l) => l.id === id);
|
|
313
|
+
if (layer) {
|
|
314
|
+
const draw = this.draw_textures.get(layer.container);
|
|
315
|
+
if (draw) {
|
|
316
|
+
return { draw };
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
delete_layer(id) {
|
|
322
|
+
const index = this.layers.findIndex((l) => l.id === id);
|
|
323
|
+
if (index > -1) {
|
|
324
|
+
// Clean up texture
|
|
325
|
+
const draw_texture = this.draw_textures.get(this.layers[index].container);
|
|
326
|
+
if (draw_texture) {
|
|
327
|
+
draw_texture.destroy();
|
|
328
|
+
this.draw_textures.delete(this.layers[index].container);
|
|
329
|
+
}
|
|
330
|
+
this.layers[index].container.destroy();
|
|
331
|
+
if (this.active_layer === this.layers[index].container) {
|
|
332
|
+
this.active_layer =
|
|
333
|
+
this.layers[Math.max(0, index - 1)]?.container || null;
|
|
334
|
+
}
|
|
335
|
+
this.layers = this.layers.filter((l) => l.id !== id);
|
|
336
|
+
this.layer_store.update((_layers) => ({
|
|
337
|
+
active_layer: _layers.active_layer === id
|
|
338
|
+
? this.layers[this.layers.length - 1]?.id
|
|
339
|
+
: _layers.active_layer,
|
|
340
|
+
layers: this.layers
|
|
341
|
+
}));
|
|
342
|
+
this.update_layer_order();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
update_layer_order() {
|
|
346
|
+
// Ensure background layer always has lowest z-index
|
|
347
|
+
if (this.background_layer) {
|
|
348
|
+
this.background_layer.zIndex = -1;
|
|
349
|
+
}
|
|
350
|
+
// Update other layers starting from 0
|
|
351
|
+
this.layers.forEach((layer, index) => {
|
|
352
|
+
layer.container.zIndex = index;
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
move_layer(id, direction) {
|
|
356
|
+
const index = this.layers.findIndex((l) => l.id === id);
|
|
357
|
+
if (index > -1) {
|
|
358
|
+
const new_index = direction === "up" ? index - 1 : index + 1;
|
|
359
|
+
this.layers = this.layers.map((l, i) => {
|
|
360
|
+
if (i === index) {
|
|
361
|
+
return this.layers[new_index];
|
|
362
|
+
}
|
|
363
|
+
if (i === new_index) {
|
|
364
|
+
return this.layers[index];
|
|
365
|
+
}
|
|
366
|
+
return l;
|
|
367
|
+
});
|
|
368
|
+
this.update_layer_order();
|
|
369
|
+
this.layer_store.update((_layers) => ({
|
|
370
|
+
active_layer: id,
|
|
371
|
+
layers: this.layers
|
|
372
|
+
}));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Resizes all layers to a new width and height
|
|
377
|
+
* @param newWidth The new width of the layers
|
|
378
|
+
* @param newHeight The new height of the layers
|
|
379
|
+
* @param scale If true, scales the layer content to fit the new dimensions. If false, keeps the content size unchanged.
|
|
380
|
+
* @param anchor The anchor point to position the content relative to the new size
|
|
381
|
+
*/
|
|
382
|
+
resize_all_layers(newWidth, newHeight, scale, anchor,
|
|
383
|
+
// Add old canvas dimensions for anchor calculations when not scaling
|
|
384
|
+
oldCanvasWidth, oldCanvasHeight) {
|
|
385
|
+
// Create a map of the old layers by ID for reference
|
|
386
|
+
const oldLayersById = new Map(this.layers.map((l) => [l.id, l]));
|
|
387
|
+
const oldBackgroundLayer = this.background_layer;
|
|
388
|
+
// Calculate offset for non-scaling resize based on anchor and dimension change
|
|
389
|
+
const calculateOffset = () => {
|
|
390
|
+
let offsetX = 0;
|
|
391
|
+
let offsetY = 0;
|
|
392
|
+
const deltaWidth = newWidth - oldCanvasWidth;
|
|
393
|
+
const deltaHeight = newHeight - oldCanvasHeight;
|
|
394
|
+
// Determine X offset based on anchor point
|
|
395
|
+
if (anchor.includes("left")) {
|
|
396
|
+
offsetX = 0;
|
|
397
|
+
}
|
|
398
|
+
else if (anchor.includes("right")) {
|
|
399
|
+
offsetX = deltaWidth;
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
// center
|
|
403
|
+
offsetX = Math.floor(deltaWidth / 2);
|
|
404
|
+
}
|
|
405
|
+
// Determine Y offset based on anchor point
|
|
406
|
+
if (anchor.includes("top")) {
|
|
407
|
+
offsetY = 0;
|
|
408
|
+
}
|
|
409
|
+
else if (anchor.includes("bottom")) {
|
|
410
|
+
offsetY = deltaHeight;
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
// center
|
|
414
|
+
offsetY = Math.floor(deltaHeight / 2);
|
|
415
|
+
}
|
|
416
|
+
return { offsetX, offsetY };
|
|
417
|
+
};
|
|
418
|
+
// First, handle the background layer using the helper method
|
|
419
|
+
// Note: create_background_layer called inside the helper might destroy the old one
|
|
420
|
+
this.background_layer = this._resize_background_layer(oldBackgroundLayer, // Pass the potentially null old layer
|
|
421
|
+
newWidth, newHeight, scale, calculateOffset);
|
|
422
|
+
// Now process regular layers
|
|
423
|
+
const processedLayers = [];
|
|
424
|
+
// Store old layer data before clearing the main arrays
|
|
425
|
+
const oldLayerData = this.layers.map((oldLayer) => ({
|
|
426
|
+
id: oldLayer.id,
|
|
427
|
+
name: oldLayer.name,
|
|
428
|
+
user_created: oldLayer.user_created,
|
|
429
|
+
texture: this.draw_textures.get(oldLayer.container),
|
|
430
|
+
container: oldLayer.container
|
|
431
|
+
}));
|
|
432
|
+
// Clear the current layers array and textures map. New layers will be added by the helper.
|
|
433
|
+
this.layers = [];
|
|
434
|
+
this.draw_textures.clear(); // Ensure map is cleared before helpers add to it
|
|
435
|
+
// Rebuild layers in the original order using the helper method
|
|
436
|
+
for (const oldData of oldLayerData) {
|
|
437
|
+
// The helper _resize_single_layer handles creating the new layer,
|
|
438
|
+
// drawing content, cleanup, and adding the new layer to this.layers.
|
|
439
|
+
// It returns null if resizing failed for that layer.
|
|
440
|
+
const newLayer = this._resize_single_layer(oldData, newWidth, newHeight, scale, calculateOffset);
|
|
441
|
+
if (newLayer) {
|
|
442
|
+
processedLayers.push(newLayer);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
// Ensure the active layer still exists, otherwise select the top one
|
|
446
|
+
const currentActiveId = get(this.layer_store).active_layer;
|
|
447
|
+
const activeLayerExists = processedLayers.some((l) => l.id === currentActiveId);
|
|
448
|
+
if (!activeLayerExists && processedLayers.length > 0) {
|
|
449
|
+
this.set_active_layer(processedLayers[0].id); // Set to top layer (index 0 is bottom visually)
|
|
450
|
+
}
|
|
451
|
+
else if (processedLayers.length === 0) {
|
|
452
|
+
this.layer_store.update((s) => ({ ...s, active_layer: "" })); // No layers left
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
// If the active layer still exists, just trigger an update to ensure consistency
|
|
456
|
+
this.layer_store.update((s) => ({ ...s }));
|
|
457
|
+
}
|
|
458
|
+
// Update Z-indices
|
|
459
|
+
this.update_layer_order();
|
|
460
|
+
// Final cleanup attempt - textures might need explicit destroy calls
|
|
461
|
+
// Using setTimeout allows the current execution stack to clear
|
|
462
|
+
setTimeout(() => {
|
|
463
|
+
// Attempt to force GC or resource cleanup if needed, though Pixi often handles this
|
|
464
|
+
Assets.cache.reset(); // Be careful with this, might clear too much
|
|
465
|
+
this.app.renderer.textureGC.run(); // Force texture garbage collection
|
|
466
|
+
}, 100);
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Helper method to resize the background layer.
|
|
470
|
+
*/
|
|
471
|
+
_resize_background_layer(oldBackgroundLayer, newWidth, newHeight, scale, calculateOffset) {
|
|
472
|
+
if (!oldBackgroundLayer) {
|
|
473
|
+
// If no old background layer existed, create a new one
|
|
474
|
+
return this.create_background_layer(newWidth, newHeight);
|
|
475
|
+
}
|
|
476
|
+
// Identify the actual image sprite within the background container
|
|
477
|
+
let backgroundImage = oldBackgroundLayer.children.find((child) => child instanceof Sprite &&
|
|
478
|
+
child.texture !== oldBackgroundLayer.children[0]?.texture);
|
|
479
|
+
// Create the new background layer first (this also destroys the old one implicitly if it exists)
|
|
480
|
+
const newBackgroundLayer = this.create_background_layer(newWidth, newHeight);
|
|
481
|
+
if (backgroundImage) {
|
|
482
|
+
const newBgImage = new Sprite(backgroundImage.texture);
|
|
483
|
+
newBgImage.width = backgroundImage.width; // Preserve original size unless scaling
|
|
484
|
+
newBgImage.height = backgroundImage.height;
|
|
485
|
+
if (scale) {
|
|
486
|
+
newBgImage.width = newWidth;
|
|
487
|
+
newBgImage.height = newHeight;
|
|
488
|
+
newBgImage.position.set(0, 0);
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
const { offsetX, offsetY } = calculateOffset();
|
|
492
|
+
newBgImage.position.set(backgroundImage.x + offsetX, backgroundImage.y + offsetY);
|
|
493
|
+
}
|
|
494
|
+
newBackgroundLayer.addChild(newBgImage);
|
|
495
|
+
}
|
|
496
|
+
// If no background image, the new layer created above is already blank and correctly sized.
|
|
497
|
+
return newBackgroundLayer;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Helper method to resize a single regular layer.
|
|
501
|
+
*/
|
|
502
|
+
_resize_single_layer(oldData, newWidth, newHeight, scale, calculateOffset) {
|
|
503
|
+
if (!oldData.texture) {
|
|
504
|
+
console.warn(`No texture found for layer ${oldData.id}, skipping cleanup.`);
|
|
505
|
+
// Destroy the old container if it exists and is not destroyed
|
|
506
|
+
if (oldData.container && !oldData.container.destroyed) {
|
|
507
|
+
if (this.image_container.children.includes(oldData.container)) {
|
|
508
|
+
this.image_container.removeChild(oldData.container);
|
|
509
|
+
}
|
|
510
|
+
oldData.container.destroy({ children: true });
|
|
511
|
+
}
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
// Create a new layer container and associated render texture
|
|
515
|
+
const newContainer = this.create_layer({
|
|
516
|
+
// This adds the layer to this.layers and draw_textures
|
|
517
|
+
width: newWidth,
|
|
518
|
+
height: newHeight,
|
|
519
|
+
layer_name: oldData.name,
|
|
520
|
+
user_created: oldData.user_created
|
|
521
|
+
});
|
|
522
|
+
// Retrieve the newly created layer data (it's the last one added by create_layer)
|
|
523
|
+
const newLayer = this.layers[this.layers.length - 1];
|
|
524
|
+
newLayer.id = oldData.id; // Restore the original ID
|
|
525
|
+
// Get the render texture for the new layer
|
|
526
|
+
const newTexture = this.draw_textures.get(newContainer);
|
|
527
|
+
if (!newTexture) {
|
|
528
|
+
console.error(`Failed to get texture for newly created layer ${newLayer.id}. Cleaning up.`);
|
|
529
|
+
// Clean up the partially created new layer resources
|
|
530
|
+
if (newContainer && !newContainer.destroyed) {
|
|
531
|
+
if (this.image_container.children.includes(newContainer)) {
|
|
532
|
+
this.image_container.removeChild(newContainer);
|
|
533
|
+
}
|
|
534
|
+
newContainer.destroy({ children: true });
|
|
535
|
+
const idx = this.layers.findIndex((l) => l.container === newContainer);
|
|
536
|
+
if (idx > -1)
|
|
537
|
+
this.layers.splice(idx, 1);
|
|
538
|
+
this.draw_textures.delete(newContainer);
|
|
539
|
+
}
|
|
540
|
+
// Clean up old resources
|
|
541
|
+
if (oldData.texture && !oldData.texture.destroyed)
|
|
542
|
+
oldData.texture.destroy(true);
|
|
543
|
+
if (oldData.container && !oldData.container.destroyed)
|
|
544
|
+
oldData.container.destroy({ children: true });
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
// Explicitly clear the new render texture (create_layer might already do this, but explicit is safer)
|
|
548
|
+
this.app.renderer.clear({ target: newTexture, clearColor: [0, 0, 0, 0] });
|
|
549
|
+
// Create a sprite with the old texture content
|
|
550
|
+
const sprite = new Sprite(oldData.texture);
|
|
551
|
+
if (scale) {
|
|
552
|
+
sprite.width = newWidth;
|
|
553
|
+
sprite.height = newHeight;
|
|
554
|
+
sprite.position.set(0, 0);
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
const { offsetX, offsetY } = calculateOffset();
|
|
558
|
+
sprite.position.set(offsetX, offsetY);
|
|
559
|
+
}
|
|
560
|
+
// Render the sprite onto the new texture
|
|
561
|
+
this.app.renderer.render(sprite, { renderTexture: newTexture });
|
|
562
|
+
// Clean up temporary sprite and old resources
|
|
563
|
+
sprite.destroy();
|
|
564
|
+
if (oldData.texture && !oldData.texture.destroyed) {
|
|
565
|
+
oldData.texture.destroy(true);
|
|
566
|
+
}
|
|
567
|
+
if (oldData.container && !oldData.container.destroyed) {
|
|
568
|
+
if (this.image_container.children.includes(oldData.container)) {
|
|
569
|
+
this.image_container.removeChild(oldData.container);
|
|
570
|
+
}
|
|
571
|
+
oldData.container.destroy({ children: true });
|
|
572
|
+
}
|
|
573
|
+
// Return the data for the successfully resized layer
|
|
574
|
+
return newLayer;
|
|
575
|
+
}
|
|
576
|
+
async get_blobs(width, height) {
|
|
577
|
+
const blobs = {
|
|
578
|
+
background: await get_canvas_blob(this.app.renderer, this.background_layer, {
|
|
579
|
+
width,
|
|
580
|
+
height,
|
|
581
|
+
x: 0,
|
|
582
|
+
y: 0
|
|
583
|
+
}),
|
|
584
|
+
layers: await Promise.all(this.layers.map(async (layer) => {
|
|
585
|
+
const blob = await get_canvas_blob(this.app.renderer, layer.container, {
|
|
586
|
+
width,
|
|
587
|
+
height,
|
|
588
|
+
x: 0,
|
|
589
|
+
y: 0
|
|
590
|
+
});
|
|
591
|
+
if (blob) {
|
|
592
|
+
return blob;
|
|
593
|
+
}
|
|
594
|
+
return null;
|
|
595
|
+
})),
|
|
596
|
+
composite: await get_canvas_blob(this.app.renderer, this.image_container)
|
|
597
|
+
};
|
|
598
|
+
return blobs;
|
|
599
|
+
}
|
|
600
|
+
reset_layers(width, height, persist = false) {
|
|
601
|
+
const _layers_to_recreate = persist
|
|
602
|
+
? this.layers.map((layer) => layer.name)
|
|
603
|
+
: this.layer_options.layers;
|
|
604
|
+
this.layers.forEach((layer) => {
|
|
605
|
+
this.delete_layer(layer.id);
|
|
606
|
+
});
|
|
607
|
+
for (const layer of _layers_to_recreate) {
|
|
608
|
+
this.create_layer({
|
|
609
|
+
width,
|
|
610
|
+
height,
|
|
611
|
+
layer_name: layer,
|
|
612
|
+
user_created: this.layer_options.layers.includes(layer) ? false : true
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
init_layers(width, height) {
|
|
617
|
+
for (const layer of this.layers) {
|
|
618
|
+
this.delete_layer(layer.id);
|
|
619
|
+
}
|
|
620
|
+
for (const layer of this.layer_options.layers) {
|
|
621
|
+
this.create_layer({
|
|
622
|
+
width,
|
|
623
|
+
height,
|
|
624
|
+
layer_name: layer,
|
|
625
|
+
user_created: false
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
this.active_layer = this.layers[0].container;
|
|
629
|
+
this.layer_store.update((_layers) => ({
|
|
630
|
+
active_layer: _layers.layers[0].id,
|
|
631
|
+
layers: this.layers
|
|
632
|
+
}));
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
const core_tools = ["image", "zoom"];
|
|
636
|
+
const core_tool_map = {
|
|
637
|
+
image: () => new ImageTool(),
|
|
638
|
+
zoom: () => new ZoomTool()
|
|
639
|
+
};
|
|
640
|
+
const spring_config = {
|
|
641
|
+
stiffness: 0.45,
|
|
642
|
+
damping: 0.8
|
|
643
|
+
};
|
|
644
|
+
class EditorState {
|
|
645
|
+
state;
|
|
646
|
+
scale;
|
|
647
|
+
position;
|
|
648
|
+
subscribers;
|
|
649
|
+
constructor(editor) {
|
|
650
|
+
if (!(editor instanceof ImageEditor)) {
|
|
651
|
+
throw new Error("EditorState must be created by ImageEditor");
|
|
652
|
+
}
|
|
653
|
+
// Store private data
|
|
654
|
+
this.scale = 1.0;
|
|
655
|
+
this.position = { x: 0, y: 0 };
|
|
656
|
+
this.subscribers = new Set();
|
|
657
|
+
// @ts-ignore
|
|
658
|
+
this.state = Object.freeze({
|
|
659
|
+
get position() {
|
|
660
|
+
return { ...this.position };
|
|
661
|
+
},
|
|
662
|
+
get scale() {
|
|
663
|
+
return this.scale;
|
|
664
|
+
},
|
|
665
|
+
get current_tool() {
|
|
666
|
+
return this.current_tool;
|
|
667
|
+
},
|
|
668
|
+
get current_subtool() {
|
|
669
|
+
return this.current_subtool;
|
|
670
|
+
},
|
|
671
|
+
subscribe: this.subscribe.bind(this)
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
// Internal methods - only accessible to EditorState class
|
|
675
|
+
_set_position(x, y) {
|
|
676
|
+
const oldPosition = { ...this.position };
|
|
677
|
+
this.position = { x, y };
|
|
678
|
+
this._notify_subscribers("position", oldPosition, this.position);
|
|
679
|
+
}
|
|
680
|
+
_set_scale(scale) {
|
|
681
|
+
const oldScale = this.scale;
|
|
682
|
+
this.scale = scale;
|
|
683
|
+
this._notify_subscribers("scale", oldScale, scale);
|
|
684
|
+
}
|
|
685
|
+
_notify_subscribers(property, oldValue, newValue) {
|
|
686
|
+
this.subscribers.forEach((callback) => {
|
|
687
|
+
callback({
|
|
688
|
+
property,
|
|
689
|
+
oldValue,
|
|
690
|
+
newValue,
|
|
691
|
+
timestamp: Date.now()
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
// Public method but only affects notifications
|
|
696
|
+
subscribe(callback) {
|
|
697
|
+
this.subscribers.add(callback);
|
|
698
|
+
return () => this.subscribers.delete(callback);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
export class ImageEditor {
|
|
702
|
+
ready;
|
|
703
|
+
background_image_present = writable(false);
|
|
704
|
+
min_zoom = writable(true);
|
|
705
|
+
app;
|
|
706
|
+
ui_container;
|
|
707
|
+
image_container;
|
|
708
|
+
command_manager;
|
|
709
|
+
layer_manager;
|
|
710
|
+
tools = new Map();
|
|
711
|
+
current_tool;
|
|
712
|
+
current_subtool;
|
|
713
|
+
target_element;
|
|
714
|
+
width;
|
|
715
|
+
height;
|
|
716
|
+
dimensions;
|
|
717
|
+
scale;
|
|
718
|
+
position;
|
|
719
|
+
state;
|
|
720
|
+
scale_value = 1;
|
|
721
|
+
position_value = { x: 0, y: 0 };
|
|
722
|
+
dimensions_value = {
|
|
723
|
+
width: 0,
|
|
724
|
+
height: 0
|
|
725
|
+
};
|
|
726
|
+
layers = writable({
|
|
727
|
+
active_layer: "",
|
|
728
|
+
layers: []
|
|
729
|
+
});
|
|
730
|
+
outline_container;
|
|
731
|
+
outline_graphics;
|
|
732
|
+
background_image;
|
|
733
|
+
ready_resolve;
|
|
734
|
+
event_callbacks = new Map();
|
|
735
|
+
fixed_canvas;
|
|
736
|
+
dark;
|
|
737
|
+
border_region;
|
|
738
|
+
layer_options;
|
|
739
|
+
overlay_container;
|
|
740
|
+
overlay_graphics;
|
|
741
|
+
pad_bottom;
|
|
742
|
+
constructor(options) {
|
|
743
|
+
this.pad_bottom = options.pad_bottom || 0;
|
|
744
|
+
this.dark = options.dark || false;
|
|
745
|
+
this.target_element = options.target_element;
|
|
746
|
+
this.width = options.width;
|
|
747
|
+
this.height = options.height;
|
|
748
|
+
this.command_manager = new CommandManager();
|
|
749
|
+
// this.layers = writable([]);
|
|
750
|
+
this.ready = new Promise((resolve) => {
|
|
751
|
+
this.ready_resolve = resolve;
|
|
752
|
+
});
|
|
753
|
+
this.fixed_canvas = options.fixed_canvas || false;
|
|
754
|
+
this.tools = new Map(options.tools.map((tool) => {
|
|
755
|
+
if (typeof tool === "string") {
|
|
756
|
+
return [tool, core_tool_map[tool]()];
|
|
757
|
+
}
|
|
758
|
+
return [tool.name, tool];
|
|
759
|
+
}));
|
|
760
|
+
for (const tool of this.tools.values()) {
|
|
761
|
+
if (tool?.on) {
|
|
762
|
+
tool.on("change", () => {
|
|
763
|
+
this.notify("change");
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
this.dimensions = spring({ width: this.width, height: this.height }, spring_config);
|
|
768
|
+
this.scale = spring(1, spring_config);
|
|
769
|
+
this.position = spring({ x: 0, y: 0 }, spring_config);
|
|
770
|
+
this.state = new EditorState(this);
|
|
771
|
+
this.border_region = options.border_region || 0;
|
|
772
|
+
this.layer_options = options.layer_options || {
|
|
773
|
+
allow_additional_layers: true,
|
|
774
|
+
layers: ["Layer 1"],
|
|
775
|
+
disabled: false
|
|
776
|
+
};
|
|
777
|
+
this.scale.subscribe((scale) => {
|
|
778
|
+
this.state._set_scale(scale);
|
|
779
|
+
});
|
|
780
|
+
this.position.subscribe((position) => {
|
|
781
|
+
this.state._set_position(position.x, position.y);
|
|
782
|
+
});
|
|
783
|
+
this.init();
|
|
784
|
+
}
|
|
785
|
+
get context() {
|
|
786
|
+
const editor = this;
|
|
787
|
+
return {
|
|
788
|
+
app: this.app,
|
|
789
|
+
ui_container: this.ui_container,
|
|
790
|
+
image_container: this.image_container,
|
|
791
|
+
get background_image() {
|
|
792
|
+
return editor.background_image;
|
|
793
|
+
},
|
|
794
|
+
pad_bottom: this.pad_bottom,
|
|
795
|
+
command_manager: this.command_manager,
|
|
796
|
+
layer_manager: this.layer_manager,
|
|
797
|
+
dimensions: { subscribe: this.dimensions.subscribe },
|
|
798
|
+
scale: { subscribe: this.scale.subscribe },
|
|
799
|
+
position: { subscribe: this.position.subscribe },
|
|
800
|
+
set_image_properties: this.set_image_properties.bind(this),
|
|
801
|
+
execute_command: this.execute_command.bind(this),
|
|
802
|
+
resize_canvas: this.resize_canvas.bind(this),
|
|
803
|
+
reset: this.reset.bind(this),
|
|
804
|
+
set_background_image: this.set_background_image.bind(this)
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
async init() {
|
|
808
|
+
const container_box = this.target_element.getBoundingClientRect();
|
|
809
|
+
new ResizeObserver((entries) => {
|
|
810
|
+
entries.forEach((entry) => {
|
|
811
|
+
this.resize_canvas(entry.contentBoxSize[0].inlineSize, entry.contentBoxSize[0].blockSize);
|
|
812
|
+
});
|
|
813
|
+
}).observe(this.target_element);
|
|
814
|
+
this.app = new Application();
|
|
815
|
+
await this.app.init({
|
|
816
|
+
width: container_box.width,
|
|
817
|
+
height: container_box.height,
|
|
818
|
+
backgroundAlpha: this.dark ? 0 : 1,
|
|
819
|
+
backgroundColor: this.dark ? 0x333333 : 0xffffff,
|
|
820
|
+
resolution: window.devicePixelRatio,
|
|
821
|
+
autoDensity: true,
|
|
822
|
+
antialias: true,
|
|
823
|
+
powerPreference: "high-performance"
|
|
824
|
+
});
|
|
825
|
+
// Set canvas properties
|
|
826
|
+
const canvas = this.app.canvas;
|
|
827
|
+
canvas.style.background = "transparent";
|
|
828
|
+
await this.setup_containers();
|
|
829
|
+
//create background layer
|
|
830
|
+
this.layer_manager.create_background_layer(this.width, this.height);
|
|
831
|
+
// Create initial layer
|
|
832
|
+
this.layer_manager.init_layers(this.width, this.height);
|
|
833
|
+
for (const tool of this.tools.values()) {
|
|
834
|
+
await tool.setup(this.context, this.current_tool, this.current_subtool);
|
|
835
|
+
}
|
|
836
|
+
const zoom = this.tools.get("zoom");
|
|
837
|
+
if (zoom) {
|
|
838
|
+
zoom.min_zoom.subscribe((is_min_zoom) => {
|
|
839
|
+
this.min_zoom.set(is_min_zoom);
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
this.target_element.appendChild(canvas);
|
|
843
|
+
// Ensure parent element is transparent
|
|
844
|
+
this.target_element.style.background = "transparent";
|
|
845
|
+
this.dimensions.subscribe((dimensions) => {
|
|
846
|
+
this.dimensions_value = dimensions;
|
|
847
|
+
this.image_container.width = dimensions.width;
|
|
848
|
+
this.image_container.height = dimensions.height;
|
|
849
|
+
});
|
|
850
|
+
this.scale.subscribe((scale) => {
|
|
851
|
+
this.scale_value = scale;
|
|
852
|
+
});
|
|
853
|
+
this.position.subscribe((position) => {
|
|
854
|
+
this.position_value = position;
|
|
855
|
+
});
|
|
856
|
+
this.app.ticker.add(() => {
|
|
857
|
+
this.image_container.scale.set(this.scale_value);
|
|
858
|
+
this.image_container.position.set(this.position_value.x, this.position_value.y);
|
|
859
|
+
const effective_width = this.dimensions_value.width * this.scale_value;
|
|
860
|
+
const effective_height = this.dimensions_value.height * this.scale_value;
|
|
861
|
+
// Convert image_container's position (in stage space) to outline_container local coordinates.
|
|
862
|
+
const local_x = Math.round(this.image_container.position.x - this.outline_container.position.x);
|
|
863
|
+
const local_y = Math.round(this.image_container.position.y - this.outline_container.position.y);
|
|
864
|
+
// Make sure overlay container follows the same position as outline container
|
|
865
|
+
this.overlay_container.position.set(this.outline_container.position.x, this.outline_container.position.y);
|
|
866
|
+
this.outline_graphics.clear();
|
|
867
|
+
this.outline_graphics
|
|
868
|
+
.rect(local_x, local_y, effective_width, effective_height)
|
|
869
|
+
.fill({
|
|
870
|
+
color: this.dark ? 0x333333 : 0xffffff,
|
|
871
|
+
alpha: 1
|
|
872
|
+
});
|
|
873
|
+
// Draw border region indicator if border_region > 0
|
|
874
|
+
if (this.border_region > 0) {
|
|
875
|
+
const scaled_border = this.border_region * this.scale_value;
|
|
876
|
+
const border_x = local_x + scaled_border - 1;
|
|
877
|
+
const border_y = local_y + scaled_border - 1;
|
|
878
|
+
const border_width = effective_width - scaled_border * 2 + 1;
|
|
879
|
+
const border_height = effective_height - scaled_border * 2 + 1;
|
|
880
|
+
// Clear previous overlay graphics
|
|
881
|
+
this.overlay_graphics.clear();
|
|
882
|
+
// Draw border rectangle using pixelLine for crisp 1px outline
|
|
883
|
+
this.overlay_graphics
|
|
884
|
+
.rect(border_x, border_y, border_width, border_height)
|
|
885
|
+
.stroke({
|
|
886
|
+
color: 0x999999,
|
|
887
|
+
width: 1,
|
|
888
|
+
alpha: 0,
|
|
889
|
+
pixelLine: true
|
|
890
|
+
});
|
|
891
|
+
// Create dashed line effect by drawing small rects along the border
|
|
892
|
+
const dashLength = 5;
|
|
893
|
+
const gapLength = 5;
|
|
894
|
+
const totalLength = dashLength + gapLength;
|
|
895
|
+
const lineColor = 0x999999;
|
|
896
|
+
// Draw dashed horizontal lines (top and bottom)
|
|
897
|
+
for (let x = border_x; x < border_x + border_width; x += totalLength) {
|
|
898
|
+
// Top dash
|
|
899
|
+
this.overlay_graphics
|
|
900
|
+
.rect(x, border_y, Math.min(dashLength, border_x + border_width - x), 1)
|
|
901
|
+
.fill({ color: lineColor, alpha: 0.7 });
|
|
902
|
+
// Bottom dash
|
|
903
|
+
this.overlay_graphics
|
|
904
|
+
.rect(x, border_y + border_height, Math.min(dashLength, border_x + border_width - x), 1)
|
|
905
|
+
.fill({ color: lineColor, alpha: 0.7 });
|
|
906
|
+
}
|
|
907
|
+
// Draw dashed vertical lines (left and right)
|
|
908
|
+
for (let y = border_y; y < border_y + border_height; y += totalLength) {
|
|
909
|
+
// Left dash
|
|
910
|
+
this.overlay_graphics
|
|
911
|
+
.rect(border_x, y, 1, Math.min(dashLength, border_y + border_height - y))
|
|
912
|
+
.fill({ color: lineColor, alpha: 0.7 });
|
|
913
|
+
// Right dash
|
|
914
|
+
this.overlay_graphics
|
|
915
|
+
.rect(border_x + border_width, y, 1, Math.min(dashLength, border_y + border_height - y))
|
|
916
|
+
.fill({ color: lineColor, alpha: 0.7 });
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
else {
|
|
920
|
+
// Clear overlay graphics if no border region
|
|
921
|
+
this.overlay_graphics.clear();
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
this.ready_resolve();
|
|
925
|
+
}
|
|
926
|
+
async setup_containers() {
|
|
927
|
+
this.image_container = new Container({
|
|
928
|
+
eventMode: "static",
|
|
929
|
+
sortableChildren: true
|
|
930
|
+
});
|
|
931
|
+
this.image_container.width = this.width;
|
|
932
|
+
this.image_container.height = this.height;
|
|
933
|
+
this.app.stage.sortableChildren = true;
|
|
934
|
+
this.app.stage.alpha = 1;
|
|
935
|
+
this.app.stage.addChild(this.image_container);
|
|
936
|
+
this.image_container.scale.set(1);
|
|
937
|
+
this.ui_container = new Container({
|
|
938
|
+
eventMode: "static"
|
|
939
|
+
});
|
|
940
|
+
this.app.stage.addChild(this.ui_container);
|
|
941
|
+
this.ui_container.width = this.width;
|
|
942
|
+
this.ui_container.height = this.height;
|
|
943
|
+
this.outline_container = new Container();
|
|
944
|
+
this.outline_container.zIndex = -10;
|
|
945
|
+
this.outline_graphics = new Graphics();
|
|
946
|
+
this.outline_graphics.rect(0, 0, this.width, this.height).fill({
|
|
947
|
+
color: this.dark ? 0x333333 : 0xffffff,
|
|
948
|
+
alpha: 0
|
|
949
|
+
});
|
|
950
|
+
if (!this.dark) {
|
|
951
|
+
const blurFilter = new BlurFilter({
|
|
952
|
+
alpha: 0.1,
|
|
953
|
+
blur: 2,
|
|
954
|
+
color: 0x000000,
|
|
955
|
+
offset: { x: 0, y: 0 },
|
|
956
|
+
quality: 4,
|
|
957
|
+
shadowOnly: false
|
|
958
|
+
});
|
|
959
|
+
this.outline_graphics.filters = [blurFilter];
|
|
960
|
+
}
|
|
961
|
+
this.outline_container.addChild(this.outline_graphics);
|
|
962
|
+
this.app.stage.addChild(this.outline_container);
|
|
963
|
+
this.overlay_container = new Container();
|
|
964
|
+
this.app.stage.addChild(this.overlay_container);
|
|
965
|
+
this.overlay_container.width = this.width;
|
|
966
|
+
this.overlay_container.height = this.height;
|
|
967
|
+
this.overlay_container.zIndex = 999;
|
|
968
|
+
this.overlay_container.eventMode = "static";
|
|
969
|
+
this.overlay_container.interactiveChildren = true;
|
|
970
|
+
this.overlay_graphics = new Graphics();
|
|
971
|
+
this.overlay_container.addChild(this.overlay_graphics);
|
|
972
|
+
this.overlay_graphics.rect(0, 0, this.width, this.height).fill({
|
|
973
|
+
color: 0x000000,
|
|
974
|
+
alpha: 0
|
|
975
|
+
});
|
|
976
|
+
const app_center_x = this.app.screen.width / 2;
|
|
977
|
+
const app_center_y = this.app.screen.height / 2;
|
|
978
|
+
this.image_container.position.set(app_center_x, app_center_y);
|
|
979
|
+
this.outline_container.position.set(app_center_x, app_center_y);
|
|
980
|
+
this.layer_manager = new LayerManager(this.image_container, this.app, this.fixed_canvas, this.dark, this.border_region, this.layer_options);
|
|
981
|
+
this.layers = this.layer_manager.layer_store;
|
|
982
|
+
}
|
|
983
|
+
// private resize_image(width: number, height: number): void {
|
|
984
|
+
// // Implement image resizing logic here
|
|
985
|
+
// this.image_container.width = width;
|
|
986
|
+
// this.image_container.height = height;
|
|
987
|
+
// }
|
|
988
|
+
resize_canvas(width, height) {
|
|
989
|
+
if (this.app.renderer) {
|
|
990
|
+
this.app.renderer.resize(width, height);
|
|
991
|
+
}
|
|
992
|
+
// Instead, update the container position to stay centered
|
|
993
|
+
const app_center_x = width / 2;
|
|
994
|
+
const app_center_y = height / 2;
|
|
995
|
+
if (this.image_container) {
|
|
996
|
+
this.image_container.position.set(app_center_x, app_center_y);
|
|
997
|
+
}
|
|
998
|
+
if (this.outline_container) {
|
|
999
|
+
this.outline_container.position.set(app_center_x, app_center_y);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
reset() {
|
|
1003
|
+
const zoom = this.tools.get("zoom");
|
|
1004
|
+
if (zoom) {
|
|
1005
|
+
zoom.cleanup();
|
|
1006
|
+
zoom.setup(this.context, this.current_tool, this.current_subtool);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
set_image_properties(properties) {
|
|
1010
|
+
let hard = typeof properties.animate !== "undefined" ? !properties.animate : true;
|
|
1011
|
+
if (properties.position) {
|
|
1012
|
+
this.position.set(properties.position, { hard });
|
|
1013
|
+
}
|
|
1014
|
+
if (properties.scale) {
|
|
1015
|
+
this.scale.set(properties.scale, { hard });
|
|
1016
|
+
}
|
|
1017
|
+
if (properties.width && properties.height) {
|
|
1018
|
+
this.width = properties.width;
|
|
1019
|
+
this.height = properties.height;
|
|
1020
|
+
this.dimensions.set({ width: properties.width, height: properties.height }, { hard });
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
execute_command(command) {
|
|
1024
|
+
this.command_manager.execute(command);
|
|
1025
|
+
}
|
|
1026
|
+
undo() {
|
|
1027
|
+
this.command_manager.undo();
|
|
1028
|
+
this.notify("change");
|
|
1029
|
+
}
|
|
1030
|
+
redo() {
|
|
1031
|
+
this.command_manager.redo();
|
|
1032
|
+
this.notify("change");
|
|
1033
|
+
}
|
|
1034
|
+
async add_image({ image, resize = true }) {
|
|
1035
|
+
const image_command = new AddImageCommand(this.context, image, this.fixed_canvas, this.border_region);
|
|
1036
|
+
await image_command.start();
|
|
1037
|
+
this.context.execute_command(image_command);
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Adds an image from a URL as the background layer
|
|
1041
|
+
* @param url The URL of the image to add
|
|
1042
|
+
*/
|
|
1043
|
+
async add_image_from_url(url) {
|
|
1044
|
+
const image_tool = this.tools.get("image");
|
|
1045
|
+
const texture = await Assets.load(url);
|
|
1046
|
+
await image_tool.add_image({
|
|
1047
|
+
image: texture,
|
|
1048
|
+
fixed_canvas: this.fixed_canvas,
|
|
1049
|
+
border_region: this.border_region
|
|
1050
|
+
});
|
|
1051
|
+
// Update resize tool if present
|
|
1052
|
+
const resize_tool = this.tools.get("resize");
|
|
1053
|
+
if (resize_tool && typeof resize_tool.set_border_region === "function") {
|
|
1054
|
+
resize_tool.set_border_region(this.border_region);
|
|
1055
|
+
}
|
|
1056
|
+
this.notify("change");
|
|
1057
|
+
this.notify("input");
|
|
1058
|
+
}
|
|
1059
|
+
set_tool(tool) {
|
|
1060
|
+
this.current_tool = tool;
|
|
1061
|
+
for (const tool of this.tools.values()) {
|
|
1062
|
+
tool.set_tool(this.current_tool, this.current_subtool);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
set_subtool(subtool) {
|
|
1066
|
+
this.current_subtool = subtool;
|
|
1067
|
+
for (const tool of this.tools.values()) {
|
|
1068
|
+
tool.set_tool(this.current_tool, this.current_subtool);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
set_background_image(image) {
|
|
1072
|
+
this.background_image = image;
|
|
1073
|
+
}
|
|
1074
|
+
reset_canvas() {
|
|
1075
|
+
this.layer_manager.reset_layers(this.width, this.height);
|
|
1076
|
+
// Clear background image
|
|
1077
|
+
this.background_image = undefined;
|
|
1078
|
+
this.background_image_present.set(false);
|
|
1079
|
+
// Reset position, scale and dimensions to default values
|
|
1080
|
+
this.set_image_properties({
|
|
1081
|
+
width: this.width,
|
|
1082
|
+
height: this.height,
|
|
1083
|
+
scale: 1,
|
|
1084
|
+
position: { x: 0, y: 0 },
|
|
1085
|
+
animate: false
|
|
1086
|
+
});
|
|
1087
|
+
// Reset command stacks
|
|
1088
|
+
this.command_manager = new CommandManager();
|
|
1089
|
+
//create background layer
|
|
1090
|
+
this.layer_manager.create_background_layer(this.width, this.height);
|
|
1091
|
+
// Reset tools
|
|
1092
|
+
for (const tool of this.tools.values()) {
|
|
1093
|
+
tool.cleanup();
|
|
1094
|
+
tool.setup(this.context, this.current_tool, this.current_subtool);
|
|
1095
|
+
}
|
|
1096
|
+
const zoom_tool = this.tools.get("zoom");
|
|
1097
|
+
if (zoom_tool) {
|
|
1098
|
+
zoom_tool.min_zoom.subscribe((is_min_zoom) => {
|
|
1099
|
+
this.min_zoom.set(is_min_zoom);
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
this.notify("change");
|
|
1103
|
+
}
|
|
1104
|
+
add_layer() {
|
|
1105
|
+
this.layer_manager.create_layer({
|
|
1106
|
+
width: this.width,
|
|
1107
|
+
height: this.height,
|
|
1108
|
+
layer_name: undefined,
|
|
1109
|
+
user_created: true
|
|
1110
|
+
});
|
|
1111
|
+
this.notify("change");
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Adds a new layer with an image loaded from a URL
|
|
1115
|
+
* @param layer_urls The URLs of the images to load
|
|
1116
|
+
* @returns A Promise that resolves when all layers are added
|
|
1117
|
+
*/
|
|
1118
|
+
async add_layers_from_url(layer_urls) {
|
|
1119
|
+
const _layers = this.layer_manager.get_layers();
|
|
1120
|
+
_layers.forEach((l) => this.layer_manager.delete_layer(l.id));
|
|
1121
|
+
if (layer_urls === undefined || layer_urls.length === 0) {
|
|
1122
|
+
this.layer_manager.create_layer({
|
|
1123
|
+
width: this.width,
|
|
1124
|
+
height: this.height,
|
|
1125
|
+
layer_name: undefined,
|
|
1126
|
+
user_created: false
|
|
1127
|
+
});
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
for await (const url of layer_urls) {
|
|
1131
|
+
await this.layer_manager.add_layer_from_url(url);
|
|
1132
|
+
}
|
|
1133
|
+
// Update resize tool if present with border region
|
|
1134
|
+
const resize_tool = this.tools.get("resize");
|
|
1135
|
+
if (resize_tool && typeof resize_tool.set_border_region === "function") {
|
|
1136
|
+
resize_tool.set_border_region(this.border_region);
|
|
1137
|
+
}
|
|
1138
|
+
this.notify("change");
|
|
1139
|
+
this.notify("input");
|
|
1140
|
+
}
|
|
1141
|
+
set_layer(id) {
|
|
1142
|
+
this.layer_manager.set_active_layer(id);
|
|
1143
|
+
this.notify("change");
|
|
1144
|
+
}
|
|
1145
|
+
move_layer(id, direction) {
|
|
1146
|
+
this.layer_manager.move_layer(id, direction);
|
|
1147
|
+
this.notify("change");
|
|
1148
|
+
}
|
|
1149
|
+
delete_layer(id) {
|
|
1150
|
+
this.layer_manager.delete_layer(id);
|
|
1151
|
+
this.notify("change");
|
|
1152
|
+
}
|
|
1153
|
+
modify_canvas_size(width, height, anchor, scale) {
|
|
1154
|
+
// Store old dimensions before updating
|
|
1155
|
+
const oldWidth = this.width;
|
|
1156
|
+
const oldHeight = this.height;
|
|
1157
|
+
// Resize layers first, passing old dimensions for anchor calculation
|
|
1158
|
+
this.layer_manager.resize_all_layers(width, height, scale, anchor, oldWidth, oldHeight);
|
|
1159
|
+
// Update the editor's main dimension tracking *after* resizing layers
|
|
1160
|
+
this.width = width;
|
|
1161
|
+
this.height = height;
|
|
1162
|
+
// Update the dimensions store and reset view scale/position
|
|
1163
|
+
this.set_image_properties({
|
|
1164
|
+
width,
|
|
1165
|
+
height,
|
|
1166
|
+
scale: 1, // Reset scale
|
|
1167
|
+
position: { x: 0, y: 0 }, // Reset pan/position of the image_container
|
|
1168
|
+
animate: false
|
|
1169
|
+
});
|
|
1170
|
+
this.notify("change");
|
|
1171
|
+
}
|
|
1172
|
+
async get_blobs() {
|
|
1173
|
+
const blobs = await this.layer_manager.get_blobs(this.width, this.height);
|
|
1174
|
+
return blobs;
|
|
1175
|
+
}
|
|
1176
|
+
on(event, callback) {
|
|
1177
|
+
this.event_callbacks.set(event, [
|
|
1178
|
+
...(this.event_callbacks.get(event) || []),
|
|
1179
|
+
callback
|
|
1180
|
+
]);
|
|
1181
|
+
}
|
|
1182
|
+
off(event, callback) {
|
|
1183
|
+
this.event_callbacks.set(event, this.event_callbacks.get(event)?.filter((cb) => cb !== callback) || []);
|
|
1184
|
+
}
|
|
1185
|
+
notify(event) {
|
|
1186
|
+
for (const callback of this.event_callbacks.get(event) || []) {
|
|
1187
|
+
callback();
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
destroy() {
|
|
1191
|
+
if (!this.app)
|
|
1192
|
+
return;
|
|
1193
|
+
// for (const tool of this.tools) {
|
|
1194
|
+
// tool[1]?.cleanup();
|
|
1195
|
+
// }
|
|
1196
|
+
this.app?.destroy();
|
|
1197
|
+
}
|
|
1198
|
+
resize(width, height) {
|
|
1199
|
+
this.set_image_properties({
|
|
1200
|
+
width,
|
|
1201
|
+
height
|
|
1202
|
+
});
|
|
1203
|
+
this.reset();
|
|
1204
|
+
}
|
|
1205
|
+
async get_crop_bounds() {
|
|
1206
|
+
const crop_tool = this.tools.get("crop");
|
|
1207
|
+
const crop_bounds = crop_tool.get_crop_bounds();
|
|
1208
|
+
const image = await crop_tool.get_image();
|
|
1209
|
+
return {
|
|
1210
|
+
image,
|
|
1211
|
+
...crop_bounds
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
get background_image_sprite() {
|
|
1215
|
+
return this.background_image;
|
|
1216
|
+
}
|
|
1217
|
+
set_layer_options(layer_options) {
|
|
1218
|
+
this.layer_options = layer_options;
|
|
1219
|
+
this.layer_manager.set_layer_options(layer_options, this.width, this.height);
|
|
1220
|
+
}
|
|
1221
|
+
}
|