fundus 0.0.2
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.
- package/dist/build-CZYFLLvl.js +191 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1420 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +2 -0
- package/dist/core/image-Dqmz8FXS.js +54 -0
- package/dist/core/index.d.ts +499 -0
- package/dist/core/index.js +962 -0
- package/dist/core/reference-graph-CjuHbLDw.d.ts +280 -0
- package/dist/core/testing.d.ts +27 -0
- package/dist/core/testing.js +80 -0
- package/dist/editor/200.html +37 -0
- package/dist/editor/_app/immutable/assets/0.DLhIsYA_.css +1 -0
- package/dist/editor/_app/immutable/assets/2.DIbbW7U1.css +1 -0
- package/dist/editor/_app/immutable/assets/3.C-9vFQ9A.css +1 -0
- package/dist/editor/_app/immutable/assets/4.BS8ElpOV.css +1 -0
- package/dist/editor/_app/immutable/assets/ConfirmDialog.CHEISE-L.css +1 -0
- package/dist/editor/_app/immutable/assets/LibraryContextMenu.CDjCmdV-.css +1 -0
- package/dist/editor/_app/immutable/chunks/BfPE5wdt.js +1 -0
- package/dist/editor/_app/immutable/chunks/Bjy-W4x2.js +81 -0
- package/dist/editor/_app/immutable/chunks/CNRz3sSw.js +3 -0
- package/dist/editor/_app/immutable/chunks/CO4RBJfa.js +1 -0
- package/dist/editor/_app/immutable/chunks/Cx6qtxJg.js +1 -0
- package/dist/editor/_app/immutable/chunks/DjdrdRzT.js +1 -0
- package/dist/editor/_app/immutable/chunks/DjvOiDmq.js +1 -0
- package/dist/editor/_app/immutable/chunks/n7mvYc42.js +5 -0
- package/dist/editor/_app/immutable/chunks/xihTtKlq.js +1 -0
- package/dist/editor/_app/immutable/entry/app.BkqbfI8n.js +2 -0
- package/dist/editor/_app/immutable/entry/start.C8HHOLly.js +1 -0
- package/dist/editor/_app/immutable/nodes/0.xdMI5GEU.js +2 -0
- package/dist/editor/_app/immutable/nodes/1.D9p2tfoJ.js +1 -0
- package/dist/editor/_app/immutable/nodes/2.BrA0Hyzu.js +3 -0
- package/dist/editor/_app/immutable/nodes/3.WO7tZH_d.js +104 -0
- package/dist/editor/_app/immutable/nodes/4.DQtx17aW.js +2 -0
- package/dist/editor/_app/version.json +1 -0
- package/dist/index.d.ts +370 -0
- package/dist/index.js +4 -0
- package/dist/operations-DYM-5KVl.js +1797 -0
- package/dist/runtime/chroma-key-gl.d.ts +23 -0
- package/dist/runtime/chroma-key-gl.js +290 -0
- package/dist/runtime/components/ChromaKeyVideo.svelte +225 -0
- package/dist/runtime/components/Image.svelte +18 -0
- package/dist/runtime/components/Slice.svelte +35 -0
- package/dist/runtime/components/SliceCanvas.svelte +162 -0
- package/dist/runtime/components/SliceDom.svelte +104 -0
- package/dist/runtime/components/Video.svelte +60 -0
- package/dist/runtime/geometry.d.ts +75 -0
- package/dist/runtime/geometry.js +209 -0
- package/dist/runtime/index.d.ts +9 -0
- package/dist/runtime/index.js +9 -0
- package/dist/runtime/kind-handlers.d.ts +46 -0
- package/dist/runtime/kind-handlers.js +86 -0
- package/dist/runtime/manifest.d.ts +60 -0
- package/dist/runtime/manifest.js +117 -0
- package/dist/runtime/observe-border-box-size.d.ts +5 -0
- package/dist/runtime/observe-border-box-size.js +12 -0
- package/dist/runtime/preload-registry.d.ts +26 -0
- package/dist/runtime/preload-registry.js +68 -0
- package/dist/start-Bhnqt9Dc.js +279 -0
- package/dist/start-D2IXOrni.js +2 -0
- package/package.json +65 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Canvas slice backend using the entry-based API (slicing arrives inline on the
|
|
3
|
+
`SliceManifestEntry`). Private: hosts render `Slice.svelte`, which
|
|
4
|
+
dispatches between this and the DOM backend.
|
|
5
|
+
|
|
6
|
+
Fills its container and draws each slice cell with one `drawImage` into a
|
|
7
|
+
backing canvas sized to the box × devicePixelRatio (for crispness). A
|
|
8
|
+
ResizeObserver triggers a redraw on resize; window `resize` and DPR changes
|
|
9
|
+
(browser zoom, moving between displays) redraw too, and a reactive effect
|
|
10
|
+
redraws when the entry changes — so it also works live inside the editor.
|
|
11
|
+
|
|
12
|
+
The measured element is a wrapper div, not the canvas: with overdraw the
|
|
13
|
+
canvas is enlarged by the overdraw's destination size and offset negatively
|
|
14
|
+
(see `computeCanvasPaintLayout`), so the baked shadow/glow paints outside
|
|
15
|
+
the layout box while the wrapper (and therefore the layout) keeps the box
|
|
16
|
+
size — exactly where the DOM backend positions its overdraw cells.
|
|
17
|
+
-->
|
|
18
|
+
<script lang="ts">
|
|
19
|
+
import type { SliceManifestEntry } from '#fundus/core';
|
|
20
|
+
import { computeCanvasPaintLayout, computeRenderCells } from '../geometry';
|
|
21
|
+
import { observeBorderBoxSize } from '../observe-border-box-size';
|
|
22
|
+
|
|
23
|
+
interface Props {
|
|
24
|
+
src: SliceManifestEntry;
|
|
25
|
+
/** Accessible label for the rendered graphic. */
|
|
26
|
+
alt?: string;
|
|
27
|
+
class?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let { src, alt = '', class: className = '' }: Props = $props();
|
|
31
|
+
|
|
32
|
+
let root = $state<HTMLDivElement>();
|
|
33
|
+
let canvas = $state<HTMLCanvasElement>();
|
|
34
|
+
let image = $state<HTMLImageElement>();
|
|
35
|
+
let boxWidth = $state(0);
|
|
36
|
+
let boxHeight = $state(0);
|
|
37
|
+
let devicePixelRatio = $state(1);
|
|
38
|
+
// Bumped by window `resize` so the draw effect re-runs even when neither the
|
|
39
|
+
// box nor the entry changed (belt and braces alongside the ResizeObserver).
|
|
40
|
+
let redrawTick = $state(0);
|
|
41
|
+
|
|
42
|
+
// Load the source image once per entry `src` — the same plain URL the DOM
|
|
43
|
+
// backend paints (slices are image-backed: the browser cache owns the bytes,
|
|
44
|
+
// preload retains nothing to resolve).
|
|
45
|
+
$effect(() => {
|
|
46
|
+
const sourceUrl = src.src;
|
|
47
|
+
const next = new Image();
|
|
48
|
+
let cancelled = false;
|
|
49
|
+
next.onload = () => {
|
|
50
|
+
if (!cancelled) image = next;
|
|
51
|
+
};
|
|
52
|
+
next.src = sourceUrl;
|
|
53
|
+
return () => {
|
|
54
|
+
cancelled = true;
|
|
55
|
+
next.onload = null;
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Track the display's devicePixelRatio: a resolution media query fires
|
|
60
|
+
// exactly when the DPR leaves the tracked value (browser zoom, moving the
|
|
61
|
+
// window to another display). The tracked state must be READ first — a
|
|
62
|
+
// state the effect writes before reading is deliberately not a dependency
|
|
63
|
+
// in Svelte, so a write-first version would never re-arm the query after
|
|
64
|
+
// the first transition. Reconciling via update() converges in one extra
|
|
65
|
+
// run when the real DPR drifted (mount, or a change event).
|
|
66
|
+
$effect(() => {
|
|
67
|
+
const trackedRatio = devicePixelRatio;
|
|
68
|
+
const query = window.matchMedia(`(resolution: ${trackedRatio}dppx)`);
|
|
69
|
+
const update = () => {
|
|
70
|
+
devicePixelRatio = window.devicePixelRatio || 1;
|
|
71
|
+
};
|
|
72
|
+
update();
|
|
73
|
+
query.addEventListener('change', update);
|
|
74
|
+
return () => query.removeEventListener('change', update);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
$effect(() => {
|
|
78
|
+
const element = root;
|
|
79
|
+
if (!element) return;
|
|
80
|
+
return observeBorderBoxSize(element, (width, height) => {
|
|
81
|
+
boxWidth = width;
|
|
82
|
+
boxHeight = height;
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const layout = $derived(computeCanvasPaintLayout(src, boxWidth, boxHeight, devicePixelRatio));
|
|
87
|
+
|
|
88
|
+
// Redraw when the box size, image, entry, DPR, or window changes.
|
|
89
|
+
$effect(() => {
|
|
90
|
+
void image;
|
|
91
|
+
void src;
|
|
92
|
+
void layout;
|
|
93
|
+
void redrawTick;
|
|
94
|
+
draw();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
function draw(): void {
|
|
98
|
+
const element = canvas;
|
|
99
|
+
const source = image;
|
|
100
|
+
if (!element || !source) return;
|
|
101
|
+
if (boxWidth === 0 || boxHeight === 0) {
|
|
102
|
+
element.width = 0;
|
|
103
|
+
element.height = 0;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
element.width = layout.backingWidth;
|
|
108
|
+
element.height = layout.backingHeight;
|
|
109
|
+
|
|
110
|
+
const context = element.getContext('2d');
|
|
111
|
+
if (!context) return;
|
|
112
|
+
// Origin at the layout box's top-left, so the (possibly negative) cell
|
|
113
|
+
// coordinates from computeRenderCells apply directly.
|
|
114
|
+
context.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, layout.originX, layout.originY);
|
|
115
|
+
context.clearRect(layout.cssLeft, layout.cssTop, layout.cssWidth, layout.cssHeight);
|
|
116
|
+
|
|
117
|
+
for (const cell of computeRenderCells(src, boxWidth, boxHeight)) {
|
|
118
|
+
context.drawImage(
|
|
119
|
+
source,
|
|
120
|
+
cell.sx,
|
|
121
|
+
cell.sy,
|
|
122
|
+
cell.sw,
|
|
123
|
+
cell.sh,
|
|
124
|
+
cell.dx,
|
|
125
|
+
cell.dy,
|
|
126
|
+
cell.dw,
|
|
127
|
+
cell.dh
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
</script>
|
|
132
|
+
|
|
133
|
+
<svelte:window onresize={() => (redrawTick += 1)} />
|
|
134
|
+
|
|
135
|
+
<div
|
|
136
|
+
bind:this={root}
|
|
137
|
+
class="fundus-slice-canvas {className}"
|
|
138
|
+
role={alt ? 'img' : undefined}
|
|
139
|
+
aria-label={alt || undefined}
|
|
140
|
+
>
|
|
141
|
+
<canvas
|
|
142
|
+
bind:this={canvas}
|
|
143
|
+
style:left="{layout.cssLeft}px"
|
|
144
|
+
style:top="{layout.cssTop}px"
|
|
145
|
+
style:width="{layout.cssWidth}px"
|
|
146
|
+
style:height="{layout.cssHeight}px"
|
|
147
|
+
></canvas>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<style>
|
|
151
|
+
.fundus-slice-canvas {
|
|
152
|
+
position: relative;
|
|
153
|
+
width: 100%;
|
|
154
|
+
height: 100%;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
canvas {
|
|
158
|
+
position: absolute;
|
|
159
|
+
display: block;
|
|
160
|
+
pointer-events: none;
|
|
161
|
+
}
|
|
162
|
+
</style>
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
DOM slice backend using the entry-based API: the slicing arrives inline on the
|
|
3
|
+
`SliceManifestEntry` (no registry, no manifest lookup). One component covers
|
|
4
|
+
all modes (nine / three-horizontal / three-vertical / one) — the mode lives
|
|
5
|
+
in the entry. Private: hosts render `Slice.svelte`, which dispatches between
|
|
6
|
+
this and the canvas backend.
|
|
7
|
+
|
|
8
|
+
Fills its container and measures it (ResizeObserver), then paints one cell per
|
|
9
|
+
slice region as an absolutely-positioned window onto the source image: a full
|
|
10
|
+
copy of the image is scaled and offset (percentages relative to the cell) so
|
|
11
|
+
the cell shows exactly its source region. Cells abut on integer pixels; corners
|
|
12
|
+
/ caps keep their aspect ratio (no distortion), the stretch bands fill the rest.
|
|
13
|
+
With overdraw the cells extend past the container (negative offsets), so the
|
|
14
|
+
root switches to overflow: visible and the baked shadow/glow draws outside the
|
|
15
|
+
layout box.
|
|
16
|
+
-->
|
|
17
|
+
<script lang="ts">
|
|
18
|
+
import type { SliceManifestEntry } from '#fundus/core';
|
|
19
|
+
import { computeRenderCells, effectiveOverdraw } from '../geometry';
|
|
20
|
+
import { observeBorderBoxSize } from '../observe-border-box-size';
|
|
21
|
+
|
|
22
|
+
interface Props {
|
|
23
|
+
src: SliceManifestEntry;
|
|
24
|
+
/** Accessible label for the rendered graphic. */
|
|
25
|
+
alt?: string;
|
|
26
|
+
class?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let { src, alt = '', class: className = '' }: Props = $props();
|
|
30
|
+
|
|
31
|
+
let root = $state<HTMLDivElement>();
|
|
32
|
+
let boxWidth = $state(0);
|
|
33
|
+
let boxHeight = $state(0);
|
|
34
|
+
|
|
35
|
+
$effect(() => {
|
|
36
|
+
const element = root;
|
|
37
|
+
if (!element) return;
|
|
38
|
+
return observeBorderBoxSize(element, (width, height) => {
|
|
39
|
+
boxWidth = width;
|
|
40
|
+
boxHeight = height;
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const cells = $derived(computeRenderCells(src, boxWidth, boxHeight));
|
|
45
|
+
const hasOverdraw = $derived.by(() => {
|
|
46
|
+
const overdraw = effectiveOverdraw(src);
|
|
47
|
+
return overdraw.top > 0 || overdraw.right > 0 || overdraw.bottom > 0 || overdraw.left > 0;
|
|
48
|
+
});
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<div
|
|
52
|
+
bind:this={root}
|
|
53
|
+
class="fundus-slice {className}"
|
|
54
|
+
class:fundus-slice--overdraw={hasOverdraw}
|
|
55
|
+
role={alt ? 'img' : undefined}
|
|
56
|
+
aria-label={alt || undefined}
|
|
57
|
+
>
|
|
58
|
+
{#each cells as cell (cell.key)}
|
|
59
|
+
<div
|
|
60
|
+
class="fundus-slice__cell"
|
|
61
|
+
style:left="{cell.dx}px"
|
|
62
|
+
style:top="{cell.dy}px"
|
|
63
|
+
style:width="{cell.dw}px"
|
|
64
|
+
style:height="{cell.dh}px"
|
|
65
|
+
>
|
|
66
|
+
<img
|
|
67
|
+
src={src.src}
|
|
68
|
+
alt=""
|
|
69
|
+
draggable="false"
|
|
70
|
+
style:width="{(src.sourceWidth / cell.sw) * 100}%"
|
|
71
|
+
style:height="{(src.sourceHeight / cell.sh) * 100}%"
|
|
72
|
+
style:left="{(-cell.sx / cell.sw) * 100}%"
|
|
73
|
+
style:top="{(-cell.sy / cell.sh) * 100}%"
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
{/each}
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<style>
|
|
80
|
+
.fundus-slice {
|
|
81
|
+
position: relative;
|
|
82
|
+
width: 100%;
|
|
83
|
+
height: 100%;
|
|
84
|
+
overflow: hidden;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* Overdraw cells deliberately extend past the box — don't clip them. */
|
|
88
|
+
.fundus-slice--overdraw {
|
|
89
|
+
overflow: visible;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.fundus-slice__cell {
|
|
93
|
+
position: absolute;
|
|
94
|
+
overflow: hidden;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.fundus-slice__cell img {
|
|
98
|
+
position: absolute;
|
|
99
|
+
max-width: none;
|
|
100
|
+
max-height: none;
|
|
101
|
+
pointer-events: none;
|
|
102
|
+
user-select: none;
|
|
103
|
+
}
|
|
104
|
+
</style>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Plain playback wrapper for a video manifest entry: renders the delivered
|
|
3
|
+
proxy with its intrinsic width/height attributes so layout never shifts
|
|
4
|
+
while the file loads, from the retained object URL when preloaded. When,
|
|
5
|
+
where and how it plays is host behavior, so loop/playing/muted/controls stay props.
|
|
6
|
+
-->
|
|
7
|
+
<script lang="ts">
|
|
8
|
+
import type { VideoManifestEntry } from '#fundus/core';
|
|
9
|
+
import { resolveRetainedSource } from '../preload-registry';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
src: VideoManifestEntry;
|
|
13
|
+
/** Loop playback (default false — a plain clip ends unless the host says otherwise). */
|
|
14
|
+
loop?: boolean;
|
|
15
|
+
/** Playback on/off (default true). */
|
|
16
|
+
playing?: boolean;
|
|
17
|
+
/** Muted (default true — required for autoplay in every mobile browser). */
|
|
18
|
+
muted?: boolean;
|
|
19
|
+
/** Show native playback controls (default false). */
|
|
20
|
+
controls?: boolean;
|
|
21
|
+
/** Bindable handle on the underlying element, so a host can drive playback itself. */
|
|
22
|
+
videoElement?: HTMLVideoElement;
|
|
23
|
+
class?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let {
|
|
27
|
+
src,
|
|
28
|
+
loop = false,
|
|
29
|
+
playing = true,
|
|
30
|
+
muted = true,
|
|
31
|
+
controls = false,
|
|
32
|
+
videoElement = $bindable(),
|
|
33
|
+
class: className = ''
|
|
34
|
+
}: Props = $props();
|
|
35
|
+
|
|
36
|
+
const source = $derived(resolveRetainedSource(src.src));
|
|
37
|
+
|
|
38
|
+
$effect(() => {
|
|
39
|
+
const video = videoElement;
|
|
40
|
+
if (!video) return;
|
|
41
|
+
if (playing) {
|
|
42
|
+
video.play().catch(() => {});
|
|
43
|
+
} else {
|
|
44
|
+
video.pause();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<video
|
|
50
|
+
bind:this={videoElement}
|
|
51
|
+
src={source}
|
|
52
|
+
width={src.width}
|
|
53
|
+
height={src.height}
|
|
54
|
+
{loop}
|
|
55
|
+
{muted}
|
|
56
|
+
{controls}
|
|
57
|
+
playsinline
|
|
58
|
+
preload="auto"
|
|
59
|
+
class={className}
|
|
60
|
+
></video>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { SliceInsets, SliceManifestEntry } from "#fundus/core";
|
|
2
|
+
|
|
3
|
+
//#region src/geometry.d.ts
|
|
4
|
+
/** One cell to paint: a source rect (px) drawn into a dest rect (CSS px). */
|
|
5
|
+
interface RenderCell {
|
|
6
|
+
key: string;
|
|
7
|
+
/** Source rect, in source pixels. */
|
|
8
|
+
sx: number;
|
|
9
|
+
sy: number;
|
|
10
|
+
sw: number;
|
|
11
|
+
sh: number;
|
|
12
|
+
/** Destination rect within the box, in CSS pixels (integers, abutting). */
|
|
13
|
+
dx: number;
|
|
14
|
+
dy: number;
|
|
15
|
+
dw: number;
|
|
16
|
+
dh: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The insets that actually apply for the entry's mode: a 3-slice mode zeroes out
|
|
20
|
+
* the axis it does not use (and `one` uses none at all), so callers never have
|
|
21
|
+
* to special-case the mode.
|
|
22
|
+
*/
|
|
23
|
+
declare function effectiveInsets(entry: SliceManifestEntry): SliceInsets;
|
|
24
|
+
/**
|
|
25
|
+
* The overdraw that actually applies: zeros when absent, clamped so a core of at
|
|
26
|
+
* least one source pixel remains on each axis.
|
|
27
|
+
*/
|
|
28
|
+
declare function effectiveOverdraw(entry: SliceManifestEntry): SliceInsets;
|
|
29
|
+
/**
|
|
30
|
+
* The overdraw's per-side destination size in CSS pixels for a box — how far the
|
|
31
|
+
* painted slice extends past the layout box on each side. Rounded to integers so
|
|
32
|
+
* cells keep abutting on integer pixels. For a 9-slice the overdraw is a fixed
|
|
33
|
+
* band (÷ `pixelRatio`, like the corners); for the 3-slice modes it scales with
|
|
34
|
+
* the caps: box cross-axis ÷ the layout box (image minus overdraw). In `one`
|
|
35
|
+
* mode the image stretches per axis, so each axis scales independently.
|
|
36
|
+
*/
|
|
37
|
+
declare function computeOverdrawDestination(entry: SliceManifestEntry, boxWidth: number, boxHeight: number): SliceInsets;
|
|
38
|
+
/**
|
|
39
|
+
* How the canvas backend's `<canvas>` relates to the layout box: with overdraw
|
|
40
|
+
* the canvas is enlarged by the overdraw's destination size and offset
|
|
41
|
+
* negatively, so the baked shadow/glow paints outside the layout box while the
|
|
42
|
+
* wrapper (and therefore the layout) keeps the box size. The backing store is
|
|
43
|
+
* the CSS size × `devicePixelRatio` (rounded) for crispness, and the paint
|
|
44
|
+
* origin sits at the layout box's top-left in device pixels — so the (possibly
|
|
45
|
+
* negative) cell coordinates from `computeRenderCells` apply directly.
|
|
46
|
+
*/
|
|
47
|
+
interface CanvasPaintLayout {
|
|
48
|
+
/** CSS offset of the canvas within the wrapper (0 or negative — overdraw). */
|
|
49
|
+
cssLeft: number;
|
|
50
|
+
cssTop: number;
|
|
51
|
+
/** CSS size of the canvas (box expanded by the overdraw's destination size). */
|
|
52
|
+
cssWidth: number;
|
|
53
|
+
cssHeight: number;
|
|
54
|
+
/** Backing-store size in device pixels (CSS size × devicePixelRatio). */
|
|
55
|
+
backingWidth: number;
|
|
56
|
+
backingHeight: number;
|
|
57
|
+
/** Paint origin (the layout box's top-left) in device pixels. */
|
|
58
|
+
originX: number;
|
|
59
|
+
originY: number;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The canvas geometry for a slice filling a `boxWidth × boxHeight` layout box
|
|
63
|
+
* at the given `devicePixelRatio` (the display's, not the asset's export
|
|
64
|
+
* scale). Pure — the canvas backend derives every size and offset from this.
|
|
65
|
+
*/
|
|
66
|
+
declare function computeCanvasPaintLayout(entry: SliceManifestEntry, boxWidth: number, boxHeight: number, devicePixelRatio: number): CanvasPaintLayout;
|
|
67
|
+
/**
|
|
68
|
+
* Compute the cells to paint for a slice filling a `boxWidth × boxHeight` box.
|
|
69
|
+
* Returns an empty array for a zero-size box. With overdraw the cells cover the
|
|
70
|
+
* box expanded by `computeOverdrawDestination` on each side, so `dx`/`dy` can be
|
|
71
|
+
* negative and cells can extend past the box.
|
|
72
|
+
*/
|
|
73
|
+
declare function computeRenderCells(entry: SliceManifestEntry, boxWidth: number, boxHeight: number): RenderCell[];
|
|
74
|
+
//#endregion
|
|
75
|
+
export { CanvasPaintLayout, RenderCell, computeCanvasPaintLayout, computeOverdrawDestination, computeRenderCells, effectiveInsets, effectiveOverdraw };
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { createZeroInsets, slicedAxesFor } from "#fundus/core";
|
|
2
|
+
//#region src/geometry.ts
|
|
3
|
+
function clamp(value, min, max) {
|
|
4
|
+
return Math.max(min, Math.min(max, value));
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The insets that actually apply for the entry's mode: a 3-slice mode zeroes out
|
|
8
|
+
* the axis it does not use (and `one` uses none at all), so callers never have
|
|
9
|
+
* to special-case the mode.
|
|
10
|
+
*/
|
|
11
|
+
function effectiveInsets(entry) {
|
|
12
|
+
const { mode, insets } = entry.slicing;
|
|
13
|
+
const axes = slicedAxesFor(mode);
|
|
14
|
+
return {
|
|
15
|
+
top: axes.vertical ? insets.top : 0,
|
|
16
|
+
bottom: axes.vertical ? insets.bottom : 0,
|
|
17
|
+
left: axes.horizontal ? insets.left : 0,
|
|
18
|
+
right: axes.horizontal ? insets.right : 0
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The overdraw that actually applies: zeros when absent, clamped so a core of at
|
|
23
|
+
* least one source pixel remains on each axis.
|
|
24
|
+
*/
|
|
25
|
+
function effectiveOverdraw(entry) {
|
|
26
|
+
const overdraw = entry.slicing.overdraw;
|
|
27
|
+
if (!overdraw) return createZeroInsets();
|
|
28
|
+
const width = entry.sourceWidth;
|
|
29
|
+
const height = entry.sourceHeight;
|
|
30
|
+
const left = clamp(overdraw.left, 0, Math.max(0, width - 1));
|
|
31
|
+
const right = clamp(overdraw.right, 0, Math.max(0, width - left - 1));
|
|
32
|
+
const top = clamp(overdraw.top, 0, Math.max(0, height - 1));
|
|
33
|
+
return {
|
|
34
|
+
top,
|
|
35
|
+
right,
|
|
36
|
+
bottom: clamp(overdraw.bottom, 0, Math.max(0, height - top - 1)),
|
|
37
|
+
left
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The layout box — the image minus its effective overdraw — in **source pixels**
|
|
42
|
+
* Never smaller than one source pixel per axis (`effectiveOverdraw` guarantees
|
|
43
|
+
* a remaining box).
|
|
44
|
+
*/
|
|
45
|
+
function sourceLayoutSize(entry) {
|
|
46
|
+
const overdraw = effectiveOverdraw(entry);
|
|
47
|
+
return {
|
|
48
|
+
width: Math.max(1, entry.sourceWidth - overdraw.left - overdraw.right),
|
|
49
|
+
height: Math.max(1, entry.sourceHeight - overdraw.top - overdraw.bottom)
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The overdraw's per-side destination size in CSS pixels for a box — how far the
|
|
54
|
+
* painted slice extends past the layout box on each side. Rounded to integers so
|
|
55
|
+
* cells keep abutting on integer pixels. For a 9-slice the overdraw is a fixed
|
|
56
|
+
* band (÷ `pixelRatio`, like the corners); for the 3-slice modes it scales with
|
|
57
|
+
* the caps: box cross-axis ÷ the layout box (image minus overdraw). In `one`
|
|
58
|
+
* mode the image stretches per axis, so each axis scales independently.
|
|
59
|
+
*/
|
|
60
|
+
function computeOverdrawDestination(entry, boxWidth, boxHeight) {
|
|
61
|
+
const overdraw = effectiveOverdraw(entry);
|
|
62
|
+
if (overdraw.top === 0 && overdraw.right === 0 && overdraw.bottom === 0 && overdraw.left === 0) return overdraw;
|
|
63
|
+
const pixelRatio = entry.slicing.pixelRatio > 0 ? entry.slicing.pixelRatio : 1;
|
|
64
|
+
const sourceLayout = sourceLayoutSize(entry);
|
|
65
|
+
let horizontalScale;
|
|
66
|
+
let verticalScale;
|
|
67
|
+
if (entry.slicing.mode === "three-horizontal") horizontalScale = verticalScale = boxHeight / sourceLayout.height;
|
|
68
|
+
else if (entry.slicing.mode === "three-vertical") horizontalScale = verticalScale = boxWidth / sourceLayout.width;
|
|
69
|
+
else if (entry.slicing.mode === "one") {
|
|
70
|
+
horizontalScale = boxWidth / sourceLayout.width;
|
|
71
|
+
verticalScale = boxHeight / sourceLayout.height;
|
|
72
|
+
} else horizontalScale = verticalScale = 1 / pixelRatio;
|
|
73
|
+
return {
|
|
74
|
+
top: Math.round(overdraw.top * verticalScale),
|
|
75
|
+
right: Math.round(overdraw.right * horizontalScale),
|
|
76
|
+
bottom: Math.round(overdraw.bottom * verticalScale),
|
|
77
|
+
left: Math.round(overdraw.left * horizontalScale)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The canvas geometry for a slice filling a `boxWidth × boxHeight` layout box
|
|
82
|
+
* at the given `devicePixelRatio` (the display's, not the asset's export
|
|
83
|
+
* scale). Pure — the canvas backend derives every size and offset from this.
|
|
84
|
+
*/
|
|
85
|
+
function computeCanvasPaintLayout(entry, boxWidth, boxHeight, devicePixelRatio) {
|
|
86
|
+
const overdrawDestination = computeOverdrawDestination(entry, boxWidth, boxHeight);
|
|
87
|
+
const cssWidth = boxWidth + overdrawDestination.left + overdrawDestination.right;
|
|
88
|
+
const cssHeight = boxHeight + overdrawDestination.top + overdrawDestination.bottom;
|
|
89
|
+
return {
|
|
90
|
+
cssLeft: -overdrawDestination.left + 0,
|
|
91
|
+
cssTop: -overdrawDestination.top + 0,
|
|
92
|
+
cssWidth,
|
|
93
|
+
cssHeight,
|
|
94
|
+
backingWidth: Math.round(cssWidth * devicePixelRatio),
|
|
95
|
+
backingHeight: Math.round(cssHeight * devicePixelRatio),
|
|
96
|
+
originX: overdrawDestination.left * devicePixelRatio,
|
|
97
|
+
originY: overdrawDestination.top * devicePixelRatio
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** Cumulative, rounded edges so adjacent cells abut exactly on integer pixels. */
|
|
101
|
+
function cumulativeEdges(sizes) {
|
|
102
|
+
let accumulated = 0;
|
|
103
|
+
const edges = [0];
|
|
104
|
+
for (const size of sizes) {
|
|
105
|
+
accumulated += size;
|
|
106
|
+
edges.push(Math.round(accumulated));
|
|
107
|
+
}
|
|
108
|
+
return edges;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Compute the cells to paint for a slice filling a `boxWidth × boxHeight` box.
|
|
112
|
+
* Returns an empty array for a zero-size box. With overdraw the cells cover the
|
|
113
|
+
* box expanded by `computeOverdrawDestination` on each side, so `dx`/`dy` can be
|
|
114
|
+
* negative and cells can extend past the box.
|
|
115
|
+
*/
|
|
116
|
+
function computeRenderCells(entry, boxWidth, boxHeight) {
|
|
117
|
+
if (boxWidth <= 0 || boxHeight <= 0) return [];
|
|
118
|
+
const overdrawDestination = computeOverdrawDestination(entry, boxWidth, boxHeight);
|
|
119
|
+
const paintWidth = boxWidth + overdrawDestination.left + overdrawDestination.right;
|
|
120
|
+
const paintHeight = boxHeight + overdrawDestination.top + overdrawDestination.bottom;
|
|
121
|
+
const width = entry.sourceWidth;
|
|
122
|
+
const height = entry.sourceHeight;
|
|
123
|
+
const pixelRatio = entry.slicing.pixelRatio > 0 ? entry.slicing.pixelRatio : 1;
|
|
124
|
+
const mode = entry.slicing.mode;
|
|
125
|
+
const insets = effectiveInsets(entry);
|
|
126
|
+
const left = clamp(insets.left, 0, width);
|
|
127
|
+
const right = clamp(insets.right, 0, Math.max(0, width - left));
|
|
128
|
+
const top = clamp(insets.top, 0, height);
|
|
129
|
+
const bottom = clamp(insets.bottom, 0, Math.max(0, height - top));
|
|
130
|
+
const centerWidth = Math.max(0, width - left - right);
|
|
131
|
+
const centerHeight = Math.max(0, height - top - bottom);
|
|
132
|
+
let leftDestination;
|
|
133
|
+
let rightDestination;
|
|
134
|
+
if (mode === "three-horizontal") {
|
|
135
|
+
const scale = paintHeight / height;
|
|
136
|
+
leftDestination = left * scale;
|
|
137
|
+
rightDestination = right * scale;
|
|
138
|
+
} else {
|
|
139
|
+
leftDestination = left / pixelRatio;
|
|
140
|
+
rightDestination = right / pixelRatio;
|
|
141
|
+
}
|
|
142
|
+
const centerDestinationWidth = Math.max(0, paintWidth - leftDestination - rightDestination);
|
|
143
|
+
let topDestination;
|
|
144
|
+
let bottomDestination;
|
|
145
|
+
if (mode === "three-vertical") {
|
|
146
|
+
const scale = paintWidth / width;
|
|
147
|
+
topDestination = top * scale;
|
|
148
|
+
bottomDestination = bottom * scale;
|
|
149
|
+
} else {
|
|
150
|
+
topDestination = top / pixelRatio;
|
|
151
|
+
bottomDestination = bottom / pixelRatio;
|
|
152
|
+
}
|
|
153
|
+
const centerDestinationHeight = Math.max(0, paintHeight - topDestination - bottomDestination);
|
|
154
|
+
const sourceColumns = [
|
|
155
|
+
left,
|
|
156
|
+
centerWidth,
|
|
157
|
+
right
|
|
158
|
+
];
|
|
159
|
+
const sourceRows = [
|
|
160
|
+
top,
|
|
161
|
+
centerHeight,
|
|
162
|
+
bottom
|
|
163
|
+
];
|
|
164
|
+
const sourceColumnStarts = [
|
|
165
|
+
0,
|
|
166
|
+
left,
|
|
167
|
+
left + centerWidth
|
|
168
|
+
];
|
|
169
|
+
const sourceRowStarts = [
|
|
170
|
+
0,
|
|
171
|
+
top,
|
|
172
|
+
top + centerHeight
|
|
173
|
+
];
|
|
174
|
+
const columnEdges = cumulativeEdges([
|
|
175
|
+
leftDestination,
|
|
176
|
+
centerDestinationWidth,
|
|
177
|
+
rightDestination
|
|
178
|
+
]);
|
|
179
|
+
const rowEdges = cumulativeEdges([
|
|
180
|
+
topDestination,
|
|
181
|
+
centerDestinationHeight,
|
|
182
|
+
bottomDestination
|
|
183
|
+
]);
|
|
184
|
+
const cells = [];
|
|
185
|
+
for (let row = 0; row < 3; row++) for (let column = 0; column < 3; column++) {
|
|
186
|
+
const sw = sourceColumns[column];
|
|
187
|
+
const sh = sourceRows[row];
|
|
188
|
+
if (sw <= 0 || sh <= 0) continue;
|
|
189
|
+
const dx = columnEdges[column];
|
|
190
|
+
const dw = columnEdges[column + 1] - dx;
|
|
191
|
+
const dy = rowEdges[row];
|
|
192
|
+
const dh = rowEdges[row + 1] - dy;
|
|
193
|
+
if (dw <= 0 || dh <= 0) continue;
|
|
194
|
+
cells.push({
|
|
195
|
+
key: `${row}-${column}`,
|
|
196
|
+
sx: sourceColumnStarts[column],
|
|
197
|
+
sy: sourceRowStarts[row],
|
|
198
|
+
sw,
|
|
199
|
+
sh,
|
|
200
|
+
dx: dx - overdrawDestination.left,
|
|
201
|
+
dy: dy - overdrawDestination.top,
|
|
202
|
+
dw,
|
|
203
|
+
dh
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return cells;
|
|
207
|
+
}
|
|
208
|
+
//#endregion
|
|
209
|
+
export { computeCanvasPaintLayout, computeOverdrawDestination, computeRenderCells, effectiveInsets, effectiveOverdraw };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Manifest, createManifest } from "./manifest.js";
|
|
2
|
+
import { resolveRetainedSource } from "./preload-registry.js";
|
|
3
|
+
import { audioSourceOf } from "./kind-handlers.js";
|
|
4
|
+
import { AudioManifestEntry, AudioPreloadOptions, ChromaKeyParameters, ChromaKeyVideoManifestEntry, ChromaKeyVideoPreloadOptions, DEFAULT_CHROMA_KEY_PARAMETERS, ImageManifestEntry, ImagePreloadOptions, ManifestEntry, PreloadOptions, SliceManifestEntry, SlicePreloadOptions, VideoManifestEntry, VideoPreloadOptions } from "#fundus/core";
|
|
5
|
+
import Image from "./components/Image.svelte";
|
|
6
|
+
import Slice from "./components/Slice.svelte";
|
|
7
|
+
import Video from "./components/Video.svelte";
|
|
8
|
+
import ChromaKeyVideo from "./components/ChromaKeyVideo.svelte";
|
|
9
|
+
export { type AudioManifestEntry, type AudioPreloadOptions, type ChromaKeyParameters, ChromaKeyVideo, type ChromaKeyVideoManifestEntry, type ChromaKeyVideoPreloadOptions, DEFAULT_CHROMA_KEY_PARAMETERS, Image, type ImageManifestEntry, type ImagePreloadOptions, Manifest, type ManifestEntry, type PreloadOptions, Slice, type SliceManifestEntry, type SlicePreloadOptions, Video, type VideoManifestEntry, type VideoPreloadOptions, audioSourceOf, createManifest, resolveRetainedSource };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { audioSourceOf } from "./kind-handlers.js";
|
|
2
|
+
import { resolveRetainedSource } from "./preload-registry.js";
|
|
3
|
+
import { Manifest, createManifest } from "./manifest.js";
|
|
4
|
+
import { DEFAULT_CHROMA_KEY_PARAMETERS } from "#fundus/core";
|
|
5
|
+
import Image from "./components/Image.svelte";
|
|
6
|
+
import Slice from "./components/Slice.svelte";
|
|
7
|
+
import Video from "./components/Video.svelte";
|
|
8
|
+
import ChromaKeyVideo from "./components/ChromaKeyVideo.svelte";
|
|
9
|
+
export { ChromaKeyVideo, DEFAULT_CHROMA_KEY_PARAMETERS, Image, Manifest, Slice, Video, audioSourceOf, createManifest, resolveRetainedSource };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { AudioManifestEntry, PreloadOptions } from "#fundus/core";
|
|
2
|
+
|
|
3
|
+
//#region src/kind-handlers.d.ts
|
|
4
|
+
/** The playable URL hosts feed to their own audio engine. */
|
|
5
|
+
declare function audioSourceOf(entry: AudioManifestEntry): string;
|
|
6
|
+
/**
|
|
7
|
+
* What a handler's `preload` acquired and its `release` must dispose. For
|
|
8
|
+
* retained media kinds this is the minted object URL; cache-backed kinds
|
|
9
|
+
* (image, slice, unknown) hold no disposable resources.
|
|
10
|
+
*/
|
|
11
|
+
interface KindHandlerResources {
|
|
12
|
+
/** Object URL minted over the fully fetched file (retained media kinds). */
|
|
13
|
+
objectUrl?: string;
|
|
14
|
+
}
|
|
15
|
+
/** How one asset kind preloads — and releases — its delivered file. */
|
|
16
|
+
interface KindRuntimeHandler {
|
|
17
|
+
/**
|
|
18
|
+
* Fully load one source of this kind — the promise resolves only once the
|
|
19
|
+
* file is completely transferred (plus the kind's strategy: decoded /
|
|
20
|
+
* retained as an object URL) — and return the resources `release` disposes.
|
|
21
|
+
*/
|
|
22
|
+
preload(url: string, options: PreloadOptions): Promise<KindHandlerResources>;
|
|
23
|
+
/**
|
|
24
|
+
* The inverse of `preload`: dispose the resources it returned (revoke the
|
|
25
|
+
* object URL). Omitted = the kind acquires nothing disposable.
|
|
26
|
+
*/
|
|
27
|
+
release?(resources: KindHandlerResources): void;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Entry kinds whose delivered file is retained as an object URL by
|
|
31
|
+
* `preload()` while held. A chroma-key-video's own `src` is a video file, so
|
|
32
|
+
* it retains too; its inlined mask/fallback entries are `image` kind and
|
|
33
|
+
* follow the image handler via the source walk.
|
|
34
|
+
*/
|
|
35
|
+
declare const RETAINED_KINDS: ReadonlySet<string>;
|
|
36
|
+
/**
|
|
37
|
+
* Whether a source of `kind` decodes under the given per-kind preload options
|
|
38
|
+
* (options are namespaced per plugin type, like `plugins.<type>` in
|
|
39
|
+
* the config). Image-backed kinds decode by default; each kind opts out
|
|
40
|
+
* independently (`{ image: { decode: false } }` leaves slices decoding).
|
|
41
|
+
*/
|
|
42
|
+
declare function shouldDecode(kind: string, options: PreloadOptions): boolean;
|
|
43
|
+
/** The runtime handler owning `kind`'s preload/release strategy (fallback for unknown kinds). */
|
|
44
|
+
declare function runtimeHandlerFor(kind: string): KindRuntimeHandler;
|
|
45
|
+
//#endregion
|
|
46
|
+
export { KindHandlerResources, KindRuntimeHandler, RETAINED_KINDS, audioSourceOf, runtimeHandlerFor, shouldDecode };
|