@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,506 @@
1
+ import { Container, type FederatedPointerEvent } from "pixi.js";
2
+ import { type ImageEditorContext, type Tool } from "../core/editor";
3
+ import { type Tool as ToolbarTool, type Subtool } from "../Toolbar.svelte";
4
+ import type { ColorInput } from "tinycolor2";
5
+ import tinycolor from "tinycolor2";
6
+ import { BrushCursor } from "./brush-cursor";
7
+ import { BrushTextures } from "./brush-textures";
8
+ import { recurse_set_cursor } from "./brush-utils";
9
+ import { type BrushState } from "./types";
10
+
11
+ /**
12
+ * BrushTool class implements the Tool interface for drawing and erasing.
13
+ * @class
14
+ * @implements {Tool}
15
+ */
16
+ export class BrushTool implements Tool {
17
+ /**
18
+ * The name of the tool.
19
+ * @type {string}
20
+ */
21
+ name = "brush" as const;
22
+
23
+ /**
24
+ * The context of the image editor.
25
+ * @private
26
+ */
27
+ private image_editor_context!: ImageEditorContext;
28
+
29
+ /**
30
+ * The current tool selected in the toolbar.
31
+ * @private
32
+ */
33
+ private current_tool!: ToolbarTool;
34
+
35
+ /**
36
+ * The current subtool selected in the toolbar.
37
+ * @private
38
+ */
39
+ private current_subtool!: Subtool;
40
+
41
+ /**
42
+ * The state of the brush tool.
43
+ * @private
44
+ */
45
+ private state: BrushState = {
46
+ opacity: 1,
47
+ brush_size: 10,
48
+ color: "#000000",
49
+ mode: "draw"
50
+ };
51
+
52
+ /**
53
+ * Size settings for brush and eraser
54
+ * @private
55
+ */
56
+ private brush_size = 10;
57
+ private eraser_size = 20;
58
+
59
+ /**
60
+ * Drawing state
61
+ * @private
62
+ */
63
+ private is_drawing = false;
64
+ private last_x = 0;
65
+ private last_y = 0;
66
+ private scale = 1;
67
+
68
+ /**
69
+ * Event listeners
70
+ * @private
71
+ */
72
+ private _bound_pointer_down: ((event: FederatedPointerEvent) => void) | null =
73
+ null;
74
+ private _bound_pointer_move: ((event: FederatedPointerEvent) => void) | null =
75
+ null;
76
+ private _bound_pointer_up: (() => void) | null = null;
77
+ private event_callbacks: Map<string, (() => void)[]> = new Map();
78
+ /**
79
+ * Utility modules
80
+ * @private
81
+ */
82
+ private brush_cursor: BrushCursor | null = null;
83
+ private brush_textures: BrushTextures | null = null;
84
+
85
+ /**
86
+ * Sets up the brush tool with the given context, tool, and subtool.
87
+ * @param {ImageEditorContext} context - The image editor context.
88
+ * @param {ToolbarTool} tool - The tool from the toolbar.
89
+ * @param {Subtool} subtool - The subtool from the toolbar.
90
+ * @returns {Promise<void>}
91
+ */
92
+ async setup(
93
+ context: ImageEditorContext,
94
+ tool: ToolbarTool,
95
+ subtool: Subtool
96
+ ): Promise<void> {
97
+ // Store context and tool information
98
+ this.image_editor_context = context;
99
+ this.current_tool = tool;
100
+ this.current_subtool = subtool;
101
+
102
+ // Set mode based on tool
103
+ this.state.mode = tool === "erase" ? "erase" : "draw";
104
+
105
+ // Set size based on mode
106
+ if (this.state.mode === "draw") {
107
+ this.state.brush_size = this.brush_size;
108
+ } else {
109
+ this.state.brush_size = this.eraser_size;
110
+ }
111
+
112
+ // Subscribe to scale changes
113
+ context.scale.subscribe((scale) => {
114
+ this.scale = scale;
115
+ if (this.brush_cursor) {
116
+ this.brush_cursor.update_state(this.state, this.scale);
117
+ }
118
+ });
119
+
120
+ // Initialize brush cursor
121
+ this.brush_cursor = new BrushCursor(
122
+ this.image_editor_context,
123
+ this.state,
124
+ this.scale
125
+ );
126
+
127
+ // Set initial active state
128
+ this.brush_cursor.set_active(tool === "draw" || tool === "erase");
129
+
130
+ // Initialize brush textures
131
+ this.brush_textures = new BrushTextures(
132
+ this.image_editor_context,
133
+ context.app
134
+ );
135
+ this.brush_textures.initialize_textures();
136
+
137
+ // Set up event listeners
138
+ this.setup_event_listeners();
139
+
140
+ // Set cursor style for image container
141
+ this.handle_cursors(tool);
142
+ }
143
+
144
+ /**
145
+ * Handles cursor styles for the brush tool.
146
+ * @param {ToolbarTool} tool - The current tool.
147
+ * @private
148
+ */
149
+ private handle_cursors(tool: ToolbarTool): void {
150
+ // Set cursor style to 'none' for all children of the image container
151
+ recurse_set_cursor(
152
+ this.image_editor_context.image_container.children as Container[],
153
+ "none"
154
+ );
155
+ }
156
+
157
+ /**
158
+ * Sets the current tool and subtool.
159
+ * @param {ToolbarTool} tool - The current tool.
160
+ * @param {Subtool} subtool - The current subtool.
161
+ */
162
+ set_tool(tool: ToolbarTool, subtool: Subtool): void {
163
+ // Update current tool and subtool
164
+ this.current_tool = tool;
165
+ this.current_subtool = subtool;
166
+
167
+ if (this.current_tool !== "erase" && this.current_tool !== "draw") {
168
+ this.commit_pending_changes();
169
+ }
170
+
171
+ // Commit any pending changes
172
+
173
+ // Set brush cursor active state
174
+ if (this.brush_cursor) {
175
+ this.brush_cursor.set_active(tool === "draw" || tool === "erase");
176
+ }
177
+
178
+ // Update mode based on tool
179
+ const new_mode = tool === "erase" ? "erase" : "draw";
180
+ if (tool === "erase" || tool === "draw") {
181
+ this.brush_textures?.initialize_textures();
182
+ }
183
+ if (this.state.mode !== new_mode) {
184
+ this.state.mode = new_mode;
185
+
186
+ // Update brush size based on mode
187
+ if (this.state.mode === "draw") {
188
+ this.state.brush_size = this.brush_size;
189
+ } else {
190
+ this.state.brush_size = this.eraser_size;
191
+ }
192
+
193
+ // Update cursor
194
+ if (this.brush_cursor) {
195
+ this.brush_cursor.update_state(this.state, this.scale);
196
+ }
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Commits any pending changes to the canvas.
202
+ * @private
203
+ */
204
+ private commit_pending_changes(): void {
205
+ if (this.is_drawing) {
206
+ this.on_pointer_up();
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Called when the user presses the mouse button.
212
+ * @param {FederatedPointerEvent} event - The pointer event.
213
+ * @private
214
+ */
215
+ private on_pointer_down(event: FederatedPointerEvent): void {
216
+ // Don't draw if not over image
217
+ if (this.current_tool !== "erase" && this.current_tool !== "draw") {
218
+ return;
219
+ }
220
+
221
+ if (this.brush_cursor && !this.brush_cursor.is_over_image()) return;
222
+
223
+ // Preserve the current canvas state
224
+ if (this.brush_textures) {
225
+ this.brush_textures.preserve_canvas_state();
226
+ }
227
+
228
+ // Get the local position
229
+ const local_pos = this.image_editor_context.image_container.toLocal(
230
+ event.global
231
+ );
232
+
233
+ // Start drawing
234
+ this.is_drawing = true;
235
+ this.last_x = local_pos.x;
236
+ this.last_y = local_pos.y;
237
+
238
+ // Draw a single point
239
+ if (this.brush_textures) {
240
+ this.brush_textures.draw_segment(
241
+ local_pos.x,
242
+ local_pos.y,
243
+ local_pos.x,
244
+ local_pos.y,
245
+ this.state.brush_size,
246
+ this.state.color,
247
+ this.state.opacity,
248
+ this.state.mode
249
+ );
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Called when the user moves the mouse while drawing.
255
+ * @param {FederatedPointerEvent} event - The pointer event.
256
+ * @private
257
+ */
258
+ private on_pointer_move(event: FederatedPointerEvent): void {
259
+ // Update cursor position
260
+ if (this.brush_cursor) {
261
+ this.brush_cursor.update_cursor_position(event);
262
+ }
263
+
264
+ // If not drawing, exit
265
+ if (!this.is_drawing) return;
266
+
267
+ // Get the local position
268
+ const local_pos = this.image_editor_context.image_container.toLocal(
269
+ event.global
270
+ );
271
+
272
+ // Draw line from last position to current position
273
+ if (this.brush_textures) {
274
+ this.brush_textures.draw_segment(
275
+ this.last_x,
276
+ this.last_y,
277
+ local_pos.x,
278
+ local_pos.y,
279
+ this.state.brush_size,
280
+ this.state.color,
281
+ this.state.opacity,
282
+ this.state.mode
283
+ );
284
+ }
285
+
286
+ // Update last position
287
+ this.last_x = local_pos.x;
288
+ this.last_y = local_pos.y;
289
+ }
290
+
291
+ /**
292
+ * Called when the user releases the mouse button.
293
+ * @private
294
+ */
295
+ private on_pointer_up(): void {
296
+ // If not drawing, exit
297
+ if (!this.is_drawing) return;
298
+
299
+ // Stop drawing
300
+ this.is_drawing = false;
301
+
302
+ // Commit the stroke
303
+ if (this.brush_textures) {
304
+ this.brush_textures.commit_stroke();
305
+ }
306
+
307
+ this.notify("change");
308
+ }
309
+
310
+ /**
311
+ * Sets up event listeners for drawing.
312
+ * @private
313
+ */
314
+ private setup_event_listeners(): void {
315
+ // Remove existing listeners if they exist
316
+ this.cleanup_event_listeners();
317
+
318
+ // Create bound event handlers
319
+ this._bound_pointer_down = this.on_pointer_down.bind(this);
320
+ this._bound_pointer_move = this.on_pointer_move.bind(this);
321
+ this._bound_pointer_up = this.on_pointer_up.bind(this);
322
+
323
+ // Set interactive mode for image container
324
+ const image_container = this.image_editor_context.image_container;
325
+ image_container.eventMode = "static";
326
+ image_container.interactiveChildren = true;
327
+
328
+ // Setup for stage
329
+ const stage = this.image_editor_context.app.stage;
330
+ stage.eventMode = "static";
331
+
332
+ // Add pointer events to the stage for global tracking
333
+ stage.on("pointerdown", this._bound_pointer_down);
334
+ stage.on("pointermove", this._bound_pointer_move);
335
+ stage.on("pointerup", this._bound_pointer_up);
336
+ stage.on("pointerupoutside", this._bound_pointer_up);
337
+
338
+ // Set up cursor event listeners
339
+ if (this.brush_cursor) {
340
+ this.brush_cursor.setup_event_listeners();
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Cleans up event listeners.
346
+ * @private
347
+ */
348
+ private cleanup_event_listeners(): void {
349
+ const stage = this.image_editor_context.app.stage;
350
+
351
+ // Remove event listeners if they exist
352
+ if (this._bound_pointer_down) {
353
+ stage.off("pointerdown", this._bound_pointer_down);
354
+ this._bound_pointer_down = null;
355
+ }
356
+
357
+ if (this._bound_pointer_move) {
358
+ stage.off("pointermove", this._bound_pointer_move);
359
+ this._bound_pointer_move = null;
360
+ }
361
+
362
+ if (this._bound_pointer_up) {
363
+ stage.off("pointerup", this._bound_pointer_up);
364
+ stage.off("pointerupoutside", this._bound_pointer_up);
365
+ this._bound_pointer_up = null;
366
+ }
367
+
368
+ // Clean up cursor event listeners
369
+ if (this.brush_cursor) {
370
+ this.brush_cursor.cleanup_event_listeners();
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Sets the brush size.
376
+ * @param {number} size - The new brush size.
377
+ */
378
+ set_brush_size(size: number): void {
379
+ // Set brush size
380
+ this.brush_size = size;
381
+
382
+ // Update state if in draw mode
383
+ if (this.state.mode === "draw") {
384
+ this.state.brush_size = size;
385
+
386
+ // Update cursor
387
+ if (this.brush_cursor) {
388
+ this.brush_cursor.update_state(this.state, this.scale);
389
+ }
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Sets the brush color.
395
+ * @param {string|ColorInput} color - The new brush color.
396
+ */
397
+ set_brush_color(color: string | ColorInput): void {
398
+ // Convert to hex string
399
+ const color_string = tinycolor(color).toHexString();
400
+
401
+ // Set color
402
+ this.state.color = color_string;
403
+
404
+ // Update cursor
405
+ if (this.brush_cursor) {
406
+ this.brush_cursor.update_state(this.state, this.scale);
407
+ }
408
+ }
409
+
410
+ /**
411
+ * Sets the brush opacity.
412
+ * @param {number} opacity - The new brush opacity.
413
+ */
414
+ set_brush_opacity(opacity: number): void {
415
+ // Clamp opacity between 0 and 1
416
+ const clamped_opacity = Math.max(0, Math.min(1, opacity));
417
+
418
+ // Set opacity
419
+ this.state.opacity = clamped_opacity;
420
+
421
+ // Update cursor
422
+ if (this.brush_cursor) {
423
+ this.brush_cursor.update_state(this.state, this.scale);
424
+ }
425
+ }
426
+
427
+ /**
428
+ * Sets the eraser size.
429
+ * @param {number} size - The new eraser size.
430
+ */
431
+ set_eraser_size(size: number): void {
432
+ // Store the new eraser size
433
+ this.eraser_size = size;
434
+
435
+ // Update state if in erase mode
436
+ if (this.state.mode === "erase") {
437
+ this.state.brush_size = size;
438
+
439
+ // Update cursor
440
+ if (this.brush_cursor) {
441
+ this.brush_cursor.update_state(this.state, this.scale);
442
+ }
443
+ }
444
+ }
445
+
446
+ /**
447
+ * Gets the current size based on mode (brush or eraser).
448
+ * @returns {number} The current size.
449
+ */
450
+ get_current_size(): number {
451
+ return this.state.mode === "draw" ? this.brush_size : this.eraser_size;
452
+ }
453
+
454
+ /**
455
+ * Shows or hides the brush preview.
456
+ * @param {boolean} show - Whether to show the preview.
457
+ */
458
+ preview_brush(show: boolean): void {
459
+ if (this.brush_cursor) {
460
+ this.brush_cursor.preview_brush(show);
461
+ }
462
+ }
463
+
464
+ /**
465
+ * Cleans up resources used by the brush tool.
466
+ */
467
+ cleanup(): void {
468
+ // Commit any pending changes
469
+ this.commit_pending_changes();
470
+
471
+ // Clean up event listeners
472
+ this.cleanup_event_listeners();
473
+
474
+ // Clean up brush textures
475
+ if (this.brush_textures) {
476
+ this.brush_textures.cleanup();
477
+ this.brush_textures = null;
478
+ }
479
+
480
+ // Clean up brush cursor
481
+ if (this.brush_cursor) {
482
+ this.brush_cursor.cleanup();
483
+ this.brush_cursor = null;
484
+ }
485
+ }
486
+
487
+ on<T extends string>(event: T, callback: () => void): void {
488
+ this.event_callbacks.set(event, [
489
+ ...(this.event_callbacks.get(event) || []),
490
+ callback
491
+ ]);
492
+ }
493
+
494
+ off<T extends string>(event: T, callback: () => void): void {
495
+ this.event_callbacks.set(
496
+ event,
497
+ this.event_callbacks.get(event)?.filter((cb) => cb !== callback) || []
498
+ );
499
+ }
500
+
501
+ private notify<T extends string>(event: T): void {
502
+ for (const callback of this.event_callbacks.get(event) || []) {
503
+ callback();
504
+ }
505
+ }
506
+ }
@@ -0,0 +1,41 @@
1
+ import { type ColorInput } from "tinycolor2";
2
+
3
+ /**
4
+ * Represents the state of the brush tool.
5
+ * @interface BrushState
6
+ * @property {number} opacity - The opacity of the brush.
7
+ * @property {number} brush_size - The size of the brush.
8
+ * @property {string} color - The color of the brush in hex format.
9
+ * @property {"draw"|"erase"} mode - The mode of the brush, either draw or erase.
10
+ */
11
+ export interface BrushState {
12
+ opacity: number;
13
+ brush_size: number;
14
+ color: string;
15
+ mode: "draw" | "erase";
16
+ }
17
+
18
+ export interface Eraser {
19
+ /**
20
+ * The default size of the eraser.
21
+ */
22
+ default_size: number | "auto";
23
+ }
24
+
25
+ export type ColorTuple = [ColorInput, number]; // [color, opacity]
26
+
27
+ export interface Brush extends Eraser {
28
+ /**
29
+ * The default color of the brush.
30
+ */
31
+ default_color: ColorInput;
32
+ /**
33
+ * The colors to show in the color swatch.
34
+ * Can be either simple color strings or [color, opacity] tuples.
35
+ */
36
+ colors: (ColorInput | ColorTuple)[];
37
+ /**
38
+ * Whether to show _only_ the color swatches specified in `colors`, or to show the color swatches specified in `colors` along with the colorpicker.
39
+ */
40
+ color_mode: "fixed" | "defaults";
41
+ }