fundus 0.11.0 → 0.12.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 (218) hide show
  1. package/README.md +7 -5
  2. package/build/client/_app/immutable/assets/2.DOKgeL9V.css +1 -0
  3. package/build/client/_app/immutable/assets/2.DOKgeL9V.css.br +0 -0
  4. package/build/client/_app/immutable/assets/2.DOKgeL9V.css.gz +0 -0
  5. package/build/client/_app/immutable/assets/{3.BuCXJbRG.css → 3.D6PF4wEW.css} +1 -1
  6. package/build/client/_app/immutable/assets/3.D6PF4wEW.css.br +0 -0
  7. package/build/client/_app/immutable/assets/3.D6PF4wEW.css.gz +0 -0
  8. package/build/client/_app/immutable/assets/LibraryContextMenu.B6By2-yb.css +1 -0
  9. package/build/client/_app/immutable/assets/LibraryContextMenu.B6By2-yb.css.br +0 -0
  10. package/build/client/_app/immutable/assets/LibraryContextMenu.B6By2-yb.css.gz +0 -0
  11. package/build/client/_app/immutable/assets/Popover.x-2x_hPE.css +1 -0
  12. package/build/client/_app/immutable/assets/Popover.x-2x_hPE.css.br +0 -0
  13. package/build/client/_app/immutable/assets/Popover.x-2x_hPE.css.gz +0 -0
  14. package/build/client/_app/immutable/chunks/B9HdDE5n.js +1 -0
  15. package/build/client/_app/immutable/chunks/B9HdDE5n.js.br +0 -0
  16. package/build/client/_app/immutable/chunks/B9HdDE5n.js.gz +0 -0
  17. package/build/client/_app/immutable/chunks/BfQXIsM7.js +1 -0
  18. package/build/client/_app/immutable/chunks/BfQXIsM7.js.br +0 -0
  19. package/build/client/_app/immutable/chunks/BfQXIsM7.js.gz +0 -0
  20. package/build/client/_app/immutable/chunks/Bj31DJwy.js +1 -0
  21. package/build/client/_app/immutable/chunks/Bj31DJwy.js.br +0 -0
  22. package/build/client/_app/immutable/chunks/Bj31DJwy.js.gz +0 -0
  23. package/build/client/_app/immutable/chunks/{DFRlZOKz.js → BufZeTod.js} +2 -2
  24. package/build/client/_app/immutable/chunks/BufZeTod.js.br +0 -0
  25. package/build/client/_app/immutable/chunks/BufZeTod.js.gz +0 -0
  26. package/build/client/_app/immutable/chunks/BxCbL__p.js +1 -0
  27. package/build/client/_app/immutable/chunks/BxCbL__p.js.br +0 -0
  28. package/build/client/_app/immutable/chunks/BxCbL__p.js.gz +0 -0
  29. package/build/client/_app/immutable/chunks/{DpFtnEPx.js → CjB5zQ4_.js} +1 -1
  30. package/build/client/_app/immutable/chunks/CjB5zQ4_.js.br +0 -0
  31. package/build/client/_app/immutable/chunks/CjB5zQ4_.js.gz +0 -0
  32. package/build/client/_app/immutable/chunks/DuXWzgb4.js +108 -0
  33. package/build/client/_app/immutable/chunks/DuXWzgb4.js.br +0 -0
  34. package/build/client/_app/immutable/chunks/DuXWzgb4.js.gz +0 -0
  35. package/build/client/_app/immutable/chunks/{BMORe2Gb.js → YpiA5Xry.js} +1 -1
  36. package/build/client/_app/immutable/chunks/YpiA5Xry.js.br +0 -0
  37. package/build/client/_app/immutable/chunks/{BMORe2Gb.js.gz → YpiA5Xry.js.gz} +0 -0
  38. package/build/client/_app/immutable/entry/app.DHoRwUAh.js +2 -0
  39. package/build/client/_app/immutable/entry/app.DHoRwUAh.js.br +0 -0
  40. package/build/client/_app/immutable/entry/app.DHoRwUAh.js.gz +0 -0
  41. package/build/client/_app/immutable/entry/start.Xp5z9Bmx.js +1 -0
  42. package/build/client/_app/immutable/entry/start.Xp5z9Bmx.js.br +0 -0
  43. package/build/client/_app/immutable/entry/start.Xp5z9Bmx.js.gz +0 -0
  44. package/build/client/_app/immutable/nodes/0.Df_skSaZ.js +1 -0
  45. package/build/client/_app/immutable/nodes/0.Df_skSaZ.js.br +0 -0
  46. package/build/client/_app/immutable/nodes/0.Df_skSaZ.js.gz +0 -0
  47. package/build/client/_app/immutable/nodes/1.B-Gp2MAj.js +1 -0
  48. package/build/client/_app/immutable/nodes/1.B-Gp2MAj.js.br +0 -0
  49. package/build/client/_app/immutable/nodes/1.B-Gp2MAj.js.gz +0 -0
  50. package/build/client/_app/immutable/nodes/2.DFHvXQZ-.js +3 -0
  51. package/build/client/_app/immutable/nodes/2.DFHvXQZ-.js.br +0 -0
  52. package/build/client/_app/immutable/nodes/2.DFHvXQZ-.js.gz +0 -0
  53. package/build/client/_app/immutable/nodes/3.DVl0Y9iQ.js +1 -0
  54. package/build/client/_app/immutable/nodes/3.DVl0Y9iQ.js.br +0 -0
  55. package/build/client/_app/immutable/nodes/3.DVl0Y9iQ.js.gz +0 -0
  56. package/build/client/_app/immutable/nodes/4.Dw8y5rjM.js +2 -0
  57. package/build/client/_app/immutable/nodes/4.Dw8y5rjM.js.br +0 -0
  58. package/build/client/_app/immutable/nodes/4.Dw8y5rjM.js.gz +0 -0
  59. package/build/client/_app/version.json +1 -1
  60. package/build/client/_app/version.json.br +0 -0
  61. package/build/client/_app/version.json.gz +0 -0
  62. package/build/handler.js +7 -7
  63. package/build/index.js +7 -7
  64. package/build/server/chunks/chunks/{context.js-BJfk3ZiR.js → context.js-BNdRFspW.js} +100 -1
  65. package/build/server/chunks/chunks/context.js-BNdRFspW.js.map +1 -0
  66. package/build/server/chunks/chunks/{exports.js-aVVdkXgn.js → exports.js-CUFgHhPL.js} +2 -2
  67. package/build/server/chunks/chunks/{exports.js-aVVdkXgn.js.map → exports.js-CUFgHhPL.js.map} +1 -1
  68. package/build/server/chunks/chunks/{internal.js-C6cIfe5d.js → internal.js-DUquB_BJ.js} +3 -3
  69. package/build/server/chunks/chunks/{internal.js-C6cIfe5d.js.map → internal.js-DUquB_BJ.js.map} +1 -1
  70. package/build/server/chunks/chunks/internal2.js-BlXt2rfE.js.map +1 -1
  71. package/build/server/chunks/chunks/{shared.js-BNd0HSxp.js → shared.js-BXL1A-vX.js} +2 -2
  72. package/build/server/chunks/chunks/{shared.js-BNd0HSxp.js.map → shared.js-BXL1A-vX.js.map} +1 -1
  73. package/build/server/chunks/chunks/{utils.js-6HSqnONB.js → utils.js-xC62cOm2.js} +3 -3
  74. package/build/server/chunks/chunks/{utils.js-6HSqnONB.js.map → utils.js-xC62cOm2.js.map} +1 -1
  75. package/build/server/chunks/entries/endpoints/api/assets/_id_/{_server.ts.js-eMNgdaf4.js → _server.ts.js-DEfBMmVd.js} +2 -2
  76. package/build/server/chunks/entries/endpoints/api/assets/_id_/{_server.ts.js-eMNgdaf4.js.map → _server.ts.js-DEfBMmVd.js.map} +1 -1
  77. package/build/server/chunks/entries/endpoints/api/assets/_id_/raw/{_server.ts.js-CEQd4Wxn.js → _server.ts.js-BEJcx3ya.js} +2 -2
  78. package/build/server/chunks/entries/endpoints/api/assets/_id_/raw/{_server.ts.js-CEQd4Wxn.js.map → _server.ts.js-BEJcx3ya.js.map} +1 -1
  79. package/build/server/chunks/entries/endpoints/api/assets/_id_/reimport/{_server.ts.js-B2JoGCzy.js → _server.ts.js-CibkO-4G.js} +2 -2
  80. package/build/server/chunks/entries/endpoints/api/assets/_id_/reimport/{_server.ts.js-B2JoGCzy.js.map → _server.ts.js-CibkO-4G.js.map} +1 -1
  81. package/build/server/chunks/entries/endpoints/api/assets/{_server.ts.js-DTpkVywC.js → _server.ts.js-BdisCQk6.js} +2 -2
  82. package/build/server/chunks/entries/endpoints/api/assets/{_server.ts.js-DTpkVywC.js.map → _server.ts.js-BdisCQk6.js.map} +1 -1
  83. package/build/server/chunks/entries/endpoints/api/assets/delete/{_server.ts.js-D6Z_pGEZ.js → _server.ts.js-Bk2hojus.js} +2 -2
  84. package/build/server/chunks/entries/endpoints/api/assets/delete/{_server.ts.js-D6Z_pGEZ.js.map → _server.ts.js-Bk2hojus.js.map} +1 -1
  85. package/build/server/chunks/entries/endpoints/api/assets/move/{_server.ts.js-CnDi9p0a.js → _server.ts.js-CTeQlqUL.js} +2 -2
  86. package/build/server/chunks/entries/endpoints/api/assets/move/{_server.ts.js-CnDi9p0a.js.map → _server.ts.js-CTeQlqUL.js.map} +1 -1
  87. package/build/server/chunks/entries/endpoints/api/folders/_path_/{_server.ts.js-BCzuPX5Z.js → _server.ts.js-DLxdefrX.js} +2 -2
  88. package/build/server/chunks/entries/endpoints/api/folders/_path_/{_server.ts.js-BCzuPX5Z.js.map → _server.ts.js-DLxdefrX.js.map} +1 -1
  89. package/build/server/chunks/entries/endpoints/api/folders/{_server.ts.js-CGbYyJ6S.js → _server.ts.js-Ceq1tMTz.js} +2 -2
  90. package/build/server/chunks/entries/endpoints/api/folders/{_server.ts.js-CGbYyJ6S.js.map → _server.ts.js-Ceq1tMTz.js.map} +1 -1
  91. package/build/server/chunks/entries/endpoints/api/imports/{_server.ts.js-CL_89klb.js → _server.ts.js-DxAWqf9V.js} +2 -2
  92. package/build/server/chunks/entries/endpoints/api/imports/{_server.ts.js-CL_89klb.js.map → _server.ts.js-DxAWqf9V.js.map} +1 -1
  93. package/build/server/chunks/entries/endpoints/api/manifests/_name_/{_server.ts.js-DZUKtjz9.js → _server.ts.js-BB_6WywB.js} +2 -2
  94. package/build/server/chunks/entries/endpoints/api/manifests/_name_/{_server.ts.js-DZUKtjz9.js.map → _server.ts.js-BB_6WywB.js.map} +1 -1
  95. package/build/server/chunks/entries/endpoints/api/manifests/{_server.ts.js-Dz25LG_P.js → _server.ts.js-BLRkduAp.js} +2 -2
  96. package/build/server/chunks/entries/endpoints/api/manifests/{_server.ts.js-Dz25LG_P.js.map → _server.ts.js-BLRkduAp.js.map} +1 -1
  97. package/build/server/chunks/entries/endpoints/api/raw/adopt/{_server.ts.js-C-NuN3JJ.js → _server.ts.js-Ba94-p0V.js} +2 -2
  98. package/build/server/chunks/entries/endpoints/api/raw/adopt/{_server.ts.js-C-NuN3JJ.js.map → _server.ts.js-Ba94-p0V.js.map} +1 -1
  99. package/build/server/chunks/entries/endpoints/api/raw/edited/accept/{_server.ts.js-CwL9UPh3.js → _server.ts.js-CNhj0R4-.js} +2 -2
  100. package/build/server/chunks/entries/endpoints/api/raw/edited/accept/{_server.ts.js-CwL9UPh3.js.map → _server.ts.js-CNhj0R4-.js.map} +1 -1
  101. package/build/server/chunks/entries/endpoints/api/raw/edited/dismiss/{_server.ts.js-DanXYrj1.js → _server.ts.js-DWEV8mgx.js} +2 -2
  102. package/build/server/chunks/entries/endpoints/api/raw/edited/dismiss/{_server.ts.js-DanXYrj1.js.map → _server.ts.js-DWEV8mgx.js.map} +1 -1
  103. package/build/server/chunks/entries/endpoints/api/reconcile/{_server.ts.js-BZrun0up.js → _server.ts.js-BnDnZdPy.js} +2 -2
  104. package/build/server/chunks/entries/endpoints/api/reconcile/{_server.ts.js-BZrun0up.js.map → _server.ts.js-BnDnZdPy.js.map} +1 -1
  105. package/build/server/chunks/entries/endpoints/api/reveal/{_server.ts.js-DQOabHbS.js → _server.ts.js-1y2Lu6HB.js} +2 -2
  106. package/build/server/chunks/entries/endpoints/api/reveal/{_server.ts.js-DQOabHbS.js.map → _server.ts.js-1y2Lu6HB.js.map} +1 -1
  107. package/build/server/chunks/entries/endpoints/api/state/{_server.ts.js-k8Ueydx1.js → _server.ts.js-CnLJo2iZ.js} +2 -2
  108. package/build/server/chunks/entries/endpoints/api/state/{_server.ts.js-k8Ueydx1.js.map → _server.ts.js-CnLJo2iZ.js.map} +1 -1
  109. package/build/server/chunks/entries/endpoints/api/usage/{_server.ts.js-DmMmiFiu.js → _server.ts.js-Dqs6d6ul.js} +2 -2
  110. package/build/server/chunks/entries/endpoints/api/usage/{_server.ts.js-DmMmiFiu.js.map → _server.ts.js-Dqs6d6ul.js.map} +1 -1
  111. package/build/server/chunks/entries/endpoints/files/proxy/_...path_/{_server.ts.js-BQTC0OSC.js → _server.ts.js-BHdIopR2.js} +2 -2
  112. package/build/server/chunks/entries/endpoints/files/proxy/_...path_/{_server.ts.js-BQTC0OSC.js.map → _server.ts.js-BHdIopR2.js.map} +1 -1
  113. package/build/server/chunks/entries/endpoints/files/raw/_...path_/{_server.ts.js-mv9cqSuS.js → _server.ts.js-CFWfVYjg.js} +2 -2
  114. package/build/server/chunks/entries/endpoints/files/raw/_...path_/{_server.ts.js-mv9cqSuS.js.map → _server.ts.js-CFWfVYjg.js.map} +1 -1
  115. package/build/server/chunks/entries/{hooks.server.js-BNQO3cZr.js → hooks.server.js-BT84TVOj.js} +2 -2
  116. package/build/server/chunks/entries/{hooks.server.js-BNQO3cZr.js.map → hooks.server.js-BT84TVOj.js.map} +1 -1
  117. package/build/server/chunks/{handler-rG16rikT.js → handler-ChRrsqKi.js} +5 -5
  118. package/build/server/chunks/{handler-rG16rikT.js.map → handler-ChRrsqKi.js.map} +1 -1
  119. package/build/server/chunks/{index.js-DYK_SfL5.js → index.js-1EfZPKuZ.js} +5 -5
  120. package/build/server/chunks/{index.js-DYK_SfL5.js.map → index.js-1EfZPKuZ.js.map} +1 -1
  121. package/build/server/chunks/{manifest.js-h2lDpEPs.js → manifest.js-DR0qLyIe.js} +27 -27
  122. package/build/server/chunks/{manifest.js-h2lDpEPs.js.map → manifest.js-DR0qLyIe.js.map} +1 -1
  123. package/build/server/chunks/nodes/{0.js-Db28RwnA.js → 0.js-CHb0mVEc.js} +2 -2
  124. package/build/server/chunks/nodes/{0.js-Db28RwnA.js.map → 0.js-CHb0mVEc.js.map} +1 -1
  125. package/build/server/chunks/nodes/1.js-DXh-Y7sw.js +7 -0
  126. package/build/server/chunks/nodes/{1.js-D26Uv3YT.js.map → 1.js-DXh-Y7sw.js.map} +1 -1
  127. package/build/server/chunks/nodes/2.js-BSEFm6uL.js +7 -0
  128. package/build/server/chunks/nodes/2.js-BSEFm6uL.js.map +1 -0
  129. package/build/server/chunks/nodes/3.js-Cob8rFik.js +7 -0
  130. package/build/server/chunks/nodes/3.js-Cob8rFik.js.map +1 -0
  131. package/build/server/chunks/nodes/4.js-CsI1onmR.js +7 -0
  132. package/build/server/chunks/nodes/4.js-CsI1onmR.js.map +1 -0
  133. package/dist/core/api-types.d.ts +2 -0
  134. package/dist/runtime/chroma-key-video-lifecycle.d.ts +2 -2
  135. package/dist/runtime/chroma-key-video-lifecycle.js +220 -124
  136. package/dist/runtime/components/ChromaKeyVideo.svelte +13 -2
  137. package/dist/runtime/components/Slice.svelte +22 -12
  138. package/dist/runtime/components/Slice.svelte.d.ts +3 -6
  139. package/dist/runtime/components/SliceCanvas.svelte +51 -40
  140. package/dist/runtime/components/SliceCanvas.svelte.d.ts +2 -2
  141. package/dist/runtime/components/SliceDom.svelte +96 -42
  142. package/dist/runtime/components/SliceDom.svelte.d.ts +2 -2
  143. package/dist/runtime/slice-source-lifecycle.d.ts +14 -0
  144. package/dist/runtime/slice-source-lifecycle.js +70 -0
  145. package/dist/server/check.d.ts +1 -1
  146. package/dist/server/check.js +16 -2
  147. package/dist/server/cli-commands.d.ts +1 -1
  148. package/dist/server/cli-commands.js +2 -1
  149. package/dist/server/image-duplicates.d.ts +18 -0
  150. package/dist/server/image-duplicates.js +138 -0
  151. package/dist/server/operations.js +4 -0
  152. package/package.json +4 -1
  153. package/build/client/_app/immutable/assets/2.6TBn1n3x.css +0 -1
  154. package/build/client/_app/immutable/assets/2.6TBn1n3x.css.br +0 -0
  155. package/build/client/_app/immutable/assets/2.6TBn1n3x.css.gz +0 -0
  156. package/build/client/_app/immutable/assets/3.BuCXJbRG.css.br +0 -0
  157. package/build/client/_app/immutable/assets/3.BuCXJbRG.css.gz +0 -0
  158. package/build/client/_app/immutable/assets/ConfirmDialog.BdKlitwi.css +0 -1
  159. package/build/client/_app/immutable/assets/ConfirmDialog.BdKlitwi.css.br +0 -0
  160. package/build/client/_app/immutable/assets/ConfirmDialog.BdKlitwi.css.gz +0 -0
  161. package/build/client/_app/immutable/assets/LibraryContextMenu.BSnKt_Is.css +0 -1
  162. package/build/client/_app/immutable/assets/LibraryContextMenu.BSnKt_Is.css.br +0 -0
  163. package/build/client/_app/immutable/assets/LibraryContextMenu.BSnKt_Is.css.gz +0 -0
  164. package/build/client/_app/immutable/assets/Popover.Et4sBRbI.css +0 -1
  165. package/build/client/_app/immutable/assets/Popover.Et4sBRbI.css.br +0 -0
  166. package/build/client/_app/immutable/assets/Popover.Et4sBRbI.css.gz +0 -0
  167. package/build/client/_app/immutable/chunks/BAva9YXm.js +0 -1
  168. package/build/client/_app/immutable/chunks/BAva9YXm.js.br +0 -0
  169. package/build/client/_app/immutable/chunks/BAva9YXm.js.gz +0 -0
  170. package/build/client/_app/immutable/chunks/BGAUmugt.js +0 -1
  171. package/build/client/_app/immutable/chunks/BGAUmugt.js.br +0 -0
  172. package/build/client/_app/immutable/chunks/BGAUmugt.js.gz +0 -0
  173. package/build/client/_app/immutable/chunks/BMORe2Gb.js.br +0 -0
  174. package/build/client/_app/immutable/chunks/CBygkKLq.js +0 -1
  175. package/build/client/_app/immutable/chunks/CBygkKLq.js.br +0 -0
  176. package/build/client/_app/immutable/chunks/CBygkKLq.js.gz +0 -0
  177. package/build/client/_app/immutable/chunks/Cil0r9BR.js +0 -108
  178. package/build/client/_app/immutable/chunks/Cil0r9BR.js.br +0 -0
  179. package/build/client/_app/immutable/chunks/Cil0r9BR.js.gz +0 -0
  180. package/build/client/_app/immutable/chunks/D-p-QSzz.js +0 -1
  181. package/build/client/_app/immutable/chunks/D-p-QSzz.js.br +0 -0
  182. package/build/client/_app/immutable/chunks/D-p-QSzz.js.gz +0 -0
  183. package/build/client/_app/immutable/chunks/D15yxLq9.js +0 -1
  184. package/build/client/_app/immutable/chunks/D15yxLq9.js.br +0 -0
  185. package/build/client/_app/immutable/chunks/D15yxLq9.js.gz +0 -0
  186. package/build/client/_app/immutable/chunks/DFRlZOKz.js.br +0 -0
  187. package/build/client/_app/immutable/chunks/DFRlZOKz.js.gz +0 -0
  188. package/build/client/_app/immutable/chunks/DpFtnEPx.js.br +0 -4
  189. package/build/client/_app/immutable/chunks/DpFtnEPx.js.gz +0 -0
  190. package/build/client/_app/immutable/entry/app.BaP08B0_.js +0 -2
  191. package/build/client/_app/immutable/entry/app.BaP08B0_.js.br +0 -0
  192. package/build/client/_app/immutable/entry/app.BaP08B0_.js.gz +0 -0
  193. package/build/client/_app/immutable/entry/start.DntfygIK.js +0 -1
  194. package/build/client/_app/immutable/entry/start.DntfygIK.js.br +0 -0
  195. package/build/client/_app/immutable/entry/start.DntfygIK.js.gz +0 -0
  196. package/build/client/_app/immutable/nodes/0.6A6JK0Xm.js +0 -1
  197. package/build/client/_app/immutable/nodes/0.6A6JK0Xm.js.br +0 -0
  198. package/build/client/_app/immutable/nodes/0.6A6JK0Xm.js.gz +0 -0
  199. package/build/client/_app/immutable/nodes/1.8K7wLW9R.js +0 -1
  200. package/build/client/_app/immutable/nodes/1.8K7wLW9R.js.br +0 -0
  201. package/build/client/_app/immutable/nodes/1.8K7wLW9R.js.gz +0 -0
  202. package/build/client/_app/immutable/nodes/2.CB_PeUBC.js +0 -3
  203. package/build/client/_app/immutable/nodes/2.CB_PeUBC.js.br +0 -0
  204. package/build/client/_app/immutable/nodes/2.CB_PeUBC.js.gz +0 -0
  205. package/build/client/_app/immutable/nodes/3.qIkVQyP6.js +0 -1
  206. package/build/client/_app/immutable/nodes/3.qIkVQyP6.js.br +0 -0
  207. package/build/client/_app/immutable/nodes/3.qIkVQyP6.js.gz +0 -0
  208. package/build/client/_app/immutable/nodes/4.SmoVIy5D.js +0 -2
  209. package/build/client/_app/immutable/nodes/4.SmoVIy5D.js.br +0 -0
  210. package/build/client/_app/immutable/nodes/4.SmoVIy5D.js.gz +0 -0
  211. package/build/server/chunks/chunks/context.js-BJfk3ZiR.js.map +0 -1
  212. package/build/server/chunks/nodes/1.js-D26Uv3YT.js +0 -7
  213. package/build/server/chunks/nodes/2.js-BeRlEFid.js +0 -7
  214. package/build/server/chunks/nodes/2.js-BeRlEFid.js.map +0 -1
  215. package/build/server/chunks/nodes/3.js-BRUNmCyG.js +0 -7
  216. package/build/server/chunks/nodes/3.js-BRUNmCyG.js.map +0 -1
  217. package/build/server/chunks/nodes/4.js-CTHWqDf1.js +0 -7
  218. package/build/server/chunks/nodes/4.js-CTHWqDf1.js.map +0 -1
@@ -5,63 +5,81 @@ function copyChromaKey(parameters) {
5
5
  keyColor: [...parameters.keyColor]
6
6
  };
7
7
  }
8
- /** Internal media state machine for a single, stable video element. */
8
+ function copyPlaybackSettings(from, to) {
9
+ to.loop = from.loop;
10
+ to.playbackRate = from.playbackRate;
11
+ }
12
+ /** Internal media state machine that stages replacements before presenting them. */
9
13
  export class ChromaKeyVideoLifecycle {
10
- #video;
14
+ #videos;
11
15
  #renderer;
12
- #active = null;
16
+ #onPresentedVideoChange;
17
+ #presented = null;
18
+ #loading = null;
13
19
  #sourceGeneration = 0;
14
20
  #playing = false;
15
21
  #disposed = false;
22
+ #frameHeld = false;
16
23
  #loopGeneration = 0;
17
24
  #loopRunning = false;
18
25
  #pendingFrame = null;
19
- constructor(video, renderer) {
20
- this.#video = video;
26
+ constructor(video, renderer, standbyVideo = video, onPresentedVideoChange) {
27
+ this.#videos = [video, standbyVideo];
21
28
  this.#renderer = renderer;
22
- video.addEventListener('play', this.#handlePlay);
23
- video.addEventListener('pause', this.#handleStop);
24
- video.addEventListener('ended', this.#handleStop);
25
- video.addEventListener('seeked', this.#handleSeeked);
26
- video.addEventListener('loadeddata', this.#handleLoadedData);
27
- video.addEventListener('error', this.#handleError);
29
+ this.#onPresentedVideoChange = onPresentedVideoChange;
30
+ for (const element of new Set(this.#videos))
31
+ this.#listen(element);
28
32
  }
29
33
  switchSource(snapshot) {
30
34
  if (this.#disposed)
31
35
  return;
32
- const previous = this.#active;
33
- if (previous &&
34
- (previous.status === 'loading' || previous.status === 'settled') &&
35
- previous.rawVideoUrl === snapshot.rawVideoUrl &&
36
- previous.maskUrl === snapshot.maskUrl) {
36
+ const loading = this.#loading;
37
+ if (loading && this.#matches(loading, snapshot) && loading.status === 'loading')
38
+ return;
39
+ const presented = this.#presented;
40
+ if (presented && this.#matches(presented, snapshot) && presented.status === 'settled') {
41
+ this.#cancelLoading();
42
+ presented.chromaKey = copyChromaKey(snapshot.chromaKey);
37
43
  return;
38
44
  }
39
- this.#stopLoop();
40
- this.#invalidatePlayRequest(previous);
45
+ this.#cancelLoading();
46
+ const loadingVideo = this.#loadingVideoFor(presented);
47
+ const playAfterHandoff = presented
48
+ ? this.#playing &&
49
+ (presented.video.ended || !presented.video.paused || presented.playRequestPending)
50
+ : this.#playing;
51
+ if (presented && loadingVideo === presented.video) {
52
+ this.#invalidatePlayRequest(presented);
53
+ this.#stopLoop();
54
+ presented.status = 'disposed';
55
+ this.#presented = null;
56
+ presented.video.pause();
57
+ }
41
58
  const active = {
42
59
  generation: ++this.#sourceGeneration,
43
60
  status: 'loading',
61
+ video: loadingVideo,
44
62
  rawVideoUrl: snapshot.rawVideoUrl,
45
63
  normalizedVideoUrl: snapshot.resolvedVideoUrl,
46
64
  maskUrl: snapshot.maskUrl,
65
+ maskImage: null,
47
66
  chromaKey: copyChromaKey(snapshot.chromaKey),
48
67
  mediaSettled: false,
49
68
  maskSettled: !snapshot.maskUrl,
69
+ playAfterHandoff,
50
70
  playRequestToken: 0,
51
71
  playRequestPending: false,
52
72
  playRejectedForCurrentIntent: false
53
73
  };
54
- this.#active = active;
55
- this.#video.pause();
56
- this.#renderer.setMask(null);
57
- // Keep the last presented canvas frame visible while the replacement video
58
- // and mask settle. Clearing here exposes a transparent frame between the old
59
- // draw and the new source's first decodable frame.
60
- this.#video.src = snapshot.resolvedVideoUrl;
74
+ this.#loading = active;
75
+ if (!this.#frameHeld)
76
+ this.#renderer.setMask(null);
77
+ loadingVideo.pause();
78
+ loadingVideo.src = snapshot.resolvedVideoUrl;
61
79
  // Reading the reflected property gives the browser-normalized absolute URL.
62
- active.normalizedVideoUrl = this.#video.src;
63
- this.#video.load();
64
- this.#primeMedia();
80
+ active.normalizedVideoUrl = loadingVideo.src;
81
+ loadingVideo.load();
82
+ this.#primeMedia(active);
65
83
  if (snapshot.maskUrl)
66
84
  this.#loadMask(active, snapshot.maskUrl);
67
85
  }
@@ -70,30 +88,34 @@ export class ChromaKeyVideoLifecycle {
70
88
  return;
71
89
  const changed = this.#playing !== playing;
72
90
  this.#playing = playing;
73
- const active = this.#active;
91
+ const presented = this.#presented;
92
+ const loading = this.#loading;
93
+ if (loading?.status === 'loading')
94
+ loading.playAfterHandoff = playing;
74
95
  if (!playing) {
75
- this.#invalidatePlayRequest(active);
96
+ this.#invalidatePlayRequest(presented);
76
97
  this.#stopLoop();
77
- // Keep the muted loading warm-up alive until the browser has produced a
78
- // decodable frame. Some engines treat preload="auto" as metadata-only and
79
- // do not reach loadeddata until playback has started.
80
- if (!active || active.status !== 'loading' || active.mediaSettled) {
81
- this.#video.pause();
82
- }
98
+ presented?.video.pause();
83
99
  return;
84
100
  }
85
- if (changed && active)
86
- active.playRejectedForCurrentIntent = false;
87
- this.#requestPlayback(active);
101
+ if (changed && presented)
102
+ presented.playRejectedForCurrentIntent = false;
103
+ if (presented) {
104
+ presented.playAfterHandoff = true;
105
+ this.#requestPlayback(presented);
106
+ }
88
107
  }
89
108
  setChromaKey(parameters) {
90
109
  if (this.#disposed)
91
110
  return;
92
- const active = this.#active;
111
+ const active = this.#loading ?? this.#presented;
93
112
  if (!active || active.status === 'failed' || active.status === 'disposed')
94
113
  return;
95
114
  active.chromaKey = copyChromaKey(parameters);
96
- if (active.status === 'settled' && this.#video.paused && this.#isCurrentResource(active)) {
115
+ if (active === this.#presented &&
116
+ active.status === 'settled' &&
117
+ active.video.paused &&
118
+ this.#isCurrentResource(active)) {
97
119
  this.#draw(active);
98
120
  }
99
121
  }
@@ -102,19 +124,52 @@ export class ChromaKeyVideoLifecycle {
102
124
  return;
103
125
  this.#disposed = true;
104
126
  this.#sourceGeneration++;
105
- const active = this.#active;
106
- this.#invalidatePlayRequest(active);
127
+ this.#invalidatePlayRequest(this.#presented);
128
+ this.#invalidatePlayRequest(this.#loading);
107
129
  this.#stopLoop();
108
- if (active)
109
- active.status = 'disposed';
110
- this.#active = null;
111
- this.#video.removeEventListener('play', this.#handlePlay);
112
- this.#video.removeEventListener('pause', this.#handleStop);
113
- this.#video.removeEventListener('ended', this.#handleStop);
114
- this.#video.removeEventListener('seeked', this.#handleSeeked);
115
- this.#video.removeEventListener('loadeddata', this.#handleLoadedData);
116
- this.#video.removeEventListener('error', this.#handleError);
117
- this.#video.pause();
130
+ if (this.#presented)
131
+ this.#presented.status = 'disposed';
132
+ if (this.#loading)
133
+ this.#loading.status = 'disposed';
134
+ this.#presented = null;
135
+ this.#loading = null;
136
+ for (const video of new Set(this.#videos)) {
137
+ this.#unlisten(video);
138
+ video.pause();
139
+ }
140
+ }
141
+ #listen(video) {
142
+ video.addEventListener('play', this.#handlePlay);
143
+ video.addEventListener('pause', this.#handleStop);
144
+ video.addEventListener('ended', this.#handleStop);
145
+ video.addEventListener('seeked', this.#handleSeeked);
146
+ video.addEventListener('loadeddata', this.#handleLoadedData);
147
+ video.addEventListener('error', this.#handleError);
148
+ }
149
+ #unlisten(video) {
150
+ video.removeEventListener('play', this.#handlePlay);
151
+ video.removeEventListener('pause', this.#handleStop);
152
+ video.removeEventListener('ended', this.#handleStop);
153
+ video.removeEventListener('seeked', this.#handleSeeked);
154
+ video.removeEventListener('loadeddata', this.#handleLoadedData);
155
+ video.removeEventListener('error', this.#handleError);
156
+ }
157
+ #matches(active, snapshot) {
158
+ return active.rawVideoUrl === snapshot.rawVideoUrl && active.maskUrl === snapshot.maskUrl;
159
+ }
160
+ #loadingVideoFor(presented) {
161
+ if (!presented)
162
+ return this.#videos[0];
163
+ return this.#videos[0] === presented.video ? this.#videos[1] : this.#videos[0];
164
+ }
165
+ #cancelLoading() {
166
+ const loading = this.#loading;
167
+ if (!loading)
168
+ return;
169
+ this.#invalidatePlayRequest(loading);
170
+ loading.status = 'disposed';
171
+ loading.video.pause();
172
+ this.#loading = null;
118
173
  }
119
174
  #loadMask(active, maskUrl) {
120
175
  const image = new Image();
@@ -122,122 +177,161 @@ export class ChromaKeyVideoLifecycle {
122
177
  image
123
178
  .decode()
124
179
  .then(() => {
125
- if (!this.#isActive(active) || active.status !== 'loading')
180
+ if (!this.#isLoading(active))
126
181
  return;
127
- this.#renderer.setMask(image);
182
+ active.maskImage = image;
128
183
  active.maskSettled = true;
129
184
  this.#settleIfReady(active);
130
185
  })
131
186
  .catch(() => {
132
- if (!this.#isActive(active) || active.status !== 'loading')
187
+ if (!this.#isLoading(active))
133
188
  return;
134
189
  console.warn(`Failed to load chroma-key mask "${maskUrl}".`);
135
190
  active.maskSettled = true;
136
191
  this.#settleIfReady(active);
137
192
  });
138
193
  }
139
- #primeMedia() {
194
+ #primeMedia(active) {
140
195
  let request;
141
196
  try {
142
- request = this.#video.play();
197
+ request = active.video.play();
143
198
  }
144
199
  catch {
145
200
  return;
146
201
  }
147
202
  Promise.resolve(request).catch(() => {
148
- // Muted warm-up is best-effort. If preload still reaches loadeddata, the
149
- // normal settled playback request below gets its own retry state.
203
+ // Muted warm-up is best-effort. preload may still reach loadeddata.
150
204
  });
151
205
  }
152
- #handleLoadedData = () => {
153
- const active = this.#active;
154
- if (!active || active.status !== 'loading')
206
+ #handleLoadedData = (event) => {
207
+ const active = this.#loading;
208
+ if (!active || active.video !== event.currentTarget || !this.#isLoading(active))
155
209
  return;
156
210
  if (!this.#isCurrentResource(active))
157
211
  return;
158
212
  active.mediaSettled = true;
159
- if (!this.#playing)
160
- this.#video.pause();
213
+ // Freeze the staged source at its first decodable frame until its mask is ready.
214
+ active.video.pause();
161
215
  this.#settleIfReady(active);
162
216
  };
163
- #handleError = () => {
164
- const active = this.#active;
165
- if (!active || active.status === 'failed' || active.status === 'disposed')
217
+ #handleError = (event) => {
218
+ const video = event.currentTarget;
219
+ const loading = this.#loading;
220
+ if (loading?.video === video && loading.status === 'loading') {
221
+ loading.status = 'failed';
222
+ this.#invalidatePlayRequest(loading);
223
+ loading.video.pause();
224
+ // A failed replacement never erases an already presented frame.
225
+ if (!this.#presented && !this.#frameHeld)
226
+ this.#clear();
166
227
  return;
167
- active.status = 'failed';
168
- this.#invalidatePlayRequest(active);
228
+ }
229
+ const presented = this.#presented;
230
+ if (!presented || presented.video !== video || presented.status !== 'settled')
231
+ return;
232
+ presented.status = 'failed';
233
+ this.#invalidatePlayRequest(presented);
169
234
  this.#stopLoop();
170
- this.#video.pause();
171
- this.#renderer.clear();
235
+ presented.video.pause();
236
+ if (!this.#loading)
237
+ this.#clear();
172
238
  };
173
- #handlePlay = () => {
174
- const active = this.#active;
175
- // Loading playback only warms the hidden media element up to loadeddata.
176
- // Drawing and the frame loop remain gated on video + mask settlement.
177
- if (active?.status === 'loading')
239
+ #handlePlay = (event) => {
240
+ const video = event.currentTarget;
241
+ if (this.#loading?.video === video)
178
242
  return;
179
- if (!active || active.status !== 'settled') {
180
- this.#stopLoop();
181
- this.#video.pause();
243
+ const presented = this.#presented;
244
+ if (!presented || presented.video !== video || presented.status !== 'settled') {
245
+ if (video && 'pause' in video)
246
+ video.pause();
182
247
  return;
183
248
  }
184
- active.playRejectedForCurrentIntent = false;
185
- if (!this.#video.paused)
186
- this.#startLoop(active);
249
+ presented.playRejectedForCurrentIntent = false;
250
+ presented.playAfterHandoff = true;
251
+ if (this.#loading?.status === 'loading')
252
+ this.#loading.playAfterHandoff = true;
253
+ if (!presented.video.paused)
254
+ this.#startLoop(presented);
187
255
  };
188
- #handleStop = () => {
189
- const active = this.#active;
256
+ #handleStop = (event) => {
257
+ const presented = this.#presented;
258
+ if (!presented || presented.video !== event.currentTarget || presented.status !== 'settled')
259
+ return;
260
+ this.#invalidatePlayRequest(presented);
190
261
  this.#stopLoop();
191
- if (active?.status === 'settled')
192
- this.#draw(active);
262
+ this.#draw(presented);
263
+ presented.playAfterHandoff = false;
264
+ if (this.#loading?.status === 'loading')
265
+ this.#loading.playAfterHandoff = false;
193
266
  };
194
- #handleSeeked = () => {
195
- const active = this.#active;
196
- if (!active || active.status !== 'settled' || !this.#video.paused)
267
+ #handleSeeked = (event) => {
268
+ const presented = this.#presented;
269
+ if (!presented ||
270
+ presented.video !== event.currentTarget ||
271
+ presented.status !== 'settled' ||
272
+ !presented.video.paused) {
197
273
  return;
198
- this.#draw(active);
274
+ }
275
+ this.#draw(presented);
199
276
  };
200
277
  #settleIfReady(active) {
201
- if (!this.#isActive(active) ||
202
- active.status !== 'loading' ||
278
+ if (!this.#isLoading(active) ||
203
279
  !active.mediaSettled ||
204
- !active.maskSettled) {
280
+ !active.maskSettled ||
281
+ !this.#isCurrentResource(active)) {
205
282
  return;
206
283
  }
207
- if (!this.#isCurrentResource(active)) {
208
- active.mediaSettled = false;
209
- return;
284
+ const previous = this.#presented;
285
+ this.#stopLoop();
286
+ this.#invalidatePlayRequest(previous);
287
+ if (previous && previous.video !== active.video) {
288
+ copyPlaybackSettings(previous.video, active.video);
210
289
  }
211
- if (!this.#playing && !this.#video.paused)
212
- this.#video.pause();
290
+ active.video.pause();
213
291
  active.status = 'settled';
292
+ this.#loading = null;
293
+ this.#presented = active;
294
+ this.#renderer.setMask(active.maskImage);
214
295
  this.#draw(active);
215
- if (this.#playing && !this.#video.paused)
216
- this.#startLoop(active);
217
- else
296
+ this.#onPresentedVideoChange?.(active.video);
297
+ if (previous && previous !== active) {
298
+ previous.status = 'disposed';
299
+ previous.video.pause();
300
+ }
301
+ if (this.#playing && active.playAfterHandoff)
218
302
  this.#requestPlayback(active);
219
303
  }
220
304
  #draw(active) {
221
- if (!this.#isActive(active) ||
305
+ if (this.#disposed ||
306
+ this.#presented !== active ||
222
307
  active.status !== 'settled' ||
223
308
  !this.#isCurrentResource(active)) {
224
309
  return;
225
310
  }
226
- this.#renderer.draw(this.#video, active.chromaKey);
311
+ this.#renderer.draw(active.video, active.chromaKey);
312
+ this.#frameHeld = true;
313
+ }
314
+ #clear() {
315
+ this.#renderer.clear();
316
+ this.#frameHeld = false;
227
317
  }
228
318
  #isCurrentResource(active) {
229
- return (this.#video.readyState >= HAVE_CURRENT_DATA &&
230
- this.#video.currentSrc === active.normalizedVideoUrl);
319
+ return (active.video.readyState >= HAVE_CURRENT_DATA &&
320
+ active.video.currentSrc === active.normalizedVideoUrl);
231
321
  }
232
- #isActive(active) {
233
- return (!this.#disposed && this.#active === active && active.generation === this.#sourceGeneration);
322
+ #isLoading(active) {
323
+ return (!this.#disposed &&
324
+ this.#loading === active &&
325
+ active.status === 'loading' &&
326
+ active.generation === this.#sourceGeneration);
234
327
  }
235
328
  #requestPlayback(active) {
236
- if (!active ||
237
- !this.#isActive(active) ||
329
+ if (this.#disposed ||
330
+ this.#presented !== active ||
238
331
  active.status !== 'settled' ||
239
332
  !this.#playing ||
240
- !this.#video.paused ||
333
+ !active.playAfterHandoff ||
334
+ !active.video.paused ||
241
335
  active.playRequestPending ||
242
336
  active.playRejectedForCurrentIntent) {
243
337
  return;
@@ -246,7 +340,7 @@ export class ChromaKeyVideoLifecycle {
246
340
  active.playRequestPending = true;
247
341
  let request;
248
342
  try {
249
- request = this.#video.play();
343
+ request = active.video.play();
250
344
  }
251
345
  catch {
252
346
  this.#finishPlayRequest(active, requestToken, false);
@@ -255,7 +349,7 @@ export class ChromaKeyVideoLifecycle {
255
349
  Promise.resolve(request).then(() => this.#finishPlayRequest(active, requestToken, true), () => this.#finishPlayRequest(active, requestToken, false));
256
350
  }
257
351
  #finishPlayRequest(active, requestToken, succeeded) {
258
- if (!this.#isActive(active) || active.playRequestToken !== requestToken)
352
+ if (this.#disposed || this.#presented !== active || active.playRequestToken !== requestToken)
259
353
  return;
260
354
  active.playRequestPending = false;
261
355
  active.playRejectedForCurrentIntent = !succeeded;
@@ -268,9 +362,10 @@ export class ChromaKeyVideoLifecycle {
268
362
  }
269
363
  #startLoop(active) {
270
364
  if (this.#loopRunning ||
271
- !this.#isActive(active) ||
365
+ this.#disposed ||
366
+ this.#presented !== active ||
272
367
  active.status !== 'settled' ||
273
- this.#video.paused) {
368
+ active.video.paused) {
274
369
  return;
275
370
  }
276
371
  this.#loopRunning = true;
@@ -279,6 +374,7 @@ export class ChromaKeyVideoLifecycle {
279
374
  }
280
375
  #scheduleNextFrame(active, loopGeneration) {
281
376
  const sourceGeneration = active.generation;
377
+ const video = active.video;
282
378
  let handle = 0;
283
379
  const callback = () => {
284
380
  const pending = this.#pendingFrame;
@@ -287,28 +383,28 @@ export class ChromaKeyVideoLifecycle {
287
383
  pending.handle !== handle ||
288
384
  this.#loopGeneration !== loopGeneration ||
289
385
  !this.#loopRunning ||
290
- !this.#isActive(active) ||
386
+ this.#presented !== active ||
291
387
  active.generation !== sourceGeneration ||
292
388
  active.status !== 'settled' ||
293
- this.#video.paused) {
389
+ video.paused) {
294
390
  return;
295
391
  }
296
392
  this.#pendingFrame = null;
297
393
  this.#draw(active);
298
394
  if (this.#loopRunning &&
299
395
  this.#loopGeneration === loopGeneration &&
300
- this.#isActive(active) &&
301
- !this.#video.paused) {
396
+ this.#presented === active &&
397
+ !video.paused) {
302
398
  this.#scheduleNextFrame(active, loopGeneration);
303
399
  }
304
400
  };
305
- if (this.#video.requestVideoFrameCallback) {
306
- handle = this.#video.requestVideoFrameCallback(callback);
307
- this.#pendingFrame = { generation: loopGeneration, kind: 'rvfc', handle };
401
+ if (video.requestVideoFrameCallback) {
402
+ handle = video.requestVideoFrameCallback(callback);
403
+ this.#pendingFrame = { generation: loopGeneration, kind: 'rvfc', handle, video };
308
404
  }
309
405
  else {
310
406
  handle = requestAnimationFrame(callback);
311
- this.#pendingFrame = { generation: loopGeneration, kind: 'raf', handle };
407
+ this.#pendingFrame = { generation: loopGeneration, kind: 'raf', handle, video };
312
408
  }
313
409
  }
314
410
  #stopLoop() {
@@ -319,7 +415,7 @@ export class ChromaKeyVideoLifecycle {
319
415
  if (!pending)
320
416
  return;
321
417
  if (pending.kind === 'rvfc')
322
- this.#video.cancelVideoFrameCallback?.(pending.handle);
418
+ pending.video.cancelVideoFrameCallback?.(pending.handle);
323
419
  else
324
420
  cancelAnimationFrame(pending.handle);
325
421
  }
@@ -33,6 +33,8 @@
33
33
  }: Props = $props();
34
34
 
35
35
  let canvasElement = $state<HTMLCanvasElement>();
36
+ let primaryVideoElement = $state<HTMLVideoElement>();
37
+ let standbyVideoElement = $state<HTMLVideoElement>();
36
38
  let webglUnavailable = $state(false);
37
39
 
38
40
  let renderer: ChromaKeyGl | null = null;
@@ -49,7 +51,15 @@
49
51
  return;
50
52
  }
51
53
  const activeRenderer = renderer;
52
- const activeLifecycle = new ChromaKeyVideoLifecycle(videoElement!, activeRenderer);
54
+ videoElement = primaryVideoElement!;
55
+ const activeLifecycle = new ChromaKeyVideoLifecycle(
56
+ primaryVideoElement!,
57
+ activeRenderer,
58
+ standbyVideoElement!,
59
+ (activeVideo) => {
60
+ videoElement = activeVideo;
61
+ }
62
+ );
53
63
  lifecycle = activeLifecycle;
54
64
 
55
65
  return () => {
@@ -102,7 +112,8 @@
102
112
  {/if}
103
113
  {:else}
104
114
  <canvas bind:this={canvasElement}></canvas>
105
- <video bind:this={videoElement} {loop} muted playsinline preload="auto"></video>
115
+ <video bind:this={primaryVideoElement} {loop} muted playsinline preload="auto"></video>
116
+ <video bind:this={standbyVideoElement} {loop} muted playsinline preload="auto"></video>
106
117
  {/if}
107
118
  </div>
108
119
 
@@ -1,23 +1,23 @@
1
1
  <!--
2
- The slice component hosts render: one component, backend chosen
3
- per use. Dispatches between the two private implementations — SliceDom
4
- (default) and SliceCanvas — which share the same geometry
5
- (`computeRenderCells`) and therefore render the same cells.
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.
6
7
  -->
7
8
  <script lang="ts">
9
+ import { onDestroy } from 'svelte';
8
10
  import type { SliceManifestEntry } from '../../core/index.js';
11
+ import { SliceSourceLifecycle, type LoadedSliceSnapshot } from '../slice-source-lifecycle.js';
9
12
  import SliceDom from './SliceDom.svelte';
10
13
  import SliceCanvas from './SliceCanvas.svelte';
11
14
 
12
15
  interface Props {
13
16
  src: SliceManifestEntry;
14
17
  /**
15
- * Rendering backend. `dom` (default) paints one absolutely-positioned
16
- * `<img>` window per cell; `canvas` draws every cell into a single
17
- * `<canvas>`. Both produce the same cells — but the DOM backend can
18
- * flicker in some situations (cells are separate elements the browser
19
- * composites independently, so a resize mid-animation can tear), so
20
- * hosts pick `canvas` where that hurts.
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.
21
21
  */
22
22
  backend?: 'dom' | 'canvas';
23
23
  /** Accessible label for the rendered graphic. */
@@ -26,10 +26,20 @@
26
26
  }
27
27
 
28
28
  let { src, backend = 'dom', alt = '', class: className = '' }: Props = $props();
29
+ let snapshot = $state.raw<LoadedSliceSnapshot | null>(null);
30
+ const sourceLifecycle = new SliceSourceLifecycle((loaded) => {
31
+ snapshot = loaded;
32
+ });
33
+
34
+ $effect(() => {
35
+ sourceLifecycle.request(src);
36
+ });
37
+
38
+ onDestroy(() => sourceLifecycle.dispose());
29
39
  </script>
30
40
 
31
41
  {#if backend === 'canvas'}
32
- <SliceCanvas {src} {alt} class={className} />
42
+ <SliceCanvas {snapshot} {alt} class={className} />
33
43
  {:else}
34
- <SliceDom {src} {alt} class={className} />
44
+ <SliceDom {snapshot} {alt} class={className} />
35
45
  {/if}
@@ -2,12 +2,9 @@ import type { SliceManifestEntry } from '../../core/index.js';
2
2
  interface Props {
3
3
  src: SliceManifestEntry;
4
4
  /**
5
- * Rendering backend. `dom` (default) paints one absolutely-positioned
6
- * `<img>` window per cell; `canvas` draws every cell into a single
7
- * `<canvas>`. Both produce the same cells — but the DOM backend can
8
- * flicker in some situations (cells are separate elements the browser
9
- * composites independently, so a resize mid-animation can tear), so
10
- * hosts pick `canvas` where that hurts.
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.
11
8
  */
12
9
  backend?: 'dom' | 'canvas';
13
10
  /** Accessible label for the rendered graphic. */