fundus 0.12.2 → 0.14.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 (175) hide show
  1. package/build/client/_app/immutable/assets/AssetSourceMenu.IjDdjx3o.css +1 -0
  2. package/build/client/_app/immutable/assets/AssetSourceMenu.IjDdjx3o.css.br +0 -0
  3. package/build/client/_app/immutable/assets/AssetSourceMenu.IjDdjx3o.css.gz +0 -0
  4. package/build/client/_app/immutable/chunks/77p2oEJ1.js +1 -0
  5. package/build/client/_app/immutable/chunks/77p2oEJ1.js.br +0 -0
  6. package/build/client/_app/immutable/chunks/77p2oEJ1.js.gz +0 -0
  7. package/build/client/_app/immutable/chunks/B_PWcB6g.js +108 -0
  8. package/build/client/_app/immutable/chunks/B_PWcB6g.js.br +0 -0
  9. package/build/client/_app/immutable/chunks/B_PWcB6g.js.gz +0 -0
  10. package/build/client/_app/immutable/chunks/{D-dMdMUj.js → RgXSPQ_h.js} +1 -1
  11. package/build/client/_app/immutable/chunks/RgXSPQ_h.js.br +0 -0
  12. package/build/client/_app/immutable/chunks/RgXSPQ_h.js.gz +0 -0
  13. package/build/client/_app/immutable/entry/{app.CmTIRtCw.js → app.BsvwTApt.js} +2 -2
  14. package/build/client/_app/immutable/entry/app.BsvwTApt.js.br +0 -0
  15. package/build/client/_app/immutable/entry/app.BsvwTApt.js.gz +0 -0
  16. package/build/client/_app/immutable/entry/start.BdjLMMsY.js +1 -0
  17. package/build/client/_app/immutable/entry/start.BdjLMMsY.js.br +0 -0
  18. package/build/client/_app/immutable/entry/start.BdjLMMsY.js.gz +0 -0
  19. package/build/client/_app/immutable/nodes/{0.CNULKexJ.js → 0.CjQfQrD6.js} +1 -1
  20. package/build/client/_app/immutable/nodes/0.CjQfQrD6.js.br +0 -0
  21. package/build/client/_app/immutable/nodes/0.CjQfQrD6.js.gz +0 -0
  22. package/build/client/_app/immutable/nodes/{1.Cq-r1S-6.js → 1.DyPcUKIh.js} +1 -1
  23. package/build/client/_app/immutable/nodes/1.DyPcUKIh.js.br +2 -0
  24. package/build/client/_app/immutable/nodes/1.DyPcUKIh.js.gz +0 -0
  25. package/build/client/_app/immutable/nodes/{2.B4CjEZCb.js → 2.CworwA2R.js} +1 -1
  26. package/build/client/_app/immutable/nodes/2.CworwA2R.js.br +0 -0
  27. package/build/client/_app/immutable/nodes/2.CworwA2R.js.gz +0 -0
  28. package/build/client/_app/immutable/nodes/3.BKqbML1T.js +1 -0
  29. package/build/client/_app/immutable/nodes/3.BKqbML1T.js.br +0 -0
  30. package/build/client/_app/immutable/nodes/3.BKqbML1T.js.gz +0 -0
  31. package/build/client/_app/immutable/nodes/{4.BkqPmamr.js → 4.Cmigd8CT.js} +1 -1
  32. package/build/client/_app/immutable/nodes/4.Cmigd8CT.js.br +0 -0
  33. package/build/client/_app/immutable/nodes/4.Cmigd8CT.js.gz +0 -0
  34. package/build/client/_app/version.json +1 -1
  35. package/build/client/_app/version.json.br +0 -0
  36. package/build/client/_app/version.json.gz +0 -0
  37. package/build/handler.js +7 -7
  38. package/build/index.js +7 -7
  39. package/build/server/chunks/chunks/{context.js-6YdbScn_.js → context.js-CVuTSPjH.js} +2 -2
  40. package/build/server/chunks/chunks/{context.js-6YdbScn_.js.map → context.js-CVuTSPjH.js.map} +1 -1
  41. package/build/server/chunks/chunks/{contract.js-BHQbpsFk.js → contract.js-itFN0Gon.js} +6 -2
  42. package/build/server/chunks/chunks/contract.js-itFN0Gon.js.map +1 -0
  43. package/build/server/chunks/chunks/{exports.js-ku1LJuiG.js → exports.js-B13PkE0J.js} +2 -2
  44. package/build/server/chunks/chunks/{exports.js-ku1LJuiG.js.map → exports.js-B13PkE0J.js.map} +1 -1
  45. package/build/server/chunks/chunks/{http.js-dbT0ifQ9.js → http.js-ChQRRKfZ.js} +2 -2
  46. package/build/server/chunks/chunks/{http.js-dbT0ifQ9.js.map → http.js-ChQRRKfZ.js.map} +1 -1
  47. package/build/server/chunks/chunks/{internal.js-BSRzKMSE.js → internal.js-DlKoWKM-.js} +3 -3
  48. package/build/server/chunks/chunks/{internal.js-BSRzKMSE.js.map → internal.js-DlKoWKM-.js.map} +1 -1
  49. package/build/server/chunks/chunks/internal2.js-BlXt2rfE.js.map +1 -1
  50. package/build/server/chunks/chunks/{shared.js-DgL50WRD.js → shared.js-DKt4pwWi.js} +2 -2
  51. package/build/server/chunks/chunks/{shared.js-DgL50WRD.js.map → shared.js-DKt4pwWi.js.map} +1 -1
  52. package/build/server/chunks/chunks/{utils.js-D4p4YSNW.js → utils.js-BYMC-4Wt.js} +3 -3
  53. package/build/server/chunks/chunks/{utils.js-D4p4YSNW.js.map → utils.js-BYMC-4Wt.js.map} +1 -1
  54. package/build/server/chunks/entries/endpoints/api/_...path_/{_server.ts.js-BaMGnbi8.js → _server.ts.js-z988EDp2.js} +3 -3
  55. package/build/server/chunks/entries/endpoints/api/_...path_/{_server.ts.js-BaMGnbi8.js.map → _server.ts.js-z988EDp2.js.map} +1 -1
  56. package/build/server/chunks/entries/endpoints/api/assets/_id_/{_server.ts.js-B6YhmqYd.js → _server.ts.js-C68ru6gS.js} +4 -4
  57. package/build/server/chunks/entries/endpoints/api/assets/_id_/{_server.ts.js-B6YhmqYd.js.map → _server.ts.js-C68ru6gS.js.map} +1 -1
  58. package/build/server/chunks/entries/endpoints/api/assets/_id_/raw/{_server.ts.js-qtGGwjZ4.js → _server.ts.js-C0tk6RFk.js} +4 -4
  59. package/build/server/chunks/entries/endpoints/api/assets/_id_/raw/{_server.ts.js-qtGGwjZ4.js.map → _server.ts.js-C0tk6RFk.js.map} +1 -1
  60. package/build/server/chunks/entries/endpoints/api/assets/_id_/reimport/{_server.ts.js-98vjovDS.js → _server.ts.js-I03oZuo8.js} +4 -4
  61. package/build/server/chunks/entries/endpoints/api/assets/_id_/reimport/{_server.ts.js-98vjovDS.js.map → _server.ts.js-I03oZuo8.js.map} +1 -1
  62. package/build/server/chunks/entries/endpoints/api/assets/{_server.ts.js-CCzQEr--.js → _server.ts.js-BsQB8YDq.js} +4 -4
  63. package/build/server/chunks/entries/endpoints/api/assets/{_server.ts.js-CCzQEr--.js.map → _server.ts.js-BsQB8YDq.js.map} +1 -1
  64. package/build/server/chunks/entries/endpoints/api/assets/delete/{_server.ts.js-Ca7G_5IJ.js → _server.ts.js-CJuFE6Fb.js} +4 -4
  65. package/build/server/chunks/entries/endpoints/api/assets/delete/{_server.ts.js-Ca7G_5IJ.js.map → _server.ts.js-CJuFE6Fb.js.map} +1 -1
  66. package/build/server/chunks/entries/endpoints/api/assets/move/{_server.ts.js-DRw4ICSn.js → _server.ts.js-DuOZRRiA.js} +4 -4
  67. package/build/server/chunks/entries/endpoints/api/assets/move/{_server.ts.js-DRw4ICSn.js.map → _server.ts.js-DuOZRRiA.js.map} +1 -1
  68. package/build/server/chunks/entries/endpoints/api/folders/_path_/{_server.ts.js-Ai46jTgw.js → _server.ts.js-DGzYpQbv.js} +4 -4
  69. package/build/server/chunks/entries/endpoints/api/folders/_path_/{_server.ts.js-Ai46jTgw.js.map → _server.ts.js-DGzYpQbv.js.map} +1 -1
  70. package/build/server/chunks/entries/endpoints/api/folders/{_server.ts.js-NQKYbtiU.js → _server.ts.js-B045QoCA.js} +4 -4
  71. package/build/server/chunks/entries/endpoints/api/folders/{_server.ts.js-NQKYbtiU.js.map → _server.ts.js-B045QoCA.js.map} +1 -1
  72. package/build/server/chunks/entries/endpoints/api/imports/{_server.ts.js-_vJ2Npqd.js → _server.ts.js-HUkVdz19.js} +4 -4
  73. package/build/server/chunks/entries/endpoints/api/imports/{_server.ts.js-_vJ2Npqd.js.map → _server.ts.js-HUkVdz19.js.map} +1 -1
  74. package/build/server/chunks/entries/endpoints/api/manifests/_name_/{_server.ts.js-DIn5srjK.js → _server.ts.js-ry509mom.js} +4 -4
  75. package/build/server/chunks/entries/endpoints/api/manifests/_name_/{_server.ts.js-DIn5srjK.js.map → _server.ts.js-ry509mom.js.map} +1 -1
  76. package/build/server/chunks/entries/endpoints/api/manifests/{_server.ts.js-CqFLRTP-.js → _server.ts.js-DU0J7U25.js} +4 -4
  77. package/build/server/chunks/entries/endpoints/api/manifests/{_server.ts.js-CqFLRTP-.js.map → _server.ts.js-DU0J7U25.js.map} +1 -1
  78. package/build/server/chunks/entries/endpoints/api/raw/adopt/{_server.ts.js-BYn_v3VS.js → _server.ts.js-AZIV2PIn.js} +4 -4
  79. package/build/server/chunks/entries/endpoints/api/raw/adopt/{_server.ts.js-BYn_v3VS.js.map → _server.ts.js-AZIV2PIn.js.map} +1 -1
  80. package/build/server/chunks/entries/endpoints/api/raw/edited/accept/{_server.ts.js-sxVzkX1S.js → _server.ts.js-CDL0hCfM.js} +4 -4
  81. package/build/server/chunks/entries/endpoints/api/raw/edited/accept/{_server.ts.js-sxVzkX1S.js.map → _server.ts.js-CDL0hCfM.js.map} +1 -1
  82. package/build/server/chunks/entries/endpoints/api/raw/edited/dismiss/{_server.ts.js-E3B6VyAs.js → _server.ts.js-B13c17fq.js} +4 -4
  83. package/build/server/chunks/entries/endpoints/api/raw/edited/dismiss/{_server.ts.js-E3B6VyAs.js.map → _server.ts.js-B13c17fq.js.map} +1 -1
  84. package/build/server/chunks/entries/endpoints/api/reconcile/{_server.ts.js-ClgS9ghS.js → _server.ts.js-BCxjyAGC.js} +4 -4
  85. package/build/server/chunks/entries/endpoints/api/reconcile/{_server.ts.js-ClgS9ghS.js.map → _server.ts.js-BCxjyAGC.js.map} +1 -1
  86. package/build/server/chunks/entries/endpoints/api/reveal/{_server.ts.js-BPUzWMNL.js → _server.ts.js-BveeMZc2.js} +4 -4
  87. package/build/server/chunks/entries/endpoints/api/reveal/{_server.ts.js-BPUzWMNL.js.map → _server.ts.js-BveeMZc2.js.map} +1 -1
  88. package/build/server/chunks/entries/endpoints/api/state/{_server.ts.js-_G6pURnx.js → _server.ts.js-BZjTd9a0.js} +4 -4
  89. package/build/server/chunks/entries/endpoints/api/state/{_server.ts.js-_G6pURnx.js.map → _server.ts.js-BZjTd9a0.js.map} +1 -1
  90. package/build/server/chunks/entries/endpoints/api/usage/{_server.ts.js-as3GcjaI.js → _server.ts.js-BbTfPvC9.js} +4 -4
  91. package/build/server/chunks/entries/endpoints/api/usage/{_server.ts.js-as3GcjaI.js.map → _server.ts.js-BbTfPvC9.js.map} +1 -1
  92. package/build/server/chunks/entries/endpoints/files/_...path_/{_server.ts.js-CR58wymO.js → _server.ts.js-8VPJMLBk.js} +3 -3
  93. package/build/server/chunks/entries/endpoints/files/_...path_/{_server.ts.js-CR58wymO.js.map → _server.ts.js-8VPJMLBk.js.map} +1 -1
  94. package/build/server/chunks/entries/endpoints/files/proxy/_...path_/{_server.ts.js-Do2LOf04.js → _server.ts.js-BqR2J2sB.js} +4 -4
  95. package/build/server/chunks/entries/endpoints/files/proxy/_...path_/{_server.ts.js-Do2LOf04.js.map → _server.ts.js-BqR2J2sB.js.map} +1 -1
  96. package/build/server/chunks/entries/endpoints/files/raw/_...path_/{_server.ts.js-Ckp6_SaE.js → _server.ts.js-DbA28dCY.js} +4 -4
  97. package/build/server/chunks/entries/endpoints/files/raw/_...path_/{_server.ts.js-Ckp6_SaE.js.map → _server.ts.js-DbA28dCY.js.map} +1 -1
  98. package/build/server/chunks/entries/{hooks.server.js-CL1tg0CT.js → hooks.server.js-9PBc_3Jh.js} +3 -3
  99. package/build/server/chunks/entries/{hooks.server.js-CL1tg0CT.js.map → hooks.server.js-9PBc_3Jh.js.map} +1 -1
  100. package/build/server/chunks/{handler-BRtsY9uB.js → handler-XxBNCHhb.js} +5 -5
  101. package/build/server/chunks/{handler-BRtsY9uB.js.map → handler-XxBNCHhb.js.map} +1 -1
  102. package/build/server/chunks/{index.js-g3JzUW3v.js → index.js-B_76Gq5Y.js} +5 -5
  103. package/build/server/chunks/{index.js-g3JzUW3v.js.map → index.js-B_76Gq5Y.js.map} +1 -1
  104. package/build/server/chunks/{manifest.js-D51Bb0-d.js → manifest.js-C2MZZt3o.js} +29 -29
  105. package/build/server/chunks/{manifest.js-D51Bb0-d.js.map → manifest.js-C2MZZt3o.js.map} +1 -1
  106. package/build/server/chunks/nodes/{0.js-B1bRRxWY.js → 0.js-BLURyWmd.js} +2 -2
  107. package/build/server/chunks/nodes/{0.js-B1bRRxWY.js.map → 0.js-BLURyWmd.js.map} +1 -1
  108. package/build/server/chunks/nodes/1.js-CITI7d3d.js +7 -0
  109. package/build/server/chunks/nodes/{1.js-BIjbx0GP.js.map → 1.js-CITI7d3d.js.map} +1 -1
  110. package/build/server/chunks/nodes/2.js-DAls5TjF.js +7 -0
  111. package/build/server/chunks/nodes/{2.js-B6X0GbFS.js.map → 2.js-DAls5TjF.js.map} +1 -1
  112. package/build/server/chunks/nodes/3.js-CesAmg_E.js +7 -0
  113. package/build/server/chunks/nodes/{3.js-CQVJ114p.js.map → 3.js-CesAmg_E.js.map} +1 -1
  114. package/build/server/chunks/nodes/{4.js-Bt9pLOHE.js → 4.js-BRlAiVnn.js} +2 -2
  115. package/build/server/chunks/nodes/{4.js-Bt9pLOHE.js.map → 4.js-BRlAiVnn.js.map} +1 -1
  116. package/dist/core/index.d.ts +1 -1
  117. package/dist/core/manifest-entry.d.ts +14 -5
  118. package/dist/runtime/components/ChromaKeyVideo.svelte +3 -0
  119. package/dist/runtime/components/Image.svelte +3 -0
  120. package/dist/runtime/components/Slice.svelte +215 -18
  121. package/dist/runtime/components/Slice.svelte.d.ts +3 -4
  122. package/dist/runtime/components/Video.svelte +3 -0
  123. package/dist/runtime/geometry.d.ts +15 -9
  124. package/dist/runtime/geometry.js +21 -14
  125. package/dist/runtime/index.d.ts +3 -0
  126. package/dist/runtime/index.js +1 -0
  127. package/dist/runtime/kind-handlers.d.ts +22 -10
  128. package/dist/runtime/kind-handlers.js +58 -21
  129. package/dist/runtime/logging.d.ts +34 -0
  130. package/dist/runtime/logging.js +27 -0
  131. package/dist/runtime/manifest.d.ts +16 -5
  132. package/dist/runtime/manifest.js +19 -8
  133. package/dist/runtime/observe-border-box-size.d.ts +1 -1
  134. package/dist/runtime/observe-border-box-size.js +2 -2
  135. package/dist/runtime/preload-registry.d.ts +20 -1
  136. package/dist/runtime/preload-registry.js +23 -2
  137. package/dist/runtime/slice-source-lifecycle.d.ts +11 -2
  138. package/dist/runtime/slice-source-lifecycle.js +21 -2
  139. package/dist/server/codegen.js +5 -1
  140. package/package.json +1 -1
  141. package/build/client/_app/immutable/assets/AssetSourceMenu.DOu9eU7n.css +0 -1
  142. package/build/client/_app/immutable/assets/AssetSourceMenu.DOu9eU7n.css.br +0 -0
  143. package/build/client/_app/immutable/assets/AssetSourceMenu.DOu9eU7n.css.gz +0 -0
  144. package/build/client/_app/immutable/chunks/BR73V5PG.js +0 -1
  145. package/build/client/_app/immutable/chunks/BR73V5PG.js.br +0 -2
  146. package/build/client/_app/immutable/chunks/BR73V5PG.js.gz +0 -0
  147. package/build/client/_app/immutable/chunks/D-dMdMUj.js.br +0 -0
  148. package/build/client/_app/immutable/chunks/D-dMdMUj.js.gz +0 -0
  149. package/build/client/_app/immutable/chunks/PGG-LyPT.js +0 -108
  150. package/build/client/_app/immutable/chunks/PGG-LyPT.js.br +0 -0
  151. package/build/client/_app/immutable/chunks/PGG-LyPT.js.gz +0 -0
  152. package/build/client/_app/immutable/entry/app.CmTIRtCw.js.br +0 -0
  153. package/build/client/_app/immutable/entry/app.CmTIRtCw.js.gz +0 -0
  154. package/build/client/_app/immutable/entry/start.M4xqAoBc.js +0 -1
  155. package/build/client/_app/immutable/entry/start.M4xqAoBc.js.br +0 -1
  156. package/build/client/_app/immutable/entry/start.M4xqAoBc.js.gz +0 -0
  157. package/build/client/_app/immutable/nodes/0.CNULKexJ.js.br +0 -0
  158. package/build/client/_app/immutable/nodes/0.CNULKexJ.js.gz +0 -0
  159. package/build/client/_app/immutable/nodes/1.Cq-r1S-6.js.br +0 -0
  160. package/build/client/_app/immutable/nodes/1.Cq-r1S-6.js.gz +0 -0
  161. package/build/client/_app/immutable/nodes/2.B4CjEZCb.js.br +0 -0
  162. package/build/client/_app/immutable/nodes/2.B4CjEZCb.js.gz +0 -0
  163. package/build/client/_app/immutable/nodes/3.DzWxKBWe.js +0 -1
  164. package/build/client/_app/immutable/nodes/3.DzWxKBWe.js.br +0 -0
  165. package/build/client/_app/immutable/nodes/3.DzWxKBWe.js.gz +0 -0
  166. package/build/client/_app/immutable/nodes/4.BkqPmamr.js.br +0 -0
  167. package/build/client/_app/immutable/nodes/4.BkqPmamr.js.gz +0 -0
  168. package/build/server/chunks/chunks/contract.js-BHQbpsFk.js.map +0 -1
  169. package/build/server/chunks/nodes/1.js-BIjbx0GP.js +0 -7
  170. package/build/server/chunks/nodes/2.js-B6X0GbFS.js +0 -7
  171. package/build/server/chunks/nodes/3.js-CQVJ114p.js +0 -7
  172. package/dist/runtime/components/SliceCanvas.svelte +0 -173
  173. package/dist/runtime/components/SliceCanvas.svelte.d.ts +0 -10
  174. package/dist/runtime/components/SliceDom.svelte +0 -161
  175. package/dist/runtime/components/SliceDom.svelte.d.ts +0 -10
@@ -1,31 +1,58 @@
1
1
  <!--
2
- The slice component hosts rendering: one component, backend chosen per use.
3
- Dispatches between the two private implementations — SliceDom (default) and
4
- SliceCanvas — which share `computeRenderCells` geometry. The DOM backend maps
5
- that geometry to one CSS border-image paint; Canvas draws the cells into one
6
- canvas backing store.
2
+ Slice renderer using the entry-based API (slicing arrives inline on the
3
+ `SliceManifestEntry`). Owns the source lifecycle (load, decode, latest-wins
4
+ commit). A one-slice snapshot renders as a regular image; sliced snapshots
5
+ draw each cell with one `drawImage` into a backing canvas. A ResizeObserver
6
+ triggers a redraw on resize; window `resize` and DPR changes (browser zoom,
7
+ moving between displays) redraw too, and a reactive effect redraws when the
8
+ entry changes — so it also works live inside the editor.
9
+
10
+ The backing-store ratio is the devicePixelRatio FLOORED to an integer and
11
+ capped at `maxDpr` (default 2), never the exact ratio. Cell edges are
12
+ integer virtual CSS pixels, so an integer ratio lands every drawImage edge on
13
+ integer backing pixels — abutting cells cannot open an antialiased gap. On
14
+ displays with a fractional devicePixelRatio (2.625-class Androids, browser
15
+ zoom, scaled desktops) those gaps are exactly the hairlines this avoids;
16
+ the compositor applies the remainder to the finished canvas as one
17
+ upscale, where slice boundaries are plain content. Floor + cap
18
+ deliberately favor seam-freeness and backing memory over sharpness.
19
+
20
+ The measured element is the root div, not its painted child: with overdraw
21
+ the canvas or image is enlarged by the overdraw's destination size and
22
+ offset negatively (see `computeCanvasPaintLayout`), so the baked shadow/glow
23
+ paints outside the layout box while the root (and therefore the layout)
24
+ keeps the box size.
7
25
  -->
8
26
  <script lang="ts">
9
27
  import { onDestroy } from 'svelte';
10
28
  import type { SliceManifestEntry } from '../../core/index.js';
11
- import { SliceSourceLifecycle, type LoadedSliceSnapshot } from '../slice-source-lifecycle.js';
12
- import SliceDom from './SliceDom.svelte';
13
- import SliceCanvas from './SliceCanvas.svelte';
29
+ import {
30
+ computeCanvasPaintLayout,
31
+ computeRenderCells,
32
+ type CanvasPaintLayout
33
+ } from '../geometry.js';
34
+ import { observeBorderBoxSize } from '../observe-border-box-size.js';
35
+ import {
36
+ SliceSourceLifecycle,
37
+ isDetachedSliceSource,
38
+ type LoadedSliceSnapshot
39
+ } from '../slice-source-lifecycle.js';
40
+ import { logRender } from '../logging.js';
14
41
 
15
42
  interface Props {
16
43
  src: SliceManifestEntry;
17
44
  /**
18
- * Rendering backend. `dom` (default) paints the shared slice geometry as
19
- * one CSS `border-image`; `canvas` draws the same cells into one `<canvas>`
20
- * with a device-pixel-ratio-sized backing store.
45
+ * Upper bound of the canvas backing-store ratio (floored itself). The
46
+ * default of 2 is rarely distinguishable and keeps backing memory bounded.
21
47
  */
22
- backend?: 'dom' | 'canvas';
48
+ maxDpr?: number;
23
49
  /** Accessible label for the rendered graphic. */
24
50
  alt?: string;
25
51
  class?: string;
26
52
  }
27
53
 
28
- let { src, backend = 'dom', alt = '', class: className = '' }: Props = $props();
54
+ let { src, maxDpr = 2, alt = '', class: className = '' }: Props = $props();
55
+
29
56
  let snapshot = $state.raw<LoadedSliceSnapshot | null>(null);
30
57
  const sourceLifecycle = new SliceSourceLifecycle((loaded) => {
31
58
  snapshot = loaded;
@@ -35,11 +62,181 @@
35
62
  sourceLifecycle.request(src);
36
63
  });
37
64
 
65
+ $effect(() => logRender(src));
66
+
38
67
  onDestroy(() => sourceLifecycle.dispose());
68
+
69
+ let root = $state<HTMLDivElement>();
70
+ let canvas = $state<HTMLCanvasElement>();
71
+ let boxWidth = $state(0);
72
+ let boxHeight = $state(0);
73
+ let devicePixelRatio = $state(1);
74
+ // Bumped by window `resize` so the draw effect re-runs even when neither the
75
+ // box nor the entry changed (belt and braces alongside the ResizeObserver).
76
+ let redrawTick = $state(0);
77
+
78
+ // Track the display's devicePixelRatio: a resolution media query fires
79
+ // exactly when the DPR leaves the tracked value (browser zoom, moving the
80
+ // window to another display). The tracked state must be READ first — a
81
+ // state the effect writes before reading is deliberately not a dependency
82
+ // in Svelte, so a write-first version would never re-arm the query after
83
+ // the first transition. Reconciling via update() converges in one extra
84
+ // run when the real DPR drifted (mount, or a change event).
85
+ $effect(() => {
86
+ const trackedRatio = devicePixelRatio;
87
+ const query = window.matchMedia(`(resolution: ${trackedRatio}dppx)`);
88
+ const update = () => {
89
+ devicePixelRatio = window.devicePixelRatio || 1;
90
+ };
91
+ update();
92
+ query.addEventListener('change', update);
93
+ return () => query.removeEventListener('change', update);
94
+ });
95
+
96
+ $effect(() => {
97
+ const element = root;
98
+ if (!element) return;
99
+ return observeBorderBoxSize(element, (width, height) => {
100
+ boxWidth = width;
101
+ boxHeight = height;
102
+ });
103
+ });
104
+
105
+ const emptyLayout: CanvasPaintLayout = {
106
+ renderWidth: 0,
107
+ renderHeight: 0,
108
+ cssLeft: 0,
109
+ cssTop: 0,
110
+ cssWidth: 0,
111
+ cssHeight: 0,
112
+ backingWidth: 0,
113
+ backingHeight: 0,
114
+ originX: 0,
115
+ originY: 0
116
+ };
117
+ // The ratio the backing store and every draw use; the css placement is
118
+ // unaffected, so the compositor bridges any difference on the whole texture.
119
+ const renderRatio = $derived.by(() => {
120
+ const cap = Math.max(1, Math.floor(maxDpr));
121
+ return Math.min(cap, Math.max(1, Math.floor(devicePixelRatio)));
122
+ });
123
+
124
+ const layout = $derived.by(() => {
125
+ const current = snapshot;
126
+ return current
127
+ ? computeCanvasPaintLayout(current.entry, boxWidth, boxHeight, renderRatio)
128
+ : emptyLayout;
129
+ });
130
+
131
+ // Redraw only when a committed snapshot, the box, DPR, or window changes.
132
+ $effect(() => {
133
+ const current = snapshot;
134
+ const currentLayout = layout;
135
+ void redrawTick;
136
+ draw(current, currentLayout);
137
+ });
138
+
139
+ function draw(current: LoadedSliceSnapshot | null, currentLayout: CanvasPaintLayout): void {
140
+ const element = canvas;
141
+ if (!element || current?.entry.slicing.mode === 'one') return;
142
+ if (!current || boxWidth === 0 || boxHeight === 0) {
143
+ element.width = 0;
144
+ element.height = 0;
145
+ return;
146
+ }
147
+ if (isDetachedSliceSource(current.image)) {
148
+ // A preload release closed the retained bitmap while this slice stayed
149
+ // mounted. Keep the last painted pixels and reload asynchronously.
150
+ sourceLifecycle.request(src);
151
+ return;
152
+ }
153
+
154
+ element.width = currentLayout.backingWidth;
155
+ element.height = currentLayout.backingHeight;
156
+
157
+ const context = element.getContext('2d');
158
+ if (!context) return;
159
+ // Origin at the layout box's top-left, so the (possibly negative) cell
160
+ // coordinates from computeRenderCells apply directly.
161
+ context.setTransform(
162
+ renderRatio,
163
+ 0,
164
+ 0,
165
+ renderRatio,
166
+ currentLayout.originX,
167
+ currentLayout.originY
168
+ );
169
+ context.clearRect(
170
+ currentLayout.cssLeft,
171
+ currentLayout.cssTop,
172
+ currentLayout.cssWidth,
173
+ currentLayout.cssHeight
174
+ );
175
+
176
+ for (const cell of computeRenderCells(
177
+ current.entry,
178
+ currentLayout.renderWidth,
179
+ currentLayout.renderHeight
180
+ )) {
181
+ context.drawImage(
182
+ current.image,
183
+ cell.sx,
184
+ cell.sy,
185
+ cell.sw,
186
+ cell.sh,
187
+ cell.dx,
188
+ cell.dy,
189
+ cell.dw,
190
+ cell.dh
191
+ );
192
+ }
193
+ }
39
194
  </script>
40
195
 
41
- {#if backend === 'canvas'}
42
- <SliceCanvas {snapshot} {alt} class={className} />
43
- {:else}
44
- <SliceDom {snapshot} {alt} class={className} />
45
- {/if}
196
+ <svelte:window onresize={() => (redrawTick += 1)} />
197
+
198
+ <div
199
+ bind:this={root}
200
+ class="fundus-slice {className}"
201
+ role={snapshot?.entry.slicing.mode === 'one' ? undefined : alt ? 'img' : undefined}
202
+ aria-label={snapshot?.entry.slicing.mode === 'one' ? undefined : alt || undefined}
203
+ >
204
+ {#if snapshot?.entry.slicing.mode === 'one'}
205
+ <img
206
+ src={snapshot.entry.src}
207
+ {alt}
208
+ style:left="{layout.cssLeft}px"
209
+ style:top="{layout.cssTop}px"
210
+ style:width="{layout.cssWidth}px"
211
+ style:height="{layout.cssHeight}px"
212
+ />
213
+ {:else if snapshot}
214
+ <canvas
215
+ bind:this={canvas}
216
+ style:left="{layout.cssLeft}px"
217
+ style:top="{layout.cssTop}px"
218
+ style:width="{layout.cssWidth}px"
219
+ style:height="{layout.cssHeight}px"
220
+ ></canvas>
221
+ {/if}
222
+ </div>
223
+
224
+ <style>
225
+ .fundus-slice {
226
+ position: relative;
227
+ width: 100%;
228
+ height: 100%;
229
+ }
230
+
231
+ canvas,
232
+ img {
233
+ position: absolute;
234
+ display: block;
235
+ pointer-events: none;
236
+ }
237
+
238
+ img {
239
+ max-width: none;
240
+ max-height: none;
241
+ }
242
+ </style>
@@ -2,11 +2,10 @@ import type { SliceManifestEntry } from '../../core/index.js';
2
2
  interface Props {
3
3
  src: SliceManifestEntry;
4
4
  /**
5
- * Rendering backend. `dom` (default) paints the shared slice geometry as
6
- * one CSS `border-image`; `canvas` draws the same cells into one `<canvas>`
7
- * with a device-pixel-ratio-sized backing store.
5
+ * Upper bound of the canvas backing-store ratio (floored itself). The
6
+ * default of 2 is rarely distinguishable and keeps backing memory bounded.
8
7
  */
9
- backend?: 'dom' | 'canvas';
8
+ maxDpr?: number;
10
9
  /** Accessible label for the rendered graphic. */
11
10
  alt?: string;
12
11
  class?: string;
@@ -7,6 +7,7 @@
7
7
  <script lang="ts">
8
8
  import type { VideoManifestEntry } from '../../core/index.js';
9
9
  import { resolveRetainedSource } from '../preload-registry.js';
10
+ import { logRender } from '../logging.js';
10
11
 
11
12
  interface Props {
12
13
  src: VideoManifestEntry;
@@ -35,6 +36,8 @@
35
36
 
36
37
  const source = $derived(resolveRetainedSource(src.src));
37
38
 
39
+ $effect(() => logRender(src));
40
+
38
41
  $effect(() => {
39
42
  const video = videoElement;
40
43
  if (!video) return;
@@ -58,22 +58,27 @@ export declare function effectiveOverdraw(entry: SliceManifestEntry): SliceInset
58
58
  */
59
59
  export declare function computeOverdrawDestination(entry: SliceManifestEntry, boxWidth: number, boxHeight: number): SliceInsets;
60
60
  /**
61
- * How the canvas backend's `<canvas>` relates to the layout box: with overdraw
61
+ * How the slice renderer's `<canvas>` relates to the layout box: with overdraw
62
62
  * the canvas is enlarged by the overdraw's destination size and offset
63
63
  * negatively, so the baked shadow/glow paints outside the layout box while the
64
64
  * wrapper (and therefore the layout) keeps the box size. The backing store is
65
- * the CSS size × `devicePixelRatio` (rounded) for crispness, and the paint
66
- * origin sits at the layout box's top-left in device pixels — so the (possibly
67
- * negative) cell coordinates from `computeRenderCells` apply directly.
65
+ * the rounded virtual CSS size × the integer render ratio. This matches the
66
+ * integer final edges from `computeRenderCells`; the browser scales the
67
+ * completed texture to the possibly fractional CSS size. The paint origin sits
68
+ * at the layout box's top-left in backing pixels — so the (possibly negative)
69
+ * cell coordinates from `computeRenderCells` apply directly.
68
70
  */
69
71
  export interface CanvasPaintLayout {
72
+ /** Integer virtual layout-box size used by `computeRenderCells`. */
73
+ renderWidth: number;
74
+ renderHeight: number;
70
75
  /** CSS offset of the canvas within the wrapper (0 or negative — overdraw). */
71
76
  cssLeft: number;
72
77
  cssTop: number;
73
- /** CSS size of the canvas (box expanded by the overdraw's destination size). */
78
+ /** CSS size of the scaled canvas, including overdraw. */
74
79
  cssWidth: number;
75
80
  cssHeight: number;
76
- /** Backing-store size in device pixels (CSS size × devicePixelRatio). */
81
+ /** Backing-store size in pixels (rounded virtual CSS size × render ratio). */
77
82
  backingWidth: number;
78
83
  backingHeight: number;
79
84
  /** Paint origin (the layout box's top-left) in device pixels. */
@@ -82,10 +87,11 @@ export interface CanvasPaintLayout {
82
87
  }
83
88
  /**
84
89
  * The canvas geometry for a slice filling a `boxWidth × boxHeight` layout box
85
- * at the given `devicePixelRatio` (the display's, not the asset's export
86
- * scale). Pure — the canvas backend derives every size and offset from this.
90
+ * at the given integer render ratio (the floored/capped display DPR, not the
91
+ * asset's export scale). Pure — the slice renderer derives every size and
92
+ * offset from this.
87
93
  */
88
- export declare function computeCanvasPaintLayout(entry: SliceManifestEntry, boxWidth: number, boxHeight: number, devicePixelRatio: number): CanvasPaintLayout;
94
+ export declare function computeCanvasPaintLayout(entry: SliceManifestEntry, boxWidth: number, boxHeight: number, renderRatio: number): CanvasPaintLayout;
89
95
  /**
90
96
  * Compute the cells to paint for a slice filling a `boxWidth × boxHeight` box.
91
97
  * Returns an empty array for a zero-size box. With overdraw the cells cover the
@@ -85,23 +85,30 @@ export function computeOverdrawDestination(entry, boxWidth, boxHeight) {
85
85
  }
86
86
  /**
87
87
  * The canvas geometry for a slice filling a `boxWidth × boxHeight` layout box
88
- * at the given `devicePixelRatio` (the display's, not the asset's export
89
- * scale). Pure — the canvas backend derives every size and offset from this.
88
+ * at the given integer render ratio (the floored/capped display DPR, not the
89
+ * asset's export scale). Pure — the slice renderer derives every size and
90
+ * offset from this.
90
91
  */
91
- export function computeCanvasPaintLayout(entry, boxWidth, boxHeight, devicePixelRatio) {
92
- const overdrawDestination = computeOverdrawDestination(entry, boxWidth, boxHeight);
93
- const cssWidth = boxWidth + overdrawDestination.left + overdrawDestination.right;
94
- const cssHeight = boxHeight + overdrawDestination.top + overdrawDestination.bottom;
92
+ export function computeCanvasPaintLayout(entry, boxWidth, boxHeight, renderRatio) {
93
+ const renderWidth = boxWidth > 0 ? Math.max(1, Math.round(boxWidth)) : 0;
94
+ const renderHeight = boxHeight > 0 ? Math.max(1, Math.round(boxHeight)) : 0;
95
+ const overdrawDestination = computeOverdrawDestination(entry, renderWidth, renderHeight);
96
+ const virtualWidth = renderWidth + overdrawDestination.left + overdrawDestination.right;
97
+ const virtualHeight = renderHeight + overdrawDestination.top + overdrawDestination.bottom;
98
+ const horizontalScale = renderWidth > 0 ? boxWidth / renderWidth : 1;
99
+ const verticalScale = renderHeight > 0 ? boxHeight / renderHeight : 1;
95
100
  return {
101
+ renderWidth,
102
+ renderHeight,
96
103
  // `+ 0` normalizes the negative zero a zero overdraw would produce.
97
- cssLeft: -overdrawDestination.left + 0,
98
- cssTop: -overdrawDestination.top + 0,
99
- cssWidth,
100
- cssHeight,
101
- backingWidth: Math.round(cssWidth * devicePixelRatio),
102
- backingHeight: Math.round(cssHeight * devicePixelRatio),
103
- originX: overdrawDestination.left * devicePixelRatio,
104
- originY: overdrawDestination.top * devicePixelRatio
104
+ cssLeft: -overdrawDestination.left * horizontalScale + 0,
105
+ cssTop: -overdrawDestination.top * verticalScale + 0,
106
+ cssWidth: virtualWidth * horizontalScale,
107
+ cssHeight: virtualHeight * verticalScale,
108
+ backingWidth: virtualWidth * renderRatio,
109
+ backingHeight: virtualHeight * renderRatio,
110
+ originX: overdrawDestination.left * renderRatio,
111
+ originY: overdrawDestination.top * renderRatio
105
112
  };
106
113
  }
107
114
  /** Cumulative, rounded edges so adjacent cells abut exactly on integer pixels. */
@@ -5,5 +5,8 @@ export { default as Video } from './components/Video.svelte';
5
5
  export { default as ChromaKeyVideo } from './components/ChromaKeyVideo.svelte';
6
6
  export { resolveRetainedSource } from './preload-registry.js';
7
7
  export { audioSourceOf } from './kind-handlers.js';
8
+ export { setFundusLogger } from './logging.js';
9
+ export type { FundusLogger, RenderedSource } from './logging.js';
10
+ export type { SourceStatus } from './preload-registry.js';
8
11
  export type { ImageManifestEntry, SliceManifestEntry, VideoManifestEntry, ChromaKeyVideoManifestEntry, AudioManifestEntry, ManifestEntry, ChromaKeyParameters, ImagePreloadOptions, SlicePreloadOptions, VideoPreloadOptions, ChromaKeyVideoPreloadOptions, AudioPreloadOptions, PreloadOptions } from '../core/index.js';
9
12
  export { DEFAULT_CHROMA_KEY_PARAMETERS } from '../core/index.js';
@@ -6,4 +6,5 @@ export { default as Video } from './components/Video.svelte';
6
6
  export { default as ChromaKeyVideo } from './components/ChromaKeyVideo.svelte';
7
7
  export { resolveRetainedSource } from './preload-registry.js';
8
8
  export { audioSourceOf } from './kind-handlers.js';
9
+ export { setFundusLogger } from './logging.js';
9
10
  export { DEFAULT_CHROMA_KEY_PARAMETERS } from '../core/index.js';
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * The runtime part of the type-plugin contract: per-kind preload
3
3
  * strategy and its inverse release strategy. Each first-party kind registers
4
- * a handler that knows how its delivered file is loaded — image-backed kinds
5
- * fetch + optionally decode, media kinds (video/audio) fetch fully and mint
6
- * an object URL — and how what it loaded is disposed (revoke the object URL;
7
- * image-backed kinds have nothing to dispose). An unknown kind falls back to
8
- * a plain full fetch: no decode, no retention.
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.
9
11
  *
10
12
  * Handlers are pure strategy: refcounting, cross-manifest dedup, and
11
13
  * in-flight bookkeeping live in the module-wide registry
@@ -16,12 +18,20 @@ import type { AudioManifestEntry, PreloadOptions } from '../core/index.js';
16
18
  export declare function audioSourceOf(entry: AudioManifestEntry): string;
17
19
  /**
18
20
  * What a handler's `preload` acquired and its `release` must dispose. For
19
- * retained media kinds this is the minted object URL; cache-backed kinds
20
- * (image, slice, unknown) hold no disposable resources.
21
+ * retained media kinds this is the minted object URL, for slices the decoded
22
+ * bitmap; cache-backed kinds (image, unknown) hold no disposable resources.
21
23
  */
22
24
  export interface KindHandlerResources {
23
25
  /** Object URL minted over the fully fetched file (retained media kinds). */
24
26
  objectUrl?: string;
27
+ /** Decoded pixels of a multi-cell slice source, pinned against cache eviction. */
28
+ imageBitmap?: ImageBitmap;
29
+ /**
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.
33
+ */
34
+ image?: HTMLImageElement;
25
35
  }
26
36
  /** How one asset kind preloads — and releases — its delivered file. */
27
37
  export interface KindRuntimeHandler {
@@ -29,12 +39,14 @@ export interface KindRuntimeHandler {
29
39
  * Fully load one source of this kind — the promise resolves only once the
30
40
  * file is completely transferred (plus the kind's strategy: decoded /
31
41
  * retained as an object URL), its body matches the build-time `byteSize`,
32
- * and return the resources `release` disposes.
42
+ * and return the resources `release` disposes. `entry` is the manifest
43
+ * entry the source came from, for strategies keyed on entry metadata.
33
44
  */
34
- preload(url: string, byteSize: number, options: PreloadOptions): Promise<KindHandlerResources>;
45
+ preload(url: string, byteSize: number, options: PreloadOptions, entry?: Record<string, unknown>): Promise<KindHandlerResources>;
35
46
  /**
36
47
  * The inverse of `preload`: dispose the resources it returned (revoke the
37
- * object URL). Omitted = the kind acquires nothing disposable.
48
+ * object URL, close the bitmap). Omitted = the kind acquires nothing
49
+ * disposable.
38
50
  */
39
51
  release?(resources: KindHandlerResources): void;
40
52
  }
@@ -66,28 +66,65 @@ function verifyByteSize(url, expected, actual) {
66
66
  }
67
67
  }
68
68
  /**
69
- * The image-backed strategy (image, slice): fetch the full file, then decode
70
- * by default. Decoding goes via an HTMLImageElement where a DOM exists (the
71
- * browser serves the bytes from cache); headless environments (Node tests,
72
- * SSR) skip decoding. Nothing to release — the browser cache owns the bytes.
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.
73
74
  */
74
- function imageBackedHandler(kind) {
75
- return {
76
- async preload(url, byteSize, options) {
77
- const response = await fetchCompletely(url);
78
- // Read the body fully so the file is actually transferred (and
79
- // cached), not just the response headers.
80
- const bytes = await response.arrayBuffer();
81
- verifyByteSize(url, byteSize, bytes.byteLength);
82
- if (shouldDecode(kind, options) && typeof globalThis.Image === 'function') {
83
- const image = new globalThis.Image();
84
- image.src = url;
85
- await image.decode();
86
- }
75
+ const imageHandler = {
76
+ async preload(url, byteSize, options) {
77
+ const response = await fetchCompletely(url);
78
+ // Read the body fully so the file is actually transferred (and
79
+ // cached), not just the response headers.
80
+ const bytes = await response.arrayBuffer();
81
+ verifyByteSize(url, byteSize, bytes.byteLength);
82
+ if (shouldDecode('image', options) && typeof globalThis.Image === 'function') {
83
+ const image = new globalThis.Image();
84
+ image.src = url;
85
+ await image.decode();
86
+ }
87
+ return {};
88
+ }
89
+ };
90
+ /**
91
+ * The slice strategy: fetch the full file and retain a decoded form while
92
+ * held, chosen by the entry's slicing mode. Multi-cell slices (nine,
93
+ * three-*) draw through a canvas and retain the pixels as an ImageBitmap —
94
+ * unlike a throwaway decode into the browser's evictable image cache, the
95
+ * bitmap survives memory pressure and lets `SliceSourceLifecycle` commit
96
+ * synchronously, so a preloaded slice paints in its mount frame. One-slice
97
+ * sources render as a plain `<img>`, which cannot paint from a bitmap: they
98
+ * retain the decoded HTMLImageElement instead — the same synchronous commit,
99
+ * and its decode warms the (evictable) cache the mounted `<img>` paints
100
+ * from. Release closes the bitmap; a retained element has nothing to
101
+ * dispose. Slice sources are small (the insets cover every rendered size),
102
+ * so retention is cheap. Environments without `createImageBitmap` retain the
103
+ * decoded element for every mode.
104
+ */
105
+ const sliceHandler = {
106
+ async preload(url, byteSize, options, entry) {
107
+ const response = await fetchCompletely(url);
108
+ const blob = await response.blob();
109
+ verifyByteSize(url, byteSize, blob.size);
110
+ if (!shouldDecode('slice', options))
87
111
  return {};
112
+ const oneSlice = entry?.slicing?.mode === 'one';
113
+ if (!oneSlice && typeof globalThis.createImageBitmap === 'function') {
114
+ return { imageBitmap: await globalThis.createImageBitmap(blob) };
88
115
  }
89
- };
90
- }
116
+ if (typeof globalThis.Image === 'function') {
117
+ const image = new globalThis.Image();
118
+ image.src = url;
119
+ await image.decode();
120
+ return { image };
121
+ }
122
+ return {};
123
+ },
124
+ release(resources) {
125
+ resources.imageBitmap?.close();
126
+ }
127
+ };
91
128
  /**
92
129
  * The retained-media strategy (video, chroma-key-video, audio): fetch the
93
130
  * full file and retain it as an object URL so later playback does not touch
@@ -118,8 +155,8 @@ const unknownKindHandler = {
118
155
  }
119
156
  };
120
157
  const runtimeHandlerCatalogue = {
121
- image: imageBackedHandler('image'),
122
- slice: imageBackedHandler('slice'),
158
+ image: imageHandler,
159
+ slice: sliceHandler,
123
160
  video: retainedMediaHandler,
124
161
  'chroma-key-video': retainedMediaHandler,
125
162
  audio: retainedMediaHandler
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Opt-in render-time diagnostics: a host registers one logger and every Fundus
3
+ * render component reports, on mount and whenever its displayed asset changes,
4
+ * which sources it needs and whether each was preloaded. Purely additive and
5
+ * gated — with no logger registered this is a single null-check, no walk, no
6
+ * cost. It never participates in rendering: `logRender` swallows any error
7
+ * (a malformed entry, a throwing logger) so diagnostics can't break the view.
8
+ */
9
+ import type { ManifestEntry } from '../core/index.js';
10
+ import { type SourceStatus } from './preload-registry.js';
11
+ /** One rendered source and its preload state at the moment of the render. */
12
+ export interface RenderedSource {
13
+ /** The asset id the source belongs to — an inlined mask/fallback carries its own. */
14
+ id: string;
15
+ kind: string;
16
+ url: string;
17
+ status: SourceStatus;
18
+ }
19
+ export interface FundusLogger {
20
+ /**
21
+ * Reported once per component render (mount and each in-place asset swap),
22
+ * listing every source the component displays. A single-file kind yields one
23
+ * source; a chroma-key-video yields its video plus the inlined mask/fallback.
24
+ * Sources are not deduped — a url shared by two of them appears twice.
25
+ */
26
+ onRender(sources: RenderedSource[]): void;
27
+ }
28
+ /** Register (or clear, with `null`) the process-wide render logger. */
29
+ export declare function setFundusLogger(logger: FundusLogger | null): void;
30
+ /**
31
+ * Report the sources a component is rendering and their current preload state.
32
+ * A no-op without a registered logger; never throws.
33
+ */
34
+ export declare function logRender(entry: ManifestEntry): void;
@@ -0,0 +1,27 @@
1
+ import { collectEntrySources } from './manifest.js';
2
+ import { sourceStatus } from './preload-registry.js';
3
+ let activeLogger = null;
4
+ /** Register (or clear, with `null`) the process-wide render logger. */
5
+ export function setFundusLogger(logger) {
6
+ activeLogger = logger;
7
+ }
8
+ /**
9
+ * Report the sources a component is rendering and their current preload state.
10
+ * A no-op without a registered logger; never throws.
11
+ */
12
+ export function logRender(entry) {
13
+ if (!activeLogger)
14
+ return;
15
+ try {
16
+ activeLogger.onRender(collectEntrySources(entry).map((source) => ({
17
+ id: source.id,
18
+ kind: source.kind,
19
+ url: source.url,
20
+ status: sourceStatus(source.url)
21
+ })));
22
+ }
23
+ catch (cause) {
24
+ // Diagnostics must never break rendering.
25
+ console.warn('fundus: render logging failed', cause);
26
+ }
27
+ }