@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.
Files changed (130) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/Example.svelte +1 -1
  3. package/IMAGE_EDITOR_OVERVIEW.md +224 -0
  4. package/ImageEditor.stories.svelte +4 -4
  5. package/Index.svelte +35 -46
  6. package/{shared/InteractiveImageEditor.svelte → InteractiveImageEditor.svelte} +56 -103
  7. package/dist/Example.svelte.d.ts +1 -1
  8. package/dist/Index.svelte +29 -33
  9. package/dist/Index.svelte.d.ts +17 -16
  10. package/dist/{shared/InteractiveImageEditor.svelte → InteractiveImageEditor.svelte} +47 -88
  11. package/dist/{shared/InteractiveImageEditor.svelte.d.ts → InteractiveImageEditor.svelte.d.ts} +13 -15
  12. package/dist/shared/Anchor.svelte +311 -0
  13. package/dist/shared/Anchor.svelte.d.ts +18 -0
  14. package/dist/shared/Controls.svelte +162 -23
  15. package/dist/shared/Controls.svelte.d.ts +7 -2
  16. package/dist/shared/{tools/IconButton.svelte → IconButton.svelte} +19 -3
  17. package/dist/shared/{tools/IconButton.svelte.d.ts → IconButton.svelte.d.ts} +4 -1
  18. package/dist/shared/ImageEditor.svelte +546 -289
  19. package/dist/shared/ImageEditor.svelte.d.ts +51 -56
  20. package/dist/shared/Layers.svelte +233 -0
  21. package/dist/shared/Layers.svelte.d.ts +35 -0
  22. package/dist/shared/Resize.svelte +218 -0
  23. package/dist/shared/Resize.svelte.d.ts +29 -0
  24. package/dist/shared/SecondaryToolbar.svelte +54 -0
  25. package/dist/shared/SecondaryToolbar.svelte.d.ts +35 -0
  26. package/dist/shared/Toolbar.svelte +337 -0
  27. package/dist/shared/Toolbar.svelte.d.ts +42 -0
  28. package/dist/shared/{tools → brush}/BrushOptions.svelte +68 -64
  29. package/dist/shared/{tools → brush}/BrushOptions.svelte.d.ts +9 -9
  30. package/dist/shared/{tools → brush}/BrushSize.svelte +2 -27
  31. package/dist/shared/{tools → brush}/ColorField.svelte +2 -1
  32. package/dist/shared/{tools → brush}/ColorField.svelte.d.ts +1 -0
  33. package/dist/shared/{tools → brush}/ColorPicker.svelte +81 -9
  34. package/dist/shared/{tools → brush}/ColorPicker.svelte.d.ts +1 -0
  35. package/dist/shared/{tools → brush}/ColorSwatch.svelte +78 -38
  36. package/dist/shared/{tools → brush}/ColorSwatch.svelte.d.ts +6 -2
  37. package/dist/shared/brush/brush-cursor.d.ts +101 -0
  38. package/dist/shared/brush/brush-cursor.js +327 -0
  39. package/dist/shared/brush/brush-textures.d.ts +71 -0
  40. package/dist/shared/brush/brush-textures.js +404 -0
  41. package/dist/shared/brush/brush-utils.d.ts +32 -0
  42. package/dist/shared/brush/brush-utils.js +59 -0
  43. package/dist/shared/brush/brush.d.ts +152 -0
  44. package/dist/shared/brush/brush.js +396 -0
  45. package/dist/shared/brush/types.d.ts +37 -0
  46. package/dist/shared/brush/types.js +1 -0
  47. package/dist/shared/core/editor.d.ts +252 -0
  48. package/dist/shared/core/editor.js +1221 -0
  49. package/dist/shared/crop/crop.d.ts +201 -0
  50. package/dist/shared/crop/crop.js +727 -0
  51. package/dist/shared/image/image.d.ts +89 -0
  52. package/dist/shared/image/image.js +185 -0
  53. package/dist/shared/resize/resize.d.ts +270 -0
  54. package/dist/shared/resize/resize.js +1413 -0
  55. package/dist/shared/types.d.ts +12 -0
  56. package/dist/shared/utils/pixi.d.ts +16 -57
  57. package/dist/shared/utils/pixi.js +15 -113
  58. package/dist/shared/zoom/zoom.d.ts +136 -0
  59. package/dist/shared/zoom/zoom.js +548 -0
  60. package/package.json +11 -10
  61. package/shared/Anchor.svelte +384 -0
  62. package/shared/Controls.svelte +194 -27
  63. package/shared/{tools/IconButton.svelte → IconButton.svelte} +21 -4
  64. package/shared/ImageEditor.svelte +682 -349
  65. package/shared/Layers.svelte +249 -0
  66. package/shared/Resize.svelte +239 -0
  67. package/shared/SecondaryToolbar.svelte +61 -0
  68. package/shared/Toolbar.svelte +386 -0
  69. package/shared/brush/BRUSH_TOOL.md +134 -0
  70. package/shared/brush/BrushOptions.svelte +200 -0
  71. package/shared/{tools → brush}/BrushSize.svelte +2 -30
  72. package/shared/{tools → brush}/ColorField.svelte +3 -1
  73. package/shared/{tools → brush}/ColorPicker.svelte +86 -9
  74. package/shared/{tools → brush}/ColorSwatch.svelte +95 -42
  75. package/shared/brush/brush-cursor.ts +408 -0
  76. package/shared/brush/brush-textures.ts +508 -0
  77. package/shared/brush/brush-utils.ts +76 -0
  78. package/shared/brush/brush.ts +506 -0
  79. package/shared/brush/types.ts +41 -0
  80. package/shared/core/EDITOR.md +182 -0
  81. package/shared/core/editor.ts +1730 -0
  82. package/shared/crop/CROP.md +139 -0
  83. package/shared/crop/crop.ts +1027 -0
  84. package/shared/image/IMAGE.md +135 -0
  85. package/shared/image/image.ts +300 -0
  86. package/shared/resize/resize.ts +1894 -0
  87. package/shared/types.ts +14 -0
  88. package/shared/utils/pixi.ts +19 -246
  89. package/shared/zoom/zoom.ts +729 -0
  90. package/dist/shared/layers/Layers.svelte +0 -231
  91. package/dist/shared/layers/Layers.svelte.d.ts +0 -18
  92. package/dist/shared/layers/utils.d.ts +0 -73
  93. package/dist/shared/layers/utils.js +0 -156
  94. package/dist/shared/tools/Brush.svelte +0 -215
  95. package/dist/shared/tools/Brush.svelte.d.ts +0 -29
  96. package/dist/shared/tools/Crop.svelte +0 -155
  97. package/dist/shared/tools/Crop.svelte.d.ts +0 -16
  98. package/dist/shared/tools/Cropper.svelte +0 -271
  99. package/dist/shared/tools/Cropper.svelte.d.ts +0 -41
  100. package/dist/shared/tools/Handle.svelte +0 -260
  101. package/dist/shared/tools/Handle.svelte.d.ts +0 -29
  102. package/dist/shared/tools/Sources.svelte +0 -257
  103. package/dist/shared/tools/Sources.svelte.d.ts +0 -34
  104. package/dist/shared/tools/Tools.svelte +0 -102
  105. package/dist/shared/tools/Tools.svelte.d.ts +0 -48
  106. package/dist/shared/tools/brush.d.ts +0 -62
  107. package/dist/shared/tools/brush.js +0 -133
  108. package/dist/shared/tools/crop.d.ts +0 -15
  109. package/dist/shared/tools/crop.js +0 -154
  110. package/dist/shared/tools/index.d.ts +0 -5
  111. package/dist/shared/tools/index.js +0 -5
  112. package/dist/shared/tools/sources.d.ts +0 -46
  113. package/dist/shared/tools/sources.js +0 -121
  114. package/dist/shared/tools/types.d.ts +0 -1
  115. package/shared/layers/Layers.svelte +0 -250
  116. package/shared/layers/utils.ts +0 -294
  117. package/shared/tools/Brush.svelte +0 -280
  118. package/shared/tools/BrushOptions.svelte +0 -180
  119. package/shared/tools/Crop.svelte +0 -189
  120. package/shared/tools/Cropper.svelte +0 -346
  121. package/shared/tools/Handle.svelte +0 -289
  122. package/shared/tools/Sources.svelte +0 -283
  123. package/shared/tools/Tools.svelte +0 -162
  124. package/shared/tools/brush.ts +0 -247
  125. package/shared/tools/crop.ts +0 -213
  126. package/shared/tools/index.ts +0 -5
  127. package/shared/tools/sources.ts +0 -195
  128. package/shared/tools/types.ts +0 -1
  129. /package/dist/shared/{tools → brush}/BrushSize.svelte.d.ts +0 -0
  130. /package/dist/shared/{tools/types.js → types.js} +0 -0
@@ -0,0 +1,508 @@
1
+ import {
2
+ Container,
3
+ RenderTexture,
4
+ Sprite,
5
+ Graphics,
6
+ Application
7
+ } from "pixi.js";
8
+ import { safe_destroy, safe_remove_child } from "./brush-utils";
9
+ import { type ImageEditorContext } from "../core/editor";
10
+
11
+ /**
12
+ * Class to handle texture operations for the brush tool.
13
+ * Simplified to work directly with layer textures.
14
+ */
15
+ export class BrushTextures {
16
+ // Only keep what we need
17
+ private stroke_texture: RenderTexture | null = null;
18
+ private erase_texture: RenderTexture | null = null;
19
+ private display_container: Container | null = null;
20
+ private stroke_container: Container | null = null;
21
+ private stroke_graphics: Graphics | null = null;
22
+ private preview_sprite: Sprite | null = null;
23
+ private erase_graphics: Graphics | null = null;
24
+ private dimensions: { width: number; height: number };
25
+
26
+ // Track if a new stroke is starting
27
+ private is_new_stroke = true;
28
+
29
+ // Current scale factor
30
+ private scale = 1;
31
+
32
+ // Track the current drawing opacity
33
+ private current_opacity = 1.0;
34
+
35
+ // Track current drawing mode
36
+ private current_mode: "draw" | "erase" = "draw";
37
+
38
+ // Store last drawing parameters for eraser recreation
39
+ private last_from_x: number | null = null;
40
+ private last_from_y: number | null = null;
41
+ private last_to_x: number | null = null;
42
+ private last_to_y: number | null = null;
43
+ private last_size: number | null = null;
44
+
45
+ constructor(
46
+ private image_editor_context: ImageEditorContext,
47
+ private app: Application
48
+ ) {
49
+ this.dimensions = {
50
+ width: this.image_editor_context.image_container.width,
51
+ height: this.image_editor_context.image_container.height
52
+ };
53
+
54
+ // Subscribe to scale changes
55
+ this.image_editor_context.scale.subscribe((scale) => {
56
+ this.scale = scale;
57
+ });
58
+ }
59
+
60
+ /**
61
+ * Initializes textures needed for the brush tool.
62
+ */
63
+ initialize_textures(): void {
64
+ // Clean up any existing resources
65
+ this.cleanup_textures();
66
+
67
+ // Get dimensions from the image container
68
+ const local_bounds =
69
+ this.image_editor_context.image_container.getLocalBounds();
70
+
71
+ this.dimensions = {
72
+ width: local_bounds.width,
73
+ height: local_bounds.height
74
+ };
75
+
76
+ // Create a texture for the current stroke preview
77
+ this.stroke_texture = RenderTexture.create({
78
+ width: this.dimensions.width,
79
+ height: this.dimensions.height,
80
+ resolution: window.devicePixelRatio || 1
81
+ });
82
+
83
+ // Create a texture for erasing
84
+ this.erase_texture = RenderTexture.create({
85
+ width: this.dimensions.width,
86
+ height: this.dimensions.height,
87
+ resolution: window.devicePixelRatio || 1
88
+ });
89
+
90
+ // Create a container to display preview
91
+ this.display_container = new Container();
92
+
93
+ // Get the active layer and add the display container to it
94
+ const active_layer =
95
+ this.image_editor_context.layer_manager.get_active_layer();
96
+ if (active_layer) {
97
+ active_layer.addChild(this.display_container);
98
+ } else {
99
+ this.image_editor_context.image_container.addChild(
100
+ this.display_container
101
+ );
102
+ }
103
+
104
+ // Create container for the stroke graphics
105
+ this.stroke_container = new Container();
106
+ this.stroke_graphics = new Graphics();
107
+ this.stroke_container.addChild(this.stroke_graphics);
108
+
109
+ // Create graphics for eraser mask
110
+ this.erase_graphics = new Graphics();
111
+
112
+ // Create a sprite for the preview of the current stroke
113
+ this.preview_sprite = new Sprite(this.stroke_texture);
114
+ // Start with the preview sprite invisible
115
+ this.preview_sprite.alpha = 0;
116
+ this.display_container.addChild(this.preview_sprite);
117
+
118
+ // Clear the stroke texture
119
+ const clear_container = new Container();
120
+ this.app.renderer.render(clear_container, {
121
+ renderTexture: this.stroke_texture
122
+ });
123
+ this.app.renderer.render(clear_container, {
124
+ renderTexture: this.erase_texture
125
+ });
126
+ clear_container.destroy();
127
+
128
+ // Reset drawing state
129
+ this.is_new_stroke = true;
130
+ this.current_opacity = 1.0;
131
+ }
132
+
133
+ /**
134
+ * Reinitializes textures when needed (e.g., after resizing).
135
+ */
136
+ reinitialize(): void {
137
+ if (
138
+ this.image_editor_context.image_container.width !==
139
+ this.dimensions.width ||
140
+ this.image_editor_context.image_container.height !==
141
+ this.dimensions.height
142
+ ) {
143
+ this.initialize_textures();
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Cleans up texture resources.
149
+ */
150
+ cleanup_textures(): void {
151
+ // Clean up stroke texture
152
+ if (this.stroke_texture) {
153
+ this.stroke_texture.destroy();
154
+ this.stroke_texture = null;
155
+ }
156
+
157
+ // Clean up erase texture
158
+ if (this.erase_texture) {
159
+ this.erase_texture.destroy();
160
+ this.erase_texture = null;
161
+ }
162
+
163
+ // Clean up display container
164
+ if (this.display_container) {
165
+ if (this.display_container.parent) {
166
+ this.display_container.parent.removeChild(this.display_container);
167
+ }
168
+ this.display_container.destroy({ children: true });
169
+ this.display_container = null;
170
+ }
171
+
172
+ // Nullify other references
173
+ this.stroke_container = null;
174
+ this.stroke_graphics = null;
175
+ this.preview_sprite = null;
176
+ this.erase_graphics = null;
177
+ }
178
+
179
+ /**
180
+ * Preserve the canvas state when starting a new stroke.
181
+ */
182
+ preserve_canvas_state(): void {
183
+ // Reset for a new stroke
184
+ this.is_new_stroke = true;
185
+ }
186
+
187
+ /**
188
+ * Resets the eraser mask.
189
+ */
190
+ reset_eraser_mask(): void {
191
+ if (!this.erase_graphics || !this.erase_texture) return;
192
+
193
+ // Clear and reset the eraser graphics
194
+ this.erase_graphics.clear();
195
+ this.erase_graphics.beginFill(0xffffff, 1);
196
+ this.erase_graphics.drawRect(
197
+ 0,
198
+ 0,
199
+ this.dimensions.width,
200
+ this.dimensions.height
201
+ );
202
+ this.erase_graphics.endFill();
203
+
204
+ // Render the eraser graphics to the erase texture
205
+ this.app.renderer.render(this.erase_graphics, {
206
+ renderTexture: this.erase_texture
207
+ });
208
+ }
209
+
210
+ /**
211
+ * Commits the current stroke to the active layer.
212
+ */
213
+ commit_stroke(): void {
214
+ if (!this.stroke_texture || !this.preview_sprite || !this.stroke_graphics)
215
+ return;
216
+
217
+ // Hide the preview sprite
218
+ this.preview_sprite.visible = false;
219
+
220
+ // Get the active layer's texture
221
+ const active_layer =
222
+ this.image_editor_context.layer_manager.get_active_layer();
223
+
224
+ if (!active_layer) return;
225
+
226
+ // Get all layers to find the ID of the active layer
227
+ const layers = this.image_editor_context.layer_manager.get_layers();
228
+ const layer = layers.find((l) => l.container === active_layer);
229
+ if (!layer) return;
230
+
231
+ // Get the active layer's draw texture
232
+ const layer_textures =
233
+ this.image_editor_context.layer_manager.get_layer_textures(layer.id);
234
+ if (!layer_textures) return;
235
+
236
+ // Save the current opacity for use in committing
237
+ const currentOpacity = this.current_opacity;
238
+
239
+ // Handle drawing or erasing
240
+ if (this.current_mode === "draw") {
241
+ // Drawing mode - simple composition
242
+ const temp_container = new Container();
243
+
244
+ // Add current layer texture as base
245
+ const base_sprite = new Sprite(layer_textures.draw);
246
+ temp_container.addChild(base_sprite);
247
+
248
+ // Add the current stroke with proper opacity
249
+ const stroke_sprite = new Sprite(this.stroke_texture);
250
+ stroke_sprite.alpha = currentOpacity;
251
+ temp_container.addChild(stroke_sprite);
252
+
253
+ // Render the composited result directly to the layer texture
254
+ this.app.renderer.render(temp_container, {
255
+ renderTexture: layer_textures.draw
256
+ });
257
+
258
+ // Clean up
259
+ temp_container.destroy({ children: true });
260
+ } else {
261
+ // Erasing mode - use proper masking with setMask
262
+ if (!this.stroke_texture) return;
263
+
264
+ // Create a container for the erase operation
265
+ const erase_container = new Container();
266
+
267
+ // Add the current layer content
268
+ const content_sprite = new Sprite(layer_textures.draw);
269
+ erase_container.addChild(content_sprite);
270
+
271
+ // Create the mask sprite from our stroke texture
272
+ const mask_sprite = new Sprite(this.stroke_texture);
273
+
274
+ // Apply the mask to the container using setMask with inverse: true
275
+ // This will only show content where the mask is NOT (inverse: true)
276
+ erase_container.setMask({ mask: mask_sprite, inverse: true });
277
+
278
+ // Render the masked content back to layer texture
279
+ this.app.renderer.render(erase_container, {
280
+ renderTexture: layer_textures.draw
281
+ });
282
+
283
+ // Clean up
284
+ erase_container.destroy({ children: true });
285
+ }
286
+
287
+ // Clear the stroke graphics and texture for the next stroke
288
+ if (this.stroke_graphics) {
289
+ this.stroke_graphics.clear();
290
+ }
291
+ const clear_container = new Container();
292
+ this.app.renderer.render(clear_container, {
293
+ renderTexture: this.stroke_texture
294
+ });
295
+ clear_container.destroy();
296
+
297
+ // Reset state for the next stroke
298
+ this.is_new_stroke = true;
299
+ }
300
+
301
+ /**
302
+ * Calculates the distance between two points.
303
+ */
304
+ private calculateDistance(
305
+ x1: number,
306
+ y1: number,
307
+ x2: number,
308
+ y2: number
309
+ ): number {
310
+ return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
311
+ }
312
+
313
+ /**
314
+ * Draws a segment for the current stroke using a simple "stamp" approach.
315
+ */
316
+ draw_segment(
317
+ from_x: number,
318
+ from_y: number,
319
+ to_x: number,
320
+ to_y: number,
321
+ size: number,
322
+ color: string,
323
+ opacity: number,
324
+ mode: "draw" | "erase"
325
+ ): void {
326
+ if (
327
+ !this.stroke_graphics ||
328
+ !this.stroke_texture ||
329
+ !this.stroke_container ||
330
+ !this.preview_sprite
331
+ )
332
+ return;
333
+
334
+ // Store the current mode and opacity
335
+ this.current_mode = mode;
336
+ this.current_opacity =
337
+ mode === "draw" ? Math.min(Math.max(opacity, 0), 1) : 0.5;
338
+
339
+ // Store parameters for potential eraser recreation
340
+ this.last_from_x = from_x;
341
+ this.last_from_y = from_y;
342
+ this.last_to_x = to_x;
343
+ this.last_to_y = to_y;
344
+ this.last_size = size;
345
+
346
+ // Apply scale to brush size for consistent rendering
347
+ const scaled_size = size;
348
+
349
+ // For new strokes, clear the graphics and texture completely
350
+ if (this.is_new_stroke) {
351
+ // Clean up for a new stroke
352
+ this.stroke_graphics.clear();
353
+
354
+ // Clear the stroke texture
355
+ const clear_container = new Container();
356
+ this.app.renderer.render(clear_container, {
357
+ renderTexture: this.stroke_texture
358
+ });
359
+ clear_container.destroy();
360
+
361
+ this.is_new_stroke = false;
362
+ }
363
+
364
+ // Set fill color and opacity for drawing
365
+ if (mode === "draw") {
366
+ // For drawing, use the specified color, but always full opacity in the graphics
367
+ // We need to convert the color string to a number
368
+ let colorValue = 0xffffff; // Default to white
369
+ try {
370
+ if (color.startsWith("#")) {
371
+ colorValue = parseInt(color.replace("#", "0x"), 16);
372
+ }
373
+ } catch (e) {
374
+ // Default to white if there's an error
375
+ colorValue = 0xffffff;
376
+ }
377
+ this.stroke_graphics.setFillStyle({
378
+ color: colorValue,
379
+ alpha: 1.0
380
+ });
381
+ } else {
382
+ // For erasing, always use white
383
+ this.stroke_graphics.setFillStyle({
384
+ color: 0xffffff,
385
+ alpha: 1.0
386
+ });
387
+ }
388
+
389
+ // Calculate the distance between the points
390
+ const distance = this.calculateDistance(from_x, from_y, to_x, to_y);
391
+
392
+ // If the points are the same, just draw a single circle
393
+ if (distance < 0.1) {
394
+ // Draw a circle at the point - use scaled_size as RADIUS
395
+ this.stroke_graphics.circle(from_x, from_y, scaled_size).fill();
396
+ } else {
397
+ // Calculate how many circles to draw along the path
398
+ // Adaptive spacing that balances quality and performance
399
+ const spacing = Math.max(scaled_size / 3, 2);
400
+ const steps = Math.max(Math.ceil(distance / spacing), 2);
401
+
402
+ // Draw a circle at each point along the path
403
+ for (let i = 0; i < steps; i++) {
404
+ const t = i / (steps - 1);
405
+ const x = from_x + (to_x - from_x) * t;
406
+ const y = from_y + (to_y - from_y) * t;
407
+
408
+ // Create a circle at this point - use scaled_size as RADIUS
409
+ this.stroke_graphics.circle(x, y, scaled_size).fill();
410
+ }
411
+ }
412
+
413
+ // End the fill after all circles are drawn
414
+ this.stroke_graphics.endFill();
415
+
416
+ // Render the stroke to the texture
417
+ this.app.renderer.render(this.stroke_container, {
418
+ renderTexture: this.stroke_texture
419
+ });
420
+
421
+ // Update the preview based on mode
422
+ if (mode === "draw") {
423
+ // For drawing, show the stroke directly with proper opacity
424
+ this.preview_sprite.texture = this.stroke_texture;
425
+ this.preview_sprite.alpha = this.current_opacity;
426
+
427
+ // Set tint to white (no tint) instead of trying to tint the texture
428
+ this.preview_sprite.tint = 0xffffff;
429
+ } else {
430
+ // For erasing, create a preview that shows what will be erased
431
+
432
+ // Get the active layer's texture for preview
433
+ const active_layer =
434
+ this.image_editor_context.layer_manager.get_active_layer();
435
+ if (!active_layer) return;
436
+
437
+ const layers = this.image_editor_context.layer_manager.get_layers();
438
+ const layer = layers.find((l) => l.container === active_layer);
439
+ if (!layer) return;
440
+
441
+ const layer_textures =
442
+ this.image_editor_context.layer_manager.get_layer_textures(layer.id);
443
+ if (!layer_textures) return;
444
+
445
+ // Create a temporary texture for the preview
446
+ const preview_texture = RenderTexture.create({
447
+ width: this.dimensions.width,
448
+ height: this.dimensions.height,
449
+ resolution: window.devicePixelRatio || 1
450
+ });
451
+
452
+ // First, create a copy of the current layer content
453
+ const base_container = new Container();
454
+ const content_sprite = new Sprite(layer_textures.draw);
455
+ base_container.addChild(content_sprite);
456
+ this.app.renderer.render(base_container, {
457
+ renderTexture: preview_texture
458
+ });
459
+ base_container.destroy({ children: true });
460
+
461
+ // Create a container for the eraser preview with red overlay
462
+ const preview_container = new Container();
463
+
464
+ // Add our current stroke as the content
465
+ const mask_sprite = new Sprite(this.stroke_texture);
466
+
467
+ // Add a red overlay to show what's being erased
468
+ const overlay = new Graphics();
469
+ overlay.setFillStyle({ color: 0xffffff, alpha: 0.5 });
470
+ overlay.rect(0, 0, this.dimensions.width, this.dimensions.height).fill();
471
+
472
+ // Use setMask with inverse: false to only show overlay where the stroke is
473
+ // We want to show the red overlay ONLY where the stroke will erase
474
+ overlay.setMask({ mask: mask_sprite, inverse: false });
475
+
476
+ preview_container.addChild(overlay);
477
+
478
+ // Render the overlay on top of the base content
479
+ this.app.renderer.render(preview_container, {
480
+ renderTexture: preview_texture
481
+ });
482
+
483
+ // Update the preview sprite with our preview texture
484
+ this.preview_sprite.texture = preview_texture;
485
+ this.preview_sprite.alpha = 1.0;
486
+
487
+ // Clean up
488
+ preview_container.destroy({ children: true });
489
+ preview_texture.destroy();
490
+ }
491
+
492
+ this.preview_sprite.visible = true;
493
+ }
494
+
495
+ /**
496
+ * Gets current dimensions of the texture.
497
+ */
498
+ get_dimensions(): { width: number; height: number } {
499
+ return this.dimensions;
500
+ }
501
+
502
+ /**
503
+ * Cleanup all resources.
504
+ */
505
+ cleanup(): void {
506
+ this.cleanup_textures();
507
+ }
508
+ }
@@ -0,0 +1,76 @@
1
+ import { Container, Sprite, Graphics } from "pixi.js";
2
+
3
+ /**
4
+ * Sets the cursor style for a single PIXI object.
5
+ * @param child - The PIXI object to set the cursor for.
6
+ * @param cursor - The cursor style to set.
7
+ */
8
+ export function set_cursor(
9
+ child: Container | Sprite | Graphics,
10
+ cursor: "unset" | "none"
11
+ ): void {
12
+ if (child instanceof Container) {
13
+ child.cursor = cursor;
14
+ } else if ("cursor" in child) {
15
+ (child as any).cursor = cursor;
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Recursively sets the cursor style for PIXI objects.
21
+ * @param children - The PIXI objects to set the cursor for.
22
+ * @param cursor - The cursor style to set.
23
+ */
24
+ export function recurse_set_cursor(
25
+ children: (Container | Sprite | Graphics)[],
26
+ cursor: "unset" | "none"
27
+ ): void {
28
+ for (const child of children) {
29
+ set_cursor(child, cursor);
30
+ if (child instanceof Container && child.children.length > 0) {
31
+ recurse_set_cursor(
32
+ child.children as (Container | Sprite | Graphics)[],
33
+ cursor
34
+ );
35
+ }
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Safely removes a child from its parent.
41
+ * @param child - The child to remove.
42
+ */
43
+ export function safe_remove_child(
44
+ child: Container | Sprite | Graphics | null
45
+ ): void {
46
+ if (child && child.parent) {
47
+ child.parent.removeChild(child);
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Safely destroys a PIXI object and nullifies it.
53
+ * @param obj - The object to destroy.
54
+ * @returns null to assist with nullifying the reference.
55
+ */
56
+ export function safe_destroy<T extends { destroy: (options?: any) => void }>(
57
+ obj: T | null
58
+ ): null {
59
+ if (obj) {
60
+ safe_remove_child(obj as any);
61
+ obj.destroy({ children: true });
62
+ }
63
+ return null;
64
+ }
65
+
66
+ /**
67
+ * Clears a timeout safely and nullifies the reference.
68
+ * @param timeout - The timeout to clear.
69
+ * @returns null to assist with nullifying the reference.
70
+ */
71
+ export function clear_timeout(timeout: number | null): null {
72
+ if (timeout !== null) {
73
+ window.clearTimeout(timeout);
74
+ }
75
+ return null;
76
+ }