@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,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
|
+
}
|