mirror of
https://github.com/gradio-app/gradio.git
synced 2024-11-21 01:01:05 +08:00
ensure the ImageEditor
works correctly with layers and change
events (#8059)
* stuff * fix layrs * add changeset * lint * ensure a default image can be passed when sources list is empty * fix loading status * add layers option to disable layer ui * types * fix tests * cleanup * cleanup * notebooks * fix composite * fix * fix trash icon * add changeset * fix layer bg * fix error display * notebooks --------- Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
This commit is contained in:
parent
92139f3d7d
commit
074ce3805a
7
.changeset/shy-pigs-buy.md
Normal file
7
.changeset/shy-pigs-buy.md
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
"@gradio/icons": minor
|
||||
"@gradio/imageeditor": minor
|
||||
"gradio": minor
|
||||
---
|
||||
|
||||
feat:ensure the `ImageEditor` works correctly with layers and `change` events
|
@ -1 +1 @@
|
||||
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: image_editor_events"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "\n", "\n", "def predict(im):\n", " return im[\"composite\"]\n", "\n", "\n", "with gr.Blocks() as demo:\n", " with gr.Group():\n", " with gr.Row():\n", " im = gr.ImageEditor(\n", " type=\"numpy\",\n", " crop_size=\"1:1\",\n", " elem_id=\"image_editor\",\n", " interactive=True,\n", " )\n", " im_preview = gr.Image()\n", " with gr.Group():\n", " with gr.Row():\n", "\n", " n_upload = gr.Label(\n", " 0,\n", " label=\"upload\",\n", " elem_id=\"upload\",\n", " )\n", " n_change = gr.Label(\n", " 0,\n", " label=\"change\",\n", " elem_id=\"change\",\n", " )\n", " n_input = gr.Label(\n", " 0,\n", " label=\"input\",\n", " elem_id=\"input\",\n", " )\n", " n_apply = gr.Label(\n", " 0,\n", " label=\"apply\",\n", " elem_id=\"apply\",\n", " )\n", " clear_btn = gr.Button(\"Clear\", elem_id=\"clear\")\n", "\n", " im.upload(\n", " lambda x: int(x) + 1, outputs=n_upload, inputs=n_upload, show_progress=\"hidden\"\n", " )\n", " im.change(\n", " lambda x: int(x) + 1, outputs=n_change, inputs=n_change, show_progress=\"hidden\"\n", " )\n", " im.input(\n", " lambda x: int(x) + 1, outputs=n_input, inputs=n_input, show_progress=\"hidden\"\n", " )\n", " im.apply(\n", " lambda x: int(x) + 1, outputs=n_apply, inputs=n_apply, show_progress=\"hidden\"\n", " )\n", " im.change(predict, outputs=im_preview, inputs=im, show_progress=\"hidden\")\n", " clear_btn.click(\n", " lambda: None,\n", " None,\n", " im,\n", " )\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
|
||||
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: image_editor_events"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "\n", "\n", "def predict(im):\n", " return im[\"composite\"]\n", "\n", "\n", "with gr.Blocks() as demo:\n", " with gr.Group():\n", " with gr.Row():\n", " im = gr.ImageEditor(\n", " type=\"numpy\",\n", " crop_size=\"1:1\",\n", " elem_id=\"image_editor\",\n", " )\n", " im_preview = gr.Image()\n", " with gr.Group():\n", " with gr.Row():\n", "\n", " n_upload = gr.Label(\n", " 0,\n", " label=\"upload\",\n", " elem_id=\"upload\",\n", " )\n", " n_change = gr.Label(\n", " 0,\n", " label=\"change\",\n", " elem_id=\"change\",\n", " )\n", " n_input = gr.Label(\n", " 0,\n", " label=\"input\",\n", " elem_id=\"input\",\n", " )\n", " n_apply = gr.Label(\n", " 0,\n", " label=\"apply\",\n", " elem_id=\"apply\",\n", " )\n", " clear_btn = gr.Button(\"Clear\", elem_id=\"clear\")\n", "\n", " im.upload(\n", " lambda x: int(x) + 1, outputs=n_upload, inputs=n_upload, show_progress=\"hidden\"\n", " )\n", " im.change(\n", " lambda x: int(x) + 1, outputs=n_change, inputs=n_change, show_progress=\"hidden\"\n", " )\n", " im.input(\n", " lambda x: int(x) + 1, outputs=n_input, inputs=n_input, show_progress=\"hidden\"\n", " )\n", " im.apply(\n", " lambda x: int(x) + 1, outputs=n_apply, inputs=n_apply, show_progress=\"hidden\"\n", " )\n", " im.change(predict, outputs=im_preview, inputs=im, show_progress=\"hidden\")\n", " clear_btn.click(\n", " lambda: None,\n", " None,\n", " im,\n", " )\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
|
@ -12,7 +12,6 @@ with gr.Blocks() as demo:
|
||||
type="numpy",
|
||||
crop_size="1:1",
|
||||
elem_id="image_editor",
|
||||
interactive=True,
|
||||
)
|
||||
im_preview = gr.Image()
|
||||
with gr.Group():
|
||||
|
BIN
demo/image_editor_layers/cheetah.jpg
Normal file
BIN
demo/image_editor_layers/cheetah.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 52 KiB |
BIN
demo/image_editor_layers/layer1.png
Normal file
BIN
demo/image_editor_layers/layer1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
1
demo/image_editor_layers/run.ipynb
Normal file
1
demo/image_editor_layers/run.ipynb
Normal file
@ -0,0 +1 @@
|
||||
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: image_editor_layers"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["# Downloading files from the demo repo\n", "import os\n", "!wget -q https://github.com/gradio-app/gradio/raw/main/demo/image_editor_layers/cheetah.jpg\n", "!wget -q https://github.com/gradio-app/gradio/raw/main/demo/image_editor_layers/layer1.png"]}, {"cell_type": "code", "execution_count": null, "id": "44380577570523278879349135829904343037", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "from pathlib import Path\n", "\n", "\n", "dir_ = Path(__file__).parent\n", "\n", "\n", "def predict(im):\n", " return im\n", "\n", "\n", "with gr.Blocks() as demo:\n", " with gr.Row():\n", " im = gr.ImageEditor(\n", " type=\"numpy\",\n", " interactive=True,\n", " )\n", " im_preview = gr.ImageEditor(\n", " interactive=True,\n", " )\n", "\n", " set_background = gr.Button(\"Set Background\")\n", " set_background.click(\n", " lambda: {\n", " \"background\": str(dir_ / \"cheetah.jpg\"),\n", " \"layers\": None,\n", " \"composite\": None,\n", " },\n", " None,\n", " im,\n", " show_progress=\"hidden\",\n", " )\n", " set_layers = gr.Button(\"Set Layers\")\n", " set_layers.click(\n", " lambda: {\n", " \"background\": str(dir_ / \"cheetah.jpg\"),\n", " \"layers\": [str(dir_ / \"layer1.png\")],\n", " \"composite\": None,\n", " },\n", " None,\n", " im,\n", " show_progress=\"hidden\",\n", " )\n", " set_composite = gr.Button(\"Set Composite\")\n", " set_composite.click(\n", " lambda: {\n", " \"background\": None,\n", " \"layers\": None,\n", " \"composite\": \"https://nationalzoo.si.edu/sites/default/files/animals/cheetah-003.jpg\",\n", " },\n", " None,\n", " im,\n", " show_progress=\"hidden\",\n", " )\n", "\n", " im.change(\n", " predict,\n", " outputs=im_preview,\n", " inputs=im,\n", " )\n", "\n", " gr.Examples(\n", " examples=[\n", " \"https://upload.wikimedia.org/wikipedia/commons/0/09/TheCheethcat.jpg\",\n", " {\n", " \"background\": str(dir_ / \"cheetah.jpg\"),\n", " \"layers\": [str(dir_ / \"layer1.png\")],\n", " \"composite\": None,\n", " },\n", " ],\n", " inputs=im,\n", " )\n", "\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
|
76
demo/image_editor_layers/run.py
Normal file
76
demo/image_editor_layers/run.py
Normal file
@ -0,0 +1,76 @@
|
||||
import gradio as gr
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
dir_ = Path(__file__).parent
|
||||
|
||||
|
||||
def predict(im):
|
||||
return im
|
||||
|
||||
|
||||
with gr.Blocks() as demo:
|
||||
with gr.Row():
|
||||
im = gr.ImageEditor(
|
||||
type="numpy",
|
||||
interactive=True,
|
||||
)
|
||||
im_preview = gr.ImageEditor(
|
||||
interactive=True,
|
||||
)
|
||||
|
||||
set_background = gr.Button("Set Background")
|
||||
set_background.click(
|
||||
lambda: {
|
||||
"background": str(dir_ / "cheetah.jpg"),
|
||||
"layers": None,
|
||||
"composite": None,
|
||||
},
|
||||
None,
|
||||
im,
|
||||
show_progress="hidden",
|
||||
)
|
||||
set_layers = gr.Button("Set Layers")
|
||||
set_layers.click(
|
||||
lambda: {
|
||||
"background": str(dir_ / "cheetah.jpg"),
|
||||
"layers": [str(dir_ / "layer1.png")],
|
||||
"composite": None,
|
||||
},
|
||||
None,
|
||||
im,
|
||||
show_progress="hidden",
|
||||
)
|
||||
set_composite = gr.Button("Set Composite")
|
||||
set_composite.click(
|
||||
lambda: {
|
||||
"background": None,
|
||||
"layers": None,
|
||||
"composite": "https://nationalzoo.si.edu/sites/default/files/animals/cheetah-003.jpg",
|
||||
},
|
||||
None,
|
||||
im,
|
||||
show_progress="hidden",
|
||||
)
|
||||
|
||||
im.change(
|
||||
predict,
|
||||
outputs=im_preview,
|
||||
inputs=im,
|
||||
)
|
||||
|
||||
gr.Examples(
|
||||
examples=[
|
||||
"https://upload.wikimedia.org/wikipedia/commons/0/09/TheCheethcat.jpg",
|
||||
{
|
||||
"background": str(dir_ / "cheetah.jpg"),
|
||||
"layers": [str(dir_ / "layer1.png")],
|
||||
"composite": None,
|
||||
},
|
||||
],
|
||||
inputs=im,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
demo.launch()
|
@ -159,6 +159,7 @@ class ImageEditor(Component):
|
||||
eraser: Eraser | None | Literal[False] = None,
|
||||
brush: Brush | None | Literal[False] = None,
|
||||
format: str = "webp",
|
||||
layers: bool = True,
|
||||
):
|
||||
"""
|
||||
Parameters:
|
||||
@ -187,6 +188,7 @@ class ImageEditor(Component):
|
||||
eraser: The options for the eraser tool in the image editor. Should be an instance of the `gr.Eraser` class, or None to use the default settings. Can also be False to hide the eraser tool.
|
||||
brush: The options for the brush tool in the image editor. Should be an instance of the `gr.Brush` class, or None to use the default settings. Can also be False to hide the brush tool, which will also hide the eraser tool.
|
||||
format: Format to save image if it does not already have a valid format (e.g. if the image is being returned to the frontend as a numpy array or PIL Image). The format should be supported by the PIL library. This parameter has no effect on SVG files.
|
||||
layers: If True, will allow users to add layers to the image. If False, the layers option will be hidden.
|
||||
|
||||
"""
|
||||
self._selectable = _selectable
|
||||
@ -224,6 +226,7 @@ class ImageEditor(Component):
|
||||
self.brush = Brush() if brush is None else brush
|
||||
self.blob_storage: dict[str, EditorDataBlobs] = {}
|
||||
self.format = format
|
||||
self.layers = layers
|
||||
|
||||
super().__init__(
|
||||
label=label,
|
||||
|
@ -108,6 +108,7 @@ class Sketchpad(components.ImageEditor):
|
||||
eraser: Eraser | None = None,
|
||||
brush: Brush | None = None,
|
||||
format: str = "webp",
|
||||
layers: bool = True,
|
||||
):
|
||||
if not brush:
|
||||
brush = Brush(colors=["#000000"], color_mode="fixed")
|
||||
@ -138,6 +139,7 @@ class Sketchpad(components.ImageEditor):
|
||||
eraser=eraser,
|
||||
brush=brush,
|
||||
format=format,
|
||||
layers=layers,
|
||||
)
|
||||
|
||||
|
||||
@ -179,6 +181,7 @@ class Paint(components.ImageEditor):
|
||||
eraser: Eraser | None = None,
|
||||
brush: Brush | None = None,
|
||||
format: str = "webp",
|
||||
layers: bool = True,
|
||||
):
|
||||
super().__init__(
|
||||
value=value,
|
||||
@ -207,6 +210,7 @@ class Paint(components.ImageEditor):
|
||||
eraser=eraser,
|
||||
brush=brush,
|
||||
format=format,
|
||||
layers=layers,
|
||||
)
|
||||
|
||||
|
||||
@ -252,6 +256,7 @@ class ImageMask(components.ImageEditor):
|
||||
eraser: Eraser | None = None,
|
||||
brush: Brush | None = None,
|
||||
format: str = "webp",
|
||||
layers: bool = True,
|
||||
):
|
||||
if not brush:
|
||||
brush = Brush(colors=["#000000"], color_mode="fixed")
|
||||
@ -282,6 +287,7 @@ class ImageMask(components.ImageEditor):
|
||||
eraser=eraser,
|
||||
brush=brush,
|
||||
format=format,
|
||||
layers=layers,
|
||||
)
|
||||
|
||||
|
||||
|
@ -1,4 +1,9 @@
|
||||
<svg id="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<svg
|
||||
id="icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 32 32"
|
||||
fill="currentColor"
|
||||
>
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
|
Before Width: | Height: | Size: 492 B After Width: | Height: | Size: 517 B |
@ -49,7 +49,7 @@
|
||||
export let eraser: Eraser;
|
||||
export let crop_size: [number, number] | `${string}:${string}` | null = null;
|
||||
export let transforms: "crop"[] = ["crop"];
|
||||
|
||||
export let layers = true;
|
||||
export let attached_events: string[] = [];
|
||||
export let server: {
|
||||
accept_blobs: (a: any) => void;
|
||||
@ -77,11 +77,8 @@
|
||||
image_id = null;
|
||||
return val;
|
||||
}
|
||||
// @ts-ignore
|
||||
loading_status = { status: "pending" };
|
||||
|
||||
const blobs = await editor_instance.get_data();
|
||||
// @ts-ignore
|
||||
loading_status = { status: "complete" };
|
||||
|
||||
return blobs;
|
||||
}
|
||||
@ -90,7 +87,15 @@
|
||||
|
||||
$: value && handle_change();
|
||||
|
||||
function handle_change(): void {
|
||||
function wait_for_next_frame(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
|
||||
});
|
||||
}
|
||||
|
||||
async function handle_change(): Promise<void> {
|
||||
await wait_for_next_frame();
|
||||
|
||||
if (
|
||||
value &&
|
||||
(value.background || value.layers?.length || value.composite)
|
||||
@ -194,6 +199,8 @@
|
||||
i18n={gradio.i18n}
|
||||
{transforms}
|
||||
accept_blobs={server.accept_blobs}
|
||||
{layers}
|
||||
status={loading_status?.status}
|
||||
></InteractiveImageEditor>
|
||||
</Block>
|
||||
{/if}
|
||||
|
@ -345,12 +345,14 @@
|
||||
</div>
|
||||
<div
|
||||
class="border"
|
||||
style:width="{$crop[2] * $editor_box.child_width}px"
|
||||
style:height="{$crop[3] * $editor_box.child_height}px"
|
||||
style:width="{$crop[2] * $editor_box.child_width + 1}px"
|
||||
style:height="{$crop[3] * $editor_box.child_height + 1}px"
|
||||
style:top="{$crop[1] * $editor_box.child_height +
|
||||
($editor_box.child_top - $editor_box.parent_top)}px"
|
||||
($editor_box.child_top - $editor_box.parent_top) -
|
||||
0.5}px"
|
||||
style:left="{$crop[0] * $editor_box.child_width +
|
||||
($editor_box.child_left - $editor_box.parent_left)}px"
|
||||
($editor_box.child_left - $editor_box.parent_left) -
|
||||
0.5}px"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
@ -373,12 +375,14 @@
|
||||
.stage-wrap {
|
||||
margin: var(--size-8);
|
||||
margin-bottom: var(--size-1);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tools-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
align-items: center;
|
||||
padding: 0 var(--spacing-xl) 0 0;
|
||||
border: 1px solid var(--block-border-color);
|
||||
border-radius: var(--radius-sm);
|
||||
|
@ -22,12 +22,12 @@
|
||||
import { type Brush as IBrush } from "./tools/Brush.svelte";
|
||||
import { type Eraser } from "./tools/Brush.svelte";
|
||||
|
||||
export let brush: IBrush | null;
|
||||
export let eraser: Eraser | null;
|
||||
import { Tools, Crop, Brush, Sources } from "./tools";
|
||||
import { BlockLabel } from "@gradio/atoms";
|
||||
import { Image as ImageIcon } from "@gradio/icons";
|
||||
|
||||
export let brush: IBrush | null;
|
||||
export let eraser: Eraser | null;
|
||||
export let sources: ("clipboard" | "webcam" | "upload")[];
|
||||
export let crop_size: [number, number] | `${string}:${string}` | null = null;
|
||||
export let i18n: I18nFormatter;
|
||||
@ -41,7 +41,9 @@
|
||||
composite: null
|
||||
};
|
||||
export let transforms: "crop"[] = ["crop"];
|
||||
export let layers: boolean;
|
||||
export let accept_blobs: (a: any) => void;
|
||||
export let status: "pending" | "complete" | "error" = "complete";
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
clear?: never;
|
||||
@ -209,18 +211,17 @@
|
||||
crop_constraint={!!crop_constraint}
|
||||
>
|
||||
<Tools {i18n}>
|
||||
<Layers layer_files={value?.layers || null} />
|
||||
<Layers layer_files={value?.layers || null} enable_layers={layers} />
|
||||
|
||||
<Sources
|
||||
{i18n}
|
||||
{root}
|
||||
{sources}
|
||||
bind:bg
|
||||
bind:active_mode
|
||||
background_file={value?.background || value?.composite || null}
|
||||
></Sources>
|
||||
|
||||
{#if sources && sources.length}
|
||||
<Sources
|
||||
{i18n}
|
||||
{root}
|
||||
{sources}
|
||||
bind:bg
|
||||
bind:active_mode
|
||||
background_file={value?.background || null}
|
||||
></Sources>
|
||||
{/if}
|
||||
{#if transforms.includes("crop")}
|
||||
<Crop {crop_constraint} />
|
||||
{/if}
|
||||
@ -239,7 +240,7 @@
|
||||
{/if}
|
||||
</Tools>
|
||||
|
||||
{#if !bg && !history && active_mode !== "webcam"}
|
||||
{#if !bg && !history && active_mode !== "webcam" && status !== "error"}
|
||||
<div class="empty wrap" style:height={`${editor_height}px`}>
|
||||
{#if sources && sources.length}
|
||||
<div>Upload an image</div>
|
||||
@ -278,13 +279,10 @@
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: var(--size-60);
|
||||
color: var(--block-label-text-color);
|
||||
line-height: var(--line-md);
|
||||
/* height: 100%; */
|
||||
font-size: var(--text-lg);
|
||||
pointer-events: none;
|
||||
/* margin-top: var(--size-8); */
|
||||
}
|
||||
|
||||
.or {
|
||||
|
@ -10,6 +10,7 @@
|
||||
let show_layers = false;
|
||||
|
||||
export let layer_files: (FileData | null)[] | null = [];
|
||||
export let enable_layers = true;
|
||||
|
||||
const { pixi, current_layer, dimensions, register_context } =
|
||||
getContext<EditorContext>(EDITOR_KEY);
|
||||
@ -29,12 +30,13 @@
|
||||
async function validate_layers(): Promise<void> {
|
||||
let invalid = layers.some(
|
||||
(layer) =>
|
||||
layer.composite.texture.width != $dimensions[0] ||
|
||||
layer.composite.texture.height != $dimensions[1]
|
||||
layer.composite.texture?.width != $dimensions[0] ||
|
||||
layer.composite.texture?.height != $dimensions[1]
|
||||
);
|
||||
if (invalid) {
|
||||
LayerManager.reset();
|
||||
new_layer();
|
||||
if (!layer_files || layer_files.length == 0) new_layer();
|
||||
else render_layer_files(layer_files);
|
||||
}
|
||||
}
|
||||
$: $dimensions, validate_layers();
|
||||
@ -62,7 +64,11 @@
|
||||
_layer_files: typeof layer_files
|
||||
): Promise<void> {
|
||||
await tick();
|
||||
if (!_layer_files || _layer_files.length == 0) return;
|
||||
if (!_layer_files || _layer_files.length == 0) {
|
||||
LayerManager.reset();
|
||||
new_layer();
|
||||
return;
|
||||
}
|
||||
if (!$pixi) return;
|
||||
|
||||
const fetch_promises = await Promise.all(
|
||||
@ -87,7 +93,8 @@
|
||||
last_layer = await LayerManager.add_layer_from_blob(
|
||||
$pixi.layer_container,
|
||||
$pixi.renderer,
|
||||
blob
|
||||
blob,
|
||||
$pixi.view
|
||||
);
|
||||
}
|
||||
|
||||
@ -105,36 +112,40 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="layer-wrap"
|
||||
class:closed={!show_layers}
|
||||
use:click_outside={() => (show_layers = false)}
|
||||
>
|
||||
<button aria-label="Show Layers" on:click={() => (show_layers = !show_layers)}
|
||||
><span class="icon"><Layers /></span> Layer {layers.findIndex(
|
||||
(l) => l === $current_layer
|
||||
) + 1}
|
||||
</button>
|
||||
{#if show_layers}
|
||||
<ul>
|
||||
{#each layers as layer, i (i)}
|
||||
{#if enable_layers}
|
||||
<div
|
||||
class="layer-wrap"
|
||||
class:closed={!show_layers}
|
||||
use:click_outside={() => (show_layers = false)}
|
||||
>
|
||||
<button
|
||||
aria-label="Show Layers"
|
||||
on:click={() => (show_layers = !show_layers)}
|
||||
><span class="icon"><Layers /></span> Layer {layers.findIndex(
|
||||
(l) => l === $current_layer
|
||||
) + 1}
|
||||
</button>
|
||||
{#if show_layers}
|
||||
<ul>
|
||||
{#each layers as layer, i (i)}
|
||||
<li>
|
||||
<button
|
||||
class:selected_layer={$current_layer === layer}
|
||||
on:click={() =>
|
||||
($current_layer = LayerManager.change_active_layer(i))}
|
||||
>Layer {i + 1}</button
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
<li>
|
||||
<button
|
||||
class:selected_layer={$current_layer === layer}
|
||||
on:click={() =>
|
||||
($current_layer = LayerManager.change_active_layer(i))}
|
||||
>Layer {i + 1}</button
|
||||
>
|
||||
<button aria-label="Add Layer" on:click={new_layer}> +</button>
|
||||
</li>
|
||||
{/each}
|
||||
<li>
|
||||
<button aria-label="Add Layer" on:click={new_layer}> +</button>
|
||||
</li>
|
||||
</ul>
|
||||
{/if}
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
<span class="sep"></span>
|
||||
</div>
|
||||
<span class="sep"></span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.icon {
|
||||
@ -170,7 +181,10 @@
|
||||
.layer-wrap li:last-child button {
|
||||
border-bottom: none;
|
||||
text-align: center;
|
||||
padding: 2.5px;
|
||||
font-size: var(--scale-0);
|
||||
line-height: 1;
|
||||
font-weight: var(--weight-bold);
|
||||
padding: 5px 0 1px 0;
|
||||
}
|
||||
|
||||
.closed > button {
|
||||
@ -187,6 +201,7 @@
|
||||
|
||||
.selected_layer {
|
||||
background-color: var(--block-background-fill);
|
||||
color: var(--color-accent);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@ -194,7 +209,7 @@
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: #fff;
|
||||
background: var(--block-background-fill);
|
||||
width: calc(100% + 1px);
|
||||
list-style: none;
|
||||
z-index: var(--layer-top);
|
||||
|
@ -93,7 +93,8 @@ interface LayerManager {
|
||||
add_layer_from_blob(
|
||||
container: Container,
|
||||
renderer: IRenderer,
|
||||
blob: Blob
|
||||
blob: Blob,
|
||||
view: HTMLCanvasElement
|
||||
): Promise<[LayerScene, LayerScene[]]>;
|
||||
}
|
||||
|
||||
@ -197,19 +198,32 @@ export function layer_manager(): LayerManager {
|
||||
async add_layer_from_blob(
|
||||
container: Container,
|
||||
renderer: IRenderer,
|
||||
blob: Blob
|
||||
blob: Blob,
|
||||
view: HTMLCanvasElement
|
||||
) {
|
||||
const img = await createImageBitmap(blob);
|
||||
const bitmap_texture = Texture.from(img);
|
||||
|
||||
const [w, h] = resize_to_fit(
|
||||
bitmap_texture.width,
|
||||
bitmap_texture.height,
|
||||
view.width,
|
||||
view.height
|
||||
);
|
||||
|
||||
const sprite = new Sprite(bitmap_texture) as Sprite & DisplayObject;
|
||||
sprite.zIndex = 0;
|
||||
|
||||
sprite.width = w;
|
||||
sprite.height = h;
|
||||
|
||||
const [layer, layers] = this.add_layer(
|
||||
container,
|
||||
renderer,
|
||||
sprite.width,
|
||||
sprite.height
|
||||
view.width,
|
||||
view.height
|
||||
);
|
||||
|
||||
renderer.render(sprite, {
|
||||
renderTexture: layer.draw_texture
|
||||
});
|
||||
@ -221,3 +235,29 @@ export function layer_manager(): LayerManager {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function resize_to_fit(
|
||||
inner_width: number,
|
||||
inner_height: number,
|
||||
outer_width: number,
|
||||
outer_height: number
|
||||
): [number, number] {
|
||||
if (inner_width <= outer_width && inner_height <= outer_height) {
|
||||
return [inner_width, inner_height];
|
||||
}
|
||||
|
||||
const inner_aspect = inner_width / inner_height;
|
||||
const outer_aspect = outer_width / outer_height;
|
||||
|
||||
let new_width, new_height;
|
||||
|
||||
if (inner_aspect > outer_aspect) {
|
||||
new_width = outer_width;
|
||||
new_height = outer_width / inner_aspect;
|
||||
} else {
|
||||
new_height = outer_height;
|
||||
new_width = outer_height * inner_aspect;
|
||||
}
|
||||
|
||||
return [new_width, new_height];
|
||||
}
|
||||
|
@ -161,54 +161,56 @@
|
||||
|
||||
<svelte:window on:keydown={handle_key} />
|
||||
|
||||
<div class="source-wrap">
|
||||
{#each sources_list as { icon, label, id, cb } (id)}
|
||||
<IconButton
|
||||
Icon={icon}
|
||||
size="medium"
|
||||
padded={false}
|
||||
label={label + " button"}
|
||||
hasPopup={true}
|
||||
transparent={true}
|
||||
on:click={cb}
|
||||
/>
|
||||
{/each}
|
||||
<span class="sep"></span>
|
||||
</div>
|
||||
<div class="upload-container">
|
||||
<Upload
|
||||
hidden={true}
|
||||
bind:this={upload}
|
||||
filetype="image/*"
|
||||
on:load={handle_upload}
|
||||
on:error
|
||||
{root}
|
||||
disable_click={!sources.includes("upload")}
|
||||
format="blob"
|
||||
></Upload>
|
||||
{#if active_mode === "webcam"}
|
||||
<div
|
||||
class="modal"
|
||||
style:max-width="{$editor_box.child_width}px"
|
||||
style:max-height="{$editor_box.child_height}px"
|
||||
style:top="{$editor_box.child_top - $editor_box.parent_top}px"
|
||||
>
|
||||
<div class="modal-inner">
|
||||
<Webcam
|
||||
{root}
|
||||
on:capture={handle_upload}
|
||||
on:error
|
||||
on:drag
|
||||
{mirror_webcam}
|
||||
streaming={false}
|
||||
mode="image"
|
||||
include_audio={false}
|
||||
{i18n}
|
||||
/>
|
||||
{#if sources.length}
|
||||
<div class="source-wrap">
|
||||
{#each sources_list as { icon, label, id, cb } (id)}
|
||||
<IconButton
|
||||
Icon={icon}
|
||||
size="medium"
|
||||
padded={false}
|
||||
label={label + " button"}
|
||||
hasPopup={true}
|
||||
transparent={true}
|
||||
on:click={cb}
|
||||
/>
|
||||
{/each}
|
||||
<span class="sep"></span>
|
||||
</div>
|
||||
<div class="upload-container">
|
||||
<Upload
|
||||
hidden={true}
|
||||
bind:this={upload}
|
||||
filetype="image/*"
|
||||
on:load={handle_upload}
|
||||
on:error
|
||||
{root}
|
||||
disable_click={!sources.includes("upload")}
|
||||
format="blob"
|
||||
></Upload>
|
||||
{#if active_mode === "webcam"}
|
||||
<div
|
||||
class="modal"
|
||||
style:max-width="{$editor_box.child_width}px"
|
||||
style:max-height="{$editor_box.child_height}px"
|
||||
style:top="{$editor_box.child_top - $editor_box.parent_top}px"
|
||||
>
|
||||
<div class="modal-inner">
|
||||
<Webcam
|
||||
{root}
|
||||
on:capture={handle_upload}
|
||||
on:error
|
||||
on:drag
|
||||
{mirror_webcam}
|
||||
streaming={false}
|
||||
mode="image"
|
||||
include_audio={false}
|
||||
{i18n}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.modal {
|
||||
|
@ -640,6 +640,7 @@ class TestImageEditor:
|
||||
"name": "imageeditor",
|
||||
"server_fns": ["accept_blobs"],
|
||||
"format": "webp",
|
||||
"layers": True,
|
||||
}
|
||||
|
||||
def test_process_example(self):
|
||||
|
Loading…
Reference in New Issue
Block a user