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.
Files changed (61) hide show
  1. package/dist/build-CZYFLLvl.js +191 -0
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.js +1420 -0
  4. package/dist/config.d.ts +2 -0
  5. package/dist/config.js +2 -0
  6. package/dist/core/image-Dqmz8FXS.js +54 -0
  7. package/dist/core/index.d.ts +499 -0
  8. package/dist/core/index.js +962 -0
  9. package/dist/core/reference-graph-CjuHbLDw.d.ts +280 -0
  10. package/dist/core/testing.d.ts +27 -0
  11. package/dist/core/testing.js +80 -0
  12. package/dist/editor/200.html +37 -0
  13. package/dist/editor/_app/immutable/assets/0.DLhIsYA_.css +1 -0
  14. package/dist/editor/_app/immutable/assets/2.DIbbW7U1.css +1 -0
  15. package/dist/editor/_app/immutable/assets/3.C-9vFQ9A.css +1 -0
  16. package/dist/editor/_app/immutable/assets/4.BS8ElpOV.css +1 -0
  17. package/dist/editor/_app/immutable/assets/ConfirmDialog.CHEISE-L.css +1 -0
  18. package/dist/editor/_app/immutable/assets/LibraryContextMenu.CDjCmdV-.css +1 -0
  19. package/dist/editor/_app/immutable/chunks/BfPE5wdt.js +1 -0
  20. package/dist/editor/_app/immutable/chunks/Bjy-W4x2.js +81 -0
  21. package/dist/editor/_app/immutable/chunks/CNRz3sSw.js +3 -0
  22. package/dist/editor/_app/immutable/chunks/CO4RBJfa.js +1 -0
  23. package/dist/editor/_app/immutable/chunks/Cx6qtxJg.js +1 -0
  24. package/dist/editor/_app/immutable/chunks/DjdrdRzT.js +1 -0
  25. package/dist/editor/_app/immutable/chunks/DjvOiDmq.js +1 -0
  26. package/dist/editor/_app/immutable/chunks/n7mvYc42.js +5 -0
  27. package/dist/editor/_app/immutable/chunks/xihTtKlq.js +1 -0
  28. package/dist/editor/_app/immutable/entry/app.BkqbfI8n.js +2 -0
  29. package/dist/editor/_app/immutable/entry/start.C8HHOLly.js +1 -0
  30. package/dist/editor/_app/immutable/nodes/0.xdMI5GEU.js +2 -0
  31. package/dist/editor/_app/immutable/nodes/1.D9p2tfoJ.js +1 -0
  32. package/dist/editor/_app/immutable/nodes/2.BrA0Hyzu.js +3 -0
  33. package/dist/editor/_app/immutable/nodes/3.WO7tZH_d.js +104 -0
  34. package/dist/editor/_app/immutable/nodes/4.DQtx17aW.js +2 -0
  35. package/dist/editor/_app/version.json +1 -0
  36. package/dist/index.d.ts +370 -0
  37. package/dist/index.js +4 -0
  38. package/dist/operations-DYM-5KVl.js +1797 -0
  39. package/dist/runtime/chroma-key-gl.d.ts +23 -0
  40. package/dist/runtime/chroma-key-gl.js +290 -0
  41. package/dist/runtime/components/ChromaKeyVideo.svelte +225 -0
  42. package/dist/runtime/components/Image.svelte +18 -0
  43. package/dist/runtime/components/Slice.svelte +35 -0
  44. package/dist/runtime/components/SliceCanvas.svelte +162 -0
  45. package/dist/runtime/components/SliceDom.svelte +104 -0
  46. package/dist/runtime/components/Video.svelte +60 -0
  47. package/dist/runtime/geometry.d.ts +75 -0
  48. package/dist/runtime/geometry.js +209 -0
  49. package/dist/runtime/index.d.ts +9 -0
  50. package/dist/runtime/index.js +9 -0
  51. package/dist/runtime/kind-handlers.d.ts +46 -0
  52. package/dist/runtime/kind-handlers.js +86 -0
  53. package/dist/runtime/manifest.d.ts +60 -0
  54. package/dist/runtime/manifest.js +117 -0
  55. package/dist/runtime/observe-border-box-size.d.ts +5 -0
  56. package/dist/runtime/observe-border-box-size.js +12 -0
  57. package/dist/runtime/preload-registry.d.ts +26 -0
  58. package/dist/runtime/preload-registry.js +68 -0
  59. package/dist/start-Bhnqt9Dc.js +279 -0
  60. package/dist/start-D2IXOrni.js +2 -0
  61. 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 };