@gradio/image 0.3.0-beta.5

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,626 @@
1
+ <script>
2
+ // @ts-nocheck
3
+ /* eslint-disable */
4
+
5
+ import { onMount, onDestroy, createEventDispatcher, tick } from "svelte";
6
+ import { fade } from "svelte/transition";
7
+ import { LazyBrush } from "lazy-brush/src";
8
+ import ResizeObserver from "resize-observer-polyfill";
9
+
10
+ const dispatch = createEventDispatcher();
11
+
12
+ export let value;
13
+ export let value_img;
14
+ export let mode = "sketch";
15
+ export let brush_color = "#0b0f19";
16
+ export let brush_radius;
17
+ export let mask_opacity = 0.7;
18
+ export let source;
19
+
20
+ export let width = 400;
21
+ export let height = 200;
22
+ export let container_height = 200;
23
+ export let shape;
24
+
25
+ $: {
26
+ if (shape && (width || height)) {
27
+ width = shape[0];
28
+ height = shape[1];
29
+ }
30
+ }
31
+
32
+ let mounted;
33
+
34
+ let catenary_color = "#aaa";
35
+
36
+ let canvas_width = width;
37
+ let canvas_height = height;
38
+
39
+ $: mounted && !value && clear();
40
+
41
+ let last_value_img;
42
+
43
+ $: console.log(value_img);
44
+
45
+ $: {
46
+ if (mounted && value_img !== last_value_img) {
47
+ last_value_img = value_img;
48
+
49
+ clear();
50
+
51
+ setTimeout(() => {
52
+ if (source === "webcam") {
53
+ ctx.temp.save();
54
+ ctx.temp.translate(width, 0);
55
+ ctx.temp.scale(-1, 1);
56
+ ctx.temp.drawImage(value_img, 0, 0);
57
+ ctx.temp.restore();
58
+ } else {
59
+ draw_cropped_image();
60
+ }
61
+
62
+ ctx.drawing.drawImage(canvas.temp, 0, 0, width, height);
63
+
64
+ draw_lines({ lines: lines.slice() });
65
+ trigger_on_change();
66
+ }, 50);
67
+ }
68
+ }
69
+
70
+ function mid_point(p1, p2) {
71
+ return {
72
+ x: p1.x + (p2.x - p1.x) / 2,
73
+ y: p1.y + (p2.y - p1.y) / 2
74
+ };
75
+ }
76
+
77
+ const canvas_types = [
78
+ {
79
+ name: "interface",
80
+ zIndex: 15
81
+ },
82
+ {
83
+ name: "mask",
84
+ zIndex: 13,
85
+ opacity: mask_opacity
86
+ },
87
+ {
88
+ name: "drawing",
89
+ zIndex: 11
90
+ },
91
+ {
92
+ name: "temp",
93
+ zIndex: 12
94
+ }
95
+ ];
96
+
97
+ let canvas = {};
98
+ let ctx = {};
99
+ let points = [];
100
+ let lines = [];
101
+ let mouse_has_moved = true;
102
+ let values_changed = true;
103
+ let is_drawing = false;
104
+ let is_pressing = false;
105
+ let lazy = null;
106
+ let canvas_container = null;
107
+ let canvas_observer = null;
108
+ let line_count = 0;
109
+
110
+ function draw_cropped_image() {
111
+ if (!shape) {
112
+ ctx.temp.drawImage(value_img, 0, 0, width, height);
113
+ return;
114
+ }
115
+
116
+ let _width = value_img.naturalWidth;
117
+ let _height = value_img.naturalHeight;
118
+
119
+ const shape_ratio = shape[0] / shape[1];
120
+ const image_ratio = _width / _height;
121
+
122
+ let x = 0;
123
+ let y = 0;
124
+
125
+ if (shape_ratio < image_ratio) {
126
+ _width = shape[1] * image_ratio;
127
+ _height = shape[1];
128
+ x = (shape[0] - _width) / 2;
129
+ } else if (shape_ratio > image_ratio) {
130
+ _width = shape[0];
131
+ _height = shape[0] / image_ratio;
132
+ y = (shape[1] - _height) / 2;
133
+ } else {
134
+ x = 0;
135
+ y = 0;
136
+ _width = shape[0];
137
+ _height = shape[1];
138
+ }
139
+
140
+ ctx.temp.drawImage(value_img, x, y, _width, _height);
141
+ }
142
+
143
+ onMount(async () => {
144
+ Object.keys(canvas).forEach((key) => {
145
+ ctx[key] = canvas[key].getContext("2d");
146
+ });
147
+
148
+ await tick();
149
+
150
+ if (value_img) {
151
+ value_img.addEventListener("load", (_) => {
152
+ if (source === "webcam") {
153
+ ctx.temp.save();
154
+ ctx.temp.translate(width, 0);
155
+ ctx.temp.scale(-1, 1);
156
+ ctx.temp.drawImage(value_img, 0, 0);
157
+ ctx.temp.restore();
158
+ } else {
159
+ draw_cropped_image();
160
+ }
161
+ ctx.drawing.drawImage(canvas.temp, 0, 0, width, height);
162
+
163
+ trigger_on_change();
164
+ });
165
+
166
+ setTimeout(() => {
167
+ if (source === "webcam") {
168
+ ctx.temp.save();
169
+ ctx.temp.translate(width, 0);
170
+ ctx.temp.scale(-1, 1);
171
+ ctx.temp.drawImage(value_img, 0, 0);
172
+ ctx.temp.restore();
173
+ } else {
174
+ draw_cropped_image();
175
+ }
176
+
177
+ ctx.drawing.drawImage(canvas.temp, 0, 0, width, height);
178
+
179
+ draw_lines({ lines: lines.slice() });
180
+ trigger_on_change();
181
+ }, 100);
182
+ }
183
+
184
+ lazy = new LazyBrush({
185
+ radius: brush_radius * 0.05,
186
+ enabled: true,
187
+ initialPoint: {
188
+ x: width / 2,
189
+ y: height / 2
190
+ }
191
+ });
192
+
193
+ canvas_observer = new ResizeObserver((entries, observer, ...rest) => {
194
+ handle_canvas_resize(entries, observer);
195
+ });
196
+ canvas_observer.observe(canvas_container);
197
+
198
+ loop();
199
+ mounted = true;
200
+
201
+ requestAnimationFrame(() => {
202
+ init();
203
+ requestAnimationFrame(() => {
204
+ clear();
205
+ });
206
+ });
207
+ });
208
+
209
+ function init() {
210
+ const initX = width / 2;
211
+ const initY = height / 2;
212
+ lazy.update({ x: initX, y: initY }, { both: true });
213
+ lazy.update({ x: initX, y: initY }, { both: false });
214
+ mouse_has_moved = true;
215
+ values_changed = true;
216
+ }
217
+
218
+ onDestroy(() => {
219
+ mounted = false;
220
+ canvas_observer.unobserve(canvas_container);
221
+ });
222
+
223
+ function redraw_image(_lines) {
224
+ clear_canvas();
225
+
226
+ if (value_img) {
227
+ if (source === "webcam") {
228
+ ctx.temp.save();
229
+ ctx.temp.translate(width, 0);
230
+ ctx.temp.scale(-1, 1);
231
+ ctx.temp.drawImage(value_img, 0, 0);
232
+ ctx.temp.restore();
233
+ } else {
234
+ draw_cropped_image();
235
+ }
236
+
237
+ if (!lines || !lines.length) {
238
+ ctx.drawing.drawImage(canvas.temp, 0, 0, width, height);
239
+ }
240
+ }
241
+
242
+ draw_lines({ lines: _lines });
243
+ line_count = _lines.length;
244
+
245
+ lines = _lines;
246
+ ctx.drawing.drawImage(canvas.temp, 0, 0, width, height);
247
+
248
+ if (lines.length == 0) {
249
+ dispatch("clear");
250
+ }
251
+ }
252
+
253
+ export function clear_mask() {
254
+ const _lines = [];
255
+
256
+ redraw_image(_lines);
257
+ trigger_on_change();
258
+ }
259
+
260
+ export function undo() {
261
+ const _lines = lines.slice(0, -1);
262
+
263
+ redraw_image(_lines);
264
+ trigger_on_change();
265
+ }
266
+
267
+ let get_save_data = () => {
268
+ return JSON.stringify({
269
+ lines: lines,
270
+ width: canvas_width,
271
+ height: canvas_height
272
+ });
273
+ };
274
+
275
+ let draw_lines = ({ lines }) => {
276
+ lines.forEach((line) => {
277
+ const { points: _points, brush_color, brush_radius } = line;
278
+ draw_points({
279
+ points: _points,
280
+ brush_color,
281
+ brush_radius,
282
+ mask: mode === "mask"
283
+ });
284
+ });
285
+
286
+ saveLine({ brush_color, brush_radius });
287
+ if (mode === "mask") {
288
+ save_mask_line();
289
+ }
290
+ };
291
+
292
+ let handle_draw_start = (e) => {
293
+ e.preventDefault();
294
+ is_pressing = true;
295
+ const { x, y } = get_pointer_pos(e);
296
+ if (e.touches && e.touches.length > 0) {
297
+ lazy.update({ x, y }, { both: true });
298
+ }
299
+ handle_pointer_move(x, y);
300
+ line_count += 1;
301
+ };
302
+
303
+ let handle_draw_move = (e) => {
304
+ e.preventDefault();
305
+ const { x, y } = get_pointer_pos(e);
306
+ handle_pointer_move(x, y);
307
+ };
308
+
309
+ let handle_draw_end = (e) => {
310
+ e.preventDefault();
311
+ handle_draw_move(e);
312
+ is_drawing = false;
313
+ is_pressing = false;
314
+ saveLine();
315
+
316
+ if (mode === "mask") {
317
+ save_mask_line();
318
+ }
319
+ };
320
+
321
+ let old_width = 0;
322
+ let old_height = 0;
323
+ let old_container_height = 0;
324
+ let add_lr_border = false;
325
+
326
+ let handle_canvas_resize = async () => {
327
+ if (shape && canvas_container) {
328
+ const x = canvas_container?.getBoundingClientRect();
329
+ const shape_ratio = shape[0] / shape[1];
330
+ const container_ratio = x.width / x.height;
331
+ add_lr_border = shape_ratio < container_ratio;
332
+ }
333
+
334
+ if (
335
+ width === old_width &&
336
+ height === old_height &&
337
+ old_container_height === container_height
338
+ ) {
339
+ return;
340
+ }
341
+ const dimensions = { width: width, height: height };
342
+
343
+ const container_dimensions = {
344
+ height: container_height,
345
+ width: container_height * (dimensions.width / dimensions.height)
346
+ };
347
+
348
+ await Promise.all([
349
+ set_canvas_size(canvas.interface, dimensions, container_dimensions),
350
+ set_canvas_size(canvas.drawing, dimensions, container_dimensions),
351
+ set_canvas_size(canvas.temp, dimensions, container_dimensions),
352
+ set_canvas_size(canvas.mask, dimensions, container_dimensions, false)
353
+ ]);
354
+
355
+ if (!brush_radius) {
356
+ brush_radius = 20 * (dimensions.width / container_dimensions.width);
357
+ }
358
+
359
+ loop({ once: true });
360
+
361
+ setTimeout(() => {
362
+ old_height = height;
363
+ old_width = width;
364
+ old_container_height = container_height;
365
+ }, 10);
366
+ await tick();
367
+
368
+ clear();
369
+ };
370
+
371
+ $: {
372
+ if (lazy) {
373
+ init();
374
+ lazy.setRadius(brush_radius * 0.05);
375
+ }
376
+ }
377
+
378
+ $: {
379
+ if (width || height) {
380
+ handle_canvas_resize();
381
+ }
382
+ }
383
+
384
+ let set_canvas_size = async (canvas, dimensions, container, scale = true) => {
385
+ if (!mounted) return;
386
+ await tick();
387
+
388
+ const dpr = window.devicePixelRatio || 1;
389
+ canvas.width = dimensions.width * (scale ? dpr : 1);
390
+ canvas.height = dimensions.height * (scale ? dpr : 1);
391
+
392
+ const ctx = canvas.getContext("2d");
393
+ scale && ctx.scale(dpr, dpr);
394
+
395
+ canvas.style.width = `${container.width}px`;
396
+ canvas.style.height = `${container.height}px`;
397
+ };
398
+
399
+ let get_pointer_pos = (e) => {
400
+ const rect = canvas.interface.getBoundingClientRect();
401
+
402
+ let clientX = e.clientX;
403
+ let clientY = e.clientY;
404
+ if (e.changedTouches && e.changedTouches.length > 0) {
405
+ clientX = e.changedTouches[0].clientX;
406
+ clientY = e.changedTouches[0].clientY;
407
+ }
408
+
409
+ return {
410
+ x: ((clientX - rect.left) / rect.width) * width,
411
+ y: ((clientY - rect.top) / rect.height) * height
412
+ };
413
+ };
414
+
415
+ let handle_pointer_move = (x, y) => {
416
+ lazy.update({ x: x, y: y });
417
+ const is_disabled = !lazy.isEnabled();
418
+ if ((is_pressing && !is_drawing) || (is_disabled && is_pressing)) {
419
+ is_drawing = true;
420
+ points.push(lazy.brush.toObject());
421
+ }
422
+ if (is_drawing) {
423
+ points.push(lazy.brush.toObject());
424
+ draw_points({
425
+ points: points,
426
+ brush_color,
427
+ brush_radius,
428
+ mask: mode === "mask"
429
+ });
430
+ }
431
+ mouse_has_moved = true;
432
+ };
433
+
434
+ let draw_points = ({ points, brush_color, brush_radius, mask }) => {
435
+ if (!points || points.length < 2) return;
436
+ let target_ctx = mask ? ctx.mask : ctx.temp;
437
+ target_ctx.lineJoin = "round";
438
+ target_ctx.lineCap = "round";
439
+
440
+ target_ctx.strokeStyle = brush_color;
441
+ target_ctx.lineWidth = brush_radius;
442
+ let p1 = points[0];
443
+ let p2 = points[1];
444
+ target_ctx.moveTo(p2.x, p2.y);
445
+ target_ctx.beginPath();
446
+ for (var i = 1, len = points.length; i < len; i++) {
447
+ var midPoint = mid_point(p1, p2);
448
+ target_ctx.quadraticCurveTo(p1.x, p1.y, midPoint.x, midPoint.y);
449
+ p1 = points[i];
450
+ p2 = points[i + 1];
451
+ }
452
+
453
+ target_ctx.lineTo(p1.x, p1.y);
454
+ target_ctx.stroke();
455
+ };
456
+
457
+ let save_mask_line = () => {
458
+ if (points.length < 1) return;
459
+ points.length = 0;
460
+
461
+ trigger_on_change();
462
+ };
463
+
464
+ let saveLine = () => {
465
+ if (points.length < 1) return;
466
+
467
+ lines.push({
468
+ points: points.slice(),
469
+ brush_color: brush_color,
470
+ brush_radius
471
+ });
472
+
473
+ if (mode !== "mask") {
474
+ points.length = 0;
475
+ }
476
+
477
+ ctx.drawing.drawImage(canvas.temp, 0, 0, width, height);
478
+
479
+ trigger_on_change();
480
+ };
481
+
482
+ let trigger_on_change = () => {
483
+ const x = get_image_data();
484
+ dispatch("change", x);
485
+ };
486
+
487
+ export function clear() {
488
+ lines = [];
489
+ clear_canvas();
490
+ line_count = 0;
491
+
492
+ return true;
493
+ }
494
+
495
+ function clear_canvas() {
496
+ values_changed = true;
497
+ ctx.temp.clearRect(0, 0, width, height);
498
+
499
+ ctx.temp.fillStyle = mode === "mask" ? "transparent" : "#FFFFFF";
500
+ ctx.temp.fillRect(0, 0, width, height);
501
+
502
+ if (mode === "mask") {
503
+ ctx.mask.clearRect(0, 0, canvas.mask.width, canvas.mask.height);
504
+ }
505
+ }
506
+
507
+ let loop = ({ once = false } = {}) => {
508
+ if (mouse_has_moved || values_changed) {
509
+ const pointer = lazy.getPointerCoordinates();
510
+ const brush = lazy.getBrushCoordinates();
511
+ draw_interface(ctx.interface, pointer, brush);
512
+ mouse_has_moved = false;
513
+ values_changed = false;
514
+ }
515
+ if (!once) {
516
+ window.requestAnimationFrame(() => {
517
+ loop();
518
+ });
519
+ }
520
+ };
521
+
522
+ $: brush_dot = brush_radius * 0.075;
523
+
524
+ let draw_interface = (ctx, pointer, brush) => {
525
+ ctx.clearRect(0, 0, width, height);
526
+
527
+ // brush preview
528
+ ctx.beginPath();
529
+ ctx.fillStyle = brush_color;
530
+ ctx.arc(brush.x, brush.y, brush_radius / 2, 0, Math.PI * 2, true);
531
+ ctx.fill();
532
+
533
+ // tiny brush point dot
534
+ ctx.beginPath();
535
+ ctx.fillStyle = catenary_color;
536
+ ctx.arc(brush.x, brush.y, brush_dot, 0, Math.PI * 2, true);
537
+ ctx.fill();
538
+ };
539
+
540
+ export function get_image_data() {
541
+ return mode === "mask"
542
+ ? canvas.mask.toDataURL("image/png")
543
+ : canvas.drawing.toDataURL("image/jpg");
544
+ }
545
+ </script>
546
+
547
+ <div
548
+ class="wrap"
549
+ bind:this={canvas_container}
550
+ bind:offsetWidth={canvas_width}
551
+ bind:offsetHeight={canvas_height}
552
+ >
553
+ {#if line_count === 0}
554
+ <div transition:fade={{ duration: 50 }} class="start-prompt">
555
+ Start drawing
556
+ </div>
557
+ {/if}
558
+ {#each canvas_types as { name, zIndex, opacity }}
559
+ <canvas
560
+ key={name}
561
+ style=" z-index:{zIndex};"
562
+ style:opacity
563
+ class:lr={add_lr_border}
564
+ class:tb={!add_lr_border}
565
+ bind:this={canvas[name]}
566
+ on:mousedown={name === "interface" ? handle_draw_start : undefined}
567
+ on:mousemove={name === "interface" ? handle_draw_move : undefined}
568
+ on:mouseup={name === "interface" ? handle_draw_end : undefined}
569
+ on:mouseout={name === "interface" ? handle_draw_end : undefined}
570
+ on:blur={name === "interface" ? handle_draw_end : undefined}
571
+ on:touchstart={name === "interface" ? handle_draw_start : undefined}
572
+ on:touchmove={name === "interface" ? handle_draw_move : undefined}
573
+ on:touchend={name === "interface" ? handle_draw_end : undefined}
574
+ on:touchcancel={name === "interface" ? handle_draw_end : undefined}
575
+ on:click|stopPropagation
576
+ />
577
+ {/each}
578
+ </div>
579
+
580
+ <style>
581
+ canvas {
582
+ display: block;
583
+ position: absolute;
584
+ top: 0px;
585
+ right: 0px;
586
+ bottom: 0px;
587
+ left: 0px;
588
+ margin: auto;
589
+ }
590
+
591
+ .lr {
592
+ border-right: 1px solid var(--border-color-primary);
593
+ border-left: 1px solid var(--border-color-primary);
594
+ }
595
+
596
+ .tb {
597
+ border-top: 1px solid var(--border-color-primary);
598
+ border-bottom: 1px solid var(--border-color-primary);
599
+ }
600
+
601
+ canvas:hover {
602
+ cursor: none;
603
+ }
604
+
605
+ .wrap {
606
+ position: relative;
607
+ width: var(--size-full);
608
+ height: var(--size-full);
609
+ touch-action: none;
610
+ }
611
+
612
+ .start-prompt {
613
+ display: flex;
614
+ position: absolute;
615
+ top: 0px;
616
+ right: 0px;
617
+ bottom: 0px;
618
+ left: 0px;
619
+ justify-content: center;
620
+ align-items: center;
621
+ z-index: var(--layer-4);
622
+ touch-action: none;
623
+ pointer-events: none;
624
+ color: var(--body-text-color-subdued);
625
+ }
626
+ </style>
@@ -0,0 +1,77 @@
1
+ <script lang="ts">
2
+ import { IconButton } from "@gradio/atoms";
3
+ import { Brush, Color } from "@gradio/icons";
4
+
5
+ let show_size = false;
6
+ let show_col = false;
7
+
8
+ export let brush_radius = 20;
9
+ export let brush_color = "#000";
10
+ export let container_height: number;
11
+ export let img_width: number;
12
+ export let img_height: number;
13
+ export let mode: "mask" | "other" = "other";
14
+
15
+ $: width = container_height * (img_width / img_height);
16
+ </script>
17
+
18
+ <div class="wrap">
19
+ <span class="brush">
20
+ <IconButton
21
+ Icon={Brush}
22
+ label="Use brush"
23
+ on:click={() => (show_size = !show_size)}
24
+ />
25
+ {#if show_size}
26
+ <input
27
+ aria-label="Brush radius"
28
+ bind:value={brush_radius}
29
+ type="range"
30
+ min={0.5 * (img_width / width)}
31
+ max={75 * (img_width / width)}
32
+ />
33
+ {/if}
34
+ </span>
35
+
36
+ {#if mode !== "mask"}
37
+ <span class="col">
38
+ <IconButton
39
+ Icon={Color}
40
+ label="Select brush color"
41
+ on:click={() => (show_col = !show_col)}
42
+ />
43
+ {#if show_col}
44
+ <input aria-label="Brush color" bind:value={brush_color} type="color" />
45
+ {/if}
46
+ </span>
47
+ {/if}
48
+ </div>
49
+
50
+ <style>
51
+ .wrap {
52
+ display: flex;
53
+ position: absolute;
54
+ top: var(--size-10);
55
+ right: var(--size-2);
56
+ flex-direction: column;
57
+ justify-content: flex-end;
58
+ gap: var(--spacing-sm);
59
+ z-index: var(--layer-5);
60
+ }
61
+ .brush {
62
+ top: 0;
63
+ right: 0;
64
+ }
65
+
66
+ .brush input {
67
+ position: absolute;
68
+ top: 3px;
69
+ right: calc(100% + 5px);
70
+ }
71
+
72
+ .col input {
73
+ position: absolute;
74
+ right: calc(100% + 5px);
75
+ bottom: -4px;
76
+ }
77
+ </style>