|
<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> |
|
|