@gradio/imageeditor 0.12.14 → 0.14.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 +36 -0
- package/Example.svelte +1 -1
- package/IMAGE_EDITOR_OVERVIEW.md +224 -0
- package/ImageEditor.stories.svelte +13 -5
- package/Index.svelte +46 -45
- package/{shared/InteractiveImageEditor.svelte → InteractiveImageEditor.svelte} +65 -103
- package/README.md +2 -0
- package/dist/Example.svelte.d.ts +1 -1
- package/dist/Index.svelte +35 -33
- package/dist/Index.svelte.d.ts +25 -16
- package/dist/{shared/InteractiveImageEditor.svelte → InteractiveImageEditor.svelte} +54 -88
- package/dist/{shared/InteractiveImageEditor.svelte.d.ts → InteractiveImageEditor.svelte.d.ts} +17 -15
- package/dist/shared/Anchor.svelte +311 -0
- package/dist/shared/Anchor.svelte.d.ts +18 -0
- package/dist/shared/Controls.svelte +169 -18
- package/dist/shared/Controls.svelte.d.ts +9 -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 +572 -289
- package/dist/shared/ImageEditor.svelte.d.ts +54 -56
- package/dist/shared/Layers.svelte +260 -0
- package/dist/shared/Layers.svelte.d.ts +37 -0
- package/dist/shared/Resize.svelte +218 -0
- package/dist/shared/Resize.svelte.d.ts +29 -0
- package/dist/shared/SecondaryToolbar.svelte +55 -0
- package/dist/shared/SecondaryToolbar.svelte.d.ts +37 -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 +65 -0
- package/dist/shared/brush/brush-textures.js +385 -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 +397 -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 +261 -0
- package/dist/shared/core/editor.js +1264 -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 +183 -0
- package/dist/shared/resize/resize.d.ts +270 -0
- package/dist/shared/resize/resize.js +1413 -0
- package/dist/shared/types.d.ts +16 -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 +8 -7
- package/shared/Anchor.svelte +384 -0
- package/shared/Controls.svelte +178 -37
- package/shared/{tools/IconButton.svelte → IconButton.svelte} +21 -4
- package/shared/ImageEditor.svelte +712 -350
- package/shared/Layers.svelte +282 -0
- package/shared/Resize.svelte +239 -0
- package/shared/SecondaryToolbar.svelte +67 -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 +485 -0
- package/shared/brush/brush-utils.ts +76 -0
- package/shared/brush/brush.ts +510 -0
- package/shared/brush/types.ts +41 -0
- package/shared/core/EDITOR.md +182 -0
- package/shared/core/editor.ts +1788 -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 +298 -0
- package/shared/resize/resize.ts +1894 -0
- package/shared/types.ts +19 -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,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>
|