|
<script lang="ts">
|
|
import { getContext, onMount, tick } from "svelte";
|
|
|
|
import { click_outside } from "../utils/events";
|
|
import { layer_manager, type LayerScene } from "./utils";
|
|
import { EDITOR_KEY, type EditorContext } from "../ImageEditor.svelte";
|
|
import type { FileData } from "@gradio/client";
|
|
import { Layers } from "@gradio/icons";
|
|
|
|
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);
|
|
|
|
const LayerManager = layer_manager();
|
|
let layers: LayerScene[] = [];
|
|
|
|
register_context("layers", {
|
|
init_fn: () => {
|
|
new_layer();
|
|
},
|
|
reset_fn: () => {
|
|
LayerManager.reset();
|
|
}
|
|
});
|
|
|
|
async function validate_layers(): Promise<void> {
|
|
let invalid = layers.some(
|
|
(layer) =>
|
|
layer.composite.texture?.width != $dimensions[0] ||
|
|
layer.composite.texture?.height != $dimensions[1]
|
|
);
|
|
if (invalid) {
|
|
LayerManager.reset();
|
|
if (!layer_files || layer_files.length == 0) new_layer();
|
|
else render_layer_files(layer_files);
|
|
}
|
|
}
|
|
$: $dimensions, validate_layers();
|
|
|
|
async function new_layer(): Promise<void> {
|
|
if (!$pixi) return;
|
|
|
|
const [active_layer, all_layers] = LayerManager.add_layer(
|
|
$pixi.layer_container,
|
|
$pixi.renderer,
|
|
...$dimensions
|
|
);
|
|
|
|
$current_layer = active_layer;
|
|
layers = all_layers;
|
|
}
|
|
|
|
$: render_layer_files(layer_files);
|
|
|
|
function is_not_null<T>(x: T | null): x is T {
|
|
return x !== null;
|
|
}
|
|
|
|
async function render_layer_files(
|
|
_layer_files: typeof layer_files
|
|
): Promise<void> {
|
|
await tick();
|
|
if (!_layer_files || _layer_files.length == 0) {
|
|
LayerManager.reset();
|
|
new_layer();
|
|
return;
|
|
}
|
|
if (!$pixi) return;
|
|
|
|
const fetch_promises = await Promise.all(
|
|
_layer_files.map((f) => {
|
|
if (!f || !f.url) return null;
|
|
|
|
return fetch(f.url);
|
|
})
|
|
);
|
|
|
|
const blobs = await Promise.all(
|
|
fetch_promises.map((p) => {
|
|
if (!p) return null;
|
|
return p.blob();
|
|
})
|
|
);
|
|
|
|
LayerManager.reset();
|
|
|
|
let last_layer: [LayerScene, LayerScene[]] | null = null;
|
|
for (const blob of blobs.filter(is_not_null)) {
|
|
last_layer = await LayerManager.add_layer_from_blob(
|
|
$pixi.layer_container,
|
|
$pixi.renderer,
|
|
blob,
|
|
$pixi.view
|
|
);
|
|
}
|
|
|
|
if (!last_layer) return;
|
|
|
|
$current_layer = last_layer[0];
|
|
layers = last_layer[1];
|
|
}
|
|
|
|
onMount(async () => {
|
|
await tick();
|
|
if (!$pixi) return;
|
|
|
|
$pixi = { ...$pixi!, get_layers: LayerManager.get_layers };
|
|
});
|
|
</script>
|
|
|
|
{#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 aria-label="Add Layer" on:click={new_layer}> +</button>
|
|
</li>
|
|
</ul>
|
|
{/if}
|
|
|
|
<span class="sep"></span>
|
|
</div>
|
|
{/if}
|
|
|
|
<style>
|
|
.icon {
|
|
width: 14px;
|
|
margin-right: var(--spacing-md);
|
|
color: var(--block-label-text-color);
|
|
margin-right: var(--spacing-lg);
|
|
margin-top: 1px;
|
|
}
|
|
|
|
.layer-wrap {
|
|
position: relative;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
|
|
.layer-wrap button {
|
|
justify-content: flex-start;
|
|
align-items: flex-start;
|
|
width: 100%;
|
|
border-bottom: 1px solid var(--block-border-color);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: var(--scale-000);
|
|
line-height: var(--line-sm);
|
|
padding-bottom: 1px;
|
|
margin-left: var(--spacing-xl);
|
|
padding: var(--spacing-sm) 0;
|
|
}
|
|
|
|
.layer-wrap li:last-child button {
|
|
border-bottom: none;
|
|
text-align: center;
|
|
font-size: var(--scale-0);
|
|
line-height: 1;
|
|
font-weight: var(--weight-bold);
|
|
padding: 5px 0 1px 0;
|
|
}
|
|
|
|
.closed > button {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.layer-wrap button:hover {
|
|
background-color: none;
|
|
}
|
|
|
|
.layer-wrap button:hover .icon {
|
|
color: var(--color-accent);
|
|
}
|
|
|
|
.selected_layer {
|
|
background-color: var(--block-background-fill);
|
|
color: var(--color-accent);
|
|
font-weight: bold;
|
|
}
|
|
|
|
ul {
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 0;
|
|
background: var(--block-background-fill);
|
|
width: calc(100% + 1px);
|
|
list-style: none;
|
|
z-index: var(--layer-top);
|
|
border: 1px solid var(--block-border-color);
|
|
padding: var(--spacing-sm) 0;
|
|
text-wrap: none;
|
|
transform: translate(-1px, 1px);
|
|
border-radius: var(--radius-sm);
|
|
border-bottom-right-radius: 0;
|
|
}
|
|
|
|
.layer-wrap ul > li > button {
|
|
margin-left: 0;
|
|
}
|
|
|
|
.sep {
|
|
height: 12px;
|
|
background-color: var(--block-border-color);
|
|
width: 1px;
|
|
display: block;
|
|
margin-left: var(--spacing-xl);
|
|
}
|
|
</style>
|
|
|