@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,386 @@
1
+ <script lang="ts" context="module">
2
+ export type Tool = "image" | "draw" | "erase" | "pan";
3
+ export type Subtool =
4
+ | "upload"
5
+ | "paste"
6
+ | "webcam"
7
+ | "color"
8
+ | "size"
9
+ | "crop"
10
+ | "remove_background"
11
+ | null;
12
+ </script>
13
+
14
+ <script lang="ts">
15
+ import { createEventDispatcher } from "svelte";
16
+ import IconButton from "./IconButton.svelte";
17
+ import {
18
+ Image,
19
+ Brush,
20
+ Erase,
21
+ Crop,
22
+ Upload,
23
+ ImagePaste,
24
+ Webcam,
25
+ Circle,
26
+ Resize,
27
+ ColorPickerSolid
28
+ } from "@gradio/icons";
29
+ import BrushOptions from "./brush/BrushOptions.svelte";
30
+ import type { Source, Transform } from "./types";
31
+ import {
32
+ type Brush as BrushType,
33
+ type Eraser as EraserType,
34
+ type ColorTuple
35
+ } from "./brush/types";
36
+ import tinycolor from "tinycolor2";
37
+
38
+ export let tool: Tool = "image";
39
+ export let subtool: Subtool = null;
40
+
41
+ export let background = false;
42
+ export let brush_options: BrushType | false;
43
+ export let selected_size =
44
+ brush_options && typeof brush_options.default_size === "number"
45
+ ? brush_options.default_size
46
+ : 25;
47
+ export let eraser_options: EraserType;
48
+ export let selected_eraser_size =
49
+ eraser_options && typeof eraser_options.default_size === "number"
50
+ ? eraser_options.default_size
51
+ : 25;
52
+
53
+ // Handle default_color including potential color-opacity tuple
54
+ export let selected_color =
55
+ brush_options &&
56
+ (() => {
57
+ const default_color = brush_options.default_color;
58
+ if (Array.isArray(default_color)) {
59
+ return default_color[0];
60
+ }
61
+ return default_color;
62
+ })();
63
+
64
+ // Set default opacity based on default_color if it's a tuple
65
+ export let selected_opacity =
66
+ brush_options &&
67
+ (() => {
68
+ const default_color = brush_options.default_color;
69
+ if (Array.isArray(default_color)) {
70
+ return default_color[1];
71
+ }
72
+ // Check if color string has opacity
73
+ const color = tinycolor(default_color);
74
+ if (color.getAlpha() < 1) {
75
+ return color.getAlpha();
76
+ }
77
+ return 1;
78
+ })();
79
+
80
+ export let preview = false;
81
+ // export let opacity = 1;
82
+ export let show_brush_color = false;
83
+ export let show_brush_size = false;
84
+ export let show_eraser_size = false;
85
+ export let sources: Source[];
86
+ export let transforms: Transform[];
87
+ // Required by the component interface
88
+ let recent_colors: string[] = [];
89
+
90
+ let enable_layers = true;
91
+ const dispatch = createEventDispatcher<{
92
+ tool_change: { tool: Tool };
93
+ subtool_change: {
94
+ tool: Tool;
95
+ subtool: Subtool;
96
+ };
97
+ }>();
98
+
99
+ /**
100
+ * Handles tool click events
101
+ * @param {Tool} _tool - The selected tool
102
+ */
103
+ function handle_tool_click(e: Event, _tool: Tool): void {
104
+ e.stopPropagation();
105
+ dispatch("tool_change", { tool: _tool });
106
+ }
107
+
108
+ /**
109
+ * Handles subtool click events
110
+ * @param {Event} e - The click event
111
+ * @param {Subtool} _subtool - The selected subtool
112
+ */
113
+ function handle_subtool_click(e: Event, _subtool: typeof subtool): void {
114
+ e.stopPropagation();
115
+
116
+ dispatch("subtool_change", { tool, subtool: _subtool });
117
+ }
118
+
119
+ $: show_brush_size = tool === "draw" && subtool === "size";
120
+ $: show_brush_color = tool === "draw" && subtool === "color";
121
+ $: show_eraser_size = tool === "erase" && subtool === "size";
122
+
123
+ $: can_crop = transforms.includes("crop");
124
+ $: can_resize = transforms.includes("resize");
125
+ $: can_upload = sources.includes("upload");
126
+ $: can_webcam = sources.includes("webcam");
127
+ $: can_paste = sources.includes("clipboard");
128
+ </script>
129
+
130
+ <div class="toolbar-wrap">
131
+ <div class="half-container">
132
+ {#if sources.length > 0}
133
+ <IconButton
134
+ Icon={Image}
135
+ label="Image"
136
+ highlight={tool === "image"}
137
+ on:click={(e) => handle_tool_click(e, "image")}
138
+ size="medium"
139
+ padded={false}
140
+ transparent={true}
141
+ />
142
+ {/if}
143
+ {#if brush_options}
144
+ <IconButton
145
+ Icon={Brush}
146
+ label="Brush"
147
+ on:click={(e) => handle_tool_click(e, "draw")}
148
+ highlight={tool === "draw"}
149
+ size="medium"
150
+ padded={false}
151
+ transparent={true}
152
+ />
153
+ {/if}
154
+ {#if eraser_options}
155
+ <IconButton
156
+ Icon={Erase}
157
+ label="Erase"
158
+ on:click={(e) => handle_tool_click(e, "erase")}
159
+ highlight={tool === "erase"}
160
+ size="medium"
161
+ padded={false}
162
+ transparent={true}
163
+ />
164
+ {/if}
165
+ </div>
166
+ <div
167
+ class="half-container right"
168
+ class:hide={tool === "pan" ||
169
+ (tool === "image" && !background && sources.length === 0) ||
170
+ (tool === "image" && background && transforms.length === 0)}
171
+ >
172
+ {#if tool === "image"}
173
+ {#if background}
174
+ {#if can_crop}
175
+ <IconButton
176
+ Icon={Crop}
177
+ label="Crop"
178
+ on:click={(e) => handle_subtool_click(e, "crop")}
179
+ highlight={subtool === "crop"}
180
+ size="medium"
181
+ padded={false}
182
+ transparent={true}
183
+ offset={0}
184
+ />
185
+ {/if}
186
+ {#if can_resize}
187
+ <IconButton
188
+ Icon={Resize}
189
+ label="Resize"
190
+ on:click={(e) => handle_subtool_click(e, "size")}
191
+ highlight={subtool === "size"}
192
+ size="medium"
193
+ padded={false}
194
+ transparent={true}
195
+ offset={0}
196
+ />
197
+ {/if}
198
+ {:else}
199
+ {#if can_upload}
200
+ <IconButton
201
+ Icon={Upload}
202
+ label="Upload"
203
+ on:click={(e) => handle_subtool_click(e, "upload")}
204
+ highlight={subtool === "upload"}
205
+ size="medium"
206
+ padded={false}
207
+ transparent={true}
208
+ offset={0}
209
+ />
210
+ {/if}
211
+ {#if can_paste}
212
+ <IconButton
213
+ Icon={ImagePaste}
214
+ label="Paste"
215
+ on:click={(e) => handle_subtool_click(e, "paste")}
216
+ highlight={subtool === "paste"}
217
+ size="large"
218
+ padded={false}
219
+ transparent={true}
220
+ offset={0}
221
+ />
222
+ {/if}
223
+ {#if can_webcam}
224
+ <IconButton
225
+ Icon={Webcam}
226
+ label="Webcam"
227
+ on:click={(e) => handle_subtool_click(e, "webcam")}
228
+ highlight={subtool === "webcam"}
229
+ size="medium"
230
+ padded={false}
231
+ transparent={true}
232
+ offset={0}
233
+ />
234
+ {/if}
235
+ {/if}
236
+ {/if}
237
+
238
+ {#if tool === "draw" && brush_options}
239
+ <IconButton
240
+ Icon={ColorPickerSolid}
241
+ label="Color"
242
+ color={selected_color}
243
+ on:click={(e) => handle_subtool_click(e, "color")}
244
+ size="medium"
245
+ padded={false}
246
+ transparent={true}
247
+ offset={0}
248
+ />
249
+ <IconButton
250
+ Icon={Circle}
251
+ label="Brush Size"
252
+ on:click={(e) => handle_subtool_click(e, "size")}
253
+ highlight={subtool === "size"}
254
+ size="medium"
255
+ padded={false}
256
+ transparent={true}
257
+ offset={0}
258
+ />
259
+
260
+ {#if show_brush_color || show_brush_size}
261
+ <BrushOptions
262
+ colors={brush_options.colors}
263
+ color_mode={brush_options.color_mode}
264
+ {recent_colors}
265
+ show_swatch={show_brush_color}
266
+ show_size={show_brush_size}
267
+ bind:selected_size
268
+ bind:selected_color
269
+ bind:selected_opacity
270
+ bind:preview
271
+ on:click_outside={(e) => {
272
+ e.stopPropagation();
273
+ preview = false;
274
+ show_brush_color = false;
275
+ show_brush_size = false;
276
+ handle_subtool_click(e, null);
277
+ }}
278
+ mode="brush"
279
+ />
280
+ {/if}
281
+ {/if}
282
+
283
+ {#if tool === "erase" && eraser_options}
284
+ <IconButton
285
+ Icon={Circle}
286
+ label="Eraser Size"
287
+ on:click={(e) => handle_subtool_click(e, "size")}
288
+ highlight={subtool === "size"}
289
+ size="medium"
290
+ padded={false}
291
+ transparent={true}
292
+ offset={0}
293
+ />
294
+ {/if}
295
+ {#if show_eraser_size}
296
+ <BrushOptions
297
+ colors={[]}
298
+ show_swatch={false}
299
+ show_size={true}
300
+ bind:selected_size={selected_eraser_size}
301
+ bind:selected_color
302
+ bind:selected_opacity
303
+ bind:preview
304
+ on:click_outside={(e) => {
305
+ e.stopPropagation();
306
+ preview = false;
307
+ show_eraser_size = false;
308
+ handle_subtool_click(e, null);
309
+ }}
310
+ mode="eraser"
311
+ />
312
+ {/if}
313
+ </div>
314
+ </div>
315
+
316
+ <style>
317
+ .toolbar-wrap {
318
+ display: inline-flex;
319
+ flex-direction: column;
320
+ justify-content: center;
321
+ align-items: center;
322
+ position: absolute;
323
+ left: 15px;
324
+ right: 0;
325
+ top: 30%;
326
+ /* transform: translateY(-50%); */
327
+ width: 30px;
328
+ border: 1px solid var(--block-border-color);
329
+ border-radius: var(--radius-md);
330
+
331
+ gap: var(--spacing-sm);
332
+ z-index: 1000;
333
+ background-color: var(--block-background-fill);
334
+ }
335
+
336
+ .half-container {
337
+ flex: 1;
338
+ display: flex;
339
+ flex-direction: column;
340
+ gap: var(--spacing-sm);
341
+ position: relative;
342
+ }
343
+
344
+ .half-container {
345
+ width: 100%;
346
+ padding: var(--spacing-sm) 0;
347
+ }
348
+
349
+ .half-container:last-child {
350
+ border-top: 1px solid var(--block-border-color);
351
+ }
352
+
353
+ .hide {
354
+ display: none;
355
+ }
356
+
357
+ /* .toolbar-left,
358
+ .toolbar-right {
359
+ display: flex;
360
+ flex-direction: row;
361
+ align-items: center;
362
+ gap: var(--spacing-sm);
363
+ display: flex;
364
+ z-index: 1000;
365
+ border-radius: var(--radius-sm);
366
+ background-color: #fff;
367
+ padding: var(--spacing-sm) 0.3rem;
368
+ width: auto;
369
+ }
370
+
371
+ .toolbar-right {
372
+ border-left: 0;
373
+ border-top-left-radius: 0;
374
+ border-bottom-left-radius: 0;
375
+ }
376
+
377
+ .toolbar-left {
378
+ border-top-right-radius: 0;
379
+ border-bottom-right-radius: 0;
380
+ }
381
+
382
+ .toolbar-layers {
383
+ border-top-left-radius: 0;
384
+ border-bottom-left-radius: 0;
385
+ } */
386
+ </style>
@@ -0,0 +1,134 @@
1
+ # Brush Tool Documentation
2
+
3
+ ## Overview
4
+
5
+ The Brush Tool is a core component of the image editor that allows users to draw and erase on the canvas. It provides a flexible drawing experience with customizable brush size, color, and opacity. This document explains how the brush tool works and the relationships between its components.
6
+
7
+ ## Key Files
8
+
9
+ - `js/imageeditor/shared/brush/brush.ts`: Main implementation of the brush tool
10
+ - `js/imageeditor/shared/brush/BrushOptions.svelte`: UI controls for brush settings
11
+ - `js/imageeditor/shared/brush/ColorPicker.svelte`: Color selection component
12
+ - `js/imageeditor/shared/brush/ColorSwatch.svelte`: Color swatch component
13
+ - `js/imageeditor/shared/brush/ColorField.svelte`: Color input field component
14
+ - `js/imageeditor/shared/brush/BrushSize.svelte`: Brush size slider component
15
+ - `js/imageeditor/shared/Toolbar.svelte`: Defines tool types and handles tool selection
16
+ - `js/imageeditor/shared/core/editor.ts`: Provides the editor context and tool interface
17
+
18
+ ## Architecture
19
+
20
+ The brush tool follows the Tool interface defined in `editor.ts`. It integrates with the image editor through the `ImageEditorContext` which provides access to the PIXI.js application, containers, and other utilities.
21
+
22
+ ### Class Structure
23
+
24
+ The `BrushTool` class implements the `Tool` interface and provides the following functionality:
25
+
26
+ 1. **Drawing and Erasing**: Handles pointer events to draw or erase on the canvas
27
+ 2. **Brush Customization**: Allows changing brush size, color, and opacity
28
+ 3. **Preview**: Shows a preview of the brush before drawing
29
+ 4. **Cursor**: Displays a custom cursor that reflects the current brush settings
30
+
31
+ ### State Management
32
+
33
+ The brush tool maintains several state variables:
34
+
35
+ - `state`: Contains the current brush settings (opacity, size, color, mode)
36
+ - `brushSize` and `eraserSize`: Separate size settings for drawing and erasing
37
+ - `isDrawing`: Tracks whether the user is currently drawing
38
+ - `isCursorOverImage`: Tracks whether the cursor is over the image container
39
+
40
+ ### Rendering Pipeline
41
+
42
+ The brush tool uses multiple PIXI.js textures and containers to manage the drawing process:
43
+
44
+ 1. `left_texture`: Stores the final result that is displayed to the user
45
+ 2. `right_texture`: Stores the current state before applying new strokes
46
+ 3. `stroke_texture`: Temporarily stores the current stroke being drawn
47
+ 4. `displayContainer`: Contains all visual elements of the brush tool
48
+ 5. `stroke_container`: Contains the graphics for the current stroke
49
+ 6. `erase_graphics`: Used for masking when in erase mode
50
+
51
+ ### Drawing Process
52
+
53
+ 1. **Pointer Down**: Initializes a new stroke, captures the starting position
54
+ 2. **Pointer Move**: Draws line segments between points with interpolation for smooth strokes
55
+ 3. **Pointer Up**: Commits the current stroke to the canvas
56
+ 4. **Commit Stroke**: Merges the temporary stroke with the existing content
57
+
58
+ ### Erasing Process
59
+
60
+ 1. **Pointer Down**: Creates an erase mask at the starting position
61
+ 2. **Pointer Move**: Extends the erase mask along the pointer path
62
+ 3. **Pointer Up**: Applies the erase mask to the canvas
63
+ 4. **Commit Stroke**: Merges the erased content with the existing content
64
+
65
+ ## UI Components
66
+
67
+ ### BrushOptions.svelte
68
+
69
+ Provides UI controls for:
70
+ - Color selection (via color picker or swatches)
71
+ - Brush size adjustment
72
+ - Recent colors management
73
+ - Brush preview
74
+
75
+ ### Brush Preview
76
+
77
+ The brush tool can show a preview of the current brush in the center of the screen, which helps users understand the brush size and color before drawing.
78
+
79
+ ## Event Handling
80
+
81
+ The brush tool sets up the following event listeners:
82
+
83
+ - `pointerdown`: Starts a new stroke
84
+ - `pointermove`: Continues the current stroke and updates the cursor position
85
+ - `pointerup`/`pointerupoutside`: Ends the current stroke
86
+ - Custom events for checking if the cursor is over the image container
87
+
88
+ ## Integration with Editor
89
+
90
+ The brush tool integrates with the editor through:
91
+
92
+ 1. **Tool Interface**: Implements the required methods (setup, cleanup, set_tool)
93
+ 2. **Context Access**: Uses the ImageEditorContext to access the PIXI.js application and containers
94
+ 3. **Tool Switching**: Handles transitions between drawing and erasing modes
95
+
96
+ ## Performance Considerations
97
+
98
+ The brush tool uses several techniques to maintain performance:
99
+
100
+ 1. **Point Interpolation**: Ensures smooth lines even with fast mouse movements
101
+ 2. **Texture Management**: Efficiently manages textures to minimize memory usage
102
+ 3. **Cursor Position Checking**: Uses debouncing to avoid excessive updates
103
+
104
+ ## Customization API
105
+
106
+ The brush tool exposes several methods for customization:
107
+
108
+ - `setBrushSize(size)`: Sets the brush size
109
+ - `setBrushColor(color)`: Sets the brush color
110
+ - `setBrushOpacity(opacity)`: Sets the brush opacity
111
+ - `set_brush_size(size)`: Sets the brush size (only affects drawing mode)
112
+ - `set_eraser_size(size)`: Sets the eraser size (only affects eraser mode)
113
+ - `set_brush_color(color)`: Sets the brush color (only affects drawing mode)
114
+ - `preview_brush(show)`: Shows or hides the brush preview
115
+
116
+ ## Maintenance Notes
117
+
118
+ When modifying the brush tool, consider the following:
119
+
120
+ 1. **Texture Cleanup**: Always clean up textures to prevent memory leaks
121
+ 2. **Event Listener Management**: Properly add and remove event listeners
122
+ 3. **Mode Transitions**: Handle transitions between drawing and erasing modes carefully
123
+ 4. **Scale Handling**: Account for the editor scale when drawing and displaying the cursor
124
+ 5. **Cursor Visibility**: Manage cursor visibility based on the current tool and cursor position
125
+
126
+ ## Future Improvements
127
+
128
+ Potential areas for enhancement:
129
+
130
+ 1. **Brush Types**: Add support for different brush types (e.g., airbrush, pencil)
131
+ 2. **Pressure Sensitivity**: Integrate with pressure-sensitive devices
132
+ 3. **Performance Optimization**: Further optimize for large canvases
133
+ 4. **Layer Support**: Improve integration with the layer system
134
+ 5. **Undo/Redo**: Enhance undo/redo support for brush strokes
@@ -0,0 +1,200 @@
1
+ <script lang="ts">
2
+ import { createEventDispatcher, tick } from "svelte";
3
+ import { click_outside } from "../utils/events";
4
+
5
+ import { type Brush, type ColorTuple } from "./types";
6
+ import ColorPicker from "./ColorPicker.svelte";
7
+ import ColorSwatch from "./ColorSwatch.svelte";
8
+ import ColorField from "./ColorField.svelte";
9
+ import BrushSize from "./BrushSize.svelte";
10
+ import type { ColorInput } from "tinycolor2";
11
+
12
+ export let colors: (ColorInput | ColorTuple)[];
13
+ export let selected_color: any;
14
+ export let color_mode: Brush["color_mode"] | undefined = undefined;
15
+ export let recent_colors: (ColorInput | ColorTuple)[] = [];
16
+ export let selected_size: number;
17
+ export let selected_opacity: number;
18
+ export let show_swatch: boolean;
19
+ export let show_size: boolean;
20
+ export let mode: "brush" | "eraser" = "brush";
21
+
22
+ let color_picker = false;
23
+ let current_mode: "hex" | "rgb" | "hsl" = "hex";
24
+ let editing_index: number | null = null;
25
+
26
+ const dispatch = createEventDispatcher<{
27
+ click_outside: void;
28
+ }>();
29
+
30
+ function handle_color_selection(
31
+ {
32
+ index,
33
+ color,
34
+ opacity
35
+ }: {
36
+ index: number | null;
37
+ color: string | null;
38
+ opacity?: number;
39
+ },
40
+ type: "core" | "user"
41
+ ): void {
42
+ if (type === "user" && !color) {
43
+ editing_index = index;
44
+ color_picker = true;
45
+ }
46
+
47
+ if (!color) return;
48
+ selected_color = color;
49
+
50
+ // Update opacity if provided
51
+ if (opacity !== undefined) {
52
+ selected_opacity = opacity;
53
+ }
54
+
55
+ if (type === "core") {
56
+ color_picker = false;
57
+ }
58
+ }
59
+
60
+ function handle_color_change(color: string): void {
61
+ if (editing_index === null) return;
62
+ recent_colors[editing_index] = color;
63
+ }
64
+
65
+ $: handle_color_change(selected_color);
66
+ let width = 0;
67
+ let height = 0;
68
+
69
+ function debounce(
70
+ func: (...args: any[]) => void,
71
+ delay: number
72
+ ): (...args: any[]) => void {
73
+ let timeout: NodeJS.Timeout;
74
+ return function (...args: any[]): void {
75
+ clearTimeout(timeout);
76
+ timeout = setTimeout(() => func(...args), delay);
77
+ };
78
+ }
79
+
80
+ export let preview = false;
81
+ function handle_preview(): void {
82
+ if (!preview) {
83
+ // Show preview immediately when requested
84
+ preview = true;
85
+ debounced_close_preview();
86
+ } else {
87
+ // Only use debounce when hiding the preview
88
+ debounced_close_preview();
89
+ }
90
+ }
91
+
92
+ function handle_close_preview(): void {
93
+ preview = false;
94
+ }
95
+
96
+ const debounced_close_preview = debounce(handle_close_preview, 1000);
97
+
98
+ $: selected_size, selected_color, handle_preview();
99
+
100
+ function handle_select(color: string): void {
101
+ selected_color = color;
102
+ }
103
+
104
+ function handle_add_color(): void {
105
+ // limit to 5
106
+ if (recent_colors.length >= 5) {
107
+ recent_colors.pop();
108
+ }
109
+ // check if the color is already in the array
110
+ if (
111
+ recent_colors.some((color) => {
112
+ if (Array.isArray(color)) {
113
+ return color[0] === selected_color && color[1] === selected_opacity;
114
+ }
115
+ return color === selected_color;
116
+ })
117
+ ) {
118
+ return;
119
+ }
120
+ recent_colors.push([selected_color, selected_opacity]);
121
+ recent_colors = recent_colors;
122
+ }
123
+ </script>
124
+
125
+ <svelte:window bind:innerHeight={height} bind:innerWidth={width} />
126
+
127
+ <div
128
+ class="wrap"
129
+ class:padded={!color_picker}
130
+ use:click_outside={() => dispatch("click_outside")}
131
+ class:color_picker
132
+ class:size_picker={show_size && mode === "brush"}
133
+ class:eraser_picker={mode === "eraser"}
134
+ >
135
+ {#if color_mode === "defaults"}
136
+ {#if color_picker}
137
+ <ColorPicker
138
+ bind:color={selected_color}
139
+ bind:opacity={selected_opacity}
140
+ />
141
+ <ColorField
142
+ bind:current_mode
143
+ color={selected_color}
144
+ on:close={() => (color_picker = false)}
145
+ on:selected={({ detail }) => handle_select(detail)}
146
+ />
147
+ {/if}
148
+ {/if}
149
+ {#if show_swatch}
150
+ <ColorSwatch
151
+ bind:color_picker
152
+ {colors}
153
+ on:select={({ detail }) => handle_color_selection(detail, "core")}
154
+ on:edit={({ detail }) => handle_color_selection(detail, "user")}
155
+ user_colors={color_mode === "defaults" ? recent_colors : null}
156
+ {selected_color}
157
+ {current_mode}
158
+ on:add_color={handle_add_color}
159
+ />
160
+ {/if}
161
+
162
+ {#if show_size}
163
+ <BrushSize max={100} min={1} bind:selected_size />
164
+ {/if}
165
+ </div>
166
+
167
+ <style>
168
+ .wrap {
169
+ max-width: 230px;
170
+ /* width: 90%; */
171
+ position: absolute;
172
+ display: flex;
173
+ flex-direction: column;
174
+ gap: 5px;
175
+ background: var(--background-fill-secondary);
176
+ border: 1px solid var(--block-border-color);
177
+ border-radius: var(--radius-md);
178
+ box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.1);
179
+ padding-bottom: var(--size-2);
180
+ pointer-events: all;
181
+ cursor: default;
182
+ z-index: var(--layer-top);
183
+ overflow: hidden;
184
+ top: 0;
185
+
186
+ transform: translate(35px, -12px);
187
+ }
188
+
189
+ .color_picker {
190
+ transform: translate(35px, -50%);
191
+ }
192
+
193
+ .size_picker {
194
+ transform: translate(35px, 18px);
195
+ }
196
+
197
+ .eraser_picker {
198
+ transform: translate(35px, -7px);
199
+ }
200
+ </style>