Sketching + Inpainting Capabilities to Gradio (#2144)

* templates

* working on backend

* formatting

* Sketching fe (#2184)

* fix scaling on sketch + bg img

* tweaks

* ketch updates

* cursor style

* sketchpad

* fixes

* ensure background is white for bw sketch

* fix everything

* re-enable demos

* updated demo and changed from dict to str

* beta release

* fix bugs, tweak webcam source

* re-anable demos

* fix clear button and tab changing

* maybe fix test

* maybe fix test again maybe

* various fixes

* fix img uplaod + color sketch

* remove lazy brush but keep smoothing

* fix sketch bg

Co-authored-by: pngwn <hello@pngwn.io>
This commit is contained in:
Abubakar Abid 2022-09-23 06:14:56 -05:00 committed by GitHub
parent 581fbabe07
commit cecaf1a635
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 728 additions and 357 deletions

BIN
demo/all_demos/tmp.zip Normal file

Binary file not shown.

View File

@ -1,25 +1,135 @@
import gradio as gr
import os
def fn(mask):
return [mask["image"], mask["mask"]]
from gradio.components import Markdown as md
demo = gr.Blocks()
with demo:
with gr.Row():
with gr.Column():
img = gr.Image(
tool="sketch", source="upload", label="Mask", value=os.path.join(os.path.dirname(__file__), "lion.jpg")
)
with gr.Row():
btn = gr.Button("Run")
with gr.Column():
img2 = gr.Image()
img3 = gr.Image()
io1a = gr.Interface(lambda x: x, gr.Image(), gr.Image())
io1b = gr.Interface(lambda x: x, gr.Image(source="webcam"), gr.Image())
btn.click(fn=fn, inputs=img, outputs=[img2, img3])
io2a = gr.Interface(lambda x: x, gr.Image(source="canvas"), gr.Image())
io2b = gr.Interface(lambda x: x, gr.Sketchpad(), gr.Image())
io3a = gr.Interface(
lambda x: [x["mask"], x["image"]],
gr.Image(source="upload", tool="sketch"),
[gr.Image(), gr.Image()],
)
io3b = gr.Interface(
lambda x: [x["mask"], x["image"]],
gr.ImageMask(),
[gr.Image(), gr.Image()],
)
io3b2 = gr.Interface(
lambda x: [x["mask"], x["image"]],
gr.ImageMask(),
[gr.Image(), gr.Image()],
)
io3b3 = gr.Interface(
lambda x: [x["mask"], x["image"]],
gr.ImageMask(),
[gr.Image(), gr.Image()],
)
io3c = gr.Interface(
lambda x: [x["mask"], x["image"]],
gr.Image(source="webcam", tool="sketch"),
[gr.Image(), gr.Image()],
)
io4a = gr.Interface(
lambda x: x, gr.Image(source="canvas", tool="color-sketch"), gr.Image()
)
io4b = gr.Interface(lambda x: x, gr.Paint(), gr.Image())
io5a = gr.Interface(
lambda x: x, gr.Image(source="upload", tool="color-sketch"), gr.Image()
)
io5b = gr.Interface(lambda x: x, gr.ImagePaint(), gr.Image())
io5c = gr.Interface(
lambda x: x, gr.Image(source="webcam", tool="color-sketch"), gr.Image()
)
with demo:
md("# Different Ways to Use the Image Input Component")
md(
"**1a. Standalone Image Upload: `gr.Interface(lambda x: x, gr.Image(), gr.Image())`**"
)
io1a.render()
md(
"**1b. Standalone Image from Webcam: `gr.Interface(lambda x: x, gr.Image(source='webcam'), gr.Image())`**"
)
io1b.render()
md(
"**2a. Black and White Sketchpad: `gr.Interface(lambda x: x, gr.Image(source='canvas'), gr.Image())`**"
)
io2a.render()
md(
"**2b. Black and White Sketchpad: `gr.Interface(lambda x: x, gr.Sketchpad(), gr.Image())`**"
)
io2b.render()
md("**3a. Binary Mask with image upload:**")
md(
"""```python
gr.Interface(
lambda x: [x['mask'], x['image']],
gr.Image(source='upload', tool='sketch'),
[gr.Image(), gr.Image()],
)
```
"""
)
io3a.render()
md("**3b. Binary Mask with image upload:**")
md(
"""```python
gr.Interface(
lambda x: [x['mask'], x['image']],
gr.ImageMask(),
[gr.Image(), gr.Image()],
)
```
"""
)
io3b.render()
md("**3c. Binary Mask with webcam upload:**")
md(
"""```python
gr.Interface(
lambda x: [x['mask'], x['image']],
gr.Image(source='webcam', tool='sketch'),
[gr.Image(), gr.Image()],
)
```
"""
)
io3c.render()
md(
"**4a. Color Sketchpad: `gr.Interface(lambda x: x, gr.Image(source='canvas', tool='color-sketch'), gr.Image())`**"
)
io4a.render()
md("**4b. Color Sketchpad: `gr.Interface(lambda x: x, gr.Paint(), gr.Image())`**")
io4b.render()
md(
"**5a. Color Sketchpad with image upload: `gr.Interface(lambda x: x, gr.Image(source='upload', tool='color-sketch'), gr.Image())`**"
)
io5a.render()
md(
"**5b. Color Sketchpad with image upload: `gr.Interface(lambda x: x, gr.ImagePaint(), gr.Image())`**"
)
io5b.render()
md(
"**5c. Color Sketchpad with webcam upload: `gr.Interface(lambda x: x, gr.Image(source='webcam', tool='color-sketch'), gr.Image())`**"
)
io5c.render()
md("**Tabs**")
with gr.Tab("One"):
io3b2.render()
with gr.Tab("Two"):
io3b3.render()
if __name__ == "__main__":

View File

@ -12,7 +12,7 @@ demo = gr.Interface(
headers=["name", "age", "gender"],
datatype=["str", "number", "str"],
row_count=5,
col_count=(3, "fixed")
col_count=(3, "fixed"),
),
gr.Dropdown(["M", "F", "O"]),
],

View File

@ -60,11 +60,14 @@ from gradio.mix import Parallel, Series
from gradio.templates import (
Files,
Highlight,
ImageMask,
ImagePaint,
List,
Matrix,
Mic,
Microphone,
Numpy,
Paint,
Pil,
PlayableVideo,
Sketchpad,

View File

@ -32,6 +32,7 @@ import matplotlib.figure
import numpy as np
import pandas as pd
import PIL
import PIL.ImageOps
from ffmpy import FFmpeg
from markdown_it import MarkdownIt
@ -1175,11 +1176,11 @@ class Dropdown(Radio):
)
@document("edit", "clear", "change", "stream", "change")
@document("edit", "clear", "change", "stream", "change", "style")
class Image(Editable, Clearable, Changeable, Streamable, IOComponent, ImgSerializable):
"""
Creates an image component that can be used to upload/draw images (as an input) or display images (as an output).
Preprocessing: passes the uploaded image as a {numpy.array}, {PIL.Image} or {str} filepath depending on `type` -- unless `tool` is `sketch`. In the special case, a {dict} with keys `image` and `mask` is passed, and the format of the corresponding values depends on `type`.
Preprocessing: passes the uploaded image as a {numpy.array}, {PIL.Image} or {str} filepath depending on `type` -- unless `tool` is `sketch` AND source is one of `upload` or `webcam`. In these cases, a {dict} with keys `image` and `mask` is passed, and the format of the corresponding values depends on `type`.
Postprocessing: expects a {numpy.array}, {PIL.Image} or {str} or {pathlib.Path} filepath to an image and displays the image.
Examples-format: a {str} filepath to a local file that contains the image.
Demos: image_mod, image_mod_default_image
@ -1194,7 +1195,7 @@ class Image(Editable, Clearable, Changeable, Streamable, IOComponent, ImgSeriali
image_mode: str = "RGB",
invert_colors: bool = False,
source: str = "upload",
tool: str = "editor",
tool: str = None,
type: str = "numpy",
label: Optional[str] = None,
show_label: bool = True,
@ -1212,7 +1213,7 @@ class Image(Editable, Clearable, Changeable, Streamable, IOComponent, ImgSeriali
image_mode: "RGB" if color, or "L" if black and white.
invert_colors: whether to invert the image as a preprocessing step.
source: Source of image. "upload" creates a box where user can drop an image file, "webcam" allows user to take snapshot from their webcam, "canvas" defaults to a white image that can be edited and drawn upon with tools.
tool: Tools used for editing. "editor" allows a full screen editor, "select" provides a cropping and zoom tool, "sketch" allows you to create a mask over the image and both the image and mask are passed into the function.
tool: Tools used for editing. "editor" allows a full screen editor (and is the default if source is "upload" or "webcam"), "select" provides a cropping and zoom tool, "sketch" allows you to create a binary sketch (and is the default if source="canvas"), and "color-sketch" allows you to created a sketch in different colors. "color-sketch" can be used with source="upload" or "webcam" to allow sketching on an image. "sketch" can also be used with "upload" or "webcam" to create a mask over an image and in that case both the image and mask are passed into the function as a dictionary with keys "image" and "mask" respectively.
type: The format the image is converted to before being passed into the prediction function. "numpy" converts the image to a numpy array with shape (width, height, 3) and values from 0 to 255, "pil" converts the image to a PIL image object, "file" produces a temporary file object whose path can be retrieved by file_obj.name, "filepath" passes a str path to a temporary file containing the image.
label: component name in interface.
show_label: if True, will display label.
@ -1228,7 +1229,10 @@ class Image(Editable, Clearable, Changeable, Streamable, IOComponent, ImgSeriali
self.image_mode = image_mode
self.source = source
requires_permissions = source == "webcam"
self.tool = tool
if tool is None:
self.tool = "sketch" if source == "canvas" else "editor"
else:
self.tool = tool
self.invert_colors = invert_colors
self.test_input = deepcopy(media_data.BASE64_IMAGE)
self.interpret_by_tokens = True
@ -1279,9 +1283,10 @@ class Image(Editable, Clearable, Changeable, Streamable, IOComponent, ImgSeriali
return IOComponent.add_interactive_to_config(updated_config, interactive)
def _format_image(
self, im: Optional[PIL.Image], fmt: str
self, im: Optional[PIL.Image]
) -> np.array | PIL.Image | str | None:
"""Helper method to format an image based on self.type"""
fmt = im.format
if im is None:
return im
if self.type == "pil":
@ -1314,17 +1319,15 @@ class Image(Editable, Clearable, Changeable, Streamable, IOComponent, ImgSeriali
def preprocess(self, x: str | Dict) -> np.array | PIL.Image | str | None:
"""
Parameters:
x: base64 url data, or (if tool == "sketch) a dict of image and mask base64 url data
x: base64 url data, or (if tool == "sketch") a dict of image and mask base64 url data
Returns:
image in requested format
image in requested format, or (if tool == "sketch") a dict of image and mask in requested format
"""
if x is None:
return x
if self.tool == "sketch":
if self.tool == "sketch" and self.source in ["upload", "webcam"]:
x, mask = x["image"], x["mask"]
im = processing_utils.decode_base64_to_image(x)
fmt = im.format
with warnings.catch_warnings():
warnings.simplefilter("ignore")
im = im.convert(self.image_mode)
@ -1332,18 +1335,21 @@ class Image(Editable, Clearable, Changeable, Streamable, IOComponent, ImgSeriali
im = processing_utils.resize_and_crop(im, self.shape)
if self.invert_colors:
im = PIL.ImageOps.invert(im)
if self.source == "webcam" and self.mirror_webcam is True:
if (
self.source == "webcam"
and self.mirror_webcam is True
and self.tool != "color-sketch"
):
im = PIL.ImageOps.mirror(im)
if not (self.tool == "sketch"):
return self._format_image(im, fmt)
if self.tool == "sketch" and self.source in ["upload", "webcam"]:
mask_im = processing_utils.decode_base64_to_image(mask)
return {
"image": self._format_image(im),
"mask": self._format_image(mask_im),
}
mask_im = processing_utils.decode_base64_to_image(mask)
mask_fmt = mask_im.format
return {
"image": self._format_image(im, fmt),
"mask": self._format_image(mask_im, mask_fmt),
}
return self._format_image(im)
def postprocess(self, y: np.ndarray | PIL.Image | str | Path) -> str:
"""

View File

@ -31,7 +31,7 @@ class Webcam(components.Image):
is_template = True
def __init__(self, **kwargs):
super().__init__(source="webcam", **kwargs)
super().__init__(source="webcam", interactive=True, **kwargs)
class Sketchpad(components.Image):
@ -47,10 +47,48 @@ class Sketchpad(components.Image):
source="canvas",
shape=(28, 28),
invert_colors=True,
interactive=True,
**kwargs
)
class Paint(components.Image):
"""
Sets source="canvas", tool="color-sketch"
"""
is_template = True
def __init__(self, **kwargs):
super().__init__(
source="canvas", tool="color-sketch", interactive=True, **kwargs
)
class ImageMask(components.Image):
"""
Sets source="canvas", tool="sketch"
"""
is_template = True
def __init__(self, **kwargs):
super().__init__(source="upload", tool="sketch", interactive=True, **kwargs)
class ImagePaint(components.Image):
"""
Sets source="upload", tool="color-sketch"
"""
is_template = True
def __init__(self, **kwargs):
super().__init__(
source="upload", tool="color-sketch", interactive=True, **kwargs
)
class Pil(components.Image):
"""
Sets: type="pil"

View File

@ -1 +1 @@
3.3.1
3.4b0

View File

@ -61,13 +61,13 @@ test("can run an api request and display the data", async ({ page }) => {
await page.check("label:has-text('Covid')");
await page.check("label:has-text('Lung Cancer')");
const run_button = await page.locator("button", { hasText: /Run/ });
const run_button = await page.locator("button", { hasText: /Run/ }).first();
await Promise.all([
run_button.click(),
page.waitForResponse("**/api/predict/")
]);
const json = await page.locator("data-testid=json");
const json = await page.locator("data-testid=json").first();
await expect(json).toContainText(`Covid: 0.75, Lung Cancer: 0.25`);
});

View File

@ -0,0 +1,9 @@
<svg width="100%" height="100%" viewBox="0 0 32 32"
><path
d="M28.828 3.172a4.094 4.094 0 0 0-5.656 0L4.05 22.292A6.954 6.954 0 0 0 2 27.242V30h2.756a6.952 6.952 0 0 0 4.95-2.05L28.828 8.829a3.999 3.999 0 0 0 0-5.657zM10.91 18.26l2.829 2.829l-2.122 2.121l-2.828-2.828zm-2.619 8.276A4.966 4.966 0 0 1 4.756 28H4v-.759a4.967 4.967 0 0 1 1.464-3.535l1.91-1.91l2.829 2.828zM27.415 7.414l-12.261 12.26l-2.829-2.828l12.262-12.26a2.047 2.047 0 0 1 2.828 0a2 2 0 0 1 0 2.828z"
fill="currentColor"
/><path
d="M6.5 15a3.5 3.5 0 0 1-2.475-5.974l3.5-3.5a1.502 1.502 0 0 0 0-2.121a1.537 1.537 0 0 0-2.121 0L3.415 5.394L2 3.98l1.99-1.988a3.585 3.585 0 0 1 4.95 0a3.504 3.504 0 0 1 0 4.949L5.439 10.44a1.502 1.502 0 0 0 0 2.121a1.537 1.537 0 0 0 2.122 0l4.024-4.024L13 9.95l-4.025 4.024A3.475 3.475 0 0 1 6.5 15z"
fill="currentColor"
/></svg
>

After

Width:  |  Height:  |  Size: 840 B

View File

@ -1,18 +1,9 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
class="feather feather-bar-chart-2"
><line x1="18" y1="20" x2="18" y2="10" /><line
x1="12"
y1="20"
x2="12"
y2="4"
/><line x1="6" y1="20" x2="6" y2="14" /></svg
<svg width="1em" height="1em" viewBox="0 0 32 32"
><path
d="M28.828 3.172a4.094 4.094 0 0 0-5.656 0L4.05 22.292A6.954 6.954 0 0 0 2 27.242V30h2.756a6.952 6.952 0 0 0 4.95-2.05L28.828 8.829a3.999 3.999 0 0 0 0-5.657zM10.91 18.26l2.829 2.829l-2.122 2.121l-2.828-2.828zm-2.619 8.276A4.966 4.966 0 0 1 4.756 28H4v-.759a4.967 4.967 0 0 1 1.464-3.535l1.91-1.91l2.829 2.828zM27.415 7.414l-12.261 12.26l-2.829-2.828l12.262-12.26a2.047 2.047 0 0 1 2.828 0a2 2 0 0 1 0 2.828z"
fill="currentColor"
/><path
d="M6.5 15a3.5 3.5 0 0 1-2.475-5.974l3.5-3.5a1.502 1.502 0 0 0 0-2.121a1.537 1.537 0 0 0-2.121 0L3.415 5.394L2 3.98l1.99-1.988a3.585 3.585 0 0 1 4.95 0a3.504 3.504 0 0 1 0 4.949L5.439 10.44a1.502 1.502 0 0 0 0 2.121a1.537 1.537 0 0 0 2.122 0l4.024-4.024L13 9.95l-4.025 4.024A3.475 3.475 0 0 1 6.5 15z"
fill="currentColor"
/></svg
>

Before

Width:  |  Height:  |  Size: 369 B

After

Width:  |  Height:  |  Size: 838 B

View File

@ -0,0 +1,16 @@
<svg width="100%" height="100%" viewBox="0 0 32 32"
><circle cx="10" cy="12" r="2" fill="currentColor" /><circle
cx="16"
cy="9"
r="2"
fill="currentColor"
/><circle cx="22" cy="12" r="2" fill="currentColor" /><circle
cx="23"
cy="18"
r="2"
fill="currentColor"
/><circle cx="19" cy="23" r="2" fill="currentColor" /><path
fill="currentColor"
d="M16.54 2A14 14 0 0 0 2 16a4.82 4.82 0 0 0 6.09 4.65l1.12-.31a3 3 0 0 1 3.79 2.9V27a3 3 0 0 0 3 3a14 14 0 0 0 14-14.54A14.05 14.05 0 0 0 16.54 2Zm8.11 22.31A11.93 11.93 0 0 1 16 28a1 1 0 0 1-1-1v-3.76a5 5 0 0 0-5-5a5.07 5.07 0 0 0-1.33.18l-1.12.31A2.82 2.82 0 0 1 4 16A12 12 0 0 1 16.47 4A12.18 12.18 0 0 1 28 15.53a11.89 11.89 0 0 1-3.35 8.79Z"
/></svg
>

After

Width:  |  Height:  |  Size: 720 B

View File

@ -1,22 +1,24 @@
export { default as Clear } from "./Clear.svelte";
export { default as Brush } from "./Brush.svelte";
export { default as Camera } from "./Camera.svelte";
export { default as Chart } from "./Chart.svelte";
export { default as Chat } from "./Chat.svelte";
export { default as Circle } from "./Circle.svelte";
export { default as Clear } from "./Clear.svelte";
export { default as Color } from "./Color.svelte";
export { default as Edit } from "./Edit.svelte";
export { default as File } from "./File.svelte";
export { default as Image } from "./Image.svelte";
export { default as JSON } from "./JSON.svelte";
export { default as LineChart } from "./LineChart.svelte";
export { default as Maximise } from "./Maximise.svelte";
export { default as Music } from "./Music.svelte";
export { default as Pause } from "./Pause.svelte";
export { default as Play } from "./Play.svelte";
export { default as Plot } from "./Plot.svelte";
export { default as Sketch } from "./Sketch.svelte";
export { default as Square } from "./Square.svelte";
export { default as Table } from "./Table.svelte";
export { default as TextHighlight } from "./TextHighlight.svelte";
export { default as Tree } from "./Tree.svelte";
export { default as Undo } from "./Undo.svelte";
export { default as Video } from "./Video.svelte";
export { default as Image } from "./Image.svelte";
export { default as Chart } from "./Chart.svelte";
export { default as Music } from "./Music.svelte";
export { default as File } from "./File.svelte";
export { default as LineChart } from "./LineChart.svelte";
export { default as TextHighlight } from "./TextHighlight.svelte";
export { default as JSON } from "./JSON.svelte";
export { default as Tree } from "./Tree.svelte";
export { default as Chat } from "./Chat.svelte";
export { default as Plot } from "./Plot.svelte";
export { default as Play } from "./Play.svelte";
export { default as Pause } from "./Pause.svelte";
export { default as Maximise } from "./Maximise.svelte";

View File

@ -11,6 +11,7 @@
"@gradio/atoms": "workspace:^0.0.1",
"@gradio/icons": "workspace:^0.0.1",
"@gradio/upload": "workspace:^0.0.1",
"@gradio/utils": "workspace:^0.0.1",
"cropperjs": "^1.5.12",
"lazy-brush": "^1.0.1",
"resize-observer-polyfill": "^1.5.1"

View File

@ -8,6 +8,7 @@
import Sketch from "./Sketch.svelte";
import Webcam from "./Webcam.svelte";
import ModifySketch from "./ModifySketch.svelte";
import SketchSettings from "./SketchSettings.svelte";
import { Upload, ModifyUpload } from "@gradio/upload";
@ -39,22 +40,43 @@
}
function handle_upload({ detail }: CustomEvent<string>) {
value =
(source === "upload" || source === "webcam") && tool === "sketch"
? { image: detail, mask: null }
: detail;
if (tool === "color-sketch") {
static_image = detail;
} else {
value =
(source === "upload" || source === "webcam") && tool === "sketch"
? { image: detail, mask: null }
: detail;
}
}
function handle_clear({ detail }: CustomEvent<null>) {
value = null;
static_image = undefined;
dispatch("clear");
}
async function handle_save({ detail }: { detail: string }) {
value =
(source === "upload" || source === "webcam") && tool === "sketch"
? { image: detail, mask: null }
: detail;
async function handle_save({ detail }: { detail: string }, initial) {
if (mode === "mask") {
if (source === "webcam" && initial) {
value = {
image: detail,
mask: null
};
} else {
value = {
image: typeof value === "string" ? value : value?.image || null,
mask: detail
};
}
} else if (
(source === "upload" || source === "webcam") &&
tool === "sketch"
) {
value = { image: detail, mask: null };
} else {
value = detail;
}
await tick();
@ -79,23 +101,52 @@
const element = event.composedPath()[0] as HTMLImageElement;
img_width = element.naturalWidth;
img_height = element.naturalHeight;
}
function handle_mask_save({ detail }: { detail: string }) {
value = {
image: typeof value === "string" ? value : value?.image || null,
mask: detail
};
container_height = element.getBoundingClientRect().height;
}
async function handle_mask_clear() {
sketch.clear();
await tick();
value = null;
static_image = undefined;
}
let img_height = 0;
let img_width = 0;
let container_height = 0;
let brush_radius = 20;
let mode;
$: {
if (source === "canvas" && tool === "sketch") {
mode = "bw-sketch";
} else if (tool === "color-sketch") {
mode = "color-sketch";
} else if (
(source === "upload" || source === "webcam") &&
tool === "sketch"
) {
mode = "mask";
} else {
mode = "editor";
}
}
$: brush_color = mode == "mask" ? "#000000" : "#000";
let value_img;
let max_height;
let max_width;
let static_image = undefined;
$: {
if (value === null || (value.image === null && value.mask === null)) {
static_image = undefined;
}
}
</script>
<BlockLabel
@ -106,16 +157,39 @@
<div
class:bg-gray-200={value}
class:h-60={source !== "webcam" || tool === "sketch"}
class:h-60={source !== "webcam" ||
tool === "sketch" ||
tool === "color-sketch"}
data-testid="image"
bind:offsetHeight={max_height}
bind:offsetWidth={max_width}
>
{#if source === "canvas"}
<ModifySketch
on:undo={() => sketch.undo()}
on:clear={() => sketch.clear()}
/>
<Sketch {value} bind:this={sketch} on:change={handle_save} />
{:else if value === null || streaming}
{#if tool === "color-sketch"}
<SketchSettings
bind:brush_radius
bind:brush_color
container_height={container_height || max_height}
img_width={img_width || max_width}
img_height={img_height || max_height}
/>
{/if}
<Sketch
{value}
bind:brush_radius
bind:brush_color
bind:this={sketch}
on:change={handle_save}
{mode}
width={img_width || max_width}
height={img_height || max_height}
container_height={container_height || max_height}
/>
{:else if (value === null && !static_image) || streaming}
{#if source === "upload"}
<Upload
bind:dragging
@ -129,9 +203,10 @@
{upload_text}
</div>
</Upload>
{:else if source === "webcam"}
{:else if source === "webcam" && !static_image}
<Webcam
on:capture={handle_save}
on:capture={(e) =>
tool === "color-sketch" ? handle_upload(e) : handle_save(e, true)}
on:stream={handle_save}
{streaming}
{pending}
@ -154,34 +229,50 @@
alt=""
class:scale-x-[-1]={source === "webcam" && mirror_webcam}
/>
{:else if tool === "sketch" && value !== null}
<img
class="absolute w-full h-full object-contain"
src={value.image}
alt=""
on:load={handle_image_load}
class:scale-x-[-1]={source === "webcam" && mirror_webcam}
/>
{:else if (tool === "sketch" || tool === "color-sketch") && (value !== null || static_image)}
{#key static_image}
<img
bind:this={value_img}
class="absolute w-full h-full object-contain"
src={static_image || value?.image || value}
alt=""
on:load={handle_image_load}
class:scale-x-[-1]={source === "webcam" && mirror_webcam}
/>
{/key}
{#if img_width > 0}
<Sketch
{value}
bind:this={sketch}
brush_radius={25}
brush_color="rgba(255, 255, 255, 0.65)"
on:change={handle_mask_save}
mode="mask"
width={img_width}
height={img_height}
bind:brush_radius
bind:brush_color
on:change={handle_save}
{mode}
width={img_width || max_width}
height={img_height || max_height}
container_height={container_height || max_height}
{value_img}
{source}
/>
<ModifySketch
on:undo={() => sketch.undo()}
on:clear={handle_mask_clear}
/>
{#if tool === "color-sketch" || tool === "sketch"}
<SketchSettings
bind:brush_radius
bind:brush_color
container_height={container_height || max_height}
img_width={img_width || max_width}
img_height={img_height || max_height}
{mode}
/>
{/if}
{/if}
{:else}
<img
class="w-full h-full object-contain"
src={value}
src={value.image || value}
alt=""
class:scale-x-[-1]={source === "webcam" && mirror_webcam}
/>

View File

@ -1,7 +1,7 @@
<script>
// @ts-nocheck
import { onMount, onDestroy, createEventDispatcher } from "svelte";
import { onMount, onDestroy, createEventDispatcher, tick } from "svelte";
import { fade } from "svelte/transition";
import { LazyBrush } from "lazy-brush/src";
import ResizeObserver from "resize-observer-polyfill";
@ -9,19 +9,22 @@
const dispatch = createEventDispatcher();
export let value;
export let value_img;
export let mode = "sketch";
export let brush_color = "#0b0f19";
export let brush_radius = 50;
export let source;
export let width = undefined;
export let height = undefined;
export let width = 400;
export let height = 200;
export let container_height = 200;
let mounted;
let catenary_color = "#aaa";
let canvas_width = width || 400;
let canvas_height = height || 400;
let canvas_width = width;
let canvas_height = height;
$: mounted && !value && clear();
@ -64,59 +67,85 @@
let is_drawing = false;
let is_pressing = false;
let lazy = null;
let chain_length = null;
let canvas_container = null;
let canvas_observer = null;
let save_data = "";
let line_count = 0;
let display_natural_ratio = 1;
function calculate_ratio() {
const x = canvas.interface.getBoundingClientRect();
display_natural_ratio = width / x.width;
}
onMount(() => {
onMount(async () => {
Object.keys(canvas).forEach((key) => {
ctx[key] = canvas[key].getContext("2d");
});
await tick();
if (value_img) {
value_img.addEventListener("load", (_) => {
if (source === "webcam") {
ctx.temp.save();
ctx.temp.translate(width, 0);
ctx.temp.scale(-1, 1);
ctx.temp.drawImage(value_img, 0, 0);
ctx.temp.restore();
} else {
ctx.temp.drawImage(value_img, 0, 0);
}
ctx.drawing.drawImage(canvas.temp, 0, 0, width, height);
trigger_on_change();
});
setTimeout(() => {
if (source === "webcam") {
ctx.temp.save();
ctx.temp.translate(width, 0);
ctx.temp.scale(-1, 1);
ctx.temp.drawImage(value_img, 0, 0);
ctx.temp.restore();
} else {
ctx.temp.drawImage(value_img, 0, 0);
}
ctx.drawing.drawImage(canvas.temp, 0, 0, width, height);
draw_lines({ lines: lines.slice() });
trigger_on_change();
}, 100);
}
lazy = new LazyBrush({
radius: brush_radius / 1.5,
radius: brush_radius * 0.05,
enabled: true,
initialPoint: {
x: window.innerWidth / 2,
y: window.innerHeight / 2
x: width / 2,
y: height / 2
}
});
chain_length = brush_radius;
canvas_observer = new ResizeObserver((entries, observer) => {
canvas_observer = new ResizeObserver((entries, observer, ...rest) => {
handle_canvas_resize(entries, observer);
calculate_ratio();
});
canvas_observer.observe(canvas_container);
loop();
mounted = true;
window.setTimeout(() => {
const initX = window.innerWidth / 2;
const initY = window.innerHeight / 2;
lazy.update({ x: initX - chain_length / 4, y: initY }, { both: true });
lazy.update({ x: initX + chain_length / 4, y: initY }, { both: false });
mouse_has_moved = true;
values_changed = true;
clear();
if (save_data) {
load_save_data(save_data);
}
}, 100);
calculate_ratio();
requestAnimationFrame(() => {
init();
requestAnimationFrame(() => {
clear();
});
});
});
function init() {
const initX = width / 2;
const initY = height / 2;
lazy.update({ x: initX, y: initY }, { both: true });
lazy.update({ x: initX, y: initY }, { both: false });
mouse_has_moved = true;
values_changed = true;
}
onDestroy(() => {
mounted = false;
canvas_observer.unobserve(canvas_container);
@ -124,9 +153,32 @@
export function undo() {
const _lines = lines.slice(0, -1);
clear();
clear_canvas();
if (value_img) {
if (source === "webcam") {
ctx.temp.save();
ctx.temp.translate(width, 0);
ctx.temp.scale(-1, 1);
ctx.temp.drawImage(value_img, 0, 0);
ctx.temp.restore();
} else {
ctx.temp.drawImage(value_img, 0, 0);
}
if (!lines || !lines.length) {
ctx.drawing.drawImage(canvas.temp, 0, 0, width, height);
}
}
draw_lines({ lines: _lines });
line_count = lines.length;
line_count = _lines.length;
if (lines.length) {
lines = _lines;
}
trigger_on_change();
}
@ -138,39 +190,9 @@
});
};
let load_save_data = (save_data) => {
if (typeof save_data !== "string") {
throw new Error("save_data needs to be of type string!");
}
const { lines, width, height } = JSON.parse(save_data);
if (!lines || typeof lines.push !== "function") {
throw new Error("save_data.lines needs to be an array!");
}
clear();
if (width === canvas_width && height === canvas_height) {
draw_lines({
lines
});
} else {
const scaleX = canvas_width / width;
const scaleY = canvas_height / height;
draw_lines({
lines: lines.map((line) => ({
...line,
points: line.points.map((p) => ({
x: p.x * scaleX,
y: p.y * scaleY
})),
brush_radius: line.brush_radius
}))
});
}
};
let draw_lines = ({ lines }) => {
lines.forEach((line) => {
const { points: _points, brush_color, brush_radius } = line;
draw_points({
points: _points,
brush_color,
@ -179,19 +201,20 @@
if (mode === "mask") {
draw_fake_points({
points: points,
points: _points,
brush_color,
brush_radius
});
}
points = _points;
saveLine({ brush_color, brush_radius });
if (mode === "mask") {
save_mask_line();
}
return;
});
saveLine({ brush_color, brush_radius });
if (mode === "mask") {
save_mask_line();
}
};
let handle_draw_start = (e) => {
@ -223,29 +246,72 @@
}
};
let handle_canvas_resize = (entries) => {
const save_data = get_save_data();
for (const entry of entries) {
const { width, height } = entry.contentRect;
set_canvas_size(canvas.interface, width, height);
set_canvas_size(canvas.drawing, width, height);
set_canvas_size(canvas.temp, width, height);
set_canvas_size(canvas.temp_fake, width, height);
set_canvas_size(canvas.mask, width, height);
let old_width = 0;
let old_height = 0;
let old_container_height = 0;
loop({ once: true });
let handle_canvas_resize = async () => {
if (
width === old_width &&
height === old_height &&
old_container_height === container_height
) {
return;
}
load_save_data(save_data, true);
const dimensions = { width: width, height: height };
const container_dimensions = {
height: container_height,
width: container_height * (dimensions.width / dimensions.height)
};
await Promise.all([
set_canvas_size(canvas.interface, dimensions, container_dimensions),
set_canvas_size(canvas.drawing, dimensions, container_dimensions),
set_canvas_size(canvas.temp, dimensions, container_dimensions),
set_canvas_size(canvas.temp_fake, dimensions, container_dimensions),
set_canvas_size(canvas.mask, dimensions, container_dimensions, false)
]);
brush_radius = 20 * (dimensions.width / container_dimensions.width);
loop({ once: true });
setTimeout(() => {
old_height = height;
old_width = width;
old_container_height = container_height;
}, 100);
clear();
};
let set_canvas_size = (canvas, _width, _height) => {
canvas.width = width || _width * 3;
canvas.height = height || _height * 3;
if (mode === "sketch") {
$: {
if (lazy) {
init();
lazy.setRadius(brush_radius * 0.05);
}
}
canvas.style.width = mode === "mask" ? "auto" : _width;
canvas.style.height = mode === "mask" ? "100%" : _height;
$: {
if (width || height) {
handle_canvas_resize();
}
}
let set_canvas_size = async (canvas, dimensions, container, scale = true) => {
if (!mounted) return;
await tick();
const dpr = window.devicePixelRatio || 1;
canvas.width = dimensions.width * (scale ? dpr : 1);
canvas.height = dimensions.height * (scale ? dpr : 1);
const ctx = canvas.getContext("2d");
scale && ctx.scale(dpr, dpr);
canvas.style.width = `${container.width}px`;
canvas.style.height = `${container.height}px`;
};
let get_pointer_pos = (e) => {
@ -257,18 +323,15 @@
clientX = e.changedTouches[0].clientX;
clientY = e.changedTouches[0].clientY;
}
return {
x: clientX - rect.left,
y: clientY - rect.top
x: ((clientX - rect.left) / rect.width) * width,
y: ((clientY - rect.top) / rect.height) * height
};
};
let handle_pointer_move = (x, y) => {
lazy.update(
mode === "sketch"
? { x: x * 3, y: y * 3 }
: { x: x * display_natural_ratio, y: y * display_natural_ratio }
);
lazy.update({ x: x, y: y });
const is_disabled = !lazy.isEnabled();
if ((is_pressing && !is_drawing) || (is_disabled && is_pressing)) {
is_drawing = true;
@ -276,7 +339,6 @@
}
if (is_drawing) {
points.push(lazy.brush.toObject());
draw_points({
points: points,
brush_color,
@ -295,11 +357,13 @@
};
let draw_points = ({ points, brush_color, brush_radius }) => {
if (!points || points.length < 2) return;
ctx.temp.lineJoin = "round";
ctx.temp.lineCap = "round";
ctx.temp.strokeStyle = brush_color;
ctx.temp.clearRect(0, 0, ctx.temp.canvas.width, ctx.temp.canvas.height);
ctx.temp.lineWidth = brush_radius;
if (!points || points.length < 2) return;
let p1 = points[0];
let p2 = points[1];
ctx.temp.moveTo(p2.x, p2.y);
@ -316,15 +380,12 @@
};
let draw_fake_points = ({ points, brush_color, brush_radius }) => {
if (!points || points.length < 2) return;
ctx.temp_fake.lineJoin = "round";
ctx.temp_fake.lineCap = "round";
ctx.temp_fake.strokeStyle = "#fff";
ctx.temp_fake.clearRect(
0,
0,
ctx.temp.canvas.width,
ctx.temp.canvas.height
);
// ctx.temp_fake.clearRect(0, 0, width, height);
ctx.temp_fake.lineWidth = brush_radius;
let p1 = points[0];
let p2 = points[1];
@ -342,52 +403,51 @@
};
let save_mask_line = () => {
// if (points.length < 2) return;
lines.push({
points: [...points],
brush_color: "#fff",
brush_radius
});
if (points.length < 1) return;
points.length = 0;
const width = canvas.temp_fake.width;
const height = canvas.temp_fake.height;
ctx.mask.drawImage(canvas.temp_fake, 0, 0, width, height);
ctx.temp_fake.clearRect(0, 0, width, height);
trigger_on_change();
};
let saveLine = () => {
if (points.length < 2) return;
if (points.length < 1) return;
lines.push({
points: [...points],
points: points.slice(),
brush_color: brush_color,
brush_radius
});
points.length = 0;
const width = canvas.temp.width;
const height = canvas.temp.height;
if (mode !== "mask") {
points.length = 0;
}
ctx.drawing.drawImage(canvas.temp, 0, 0, width, height);
ctx.temp.clearRect(0, 0, width, height);
trigger_on_change();
};
let trigger_on_change = () => {
dispatch("change", get_image_data());
const x = get_image_data();
dispatch("change", x);
};
export function clear() {
lines = [];
values_changed = true;
ctx.drawing.clearRect(0, 0, canvas.drawing.width, canvas.drawing.height);
ctx.temp.clearRect(0, 0, canvas.temp.width, canvas.temp.height);
clear_canvas();
line_count = 0;
return true;
}
function clear_canvas() {
values_changed = true;
ctx.temp.clearRect(0, 0, width, height);
ctx.temp.fillStyle = mode === "mask" ? "transparent" : "#FFFFFF";
ctx.temp.fillRect(0, 0, width, height);
ctx.drawing.fillStyle = mode === "sketch" ? "#FFFFFF" : "transparent";
ctx.drawing.fillRect(0, 0, canvas.drawing.width, canvas.drawing.height);
if (mode === "mask") {
ctx.temp_fake.clearRect(
0,
@ -395,12 +455,10 @@
canvas.temp_fake.width,
canvas.temp_fake.height
);
ctx.mask.clearRect(0, 0, canvas.temp_fake.width, canvas.temp_fake.height);
ctx.mask.clearRect(0, 0, width, height);
ctx.mask.fillStyle = "#000";
ctx.mask.fillRect(0, 0, canvas.mask.width, canvas.mask.height);
ctx.mask.fillRect(0, 0, width, height);
}
line_count = 0;
}
let loop = ({ once = false } = {}) => {
@ -418,8 +476,10 @@
}
};
$: brush_dot = brush_radius * 0.075;
let draw_interface = (ctx, pointer, brush) => {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.clearRect(0, 0, width, height);
// brush preview
ctx.beginPath();
@ -427,33 +487,17 @@
ctx.arc(brush.x, brush.y, brush_radius / 2, 0, Math.PI * 2, true);
ctx.fill();
// mouse point dangler
ctx.beginPath();
ctx.fillStyle = catenary_color;
ctx.arc(pointer.x, pointer.y, 4, 0, Math.PI * 2, true);
ctx.fill();
// catenary
if (lazy.isEnabled()) {
ctx.beginPath();
ctx.lineWidth = 2;
ctx.lineCap = "round";
ctx.setLineDash([2, 4]);
ctx.strokeStyle = catenary_color;
ctx.stroke();
}
// tiny brush point dot
ctx.beginPath();
ctx.fillStyle = catenary_color;
ctx.arc(brush.x, brush.y, 2, 0, Math.PI * 2, true);
ctx.arc(brush.x, brush.y, brush_dot, 0, Math.PI * 2, true);
ctx.fill();
};
export function get_image_data() {
return mode === "mask"
? canvas.mask.toDataURL("image/png")
: canvas.drawing.toDataURL("image/png");
? canvas.mask.toDataURL("image/jpg")
: canvas.drawing.toDataURL("image/jpg");
}
</script>
@ -474,11 +518,8 @@
{#each canvas_types as { name, zIndex }}
<canvas
key={name}
class="inset-0 m-auto"
style=" display:block;position:absolute; z-index:{zIndex}; width: {mode ===
'sketch'
? canvas_width
: width}px; height: {mode === 'sketch' ? canvas_height : height}px"
class="inset-0 m-auto hover:cursor-none"
style=" display:block;position:absolute; z-index:{zIndex};"
bind:this={canvas[name]}
on:mousedown={name === "interface" ? handle_draw_start : undefined}
on:mousemove={name === "interface" ? handle_draw_move : undefined}

View File

@ -0,0 +1,49 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { IconButton } from "@gradio/atoms";
import { Brush, Color } from "@gradio/icons";
const dispatch = createEventDispatcher();
let show_size = false;
let show_col = false;
export let brush_radius = 20;
export let brush_color = "#000";
export let container_height: number;
export let img_width: number;
export let img_height: number;
export let mode: "mask" | "other" = "other";
$: width = container_height * (img_width / img_height);
</script>
<div class="z-50 top-10 right-2 justify-end flex gap-1 absolute">
<!-- <IconButton Icon={Undo} on:click={() => dispatch("undo")} /> -->
<span class="absolute top-0 right-0">
<IconButton Icon={Brush} on:click={() => (show_size = !show_size)} />
{#if show_size}
<input
bind:value={brush_radius}
class="absolute top-[2px] right-6"
type="range"
min={0.5 * (img_width / width)}
max={75 * (img_width / width)}
/>
{/if}
</span>
{#if mode !== "mask"}
<span class="absolute top-6 right-0">
<IconButton Icon={Color} on:click={() => (show_col = !show_col)} />
{#if show_col}
<input
bind:value={brush_color}
class="absolute top-[-3px] right-6"
type="color"
/>
{/if}
</span>
{/if}
</div>

View File

@ -20,13 +20,12 @@
$: $selected_tab === id && tick().then(() => dispatch("select"));
</script>
{#if $selected_tab === id}
<div
id={elem_id}
class="tabitem p-2 border-2 border-t-0 border-gray-200 relative flex"
>
<Column>
<slot />
</Column>
</div>
{/if}
<div
id={elem_id}
class="tabitem p-2 border-2 border-t-0 border-gray-200 relative flex"
style:display={$selected_tab === id ? "block" : "none"}
>
<Column>
<slot />
</Column>
</div>

View File

@ -1,2 +1,3 @@
export * from "./color";
export * from "./styles";
export * from "./utils";

View File

@ -0,0 +1,9 @@
export const debounce = (callback: Function, wait = 250) => {
let timeout: NodeJS.Timeout | null = null;
return (...args: Array<unknown>) => {
const next = () => callback(...args);
if (timeout) clearTimeout(timeout);
timeout = setTimeout(next, wait);
};
};

View File

@ -232,6 +232,7 @@ importers:
'@gradio/atoms': workspace:^0.0.1
'@gradio/icons': workspace:^0.0.1
'@gradio/upload': workspace:^0.0.1
'@gradio/utils': workspace:^0.0.1
cropperjs: ^1.5.12
lazy-brush: ^1.0.1
resize-observer-polyfill: ^1.5.1
@ -239,6 +240,7 @@ importers:
'@gradio/atoms': link:../atoms
'@gradio/icons': link:../icons
'@gradio/upload': link:../upload
'@gradio/utils': link:../utils
cropperjs: 1.5.12
lazy-brush: 1.0.1
resize-observer-polyfill: 1.5.1
@ -387,7 +389,7 @@ importers:
'@gradio/upload': link:../upload
'@gradio/video': link:../video
devDependencies:
'@sveltejs/adapter-auto': 1.0.0-next.70
'@sveltejs/adapter-auto': 1.0.0-next.75
'@sveltejs/kit': 1.0.0-next.318_svelte@3.49.0
autoprefixer: 10.4.2_postcss@8.4.6
postcss: 8.4.6
@ -437,8 +439,8 @@ packages:
resolution: {integrity: sha512-B1/plF62pt+H2IJHvApK8fdOJAVsvojvacuac8x8s+JIyqbropMyqNqHTKLm3YD8ZFLGwYeFTudU+PQ7vGvBdA==}
dev: true
/@esbuild/linux-loong64/0.14.53:
resolution: {integrity: sha512-W2dAL6Bnyn4xa/QRSU3ilIK4EzD5wgYXKXJiS1HDF5vU3675qc2bvFyLwbUcdmssDveyndy7FbitrCoiV/eMLg==}
/@esbuild/linux-loong64/0.15.7:
resolution: {integrity: sha512-IKznSJOsVUuyt7cDzzSZyqBEcZe+7WlBqTVXiF1OXP/4Nm387ToaXZ0fyLwI1iBlI/bzpxVq411QE2/Bt2XWWw==}
engines: {node: '>=12'}
cpu: [loong64]
os: [linux]
@ -567,39 +569,38 @@ packages:
estree-walker: 2.0.2
picomatch: 2.3.1
/@sveltejs/adapter-auto/1.0.0-next.70:
resolution: {integrity: sha512-FJlDO6oUqbuFJjQoguGb4gdBj3iCSM3evFXkBpQ7hvwu3y2gKbcdzsxdn9tZ5LzkHh79CeJcwiszXFQ8usKk/A==}
/@sveltejs/adapter-auto/1.0.0-next.75:
resolution: {integrity: sha512-UEE6XkeXVrNhpEceqcCbtfV5EYzulIt1D/L+RsjIVsPVtUIZMMpPWzuHHzVvPemFRAuYho+4C1hJjIJ9iCgPeQ==}
dependencies:
'@sveltejs/adapter-cloudflare': 1.0.0-next.32
'@sveltejs/adapter-netlify': 1.0.0-next.75
'@sveltejs/adapter-vercel': 1.0.0-next.72
'@sveltejs/adapter-cloudflare': 1.0.0-next.34
'@sveltejs/adapter-netlify': 1.0.0-next.78
'@sveltejs/adapter-vercel': 1.0.0-next.76
transitivePeerDependencies:
- encoding
- supports-color
dev: true
/@sveltejs/adapter-cloudflare/1.0.0-next.32:
resolution: {integrity: sha512-tzkUsdQlBk9xUjcGUOBYos4HKaeaXvz9v4TQ1QS2yIHEtL5xvMEDPZ94/DB2gPL4LZCnYbdY2lsy5HCsoN0hkQ==}
/@sveltejs/adapter-cloudflare/1.0.0-next.34:
resolution: {integrity: sha512-9/YJsx5O+iy2+XGuH0vVzZ9OSeHGjkInh8JG8CLmIc0cKkv2t7sEu7qQ/qXA5CcvmS1AqNSUgIMxGoeEDVlO3g==}
dependencies:
'@cloudflare/workers-types': 3.14.1
esbuild: 0.14.53
esbuild: 0.15.7
worktop: 0.8.0-next.14
dev: true
/@sveltejs/adapter-netlify/1.0.0-next.75:
resolution: {integrity: sha512-1zTR/U/ceEAyqIGJ74v54G+JbIR+fSmTN9qfqGOM0gBwVoBVRUujGm4tDFJQNYzvuGzVnC7br/rhYMLZd2JluQ==}
/@sveltejs/adapter-netlify/1.0.0-next.78:
resolution: {integrity: sha512-Yyn/j/0QcLK3Db442ducLUZmyvkO74j7Gdcwu9xN0fQN3kBlCJP9Itx5o4SySrPFGc4Q8cLJ5ELNg+mWduLBAA==}
dependencies:
'@iarna/toml': 2.2.5
esbuild: 0.14.53
esbuild: 0.15.7
set-cookie-parser: 2.4.8
tiny-glob: 0.2.9
dev: true
/@sveltejs/adapter-vercel/1.0.0-next.72:
resolution: {integrity: sha512-oNs8FQaYC2NnwDcvX/jc9MDNqXc9HxwGPQNkd+1vBpFVWZl9mShQgCcOMzfTOIH0ka984jYNa0ZawYYHex79xg==}
/@sveltejs/adapter-vercel/1.0.0-next.76:
resolution: {integrity: sha512-Od9DBfeMwWC/sZNeCJw4TYVE3LMR8lGJivSdkXWgpvksgG+QizLyzTfvBacapId3wcu+7X4PPTLoH00o5iQGEQ==}
dependencies:
'@vercel/nft': 0.22.0
esbuild: 0.14.53
esbuild: 0.15.7
transitivePeerDependencies:
- encoding
- supports-color
@ -1531,8 +1532,8 @@ packages:
requiresBuild: true
optional: true
/esbuild-android-64/0.14.53:
resolution: {integrity: sha512-fIL93sOTnEU+NrTAVMIKiAw0YH22HWCAgg4N4Z6zov2t0kY9RAJ50zY9ZMCQ+RT6bnOfDt8gCTnt/RaSNA2yRA==}
/esbuild-android-64/0.15.7:
resolution: {integrity: sha512-p7rCvdsldhxQr3YHxptf1Jcd86dlhvc3EQmQJaZzzuAxefO9PvcI0GLOa5nCWem1AJ8iMRu9w0r5TG8pHmbi9w==}
engines: {node: '>=12'}
cpu: [x64]
os: [android]
@ -1548,8 +1549,8 @@ packages:
requiresBuild: true
optional: true
/esbuild-android-arm64/0.14.53:
resolution: {integrity: sha512-PC7KaF1v0h/nWpvlU1UMN7dzB54cBH8qSsm7S9mkwFA1BXpaEOufCg8hdoEI1jep0KeO/rjZVWrsH8+q28T77A==}
/esbuild-android-arm64/0.15.7:
resolution: {integrity: sha512-L775l9ynJT7rVqRM5vo+9w5g2ysbOCfsdLV4CWanTZ1k/9Jb3IYlQ06VCI1edhcosTYJRECQFJa3eAvkx72eyQ==}
engines: {node: '>=12'}
cpu: [arm64]
os: [android]
@ -1565,8 +1566,8 @@ packages:
requiresBuild: true
optional: true
/esbuild-darwin-64/0.14.53:
resolution: {integrity: sha512-gE7P5wlnkX4d4PKvLBUgmhZXvL7lzGRLri17/+CmmCzfncIgq8lOBvxGMiQ4xazplhxq+72TEohyFMZLFxuWvg==}
/esbuild-darwin-64/0.15.7:
resolution: {integrity: sha512-KGPt3r1c9ww009t2xLB6Vk0YyNOXh7hbjZ3EecHoVDxgtbUlYstMPDaReimKe6eOEfyY4hBEEeTvKwPsiH5WZg==}
engines: {node: '>=12'}
cpu: [x64]
os: [darwin]
@ -1582,8 +1583,8 @@ packages:
requiresBuild: true
optional: true
/esbuild-darwin-arm64/0.14.53:
resolution: {integrity: sha512-otJwDU3hnI15Q98PX4MJbknSZ/WSR1I45il7gcxcECXzfN4Mrpft5hBDHXNRnCh+5858uPXBXA1Vaz2jVWLaIA==}
/esbuild-darwin-arm64/0.15.7:
resolution: {integrity: sha512-kBIHvtVqbSGajN88lYMnR3aIleH3ABZLLFLxwL2stiuIGAjGlQW741NxVTpUHQXUmPzxi6POqc9npkXa8AcSZQ==}
engines: {node: '>=12'}
cpu: [arm64]
os: [darwin]
@ -1599,8 +1600,8 @@ packages:
requiresBuild: true
optional: true
/esbuild-freebsd-64/0.14.53:
resolution: {integrity: sha512-WkdJa8iyrGHyKiPF4lk0MiOF87Q2SkE+i+8D4Cazq3/iqmGPJ6u49je300MFi5I2eUsQCkaOWhpCVQMTKGww2w==}
/esbuild-freebsd-64/0.15.7:
resolution: {integrity: sha512-hESZB91qDLV5MEwNxzMxPfbjAhOmtfsr9Wnuci7pY6TtEh4UDuevmGmkUIjX/b+e/k4tcNBMf7SRQ2mdNuK/HQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [freebsd]
@ -1616,8 +1617,8 @@ packages:
requiresBuild: true
optional: true
/esbuild-freebsd-arm64/0.14.53:
resolution: {integrity: sha512-9T7WwCuV30NAx0SyQpw8edbKvbKELnnm1FHg7gbSYaatH+c8WJW10g/OdM7JYnv7qkimw2ZTtSA+NokOLd2ydQ==}
/esbuild-freebsd-arm64/0.15.7:
resolution: {integrity: sha512-dLFR0ChH5t+b3J8w0fVKGvtwSLWCv7GYT2Y2jFGulF1L5HftQLzVGN+6pi1SivuiVSmTh28FwUhi9PwQicXI6Q==}
engines: {node: '>=12'}
cpu: [arm64]
os: [freebsd]
@ -1633,8 +1634,8 @@ packages:
requiresBuild: true
optional: true
/esbuild-linux-32/0.14.53:
resolution: {integrity: sha512-VGanLBg5en2LfGDgLEUxQko2lqsOS7MTEWUi8x91YmsHNyzJVT/WApbFFx3MQGhkf+XdimVhpyo5/G0PBY91zg==}
/esbuild-linux-32/0.15.7:
resolution: {integrity: sha512-v3gT/LsONGUZcjbt2swrMjwxo32NJzk+7sAgtxhGx1+ZmOFaTRXBAi1PPfgpeo/J//Un2jIKm/I+qqeo4caJvg==}
engines: {node: '>=12'}
cpu: [ia32]
os: [linux]
@ -1650,8 +1651,8 @@ packages:
requiresBuild: true
optional: true
/esbuild-linux-64/0.14.53:
resolution: {integrity: sha512-pP/FA55j/fzAV7N9DF31meAyjOH6Bjuo3aSKPh26+RW85ZEtbJv9nhoxmGTd9FOqjx59Tc1ZbrJabuiXlMwuZQ==}
/esbuild-linux-64/0.15.7:
resolution: {integrity: sha512-LxXEfLAKwOVmm1yecpMmWERBshl+Kv5YJ/1KnyAr6HRHFW8cxOEsEfisD3sVl/RvHyW//lhYUVSuy9jGEfIRAQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [linux]
@ -1667,8 +1668,8 @@ packages:
requiresBuild: true
optional: true
/esbuild-linux-arm/0.14.53:
resolution: {integrity: sha512-/u81NGAVZMopbmzd21Nu/wvnKQK3pT4CrvQ8BTje1STXcQAGnfyKgQlj3m0j2BzYbvQxSy+TMck4TNV2onvoPA==}
/esbuild-linux-arm/0.15.7:
resolution: {integrity: sha512-JKgAHtMR5f75wJTeuNQbyznZZa+pjiUHV7sRZp42UNdyXC6TiUYMW/8z8yIBAr2Fpad8hM1royZKQisqPABPvQ==}
engines: {node: '>=12'}
cpu: [arm]
os: [linux]
@ -1684,8 +1685,8 @@ packages:
requiresBuild: true
optional: true
/esbuild-linux-arm64/0.14.53:
resolution: {integrity: sha512-GDmWITT+PMsjCA6/lByYk7NyFssW4Q6in32iPkpjZ/ytSyH+xeEx8q7HG3AhWH6heemEYEWpTll/eui3jwlSnw==}
/esbuild-linux-arm64/0.15.7:
resolution: {integrity: sha512-P3cfhudpzWDkglutWgXcT2S7Ft7o2e3YDMrP1n0z2dlbUZghUkKCyaWw0zhp4KxEEzt/E7lmrtRu/pGWnwb9vw==}
engines: {node: '>=12'}
cpu: [arm64]
os: [linux]
@ -1701,8 +1702,8 @@ packages:
requiresBuild: true
optional: true
/esbuild-linux-mips64le/0.14.53:
resolution: {integrity: sha512-d6/XHIQW714gSSp6tOOX2UscedVobELvQlPMkInhx1NPz4ThZI9uNLQ4qQJHGBGKGfu+rtJsxM4NVHLhnNRdWQ==}
/esbuild-linux-mips64le/0.15.7:
resolution: {integrity: sha512-T7XKuxl0VpeFLCJXub6U+iybiqh0kM/bWOTb4qcPyDDwNVhLUiPcGdG2/0S7F93czUZOKP57YiLV8YQewgLHKw==}
engines: {node: '>=12'}
cpu: [mips64el]
os: [linux]
@ -1718,8 +1719,8 @@ packages:
requiresBuild: true
optional: true
/esbuild-linux-ppc64le/0.14.53:
resolution: {integrity: sha512-ndnJmniKPCB52m+r6BtHHLAOXw+xBCWIxNnedbIpuREOcbSU/AlyM/2dA3BmUQhsHdb4w3amD5U2s91TJ3MzzA==}
/esbuild-linux-ppc64le/0.15.7:
resolution: {integrity: sha512-6mGuC19WpFN7NYbecMIJjeQgvDb5aMuvyk0PDYBJrqAEMkTwg3Z98kEKuCm6THHRnrgsdr7bp4SruSAxEM4eJw==}
engines: {node: '>=12'}
cpu: [ppc64]
os: [linux]
@ -1735,8 +1736,8 @@ packages:
requiresBuild: true
optional: true
/esbuild-linux-riscv64/0.14.53:
resolution: {integrity: sha512-yG2sVH+QSix6ct4lIzJj329iJF3MhloLE6/vKMQAAd26UVPVkhMFqFopY+9kCgYsdeWvXdPgmyOuKa48Y7+/EQ==}
/esbuild-linux-riscv64/0.15.7:
resolution: {integrity: sha512-uUJsezbswAYo/X7OU/P+PuL/EI9WzxsEQXDekfwpQ23uGiooxqoLFAPmXPcRAt941vjlY9jtITEEikWMBr+F/g==}
engines: {node: '>=12'}
cpu: [riscv64]
os: [linux]
@ -1752,8 +1753,8 @@ packages:
requiresBuild: true
optional: true
/esbuild-linux-s390x/0.14.53:
resolution: {integrity: sha512-OCJlgdkB+XPYndHmw6uZT7jcYgzmx9K+28PVdOa/eLjdoYkeAFvH5hTwX4AXGLZLH09tpl4bVsEtvuyUldaNCg==}
/esbuild-linux-s390x/0.15.7:
resolution: {integrity: sha512-+tO+xOyTNMc34rXlSxK7aCwJgvQyffqEM5MMdNDEeMU3ss0S6wKvbBOQfgd5jRPblfwJ6b+bKiz0g5nABpY0QQ==}
engines: {node: '>=12'}
cpu: [s390x]
os: [linux]
@ -1769,8 +1770,8 @@ packages:
requiresBuild: true
optional: true
/esbuild-netbsd-64/0.14.53:
resolution: {integrity: sha512-gp2SB+Efc7MhMdWV2+pmIs/Ja/Mi5rjw+wlDmmbIn68VGXBleNgiEZG+eV2SRS0kJEUyHNedDtwRIMzaohWedQ==}
/esbuild-netbsd-64/0.15.7:
resolution: {integrity: sha512-yVc4Wz+Pu3cP5hzm5kIygNPrjar/v5WCSoRmIjCPWfBVJkZNb5brEGKUlf+0Y759D48BCWa0WHrWXaNy0DULTQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [netbsd]
@ -1786,8 +1787,8 @@ packages:
requiresBuild: true
optional: true
/esbuild-openbsd-64/0.14.53:
resolution: {integrity: sha512-eKQ30ZWe+WTZmteDYg8S+YjHV5s4iTxeSGhJKJajFfQx9TLZJvsJX0/paqwP51GicOUruFpSUAs2NCc0a4ivQQ==}
/esbuild-openbsd-64/0.15.7:
resolution: {integrity: sha512-GsimbwC4FSR4lN3wf8XmTQ+r8/0YSQo21rWDL0XFFhLHKlzEA4SsT1Tl8bPYu00IU6UWSJ+b3fG/8SB69rcuEQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [openbsd]
@ -1803,8 +1804,8 @@ packages:
requiresBuild: true
optional: true
/esbuild-sunos-64/0.14.53:
resolution: {integrity: sha512-OWLpS7a2FrIRukQqcgQqR1XKn0jSJoOdT+RlhAxUoEQM/IpytS3FXzCJM6xjUYtpO5GMY0EdZJp+ur2pYdm39g==}
/esbuild-sunos-64/0.15.7:
resolution: {integrity: sha512-8CDI1aL/ts0mDGbWzjEOGKXnU7p3rDzggHSBtVryQzkSOsjCHRVe0iFYUuhczlxU1R3LN/E7HgUO4NXzGGP/Ag==}
engines: {node: '>=12'}
cpu: [x64]
os: [sunos]
@ -1820,8 +1821,8 @@ packages:
requiresBuild: true
optional: true
/esbuild-windows-32/0.14.53:
resolution: {integrity: sha512-m14XyWQP5rwGW0tbEfp95U6A0wY0DYPInWBB7D69FAXUpBpBObRoGTKRv36lf2RWOdE4YO3TNvj37zhXjVL5xg==}
/esbuild-windows-32/0.15.7:
resolution: {integrity: sha512-cOnKXUEPS8EGCzRSFa1x6NQjGhGsFlVgjhqGEbLTPsA7x4RRYiy2RKoArNUU4iR2vHmzqS5Gr84MEumO/wxYKA==}
engines: {node: '>=12'}
cpu: [ia32]
os: [win32]
@ -1837,8 +1838,8 @@ packages:
requiresBuild: true
optional: true
/esbuild-windows-64/0.14.53:
resolution: {integrity: sha512-s9skQFF0I7zqnQ2K8S1xdLSfZFsPLuOGmSx57h2btSEswv0N0YodYvqLcJMrNMXh6EynOmWD7rz+0rWWbFpIHQ==}
/esbuild-windows-64/0.15.7:
resolution: {integrity: sha512-7MI08Ec2sTIDv+zH6StNBKO+2hGUYIT42GmFyW6MBBWWtJhTcQLinKS6ldIN1d52MXIbiJ6nXyCJ+LpL4jBm3Q==}
engines: {node: '>=12'}
cpu: [x64]
os: [win32]
@ -1854,8 +1855,8 @@ packages:
requiresBuild: true
optional: true
/esbuild-windows-arm64/0.14.53:
resolution: {integrity: sha512-E+5Gvb+ZWts+00T9II6wp2L3KG2r3iGxByqd/a1RmLmYWVsSVUjkvIxZuJ3hYTIbhLkH5PRwpldGTKYqVz0nzQ==}
/esbuild-windows-arm64/0.15.7:
resolution: {integrity: sha512-R06nmqBlWjKHddhRJYlqDd3Fabx9LFdKcjoOy08YLimwmsswlFBJV4rXzZCxz/b7ZJXvrZgj8DDv1ewE9+StMw==}
engines: {node: '>=12'}
cpu: [arm64]
os: [win32]
@ -1890,33 +1891,33 @@ packages:
esbuild-windows-64: 0.14.31
esbuild-windows-arm64: 0.14.31
/esbuild/0.14.53:
resolution: {integrity: sha512-ohO33pUBQ64q6mmheX1mZ8mIXj8ivQY/L4oVuAshr+aJI+zLl+amrp3EodrUNDNYVrKJXGPfIHFGhO8slGRjuw==}
/esbuild/0.15.7:
resolution: {integrity: sha512-7V8tzllIbAQV1M4QoE52ImKu8hT/NLGlGXkiDsbEU5PS6K8Mn09ZnYoS+dcmHxOS9CRsV4IRAMdT3I67IyUNXw==}
engines: {node: '>=12'}
hasBin: true
requiresBuild: true
optionalDependencies:
'@esbuild/linux-loong64': 0.14.53
esbuild-android-64: 0.14.53
esbuild-android-arm64: 0.14.53
esbuild-darwin-64: 0.14.53
esbuild-darwin-arm64: 0.14.53
esbuild-freebsd-64: 0.14.53
esbuild-freebsd-arm64: 0.14.53
esbuild-linux-32: 0.14.53
esbuild-linux-64: 0.14.53
esbuild-linux-arm: 0.14.53
esbuild-linux-arm64: 0.14.53
esbuild-linux-mips64le: 0.14.53
esbuild-linux-ppc64le: 0.14.53
esbuild-linux-riscv64: 0.14.53
esbuild-linux-s390x: 0.14.53
esbuild-netbsd-64: 0.14.53
esbuild-openbsd-64: 0.14.53
esbuild-sunos-64: 0.14.53
esbuild-windows-32: 0.14.53
esbuild-windows-64: 0.14.53
esbuild-windows-arm64: 0.14.53
'@esbuild/linux-loong64': 0.15.7
esbuild-android-64: 0.15.7
esbuild-android-arm64: 0.15.7
esbuild-darwin-64: 0.15.7
esbuild-darwin-arm64: 0.15.7
esbuild-freebsd-64: 0.15.7
esbuild-freebsd-arm64: 0.15.7
esbuild-linux-32: 0.15.7
esbuild-linux-64: 0.15.7
esbuild-linux-arm: 0.15.7
esbuild-linux-arm64: 0.15.7
esbuild-linux-mips64le: 0.15.7
esbuild-linux-ppc64le: 0.15.7
esbuild-linux-riscv64: 0.15.7
esbuild-linux-s390x: 0.15.7
esbuild-netbsd-64: 0.15.7
esbuild-openbsd-64: 0.15.7
esbuild-sunos-64: 0.15.7
esbuild-windows-32: 0.15.7
esbuild-windows-64: 0.15.7
esbuild-windows-arm64: 0.15.7
dev: true
/escalade/3.1.1:
@ -2060,9 +2061,11 @@ packages:
/globalyzer/0.1.0:
resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==}
dev: false
/globrex/0.1.2:
resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
dev: false
/graceful-fs/4.2.9:
resolution: {integrity: sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==}
@ -3481,6 +3484,7 @@ packages:
/svelte/3.49.0:
resolution: {integrity: sha512-+lmjic1pApJWDfPCpUUTc1m8azDqYCG1JN9YEngrx/hUyIcFJo6VZhj0A1Ai0wqoHcEIuQy+e9tk+4uDgdtsFA==}
engines: {node: '>= 8'}
dev: false
/sync-request/6.1.0:
resolution: {integrity: sha512-8fjNkrNlNCrVc/av+Jn+xxqfCjYaBoHqCsDz6mt030UMxJGr+GSfCV1dQt2gRtlL63+VPidwDVLr7V2OcTSdRw==}
@ -3595,6 +3599,7 @@ packages:
dependencies:
globalyzer: 0.1.0
globrex: 0.1.2
dev: false
/tinydate/1.3.0:
resolution: {integrity: sha512-7cR8rLy2QhYHpsBDBVYnnWXm8uRTr38RoZakFSW7Bs7PzfMPNZthuMLkwqZv7MTu8lhQ91cOFYS5a7iFj2oR3w==}