fundus 0.22.1 → 0.24.0

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 (102) hide show
  1. package/README.md +119 -7
  2. package/build/client/_app/immutable/chunks/CCIxsMRK.js +1 -0
  3. package/build/client/_app/immutable/chunks/CCIxsMRK.js.br +0 -0
  4. package/build/client/_app/immutable/chunks/CCIxsMRK.js.gz +0 -0
  5. package/build/client/_app/immutable/chunks/{BrWiKjrr.js → CIZHDrUF.js} +1 -1
  6. package/build/client/_app/immutable/chunks/CIZHDrUF.js.br +0 -0
  7. package/build/client/_app/immutable/chunks/CIZHDrUF.js.gz +0 -0
  8. package/build/client/_app/immutable/chunks/{D68C0q2O.js → CnnMN4wM.js} +7 -7
  9. package/build/client/_app/immutable/chunks/CnnMN4wM.js.br +0 -0
  10. package/build/client/_app/immutable/chunks/CnnMN4wM.js.gz +0 -0
  11. package/build/client/_app/immutable/entry/{app.C6utCKyq.js → app.BPBGxhbo.js} +2 -2
  12. package/build/client/_app/immutable/entry/app.BPBGxhbo.js.br +0 -0
  13. package/build/client/_app/immutable/entry/app.BPBGxhbo.js.gz +0 -0
  14. package/build/client/_app/immutable/entry/start.CTftTL5f.js +1 -0
  15. package/build/client/_app/immutable/entry/start.CTftTL5f.js.br +0 -0
  16. package/build/client/_app/immutable/entry/start.CTftTL5f.js.gz +0 -0
  17. package/build/client/_app/immutable/nodes/{0.D3X2aDLe.js → 0.CquXHiCP.js} +1 -1
  18. package/build/client/_app/immutable/nodes/0.CquXHiCP.js.br +0 -0
  19. package/build/client/_app/immutable/nodes/0.CquXHiCP.js.gz +0 -0
  20. package/build/client/_app/immutable/nodes/{1.CoCDztxo.js → 1.xSi5hUiK.js} +1 -1
  21. package/build/client/_app/immutable/nodes/1.xSi5hUiK.js.br +1 -0
  22. package/build/client/_app/immutable/nodes/1.xSi5hUiK.js.gz +0 -0
  23. package/build/client/_app/immutable/nodes/{2.D6Bsk2OP.js → 2.CgzHO6l0.js} +1 -1
  24. package/build/client/_app/immutable/nodes/2.CgzHO6l0.js.br +0 -0
  25. package/build/client/_app/immutable/nodes/2.CgzHO6l0.js.gz +0 -0
  26. package/build/client/_app/immutable/nodes/{3.ZFuSk2Qg.js → 3.B-HZ9QO3.js} +1 -1
  27. package/build/client/_app/immutable/nodes/3.B-HZ9QO3.js.br +0 -0
  28. package/build/client/_app/immutable/nodes/3.B-HZ9QO3.js.gz +0 -0
  29. package/build/client/_app/immutable/nodes/{4.CGsGyx14.js → 4._rTlKXDo.js} +1 -1
  30. package/build/client/_app/immutable/nodes/4._rTlKXDo.js.br +0 -0
  31. package/build/client/_app/immutable/nodes/{4.CGsGyx14.js.gz → 4._rTlKXDo.js.gz} +0 -0
  32. package/build/client/_app/version.json +1 -1
  33. package/build/client/_app/version.json.br +0 -0
  34. package/build/client/_app/version.json.gz +0 -0
  35. package/build/handler.js +7 -7
  36. package/build/index.js +7 -7
  37. package/build/server/chunks/chunks/{exports.js-BO7CNNsS.js → exports.js-CcnW-JJJ.js} +2 -2
  38. package/build/server/chunks/chunks/{exports.js-BO7CNNsS.js.map → exports.js-CcnW-JJJ.js.map} +1 -1
  39. package/build/server/chunks/chunks/{internal.js-CFdkiEzR.js → internal.js-ByBTomBV.js} +2 -2
  40. package/build/server/chunks/chunks/{internal.js-CFdkiEzR.js.map → internal.js-ByBTomBV.js.map} +1 -1
  41. package/build/server/chunks/chunks/internal2.js-BlXt2rfE.js.map +1 -1
  42. package/build/server/chunks/chunks/{shared.js-CwOc93ku.js → shared.js-CCqNepI3.js} +2 -2
  43. package/build/server/chunks/chunks/{shared.js-CwOc93ku.js.map → shared.js-CCqNepI3.js.map} +1 -1
  44. package/build/server/chunks/chunks/{utils.js-CWPwVcP0.js → utils.js-GaDYRoLP.js} +3 -3
  45. package/build/server/chunks/chunks/{utils.js-CWPwVcP0.js.map → utils.js-GaDYRoLP.js.map} +1 -1
  46. package/build/server/chunks/{handler-lAULIciq.js → handler-DegMVxhC.js} +5 -5
  47. package/build/server/chunks/{handler-lAULIciq.js.map → handler-DegMVxhC.js.map} +1 -1
  48. package/build/server/chunks/{index.js-BZzPujOC.js → index.js-BrCnc8Gq.js} +5 -5
  49. package/build/server/chunks/{index.js-BZzPujOC.js.map → index.js-BrCnc8Gq.js.map} +1 -1
  50. package/build/server/chunks/{manifest.js-B45gle3C.js → manifest.js-mKxXvxuB.js} +7 -7
  51. package/build/server/chunks/{manifest.js-B45gle3C.js.map → manifest.js-mKxXvxuB.js.map} +1 -1
  52. package/build/server/chunks/nodes/{0.js-CeflwKeh.js → 0.js-Bnw_8E4c.js} +2 -2
  53. package/build/server/chunks/nodes/{0.js-CeflwKeh.js.map → 0.js-Bnw_8E4c.js.map} +1 -1
  54. package/build/server/chunks/nodes/1.js-DpvNtg1z.js +7 -0
  55. package/build/server/chunks/nodes/{1.js-BokZtd9m.js.map → 1.js-DpvNtg1z.js.map} +1 -1
  56. package/build/server/chunks/nodes/{2.js-DDXiE5Ra.js → 2.js-h7fLpCKT.js} +2 -2
  57. package/build/server/chunks/nodes/{2.js-DDXiE5Ra.js.map → 2.js-h7fLpCKT.js.map} +1 -1
  58. package/build/server/chunks/nodes/3.js-XlL1_Sjt.js +7 -0
  59. package/build/server/chunks/nodes/{3.js-BZ_k58Ty.js.map → 3.js-XlL1_Sjt.js.map} +1 -1
  60. package/build/server/chunks/nodes/{4.js-C4Z183Ra.js → 4.js-BRkm0s7e.js} +2 -2
  61. package/build/server/chunks/nodes/{4.js-C4Z183Ra.js.map → 4.js-BRkm0s7e.js.map} +1 -1
  62. package/dist/core/manifest-entry.d.ts +6 -7
  63. package/dist/runtime/canvas.d.ts +37 -0
  64. package/dist/runtime/canvas.js +71 -0
  65. package/dist/runtime/components/Slice.svelte +12 -23
  66. package/dist/runtime/geometry.d.ts +61 -33
  67. package/dist/runtime/geometry.js +54 -38
  68. package/dist/runtime/index.d.ts +6 -4
  69. package/dist/runtime/index.js +3 -3
  70. package/dist/runtime/kind-handlers.d.ts +5 -30
  71. package/dist/runtime/kind-handlers.js +16 -31
  72. package/dist/runtime/manifest.d.ts +13 -15
  73. package/dist/runtime/manifest.js +23 -23
  74. package/dist/runtime/preload-registry.d.ts +7 -26
  75. package/dist/runtime/preload-registry.js +27 -8
  76. package/dist/runtime/retained-entry.d.ts +48 -0
  77. package/dist/runtime/retained-entry.js +61 -0
  78. package/dist/runtime/slice-source-lifecycle.js +2 -2
  79. package/package.json +1 -1
  80. package/build/client/_app/immutable/chunks/BrWiKjrr.js.br +0 -0
  81. package/build/client/_app/immutable/chunks/BrWiKjrr.js.gz +0 -0
  82. package/build/client/_app/immutable/chunks/CK_6irtk.js +0 -1
  83. package/build/client/_app/immutable/chunks/CK_6irtk.js.br +0 -0
  84. package/build/client/_app/immutable/chunks/CK_6irtk.js.gz +0 -0
  85. package/build/client/_app/immutable/chunks/D68C0q2O.js.br +0 -0
  86. package/build/client/_app/immutable/chunks/D68C0q2O.js.gz +0 -0
  87. package/build/client/_app/immutable/entry/app.C6utCKyq.js.br +0 -0
  88. package/build/client/_app/immutable/entry/app.C6utCKyq.js.gz +0 -0
  89. package/build/client/_app/immutable/entry/start.BaAqfP-b.js +0 -1
  90. package/build/client/_app/immutable/entry/start.BaAqfP-b.js.br +0 -0
  91. package/build/client/_app/immutable/entry/start.BaAqfP-b.js.gz +0 -0
  92. package/build/client/_app/immutable/nodes/0.D3X2aDLe.js.br +0 -0
  93. package/build/client/_app/immutable/nodes/0.D3X2aDLe.js.gz +0 -0
  94. package/build/client/_app/immutable/nodes/1.CoCDztxo.js.br +0 -1
  95. package/build/client/_app/immutable/nodes/1.CoCDztxo.js.gz +0 -0
  96. package/build/client/_app/immutable/nodes/2.D6Bsk2OP.js.br +0 -0
  97. package/build/client/_app/immutable/nodes/2.D6Bsk2OP.js.gz +0 -0
  98. package/build/client/_app/immutable/nodes/3.ZFuSk2Qg.js.br +0 -0
  99. package/build/client/_app/immutable/nodes/3.ZFuSk2Qg.js.gz +0 -0
  100. package/build/client/_app/immutable/nodes/4.CGsGyx14.js.br +0 -0
  101. package/build/server/chunks/nodes/1.js-BokZtd9m.js +0 -7
  102. package/build/server/chunks/nodes/3.js-BZ_k58Ty.js +0 -7
@@ -145,6 +145,9 @@ export function computeCanvasPaintLayout(entry, boxWidth, boxHeight, renderRatio
145
145
  originY: overdrawDestination.top * renderRatio
146
146
  };
147
147
  }
148
+ function mapEdges(edges, transform) {
149
+ return [transform(edges[0]), transform(edges[1]), transform(edges[2]), transform(edges[3])];
150
+ }
148
151
  /** Cumulative, rounded edges so adjacent cells abut exactly on integer pixels. */
149
152
  function cumulativeEdges(sizes) {
150
153
  let accumulated = 0;
@@ -156,15 +159,14 @@ function cumulativeEdges(sizes) {
156
159
  return edges;
157
160
  }
158
161
  /**
159
- * Compute the cells to paint for a slice filling a `boxWidth × boxHeight` box.
160
- * Returns an empty array for a zero-size box. With overdraw the cells cover the
161
- * box expanded by `computeOverdrawDestination` on each side, so `dx`/`dy` can be
162
- * negative and cells can extend past the box.
162
+ * The grid for a slice filling an integer `boxWidth × boxHeight` box with its
163
+ * core at the origin: destination edges are integers, so adjacent cells abut
164
+ * exactly. With overdraw the grid covers the box expanded by
165
+ * `computeOverdrawDestination` on each side, so the first edges are negative
166
+ * and the last extend past the box.
163
167
  */
164
- export function computeRenderCells(entry, boxWidth, boxHeight) {
165
- if (boxWidth <= 0 || boxHeight <= 0)
166
- return [];
167
- // Render into a virtual box expanded by the overdraw, then shift every cell
168
+ function integerSliceGrid(entry, boxWidth, boxHeight) {
169
+ // Render into a virtual box expanded by the overdraw, then shift every edge
168
170
  // back so the element core lands exactly in the layout box.
169
171
  const overdrawDestination = computeOverdrawDestination(entry, boxWidth, boxHeight);
170
172
  const paintWidth = boxWidth + overdrawDestination.left + overdrawDestination.right;
@@ -209,37 +211,51 @@ export function computeRenderCells(entry, boxWidth, boxHeight) {
209
211
  bottomDestination = bottom / pixelRatio;
210
212
  }
211
213
  const centerDestinationHeight = Math.max(0, paintHeight - topDestination - bottomDestination);
212
- const sourceColumns = [left, centerWidth, right];
213
- const sourceRows = [top, centerHeight, bottom];
214
- const sourceColumnStarts = [0, left, left + centerWidth];
215
- const sourceRowStarts = [0, top, top + centerHeight];
216
214
  const columnEdges = cumulativeEdges([leftDestination, centerDestinationWidth, rightDestination]);
217
215
  const rowEdges = cumulativeEdges([topDestination, centerDestinationHeight, bottomDestination]);
218
- const cells = [];
219
- for (let row = 0; row < 3; row++) {
220
- for (let column = 0; column < 3; column++) {
221
- const sw = sourceColumns[column];
222
- const sh = sourceRows[row];
223
- if (sw <= 0 || sh <= 0)
224
- continue;
225
- const dx = columnEdges[column];
226
- const dw = columnEdges[column + 1] - dx;
227
- const dy = rowEdges[row];
228
- const dh = rowEdges[row + 1] - dy;
229
- if (dw <= 0 || dh <= 0)
230
- continue;
231
- cells.push({
232
- key: `${row}-${column}`,
233
- sx: sourceColumnStarts[column],
234
- sy: sourceRowStarts[row],
235
- sw,
236
- sh,
237
- dx: dx - overdrawDestination.left,
238
- dy: dy - overdrawDestination.top,
239
- dw,
240
- dh
241
- });
242
- }
216
+ return {
217
+ sourceX: [0, left, left + centerWidth, left + centerWidth + right],
218
+ sourceY: [0, top, top + centerHeight, top + centerHeight + bottom],
219
+ destX: mapEdges(columnEdges, (edge) => edge - overdrawDestination.left),
220
+ destY: mapEdges(rowEdges, (edge) => edge - overdrawDestination.top)
221
+ };
222
+ }
223
+ /**
224
+ * The renderer-agnostic paint grid for a slice whose core fills the box
225
+ * `(x, y, width, height)`, in any unit the caller draws in — pass device
226
+ * pixels for seam-free edges on a GPU. This is exactly the geometry `<Slice>`
227
+ * and `drawSlice` paint: the box is laid out on the integer grid
228
+ * `max(1, round(size))` per axis, then scaled back to the requested size and
229
+ * offset by `(x, y)`. Paint cells by the rule documented on `SliceGrid`.
230
+ *
231
+ * Baked-in overdraw paints outside the core box, so `destX[0]` / `destY[0]`
232
+ * may be smaller than `x` / `y` and the last edges may exceed `x + width` /
233
+ * `y + height`. The last edges also exceed the box when it is smaller than
234
+ * the fixed borders: the center clamps to zero and the borders keep their
235
+ * size.
236
+ *
237
+ * A non-positive `width` (or `height`) yields destination edges that all
238
+ * equal `x` (or `y`), so nothing is painted.
239
+ *
240
+ * @throws {RangeError} When any coordinate or dimension is not finite.
241
+ */
242
+ export function sliceGrid(entry, x, y, width, height) {
243
+ if (![x, y, width, height].every(Number.isFinite)) {
244
+ throw new RangeError('Slice grid coordinates and dimensions must be finite.');
243
245
  }
244
- return cells;
246
+ const renderWidth = width > 0 ? Math.max(1, Math.round(width)) : 0;
247
+ const renderHeight = height > 0 ? Math.max(1, Math.round(height)) : 0;
248
+ const grid = integerSliceGrid(entry, renderWidth, renderHeight);
249
+ const place = (edges, origin, size, renderSize) => {
250
+ if (renderSize === 0)
251
+ return [origin, origin, origin, origin];
252
+ const scale = size / renderSize;
253
+ return mapEdges(edges, (edge) => origin + edge * scale);
254
+ };
255
+ return {
256
+ sourceX: grid.sourceX,
257
+ sourceY: grid.sourceY,
258
+ destX: place(grid.destX, x, width, renderWidth),
259
+ destY: place(grid.destY, y, height, renderHeight)
260
+ };
245
261
  }
@@ -1,12 +1,14 @@
1
1
  export { Manifest, createManifest } from './manifest.js';
2
+ export { drawImage, drawSlice } from './canvas.js';
3
+ export type { CanvasDrawTarget } from './canvas.js';
4
+ export { retainEntry } from './retained-entry.js';
5
+ export type { RetainedEntry, RetainedSourceOf } from './retained-entry.js';
2
6
  export { default as Image } from './components/Image.svelte';
3
7
  export { default as Slice } from './components/Slice.svelte';
4
- export { sliceLayoutGeometry } from './geometry.js';
5
- export type { SliceLayoutGeometry, SliceLayoutEntry } from './geometry.js';
8
+ export { sliceGrid, sliceLayoutGeometry } from './geometry.js';
9
+ export type { SliceGrid, SliceLayoutGeometry, SliceLayoutEntry } from './geometry.js';
6
10
  export { default as Video } from './components/Video.svelte';
7
11
  export { default as ChromaKeyVideo } from './components/ChromaKeyVideo.svelte';
8
- export { resolveRetainedSource } from './preload-registry.js';
9
- export { audioSourceOf } from './kind-handlers.js';
10
12
  export { setFundusLogger } from './logging.js';
11
13
  export type { FundusLogger, RenderedSource } from './logging.js';
12
14
  export type { SourceStatus } from './preload-registry.js';
@@ -1,11 +1,11 @@
1
1
  // Supported host-facing manifest and rendering APIs for the `fundus` root export.
2
2
  export { Manifest, createManifest } from './manifest.js';
3
+ export { drawImage, drawSlice } from './canvas.js';
4
+ export { retainEntry } from './retained-entry.js';
3
5
  export { default as Image } from './components/Image.svelte';
4
6
  export { default as Slice } from './components/Slice.svelte';
5
- export { sliceLayoutGeometry } from './geometry.js';
7
+ export { sliceGrid, sliceLayoutGeometry } from './geometry.js';
6
8
  export { default as Video } from './components/Video.svelte';
7
9
  export { default as ChromaKeyVideo } from './components/ChromaKeyVideo.svelte';
8
- export { resolveRetainedSource } from './preload-registry.js';
9
- export { audioSourceOf } from './kind-handlers.js';
10
10
  export { setFundusLogger } from './logging.js';
11
11
  export { DEFAULT_CHROMA_KEY_PARAMETERS } from '../core/index.js';
@@ -1,25 +1,7 @@
1
- /**
2
- * The runtime part of the type-plugin contract: per-kind preload
3
- * strategy and its inverse release strategy. Each first-party kind registers
4
- * a handler that knows how its delivered file is loaded — images fetch +
5
- * optionally decode, slices fetch and retain a decoded form (an ImageBitmap
6
- * for multi-cell slices, the decoded element for one-slices), media kinds
7
- * (video/audio) fetch fully and mint an object URL — and how what it loaded
8
- * is disposed (revoke the object URL, close the bitmap; images have nothing
9
- * to dispose). An unknown kind falls back to a plain full fetch: no decode,
10
- * no retention.
11
- *
12
- * Handlers are pure strategy: refcounting, cross-manifest dedup, and
13
- * in-flight bookkeeping live in the module-wide registry
14
- * (preload-registry.ts); kind-agnostic orchestration lives in `Manifest`.
15
- */
16
- import type { AudioManifestEntry, PreloadOptions } from '../core/index.js';
17
- /** The playable URL hosts feed to their own audio engine. */
18
- export declare function audioSourceOf(entry: AudioManifestEntry): string;
19
1
  /**
20
2
  * What a handler's `preload` acquired and its `release` must dispose. For
21
3
  * retained media kinds this is the minted object URL, for slices the decoded
22
- * bitmap; cache-backed kinds (image, unknown) hold no disposable resources.
4
+ * bitmap; image elements and unknown kinds hold no disposable resources.
23
5
  */
24
6
  export interface KindHandlerResources {
25
7
  /** Object URL minted over the fully fetched file (retained media kinds). */
@@ -27,9 +9,9 @@ export interface KindHandlerResources {
27
9
  /** Decoded pixels of a multi-cell slice source, pinned against cache eviction. */
28
10
  imageBitmap?: ImageBitmap;
29
11
  /**
30
- * Decoded element of a one-slice source: the synchronous-commit token for
31
- * `SliceSourceLifecycle`, whose decode also warms the (evictable) browser
32
- * cache the mounted `<img>` paints from. Nothing to dispose.
12
+ * Decoded image or one-slice source, reused for canvas drawing and by
13
+ * `SliceSourceLifecycle`. Its decode also warms the browser image cache.
14
+ * Nothing to dispose; release drops the registry's reference.
33
15
  */
34
16
  image?: HTMLImageElement;
35
17
  }
@@ -42,7 +24,7 @@ export interface KindRuntimeHandler {
42
24
  * and return the resources `release` disposes. `entry` is the manifest
43
25
  * entry the source came from, for strategies keyed on entry metadata.
44
26
  */
45
- preload(url: string, byteSize: number, options: PreloadOptions, entry?: Record<string, unknown>): Promise<KindHandlerResources>;
27
+ preload(url: string, byteSize: number, entry?: Record<string, unknown>): Promise<KindHandlerResources>;
46
28
  /**
47
29
  * The inverse of `preload`: dispose the resources it returned (revoke the
48
30
  * object URL, close the bitmap). Omitted = the kind acquires nothing
@@ -51,12 +33,5 @@ export interface KindRuntimeHandler {
51
33
  release?(resources: KindHandlerResources): void;
52
34
  }
53
35
  export declare const RETAINED_KINDS: ReadonlySet<string>;
54
- /**
55
- * Whether a source of `kind` decodes under the given per-kind preload options
56
- * (options are namespaced per plugin type, like `plugins.<type>` in
57
- * the config). Image-backed kinds decode by default; each kind opts out
58
- * independently (`{ image: { decode: false } }` leaves slices decoding).
59
- */
60
- export declare function shouldDecode(kind: string, options: PreloadOptions): boolean;
61
36
  /** The runtime handler owning `kind`'s preload/release strategy (fallback for unknown kinds). */
62
37
  export declare function runtimeHandlerFor(kind: string): KindRuntimeHandler;
@@ -1,11 +1,4 @@
1
1
  import { plugins } from '../core/plugins/registry.js';
2
- import { resolveRetainedSource } from './preload-registry.js';
3
- /** The playable URL hosts feed to their own audio engine. */
4
- export function audioSourceOf(entry) {
5
- return resolveRetainedSource(entry.src);
6
- }
7
- /** Entry kinds whose delivered file is an image the browser can pre-decode. */
8
- const IMAGE_BACKED_KINDS = new Set(['image', 'slice']);
9
2
  /**
10
3
  * Entry kinds whose delivered file is retained as an object URL by
11
4
  * `preload()` while held. A chroma-key-video's own `src` is a video file, so
@@ -18,19 +11,6 @@ const RETAINED_KIND_CATALOGUE = new Set([
18
11
  'audio'
19
12
  ]);
20
13
  export const RETAINED_KINDS = new Set([...RETAINED_KIND_CATALOGUE].filter((kind) => plugins.has(kind)));
21
- /**
22
- * Whether a source of `kind` decodes under the given per-kind preload options
23
- * (options are namespaced per plugin type, like `plugins.<type>` in
24
- * the config). Image-backed kinds decode by default; each kind opts out
25
- * independently (`{ image: { decode: false } }` leaves slices decoding).
26
- */
27
- export function shouldDecode(kind, options) {
28
- if (!IMAGE_BACKED_KINDS.has(kind))
29
- return false;
30
- // The image-backed check above narrows the kind — only these carry `decode`.
31
- const kindOptions = options[kind];
32
- return kindOptions?.decode ?? true;
33
- }
34
14
  /**
35
15
  * Whether a fetch response represents a successfully delivered resource.
36
16
  *
@@ -66,23 +46,24 @@ function verifyByteSize(url, expected, actual) {
66
46
  }
67
47
  }
68
48
  /**
69
- * The image strategy: fetch the full file, then decode by default. Decoding
70
- * goes via an HTMLImageElement where a DOM exists (the browser serves the
71
- * bytes from cache); headless environments (Node tests, SSR) skip decoding.
72
- * Nothing to release — the browser cache owns the bytes, and the mounted
73
- * `<img>` decodes from it.
49
+ * The image strategy: fetch the full file, then decode it via an
50
+ * HTMLImageElement where a DOM exists (the browser serves the bytes from
51
+ * cache); headless environments (Node tests, SSR) skip decoding.
52
+ * Retains the decoded element for canvas drawing until the last holder releases.
53
+ * Nothing to explicitly dispose — release drops the registry's reference.
74
54
  */
75
55
  const imageHandler = {
76
- async preload(url, byteSize, options) {
56
+ async preload(url, byteSize) {
77
57
  const response = await fetchCompletely(url);
78
58
  // Read the body fully so the file is actually transferred (and
79
59
  // cached), not just the response headers.
80
60
  const bytes = await response.arrayBuffer();
81
61
  verifyByteSize(url, byteSize, bytes.byteLength);
82
- if (shouldDecode('image', options) && typeof globalThis.Image === 'function') {
62
+ if (typeof globalThis.Image === 'function') {
83
63
  const image = new globalThis.Image();
84
64
  image.src = url;
85
65
  await image.decode();
66
+ return { image };
86
67
  }
87
68
  return {};
88
69
  }
@@ -101,17 +82,21 @@ const imageHandler = {
101
82
  * dispose. Slice sources are small (the insets cover every rendered size),
102
83
  * so retention is cheap. Environments without `createImageBitmap` retain the
103
84
  * decoded element for every mode.
85
+ *
86
+ * Bitmaps are premultiplied so WebGL uploads match the premultiplied texture a
87
+ * retained element yields with `UNPACK_PREMULTIPLY_ALPHA_WEBGL` (bitmaps ignore
88
+ * that flag); Canvas 2D draws either form identically.
104
89
  */
105
90
  const sliceHandler = {
106
- async preload(url, byteSize, options, entry) {
91
+ async preload(url, byteSize, entry) {
107
92
  const response = await fetchCompletely(url);
108
93
  const blob = await response.blob();
109
94
  verifyByteSize(url, byteSize, blob.size);
110
- if (!shouldDecode('slice', options))
111
- return {};
112
95
  const oneSlice = entry?.slicing?.mode === 'one';
113
96
  if (!oneSlice && typeof globalThis.createImageBitmap === 'function') {
114
- return { imageBitmap: await globalThis.createImageBitmap(blob) };
97
+ return {
98
+ imageBitmap: await globalThis.createImageBitmap(blob, { premultiplyAlpha: 'premultiply' })
99
+ };
115
100
  }
116
101
  if (typeof globalThis.Image === 'function') {
117
102
  const image = new globalThis.Image();
@@ -23,6 +23,8 @@ export interface EntrySource {
23
23
  */
24
24
  entry: Record<string, unknown>;
25
25
  }
26
+ /** Reject an entry `byteSize` that is not a non-negative safe integer. */
27
+ export declare function assertEntryByteSize(src: string, byteSize: unknown): asserts byteSize is number;
26
28
  /**
27
29
  * Collect every deliverable source of an entry, including the entries codegen
28
30
  * inlined in place of asset references: a source is any nested object carrying
@@ -38,30 +40,26 @@ export declare class Manifest {
38
40
  /**
39
41
  * Fetch every deliverable file of this manifest up front — including the
40
42
  * reference closure, which codegen inlined into the entries — and decode
41
- * image-backed entries. Options are namespaced per asset kind, mirroring
42
- * `plugins.<type>` in the config: `{ image: { decode: false } }`
43
- * skips decoding for image entries only; slices keep decoding unless
44
- * `slice: { decode: false }` is passed too. An option applies to every
45
- * entry of its kind, inlined reference entries included.
43
+ * image-backed entries where a DOM exists. Options are namespaced per asset
44
+ * kind, mirroring `plugins.<type>` in the config; no kind defines any yet.
46
45
  *
47
46
  * Idempotent and concurrent-safe: one in-flight promise per manifest; a
48
47
  * second call joins it (or resolves immediately once done), and URLs shared
49
48
  * by several entries are fetched once. Sharing goes further than this
50
49
  * manifest: every URL is acquired through the module-wide refcounted
51
- * registry, so a URL another manifest already preloaded (or is preloading)
52
- * is never fetched again — this manifest just takes a hold on it. A failed
53
- * preload rolls back this manifest's holds and clears the in-flight state
54
- * so it can be retried. A joined call's options are ignored: options apply
55
- * per preload run (the first call after construction or `release()`).
50
+ * registry, so a URL another holder (a manifest or a `retainEntry` handle)
51
+ * already loaded (or is loading) is never fetched again — this manifest
52
+ * just takes a hold on it. A failed preload rolls back this manifest's
53
+ * holds and clears the in-flight state so it can be retried.
56
54
  */
57
55
  preload(options?: PreloadOptions): Promise<void>;
58
56
  /**
59
57
  * Drop this manifest's holds on everything it preloaded. Assets shared
60
- * with another preloaded manifest stay fully alive for that holder; assets
61
- * nobody holds anymore are disposed via their kind's release strategy
62
- * (retained media object URLs are revoked, slice bitmaps closed, freeing
63
- * the memory). Consumers of a fully released source fall back to its
64
- * network URL.
58
+ * with another holder (a manifest or a `retainEntry` handle) stay fully
59
+ * alive for that holder; assets nobody holds anymore are disposed via their
60
+ * kind's release strategy (retained media object URLs are revoked, slice
61
+ * bitmaps closed, decoded image references dropped). Components fall back
62
+ * to the network URL.
65
63
  *
66
64
  * Safe in every state: before or without `preload()` it is a no-op, and
67
65
  * calling it while a `preload()` is still in flight drops the holds
@@ -1,4 +1,10 @@
1
1
  import { acquireSource, releaseSource } from './preload-registry.js';
2
+ /** Reject an entry `byteSize` that is not a non-negative safe integer. */
3
+ export function assertEntryByteSize(src, byteSize) {
4
+ if (!Number.isSafeInteger(byteSize) || byteSize < 0) {
5
+ throw new Error(`Manifest entry for "${src}" requires a non-negative safe integer "byteSize".`);
6
+ }
7
+ }
2
8
  /**
3
9
  * Collect every deliverable source of an entry, including the entries codegen
4
10
  * inlined in place of asset references: a source is any nested object carrying
@@ -23,9 +29,7 @@ export function collectEntrySources(entry) {
23
29
  if (typeof record.id === 'string' &&
24
30
  typeof record.kind === 'string' &&
25
31
  typeof record.src === 'string') {
26
- if (!Number.isSafeInteger(record.byteSize) || record.byteSize < 0) {
27
- throw new Error(`Manifest entry for "${record.src}" requires a non-negative safe integer "byteSize".`);
28
- }
32
+ assertEntryByteSize(record.src, record.byteSize);
29
33
  sources.push({
30
34
  id: record.id,
31
35
  url: record.src,
@@ -43,8 +47,8 @@ export class Manifest {
43
47
  #preloading = null;
44
48
  /**
45
49
  * URLs this manifest currently holds in the module-wide registry (each
46
- * held at most once — the registry's reference count counts holding
47
- * manifests, not preload calls). Replaced wholesale by `release()`: an
50
+ * held at most once — the registry's reference count counts holders, not
51
+ * preload calls). Replaced wholesale by `release()`: an
48
52
  * in-flight preload run keeps operating on the set object it captured, so
49
53
  * a release during the run (which drains that set) turns the run's own
50
54
  * failure rollback into a no-op instead of a double release.
@@ -64,25 +68,21 @@ export class Manifest {
64
68
  /**
65
69
  * Fetch every deliverable file of this manifest up front — including the
66
70
  * reference closure, which codegen inlined into the entries — and decode
67
- * image-backed entries. Options are namespaced per asset kind, mirroring
68
- * `plugins.<type>` in the config: `{ image: { decode: false } }`
69
- * skips decoding for image entries only; slices keep decoding unless
70
- * `slice: { decode: false }` is passed too. An option applies to every
71
- * entry of its kind, inlined reference entries included.
71
+ * image-backed entries where a DOM exists. Options are namespaced per asset
72
+ * kind, mirroring `plugins.<type>` in the config; no kind defines any yet.
72
73
  *
73
74
  * Idempotent and concurrent-safe: one in-flight promise per manifest; a
74
75
  * second call joins it (or resolves immediately once done), and URLs shared
75
76
  * by several entries are fetched once. Sharing goes further than this
76
77
  * manifest: every URL is acquired through the module-wide refcounted
77
- * registry, so a URL another manifest already preloaded (or is preloading)
78
- * is never fetched again — this manifest just takes a hold on it. A failed
79
- * preload rolls back this manifest's holds and clears the in-flight state
80
- * so it can be retried. A joined call's options are ignored: options apply
81
- * per preload run (the first call after construction or `release()`).
78
+ * registry, so a URL another holder (a manifest or a `retainEntry` handle)
79
+ * already loaded (or is loading) is never fetched again — this manifest
80
+ * just takes a hold on it. A failed preload rolls back this manifest's
81
+ * holds and clears the in-flight state so it can be retried.
82
82
  */
83
83
  preload(options = {}) {
84
84
  if (!this.#preloading) {
85
- const tracked = this.#preloadAll(options, this.#heldUrls).catch((cause) => {
85
+ const tracked = this.#preloadAll(this.#heldUrls).catch((cause) => {
86
86
  // Reset for retry — unless release() or a follow-up preload()
87
87
  // already superseded this run while it was in flight.
88
88
  if (this.#preloading === tracked)
@@ -95,11 +95,11 @@ export class Manifest {
95
95
  }
96
96
  /**
97
97
  * Drop this manifest's holds on everything it preloaded. Assets shared
98
- * with another preloaded manifest stay fully alive for that holder; assets
99
- * nobody holds anymore are disposed via their kind's release strategy
100
- * (retained media object URLs are revoked, slice bitmaps closed, freeing
101
- * the memory). Consumers of a fully released source fall back to its
102
- * network URL.
98
+ * with another holder (a manifest or a `retainEntry` handle) stay fully
99
+ * alive for that holder; assets nobody holds anymore are disposed via their
100
+ * kind's release strategy (retained media object URLs are revoked, slice
101
+ * bitmaps closed, decoded image references dropped). Components fall back
102
+ * to the network URL.
103
103
  *
104
104
  * Safe in every state: before or without `preload()` it is a no-op, and
105
105
  * calling it while a `preload()` is still in flight drops the holds
@@ -117,7 +117,7 @@ export class Manifest {
117
117
  // captured it) finds nothing left to release.
118
118
  held.clear();
119
119
  }
120
- async #preloadAll(options, held) {
120
+ async #preloadAll(held) {
121
121
  const sourceByUrl = new Map();
122
122
  for (const entry of Object.values(this.#entries)) {
123
123
  for (const source of collectEntrySources(entry)) {
@@ -132,7 +132,7 @@ export class Manifest {
132
132
  const acquisitions = [];
133
133
  for (const [url, source] of sourceByUrl) {
134
134
  held.add(url);
135
- acquisitions.push(acquireSource(url, source.kind, source.byteSize, options, source.entry));
135
+ acquisitions.push(acquireSource(url, source.kind, source.byteSize, source.entry));
136
136
  }
137
137
  const outcomes = await Promise.allSettled(acquisitions);
138
138
  const failure = outcomes.find((outcome) => outcome.status === 'rejected');
@@ -1,30 +1,11 @@
1
1
  /**
2
- * The module-wide refcounted source registry: one entry per
3
- * URL — reference count, in-flight promise, loaded handler resources.
4
- *
5
- * Guarantees:
6
- * - **Once-per-URL fetches across manifests, all kinds** — a URL in flight or
7
- * loaded is never fetched again while held, including image and slice.
8
- * - **The reference count counts holding manifests, not calls** — each
9
- * `Manifest` acquires a URL at most once per hold (its held-URL set guards
10
- * idempotent `preload()` calls).
11
- * - **Release at zero disposes via the kind handler** — the object URL is
12
- * revoked and the entry cleared, so releasing one of two holders keeps the
13
- * resource fully alive for the other, and a released URL can be re-acquired
14
- * with a fresh fetch.
15
- * - **No leak on release-during-in-flight** — dropping the last hold while
16
- * the fetch is still running lets the acquisition complete, then disposes
17
- * immediately (no cancellation, no throw).
18
- */
19
- import type { PreloadOptions } from '../core/index.js';
20
- /**
21
- * Acquire one hold on `url` for a manifest: increments the reference count
2
+ * Acquire one hold on `url` for a holder: increments the reference count
22
3
  * immediately, then loads via the kind handler unless the URL is already
23
4
  * loaded (resolves at once) or in flight (joins the pending load). A failed
24
5
  * load clears the in-flight state so the URL stays retryable; the caller
25
6
  * rolls back its own hold via `releaseSource`.
26
7
  */
27
- export declare function acquireSource(url: string, kind: string, byteSize: number, options: PreloadOptions, sourceEntry?: Record<string, unknown>): Promise<void>;
8
+ export declare function acquireSource(url: string, kind: string, byteSize: number, sourceEntry?: Record<string, unknown>): Promise<void>;
28
9
  /**
29
10
  * Drop one hold on `url`. At zero holds the kind handler's release strategy
30
11
  * runs (object URL revoked) and the entry is cleared — unless the load is
@@ -33,19 +14,19 @@ export declare function acquireSource(url: string, kind: string, byteSize: numbe
33
14
  */
34
15
  export declare function releaseSource(url: string): void;
35
16
  /**
36
- * The retained object URL of a source while some manifest holds it, or the
17
+ * The retained object URL of a source while some holder holds it, or the
37
18
  * source itself when not (or no longer) preloaded — after a full release a
38
19
  * consumer falls back to the network URL and simply streams.
39
20
  */
40
21
  export declare function resolveRetainedSource(url: string): string;
41
22
  /**
42
- * The retained decoded source of a slice URL while some manifest holds it —
43
- * the pinned bitmap of a multi-cell slice, or the decoded element of a
44
- * one-slice — or `null` when not (or no longer) preloaded; the consumer then
23
+ * The retained decoded source of an image or slice URL while a holder holds
24
+ * it — a multi-cell slice bitmap or decoded image element — or `null` when
25
+ * not (or no longer) preloaded; the consumer then
45
26
  * falls back to an async image load. Never returns a closed bitmap: closing
46
27
  * happens only when the entry is dropped from the registry.
47
28
  */
48
- export declare function resolveRetainedSliceSource(url: string): ImageBitmap | HTMLImageElement | null;
29
+ export declare function resolveRetainedImageSource(url: string): ImageBitmap | HTMLImageElement | null;
49
30
  /**
50
31
  * A source's preload state, for render-time diagnostics:
51
32
  * - `loaded` — fully preloaded and held (every kind resolves non-null
@@ -1,13 +1,32 @@
1
+ /**
2
+ * The module-wide refcounted source registry: one entry per
3
+ * URL — reference count, in-flight promise, loaded handler resources.
4
+ *
5
+ * Guarantees:
6
+ * - **Once-per-URL fetches across holders, all kinds** — a URL in flight or
7
+ * loaded is never fetched again while held, including image and slice.
8
+ * - **The reference count counts holders, not calls** — a holder is a
9
+ * `Manifest` or a `retainEntry` handle. Each `Manifest` acquires a URL at
10
+ * most once per hold (its held-URL set guards idempotent `preload()` calls);
11
+ * each handle takes exactly one hold and its idempotent `release` drops it.
12
+ * - **Release at zero disposes via the kind handler** — the object URL is
13
+ * revoked and the entry cleared, so releasing one of two holders keeps the
14
+ * resource fully alive for the other, and a released URL can be re-acquired
15
+ * with a fresh fetch.
16
+ * - **No leak on release-during-in-flight** — dropping the last hold while
17
+ * the fetch is still running lets the acquisition complete, then disposes
18
+ * immediately (no cancellation, no throw).
19
+ */
1
20
  import { runtimeHandlerFor } from './kind-handlers.js';
2
21
  const registryByUrl = new Map();
3
22
  /**
4
- * Acquire one hold on `url` for a manifest: increments the reference count
23
+ * Acquire one hold on `url` for a holder: increments the reference count
5
24
  * immediately, then loads via the kind handler unless the URL is already
6
25
  * loaded (resolves at once) or in flight (joins the pending load). A failed
7
26
  * load clears the in-flight state so the URL stays retryable; the caller
8
27
  * rolls back its own hold via `releaseSource`.
9
28
  */
10
- export function acquireSource(url, kind, byteSize, options, sourceEntry) {
29
+ export function acquireSource(url, kind, byteSize, sourceEntry) {
11
30
  let entry = registryByUrl.get(url);
12
31
  if (!entry) {
13
32
  entry = { kind, byteSize, referenceCount: 0, pending: null, resources: null };
@@ -23,7 +42,7 @@ export function acquireSource(url, kind, byteSize, options, sourceEntry) {
23
42
  if (!pending) {
24
43
  const acquired = entry;
25
44
  pending = runtimeHandlerFor(kind)
26
- .preload(url, byteSize, options, sourceEntry)
45
+ .preload(url, byteSize, sourceEntry)
27
46
  .then((resources) => {
28
47
  acquired.pending = null;
29
48
  if (acquired.referenceCount === 0) {
@@ -65,7 +84,7 @@ export function releaseSource(url) {
65
84
  registryByUrl.delete(url);
66
85
  }
67
86
  /**
68
- * The retained object URL of a source while some manifest holds it, or the
87
+ * The retained object URL of a source while some holder holds it, or the
69
88
  * source itself when not (or no longer) preloaded — after a full release a
70
89
  * consumer falls back to the network URL and simply streams.
71
90
  */
@@ -73,13 +92,13 @@ export function resolveRetainedSource(url) {
73
92
  return registryByUrl.get(url)?.resources?.objectUrl ?? url;
74
93
  }
75
94
  /**
76
- * The retained decoded source of a slice URL while some manifest holds it —
77
- * the pinned bitmap of a multi-cell slice, or the decoded element of a
78
- * one-slice — or `null` when not (or no longer) preloaded; the consumer then
95
+ * The retained decoded source of an image or slice URL while a holder holds
96
+ * it — a multi-cell slice bitmap or decoded image element — or `null` when
97
+ * not (or no longer) preloaded; the consumer then
79
98
  * falls back to an async image load. Never returns a closed bitmap: closing
80
99
  * happens only when the entry is dropped from the registry.
81
100
  */
82
- export function resolveRetainedSliceSource(url) {
101
+ export function resolveRetainedImageSource(url) {
83
102
  const resources = registryByUrl.get(url)?.resources;
84
103
  return resources?.imageBitmap ?? resources?.image ?? null;
85
104
  }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Handle-based retention of one manifest entry's own source: the renderer-
3
+ * agnostic way to hold a loaded file (Canvas 2D, WebGL, audio engines)
4
+ * without depending on some manifest still being preloaded. Each handle is
5
+ * one holder in the module-wide registry (preload-registry.ts), sharing loads
6
+ * and disposal with manifests.
7
+ */
8
+ import type { ManifestEntry } from '../core/index.js';
9
+ /**
10
+ * The retained source a `RetainedEntry` exposes for its entry kind: decoded
11
+ * pixels for image-backed kinds, a playable URL (an object URL over the fully
12
+ * fetched file where supported) for media kinds.
13
+ */
14
+ export type RetainedSourceOf<E extends ManifestEntry> = E extends {
15
+ kind: 'image' | 'slice';
16
+ } ? HTMLImageElement | ImageBitmap : E extends {
17
+ kind: 'video' | 'chroma-key-video' | 'audio';
18
+ } ? string : never;
19
+ /** One hold on an entry's loaded source, from `retainEntry`. */
20
+ export interface RetainedEntry<E extends ManifestEntry> {
21
+ readonly entry: E;
22
+ /**
23
+ * The loaded source. The same object for every handle while any holder
24
+ * keeps the source loaded; a later re-retain may load a new one.
25
+ *
26
+ * @throws {Error} After `release()`.
27
+ */
28
+ readonly source: RetainedSourceOf<E>;
29
+ /** Drop this handle's hold. Idempotent; safe to call unbound (`onDestroy(handle.release)`). */
30
+ readonly release: () => void;
31
+ }
32
+ /**
33
+ * Take one hold on `entry`'s own source and resolve with a handle to it once
34
+ * loaded. An already loaded source (e.g. after `manifest.preload()`) resolves
35
+ * without loading, a source in flight is joined, and an absent one loads via
36
+ * its kind — deduplicated and cached exactly like `preload()`. Only the
37
+ * entry's own `src` is retained: a chroma-key-video's `mask` and `fallback`
38
+ * are separate entries to retain on their own.
39
+ *
40
+ * The hold lasts until `release()`, even when the source was already
41
+ * preloaded; while any holder keeps it, the source object stays the same.
42
+ * Always release, including when a caller stops waiting for the promise.
43
+ *
44
+ * @throws {Error} (rejects) When the entry's `byteSize` is invalid, the load
45
+ * fails, or an image-backed source cannot be decoded in this environment.
46
+ * A rejection leaves no hold behind.
47
+ */
48
+ export declare function retainEntry<E extends ManifestEntry>(entry: E): Promise<RetainedEntry<E>>;