@gradio/imageeditor 0.1.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.
@@ -0,0 +1,170 @@
1
+ <script lang="ts">
2
+ import tinycolor from "tinycolor2";
3
+ import { createEventDispatcher } from "svelte";
4
+ export let selected_color: string;
5
+ export let colors: string[];
6
+ export let user_colors: (string | null)[] | null = [];
7
+
8
+ export let show_empty = false;
9
+ export let current_mode: "hex" | "rgb" | "hsl" = "hex";
10
+ export let color_picker = false;
11
+ const dispatch = createEventDispatcher<{
12
+ select: { index: number | null; color: string | null };
13
+ edit: { index: number; color: string | null };
14
+ }>();
15
+
16
+ $: _colors = show_empty ? colors : colors.filter((c) => c);
17
+
18
+ function get_formatted_color(
19
+ color: string,
20
+ mode: "hex" | "rgb" | "hsl"
21
+ ): string {
22
+ if (mode === "hex") {
23
+ return tinycolor(color).toHexString();
24
+ } else if (mode === "rgb") {
25
+ return tinycolor(color).toRgbString();
26
+ }
27
+ return tinycolor(color).toHslString();
28
+ }
29
+
30
+ let current_index = `select-${colors.findIndex(
31
+ (c) =>
32
+ get_formatted_color(c, current_mode) ===
33
+ get_formatted_color(selected_color, current_mode)
34
+ )}`;
35
+
36
+ function handle_select(
37
+ type: "edit" | "select",
38
+ detail: {
39
+ index: number;
40
+ color: string | null;
41
+ }
42
+ ): void {
43
+ current_index = `${type}-${detail.index}`;
44
+ dispatch(type, detail);
45
+ }
46
+
47
+ function handle_picker_click(): void {
48
+ dispatch("select", { index: null, color: selected_color });
49
+ color_picker = !color_picker;
50
+ }
51
+ </script>
52
+
53
+ {#if user_colors}
54
+ <div class="swatch">
55
+ {#each user_colors as color, i}
56
+ <button
57
+ on:click={() => handle_select("edit", { index: i, color })}
58
+ class="color"
59
+ class:empty={color === null}
60
+ style="background-color: {color}"
61
+ class:selected={`edit-${i}` === current_index}
62
+ ></button>
63
+ {/each}
64
+
65
+ <button
66
+ on:click={handle_picker_click}
67
+ class="color colorpicker"
68
+ class:hidden={!color_picker}
69
+ ></button>
70
+ </div>
71
+ {/if}
72
+ <menu class="swatch">
73
+ {#each _colors as color, i}
74
+ <button
75
+ on:click={() => handle_select("select", { index: i, color })}
76
+ class="color"
77
+ class:empty={color === null}
78
+ style="background-color: {color}"
79
+ class:selected={`select-${i}` === current_index}
80
+ ></button>
81
+ {/each}
82
+ </menu>
83
+
84
+ <style>
85
+ .swatch {
86
+ display: flex;
87
+ width: 100%;
88
+ gap: var(--size-2);
89
+ justify-content: center;
90
+ margin-bottom: var(--size-1);
91
+ }
92
+
93
+ .empty {
94
+ border: 1px solid var(--block-border-color);
95
+ font-weight: var(--weight-bold);
96
+ display: flex;
97
+ justify-content: center;
98
+ align-items: center;
99
+ padding-top: 1px;
100
+ text-align: center;
101
+ font-size: var(--scale-0);
102
+ cursor: pointer;
103
+ }
104
+
105
+ .empty::after {
106
+ content: "+";
107
+ }
108
+
109
+ .color {
110
+ width: 30px;
111
+ height: 30px;
112
+ border-radius: 50%;
113
+ }
114
+
115
+ .empty.selected,
116
+ .color.selected {
117
+ border: 2px solid var(--color-accent);
118
+ }
119
+
120
+ .colorpicker {
121
+ border: 1px solid var(--block-border-color);
122
+
123
+ background: conic-gradient(
124
+ red,
125
+ #ff0 60deg,
126
+ lime 120deg,
127
+ cyan 180deg,
128
+ blue 240deg,
129
+ magenta 300deg,
130
+ red 360deg
131
+ );
132
+ overflow: hidden;
133
+ position: relative;
134
+ color: white;
135
+ cursor: pointer;
136
+ }
137
+
138
+ .colorpicker::before {
139
+ content: "";
140
+ position: absolute;
141
+ width: 100%;
142
+ height: 100%;
143
+ background: rgba(0, 0, 0, 0.6);
144
+ opacity: 1;
145
+ transition: 0.1s;
146
+ top: 0;
147
+ left: 0;
148
+ }
149
+
150
+ .colorpicker::after {
151
+ display: flex;
152
+ justify-content: center;
153
+ align-content: center;
154
+ content: "⨯";
155
+ color: white;
156
+ font-size: var(--scale-5);
157
+ position: absolute;
158
+ width: 100%;
159
+ height: 100%;
160
+ transform: translateY(-17px);
161
+ opacity: 1;
162
+ transition: 0.1s;
163
+ top: 0;
164
+ }
165
+
166
+ .colorpicker.hidden::after,
167
+ .colorpicker.hidden::before {
168
+ opacity: 0;
169
+ }
170
+ </style>
@@ -0,0 +1,193 @@
1
+ <script lang="ts">
2
+ import { getContext, onDestroy, onMount, tick } from "svelte";
3
+
4
+ import { type ToolContext, TOOL_KEY } from "./Tools.svelte";
5
+ import { type EditorContext, EDITOR_KEY } from "../ImageEditor.svelte";
6
+ import { crop_canvas, resize_and_reposition, type CropCommand } from "./crop";
7
+
8
+ import Cropper from "./Cropper.svelte";
9
+
10
+ const { active_tool, register_tool } = getContext<ToolContext>(TOOL_KEY);
11
+ const {
12
+ dimensions,
13
+ editor_box,
14
+ pixi,
15
+ crop,
16
+ command_manager,
17
+ current_history
18
+ } = getContext<EditorContext>(EDITOR_KEY);
19
+
20
+ let cropper: CropCommand | null;
21
+
22
+ export let crop_constraint: [number, number] | `${string}:${string}` | null;
23
+
24
+ let _crop_constraint: number | null = null;
25
+ $: {
26
+ if (typeof crop_constraint === "string") {
27
+ const [w, h] = crop_constraint.split(":");
28
+ _crop_constraint = parseInt(w) / parseInt(h);
29
+ } else if (Array.isArray(crop_constraint)) {
30
+ _crop_constraint = crop_constraint[0] / crop_constraint[1];
31
+ }
32
+ }
33
+
34
+ let w_p = 1;
35
+ let h_p = 1;
36
+ let l_p = 0;
37
+ let t_p = 0;
38
+
39
+ let current_opacity = 0;
40
+
41
+ let manually_cropped = false;
42
+
43
+ $: {
44
+ if (!manually_cropped && _crop_constraint && $pixi && $active_tool) {
45
+ requestAnimationFrame(() => {
46
+ initial_crop();
47
+ });
48
+ }
49
+ }
50
+
51
+ let c: CropCommand | null = null;
52
+
53
+ async function initial_crop(): Promise<void> {
54
+ if (c || $current_history.previous) return;
55
+ const { new_height, new_width, x_offset, y_offset } = resize_and_reposition(
56
+ $editor_box.child_width,
57
+ $editor_box.child_height,
58
+ "c",
59
+ _crop_constraint!,
60
+
61
+ $editor_box.child_width,
62
+ $editor_box.child_height
63
+ );
64
+
65
+ w_p = new_width / $editor_box.child_width;
66
+ h_p = new_height / $editor_box.child_height;
67
+ l_p = x_offset / $editor_box.child_width;
68
+ t_p = y_offset / $editor_box.child_height;
69
+
70
+ c = crop_canvas($pixi!.renderer, $pixi!.mask_container, crop, 0.2);
71
+ c.start(...$dimensions, current_crop);
72
+
73
+ c.continue([
74
+ l_p * $dimensions[0],
75
+ t_p * $dimensions[1],
76
+ w_p * $dimensions[0],
77
+ h_p * $dimensions[1]
78
+ ]);
79
+ c.stop();
80
+
81
+ c.execute();
82
+ c = null;
83
+ current_opacity = 0;
84
+ }
85
+
86
+ function handle_crop(
87
+ type: "start" | "stop" | "continue",
88
+
89
+ {
90
+ x,
91
+ y,
92
+ width,
93
+ height
94
+ }: {
95
+ x: number;
96
+ y: number;
97
+ width: number;
98
+ height: number;
99
+ }
100
+ ): void {
101
+ if (!$pixi) return;
102
+
103
+ if (type === "start") {
104
+ if (cropper) {
105
+ current_opacity = cropper.stop();
106
+ cropper = null;
107
+ }
108
+
109
+ cropper = crop_canvas(
110
+ $pixi?.renderer,
111
+ $pixi.mask_container,
112
+ crop,
113
+ current_opacity
114
+ );
115
+ cropper.start(...$dimensions, current_crop);
116
+ } else if (type === "continue") {
117
+ if (!cropper) return;
118
+ cropper.continue([
119
+ x * $dimensions[0],
120
+ y * $dimensions[1],
121
+ width * $dimensions[0],
122
+ height * $dimensions[1]
123
+ ]);
124
+ } else if (type === "stop") {
125
+ if (!cropper) return;
126
+ command_manager.execute(cropper);
127
+ }
128
+ manually_cropped = true;
129
+ }
130
+
131
+ let measured = false;
132
+
133
+ function get_measurements(): void {
134
+ if (measured) return;
135
+ if (!$editor_box.child_height) return;
136
+ const _height = $editor_box.child_height;
137
+ const _width = $editor_box.child_width;
138
+ const _top = $editor_box.child_top - $editor_box.parent_top;
139
+ const _left = $editor_box.child_left - $editor_box.parent_left;
140
+
141
+ w_p = _width / $editor_box.child_width;
142
+ h_p = _height / $editor_box.child_height;
143
+ l_p =
144
+ (_left - $editor_box.child_left + $editor_box.parent_left) /
145
+ $editor_box.child_width;
146
+ t_p =
147
+ (_top - $editor_box.child_top + $editor_box.parent_top) /
148
+ $editor_box.child_height;
149
+
150
+ measured = true;
151
+ }
152
+
153
+ let current_crop: [number, number, number, number];
154
+ $: current_crop = [
155
+ l_p * $dimensions[0],
156
+ t_p * $dimensions[1],
157
+ w_p * $dimensions[0],
158
+ h_p * $dimensions[1]
159
+ ];
160
+
161
+ $: {
162
+ l_p = $crop[0];
163
+ t_p = $crop[1];
164
+ w_p = $crop[2];
165
+ h_p = $crop[3];
166
+ }
167
+
168
+ $: $editor_box && get_measurements();
169
+
170
+ onMount(() =>
171
+ register_tool("crop", {
172
+ default: "crop",
173
+ options: []
174
+ })
175
+ );
176
+ </script>
177
+
178
+ {#if $active_tool === "crop" && measured}
179
+ <Cropper
180
+ crop_constraint={_crop_constraint}
181
+ {w_p}
182
+ {h_p}
183
+ {l_p}
184
+ {t_p}
185
+ {editor_box}
186
+ on:crop_start={({ detail }) => handle_crop("start", detail)}
187
+ on:crop_continue={({ detail }) => handle_crop("continue", detail)}
188
+ on:crop_end={({ detail }) => handle_crop("stop", detail)}
189
+ />
190
+ {/if}
191
+
192
+ <style>
193
+ </style>
@@ -0,0 +1,344 @@
1
+ <script lang="ts">
2
+ import { createEventDispatcher } from "svelte";
3
+ import Handle from "./Handle.svelte";
4
+ import { type EditorContext } from "../ImageEditor.svelte";
5
+ import { clamp } from "../utils/pixi";
6
+ import { resize_and_reposition } from "./crop";
7
+
8
+ export let editor_box: EditorContext["editor_box"];
9
+ export let crop_constraint: number | null;
10
+
11
+ const dispatch = createEventDispatcher<{
12
+ crop_start: {
13
+ x: number;
14
+ y: number;
15
+ width: number;
16
+ height: number;
17
+ };
18
+ crop_continue: {
19
+ x: number;
20
+ y: number;
21
+ width: number;
22
+ height: number;
23
+ };
24
+ crop_end: {
25
+ x: number;
26
+ y: number;
27
+ width: number;
28
+ height: number;
29
+ };
30
+ }>();
31
+
32
+ let _height = $editor_box.child_height;
33
+ let _width = $editor_box.child_width;
34
+ let _top = $editor_box.child_top - $editor_box.parent_top;
35
+ let _left = $editor_box.child_left - $editor_box.parent_left;
36
+ let _right = $editor_box.child_right - $editor_box.parent_left;
37
+ let _bottom = 0;
38
+
39
+ let dragging = false;
40
+
41
+ export let w_p: number;
42
+ export let h_p: number;
43
+ export let l_p: number;
44
+ export let t_p: number;
45
+
46
+ const positions = ["tl", "br", "tr", "bl", "t", "b", "l", "r"] as const;
47
+
48
+ let finished: boolean;
49
+
50
+ let timer: NodeJS.Timeout;
51
+ let triggered = false;
52
+
53
+ $: {
54
+ if (dragging || position_drag) {
55
+ clearTimeout(timer);
56
+
57
+ finished = false;
58
+ } else {
59
+ set_finished_with_timeout_and_delay();
60
+ }
61
+ }
62
+
63
+ function set_finished_with_timeout_and_delay(): void {
64
+ if (timer) {
65
+ clearTimeout(timer);
66
+ }
67
+ timer = setTimeout(() => {
68
+ if (triggered) {
69
+ dispatch("crop_end", {
70
+ x: l_p,
71
+ y: t_p,
72
+ width: w_p,
73
+ height: h_p
74
+ });
75
+ triggered = false;
76
+ }
77
+
78
+ finished = true;
79
+ position_drag = false;
80
+ }, 1000);
81
+ }
82
+
83
+ function handle_change(
84
+ {
85
+ top,
86
+ bottom,
87
+ left,
88
+ right
89
+ }: {
90
+ top: number | undefined;
91
+ bottom: number | undefined;
92
+ left: number | undefined;
93
+ right: number | undefined;
94
+ },
95
+ position: (typeof positions)[number]
96
+ ): void {
97
+ _top = clamp(
98
+ top ? top - $editor_box.parent_top : _top,
99
+ $editor_box.child_top - $editor_box.parent_top,
100
+ $editor_box.child_bottom - $editor_box.parent_top
101
+ );
102
+
103
+ _left = clamp(
104
+ left ? left - $editor_box.parent_left : _left,
105
+ $editor_box.child_left - $editor_box.parent_left,
106
+ $editor_box.child_right - $editor_box.parent_left
107
+ );
108
+ _right = clamp(
109
+ right ? right - $editor_box.parent_left : _right,
110
+ $editor_box.child_left - $editor_box.parent_left,
111
+ $editor_box.child_right - $editor_box.parent_left
112
+ );
113
+ _bottom = clamp(
114
+ bottom ? bottom - $editor_box.parent_top : _bottom,
115
+ $editor_box.child_top - $editor_box.parent_top,
116
+ $editor_box.child_bottom - $editor_box.parent_top
117
+ );
118
+ _width = clamp(
119
+ right ? right - _left - $editor_box.parent_left : _width,
120
+ 0,
121
+ _right - _left
122
+ );
123
+ _width = clamp(
124
+ left ? _right - left + $editor_box.parent_left : _width,
125
+ 0,
126
+ _right - _left
127
+ );
128
+ _height = clamp(
129
+ bottom ? bottom - _top - $editor_box.parent_top : _height,
130
+ 0,
131
+ _bottom - _top
132
+ );
133
+ _height = clamp(
134
+ top ? _bottom - top + $editor_box.parent_top : _height,
135
+ 0,
136
+ _bottom - _top
137
+ );
138
+
139
+ const anchors_for_position = {
140
+ tl: "br",
141
+ tr: "bl",
142
+ bl: "tr",
143
+ br: "tl",
144
+ t: "b",
145
+ b: "t",
146
+ l: "r",
147
+ r: "l",
148
+ c: "c"
149
+ } as const;
150
+
151
+ if (crop_constraint) {
152
+ const max_w = ["t", "b"].includes(position)
153
+ ? $editor_box.child_width
154
+ : _right - _left;
155
+ const max_h = ["l", "r"].includes(position)
156
+ ? $editor_box.child_height
157
+ : _bottom - _top;
158
+ let result = resize_and_reposition(
159
+ _width,
160
+ _height,
161
+ anchors_for_position[position],
162
+ crop_constraint,
163
+ max_w,
164
+ max_h
165
+ );
166
+
167
+ _width = result.new_width;
168
+ _height = result.new_height;
169
+ _left = _left + result.x_offset;
170
+ _top = _top + result.y_offset;
171
+ _right = _left + _width;
172
+ _bottom = _top + _height;
173
+ }
174
+
175
+ w_p = _width / $editor_box.child_width;
176
+ h_p = _height / $editor_box.child_height;
177
+ l_p =
178
+ (_left - $editor_box.child_left + $editor_box.parent_left) /
179
+ $editor_box.child_width;
180
+ t_p =
181
+ (_top - $editor_box.child_top + $editor_box.parent_top) /
182
+ $editor_box.child_height;
183
+
184
+ dispatch(triggered ? "crop_continue" : "crop_start", {
185
+ x: l_p,
186
+ y: t_p,
187
+ width: w_p,
188
+ height: h_p
189
+ });
190
+
191
+ triggered = true;
192
+ }
193
+
194
+ function resize(): void {
195
+ _width = w_p * $editor_box.child_width;
196
+ _height = h_p * $editor_box.child_height;
197
+ _left =
198
+ l_p * $editor_box.child_width +
199
+ ($editor_box.child_left - $editor_box.parent_left);
200
+ _top =
201
+ t_p * $editor_box.child_height +
202
+ ($editor_box.child_top - $editor_box.parent_top);
203
+ _right = _left + _width;
204
+ _bottom = _top + _height;
205
+ }
206
+
207
+ $: $editor_box && resize();
208
+
209
+ let start_x = 0;
210
+ let start_y = 0;
211
+ let position_drag = false;
212
+ function handle_drag_start(e: MouseEvent): void {
213
+ position_drag = true;
214
+
215
+ start_x = e.clientX;
216
+ start_y = e.clientY;
217
+
218
+ if (!finished) return;
219
+ finished = false;
220
+ dispatch("crop_start", {
221
+ x: l_p,
222
+ y: t_p,
223
+ width: w_p,
224
+ height: h_p
225
+ });
226
+ }
227
+
228
+ function handle_drag_end(e: MouseEvent): void {
229
+ if (!position_drag) return;
230
+
231
+ position_drag = false;
232
+ }
233
+
234
+ function handle_dragging(e: MouseEvent): void {
235
+ if (!position_drag) return;
236
+
237
+ const x_delta = e.clientX - start_x;
238
+ const y_delta = e.clientY - start_y;
239
+
240
+ _left = clamp(
241
+ _left + x_delta,
242
+ $editor_box.child_left - $editor_box.parent_left,
243
+ $editor_box.child_right - $editor_box.parent_left - _width
244
+ );
245
+
246
+ _top = clamp(
247
+ _top + y_delta,
248
+ $editor_box.child_top - $editor_box.parent_top,
249
+ $editor_box.child_bottom - $editor_box.parent_top - _height
250
+ );
251
+
252
+ _right = _left + _width;
253
+ _bottom = _top + _height;
254
+
255
+ l_p =
256
+ (_left - $editor_box.child_left + $editor_box.parent_left) /
257
+ $editor_box.child_width;
258
+ t_p =
259
+ (_top - $editor_box.child_top + $editor_box.parent_top) /
260
+ $editor_box.child_height;
261
+
262
+ start_x = e.clientX;
263
+ start_y = e.clientY;
264
+ dispatch("crop_continue", {
265
+ x: l_p,
266
+ y: t_p,
267
+ width: w_p,
268
+ height: h_p
269
+ });
270
+
271
+ triggered = true;
272
+ }
273
+ </script>
274
+
275
+ <svelte:window on:mousemove={handle_dragging} on:mouseup={handle_drag_end} />
276
+
277
+ <div class="wrap">
278
+ <div
279
+ class="box"
280
+ style:width="{_width}px"
281
+ style:height="{_height}px"
282
+ style:top="{_top}px"
283
+ style:left="{_left}px"
284
+ >
285
+ {#each positions as position}
286
+ <Handle
287
+ on:change={({ detail }) => handle_change(detail, position)}
288
+ bind:dragging
289
+ location={position}
290
+ />
291
+ {/each}
292
+
293
+ <!-- svelte-ignore a11y-no-static-element-interactions -->
294
+ <div class="grid" class:finished on:mousedown={handle_drag_start}>
295
+ {#each { length: 25 } as _}
296
+ <div></div>
297
+ {/each}
298
+ </div>
299
+ </div>
300
+ </div>
301
+
302
+ <style>
303
+ .grid {
304
+ height: 100%;
305
+ width: 100%;
306
+ display: grid;
307
+ grid-template-rows: 1fr 1px 1fr 1px 1fr;
308
+ grid-template-columns: 1fr 1px 1fr 1px 1fr;
309
+ overflow: hidden;
310
+ transition: 0.2s;
311
+ opacity: 1;
312
+ /* pointer-events: none; */
313
+ }
314
+
315
+ .grid.finished {
316
+ opacity: 0;
317
+ }
318
+
319
+ .grid > div {
320
+ width: 100%;
321
+ height: 100%;
322
+ }
323
+
324
+ .grid > div:nth-of-type(even) {
325
+ background: black;
326
+ opacity: 0.5;
327
+ }
328
+
329
+ .wrap {
330
+ position: absolute;
331
+ top: 0;
332
+ left: 0;
333
+ width: 100%;
334
+ height: 100%;
335
+ /* transform: translateY(-20px); */
336
+ }
337
+
338
+ .box {
339
+ position: absolute;
340
+ width: 100%;
341
+ height: 100%;
342
+ border: 1px solid black;
343
+ }
344
+ </style>