@gradio/imageeditor 0.15.5 → 0.16.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 +14 -0
- package/ImageEditor.stories.svelte +0 -1
- package/Index.svelte +3 -3
- package/InteractiveImageEditor.svelte +1 -1
- package/dist/Index.svelte +1 -2
- package/dist/Index.svelte.d.ts +4 -3
- package/dist/InteractiveImageEditor.svelte +1 -1
- package/dist/InteractiveImageEditor.svelte.d.ts +1 -1
- package/dist/shared/ImageEditor.svelte +62 -10
- package/dist/shared/ImageEditor.svelte.d.ts +1 -3
- package/dist/shared/brush/brush-textures.d.ts +42 -9
- package/dist/shared/brush/brush-textures.js +211 -74
- package/dist/shared/brush/brush.js +8 -3
- package/dist/shared/core/commands.d.ts +7 -6
- package/dist/shared/core/commands.js +26 -15
- package/dist/shared/core/editor.d.ts +1 -1
- package/dist/shared/core/editor.js +19 -11
- package/dist/shared/core/layers.d.ts +3 -3
- package/dist/shared/core/layers.js +22 -5
- package/dist/shared/image/image.d.ts +1 -1
- package/dist/shared/image/image.js +4 -1
- package/dist/shared/resize/resize.d.ts +1 -1
- package/dist/shared/resize/resize.js +4 -1
- package/package.json +9 -9
- package/shared/ImageEditor.svelte +85 -17
- package/shared/brush/BrushOptions.svelte +0 -3
- package/shared/brush/ColorPicker.svelte +2 -2
- package/shared/brush/brush-textures.ts +296 -107
- package/shared/brush/brush.ts +14 -3
- package/shared/core/commands.ts +36 -20
- package/shared/core/editor.ts +22 -13
- package/shared/core/layers.ts +29 -5
- package/shared/image/image.ts +5 -1
- package/shared/resize/resize.ts +5 -1
|
@@ -8,29 +8,53 @@ import {
|
|
|
8
8
|
} from "pixi.js";
|
|
9
9
|
import { type ImageEditorContext } from "../core/editor";
|
|
10
10
|
import { type Command } from "../core/commands";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Represents a single drawing segment with all its parameters
|
|
14
|
+
*/
|
|
15
|
+
interface BrushSegment {
|
|
16
|
+
from_x: number;
|
|
17
|
+
from_y: number;
|
|
18
|
+
to_x: number;
|
|
19
|
+
to_y: number;
|
|
20
|
+
size: number;
|
|
21
|
+
color: string;
|
|
22
|
+
opacity: number;
|
|
23
|
+
mode: "draw" | "erase";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Represents a complete brush stroke containing multiple segments
|
|
28
|
+
*/
|
|
29
|
+
interface BrushStroke {
|
|
30
|
+
segments: BrushSegment[];
|
|
31
|
+
layer_id: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
11
34
|
export class BrushCommand implements Command {
|
|
12
|
-
private
|
|
13
|
-
private
|
|
14
|
-
private
|
|
35
|
+
private stroke_data: BrushStroke;
|
|
36
|
+
private context: ImageEditorContext;
|
|
37
|
+
private original_texture: Texture | null = null;
|
|
38
|
+
|
|
15
39
|
name: string;
|
|
16
40
|
|
|
17
41
|
constructor(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
original_texture
|
|
21
|
-
final_texture: Texture
|
|
42
|
+
context: ImageEditorContext,
|
|
43
|
+
stroke_data: BrushStroke,
|
|
44
|
+
original_texture?: Texture
|
|
22
45
|
) {
|
|
23
46
|
this.name = "Draw";
|
|
24
|
-
this.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
47
|
+
this.stroke_data = stroke_data;
|
|
48
|
+
this.context = context;
|
|
49
|
+
if (original_texture) {
|
|
50
|
+
this.original_texture = this.create_texture_from(original_texture);
|
|
51
|
+
}
|
|
28
52
|
}
|
|
29
53
|
|
|
30
54
|
/**
|
|
31
55
|
* Creates a new texture with the same content as the source texture
|
|
32
56
|
*/
|
|
33
|
-
private
|
|
57
|
+
private create_texture_from(source: Texture): Texture {
|
|
34
58
|
const texture = RenderTexture.create({
|
|
35
59
|
width: source.width,
|
|
36
60
|
height: source.height,
|
|
@@ -49,26 +73,226 @@ export class BrushCommand implements Command {
|
|
|
49
73
|
return texture;
|
|
50
74
|
}
|
|
51
75
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Recreates the stroke by rendering all segments from the stored parameters
|
|
78
|
+
* Preserves the exact order of draw/erase operations
|
|
79
|
+
*/
|
|
80
|
+
private render_stroke_from_data(
|
|
81
|
+
stroke_data: BrushStroke,
|
|
82
|
+
target_texture: RenderTexture
|
|
83
|
+
): void {
|
|
84
|
+
const draw_segments = stroke_data.segments.filter((s) => s.mode === "draw");
|
|
85
|
+
const erase_segments = stroke_data.segments.filter(
|
|
86
|
+
(s) => s.mode === "erase"
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (draw_segments.length > 0) {
|
|
90
|
+
const graphics = new Graphics();
|
|
91
|
+
const container = new Container();
|
|
92
|
+
container.addChild(graphics);
|
|
93
|
+
let alpha = 1;
|
|
94
|
+
|
|
95
|
+
for (const segment of draw_segments) {
|
|
96
|
+
if (segment.opacity < alpha) {
|
|
97
|
+
alpha = segment.opacity;
|
|
98
|
+
}
|
|
99
|
+
let colorValue = 0xffffff;
|
|
100
|
+
if (segment.color.startsWith("#")) {
|
|
101
|
+
colorValue = parseInt(segment.color.replace("#", "0x"), 16);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
graphics.setFillStyle({
|
|
105
|
+
color: colorValue,
|
|
106
|
+
alpha: 1
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
this.render_segment_to_graphics(graphics, segment);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// we need a sprite in order to set the alpha and for that we need a texture
|
|
113
|
+
// i'm not entirely sure why other approaches didn't work
|
|
114
|
+
const alpha_sprite_texture = RenderTexture.create({
|
|
115
|
+
width: target_texture.width,
|
|
116
|
+
height: target_texture.height,
|
|
117
|
+
resolution: window.devicePixelRatio || 1
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const alpha_sprite = new Sprite(alpha_sprite_texture);
|
|
121
|
+
this.context.app.renderer.render({
|
|
122
|
+
container: container,
|
|
123
|
+
target: alpha_sprite_texture
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
alpha_sprite.alpha = alpha;
|
|
127
|
+
|
|
128
|
+
this.context.app.renderer.render({
|
|
129
|
+
container: alpha_sprite,
|
|
130
|
+
target: target_texture,
|
|
131
|
+
clear: false
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
container.destroy({ children: true });
|
|
135
|
+
alpha_sprite.destroy();
|
|
136
|
+
alpha_sprite_texture.destroy();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (erase_segments.length > 0) {
|
|
140
|
+
// create a temp texture to work with
|
|
141
|
+
const temp_content_texture = RenderTexture.create({
|
|
142
|
+
width: target_texture.width,
|
|
143
|
+
height: target_texture.height,
|
|
144
|
+
resolution: window.devicePixelRatio || 1
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const copy_sprite = new Sprite(target_texture);
|
|
148
|
+
const copy_container = new Container();
|
|
149
|
+
copy_container.addChild(copy_sprite);
|
|
150
|
+
|
|
151
|
+
this.context.app.renderer.render({
|
|
152
|
+
container: copy_container,
|
|
153
|
+
target: temp_content_texture,
|
|
154
|
+
clear: true
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// create a graphics object to draw the erase segments
|
|
158
|
+
const erase_graphics = new Graphics();
|
|
159
|
+
const erase_container = new Container();
|
|
160
|
+
erase_container.addChild(erase_graphics);
|
|
161
|
+
|
|
162
|
+
erase_graphics.setFillStyle({
|
|
163
|
+
color: 0xffffff,
|
|
164
|
+
alpha: 1.0
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
for (const segment of erase_segments) {
|
|
168
|
+
this.render_segment_to_graphics(erase_graphics, segment);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// create a separate texture to hold the mask
|
|
172
|
+
const mask_texture = RenderTexture.create({
|
|
173
|
+
width: target_texture.width,
|
|
174
|
+
height: target_texture.height,
|
|
175
|
+
resolution: window.devicePixelRatio || 1
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
this.context.app.renderer.render({
|
|
179
|
+
container: erase_container,
|
|
180
|
+
target: mask_texture,
|
|
181
|
+
clear: true
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const content_sprite = new Sprite(temp_content_texture);
|
|
185
|
+
const mask_sprite = new Sprite(mask_texture);
|
|
186
|
+
|
|
187
|
+
// only now do we create a sprite from the original texture and add the mask
|
|
188
|
+
const masked_container = new Container();
|
|
189
|
+
masked_container.addChild(content_sprite);
|
|
190
|
+
masked_container.setMask({ mask: mask_sprite, inverse: true }); // inverse mask = erase
|
|
191
|
+
|
|
192
|
+
// now we can render the masked content back to the target texture
|
|
193
|
+
this.context.app.renderer.render({
|
|
194
|
+
container: masked_container,
|
|
195
|
+
target: target_texture,
|
|
196
|
+
clear: true
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
copy_container.destroy({ children: true });
|
|
200
|
+
erase_container.destroy({ children: true });
|
|
201
|
+
masked_container.destroy({ children: true });
|
|
202
|
+
temp_content_texture.destroy();
|
|
203
|
+
mask_texture.destroy();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Renders a segment to a graphics object (extracted from renderSegment)
|
|
209
|
+
*/
|
|
210
|
+
private render_segment_to_graphics(
|
|
211
|
+
graphics: Graphics,
|
|
212
|
+
segment: BrushSegment
|
|
213
|
+
): void {
|
|
214
|
+
const distance = Math.sqrt(
|
|
215
|
+
Math.pow(segment.to_x - segment.from_x, 2) +
|
|
216
|
+
Math.pow(segment.to_y - segment.from_y, 2)
|
|
55
217
|
);
|
|
218
|
+
|
|
219
|
+
if (distance < 0.1) {
|
|
220
|
+
graphics.circle(segment.from_x, segment.from_y, segment.size).fill();
|
|
221
|
+
} else {
|
|
222
|
+
const spacing = Math.max(segment.size / 3, 2);
|
|
223
|
+
const steps = Math.max(Math.ceil(distance / spacing), 2);
|
|
224
|
+
|
|
225
|
+
for (let i = 0; i < steps; i++) {
|
|
226
|
+
const t = i / (steps - 1);
|
|
227
|
+
const x = segment.from_x + (segment.to_x - segment.from_x) * t;
|
|
228
|
+
const y = segment.from_y + (segment.to_y - segment.from_y) * t;
|
|
229
|
+
|
|
230
|
+
graphics.circle(x, y, segment.size).fill();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async execute(context: ImageEditorContext): Promise<void> {
|
|
236
|
+
if (context) {
|
|
237
|
+
this.context = context;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let layer_textures = this.context.layer_manager.get_layer_textures(
|
|
241
|
+
this.stroke_data.layer_id
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
if (!layer_textures) {
|
|
245
|
+
const all_layers = this.context.layer_manager.get_layers();
|
|
246
|
+
const top_layer = all_layers[all_layers.length - 1];
|
|
247
|
+
layer_textures = this.context.layer_manager.get_layer_textures(
|
|
248
|
+
top_layer.id
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
56
252
|
if (!layer_textures) return;
|
|
57
253
|
|
|
58
|
-
|
|
254
|
+
// create a temporary texture to render the stroke
|
|
255
|
+
const temp_texture = RenderTexture.create({
|
|
256
|
+
width: layer_textures.draw.width,
|
|
257
|
+
height: layer_textures.draw.height,
|
|
258
|
+
resolution: window.devicePixelRatio || 1
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// copy current layer content to temp texture
|
|
262
|
+
const current_sprite = new Sprite(layer_textures.draw);
|
|
59
263
|
const temp_container = new Container();
|
|
60
|
-
temp_container.addChild(
|
|
264
|
+
temp_container.addChild(current_sprite);
|
|
61
265
|
|
|
62
|
-
this.context.app.renderer.render(
|
|
63
|
-
|
|
266
|
+
this.context.app.renderer.render({
|
|
267
|
+
container: temp_container,
|
|
268
|
+
target: temp_texture,
|
|
269
|
+
clear: true
|
|
64
270
|
});
|
|
65
271
|
|
|
66
272
|
temp_container.destroy({ children: true });
|
|
273
|
+
|
|
274
|
+
this.render_stroke_from_data(this.stroke_data, temp_texture);
|
|
275
|
+
|
|
276
|
+
// copy final result back to layer texture
|
|
277
|
+
const final_sprite = new Sprite(temp_texture);
|
|
278
|
+
const final_container = new Container();
|
|
279
|
+
final_container.addChild(final_sprite);
|
|
280
|
+
|
|
281
|
+
this.context.app.renderer.render({
|
|
282
|
+
container: final_container,
|
|
283
|
+
target: layer_textures.draw,
|
|
284
|
+
clear: true
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
final_container.destroy({ children: true });
|
|
288
|
+
temp_texture.destroy();
|
|
67
289
|
}
|
|
68
290
|
|
|
69
291
|
async undo(): Promise<void> {
|
|
292
|
+
if (!this.original_texture) return;
|
|
293
|
+
|
|
70
294
|
const layer_textures = this.context.layer_manager.get_layer_textures(
|
|
71
|
-
this.layer_id
|
|
295
|
+
this.stroke_data.layer_id
|
|
72
296
|
);
|
|
73
297
|
if (!layer_textures) return;
|
|
74
298
|
|
|
@@ -76,8 +300,10 @@ export class BrushCommand implements Command {
|
|
|
76
300
|
const temp_container = new Container();
|
|
77
301
|
temp_container.addChild(temp_sprite);
|
|
78
302
|
|
|
79
|
-
this.context.app.renderer.render(
|
|
80
|
-
|
|
303
|
+
this.context.app.renderer.render({
|
|
304
|
+
container: temp_container,
|
|
305
|
+
target: layer_textures.draw,
|
|
306
|
+
clear: true
|
|
81
307
|
});
|
|
82
308
|
|
|
83
309
|
temp_container.destroy({ children: true });
|
|
@@ -97,20 +323,19 @@ export class BrushTextures {
|
|
|
97
323
|
private preview_sprite: Sprite | null = null;
|
|
98
324
|
private erase_graphics: Graphics | null = null;
|
|
99
325
|
private dimensions: { width: number; height: number };
|
|
100
|
-
|
|
326
|
+
private image_editor_context: ImageEditorContext;
|
|
327
|
+
private app: Application;
|
|
101
328
|
private is_new_stroke = true;
|
|
102
329
|
|
|
103
330
|
private current_opacity = 1.0;
|
|
104
|
-
|
|
105
|
-
private current_mode: "draw" | "erase" = "draw";
|
|
106
|
-
|
|
107
331
|
private original_layer_texture: Texture | null = null;
|
|
108
332
|
private active_layer_id: string | null = null;
|
|
333
|
+
private current_stroke_segments: BrushSegment[] = [];
|
|
334
|
+
|
|
335
|
+
constructor(image_editor_context: ImageEditorContext, app: Application) {
|
|
336
|
+
this.image_editor_context = image_editor_context;
|
|
337
|
+
this.app = app;
|
|
109
338
|
|
|
110
|
-
constructor(
|
|
111
|
-
private image_editor_context: ImageEditorContext,
|
|
112
|
-
private app: Application
|
|
113
|
-
) {
|
|
114
339
|
this.dimensions = {
|
|
115
340
|
width: this.image_editor_context.image_container.width,
|
|
116
341
|
height: this.image_editor_context.image_container.height
|
|
@@ -145,15 +370,7 @@ export class BrushTextures {
|
|
|
145
370
|
|
|
146
371
|
this.display_container = new Container();
|
|
147
372
|
|
|
148
|
-
|
|
149
|
-
this.image_editor_context.layer_manager.get_active_layer();
|
|
150
|
-
if (active_layer) {
|
|
151
|
-
active_layer.addChild(this.display_container);
|
|
152
|
-
} else {
|
|
153
|
-
this.image_editor_context.image_container.addChild(
|
|
154
|
-
this.display_container
|
|
155
|
-
);
|
|
156
|
-
}
|
|
373
|
+
this.image_editor_context.image_container.addChild(this.display_container);
|
|
157
374
|
|
|
158
375
|
this.stroke_container = new Container();
|
|
159
376
|
this.stroke_graphics = new Graphics();
|
|
@@ -274,13 +491,10 @@ export class BrushTextures {
|
|
|
274
491
|
if (!this.erase_graphics || !this.erase_texture) return;
|
|
275
492
|
|
|
276
493
|
this.erase_graphics.clear();
|
|
277
|
-
this.erase_graphics.
|
|
278
|
-
this.erase_graphics
|
|
279
|
-
0,
|
|
280
|
-
|
|
281
|
-
this.dimensions.width,
|
|
282
|
-
this.dimensions.height
|
|
283
|
-
);
|
|
494
|
+
this.erase_graphics.setFillStyle({ color: 0xffffff, alpha: 1.0 });
|
|
495
|
+
this.erase_graphics
|
|
496
|
+
.rect(0, 0, this.dimensions.width, this.dimensions.height)
|
|
497
|
+
.fill();
|
|
284
498
|
this.erase_graphics.endFill();
|
|
285
499
|
|
|
286
500
|
this.app.renderer.render(this.erase_graphics, {
|
|
@@ -318,63 +532,15 @@ export class BrushTextures {
|
|
|
318
532
|
this.image_editor_context.layer_manager.get_layer_textures(layer.id);
|
|
319
533
|
if (!layer_textures) return;
|
|
320
534
|
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const base_sprite = new Sprite(layer_textures.draw);
|
|
327
|
-
temp_container.addChild(base_sprite);
|
|
328
|
-
|
|
329
|
-
const stroke_sprite = new Sprite(this.stroke_texture);
|
|
330
|
-
stroke_sprite.alpha = currentOpacity;
|
|
331
|
-
temp_container.addChild(stroke_sprite);
|
|
332
|
-
|
|
333
|
-
this.app.renderer.render(temp_container, {
|
|
334
|
-
renderTexture: layer_textures.draw
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
temp_container.destroy({ children: true });
|
|
338
|
-
} else {
|
|
339
|
-
if (!this.stroke_texture) return;
|
|
340
|
-
|
|
341
|
-
const erase_container = new Container();
|
|
342
|
-
|
|
343
|
-
const content_sprite = new Sprite(layer_textures.draw);
|
|
344
|
-
erase_container.addChild(content_sprite);
|
|
345
|
-
|
|
346
|
-
const mask_sprite = new Sprite(this.stroke_texture);
|
|
347
|
-
|
|
348
|
-
erase_container.setMask({ mask: mask_sprite, inverse: true });
|
|
349
|
-
|
|
350
|
-
this.app.renderer.render(erase_container, {
|
|
351
|
-
renderTexture: layer_textures.draw
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
erase_container.destroy({ children: true });
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const final_texture = RenderTexture.create({
|
|
358
|
-
width: this.dimensions.width,
|
|
359
|
-
height: this.dimensions.height,
|
|
360
|
-
resolution: window.devicePixelRatio || 1
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
const final_container = new Container();
|
|
364
|
-
const final_sprite = new Sprite(layer_textures.draw);
|
|
365
|
-
final_container.addChild(final_sprite);
|
|
366
|
-
|
|
367
|
-
this.app.renderer.render(final_container, {
|
|
368
|
-
renderTexture: final_texture
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
final_container.destroy({ children: true });
|
|
535
|
+
const stroke_data: BrushStroke = {
|
|
536
|
+
segments: [...this.current_stroke_segments],
|
|
537
|
+
layer_id: this.active_layer_id
|
|
538
|
+
};
|
|
372
539
|
|
|
373
540
|
const brush_command = new BrushCommand(
|
|
374
541
|
this.image_editor_context,
|
|
375
|
-
|
|
376
|
-
this.original_layer_texture
|
|
377
|
-
final_texture
|
|
542
|
+
stroke_data,
|
|
543
|
+
this.original_layer_texture
|
|
378
544
|
);
|
|
379
545
|
|
|
380
546
|
if (this.stroke_graphics) {
|
|
@@ -387,19 +553,20 @@ export class BrushTextures {
|
|
|
387
553
|
clear_container.destroy();
|
|
388
554
|
|
|
389
555
|
this.is_new_stroke = true;
|
|
390
|
-
|
|
391
|
-
final_texture.destroy();
|
|
392
|
-
|
|
393
556
|
this.original_layer_texture = null;
|
|
394
557
|
this.active_layer_id = null;
|
|
558
|
+
this.current_stroke_segments = [];
|
|
395
559
|
|
|
396
|
-
this.image_editor_context.command_manager.execute(
|
|
560
|
+
this.image_editor_context.command_manager.execute(
|
|
561
|
+
brush_command,
|
|
562
|
+
this.image_editor_context
|
|
563
|
+
);
|
|
397
564
|
}
|
|
398
565
|
|
|
399
566
|
/**
|
|
400
567
|
* Calculates the distance between two points.
|
|
401
568
|
*/
|
|
402
|
-
private
|
|
569
|
+
private calculate_distance(
|
|
403
570
|
x1: number,
|
|
404
571
|
y1: number,
|
|
405
572
|
x2: number,
|
|
@@ -433,7 +600,6 @@ export class BrushTextures {
|
|
|
433
600
|
this.preserve_canvas_state();
|
|
434
601
|
}
|
|
435
602
|
|
|
436
|
-
this.current_mode = mode;
|
|
437
603
|
this.current_opacity =
|
|
438
604
|
mode === "draw" ? Math.min(Math.max(opacity, 0), 1) : 0.5;
|
|
439
605
|
|
|
@@ -441,6 +607,7 @@ export class BrushTextures {
|
|
|
441
607
|
|
|
442
608
|
if (this.is_new_stroke) {
|
|
443
609
|
this.stroke_graphics.clear();
|
|
610
|
+
this.current_stroke_segments = [];
|
|
444
611
|
|
|
445
612
|
const clear_container = new Container();
|
|
446
613
|
this.app.renderer.render(clear_container, {
|
|
@@ -451,6 +618,17 @@ export class BrushTextures {
|
|
|
451
618
|
this.is_new_stroke = false;
|
|
452
619
|
}
|
|
453
620
|
|
|
621
|
+
this.current_stroke_segments.push({
|
|
622
|
+
from_x,
|
|
623
|
+
from_y,
|
|
624
|
+
to_x,
|
|
625
|
+
to_y,
|
|
626
|
+
size: scaled_size,
|
|
627
|
+
color,
|
|
628
|
+
opacity: this.current_opacity,
|
|
629
|
+
mode
|
|
630
|
+
});
|
|
631
|
+
|
|
454
632
|
if (mode === "draw") {
|
|
455
633
|
let colorValue = 0xffffff;
|
|
456
634
|
try {
|
|
@@ -471,7 +649,7 @@ export class BrushTextures {
|
|
|
471
649
|
});
|
|
472
650
|
}
|
|
473
651
|
|
|
474
|
-
const distance = this.
|
|
652
|
+
const distance = this.calculate_distance(from_x, from_y, to_x, to_y);
|
|
475
653
|
|
|
476
654
|
if (distance < 0.1) {
|
|
477
655
|
this.stroke_graphics.circle(from_x, from_y, scaled_size).fill();
|
|
@@ -559,6 +737,17 @@ export class BrushTextures {
|
|
|
559
737
|
return this.dimensions;
|
|
560
738
|
}
|
|
561
739
|
|
|
740
|
+
/**
|
|
741
|
+
* Checks if textures are properly initialized.
|
|
742
|
+
*/
|
|
743
|
+
get textures_initialized(): boolean {
|
|
744
|
+
return !!(
|
|
745
|
+
this.stroke_texture &&
|
|
746
|
+
this.display_container &&
|
|
747
|
+
this.preview_sprite
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
|
|
562
751
|
/**
|
|
563
752
|
* Cleanup all resources.
|
|
564
753
|
*/
|
package/shared/brush/brush.ts
CHANGED
|
@@ -158,13 +158,21 @@ export class BrushTool implements Tool {
|
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
if (this.brush_cursor) {
|
|
161
|
-
|
|
161
|
+
const should_be_active = tool === "draw" || tool === "erase";
|
|
162
|
+
|
|
163
|
+
this.brush_cursor.set_active(should_be_active);
|
|
162
164
|
}
|
|
163
165
|
|
|
164
166
|
const new_mode = tool === "erase" ? "erase" : "draw";
|
|
165
|
-
|
|
167
|
+
const mode_changed = this.state.mode !== new_mode;
|
|
168
|
+
const needs_brush_tool = tool === "erase" || tool === "draw";
|
|
169
|
+
const textures_initialized =
|
|
170
|
+
this.brush_textures?.textures_initialized ?? false;
|
|
171
|
+
|
|
172
|
+
if (needs_brush_tool && (mode_changed || !textures_initialized)) {
|
|
166
173
|
this.brush_textures?.initialize_textures();
|
|
167
174
|
}
|
|
175
|
+
|
|
168
176
|
if (this.state.mode !== new_mode) {
|
|
169
177
|
this.state.mode = new_mode;
|
|
170
178
|
|
|
@@ -198,6 +206,7 @@ export class BrushTool implements Tool {
|
|
|
198
206
|
private on_pointer_down(event: FederatedPointerEvent): void {
|
|
199
207
|
const current_layer =
|
|
200
208
|
this.image_editor_context.layer_manager.get_active_layer();
|
|
209
|
+
|
|
201
210
|
if (
|
|
202
211
|
!current_layer?.visible ||
|
|
203
212
|
(this.current_tool !== "erase" && this.current_tool !== "draw")
|
|
@@ -205,7 +214,9 @@ export class BrushTool implements Tool {
|
|
|
205
214
|
return;
|
|
206
215
|
}
|
|
207
216
|
|
|
208
|
-
if (this.brush_cursor && !this.brush_cursor.is_over_image())
|
|
217
|
+
if (this.brush_cursor && !this.brush_cursor.is_over_image()) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
209
220
|
|
|
210
221
|
if (this.brush_textures) {
|
|
211
222
|
this.brush_textures.preserve_canvas_state();
|
package/shared/core/commands.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { writable
|
|
1
|
+
import { writable } from "svelte/store";
|
|
2
|
+
import type { ImageEditorContext } from "./editor";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Base command interface that is added to the command_managers history
|
|
@@ -27,7 +28,7 @@ export interface Command {
|
|
|
27
28
|
* Called by the command manager to execute the command, can act as a no-op if the work has already been done
|
|
28
29
|
* This function must be able to recreate the command if the command is undone and redone (`stop`/`start`/`continue` will not be called again)
|
|
29
30
|
*/
|
|
30
|
-
execute(): any | Promise<any>;
|
|
31
|
+
execute(context?: ImageEditorContext): any | Promise<any>;
|
|
31
32
|
/**
|
|
32
33
|
* Called by the command manager to undo the command
|
|
33
34
|
* This function must be able to undo the work done by the execute function
|
|
@@ -52,6 +53,8 @@ export class CommandNode {
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
push(command: Command): void {
|
|
56
|
+
this.next = null;
|
|
57
|
+
|
|
55
58
|
const node = new CommandNode(command);
|
|
56
59
|
node.previous = this;
|
|
57
60
|
this.next = node;
|
|
@@ -64,7 +67,6 @@ export class CommandNode {
|
|
|
64
67
|
*/
|
|
65
68
|
export class CommandManager {
|
|
66
69
|
history: CommandNode = new CommandNode();
|
|
67
|
-
|
|
68
70
|
current_history = writable(this.history);
|
|
69
71
|
|
|
70
72
|
undo(): void {
|
|
@@ -72,33 +74,48 @@ export class CommandManager {
|
|
|
72
74
|
this.history.command?.undo();
|
|
73
75
|
this.history = this.history.previous;
|
|
74
76
|
|
|
75
|
-
this.current_history.
|
|
77
|
+
this.current_history.update(() => this.history);
|
|
76
78
|
}
|
|
77
79
|
}
|
|
78
|
-
redo(): void {
|
|
80
|
+
redo(context?: ImageEditorContext): void {
|
|
79
81
|
if (this.history.next) {
|
|
80
82
|
this.history = this.history.next;
|
|
81
|
-
this.history.command?.execute();
|
|
83
|
+
this.history.command?.execute(context);
|
|
84
|
+
this.current_history.update(() => this.history);
|
|
82
85
|
}
|
|
83
|
-
|
|
84
|
-
this.current_history.set(this.history);
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
execute(command: Command): void {
|
|
88
|
-
command.execute();
|
|
88
|
+
async execute(command: Command, context: ImageEditorContext): Promise<void> {
|
|
89
|
+
await command.execute(context);
|
|
89
90
|
this.history.push(command);
|
|
90
91
|
this.history = this.history.next!;
|
|
91
92
|
|
|
92
|
-
this.current_history.
|
|
93
|
+
this.current_history.update(() => this.history);
|
|
93
94
|
}
|
|
94
95
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
96
|
+
async wait_for_next_frame(): Promise<void> {
|
|
97
|
+
return new Promise((resolve) => {
|
|
98
|
+
requestAnimationFrame(() => {
|
|
99
|
+
resolve();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async replay(
|
|
105
|
+
full_history: CommandNode,
|
|
106
|
+
context: ImageEditorContext
|
|
107
|
+
): Promise<void> {
|
|
108
|
+
while (full_history.previous) {
|
|
109
|
+
full_history = full_history.previous;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
while (full_history.next) {
|
|
113
|
+
await full_history.next.command!.execute(context);
|
|
114
|
+
full_history = full_history.next;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
this.history = full_history;
|
|
118
|
+
this.current_history.update(() => this.history);
|
|
102
119
|
}
|
|
103
120
|
|
|
104
121
|
contains(command_name: string): boolean {
|
|
@@ -112,7 +129,6 @@ export class CommandManager {
|
|
|
112
129
|
|
|
113
130
|
reset(): void {
|
|
114
131
|
this.history = new CommandNode();
|
|
115
|
-
|
|
116
|
-
this.current_history.set(this.history);
|
|
132
|
+
this.current_history.update(() => this.history);
|
|
117
133
|
}
|
|
118
134
|
}
|