framefields 2.0.8 → 2.0.9

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 (310) hide show
  1. package/dist/audio/index.d.mts +4 -4
  2. package/dist/audio/index.mjs +10 -10
  3. package/dist/{audio-a6pY05g2.d.mts → audio-BxA0sL9z.d.mts} +3 -3
  4. package/dist/{audio-a6pY05g2.d.mts.map → audio-BxA0sL9z.d.mts.map} +1 -1
  5. package/dist/{audio--hi7MiZt.mjs → audio-Dev4NGPa.mjs} +3 -3
  6. package/dist/{audio--hi7MiZt.mjs.map → audio-Dev4NGPa.mjs.map} +1 -1
  7. package/dist/{client-C74hb7fy.mjs → client-CwRhflHE.mjs} +1 -1
  8. package/dist/{client-C74hb7fy.mjs.map → client-CwRhflHE.mjs.map} +1 -1
  9. package/dist/{dist-ZVlYkuR4.mjs → dist-CoSg-CWr.mjs} +17 -8
  10. package/dist/dist-CoSg-CWr.mjs.map +1 -0
  11. package/dist/{dist-BQFap0wr.mjs → dist-DgfykO99.mjs} +53 -5
  12. package/dist/dist-DgfykO99.mjs.map +1 -0
  13. package/dist/{dist-cjs-qEJrwjGk.mjs → dist-cjs--lfMLs3U.mjs} +4 -4
  14. package/dist/{dist-cjs-qEJrwjGk.mjs.map → dist-cjs--lfMLs3U.mjs.map} +1 -1
  15. package/dist/{dist-cjs-x5cwlqA5.mjs → dist-cjs--pxyGFw2.mjs} +1 -1
  16. package/dist/{dist-cjs-x5cwlqA5.mjs.map → dist-cjs--pxyGFw2.mjs.map} +1 -1
  17. package/dist/dist-cjs-3pon9kTt.mjs +6 -0
  18. package/dist/{dist-cjs-Dzs5P3K0.mjs → dist-cjs-BDpO-TH_.mjs} +1 -1
  19. package/dist/{dist-cjs-Dzs5P3K0.mjs.map → dist-cjs-BDpO-TH_.mjs.map} +1 -1
  20. package/dist/{dist-cjs-lYvO8aDo.mjs → dist-cjs-BiKQozdJ.mjs} +3 -3
  21. package/dist/{dist-cjs-lYvO8aDo.mjs.map → dist-cjs-BiKQozdJ.mjs.map} +1 -1
  22. package/dist/{dist-cjs-BUwe2XhR.mjs → dist-cjs-BmyyWYWv.mjs} +2 -2
  23. package/dist/{dist-cjs-BUwe2XhR.mjs.map → dist-cjs-BmyyWYWv.mjs.map} +1 -1
  24. package/dist/{dist-cjs-DgxjcvBN.mjs → dist-cjs-CND_ghfH.mjs} +1 -1
  25. package/dist/{dist-cjs-DgxjcvBN.mjs.map → dist-cjs-CND_ghfH.mjs.map} +1 -1
  26. package/dist/{dist-cjs-BWgoUCXJ.mjs → dist-cjs-CQwmMllY.mjs} +6 -6
  27. package/dist/{dist-cjs-BWgoUCXJ.mjs.map → dist-cjs-CQwmMllY.mjs.map} +1 -1
  28. package/dist/{dist-cjs-B5EVqDFp.mjs → dist-cjs-CgFzrgj8.mjs} +1 -1
  29. package/dist/{dist-cjs-B5EVqDFp.mjs.map → dist-cjs-CgFzrgj8.mjs.map} +1 -1
  30. package/dist/{dist-cjs-ECMuIF57.mjs → dist-cjs-CsU2sZoX.mjs} +4 -4
  31. package/dist/{dist-cjs-ECMuIF57.mjs.map → dist-cjs-CsU2sZoX.mjs.map} +1 -1
  32. package/dist/{dist-cjs-BKmGCgtm.mjs → dist-cjs-DJ8fcQEe.mjs} +11 -11
  33. package/dist/{dist-cjs-BKmGCgtm.mjs.map → dist-cjs-DJ8fcQEe.mjs.map} +1 -1
  34. package/dist/{dist-cjs-D7kEM-Mj.mjs → dist-cjs-DT2jJlcK.mjs} +5 -5
  35. package/dist/{dist-cjs-D7kEM-Mj.mjs.map → dist-cjs-DT2jJlcK.mjs.map} +1 -1
  36. package/dist/{dist-cjs-BUc0_kg-.mjs → dist-cjs-DjzmP0-x.mjs} +13 -13
  37. package/dist/{dist-cjs-BUc0_kg-.mjs.map → dist-cjs-DjzmP0-x.mjs.map} +1 -1
  38. package/dist/{dist-cjs-BGJkdexX.mjs → dist-cjs-DzjrlfbE.mjs} +14 -14
  39. package/dist/{dist-cjs-BGJkdexX.mjs.map → dist-cjs-DzjrlfbE.mjs.map} +1 -1
  40. package/dist/{dist-cjs-Df7144zM.mjs → dist-cjs-M-8QOgS_.mjs} +2 -2
  41. package/dist/{dist-cjs-Df7144zM.mjs.map → dist-cjs-M-8QOgS_.mjs.map} +1 -1
  42. package/dist/{dist-cjs-DFUmy89O.mjs → dist-cjs-TsA8LFXC.mjs} +3 -3
  43. package/dist/{dist-cjs-DFUmy89O.mjs.map → dist-cjs-TsA8LFXC.mjs.map} +1 -1
  44. package/dist/{dist-cjs-DsMdld8l.mjs → dist-cjs-agQl9T8M.mjs} +6 -6
  45. package/dist/{dist-cjs-DsMdld8l.mjs.map → dist-cjs-agQl9T8M.mjs.map} +1 -1
  46. package/dist/effects/index.d.mts +5 -5
  47. package/dist/effects/index.mjs +27 -26
  48. package/dist/{effects-axfL_C3a.mjs → effects-hxRwxjyC.mjs} +27 -3
  49. package/dist/effects-hxRwxjyC.mjs.map +1 -0
  50. package/dist/{event-streams-D-APZ7Ke.mjs → event-streams-BaNGaLxh.mjs} +2 -2
  51. package/dist/{event-streams-D-APZ7Ke.mjs.map → event-streams-BaNGaLxh.mjs.map} +1 -1
  52. package/dist/fonts/index.d.mts +1 -1
  53. package/dist/fonts/index.mjs +1 -1
  54. package/dist/{fonts-Cy26mbrO.mjs → fonts-BznIea-I.mjs} +2 -2
  55. package/dist/{fonts-Cy26mbrO.mjs.map → fonts-BznIea-I.mjs.map} +1 -1
  56. package/dist/{index-BJUS4Nx6.d.mts → index-BXNNJVs8.d.mts} +2 -2
  57. package/dist/{index-BJUS4Nx6.d.mts.map → index-BXNNJVs8.d.mts.map} +1 -1
  58. package/dist/{index-CbzEM_o6.d.mts → index-CQcdKyxF.d.mts} +1 -1
  59. package/dist/{index-CbzEM_o6.d.mts.map → index-CQcdKyxF.d.mts.map} +1 -1
  60. package/dist/{index-BJx_Qxc_.d.mts → index-CvDVpaKn.d.mts} +69 -27
  61. package/dist/index-CvDVpaKn.d.mts.map +1 -0
  62. package/dist/{index-WIPHLy5m.d.mts → index-D1zaOwEU.d.mts} +1 -1
  63. package/dist/index-D1zaOwEU.d.mts.map +1 -0
  64. package/dist/{index-CZChQhXZ.d.mts → index-J9CIK_nu.d.mts} +4 -2
  65. package/dist/index-J9CIK_nu.d.mts.map +1 -0
  66. package/dist/{index-Bi2nRZ_g.d.mts → index-fo4E7A6C.d.mts} +3 -3
  67. package/dist/{index-Bi2nRZ_g.d.mts.map → index-fo4E7A6C.d.mts.map} +1 -1
  68. package/dist/{index-BLbUQolI.d.mts → index-oodg5k9K.d.mts} +3 -3
  69. package/dist/{index-BLbUQolI.d.mts.map → index-oodg5k9K.d.mts.map} +1 -1
  70. package/dist/index.d.mts +11 -10
  71. package/dist/index.d.mts.map +1 -1
  72. package/dist/index.mjs +43 -42
  73. package/dist/index.mjs.map +1 -1
  74. package/dist/{package-C58KmGKw.mjs → package-XpCRbTOA.mjs} +1 -1
  75. package/dist/{package-C58KmGKw.mjs.map → package-XpCRbTOA.mjs.map} +1 -1
  76. package/dist/preview/index.d.mts +2 -2
  77. package/dist/preview/index.mjs +11 -11
  78. package/dist/{preview-BUPB38Kk.mjs → preview-DZTbEON9.mjs} +2 -2
  79. package/dist/{preview-BUPB38Kk.mjs.map → preview-DZTbEON9.mjs.map} +1 -1
  80. package/dist/preview-engine/audio.mjs +1 -1
  81. package/dist/preview-engine/{chunk-3ROJ7JVY.mjs → chunk-25IW7NYO.mjs} +1 -1
  82. package/dist/preview-engine/chunk-36YGYLKD.mjs +58 -0
  83. package/dist/preview-engine/chunk-4VOHF3FF.mjs +1 -0
  84. package/dist/preview-engine/{chunk-TNZENER3.mjs → chunk-7DIAFPQP.mjs} +2 -2
  85. package/dist/preview-engine/{chunk-CXFEQ5PM.mjs → chunk-7O24H4YT.mjs} +2 -2
  86. package/dist/preview-engine/{chunk-BE3CEVJB.mjs → chunk-AUFSM5KI.mjs} +2 -2
  87. package/dist/preview-engine/chunk-AW7HNZKR.mjs +98 -0
  88. package/dist/preview-engine/chunk-BHFI3Y4T.mjs +1 -0
  89. package/dist/preview-engine/chunk-DLXQDDIN.mjs +46 -0
  90. package/dist/preview-engine/chunk-EJGLUZMW.mjs +1 -0
  91. package/dist/preview-engine/{chunk-XSMK5E2V.mjs → chunk-JAOXXYXG.mjs} +2 -2
  92. package/dist/preview-engine/{chunk-PO4VAAQC.mjs → chunk-K52S5DJX.mjs} +1 -1
  93. package/dist/preview-engine/{chunk-IGFT3XLN.mjs → chunk-MDDF2GCF.mjs} +2 -2
  94. package/dist/preview-engine/{chunk-JNEVSC4I.mjs → chunk-OF75EHTX.mjs} +1 -1
  95. package/dist/preview-engine/{chunk-POPVFC5P.mjs → chunk-S4CSX6K5.mjs} +10 -10
  96. package/dist/preview-engine/chunk-SNYAHQ25.mjs +186 -0
  97. package/dist/preview-engine/{chunk-MC7TZZLD.mjs → chunk-T6COGNOW.mjs} +2 -2
  98. package/dist/preview-engine/chunk-VVLM6LZ3.mjs +1 -0
  99. package/dist/preview-engine/chunk-XUM42I4R.mjs +1 -0
  100. package/dist/preview-engine/chunk-ZDCA6NVT.mjs +82 -0
  101. package/dist/preview-engine/effects.mjs +1 -1
  102. package/dist/preview-engine/framefields.mjs +13 -13
  103. package/dist/preview-engine/player.mjs +5 -5
  104. package/dist/preview-engine/{renderers-Q2ZDD5K2.mjs → renderers-2US2NIUG.mjs} +2 -2
  105. package/dist/preview-engine/{renderers-54AQK6RF.mjs → renderers-32PIFHPS.mjs} +1 -1
  106. package/dist/preview-engine/{renderers-O6HOYIPS.mjs → renderers-76PQY2SO.mjs} +1 -1
  107. package/dist/preview-engine/{renderers-PXGLEFZO.mjs → renderers-CI2GXWLP.mjs} +1 -1
  108. package/dist/preview-engine/{renderers-GC7EGB5Z.mjs → renderers-CRKRWR2Y.mjs} +1 -1
  109. package/dist/preview-engine/{renderers-47UITXLZ.mjs → renderers-EE2GFG7J.mjs} +1 -1
  110. package/dist/preview-engine/{renderers-E3JYRLSD.mjs → renderers-GJSUN5HU.mjs} +1 -1
  111. package/dist/preview-engine/{renderers-KCP4TFHG.mjs → renderers-IHKORHMT.mjs} +1 -1
  112. package/dist/preview-engine/renderers-LTNTQLWE.mjs +248 -0
  113. package/dist/preview-engine/{renderers-OBMTU3QY.mjs → renderers-NQUDQEHY.mjs} +1 -1
  114. package/dist/preview-engine/{renderers-QECJP22T.mjs → renderers-NSY2SDIE.mjs} +1 -1
  115. package/dist/preview-engine/{renderers-KS4HPV6Q.mjs → renderers-PWLWOZY4.mjs} +1 -1
  116. package/dist/preview-engine/{renderers-TUYQLKHJ.mjs → renderers-W5CVDJVH.mjs} +1 -1
  117. package/dist/preview-engine/{renderers-N5UPJY73.mjs → renderers-WLLJFZTF.mjs} +1 -1
  118. package/dist/preview-engine/renderers-WN4SYFCU.mjs +1 -0
  119. package/dist/preview-engine/{renderers-TG62LZTF.mjs → renderers-XFBSGTHU.mjs} +1 -1
  120. package/dist/preview-engine/signals.mjs +1 -1
  121. package/dist/{program-Jt07Tx1J.mjs → program-C797QMRL.mjs} +2 -2
  122. package/dist/{program-Jt07Tx1J.mjs.map → program-C797QMRL.mjs.map} +1 -1
  123. package/dist/renderer/index.d.mts +2 -2
  124. package/dist/renderer/index.mjs +3 -3
  125. package/dist/{renderer-BkOXe6o6.mjs → renderer--vY6O19V.mjs} +2 -2
  126. package/dist/{renderer-BkOXe6o6.mjs.map → renderer--vY6O19V.mjs.map} +1 -1
  127. package/dist/{renderer-Bal8734U.d.mts → renderer-2NQWaZd7.d.mts} +2 -2
  128. package/dist/{renderer-Bal8734U.d.mts.map → renderer-2NQWaZd7.d.mts.map} +1 -1
  129. package/dist/renderer-3uTfN95I.mjs +3 -0
  130. package/dist/{renderer-D49-PYDo.mjs → renderer-5jfDUKd4.mjs} +2 -2
  131. package/dist/{renderer-D49-PYDo.mjs.map → renderer-5jfDUKd4.mjs.map} +1 -1
  132. package/dist/{renderer-C5RhNQdr.mjs → renderer-6PqlYQjC.mjs} +10 -9
  133. package/dist/{renderer-C5RhNQdr.mjs.map → renderer-6PqlYQjC.mjs.map} +1 -1
  134. package/dist/{renderer-CceiI6P7.mjs → renderer-9QCHY8Kh.mjs} +18 -4
  135. package/dist/renderer-9QCHY8Kh.mjs.map +1 -0
  136. package/dist/{renderer-o4wOvjEC.mjs → renderer-B1UeU7lX.mjs} +21 -18
  137. package/dist/renderer-B1UeU7lX.mjs.map +1 -0
  138. package/dist/{renderer-DDd3cjaz.mjs → renderer-B4ecTNR7.mjs} +18 -11
  139. package/dist/renderer-B4ecTNR7.mjs.map +1 -0
  140. package/dist/renderer-B5GslTP-.mjs +3 -0
  141. package/dist/{renderer-DXyaswab.mjs → renderer-B8gTyU3w.mjs} +18 -17
  142. package/dist/renderer-B8gTyU3w.mjs.map +1 -0
  143. package/dist/{renderer-CIQYTiT9.mjs → renderer-BE4Ns0nw.mjs} +2 -2
  144. package/dist/{renderer-CIQYTiT9.mjs.map → renderer-BE4Ns0nw.mjs.map} +1 -1
  145. package/dist/{renderer-BDLnExoP.mjs → renderer-BM05sB8m.mjs} +8 -7
  146. package/dist/renderer-BM05sB8m.mjs.map +1 -0
  147. package/dist/{renderer-DxBUppHf.mjs → renderer-BTYncYDf.mjs} +74 -63
  148. package/dist/{renderer-DxBUppHf.mjs.map → renderer-BTYncYDf.mjs.map} +1 -1
  149. package/dist/renderer-BUIWeIKQ.mjs +3 -0
  150. package/dist/{renderer-0uccAKQe.mjs → renderer-BXaK02qA.mjs} +2 -2
  151. package/dist/{renderer-0uccAKQe.mjs.map → renderer-BXaK02qA.mjs.map} +1 -1
  152. package/dist/renderer-B_LfC88A.mjs +3 -0
  153. package/dist/renderer-B_zmicsE.mjs +3 -0
  154. package/dist/{renderer-DrgNyCSx.mjs → renderer-BcPpcKDI.mjs} +2 -2
  155. package/dist/{renderer-DrgNyCSx.mjs.map → renderer-BcPpcKDI.mjs.map} +1 -1
  156. package/dist/{renderer-C-Bv9nVB.mjs → renderer-BgqjWI7K.mjs} +2 -2
  157. package/dist/{renderer-C-Bv9nVB.mjs.map → renderer-BgqjWI7K.mjs.map} +1 -1
  158. package/dist/{renderer-BPbNLPcz.mjs → renderer-BpJgfBQ8.mjs} +2 -2
  159. package/dist/{renderer-BPbNLPcz.mjs.map → renderer-BpJgfBQ8.mjs.map} +1 -1
  160. package/dist/renderer-BtNxtXkR.mjs +3 -0
  161. package/dist/{renderer-D5xelwpw.mjs → renderer-BwmZUkvd.mjs} +2 -2
  162. package/dist/{renderer-D5xelwpw.mjs.map → renderer-BwmZUkvd.mjs.map} +1 -1
  163. package/dist/{renderer-DIfk6m2O.mjs → renderer-C-PfbkPu.mjs} +2 -2
  164. package/dist/{renderer-DIfk6m2O.mjs.map → renderer-C-PfbkPu.mjs.map} +1 -1
  165. package/dist/renderer-C-sZSHtf.mjs +3 -0
  166. package/dist/renderer-C1zifktw.mjs +3 -0
  167. package/dist/{renderer-U0LJuM9D.mjs → renderer-C2gnWTQZ.mjs} +2 -2
  168. package/dist/{renderer-U0LJuM9D.mjs.map → renderer-C2gnWTQZ.mjs.map} +1 -1
  169. package/dist/{renderer-BdSHGzHj.mjs → renderer-CB8hMQgm.mjs} +1 -1
  170. package/dist/renderer-CCt2XTSM.mjs +3 -0
  171. package/dist/{renderer-BjROyv5F.mjs → renderer-CKJb_Lky.mjs} +2 -2
  172. package/dist/{renderer-BjROyv5F.mjs.map → renderer-CKJb_Lky.mjs.map} +1 -1
  173. package/dist/renderer-CMXcaArD.mjs +3 -0
  174. package/dist/{renderer-CIMjMFGm.mjs → renderer-CV8diddR.mjs} +2 -2
  175. package/dist/{renderer-CIMjMFGm.mjs.map → renderer-CV8diddR.mjs.map} +1 -1
  176. package/dist/renderer-CW9D5zCP.mjs +3 -0
  177. package/dist/{renderer-Dgg6pQGI.mjs → renderer-CckJJHbM.mjs} +2 -2
  178. package/dist/{renderer-Dgg6pQGI.mjs.map → renderer-CckJJHbM.mjs.map} +1 -1
  179. package/dist/{renderer-DFKsLrUs.mjs → renderer-CepMDQ06.mjs} +1 -1
  180. package/dist/{renderer-DFKsLrUs.mjs.map → renderer-CepMDQ06.mjs.map} +1 -1
  181. package/dist/renderer-CjY77sB_.mjs +3 -0
  182. package/dist/{renderer-DVQp5fLG.mjs → renderer-CpUbDJAV.mjs} +5 -4
  183. package/dist/{renderer-DVQp5fLG.mjs.map → renderer-CpUbDJAV.mjs.map} +1 -1
  184. package/dist/{renderer-zwxwjWqa.mjs → renderer-CrVAvP92.mjs} +2 -2
  185. package/dist/{renderer-zwxwjWqa.mjs.map → renderer-CrVAvP92.mjs.map} +1 -1
  186. package/dist/{renderer-CboM4tba.mjs → renderer-CuqhAB4i.mjs} +11 -5
  187. package/dist/renderer-CuqhAB4i.mjs.map +1 -0
  188. package/dist/{renderer-CmIySXSN.mjs → renderer-CxQ5v4fJ.mjs} +11 -5
  189. package/dist/renderer-CxQ5v4fJ.mjs.map +1 -0
  190. package/dist/renderer-D1R7vBUl.mjs +3 -0
  191. package/dist/{renderer-DabG62XS.mjs → renderer-D2XD4QYk.mjs} +1 -1
  192. package/dist/{renderer-DabG62XS.mjs.map → renderer-D2XD4QYk.mjs.map} +1 -1
  193. package/dist/{renderer-Bv-ljXHc.mjs → renderer-D3uNI10l.mjs} +2 -2
  194. package/dist/{renderer-Bv-ljXHc.mjs.map → renderer-D3uNI10l.mjs.map} +1 -1
  195. package/dist/renderer-D5yawxtJ.mjs +3 -0
  196. package/dist/{renderer-Bq3Geqe3.mjs → renderer-D6Gi_h4K.mjs} +1 -1
  197. package/dist/{renderer-DjI45OYC.mjs → renderer-D7zvPHKn.mjs} +1 -1
  198. package/dist/{renderer-DjI45OYC.mjs.map → renderer-D7zvPHKn.mjs.map} +1 -1
  199. package/dist/renderer-D9NnIx7o.mjs +3 -0
  200. package/dist/{renderer-D5GsF1LT.mjs → renderer-DAFyJ4dv.mjs} +2 -2
  201. package/dist/{renderer-D5GsF1LT.mjs.map → renderer-DAFyJ4dv.mjs.map} +1 -1
  202. package/dist/renderer-DC8jr1Ay.mjs +3 -0
  203. package/dist/{renderer-CXJxK7HP.mjs → renderer-DCK73tcD.mjs} +17 -4
  204. package/dist/renderer-DCK73tcD.mjs.map +1 -0
  205. package/dist/renderer-DCv-Abvn.mjs +4 -0
  206. package/dist/{renderer-B80s47yo.mjs → renderer-DEJwPRkR.mjs} +1 -1
  207. package/dist/{renderer-B80s47yo.mjs.map → renderer-DEJwPRkR.mjs.map} +1 -1
  208. package/dist/renderer-DPLFhzaV.mjs +3 -0
  209. package/dist/{renderer-B6Q5e9CU.mjs → renderer-DPpgBTlK.mjs} +2 -2
  210. package/dist/{renderer-B6Q5e9CU.mjs.map → renderer-DPpgBTlK.mjs.map} +1 -1
  211. package/dist/{renderer-DP-xNaYX.mjs → renderer-DRpE3ZW4.mjs} +2 -2
  212. package/dist/{renderer-DP-xNaYX.mjs.map → renderer-DRpE3ZW4.mjs.map} +1 -1
  213. package/dist/renderer-DVsF2FDr.mjs +3 -0
  214. package/dist/{renderer-VPK8JnSw.mjs → renderer-DX38J6mK.mjs} +7 -6
  215. package/dist/{renderer-VPK8JnSw.mjs.map → renderer-DX38J6mK.mjs.map} +1 -1
  216. package/dist/{renderer-CIvaRnP_.mjs → renderer-D_1oDOCP.mjs} +1 -1
  217. package/dist/{renderer-CIvaRnP_.mjs.map → renderer-D_1oDOCP.mjs.map} +1 -1
  218. package/dist/renderer-DetDm3ep.mjs +3 -0
  219. package/dist/{renderer-BENX0vRC.mjs → renderer-DgSO5OO0.mjs} +1 -1
  220. package/dist/renderer-DhMhJh_G.mjs +3 -0
  221. package/dist/renderer-DhQld4Qt.mjs +3 -0
  222. package/dist/renderer-Dn3rc3ah.mjs +3 -0
  223. package/dist/{renderer-DP948J9X.mjs → renderer-Dn8v2o9L.mjs} +18 -4
  224. package/dist/renderer-Dn8v2o9L.mjs.map +1 -0
  225. package/dist/{renderer-BQZniXw0.mjs → renderer-DnF3aXPX.mjs} +2 -2
  226. package/dist/{renderer-BQZniXw0.mjs.map → renderer-DnF3aXPX.mjs.map} +1 -1
  227. package/dist/renderer-Dw9LxE2f.mjs +3 -0
  228. package/dist/{renderer-CXZe0lfI.mjs → renderer-EmG34sT2.mjs} +2 -2
  229. package/dist/{renderer-CXZe0lfI.mjs.map → renderer-EmG34sT2.mjs.map} +1 -1
  230. package/dist/renderer-HvrQZw5E.mjs +3 -0
  231. package/dist/renderer-QdJE56ts.mjs +3 -0
  232. package/dist/{renderer-h-HOoLwD.mjs → renderer-_bygZSKk.mjs} +1 -1
  233. package/dist/{renderer-h-HOoLwD.mjs.map → renderer-_bygZSKk.mjs.map} +1 -1
  234. package/dist/{renderer-BD6Jkc8j.mjs → renderer-b6sZN90h.mjs} +4 -3
  235. package/dist/{renderer-BD6Jkc8j.mjs.map → renderer-b6sZN90h.mjs.map} +1 -1
  236. package/dist/renderer-d5-F_48R.mjs +3 -0
  237. package/dist/renderer-euGyUN2O.mjs +3 -0
  238. package/dist/{renderer-Bu0xOP_v.mjs → renderer-gT0ADXMW.mjs} +2 -2
  239. package/dist/{renderer-Bu0xOP_v.mjs.map → renderer-gT0ADXMW.mjs.map} +1 -1
  240. package/dist/{renderer-DbaSgadp.mjs → renderer-lq4n1hSS.mjs} +2 -2
  241. package/dist/{renderer-DbaSgadp.mjs.map → renderer-lq4n1hSS.mjs.map} +1 -1
  242. package/dist/renderer-p3SmBFP9.mjs +3 -0
  243. package/dist/renderer-xp_J0ln7.mjs +263 -0
  244. package/dist/renderer-xp_J0ln7.mjs.map +1 -0
  245. package/dist/{renderer-BTFPC3UJ.mjs → renderer-zvtpCnqy.mjs} +1 -1
  246. package/dist/{renderers-CP8CFWDs-BQBjaV9O.mjs → renderers-CP8CFWDs-Ciap7y_j.mjs} +2 -2
  247. package/dist/{renderers-CP8CFWDs-BQBjaV9O.mjs.map → renderers-CP8CFWDs-Ciap7y_j.mjs.map} +1 -1
  248. package/dist/signals/index.d.mts +5 -5
  249. package/dist/signals/index.mjs +4 -4
  250. package/dist/{signin-CZj4Oc5s.mjs → signin-Db08vl-g.mjs} +9 -9
  251. package/dist/{signin-CZj4Oc5s.mjs.map → signin-Db08vl-g.mjs.map} +1 -1
  252. package/dist/{sso-oidc-C0_31ThY.mjs → sso-oidc-CpLz_lIu.mjs} +9 -9
  253. package/dist/{sso-oidc-C0_31ThY.mjs.map → sso-oidc-CpLz_lIu.mjs.map} +1 -1
  254. package/dist/{sts-DD4ggEXC.mjs → sts-BQefGuml.mjs} +2 -2
  255. package/dist/{sts-nXw8RDJI.mjs → sts-Dgl0kH-t.mjs} +9 -9
  256. package/dist/{sts-nXw8RDJI.mjs.map → sts-Dgl0kH-t.mjs.map} +1 -1
  257. package/package.json +55 -54
  258. package/dist/dist-BQFap0wr.mjs.map +0 -1
  259. package/dist/dist-ZVlYkuR4.mjs.map +0 -1
  260. package/dist/dist-cjs-CmZEMtO6.mjs +0 -6
  261. package/dist/effects-axfL_C3a.mjs.map +0 -1
  262. package/dist/index-BJx_Qxc_.d.mts.map +0 -1
  263. package/dist/index-CZChQhXZ.d.mts.map +0 -1
  264. package/dist/index-WIPHLy5m.d.mts.map +0 -1
  265. package/dist/preview-engine/chunk-2XXICKQV.mjs +0 -82
  266. package/dist/preview-engine/chunk-66NVL4BN.mjs +0 -58
  267. package/dist/preview-engine/chunk-7MJVROKR.mjs +0 -98
  268. package/dist/preview-engine/chunk-7NVDTELW.mjs +0 -1
  269. package/dist/preview-engine/chunk-DMWSTYFF.mjs +0 -1
  270. package/dist/preview-engine/chunk-FKZ4XXPE.mjs +0 -1
  271. package/dist/preview-engine/chunk-JQ4WY6BZ.mjs +0 -186
  272. package/dist/preview-engine/chunk-S3P3MJYU.mjs +0 -1
  273. package/dist/preview-engine/renderers-MZBR3MSR.mjs +0 -248
  274. package/dist/renderer-1mNREW16.mjs +0 -3
  275. package/dist/renderer-4tKI3p9c.mjs +0 -3
  276. package/dist/renderer-B3XeKN-e.mjs +0 -3
  277. package/dist/renderer-B7_6SHd8.mjs +0 -3
  278. package/dist/renderer-BDLnExoP.mjs.map +0 -1
  279. package/dist/renderer-BENF6Wts.mjs +0 -3
  280. package/dist/renderer-BMovYRoR.mjs +0 -3
  281. package/dist/renderer-BPiLdwI1.mjs +0 -3
  282. package/dist/renderer-BdyMrzUX.mjs +0 -3
  283. package/dist/renderer-BfpdekRn.mjs +0 -3
  284. package/dist/renderer-BfvBHHNr.mjs +0 -3
  285. package/dist/renderer-BtdnLjKZ.mjs +0 -3
  286. package/dist/renderer-C0q5UUY7.mjs +0 -3
  287. package/dist/renderer-CXJxK7HP.mjs.map +0 -1
  288. package/dist/renderer-CboM4tba.mjs.map +0 -1
  289. package/dist/renderer-CceiI6P7.mjs.map +0 -1
  290. package/dist/renderer-Cg-rJgwM.mjs +0 -3
  291. package/dist/renderer-CkgQArsi.mjs +0 -3
  292. package/dist/renderer-CmIySXSN.mjs.map +0 -1
  293. package/dist/renderer-CnO9KcmP.mjs +0 -3
  294. package/dist/renderer-Cnp5JCwi.mjs +0 -3
  295. package/dist/renderer-CwiCQzjW.mjs +0 -4
  296. package/dist/renderer-D2LCMZ0l.mjs +0 -3
  297. package/dist/renderer-D6bWQtr-.mjs +0 -3
  298. package/dist/renderer-DBGy4qIh.mjs +0 -3
  299. package/dist/renderer-DBRnvHBV.mjs +0 -3
  300. package/dist/renderer-DDd3cjaz.mjs.map +0 -1
  301. package/dist/renderer-DJgrcu3v.mjs +0 -3
  302. package/dist/renderer-DP948J9X.mjs.map +0 -1
  303. package/dist/renderer-DTpBOlDA.mjs +0 -3
  304. package/dist/renderer-DWwIX4Mr.mjs +0 -3
  305. package/dist/renderer-DXyaswab.mjs.map +0 -1
  306. package/dist/renderer-DfnacE0H.mjs +0 -3
  307. package/dist/renderer-RA8pee2T.mjs +0 -3
  308. package/dist/renderer-eH4u4-2c.mjs +0 -3
  309. package/dist/renderer-eWOqFUz9.mjs +0 -3
  310. package/dist/renderer-o4wOvjEC.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"preview-BUPB38Kk.mjs","names":["url","resolve"],"sources":["../src/preview/bundle.ts","../src/preview/notes.ts","../src/preview/page.ts","../src/preview/index.ts"],"sourcesContent":["/**\n * The preview page runs the project's composition code and renders it with\n * WebGPU. Two bundles make that work:\n *\n * - The engine (`buildEngine`): framefields and its browser player, built once\n * for the browser. Published packages ship it in `dist/preview-engine`;\n * running from source, it is built on first use.\n * - The project (`bundleProject`): only the user's own files, built on each\n * preview. Its `framefields` imports point at the engine's files, so the\n * composition and the player share one engine (signals, clock, caches).\n *\n * Node APIs that compositions use at build time are shimmed: `path`, `url`,\n * `os`, `import.meta.url` (each file keeps its own `file://` URL), and `fs`\n * reads, which fetch the file from the preview server. Absolute file paths\n * double as URLs there, so assets referenced by path load unchanged.\n * Node-only packages become stubs that throw when used.\n */\nimport fs from \"node:fs\";\nimport { createRequire, isBuiltin } from \"node:module\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\nimport type { BuildOptions, PluginBuild } from \"esbuild\";\n\nconst HERE = path.dirname(fileURLToPath(import.meta.url));\n// Spelled in two parts so this file survives its own rewrite.\nconst META_URL = [\"import\", \"meta\", \"url\"].join(\".\");\nconst require = createRequire(import.meta.url);\n\n/** URL prefix the server serves the engine's files under. */\nexport const ENGINE_URL = \"/@framefields/engine\";\n\n/** Public entry points a composition may import, and their sources. */\nconst ENTRIES: Record<string, string> = {\n\tframefields: \"index.ts\",\n\t\"framefields/effects\": \"effects/index.ts\",\n\t\"framefields/audio\": \"audio/index.ts\",\n\t\"framefields/signals\": \"signals/index.ts\",\n\t\"framefields/fonts\": \"fonts/index.ts\",\n};\n\n/** Node built-ins with a browser version (shim file names); others are stubbed. */\nconst SHIMS: Record<string, string> = {\n\tpath: \"path\",\n\t\"path/posix\": \"path\",\n\turl: \"url\",\n\tos: \"os\",\n\tfs: \"fs\",\n\t\"fs/promises\": \"fs-promises\",\n\tmodule: \"module\",\n};\n\n/** Packages that only work in Node; the browser never needs them to draw. */\nconst NODE_ONLY = new Set([\n\t\"sharp\",\n\t\"skia-canvas\",\n\t\"node-av\",\n\t\"onnxruntime-node\",\n\t\"@resvg/resvg-js\",\n\t\"@napi-rs/webcodecs\",\n\t\"@mediabunny/server\",\n\t\"@mediabunny/mp3-encoder\",\n\t\"webgpu\",\n\t\"pino\",\n\t\"pino-pretty\",\n\t\"dotenv\",\n\t\"esbuild\",\n\t\"@framefields/renderer\",\n\t\"@framefields/server-utils\",\n\t\"framefields/renderer\",\n\t\"framefields/preview\",\n]);\n\n// Reported as an ES module so named imports (`import { homedir } from \"os\"`)\n// read through the proxy instead of being copied off it.\nconst STUB = `\nconst stub = () => new Proxy(function () {}, {\n\tget: (_t, k) => (k === \"__esModule\" ? true : k === Symbol.toPrimitive ? () => \"\" : stub()),\n\tapply: () => stub(),\n\tconstruct: () => stub(),\n});\nmodule.exports = stub();\n`;\n\n/**\n * Builds framefields for the browser into `outDir`: one module per public entry,\n * the player and the shims, with shared code split into chunks (node renderers\n * load only when a composition uses them).\n */\nexport async function buildEngine(outDir: string): Promise<void> {\n\tconst { build } = await import(\"esbuild\");\n\tconst src = path.resolve(HERE, \"..\");\n\tconst shims = path.join(HERE, \"shims\");\n\tconst entryPoints: Record<string, string> = {\n\t\tplayer: path.join(HERE, \"player.ts\"),\n\t};\n\tfor (const [spec, file] of Object.entries(ENTRIES)) {\n\t\tentryPoints[engineName(spec)] = path.join(src, file);\n\t}\n\tfor (const name of new Set(Object.values(SHIMS))) {\n\t\tentryPoints[`shims/${name}`] = path.join(shims, `${name}.ts`);\n\t}\n\n\tawait fs.promises.rm(outDir, { recursive: true, force: true });\n\tawait build({\n\t\t...BROWSER,\n\t\tentryPoints,\n\t\toutdir: outDir,\n\t\toutExtension: { \".js\": \".mjs\" },\n\t\tsplitting: true,\n\t\tminify: true,\n\t\tconditions: [\"development\", \"browser\", \"import\"],\n\t\tdefine: { \"process.env.NODE_ENV\": '\"production\"', global: \"globalThis\" },\n\t\tinject: [path.join(shims, \"process.ts\")],\n\t\tplugins: [\n\t\t\t{\n\t\t\t\tname: \"framefields-engine\",\n\t\t\t\tsetup(b) {\n\t\t\t\t\tb.onResolve({ filter: /^framefields-preview:renderers$/ }, () => ({\n\t\t\t\t\t\tpath: builtinRenderersModule(),\n\t\t\t\t\t}));\n\t\t\t\t\tresolveNode(b, (name) => ({ path: path.join(shims, `${name}.ts`) }));\n\t\t\t\t},\n\t\t\t},\n\t\t],\n\t});\n}\n\nexport interface ProjectBundle {\n\tcode: string;\n\t/** The project's source files, to watch for changes. */\n\tinputs: string[];\n}\n\n/** Bundles the module that builds the composition, and what it imports. */\nexport async function bundleProject(options: {\n\t/** Module that exports the composition factory. */\n\tentry: string;\n\t/** Name of the export; a function returning a Composition (or one itself). */\n\texportName: string;\n\t/** Where cached vision models are, and where onnxruntime-web loads from. */\n\tmodelsDir: string;\n\tortBase: string;\n}): Promise<ProjectBundle> {\n\tconst { build } = await import(\"esbuild\");\n\tconst entry = path.resolve(options.entry);\n\tconst fromEngine = (file: string) => ({\n\t\tpath: `${ENGINE_URL}/${file}.mjs`,\n\t\texternal: true,\n\t});\n\tconst settings = { modelsDir: options.modelsDir, ortBase: options.ortBase };\n\n\tconst result = await build({\n\t\t...BROWSER,\n\t\tentryPoints: [\"framefields-preview:entry\"],\n\t\twrite: false,\n\t\tmetafile: true,\n\t\tsourcemap: \"inline\",\n\t\tplugins: [\n\t\t\t{\n\t\t\t\tname: \"framefields-project\",\n\t\t\t\tsetup(b) {\n\t\t\t\t\tb.onResolve({ filter: /^framefields-preview:entry$/ }, () => ({\n\t\t\t\t\t\tpath: \"entry\",\n\t\t\t\t\t\tnamespace: \"framefields-preview\",\n\t\t\t\t\t}));\n\t\t\t\t\tb.onLoad({ filter: /.*/, namespace: \"framefields-preview\" }, () => ({\n\t\t\t\t\t\tcontents: [\n\t\t\t\t\t\t\t`import { startPlayer } from ${JSON.stringify(`${ENGINE_URL}/player.mjs`)};`,\n\t\t\t\t\t\t\t`import * as mod from ${JSON.stringify(entry)};`,\n\t\t\t\t\t\t\t`startPlayer(mod, ${JSON.stringify(options.exportName)}, ${JSON.stringify(settings)});`,\n\t\t\t\t\t\t].join(\"\\n\"),\n\t\t\t\t\t\tresolveDir: path.dirname(entry),\n\t\t\t\t\t\tloader: \"js\",\n\t\t\t\t\t}));\n\t\t\t\t\tb.onResolve({ filter: /^\\/@framefields\\/engine\\// }, (args) => ({\n\t\t\t\t\t\tpath: args.path,\n\t\t\t\t\t\texternal: true,\n\t\t\t\t\t}));\n\t\t\t\t\tb.onResolve({ filter: /^framefields(\\/|$)/ }, (args) =>\n\t\t\t\t\t\tENTRIES[args.path] ? fromEngine(engineName(args.path)) : undefined,\n\t\t\t\t\t);\n\t\t\t\t\tresolveNode(b, (name) => fromEngine(`shims/${name}`));\n\t\t\t\t\trewriteMetaUrl(b);\n\t\t\t\t},\n\t\t\t},\n\t\t],\n\t});\n\n\tconst inputs = Object.keys(result.metafile?.inputs ?? {})\n\t\t.filter((p) => !p.includes(\":\") && !p.includes(\"node_modules\"))\n\t\t.map((p) => path.resolve(p));\n\treturn { code: result.outputFiles[0].text, inputs };\n}\n\nlet engineBuild: Promise<string> | undefined;\n\n/**\n * The prebuilt engine shipped beside a compiled bundle, if there is one. The\n * bundle lands either at the package's dist root or beside dist/preview,\n * depending on how the bundler splits it, so both are checked.\n */\nexport function prebuiltEngineDir(here: string): string | undefined {\n\treturn [here, path.join(here, \"..\")]\n\t\t.map((base) => path.join(base, \"preview-engine\"))\n\t\t.find((dir) => fs.existsSync(path.join(dir, \"player.mjs\")));\n}\n\n/**\n * The engine to serve: prebuilt in the package's dist, or, running from\n * source, built once per process (so engine edits show without a rebuild).\n */\nexport function engineDir(): Promise<string> {\n\t// From source HERE is src/preview, so this misses and the engine is built.\n\tconst prebuilt = prebuiltEngineDir(HERE);\n\tif (prebuilt) {\n\t\treturn Promise.resolve(prebuilt);\n\t}\n\tengineBuild ??= (async () => {\n\t\tconst dir = path.join(\n\t\t\tos.tmpdir(),\n\t\t\t`framefields-preview-engine-${process.pid}`,\n\t\t);\n\t\tawait buildEngine(dir);\n\t\treturn dir;\n\t})();\n\tengineBuild.catch(() => {\n\t\tengineBuild = undefined;\n\t});\n\treturn engineBuild;\n}\n\n/** Where vision models are cached, as `@framefields/vision` resolves it. */\nexport function visionModelsDir(): string {\n\treturn process.env.FRAMEFIELDS_MODELS_DIR\n\t\t? path.resolve(process.env.FRAMEFIELDS_MODELS_DIR)\n\t\t: path.join(os.homedir(), \".cache\", \"framefields\", \"models\");\n}\n\nconst ORT_WEB_VERSION = \"1.30.0\";\n\n/**\n * onnxruntime-web, for vision effects: the project's own install when it has\n * one (served from disk), otherwise a pinned CDN build. Loaded only when a\n * vision effect runs, so framefields doesn't ship its WebAssembly.\n */\nexport function onnxRuntimeWeb(fromDir: string): {\n\tbase: string;\n\tdir?: string;\n} {\n\tfor (const from of [fromDir, HERE]) {\n\t\ttry {\n\t\t\tconst main = createRequire(path.join(from, \"noop.js\")).resolve(\n\t\t\t\t\"onnxruntime-web\",\n\t\t\t);\n\t\t\tconst dir = path.dirname(main);\n\t\t\treturn { base: `/@fs${dir}/`, dir };\n\t\t} catch {}\n\t}\n\treturn {\n\t\tbase: `https://cdn.jsdelivr.net/npm/onnxruntime-web@${ORT_WEB_VERSION}/dist/`,\n\t};\n}\n\n// ── shared ──\n\nconst BROWSER: BuildOptions = {\n\tbundle: true,\n\tformat: \"esm\",\n\tplatform: \"browser\",\n\ttarget: \"es2022\",\n\tmainFields: [\"browser\", \"module\", \"main\"],\n\tlogLevel: \"silent\",\n};\n\nfunction engineName(spec: string): string {\n\treturn spec === \"framefields\"\n\t\t? \"framefields\"\n\t\t: spec.slice(\"framefields/\".length);\n}\n\n/** Node built-ins to shims, the rest of Node to stubs. */\nfunction resolveNode(\n\tb: PluginBuild,\n\tshim: (name: string) => { path: string; external?: boolean },\n): void {\n\tb.onResolve({ filter: /^(node:)?[a-z_/]+$/ }, (args) => {\n\t\tconst name = args.path.replace(/^node:/, \"\");\n\t\tif (SHIMS[name]) return shim(SHIMS[name]);\n\t\tif (args.path.startsWith(\"node:\") || isBuiltin(name))\n\t\t\treturn { path: name, namespace: \"framefields-stub\" };\n\t\treturn undefined;\n\t});\n\tb.onResolve({ filter: /.*/ }, (args) => {\n\t\tconst pkg = packageName(args.path);\n\t\tif (pkg && (NODE_ONLY.has(pkg) || NODE_ONLY.has(args.path)))\n\t\t\treturn { path: args.path, namespace: \"framefields-stub\" };\n\t\treturn undefined;\n\t});\n\tb.onLoad({ filter: /.*/, namespace: \"framefields-stub\" }, () => ({\n\t\tcontents: STUB,\n\t\tloader: \"js\",\n\t}));\n}\n\n/** Each project file keeps its own URL, as `fileURLToPath(import.meta.url)` expects. */\nfunction rewriteMetaUrl(b: PluginBuild): void {\n\tb.onLoad({ filter: /\\.[cm]?[jt]sx?$/ }, async (args) => {\n\t\tif (args.path.includes(`${path.sep}node_modules${path.sep}`))\n\t\t\treturn undefined;\n\t\tconst source = await fs.promises.readFile(args.path, \"utf8\");\n\t\tif (!source.includes(META_URL)) return undefined;\n\t\treturn {\n\t\t\tcontents: `const __framefieldsFileUrl = ${JSON.stringify(pathToFileURL(args.path).href)};\\n${source.replaceAll(META_URL, \"__framefieldsFileUrl\")}`,\n\t\t\tloader: loaderFor(args.path),\n\t\t};\n\t});\n}\n\n/** The generated list of node renderers (needed only to build the engine). */\nfunction builtinRenderersModule(): string {\n\tconst dist = require.resolve(\"@framefields/renderer\");\n\tconst file = path.join(\n\t\tpath.dirname(dist),\n\t\t\"..\",\n\t\t\"src\",\n\t\t\"generated\",\n\t\t\"node-renderers.ts\",\n\t);\n\tif (!fs.existsSync(file))\n\t\tthrow new Error(`[framefields] node renderer list not found at ${file}`);\n\treturn file;\n}\n\nfunction packageName(spec: string): string | undefined {\n\tif (spec.startsWith(\".\") || spec.startsWith(\"/\")) return undefined;\n\tconst parts = spec.split(\"/\");\n\treturn spec.startsWith(\"@\") ? parts.slice(0, 2).join(\"/\") : parts[0];\n}\n\nfunction loaderFor(file: string): \"ts\" | \"tsx\" | \"js\" | \"jsx\" {\n\tif (file.endsWith(\".tsx\")) return \"tsx\";\n\tif (file.endsWith(\".jsx\")) return \"jsx\";\n\treturn /\\.[cm]?ts$/.test(file) ? \"ts\" : \"js\";\n}\n","/**\n * Notes the user pins on the preview: a moment, a spot (or an area) on the\n * frame, and what should change there. They are kept as files so whoever\n * edits the film (a person, or an agent) can read them: `notes.json`, plus the\n * frame with the spot marked as `note-<id>.jpg`. The folder ignores itself in\n * git. `notes.json` is read on every change, so it can be edited by hand\n * (marking a note done, say) while the preview runs.\n */\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nexport interface PreviewNote {\n\tid: number;\n\tframe: number;\n\t/** Seconds into the film. */\n\ttime: number;\n\t/** The spot, as fractions of the frame's width and height (0..1). */\n\tx: number;\n\ty: number;\n\t/** Set when an area was marked: its size, from the spot at its top left. */\n\tw?: number;\n\th?: number;\n\ttext: string;\n\t/** The frame with the spot marked (absolute path), when it was captured. */\n\timage?: string;\n\tdone: boolean;\n\tcreated: string;\n}\n\nexport interface NewNote {\n\tframe: number;\n\ttime: number;\n\tx: number;\n\ty: number;\n\tw?: number;\n\th?: number;\n\ttext: string;\n\t/** JPEG as a base64 data URL. */\n\timage?: string;\n}\n\nexport class NoteStore {\n\treadonly file: string;\n\n\tconstructor(readonly dir: string) {\n\t\tthis.file = path.join(dir, \"notes.json\");\n\t}\n\n\tasync list(): Promise<PreviewNote[]> {\n\t\ttry {\n\t\t\tconst notes = JSON.parse(await fs.promises.readFile(this.file, \"utf8\"));\n\t\t\treturn Array.isArray(notes) ? notes : [];\n\t\t} catch {\n\t\t\treturn [];\n\t\t}\n\t}\n\n\tasync add(input: NewNote): Promise<PreviewNote> {\n\t\tconst notes = await this.list();\n\t\tconst id = notes.reduce((max, n) => Math.max(max, n.id), 0) + 1;\n\t\tconst note: PreviewNote = {\n\t\t\tid,\n\t\t\tframe: Math.max(0, Math.round(Number(input.frame) || 0)),\n\t\t\ttime: round(Number(input.time) || 0, 3),\n\t\t\tx: round(clamp01(input.x), 4),\n\t\t\ty: round(clamp01(input.y), 4),\n\t\t\t...(input.w !== undefined &&\n\t\t\t\tinput.h !== undefined && {\n\t\t\t\t\tw: round(clamp01(input.w), 4),\n\t\t\t\t\th: round(clamp01(input.h), 4),\n\t\t\t\t}),\n\t\t\ttext: String(input.text ?? \"\").trim(),\n\t\t\tdone: false,\n\t\t\tcreated: new Date().toISOString(),\n\t\t};\n\t\tawait this.ensureDir();\n\t\tconst jpeg = /^data:image\\/jpeg;base64,(.+)$/.exec(input.image ?? \"\");\n\t\tif (jpeg) {\n\t\t\tnote.image = path.join(this.dir, `note-${id}.jpg`);\n\t\t\tawait fs.promises.writeFile(note.image, Buffer.from(jpeg[1], \"base64\"));\n\t\t}\n\t\tawait this.save([...notes, note]);\n\t\treturn note;\n\t}\n\n\tasync update(\n\t\tid: number,\n\t\tpatch: { done?: boolean; text?: string },\n\t): Promise<PreviewNote | undefined> {\n\t\tconst notes = await this.list();\n\t\tconst note = notes.find((n) => n.id === id);\n\t\tif (!note) return undefined;\n\t\tif (typeof patch.done === \"boolean\") note.done = patch.done;\n\t\tif (typeof patch.text === \"string\" && patch.text.trim())\n\t\t\tnote.text = patch.text.trim();\n\t\tawait this.save(notes);\n\t\treturn note;\n\t}\n\n\tasync remove(id: number): Promise<PreviewNote | undefined> {\n\t\tconst notes = await this.list();\n\t\tconst note = notes.find((n) => n.id === id);\n\t\tif (!note) return undefined;\n\t\tif (note.image) await fs.promises.rm(note.image, { force: true });\n\t\tawait this.save(notes.filter((n) => n !== note));\n\t\treturn note;\n\t}\n\n\tprivate async save(notes: PreviewNote[]): Promise<void> {\n\t\tawait this.ensureDir();\n\t\tconst tmp = `${this.file}.${process.pid}.tmp`;\n\t\tawait fs.promises.writeFile(tmp, `${JSON.stringify(notes, null, \"\\t\")}\\n`);\n\t\tawait fs.promises.rename(tmp, this.file);\n\t}\n\n\tprivate async ensureDir(): Promise<void> {\n\t\tawait fs.promises.mkdir(this.dir, { recursive: true });\n\t\t// Notes are working material, not part of the project.\n\t\tconst ignore = path.join(path.dirname(this.dir), \".gitignore\");\n\t\tif (!fs.existsSync(ignore)) await fs.promises.writeFile(ignore, \"*\\n\");\n\t}\n}\n\n/** One line for a terminal (or an agent reading one). */\nexport function describeNote(note: PreviewNote, fps: number): string {\n\tconst pct = (v: number) => `${Math.round(v * 100)}%`;\n\tconst where =\n\t\tnote.w !== undefined && note.h !== undefined\n\t\t\t? `area ${pct(note.x)},${pct(note.y)} to ${pct(note.x + note.w)},${pct(note.y + note.h)}`\n\t\t\t: `spot ${pct(note.x)},${pct(note.y)}`;\n\treturn `note ${note.id} at ${timecode(note.frame, fps)} (frame ${note.frame}), ${where}: ${note.text}`;\n}\n\nfunction timecode(frame: number, fps: number): string {\n\tconst s = frame / fps;\n\tconst m = Math.floor(s / 60);\n\treturn `${m}:${(s - m * 60).toFixed(2).padStart(5, \"0\")}`;\n}\n\nfunction clamp01(v: unknown): number {\n\tconst n = Number(v);\n\treturn Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : 0;\n}\n\nfunction round(v: number, digits: number): number {\n\treturn Number(v.toFixed(digits));\n}\n","/**\n * Single-file preview UI served by `startPreview`. No framework, no build step.\n * The page script avoids template literals so it can live inside this one.\n */\nexport const previewPage = /* html */ `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>framefields preview</title>\n<link rel=\"icon\" href=\"data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 512 512%22%3E%3Cg fill=%22none%22 stroke=%22%233B82F6%22 stroke-linecap=%22round%22%3E%3Cpath d=%22M184 56H86q-30 0-30 30v98M328 56h98q30 0 30 30v98M184 456H86q-30 0-30-30v-98M328 456h98q30 0 30-30v-98%22 stroke-width=%2234%22/%3E%3Cpath d=%22M176 182v148m0-148c0 74 60 74 100 74%22 stroke-width=%2228%22/%3E%3C/g%3E%3Cg fill=%22%233B82F6%22%3E%3Ccircle cx=%22176%22 cy=%22172%22 r=%2232%22/%3E%3Ccircle cx=%22176%22 cy=%22340%22 r=%2232%22/%3E%3Cpath d=%22M284 200l78 56-78 56z%22 stroke=%22%233B82F6%22 stroke-width=%2234%22 stroke-linejoin=%22round%22/%3E%3C/g%3E%3C/svg%3E\">\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap\" rel=\"stylesheet\">\n<style>\n :root {\n --bg: #0e0f11; --surface: #16171a; --surface-2: #1d1e22; --surface-3: #26282d;\n --line: #2a2c31; --line-2: #3a3c43;\n --ink: #ededf0; --ink-2: #a6a8b0; --ink-3: #74777f;\n --stage: #060607;\n --primary-bg: #ededf0; --primary-ink: #0e0f11;\n --brand: #3b82f6; --focus: #5eaefc; --ok: #4fbf7a; --bad: #f27d8d;\n --wave: #4b4e57;\n --radius: 10px;\n --font: \"Inter\", ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", sans-serif;\n color-scheme: dark;\n }\n @media (prefers-color-scheme: light) {\n :root {\n --bg: #f5f5f6; --surface: #ffffff; --surface-2: #f0f0f2; --surface-3: #e6e6e9;\n --line: #e2e2e6; --line-2: #cfcfd5;\n --ink: #15161a; --ink-2: #53555d; --ink-3: #85878f;\n --stage: #0b0b0c;\n --primary-bg: #15161a; --primary-ink: #ffffff;\n --brand: #2563eb; --focus: #1c74cc; --ok: #23874a; --bad: #cf4057;\n --wave: #c2c3ca;\n color-scheme: light;\n }\n }\n * { box-sizing: border-box; }\n html, body { height: 100%; margin: 0; overflow: hidden; background: var(--bg); color: var(--ink); }\n body {\n display: flex; flex-direction: column; height: 100dvh;\n font: 13.5px/1.45 var(--font); -webkit-font-smoothing: antialiased;\n font-feature-settings: \"cv11\", \"ss03\";\n }\n button, input { font: inherit; color: inherit; }\n button { cursor: pointer; }\n button:focus-visible, input:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }\n .tnum { font-variant-numeric: tabular-nums; }\n .grow { flex: 1; }\n .spinner {\n width: 12px; height: 12px; border-radius: 50%; flex: none;\n border: 1.5px solid var(--line-2); border-top-color: var(--ink); animation: spin .8s linear infinite;\n }\n @keyframes spin { to { transform: rotate(360deg); } }\n .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }\n\n /* ── Top bar ── */\n .top { flex: none; display: flex; align-items: center; gap: 10px; padding: 0 20px; height: 50px; border-bottom: 1px solid var(--line); min-width: 0; }\n .logo { width: 22px; height: 22px; flex: none; color: var(--brand); }\n .sep { width: 1px; height: 18px; background: var(--line-2); flex: none; margin: 0 2px; }\n .top h1 { font-size: 14px; font-weight: 600; margin: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n .spec { color: var(--ink-3); font-size: 12px; white-space: nowrap; }\n @media (max-width: 640px) { .spec { display: none; } }\n .chip {\n display: inline-flex; align-items: center; gap: 8px; height: 26px; padding: 0 10px; white-space: nowrap;\n border: 1px solid var(--line); border-radius: 999px; color: var(--ink-2); font-size: 12px; background: var(--surface);\n }\n .chip[data-state=\"ready\"] .dot { color: var(--ok); }\n .chip[data-state=\"none\"] .dot, .chip[data-state=\"offline\"] .dot { color: var(--ink-3); }\n .chip[data-state=\"error\"] { color: var(--bad); }\n\n /* ── Player: one screen, no page scroll; the picture takes the space left ── */\n .player {\n flex: 1; min-height: 0; display: flex; flex-direction: column; gap: 10px;\n width: 100%; max-width: 1600px; margin: 0 auto; padding: 16px 20px 20px;\n }\n @media (max-width: 640px) { .player { padding: 12px; } }\n .stage-wrap { flex: 1 1 0; min-height: 96px; container-type: size; display: grid; place-items: center; }\n .stage {\n position: relative; background: var(--stage); border-radius: var(--radius); overflow: hidden;\n box-shadow: 0 0 0 1px var(--line); user-select: none; -webkit-user-select: none;\n aspect-ratio: var(--ar, 16 / 9); width: min(100cqw, calc(100cqh * var(--ar-n, 1.7778)));\n }\n .stage canvas { display: block; width: 100%; height: 100%; }\n .stage-msg {\n position: absolute; inset: 0; display: none; place-items: center; padding: 24px; text-align: center;\n color: #fff; background: rgb(0 0 0 / .72); font-size: 13px; line-height: 1.5; white-space: pre-wrap; overflow: auto;\n }\n .stage-msg.on { display: grid; }\n /* Buffering: a centred spinning arc over the picture, like YouTube's. It\n fades in after a beat so brief waits don't flash it. */\n .buffering {\n position: absolute; left: 50%; top: 50%; width: clamp(36px, 9cqmin, 64px); aspect-ratio: 1;\n transform: translate(-50%, -50%); pointer-events: none; opacity: 0; visibility: hidden;\n transition: opacity .15s, visibility 0s .15s; filter: drop-shadow(0 0 3px rgb(0 0 0 / .45));\n }\n .buffering.on { opacity: 1; visibility: visible; transition: opacity .2s .25s, visibility 0s .25s; }\n .buffering svg { width: 100%; height: 100%; animation: yt-rotate 1.4s linear infinite; }\n .buffering circle {\n fill: none; stroke: #fff; stroke-width: 3.6; stroke-linecap: round;\n stroke-dasharray: 1 150; stroke-dashoffset: 0; animation: yt-dash 1.4s ease-in-out infinite;\n }\n @keyframes yt-rotate { to { transform: rotate(360deg); } }\n @keyframes yt-dash {\n 0% { stroke-dasharray: 1 150; stroke-dashoffset: 0; }\n 50% { stroke-dasharray: 90 150; stroke-dashoffset: -35; }\n 100% { stroke-dasharray: 90 150; stroke-dashoffset: -124; }\n }\n @media (prefers-reduced-motion: reduce) { .buffering circle, .play-btn .spin circle { animation: none; stroke-dasharray: 70 150; } }\n\n /* ── Transport ── */\n .transport { flex: none; display: flex; align-items: center; gap: 4px; flex-wrap: wrap; row-gap: 6px; }\n .icon-btn {\n width: 32px; height: 32px; display: inline-grid; place-items: center; border-radius: 8px;\n border: 0; background: transparent; color: var(--ink-2); flex: none;\n }\n .icon-btn:hover:not(:disabled) { background: var(--surface-2); color: var(--ink); }\n .icon-btn:disabled { opacity: .35; cursor: default; }\n .icon-btn svg { width: 16px; height: 16px; }\n .play-btn {\n width: 36px; height: 36px; border-radius: 50%; border: 0; display: inline-grid; place-items: center; flex: none;\n background: var(--primary-bg); color: var(--primary-ink); margin: 0 2px;\n }\n .play-btn:hover:not(:disabled) { filter: brightness(.9); }\n .play-btn:disabled { opacity: .4; cursor: default; }\n .play-btn svg { width: 14px; height: 14px; }\n .play-btn .spin { display: none; width: 20px; height: 20px; animation: yt-rotate 1.4s linear infinite; }\n .play-btn .spin circle {\n fill: none; stroke: currentColor; stroke-width: 5; stroke-linecap: round;\n stroke-dasharray: 1 150; animation: yt-dash 1.4s ease-in-out infinite;\n }\n .play-btn.loading .spin { display: block; }\n .play-btn.loading svg:not(.spin) { display: none !important; }\n .timecode { display: flex; align-items: baseline; gap: 5px; margin-left: 8px; font-size: 13px; white-space: nowrap; }\n .timecode .dim { color: var(--ink-3); }\n .timecode .frame { color: var(--ink-3); font-size: 11.5px; margin-left: 6px; }\n .fps { margin-left: 10px; color: var(--ink-3); font-size: 11.5px; white-space: nowrap; }\n input[type=\"range\"].slider { -webkit-appearance: none; appearance: none; width: 72px; height: 4px; border-radius: 2px; background: var(--line-2); margin: 0 8px 0 0; }\n input[type=\"range\"].slider::-webkit-slider-thumb { -webkit-appearance: none; width: 12px; height: 12px; border-radius: 50%; background: var(--ink); border: 0; }\n input[type=\"range\"].slider::-moz-range-thumb { width: 12px; height: 12px; border-radius: 50%; background: var(--ink); border: 0; }\n input[type=\"range\"].slider:disabled { opacity: .35; }\n\n /* ── Timeline ── */\n .timeline {\n flex: none; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface);\n padding: 0 14px; user-select: none; -webkit-user-select: none; touch-action: none; cursor: pointer;\n }\n .tl-inner { position: relative; }\n .ruler { position: relative; height: 22px; border-bottom: 1px solid var(--line); }\n .tick { position: absolute; bottom: 0; width: 1px; height: 4px; background: var(--line-2); }\n .tick.major { height: 8px; background: var(--ink-3); }\n .tick span { position: absolute; bottom: 9px; left: 4px; font-size: 10.5px; line-height: 1; color: var(--ink-3); white-space: nowrap; font-variant-numeric: tabular-nums; }\n .wave-lane { position: relative; height: 48px; }\n @media (max-height: 600px) { .wave-lane { height: 30px; } }\n .wave-lane canvas { position: absolute; inset: 0; width: 100%; height: 100%; }\n .wave-state { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: 8px; color: var(--ink-3); font-size: 12px; }\n .wave-state.shimmer {\n background: linear-gradient(90deg, transparent 0%, color-mix(in srgb, var(--ink) 6%, transparent) 50%, transparent 100%);\n background-size: 200% 100%; animation: shimmer 1.4s linear infinite;\n }\n @keyframes shimmer { from { background-position: 100% 0; } to { background-position: -100% 0; } }\n .playhead { position: absolute; top: 0; bottom: 0; width: 0; pointer-events: none; z-index: 3; }\n .playhead::before { content: \"\"; position: absolute; top: 0; bottom: 0; left: -0.5px; width: 1px; background: var(--ink); }\n .playhead::after { content: \"\"; position: absolute; top: 0; left: -5px; width: 10px; height: 11px; background: var(--ink); clip-path: polygon(0 0, 100% 0, 100% 55%, 50% 100%, 0 55%); }\n .hover-line { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--line-2); display: none; pointer-events: none; }\n .hover-time {\n position: absolute; top: -28px; transform: translateX(-50%); display: none; pointer-events: none;\n font-size: 11px; line-height: 1; background: var(--surface-3); color: var(--ink); padding: 5px 7px; border-radius: 5px;\n white-space: nowrap; z-index: 4; font-variant-numeric: tabular-nums;\n }\n /* ── Notes: pinned to a spot (or an area) and a moment ── */\n .stage.can-note { cursor: crosshair; }\n .overlay { position: absolute; inset: 0; pointer-events: none; }\n .pin {\n position: absolute; width: 22px; height: 22px; margin: -11px 0 0 -11px; padding: 0; border-radius: 50%;\n display: grid; place-items: center; pointer-events: auto; cursor: pointer;\n background: var(--brand); color: #fff; font-size: 11px; font-weight: 600; line-height: 1;\n border: 2px solid #fff; box-shadow: 0 1px 5px rgb(0 0 0 / .55); transition: transform .12s;\n }\n .pin:hover, .pin.active { transform: scale(1.18); }\n .pin.done { background: #6b6e76; }\n .pin.draft { animation: pin-pulse 1.5s ease-out infinite; }\n .pin svg { width: 11px; height: 11px; }\n @keyframes pin-pulse {\n from { box-shadow: 0 0 0 0 rgb(59 130 246 / .6), 0 1px 5px rgb(0 0 0 / .55); }\n to { box-shadow: 0 0 0 14px rgb(59 130 246 / 0), 0 1px 5px rgb(0 0 0 / .55); }\n }\n .area {\n position: absolute; border: 1.5px solid var(--brand); border-radius: 3px;\n background: rgb(59 130 246 / .12); box-shadow: 0 0 0 1px rgb(0 0 0 / .35);\n }\n .area.done { border-color: #9a9da5; background: rgb(255 255 255 / .05); }\n .marquee { position: absolute; display: none; border: 1.5px dashed #fff; background: rgb(59 130 246 / .16); border-radius: 3px; pointer-events: none; }\n .stage-hint, .toast {\n position: absolute; left: 50%; transform: translateX(-50%); pointer-events: none; white-space: nowrap;\n padding: 6px 11px; border-radius: 999px; font-size: 12px; color: #fff; background: rgb(0 0 0 / .62);\n backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); opacity: 0; transition: opacity .15s;\n }\n .stage-hint { top: 12px; }\n .stage.can-note.hint:hover .stage-hint { opacity: 1; }\n .toast { bottom: 14px; }\n .toast.on { opacity: 1; }\n\n .pop {\n position: fixed; z-index: 20; width: 280px; display: none; padding: 10px 10px 8px;\n background: var(--surface); border: 1px solid var(--line-2); border-radius: 12px;\n box-shadow: 0 12px 32px rgb(0 0 0 / .35);\n }\n .pop.on { display: block; }\n .pop-head { display: flex; align-items: center; gap: 8px; font-size: 11.5px; color: var(--ink-3); margin-bottom: 6px; }\n .badge {\n min-width: 18px; height: 18px; padding: 0 5px; border-radius: 9px; display: inline-grid; place-items: center; flex: none;\n background: var(--brand); color: #fff; font-size: 10.5px; font-weight: 600;\n }\n .badge.done { background: var(--ink-3); }\n .pop textarea {\n display: block; width: 100%; min-height: 54px; max-height: 180px; resize: none; padding: 2px 2px 0;\n border: 0; outline: none; background: transparent; color: var(--ink); font: inherit; line-height: 1.45;\n }\n .pop textarea::placeholder { color: var(--ink-3); }\n .pop-text { white-space: pre-wrap; overflow-wrap: anywhere; padding: 0 2px; max-height: 200px; overflow: auto; }\n .pop-foot { display: flex; align-items: center; gap: 6px; margin-top: 8px; font-size: 11.5px; color: var(--ink-3); }\n .pop-foot .err { color: var(--bad); }\n .btn {\n height: 26px; padding: 0 10px; border-radius: 7px; border: 0; font-size: 12px; font-weight: 500;\n display: inline-flex; align-items: center; gap: 6px; background: transparent; color: var(--ink-2); white-space: nowrap;\n }\n .btn:hover:not(:disabled) { background: var(--surface-2); color: var(--ink); }\n .btn.primary { background: var(--primary-bg); color: var(--primary-ink); }\n .btn.primary:hover:not(:disabled) { background: var(--primary-bg); filter: brightness(.9); }\n .btn.danger:hover:not(:disabled) { color: var(--bad); }\n .btn:disabled { opacity: .4; cursor: default; }\n .btn svg { width: 13px; height: 13px; }\n .x-btn { width: 22px; height: 22px; padding: 0; justify-content: center; margin-right: -4px; }\n\n .transport { position: relative; }\n .notes-btn { height: 32px; padding: 0 9px 0 8px; margin-right: 6px; font-size: 12.5px; }\n .notes-btn svg { width: 15px; height: 15px; }\n .notes-btn .badge { background: var(--surface-3); color: var(--ink-2); }\n .notes-btn .badge.open { background: var(--brand); color: #fff; }\n .notes-panel {\n position: absolute; right: 0; bottom: calc(100% + 8px); z-index: 15; width: min(360px, calc(100vw - 24px));\n max-height: min(440px, 62vh); display: none; flex-direction: column; overflow: hidden;\n background: var(--surface); border: 1px solid var(--line-2); border-radius: 12px; box-shadow: 0 12px 32px rgb(0 0 0 / .35);\n }\n .notes-panel.on { display: flex; }\n .np-head { flex: none; display: flex; align-items: center; gap: 8px; padding: 8px 8px 8px 14px; border-bottom: 1px solid var(--line); }\n .np-head h2 { margin: 0; font-size: 13px; font-weight: 600; }\n .np-list { overflow: auto; padding: 4px; }\n .np-empty { padding: 22px 18px; text-align: center; color: var(--ink-3); font-size: 12.5px; }\n .np-foot { flex: none; padding: 8px 14px; border-top: 1px solid var(--line); color: var(--ink-3); font-size: 11.5px; overflow-wrap: anywhere; }\n .note-row {\n display: grid; grid-template-columns: auto 1fr auto; gap: 10px; align-items: start; padding: 8px 8px 8px 10px;\n border-radius: 8px; cursor: pointer;\n }\n .note-row:hover { background: var(--surface-2); }\n .note-row .badge { margin-top: 1px; }\n .note-row .when { font-size: 11.5px; color: var(--ink-3); }\n .note-row .text { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; overflow-wrap: anywhere; }\n .note-row.done .text { color: var(--ink-3); text-decoration: line-through; }\n .check {\n width: 20px; height: 20px; border-radius: 50%; padding: 0; display: grid; place-items: center; margin-top: 1px;\n border: 1.5px solid var(--line-2); background: transparent; color: transparent;\n }\n .check:hover { border-color: var(--ok); color: var(--ok); }\n .check.on { background: var(--ok); border-color: var(--ok); color: #fff; }\n .check svg { width: 11px; height: 11px; }\n\n .note-marks { position: absolute; left: 0; right: 0; top: 22px; bottom: 0; pointer-events: none; z-index: 2; }\n .note-mark {\n position: absolute; top: 5px; width: 14px; height: 14px; margin-left: -7px; padding: 0; border: 0; border-radius: 50%;\n display: grid; place-items: center; pointer-events: auto; cursor: pointer;\n background: var(--brand); color: #fff; font-size: 8.5px; font-weight: 700; box-shadow: 0 0 0 2px var(--surface);\n }\n .note-mark::after { content: \"\"; position: absolute; top: 14px; left: 6.5px; width: 1px; height: 30px; background: var(--brand); opacity: .45; }\n .note-mark.done { background: var(--ink-3); }\n .note-mark.stacked::after { display: none; }\n .note-mark.done::after { background: var(--ink-3); }\n .note-mark:hover { transform: scale(1.2); }\n</style>\n</head>\n<body>\n<header class=\"top\">\n <svg class=\"logo\" viewBox=\"0 0 512 512\" aria-label=\"framefields\" role=\"img\"><g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\"><path d=\"M184 56H86q-30 0-30 30v98M328 56h98q30 0 30 30v98M184 456H86q-30 0-30-30v-98M328 456h98q30 0 30-30v-98\" stroke-width=\"34\" stroke-linejoin=\"round\"/><path d=\"M176 182v148m0-148c0 74 60 74 100 74\" stroke-width=\"28\"/></g><g fill=\"currentColor\"><circle cx=\"176\" cy=\"172\" r=\"32\"/><circle cx=\"176\" cy=\"340\" r=\"32\"/><path d=\"M284 200l78 56-78 56z\" stroke=\"currentColor\" stroke-width=\"34\" stroke-linejoin=\"round\"/></g></svg>\n <span class=\"sep\" aria-hidden=\"true\"></span>\n <h1 id=\"title\">framefields preview</h1>\n <span class=\"grow\"></span>\n <span class=\"spec tnum\" id=\"spec\"></span>\n <span class=\"chip\" id=\"chip\" data-state=\"mixing\" role=\"status\" aria-live=\"polite\"><span class=\"spinner\"></span><span>Mixing audio</span></span>\n</header>\n\n<main class=\"player\" aria-label=\"Player\">\n <div class=\"stage-wrap\">\n <div class=\"stage\" id=\"stage\">\n <canvas id=\"canvas\" aria-label=\"Rendered frame\"></canvas>\n <div class=\"stage-msg\" id=\"stageMsg\" role=\"alert\"></div>\n <div class=\"overlay\" id=\"pins\"></div>\n <div class=\"marquee\" id=\"marquee\"></div>\n <div class=\"stage-hint\" id=\"stageHint\">Click to pin a note · drag to mark an area</div>\n <div class=\"toast\" id=\"toast\" role=\"status\" aria-live=\"polite\"></div>\n <div class=\"buffering\" id=\"buffering\" role=\"status\" aria-label=\"Loading\"><svg viewBox=\"0 0 50 50\"><circle cx=\"25\" cy=\"25\" r=\"20\"/></svg></div>\n </div>\n </div>\n\n <div class=\"transport\">\n <button class=\"icon-btn\" id=\"prev\" title=\"Previous frame\" aria-label=\"Previous frame\">\n <svg viewBox=\"0 0 16 16\" fill=\"currentColor\"><path d=\"M3 3h1.6v10H3zM13 3.6v8.8a.6.6 0 0 1-.92.5L5.8 8.5a.6.6 0 0 1 0-1l6.28-4.4a.6.6 0 0 1 .92.5z\"/></svg>\n </button>\n <button class=\"play-btn\" id=\"play\" title=\"Play\" aria-label=\"Play\" disabled>\n <svg id=\"playIcon\" viewBox=\"0 0 16 16\" fill=\"currentColor\"><path d=\"M4.5 2.9v10.2a.7.7 0 0 0 1.06.6l8.1-5.1a.7.7 0 0 0 0-1.2l-8.1-5.1a.7.7 0 0 0-1.06.6z\"/></svg>\n <svg class=\"spin\" viewBox=\"0 0 50 50\" aria-hidden=\"true\"><circle cx=\"25\" cy=\"25\" r=\"18\"/></svg>\n <svg id=\"pauseIcon\" viewBox=\"0 0 16 16\" fill=\"currentColor\" style=\"display:none\"><rect x=\"3.5\" y=\"2.5\" width=\"3.2\" height=\"11\" rx=\".8\"/><rect x=\"9.3\" y=\"2.5\" width=\"3.2\" height=\"11\" rx=\".8\"/></svg>\n </button>\n <button class=\"icon-btn\" id=\"next\" title=\"Next frame\" aria-label=\"Next frame\">\n <svg viewBox=\"0 0 16 16\" fill=\"currentColor\"><path d=\"M11.4 3H13v10h-1.6zM3 3.6v8.8a.6.6 0 0 0 .92.5l6.28-4.4a.6.6 0 0 0 0-1L3.92 3.1A.6.6 0 0 0 3 3.6z\"/></svg>\n </button>\n <div class=\"timecode tnum\">\n <span id=\"tcNow\">0:00.00</span><span class=\"dim\">/</span><span class=\"dim\" id=\"tcDur\">0:00.00</span>\n <span class=\"frame\" id=\"tcFrame\">frame 0</span>\n </div>\n <span class=\"fps tnum\" id=\"fps\" role=\"status\" hidden></span>\n <span class=\"grow\"></span>\n <button class=\"btn notes-btn\" id=\"notesBtn\" title=\"Notes\" aria-haspopup=\"dialog\" aria-expanded=\"false\" hidden>\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linejoin=\"round\"><path d=\"M3 2.75h10a1.25 1.25 0 0 1 1.25 1.25v6.5A1.25 1.25 0 0 1 13 11.75H7.5L4.5 14v-2.25H3A1.25 1.25 0 0 1 1.75 10.5V4A1.25 1.25 0 0 1 3 2.75z\"/></svg>\n Notes <span class=\"badge\" id=\"notesCount\">0</span>\n </button>\n <div class=\"notes-panel\" id=\"notesPanel\" role=\"dialog\" aria-label=\"Notes\">\n <div class=\"np-head\">\n <h2>Notes</h2><span class=\"grow\"></span>\n <button class=\"btn\" id=\"notesCopy\" title=\"Copy the open notes, to paste into a chat\">\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"><rect x=\"5.25\" y=\"5.25\" width=\"8\" height=\"8\" rx=\"1.5\"/><path d=\"M10.75 3.5V3.25a1.5 1.5 0 0 0-1.5-1.5h-5.5a1.5 1.5 0 0 0-1.5 1.5v5.5a1.5 1.5 0 0 0 1.5 1.5H4\"/></svg>\n <span id=\"notesCopyLabel\">Copy</span>\n </button>\n <button class=\"btn x-btn\" id=\"notesClose\" title=\"Close\" aria-label=\"Close\"><svg viewBox=\"0 0 16 16\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"><path d=\"M4 4l8 8M12 4l-8 8\"/></svg></button>\n </div>\n <div class=\"np-list\" id=\"notesList\"></div>\n <div class=\"np-foot\" id=\"notesFoot\"></div>\n </div>\n <button class=\"icon-btn\" id=\"mute\" title=\"Mute\" aria-label=\"Mute\" disabled>\n <svg id=\"volIcon\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M2.5 6h2.2L8 3.2v9.6L4.7 10H2.5z\" fill=\"currentColor\" stroke=\"none\"/><path d=\"M10.6 5.6a3.4 3.4 0 0 1 0 4.8M12.4 3.9a5.8 5.8 0 0 1 0 8.2\"/></svg>\n <svg id=\"muteIcon\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" style=\"display:none\"><path d=\"M2.5 6h2.2L8 3.2v9.6L4.7 10H2.5z\" fill=\"currentColor\" stroke=\"none\"/><path d=\"M10.5 6l3.5 4M14 6l-3.5 4\"/></svg>\n </button>\n <input class=\"slider\" type=\"range\" id=\"volume\" min=\"0\" max=\"1\" step=\"0.01\" value=\"0.9\" aria-label=\"Volume\" disabled>\n </div>\n\n <div class=\"timeline\" id=\"timeline\" aria-label=\"Timeline\">\n <div class=\"tl-inner\" id=\"tlInner\">\n <div class=\"ruler\" id=\"ruler\"></div>\n <div class=\"note-marks\" id=\"noteMarks\"></div>\n <div class=\"wave-lane\">\n <canvas id=\"wave\"></canvas>\n <div class=\"wave-state shimmer\" id=\"waveState\"><span class=\"spinner\"></span>Mixing audio</div>\n </div>\n <div class=\"hover-line\" id=\"hoverLine\"></div>\n <div class=\"hover-time\" id=\"hoverTime\"></div>\n <div class=\"playhead\" id=\"playhead\"></div>\n </div>\n </div>\n</main>\n\n<div class=\"pop\" id=\"pop\" role=\"dialog\" aria-label=\"Note\"></div>\n\n<script>\n(function () {\n var $ = function (id) { return document.getElementById(id); };\n var stage = $(\"stage\"), timeline = $(\"timeline\"), tlInner = $(\"tlInner\");\n\n var meta = null, session = null, frame = 0, shown = -1, playing = false, playToken = 0;\n var player = null;\n\n // ── Helpers ──\n function fmt(f) {\n var s = f / meta.fps, m = Math.floor(s / 60);\n return m + \":\" + (s - m * 60).toFixed(2).padStart(5, \"0\");\n }\n function last() { return meta.frameCount - 1; }\n function clampFrame(f) { return Math.max(0, Math.min(last(), Math.round(f))); }\n function pct(f) { return meta.frameCount > 1 ? (f / last()) * 100 : 0; }\n function el(tag, cls, text) {\n var e = document.createElement(tag);\n if (cls) e.className = cls;\n if (text != null) e.textContent = text;\n return e;\n }\n function sleep(ms) { return new Promise(function (r) { setTimeout(r, ms); }); }\n\n // ── Reporting: errors and the engine's warnings go to the preview's\n // terminal too, so whoever runs it sees them without opening this tab.\n // Once each; frame numbers aside, playback repeats the same one per frame ──\n var reportedKeys = new Set();\n function errText(x) {\n if (x && x.message) return x.stack && x.stack.indexOf(x.message) >= 0 ? x.stack.split(\"\\\\n\").slice(0, 4).join(\"\\\\n\") : x.message;\n return typeof x === \"string\" ? x : String(x);\n }\n function report(level, parts) {\n var text = parts.map(errText).join(\" \").trim(), key = level + text.replace(/\\\\d+/g, \"#\");\n if (!text || reportedKeys.has(key) || reportedKeys.size > 100) return;\n reportedKeys.add(key);\n fetch(\"/@framefields/log\", {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ level: level, message: text.slice(0, 4000) }),\n keepalive: true\n }).catch(function () {});\n }\n [\"error\", \"warn\"].forEach(function (name) {\n var original = console[name].bind(console);\n console[name] = function () {\n original.apply(null, arguments);\n var args = Array.prototype.slice.call(arguments);\n // Errors from anywhere; warnings only from the engine (fonts, renderers).\n if (name === \"error\" || (typeof args[0] === \"string\" && args[0].indexOf(\"[framefields]\") === 0)) {\n report(name === \"error\" ? \"error\" : \"warning\", args);\n }\n };\n });\n\n // ── Frames: drawn in this tab by the composition's own code ──\n function setTime(f) {\n frame = f;\n $(\"tcNow\").textContent = fmt(f);\n $(\"tcFrame\").textContent = \"frame \" + f;\n $(\"playhead\").style.left = pct(f) + \"%\";\n if (!playing && notesOn) renderPins();\n }\n function show(f) {\n setTime(f);\n if (!player) return Promise.resolve();\n $(\"buffering\").classList.add(\"on\");\n // Calls made while a frame draws collapse to the latest.\n return player.render(f).then(function (drawn) {\n shown = drawn;\n if (!stalled) $(\"buffering\").classList.remove(\"on\");\n });\n }\n function seek(f) {\n stop();\n var done = show(clampFrame(f));\n keepPosition();\n scheduleWarm();\n return done;\n }\n\n // ── Position in the address: #t=12.5 opens there, and a reload (the next\n // preview taking over) comes back to the same moment ──\n function startFrame() {\n var m = /[#&]t=([\\\\d.]+)/.exec(location.hash), n = /[#&]frame=(\\\\d+)/.exec(location.hash);\n if (m) return clampFrame(Number(m[1]) * meta.fps);\n if (n) return clampFrame(Number(n[1]));\n return 0;\n }\n function keepPosition() {\n var at = frame > 0 ? \"#t=\" + Number((frame / meta.fps).toFixed(3)) : location.pathname;\n try { history.replaceState(null, \"\", at); } catch (e) {}\n }\n\n // ── Analysing ahead: while paused, frames after the playhead are drawn\n // off-screen so vision results are cached before they play ──\n var warmTimer = null, warmToken = 0, warmed = new Set();\n function scheduleWarm() {\n clearTimeout(warmTimer);\n warmToken++;\n if (!player || !player.hasVision || playing) return;\n warmTimer = setTimeout(warmAhead, 1000);\n }\n async function warmAhead() {\n var token = ++warmToken;\n var end = Math.min(last(), frame + Math.round(meta.fps * 20));\n for (var f = frame + 1; f <= end && token === warmToken && !playing; f++) {\n if (warmed.has(f)) continue;\n if (!(await player.warm(f))) { scheduleWarm(); return; }\n warmed.add(f);\n await sleep(0);\n }\n }\n function showError(err) {\n report(\"error\", [err]);\n pageState = \"error\";\n $(\"buffering\").classList.remove(\"on\");\n $(\"stageMsg\").textContent = String((err && err.message) || err);\n $(\"stageMsg\").classList.add(\"on\");\n }\n\n // ── Audio ──\n var audio = { state: \"mixing\", error: \"\", buffer: null, peaks: null, ctx: null, gain: null, src: null, muted: false, volume: 0.9 };\n var AUDIO_LABEL = { mixing: \"Mixing audio\", loading: \"Loading audio\", ready: \"Audio ready\", none: \"No audio\", error: \"Audio failed\" };\n var offline = false;\n\n function renderChip() {\n var chip = $(\"chip\"), state, label, busy;\n if (offline) { state = \"offline\"; label = \"Preview stopped\"; busy = false; }\n else { state = audio.state; label = AUDIO_LABEL[audio.state]; busy = state === \"mixing\" || state === \"loading\"; }\n chip.dataset.state = state;\n chip.title = offline ? \"The preview command isn't running. This tab reconnects when it starts again.\" : audio.error || \"\";\n chip.replaceChildren(el(\"span\", busy ? \"spinner\" : \"dot\"), el(\"span\", null, label));\n }\n function setAudioStatus(state, error) {\n audio.state = state;\n audio.error = error || \"\";\n var busy = state === \"mixing\" || state === \"loading\", ws = $(\"waveState\");\n ws.classList.toggle(\"shimmer\", busy);\n ws.style.display = state === \"ready\" ? \"none\" : \"flex\";\n ws.replaceChildren();\n if (busy) ws.append(el(\"span\", \"spinner\"));\n ws.append(document.createTextNode(state === \"none\" ? \"No audio in this composition\" : state === \"error\" ? \"Audio could not be mixed\" : AUDIO_LABEL[state]));\n var usable = state === \"ready\" && !!audio.buffer;\n $(\"mute\").disabled = !usable;\n $(\"volume\").disabled = !usable;\n renderChip();\n drawWave();\n }\n function loadAudio(status) {\n stopClock();\n audio.buffer = null;\n audio.peaks = null;\n if (!status || status.state !== \"ready\") { setAudioStatus(status ? status.state : \"none\", status && status.error); return; }\n audio.peaks = status.peaks;\n setAudioStatus(\"loading\");\n if (!audio.ctx) {\n var Ctx = window.AudioContext || window.webkitAudioContext;\n audio.ctx = new Ctx();\n audio.gain = audio.ctx.createGain();\n audio.gain.connect(audio.ctx.destination);\n applyVolume();\n }\n fetch(\"/audio\")\n .then(function (r) { if (!r.ok) throw new Error(\"HTTP \" + r.status); return r.arrayBuffer(); })\n .then(function (data) { return audio.ctx.decodeAudioData(data); })\n .then(function (buf) { audio.buffer = buf; setAudioStatus(\"ready\"); })\n .catch(function (err) { setAudioStatus(\"error\", String((err && err.message) || err)); });\n }\n function applyVolume() {\n if (audio.gain) audio.gain.gain.value = audio.muted ? 0 : audio.volume;\n $(\"volIcon\").style.display = audio.muted ? \"none\" : \"\";\n $(\"muteIcon\").style.display = audio.muted ? \"\" : \"none\";\n $(\"mute\").title = audio.muted ? \"Unmute\" : \"Mute\";\n $(\"mute\").setAttribute(\"aria-label\", audio.muted ? \"Unmute\" : \"Mute\");\n }\n // The clock for real-time playback: the soundtrack when there is one,\n // otherwise the wall clock. Audio as master keeps picture and sound in sync.\n function startClock(f0) {\n if (audio.buffer && audio.ctx) {\n audio.ctx.resume();\n var src = audio.ctx.createBufferSource();\n src.buffer = audio.buffer;\n src.connect(audio.gain);\n var startAt = audio.ctx.currentTime + 0.06, offset = f0 / meta.fps;\n if (offset < audio.buffer.duration) src.start(startAt, offset);\n audio.src = src;\n return function () { return Math.max(0, audio.ctx.currentTime - startAt); };\n }\n var t0 = performance.now();\n return function () { return (performance.now() - t0) / 1000; };\n }\n function stopClock() {\n if (!audio.src) return;\n try { audio.src.stop(); } catch (e) {}\n audio.src.disconnect();\n audio.src = null;\n }\n\n // ── Playback ──\n var stalled = false;\n // Waiting on frames mid-playback: the play button and the picture both spin.\n function setStalled(on) {\n stalled = on;\n $(\"play\").classList.toggle(\"loading\", on);\n $(\"buffering\").classList.toggle(\"on\", on);\n }\n function setPlayingUi(on) {\n $(\"playIcon\").style.display = on ? \"none\" : \"\";\n $(\"pauseIcon\").style.display = on ? \"\" : \"none\";\n $(\"play\").title = on ? \"Pause\" : \"Play\";\n $(\"play\").setAttribute(\"aria-label\", on ? \"Pause\" : \"Play\");\n }\n function play() {\n if (playing || !player) return;\n playing = true;\n clearTimeout(warmTimer);\n warmToken++;\n var token = ++playToken;\n var alive = function () { return playing && token === playToken; };\n setPlayingUi(true);\n if (openId != null) closePop();\n if (draft && !draft.text.trim()) cancelDraft();\n if (notesOn) renderPins();\n if (frame >= last()) frame = 0;\n playFrames(alive).finally(function () { if (token === playToken && playing) pause(); });\n }\n function stop() {\n playing = false;\n stopClock();\n setStalled(false);\n setPlayingUi(false);\n meter.stop();\n renderFps();\n }\n\n // ── Frame rate reached: distinct frames shown per second while playing,\n // over the last 2 s, stalls included. The film's own rate is in meta ──\n var meter = (function () {\n var started = null, lastFrame = null, samples = [], lastFps = null;\n function read() {\n if (started === null) return { state: \"paused\", fps: lastFps };\n var now = performance.now(), elapsed = Math.min(2000, now - started);\n samples = samples.filter(function (t) { return t > now - 2000; });\n if (elapsed < 1000) return { state: \"measuring\", fps: null };\n // Counting whole frames in the window can overshoot the film's rate.\n return { state: \"playing\", fps: Math.min(meta ? meta.fps : Infinity, (samples.length * 1000) / elapsed) };\n }\n return {\n read: read,\n start: function () { started = performance.now(); lastFrame = null; samples = []; },\n stop: function () {\n if (started === null) return;\n // A playback too short to measure leaves no rate, not an older one.\n var r = read();\n lastFps = r.state === \"playing\" ? r.fps : null;\n started = null;\n samples = [];\n },\n record: function (f) {\n if (started === null || f === lastFrame) return;\n lastFrame = f;\n samples.push(performance.now());\n }\n };\n })();\n function renderFps() {\n var r = meter.read(), out = $(\"fps\");\n out.hidden = r.state === \"paused\" && r.fps === null;\n if (out.hidden) return;\n out.textContent = r.state === \"measuring\" ? \"… fps\" : r.fps.toFixed(1) + \" fps\";\n out.title = (r.state === \"paused\" ? \"Frame rate the last playback reached\" : \"Frame rate playback reaches\") + (meta ? \"; the film runs at \" + meta.fps + \" fps\" : \"\");\n }\n setInterval(function () { if (playing) renderFps(); }, 500);\n // Pausing settles on exactly the frame the timecode shows.\n function pause() { stop(); keepPosition(); show(frame).then(scheduleWarm); }\n function togglePlay() { playing ? pause() : play(); }\n\n // Real time: nothing is rendered ahead. Each turn draws the frame due on\n // the clock straight to the screen, and frames that drawing missed are\n // skipped. Playback starts at once and the sound keeps its speed; a device\n // that can't keep up shows fewer frames, and the frame rate says how many.\n async function playFrames(alive) {\n meter.start();\n renderFps();\n // A soundtrack still downloading would start late: wait for it.\n if (audio.state === \"loading\") {\n setStalled(true);\n while (alive() && audio.state === \"loading\") await sleep(50);\n // A newer playback may be waiting too: only this one's own stall ends here.\n if (!alive()) return;\n setStalled(false);\n }\n var f0 = frame, clock = startClock(f0), presented = -1;\n while (alive()) {\n var due = Math.min(last(), f0 + Math.floor(clock() * meta.fps));\n if (due === presented) { await sleep(4); continue; }\n setTime(due);\n // Each frame is drawn once; caching it would cost a texture and a copy per frame.\n var drawn = await player.render(due, { cache: false });\n if (!alive()) return;\n presented = shown = drawn;\n meter.record(drawn);\n if (drawn >= last()) return;\n }\n }\n\n // ── Timeline ──\n function frameAtX(clientX) {\n var r = tlInner.getBoundingClientRect();\n return clampFrame(Math.min(1, Math.max(0, (clientX - r.left) / r.width)) * last());\n }\n function renderRuler() {\n var ruler = $(\"ruler\"), width = tlInner.clientWidth || 800, dur = last() / meta.fps;\n ruler.replaceChildren();\n if (dur <= 0) return;\n var steps = [0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600];\n var major = steps.find(function (s) { return (s / dur) * width >= 80; }) || 1200, minor = major / 5;\n for (var i = 0; i * minor <= dur + 1e-6; i++) {\n var t = i * minor, isMajor = i % 5 === 0, x = (t / dur) * width;\n var tick = el(\"div\", \"tick\" + (isMajor ? \" major\" : \"\"));\n tick.style.left = (t / dur) * 100 + \"%\";\n if (isMajor && x < width - 34) {\n var m = Math.floor(t / 60), s = Math.round((t - m * 60) * 10) / 10;\n tick.append(el(\"span\", null, m + \":\" + (s < 10 ? \"0\" : \"\") + s));\n }\n ruler.append(tick);\n }\n }\n function drawWave() {\n var canvas = $(\"wave\"), dpr = window.devicePixelRatio || 1;\n var w = canvas.clientWidth, h = canvas.clientHeight;\n canvas.width = Math.round(w * dpr);\n canvas.height = Math.round(h * dpr);\n var ctx = canvas.getContext(\"2d\");\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n ctx.clearRect(0, 0, w, h);\n var peaks = audio.peaks;\n if (!peaks || !peaks.length || !meta) return;\n var videoSec = Math.max(1e-6, last() / meta.fps);\n var audioSec = audio.buffer ? audio.buffer.duration : videoSec;\n var spanPx = Math.min(1, audioSec / videoSec) * w, mid = h / 2;\n ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue(\"--wave\").trim();\n for (var x = 0; x < spanPx; x += 3) {\n var v = Math.max(peaks[Math.floor((x / spanPx) * peaks.length)] || 0, 0.015);\n var bh = Math.max(1, Math.sqrt(v) * (h - 8));\n ctx.fillRect(x, mid - bh / 2, 2, bh);\n }\n }\n\n var dragging = false;\n timeline.addEventListener(\"pointerdown\", function (e) {\n if (!meta || e.button !== 0) return;\n timeline.setPointerCapture(e.pointerId);\n dragging = true;\n seek(frameAtX(e.clientX));\n });\n timeline.addEventListener(\"pointermove\", function (e) {\n if (!meta) return;\n var f = frameAtX(e.clientX), r = tlInner.getBoundingClientRect();\n var x = Math.min(r.width, Math.max(0, e.clientX - r.left));\n $(\"hoverLine\").style.display = \"block\";\n $(\"hoverLine\").style.left = x + \"px\";\n $(\"hoverTime\").style.display = \"block\";\n $(\"hoverTime\").style.left = x + \"px\";\n $(\"hoverTime\").textContent = fmt(f);\n if (dragging && f !== frame) seek(f);\n });\n timeline.addEventListener(\"pointerleave\", function () {\n $(\"hoverLine\").style.display = \"none\";\n $(\"hoverTime\").style.display = \"none\";\n });\n timeline.addEventListener(\"pointerup\", function () { dragging = false; });\n timeline.addEventListener(\"pointercancel\", function () { dragging = false; });\n\n // ── Notes: pause, click the picture (or drag an area) and say what should\n // change. Each is saved with its moment, its spot and a picture of the\n // frame with the spot marked, for whoever edits the film ──\n var notes = [], notesOn = false, draft = null, openId = null, saving = false, press = null;\n var pop = $(\"pop\"), pins = $(\"pins\"), NL = String.fromCharCode(10);\n var ICON_CHECK = '<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3.5 8.5l3 3 6-7\"/></svg>';\n var ICON_X = '<svg viewBox=\"0 0 16 16\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"><path d=\"M4 4l8 8M12 4l-8 8\"/></svg>';\n\n function pct1(v) { return Math.round(v * 100) + \"%\"; }\n function whereText(n) {\n return n.w != null ? \"area \" + pct1(n.x) + \",\" + pct1(n.y) + \" to \" + pct1(n.x + n.w) + \",\" + pct1(n.y + n.h) : \"spot \" + pct1(n.x) + \",\" + pct1(n.y);\n }\n function noteById(id) { return notes.find(function (n) { return n.id === id; }); }\n function openCount() { return notes.filter(function (n) { return !n.done; }).length; }\n\n function renderNotes() {\n renderPins();\n renderMarks();\n renderList();\n var open = openCount(), count = $(\"notesCount\");\n count.textContent = String(open || notes.length);\n count.classList.toggle(\"open\", open > 0);\n $(\"notesBtn\").title = open + \" open of \" + notes.length;\n }\n function renderPins() {\n if (!meta) return;\n stage.classList.toggle(\"can-note\", notesOn && !!player && !playing);\n stage.classList.toggle(\"hint\", notes.length === 0 && !draft);\n pins.replaceChildren();\n if (playing) return;\n var here = notes.filter(function (n) { return n.frame === frame; });\n if (draft && draft.frame === frame) here.push(draft);\n here.forEach(function (n) {\n if (n.w != null) {\n var a = el(\"div\", \"area\" + (n.done ? \" done\" : \"\"));\n a.style.left = n.x * 100 + \"%\"; a.style.top = n.y * 100 + \"%\";\n a.style.width = n.w * 100 + \"%\"; a.style.height = n.h * 100 + \"%\";\n pins.append(a);\n }\n var pin = el(\"button\", \"pin\" + (n.done ? \" done\" : \"\") + (n === draft ? \" draft\" : \"\") + (n.id === openId ? \" active\" : \"\"));\n pin.style.left = n.x * 100 + \"%\"; pin.style.top = n.y * 100 + \"%\";\n if (n === draft) { pin.setAttribute(\"aria-label\", \"New note\"); }\n else {\n if (n.done) pin.innerHTML = ICON_CHECK; else pin.textContent = String(n.id);\n pin.title = n.text;\n pin.setAttribute(\"aria-label\", \"Note \" + n.id);\n pin.addEventListener(\"click\", function (e) { e.stopPropagation(); openCard(n.id); });\n }\n pins.append(pin);\n });\n }\n function renderMarks() {\n var marks = $(\"noteMarks\");\n marks.replaceChildren();\n if (!meta) return;\n // Notes close together stack, up to three high.\n var width = tlInner.clientWidth || 800, lanes = [];\n notes.slice().sort(function (a, b) { return a.frame - b.frame || a.id - b.id; }).forEach(function (n) {\n var x = (pct(n.frame) / 100) * width, lane = lanes.findIndex(function (end) { return x - end >= 16; });\n if (lane < 0) lane = lanes.length < 3 ? lanes.length : 2;\n lanes[lane] = x;\n var m = el(\"button\", \"note-mark\" + (n.done ? \" done\" : \"\") + (lane ? \" stacked\" : \"\"), String(n.id));\n m.style.left = pct(n.frame) + \"%\";\n m.style.top = 4 + lane * 15 + \"px\";\n m.title = n.id + \" · \" + fmt(n.frame) + \" · \" + n.text;\n m.addEventListener(\"pointerdown\", function (e) { e.stopPropagation(); });\n m.addEventListener(\"click\", function (e) { e.stopPropagation(); goToNote(n.id); });\n marks.append(m);\n });\n }\n function renderList() {\n var list = $(\"notesList\");\n list.replaceChildren();\n $(\"notesCopy\").disabled = openCount() === 0;\n if (!notes.length) {\n list.append(el(\"div\", \"np-empty\", \"Pause and click the picture to pin a note to that spot and moment. Drag to mark an area.\"));\n }\n notes.slice().sort(function (a, b) { return a.frame - b.frame || a.id - b.id; }).forEach(function (n) {\n var row = el(\"div\", \"note-row\" + (n.done ? \" done\" : \"\"));\n var body = el(\"div\");\n body.append(el(\"div\", \"when tnum\", fmt(n.frame) + \" · frame \" + n.frame), el(\"div\", \"text\", n.text));\n var check = el(\"button\", \"check\" + (n.done ? \" on\" : \"\"));\n check.innerHTML = ICON_CHECK;\n check.title = n.done ? \"Reopen\" : \"Mark done\";\n check.setAttribute(\"aria-label\", check.title);\n check.addEventListener(\"click\", function (e) { e.stopPropagation(); setDone(n.id, !n.done); });\n row.append(el(\"span\", \"badge\" + (n.done ? \" done\" : \"\"), String(n.id)), body, check);\n row.addEventListener(\"click\", function () { goToNote(n.id); });\n list.append(row);\n });\n var dir = meta && meta.notesDir ? meta.notesDir.split(\"/\").slice(-2).join(\"/\") : \"\";\n $(\"notesFoot\").textContent = \"Saved in \" + dir + \" with a picture of each spot. Ask your agent to go through them.\";\n }\n\n function notesRequest(body) {\n return fetch(\"/@framefields/notes\", { method: \"POST\", headers: { \"content-type\": \"application/json\" }, body: JSON.stringify(body) })\n .then(function (r) { if (!r.ok) throw new Error(\"HTTP \" + r.status); return r.json(); });\n }\n function upsert(note) {\n var i = notes.findIndex(function (n) { return n.id === note.id; });\n if (i >= 0) notes[i] = note; else notes.push(note);\n renderNotes();\n }\n function setDone(id, done) {\n notesRequest({ op: \"update\", id: id, done: done }).then(function (n) {\n upsert(n);\n if (openId === id) openCard(id);\n }, function () { toast(\"Couldn't save. Is the preview running?\"); });\n }\n function removeNote(id) {\n notesRequest({ op: \"remove\", id: id }).then(function () {\n notes = notes.filter(function (n) { return n.id !== id; });\n closePop();\n renderNotes();\n }, function () { toast(\"Couldn't delete. Is the preview running?\"); });\n }\n function goToNote(id) {\n var n = noteById(id);\n if (!n) return;\n $(\"notesPanel\").classList.remove(\"on\");\n $(\"notesBtn\").setAttribute(\"aria-expanded\", \"false\");\n seek(n.frame).then(function () { openCard(id); });\n }\n\n var toastTimer = null;\n function toast(text) {\n var t = $(\"toast\");\n t.textContent = text;\n t.classList.add(\"on\");\n clearTimeout(toastTimer);\n toastTimer = setTimeout(function () { t.classList.remove(\"on\"); }, 2200);\n }\n\n // ── Placing a note on the picture ──\n function fracAt(e) {\n var r = stage.getBoundingClientRect();\n return { x: Math.min(1, Math.max(0, (e.clientX - r.left) / r.width)), y: Math.min(1, Math.max(0, (e.clientY - r.top) / r.height)), r: r };\n }\n stage.addEventListener(\"pointerdown\", function (e) {\n if (e.button !== 0 || !player || e.target.closest(\".pin\")) return;\n if (playing) { pause(); return; }\n if (!notesOn) return;\n if (draft && pop.querySelector(\"textarea\") && pop.querySelector(\"textarea\").value.trim()) {\n pop.querySelector(\"textarea\").focus();\n return;\n }\n cancelDraft();\n var p = fracAt(e);\n press = { x: p.x, y: p.y, cx: e.clientX, cy: e.clientY, area: null };\n stage.setPointerCapture(e.pointerId);\n e.preventDefault();\n });\n stage.addEventListener(\"pointermove\", function (e) {\n if (!press) return;\n var p = fracAt(e);\n if (!press.area && Math.hypot(e.clientX - press.cx, e.clientY - press.cy) < 6) return;\n press.area = { x: Math.min(press.x, p.x), y: Math.min(press.y, p.y), w: Math.abs(p.x - press.x), h: Math.abs(p.y - press.y) };\n var m = $(\"marquee\");\n m.style.display = \"block\";\n m.style.left = press.area.x * 100 + \"%\"; m.style.top = press.area.y * 100 + \"%\";\n m.style.width = press.area.w * 100 + \"%\"; m.style.height = press.area.h * 100 + \"%\";\n });\n function endPress() {\n if (!press) return;\n var a = press.area;\n $(\"marquee\").style.display = \"none\";\n draft = a && a.w > 0.01 && a.h > 0.01\n ? { frame: frame, x: a.x, y: a.y, w: a.w, h: a.h, text: \"\" }\n : { frame: frame, x: press.x, y: press.y, text: \"\" };\n press = null;\n openComposer();\n }\n stage.addEventListener(\"pointerup\", endPress);\n stage.addEventListener(\"pointercancel\", function () { press = null; $(\"marquee\").style.display = \"none\"; });\n\n // ── The popover: writing a note, or reading one ──\n var popAnchor = null;\n function placePop() {\n if (!popAnchor || !pop.classList.contains(\"on\")) return;\n var r = stage.getBoundingClientRect(), n = popAnchor;\n var ax = r.left + (n.x + (n.w || 0)) * r.width, ay = r.top + (n.y + (n.h || 0)) * r.height;\n var w = pop.offsetWidth, h = pop.offsetHeight, gap = 16;\n var left = ax + gap + w <= innerWidth - 8 ? ax + gap : ax - gap - w - (n.w || 0) * r.width;\n var top = ay + gap + h <= innerHeight - 8 ? ay + gap / 2 : ay - h - gap / 2 - (n.h || 0) * r.height;\n pop.style.left = Math.max(8, Math.min(innerWidth - w - 8, left)) + \"px\";\n pop.style.top = Math.max(8, Math.min(innerHeight - h - 8, top)) + \"px\";\n }\n function showPop(anchor) {\n popAnchor = anchor;\n pop.classList.add(\"on\");\n placePop();\n }\n function closePop() {\n pop.classList.remove(\"on\");\n pop.replaceChildren();\n popAnchor = null;\n openId = null;\n renderPins();\n }\n function cancelDraft() {\n if (!draft) return;\n draft = null;\n closePop();\n }\n function openComposer() {\n openId = null;\n var head = el(\"div\", \"pop-head\");\n head.append(el(\"span\", \"badge\", \"+\"), el(\"span\", \"tnum\", fmt(draft.frame) + \" · \" + (draft.w != null ? \"area\" : \"spot\")));\n var ta = document.createElement(\"textarea\");\n ta.placeholder = \"What should change here?\";\n ta.rows = 2;\n ta.setAttribute(\"aria-label\", \"Note\");\n var foot = el(\"div\", \"pop-foot\"), status = el(\"span\", \"grow\", \"Enter to add\");\n var cancel = el(\"button\", \"btn\", \"Cancel\"), add = el(\"button\", \"btn primary\", \"Add note\");\n add.disabled = true;\n foot.append(status, cancel, add);\n pop.replaceChildren(head, ta, foot);\n ta.addEventListener(\"input\", function () {\n draft.text = ta.value;\n add.disabled = !ta.value.trim() || saving;\n ta.style.height = \"auto\";\n ta.style.height = Math.min(180, ta.scrollHeight) + \"px\";\n placePop();\n });\n ta.addEventListener(\"keydown\", function (e) {\n if (e.key === \"Enter\" && !e.shiftKey && !e.isComposing) { e.preventDefault(); saveDraft(ta, add, status); }\n if (e.key === \"Escape\") { e.preventDefault(); cancelDraft(); }\n });\n cancel.addEventListener(\"click\", cancelDraft);\n add.addEventListener(\"click\", function () { saveDraft(ta, add, status); });\n renderPins();\n showPop(draft);\n ta.focus();\n }\n async function saveDraft(ta, add, status) {\n var d = draft, text = ta.value.trim();\n if (!d || !text || saving) return;\n saving = true;\n ta.readOnly = true;\n add.disabled = true;\n status.className = \"grow\";\n status.textContent = \"Saving…\";\n try {\n var image = null;\n try { image = await capture(d); } catch (err) { report(\"warning\", [\"[framefields] no picture for the note:\", err]); }\n var note = await notesRequest({ op: \"add\", note: { frame: d.frame, time: d.frame / meta.fps, x: d.x, y: d.y, w: d.w, h: d.h, text: text, image: image } });\n if (draft === d) draft = null;\n closePop();\n upsert(note);\n toast(\"Note \" + note.id + \" saved\");\n } catch (err) {\n status.className = \"grow err\";\n status.textContent = \"Couldn't save. Is the preview running?\";\n ta.readOnly = false;\n add.disabled = false;\n } finally {\n saving = false;\n }\n }\n // The frame with the spot marked, small enough to read at a glance.\n async function capture(d) {\n var bmp = await player.snapshot(d.frame);\n var scale = Math.min(1, 1600 / bmp.width), w = Math.round(bmp.width * scale), h = Math.round(bmp.height * scale);\n var c = document.createElement(\"canvas\");\n c.width = w; c.height = h;\n var g = c.getContext(\"2d\");\n g.drawImage(bmp, 0, 0, w, h);\n bmp.close();\n var lw = Math.max(3, w / 400);\n function mark(draw) {\n // Dark, white, then blue: it reads on any picture.\n g.lineWidth = lw + 5; g.strokeStyle = \"rgba(0,0,0,0.5)\"; draw();\n g.lineWidth = lw + 2.5; g.strokeStyle = \"#fff\"; draw();\n g.lineWidth = lw; g.strokeStyle = \"#3b82f6\"; draw();\n }\n if (d.w != null) {\n mark(function () { g.strokeRect(d.x * w, d.y * h, d.w * w, d.h * h); });\n } else {\n var rad = Math.max(16, w / 40);\n mark(function () { g.beginPath(); g.arc(d.x * w, d.y * h, rad, 0, Math.PI * 2); g.stroke(); });\n g.fillStyle = \"#3b82f6\";\n g.beginPath(); g.arc(d.x * w, d.y * h, lw, 0, Math.PI * 2); g.fill();\n }\n return c.toDataURL(\"image/jpeg\", 0.88);\n }\n function openCard(id) {\n var n = noteById(id);\n if (!n) return;\n if (draft) cancelDraft();\n openId = id;\n var head = el(\"div\", \"pop-head\");\n var close = el(\"button\", \"btn x-btn\");\n close.innerHTML = ICON_X;\n close.title = \"Close\";\n close.setAttribute(\"aria-label\", \"Close\");\n close.addEventListener(\"click\", closePop);\n head.append(el(\"span\", \"badge\" + (n.done ? \" done\" : \"\"), String(n.id)), el(\"span\", \"tnum grow\", fmt(n.frame) + \" · frame \" + n.frame), close);\n var foot = el(\"div\", \"pop-foot\");\n var done = el(\"button\", \"btn\");\n done.innerHTML = ICON_CHECK;\n done.append(document.createTextNode(n.done ? \"Reopen\" : \"Mark done\"));\n done.addEventListener(\"click\", function () { setDone(n.id, !n.done); });\n var del = el(\"button\", \"btn danger\", \"Delete\");\n del.addEventListener(\"click\", function () { removeNote(n.id); });\n foot.append(done, el(\"span\", \"grow\"), del);\n pop.replaceChildren(head, el(\"div\", \"pop-text\", n.text), foot);\n renderPins();\n showPop(n);\n }\n\n document.addEventListener(\"pointerdown\", function (e) {\n if (pop.classList.contains(\"on\") && !pop.contains(e.target) && !stage.contains(e.target) && !e.target.closest(\".note-mark, .note-row\")) {\n if (openId != null) closePop();\n else if (draft && !draft.text.trim()) cancelDraft();\n }\n var panel = $(\"notesPanel\");\n if (panel.classList.contains(\"on\") && !panel.contains(e.target) && !$(\"notesBtn\").contains(e.target)) {\n panel.classList.remove(\"on\");\n $(\"notesBtn\").setAttribute(\"aria-expanded\", \"false\");\n }\n });\n document.addEventListener(\"keydown\", function (e) {\n if (e.key !== \"Escape\") return;\n if (openId != null) closePop();\n $(\"notesPanel\").classList.remove(\"on\");\n $(\"notesBtn\").setAttribute(\"aria-expanded\", \"false\");\n });\n window.addEventListener(\"resize\", placePop);\n new ResizeObserver(placePop).observe(stage);\n\n $(\"notesBtn\").addEventListener(\"click\", function () {\n var on = $(\"notesPanel\").classList.toggle(\"on\");\n $(\"notesBtn\").setAttribute(\"aria-expanded\", String(on));\n });\n $(\"notesClose\").addEventListener(\"click\", function () {\n $(\"notesPanel\").classList.remove(\"on\");\n $(\"notesBtn\").setAttribute(\"aria-expanded\", \"false\");\n });\n // The open notes as text, for a chat that can't read the notes folder.\n $(\"notesCopy\").addEventListener(\"click\", function () {\n var lines = [\"Notes on the preview of \" + JSON.stringify(meta.title) + \":\"];\n notes.filter(function (n) { return !n.done; }).sort(function (a, b) { return a.frame - b.frame; }).forEach(function (n) {\n lines.push(\"- Note \" + n.id + \" at \" + fmt(n.frame) + \" (frame \" + n.frame + \"), \" + whereText(n) + \": \" + n.text + (n.image ? \" [picture: \" + n.image + \"]\" : \"\"));\n });\n var text = lines.join(NL), label = $(\"notesCopyLabel\");\n var done = function () { label.textContent = \"Copied\"; setTimeout(function () { label.textContent = \"Copy\"; }, 1600); };\n (navigator.clipboard ? navigator.clipboard.writeText(text) : Promise.reject()).then(done, function () {\n var t = document.createElement(\"textarea\");\n t.value = text;\n document.body.append(t);\n t.select();\n try { document.execCommand(\"copy\"); done(); } catch (e) {}\n t.remove();\n });\n });\n\n function loadNotes() {\n if (!notesOn) return;\n fetch(\"/@framefields/notes\").then(function (r) { return r.json(); }).then(function (list) {\n notes = Array.isArray(list) ? list : [];\n renderNotes();\n }, function () {});\n }\n\n // ── Wiring ──\n $(\"play\").addEventListener(\"click\", togglePlay);\n $(\"prev\").addEventListener(\"click\", function () { seek(frame - 1); });\n $(\"next\").addEventListener(\"click\", function () { seek(frame + 1); });\n $(\"mute\").addEventListener(\"click\", function () { audio.muted = !audio.muted; applyVolume(); });\n $(\"volume\").addEventListener(\"input\", function (e) { audio.volume = Number(e.target.value); audio.muted = audio.volume === 0; applyVolume(); });\n\n // A hidden tab's timers are throttled, so playback would stutter: pause.\n document.addEventListener(\"visibilitychange\", function () {\n if (document.hidden && playing) pause();\n });\n\n // ── For scripts and agents driving the page: seek, play, and read the\n // state instead of clicking at coordinates ──\n var pageState = \"loading\";\n window.framefieldsPreview = {\n /** Shows the frame at a time in seconds; resolves once it is drawn. */\n seek: function (sec) { return ready().then(function () { return seek(Number(sec) * meta.fps); }); },\n /** Shows a frame by number; resolves once it is drawn. */\n seekFrame: function (f) { return ready().then(function () { return seek(f); }); },\n play: function () { return ready().then(play); },\n /** Notes pinned on the page, as saved in notes.json. */\n notes: function () { return notes.slice(); },\n pause: function () { if (playing) pause(); },\n /** Where the preview is: loading, ready or error, and what it shows. */\n state: function () {\n var rate = meter.read().fps;\n return {\n state: pageState,\n error: pageState === \"error\" ? $(\"stageMsg\").textContent : null,\n frame: frame,\n time: meta ? Number((frame / meta.fps).toFixed(3)) : 0,\n playing: playing,\n buffering: stalled,\n audio: audio.state,\n connected: !offline,\n // Frames shown per second over the last 2 s of playback (the last\n // playback's while paused), and how long the last frame took to draw.\n playbackFps: rate == null ? null : Number(rate.toFixed(1)),\n renderMs: player ? Math.round(player.lastRenderMs) : null,\n meta: meta && { title: meta.title, width: meta.width, height: meta.height, fps: meta.fps, frameCount: meta.frameCount, duration: Number((last() / meta.fps).toFixed(3)) }\n };\n }\n };\n var readyWaiters = [];\n function ready() {\n if (player) return Promise.resolve();\n if (pageState === \"error\") return Promise.reject(new Error($(\"stageMsg\").textContent));\n return new Promise(function (resolve, reject) { readyWaiters.push([resolve, reject]); });\n }\n function settleReady(err) {\n readyWaiters.splice(0).forEach(function (w) { err ? w[1](err) : w[0](); });\n }\n\n function applyMeta(m) {\n meta = m;\n document.title = m.title + \" · preview\";\n $(\"title\").textContent = m.title;\n $(\"spec\").textContent = m.width + \"×\" + m.height + \" · \" + m.fps + \" fps · \" + fmt(last());\n $(\"tcDur\").textContent = fmt(last());\n stage.style.setProperty(\"--ar\", m.width + \" / \" + m.height);\n stage.style.setProperty(\"--ar-n\", String(m.width / m.height));\n renderRuler();\n }\n\n new ResizeObserver(function () {\n if (!meta) return;\n renderRuler();\n drawWave();\n renderMarks();\n }).observe(tlInner);\n\n // When the preview command ends the event stream drops and the browser keeps\n // retrying. The next preview of this project (same port) says hello with a\n // new session, and this tab reloads into it.\n var events = new EventSource(\"/events\");\n events.addEventListener(\"hello\", function (e) {\n var s = JSON.parse(e.data).session;\n if (session && s !== session) { location.reload(); return; }\n offline = false;\n renderChip();\n });\n events.addEventListener(\"error\", function () {\n if (!meta) return;\n if (playing) { stop(); keepPosition(); }\n offline = true;\n renderChip();\n });\n events.addEventListener(\"notes\", function (e) {\n notes = JSON.parse(e.data);\n if (openId != null && !noteById(openId)) closePop();\n renderNotes();\n });\n events.addEventListener(\"audio\", function (e) {\n if (meta) loadAudio(JSON.parse(e.data));\n });\n\n fetch(\"/meta\").then(function (r) { return r.json(); }).then(function (m) {\n session = m.session;\n applyMeta(m);\n notesOn = !!m.notesDir;\n $(\"notesBtn\").hidden = !notesOn;\n loadNotes();\n loadAudio(m.audio);\n setTime(startFrame());\n $(\"buffering\").classList.add(\"on\");\n whenPlayer();\n });\n\n // The composition's bundle (loaded below) announces its player.\n function whenPlayer() {\n if (!window.framefieldsPlayer) return;\n window.framefieldsPlayer.then(function (p) {\n player = p;\n $(\"play\").disabled = false;\n show(frame).then(function () {\n pageState = \"ready\";\n renderPins();\n document.documentElement.dataset.preview = \"ready\";\n settleReady();\n scheduleWarm();\n });\n }, function (err) {\n showError(err);\n document.documentElement.dataset.preview = \"error\";\n settleReady(err instanceof Error ? err : new Error(String(err)));\n });\n }\n window.addEventListener(\"framefields:player\", function () { if (meta) whenPlayer(); });\n window.addEventListener(\"error\", function (e) {\n if (!player) showError(e.error || e.message);\n else report(\"error\", [e.error || e.message]);\n });\n window.addEventListener(\"unhandledrejection\", function (e) { report(\"error\", [e.reason]); });\n})();\n</script>\n<script type=\"module\" src=\"/@framefields/player.js\"></script>\n</body>\n</html>\n`;\n","import { spawn } from \"node:child_process\";\nimport { createHash, randomUUID } from \"node:crypto\";\nimport fs from \"node:fs\";\nimport http from \"node:http\";\nimport type { AddressInfo } from \"node:net\";\nimport path from \"node:path\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\nimport type { VirtualMediaData } from \"@framefields/core\";\nimport { encodeStereoWav } from \"../audio/index.js\";\nimport { HeadlessMediaRenderer } from \"../renderer/index.js\";\nimport {\n\tbundleProject,\n\tENGINE_URL,\n\tengineDir,\n\tonnxRuntimeWeb,\n\tvisionModelsDir,\n} from \"./bundle.js\";\nimport { describeNote, type NewNote, NoteStore } from \"./notes.js\";\nimport { previewPage } from \"./page.js\";\n\n/** Where the composition comes from: the module that builds it. */\nexport interface PreviewSource {\n\t/** Path or file URL of the module, e.g. `new URL(\"./film.ts\", import.meta.url)`. */\n\tentry: string | URL;\n\t/**\n\t * The export to preview: a function returning a `Composition` (it may be\n\t * async), or a `Composition`. Defaults to `\"default\"`.\n\t */\n\texport?: string;\n}\n\n/** What the server needs from the composition: its size, timing and soundtrack. */\ninterface PreviewTarget {\n\ttoVirtualMedia?(): VirtualMediaData;\n\ttoVirtualMediaAsync?(): Promise<VirtualMediaData>;\n\tcomputeDuration?(): Promise<number>;\n\tfps?: number;\n\tdurationMs?: number;\n\twidth?: number;\n\theight?: number;\n}\n\nexport interface PreviewOptions {\n\t/**\n\t * Port to listen on. Defaults to a port derived from the working directory,\n\t * so every preview of a project lands on the same URL and an open tab picks\n\t * up the next one by itself. A preview already running there is replaced.\n\t */\n\tport?: number;\n\t/** Interface to bind. Defaults to \"127.0.0.1\" so the preview never leaves the machine. */\n\thost?: string;\n\t/**\n\t * Also open the page in the system's default browser. Off by default: the\n\t * caller shows the URL where it suits, e.g. an agent app's own browser\n\t * pane. Skipped when a tab from an earlier preview reconnects on its own.\n\t */\n\topen?: boolean;\n\t/** Title shown in the page header. */\n\ttitle?: string;\n\t/** Mix and play the composition's audio. Defaults to true. */\n\taudio?: boolean;\n\t/** Optional shared renderer instance, used to mix the soundtrack. */\n\trenderer?: HeadlessMediaRenderer;\n\t/**\n\t * Close the server this many milliseconds after the last open tab goes\n\t * away (closed, not reloaded). Defaults to 5000. `false` keeps it serving\n\t * until `close()`. Nothing closes before a tab has connected.\n\t */\n\tidleCloseMs?: number | false;\n\t/**\n\t * Files the page may load by absolute path (the composition's assets).\n\t * Defaults to the project that holds the entry: the nearest `.git`, else\n\t * the nearest `package.json`, else the entry's directory. Pass one path or\n\t * several to allow assets kept outside it.\n\t */\n\troot?: string | string[];\n\t/**\n\t * Where the preview reports what happens out of sight: the page opening,\n\t * errors in the page (a failed build, a frame that threw, no WebGPU), and\n\t * why the server stopped. Defaults to stderr, so whoever runs the preview\n\t * (an agent's background command, say) reads them in its output. `false`\n\t * keeps it quiet.\n\t */\n\tlog?: ((line: string) => void) | false;\n\t/**\n\t * Where notes pinned on the page are kept: `notes.json` and, per note, the\n\t * frame with its spot marked. Defaults to `.framefields/preview-notes` in the\n\t * working directory (`.framefields` ignores itself in git). `false` turns\n\t * notes off.\n\t */\n\tnotesDir?: string | false;\n}\n\n/** Why a preview stopped. */\nexport type PreviewCloseReason =\n\t/** `close()` was called. */\n\t| \"closed\"\n\t/** Its last tab closed (see `idleCloseMs`). */\n\t| \"idle\"\n\t/** A newer preview of the project took over its port. */\n\t| \"replaced\";\n\nexport interface PreviewSession {\n\turl: string;\n\tclose(): Promise<void>;\n\t/** Resolves with the reason when the server stops. */\n\tclosed: Promise<PreviewCloseReason>;\n}\n\ninterface PreviewMeta {\n\tsession: string;\n\t/** Where notes are kept; null when they are off. */\n\tnotesDir: string | null;\n\ttitle: string;\n\tfps: number;\n\tframeCount: number;\n\twidth: number;\n\theight: number;\n}\n\ntype AudioStatus =\n\t| { state: \"mixing\" }\n\t| { state: \"ready\"; durationSec: number; peaks: number[] }\n\t| { state: \"none\" }\n\t| { state: \"error\"; error: string };\n\nconst PEAK_BINS = 1200;\n/** Requests carrying this header come from another preview process, not a page. */\nconst TAKEOVER_HEADER = \"x-framefields-preview\";\nconst PLAYER_PATH = \"/@framefields/player.js\";\n\n/**\n * Serves a localhost player for a composition. The page loads the\n * composition's own code, bundled with the engine, and renders its frames\n * with WebGPU in the browser, like the editor's player; nothing is streamed.\n * The server provides the bundle, the project's files, and the soundtrack,\n * which it mixes with the export engine.\n *\n * The server runs until its tab closes or `close()`. Starting another preview\n * on the same port (the default for a project) stops this one, and open tabs\n * reload into it.\n */\nexport async function startPreview(\n\tsource: PreviewSource,\n\toptions: PreviewOptions = {},\n): Promise<PreviewSession> {\n\tconst entry =\n\t\tsource.entry instanceof URL || String(source.entry).startsWith(\"file:\")\n\t\t\t? fileURLToPath(source.entry)\n\t\t\t: path.resolve(String(source.entry));\n\tconst exportName = source.export ?? \"default\";\n\t// Vision nodes load their cached models, and onnxruntime-web's files when\n\t// the project has it installed.\n\tconst modelsDir = visionModelsDir();\n\tconst ort = onnxRuntimeWeb(path.dirname(entry));\n\tconst roots = [\n\t\t...(options.root === undefined\n\t\t\t? [repoRoot(path.dirname(entry))]\n\t\t\t: Array.isArray(options.root)\n\t\t\t\t? options.root\n\t\t\t\t: [options.root]),\n\t\tmodelsDir,\n\t\tort.dir,\n\t]\n\t\t.filter((r): r is string => !!r)\n\t\t.map((r) => path.resolve(r));\n\tconst sseClients = new Set<http.ServerResponse>();\n\tconst idleCloseMs = options.idleCloseMs ?? 5000;\n\tlet idleTimer: NodeJS.Timeout | undefined;\n\tconst title = options.title ?? \"framefields preview\";\n\tconst session = randomUUID();\n\tconst log =\n\t\toptions.log === false\n\t\t\t? () => {}\n\t\t\t: (options.log ?? ((line: string) => console.error(line)));\n\tconst say = (line: string) => log(`[framefields preview] ${line}`);\n\t// Each distinct page error is reported once; playback repeats them per frame.\n\tconst reported = new Set<string>();\n\tlet pageOpened = false;\n\n\tconst notes =\n\t\toptions.notesDir === false\n\t\t\t? undefined\n\t\t\t: new NoteStore(\n\t\t\t\t\tpath.resolve(\n\t\t\t\t\t\toptions.notesDir ??\n\t\t\t\t\t\t\tpath.join(process.cwd(), \".framefields\", \"preview-notes\"),\n\t\t\t\t\t),\n\t\t\t\t);\n\t// Changes run one at a time: each reads and rewrites notes.json.\n\tlet noteQueue: Promise<unknown> = Promise.resolve();\n\n\tconst target = await loadTarget(entry, exportName);\n\tconst meta = await resolveMeta(target, title, session, notes?.dir ?? null);\n\t// The project's own code, bundled against the browser engine; the engine\n\t// itself is prebuilt (or built once, running from source).\n\tlet bundle: Promise<string> | undefined;\n\tconst playerBundle = () => {\n\t\tif (!bundle) {\n\t\t\tconst built = bundleProject({\n\t\t\t\tentry,\n\t\t\t\texportName,\n\t\t\t\tmodelsDir,\n\t\t\t\tortBase: ort.base,\n\t\t\t}).then((b) => b.code);\n\t\t\tbundle = built;\n\t\t\tbuilt.catch((err: unknown) => {\n\t\t\t\tsay(\n\t\t\t\t\t`could not build ${path.relative(process.cwd(), entry)}:\\n${errorText(err)}`,\n\t\t\t\t);\n\t\t\t\t// Let a later request try again, unless it started its own build.\n\t\t\t\tif (bundle === built) bundle = undefined;\n\t\t\t});\n\t\t}\n\t\treturn bundle;\n\t};\n\tconst engine = engineDir();\n\tengine.catch(() => {});\n\n\t// ── Audio: mixed once, by the export engine ──\n\tlet audio: AudioStatus = {\n\t\tstate: options.audio === false ? \"none\" : \"mixing\",\n\t};\n\tlet audioWav: Buffer | undefined;\n\n\tfunction broadcast(event: string, data: unknown) {\n\t\tfor (const client of sseClients) {\n\t\t\tclient.write(`event: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`);\n\t\t}\n\t}\n\n\tfunction setAudio(next: AudioStatus) {\n\t\taudio = next;\n\t\tbroadcast(\"audio\", audio);\n\t}\n\n\tfunction startAudio() {\n\t\tif (audio.state !== \"mixing\") return;\n\t\tif (!target.toVirtualMediaAsync && !target.toVirtualMedia) {\n\t\t\tsetAudio({ state: \"none\" });\n\t\t\treturn;\n\t\t}\n\t\tconst renderer = options.renderer ?? new HeadlessMediaRenderer();\n\t\t(async () => {\n\t\t\tconst vm = target.toVirtualMediaAsync\n\t\t\t\t? await target.toVirtualMediaAsync()\n\t\t\t\t: (target.toVirtualMedia as () => VirtualMediaData)();\n\t\t\treturn renderer.renderAudio(vm, { fps: meta.fps });\n\t\t})().then(\n\t\t\t({ channels, sampleRate }) => {\n\t\t\t\tconst left = channels[0] ?? new Float32Array(0);\n\t\t\t\tconst right = channels[1] ?? left;\n\t\t\t\tconst peaks = computePeaks(left, right, PEAK_BINS);\n\t\t\t\tif (left.length === 0 || Math.max(...peaks) < 0.001) {\n\t\t\t\t\tsetAudio({ state: \"none\" });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\taudioWav = encodeStereoWav([left, right], { sampleRate });\n\t\t\t\tsetAudio({\n\t\t\t\t\tstate: \"ready\",\n\t\t\t\t\tdurationSec: left.length / sampleRate,\n\t\t\t\t\tpeaks: peaks.map((p) => Math.round(p * 1000) / 1000),\n\t\t\t\t});\n\t\t\t},\n\t\t\t(err: unknown) => {\n\t\t\t\tsetAudio({\n\t\t\t\t\tstate: \"error\",\n\t\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t\t});\n\t\t\t},\n\t\t);\n\t}\n\n\tlet resolveClosed!: (reason: PreviewCloseReason) => void;\n\tconst closed = new Promise<PreviewCloseReason>((r) => {\n\t\tresolveClosed = r;\n\t});\n\tlet closing: Promise<void> | undefined;\n\tfunction close(reason: PreviewCloseReason = \"closed\"): Promise<void> {\n\t\tclosing ??= new Promise<void>((resolve) => {\n\t\t\tclearTimeout(idleTimer);\n\t\t\tif (reason === \"idle\") say(\"stopped: its tab was closed\");\n\t\t\tif (reason === \"replaced\") say(\"stopped: a newer preview took over\");\n\t\t\tfor (const client of sseClients) client.end();\n\t\t\tsseClients.clear();\n\t\t\tserver.close(() => {\n\t\t\t\tresolve();\n\t\t\t\tresolveClosed(reason);\n\t\t\t});\n\t\t\tserver.closeAllConnections();\n\t\t});\n\t\treturn closing;\n\t}\n\n\tconst server = http.createServer(async (req, res) => {\n\t\tconst url = new URL(req.url ?? \"/\", \"http://localhost\");\n\t\t// Cross-origin isolation: lets onnxruntime-web use SharedArrayBuffer and\n\t\t// threads for vision models (single-threaded otherwise). \"credentialless\"\n\t\t// keeps cross-origin assets such as the font stylesheet loading.\n\t\tres.setHeader(\"cross-origin-opener-policy\", \"same-origin\");\n\t\tres.setHeader(\"cross-origin-embedder-policy\", \"credentialless\");\n\t\ttry {\n\t\t\tif (req.method === \"GET\" && url.pathname === \"/\") {\n\t\t\t\tres.writeHead(200, {\n\t\t\t\t\t\"content-type\": \"text/html; charset=utf-8\",\n\t\t\t\t\t\"cache-control\": \"no-store\",\n\t\t\t\t});\n\t\t\t\tres.end(previewPage);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (req.method === \"GET\" && url.pathname === \"/meta\") {\n\t\t\t\tsendJson(res, 200, { ...meta, audio });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (req.method === \"GET\" && url.pathname.startsWith(`${ENGINE_URL}/`)) {\n\t\t\t\tconst dir = await engine;\n\t\t\t\tconst file = path.resolve(\n\t\t\t\t\tdir,\n\t\t\t\t\t`.${url.pathname.slice(ENGINE_URL.length)}`,\n\t\t\t\t);\n\t\t\t\tif (isInside(dir, file) && (await isFile(file))) {\n\t\t\t\t\tawait sendFile(req, res, file);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tsendJson(res, 404, { error: \"not found\" });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (req.method === \"GET\" && url.pathname === PLAYER_PATH) {\n\t\t\t\tlet code: string;\n\t\t\t\ttry {\n\t\t\t\t\tawait engine;\n\t\t\t\t\tcode = await playerBundle();\n\t\t\t\t} catch (err) {\n\t\t\t\t\t// Shown on the page: usually an error in the composition's code.\n\t\t\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tcode = `throw new Error(${JSON.stringify(`Could not build the preview:\\n${message}`)});`;\n\t\t\t\t}\n\t\t\t\tres.writeHead(200, {\n\t\t\t\t\t\"content-type\": \"text/javascript; charset=utf-8\",\n\t\t\t\t\t\"cache-control\": \"no-store\",\n\t\t\t\t});\n\t\t\t\tres.end(code);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// `fs.existsSync` / `statSync` in the page: a missing file is an\n\t\t\t// answer here, not a 404 the browser logs as an error.\n\t\t\tif (req.method === \"GET\" && url.pathname === \"/@framefields/stat\") {\n\t\t\t\tconst file = path.resolve(url.searchParams.get(\"path\") ?? \"\");\n\t\t\t\tlet size = -1;\n\t\t\t\tif (roots.some((r) => isInside(r, file))) {\n\t\t\t\t\tsize = await fs.promises.stat(file).then(\n\t\t\t\t\t\t(st) => (st.isFile() ? st.size : -1),\n\t\t\t\t\t\t() => -1,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tsendJson(res, 200, { exists: size >= 0, size: Math.max(0, size) });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (req.method === \"GET\" && url.pathname === \"/events\") {\n\t\t\t\tres.writeHead(200, {\n\t\t\t\t\t\"content-type\": \"text/event-stream\",\n\t\t\t\t\t\"cache-control\": \"no-store\",\n\t\t\t\t\tconnection: \"keep-alive\",\n\t\t\t\t});\n\t\t\t\t// Reconnect fast: a tab left open finds the next preview's\n\t\t\t\t// server within a second of it starting.\n\t\t\t\tres.write(\"retry: 1000\\n\");\n\t\t\t\tres.write(\n\t\t\t\t\t`event: hello\\ndata: ${JSON.stringify({ session: meta.session })}\\n\\n`,\n\t\t\t\t);\n\t\t\t\tsseClients.add(res);\n\t\t\t\tclearTimeout(idleTimer);\n\t\t\t\tif (!pageOpened) {\n\t\t\t\t\tpageOpened = true;\n\t\t\t\t\tsay(\n\t\t\t\t\t\tidleCloseMs === false\n\t\t\t\t\t\t\t? \"page opened\"\n\t\t\t\t\t\t\t: `page opened; the server stops ${Number((idleCloseMs / 1000).toFixed(1))} s after its last tab closes`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tonClient?.();\n\t\t\t\treq.on(\"close\", () => {\n\t\t\t\t\tsseClients.delete(res);\n\t\t\t\t\t// A reload reconnects within a second; a closed tab doesn't.\n\t\t\t\t\tif (sseClients.size === 0 && idleCloseMs !== false && !closing) {\n\t\t\t\t\t\tidleTimer = setTimeout(() => {\n\t\t\t\t\t\t\tif (sseClients.size === 0) void close(\"idle\");\n\t\t\t\t\t\t}, idleCloseMs);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// A newer preview of the same project wants this port. The custom\n\t\t\t// header keeps web pages from triggering it (it forces a CORS preflight).\n\t\t\tif (\n\t\t\t\treq.method === \"POST\" &&\n\t\t\t\turl.pathname === \"/takeover\" &&\n\t\t\t\treq.headers[TAKEOVER_HEADER] === \"1\"\n\t\t\t) {\n\t\t\t\tsendJson(res, 200, { ok: true });\n\t\t\t\tsetImmediate(() => void close(\"replaced\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Errors in the page, so they reach the terminal (and an agent)\n\t\t\t// without anyone looking at the browser. JSON only: other sites\n\t\t\t// can't send that without a preflight this server doesn't answer.\n\t\t\tif (\n\t\t\t\treq.method === \"POST\" &&\n\t\t\t\turl.pathname === \"/@framefields/log\" &&\n\t\t\t\treq.headers[\"content-type\"] === \"application/json\"\n\t\t\t) {\n\t\t\t\tconst body = await readBody(req, 16 * 1024);\n\t\t\t\tconst { message, level } = JSON.parse(body) as {\n\t\t\t\t\tmessage?: unknown;\n\t\t\t\t\tlevel?: unknown;\n\t\t\t\t};\n\t\t\t\tconst text = String(message ?? \"\").trim();\n\t\t\t\tif (text && !reported.has(text) && reported.size < 200) {\n\t\t\t\t\treported.add(text);\n\t\t\t\t\tsay(\n\t\t\t\t\t\t`${level === \"warning\" ? \"warning\" : \"error\"} in the page: ${text}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tres.writeHead(204).end();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (notes && url.pathname === \"/@framefields/notes\") {\n\t\t\t\tif (req.method === \"GET\") {\n\t\t\t\t\tsendJson(res, 200, await notes.list());\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (\n\t\t\t\t\treq.method === \"POST\" &&\n\t\t\t\t\treq.headers[\"content-type\"] === \"application/json\"\n\t\t\t\t) {\n\t\t\t\t\tconst body = JSON.parse(await readBody(req, 16 * 1024 * 1024)) as {\n\t\t\t\t\t\top?: string;\n\t\t\t\t\t\tid?: number;\n\t\t\t\t\t\tdone?: boolean;\n\t\t\t\t\t\ttext?: string;\n\t\t\t\t\t\tnote?: NewNote;\n\t\t\t\t\t};\n\t\t\t\t\tconst change = noteQueue.then(() =>\n\t\t\t\t\t\tchangeNote(notes, body, meta.fps, say),\n\t\t\t\t\t);\n\t\t\t\t\tnoteQueue = change.catch(() => {});\n\t\t\t\t\tconst note = await change;\n\t\t\t\t\tbroadcast(\"notes\", await notes.list());\n\t\t\t\t\tsendJson(res, note ? 200 : 404, note ?? { error: \"no such note\" });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (req.method === \"GET\" && url.pathname === \"/audio\") {\n\t\t\t\tif (audio.state !== \"ready\" || !audioWav) {\n\t\t\t\t\tsendJson(res, 404, { error: `audio is ${audio.state}` });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tres.writeHead(200, {\n\t\t\t\t\t\"content-type\": \"audio/wav\",\n\t\t\t\t\t\"content-length\": audioWav.length,\n\t\t\t\t\t\"cache-control\": \"no-store\",\n\t\t\t\t});\n\t\t\t\tres.end(audioWav);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Fonts referenced by family only resolve to assets/fonts/<Family>.ttf,\n\t\t\t// read from the project's folder, as the export engine does.\n\t\t\tconst fontMatch = /^\\/assets\\/fonts\\/([^/]+)$/.exec(url.pathname);\n\t\t\tif (fontMatch && (req.method === \"GET\" || req.method === \"HEAD\")) {\n\t\t\t\tconst file = path.join(\n\t\t\t\t\tprocess.cwd(),\n\t\t\t\t\t\"assets\",\n\t\t\t\t\t\"fonts\",\n\t\t\t\t\tdecodeURIComponent(fontMatch[1]),\n\t\t\t\t);\n\t\t\t\tif (await isFile(file)) {\n\t\t\t\t\tawait sendFile(req, res, file);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\t// The composition's files, by absolute path: what the program\n\t\t\t// references loads as-is.\n\t\t\tif (req.method === \"GET\" || req.method === \"HEAD\") {\n\t\t\t\t// The engine asks for local files under Vite's `/@fs/` prefix.\n\t\t\t\tconst file = path.resolve(\n\t\t\t\t\tdecodeURIComponent(url.pathname.replace(/^\\/@fs(?=\\/)/, \"\")),\n\t\t\t\t);\n\t\t\t\tif (roots.some((r) => isInside(r, file)) && (await isFile(file))) {\n\t\t\t\t\tawait sendFile(req, res, file);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\tsendJson(res, 404, { error: \"not found\" });\n\t\t} catch (err) {\n\t\t\tif (!res.headersSent) {\n\t\t\t\tsendJson(res, 500, {\n\t\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tres.end();\n\t\t\t}\n\t\t}\n\t});\n\n\tlet onClient: (() => void) | undefined;\n\tconst host = options.host ?? \"127.0.0.1\";\n\tconst port = options.port ?? stablePort();\n\tconst listen = (p: number) =>\n\t\tnew Promise<void>((resolve, reject) => {\n\t\t\tconst fail = (err: Error) => {\n\t\t\t\tserver.off(\"listening\", ok);\n\t\t\t\treject(err);\n\t\t\t};\n\t\t\tconst ok = () => {\n\t\t\t\tserver.off(\"error\", fail);\n\t\t\t\tresolve();\n\t\t\t};\n\t\t\tserver.once(\"error\", fail);\n\t\t\tserver.once(\"listening\", ok);\n\t\t\tserver.listen(p, host);\n\t\t});\n\ttry {\n\t\tawait listen(port);\n\t} catch (err) {\n\t\tif ((err as NodeJS.ErrnoException).code !== \"EADDRINUSE\") throw err;\n\t\t// Most likely the previous preview of this project: ask it to step aside.\n\t\tif (await requestTakeover(host, port)) {\n\t\t\tawait listen(port).catch(() => listen(0));\n\t\t} else if (options.port === undefined) {\n\t\t\tawait listen(0);\n\t\t} else {\n\t\t\tthrow err;\n\t\t}\n\t}\n\tconst { port: actualPort } = server.address() as AddressInfo;\n\tconst url = `http://${host.includes(\":\") ? `[${host}]` : host}:${actualPort}/`;\n\n\t// Build the page's bundle and mix the soundtrack while the browser opens.\n\tvoid playerBundle().catch(() => {});\n\tstartAudio();\n\tif (options.open === true) {\n\t\t// A tab from the last preview reconnects within ~1 s and reloads itself;\n\t\t// only open a new one when nobody comes back.\n\t\tconst reconnected = await new Promise<boolean>((resolve) => {\n\t\t\tconst timer = setTimeout(() => resolve(false), 2000);\n\t\t\tonClient = () => {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\tresolve(true);\n\t\t\t};\n\t\t\tif (sseClients.size > 0) onClient();\n\t\t});\n\t\tonClient = undefined;\n\t\tif (!reconnected) openBrowser(url);\n\t}\n\n\treturn { url, closed, close: () => close() };\n}\n\nasync function loadTarget(\n\tentry: string,\n\texportName: string,\n): Promise<PreviewTarget> {\n\tconst mod = (await import(pathToFileURL(entry).href)) as Record<\n\t\tstring,\n\t\tunknown\n\t>;\n\tconst exported = mod[exportName];\n\tif (exported === undefined) {\n\t\tthrow new Error(\n\t\t\t`[framefields] ${entry} has no export named \"${exportName}\" to preview.`,\n\t\t);\n\t}\n\treturn (\n\t\ttypeof exported === \"function\"\n\t\t\t? await (exported as () => unknown)()\n\t\t\t: exported\n\t) as PreviewTarget;\n}\n\n/**\n * The project holding `dir`: the nearest `.git`, else the nearest\n * `package.json`, else `dir` itself. A repo wins over a nested package, so a\n * monorepo's shared assets stay reachable.\n */\nfunction repoRoot(dir: string): string {\n\tlet project: string | undefined;\n\tfor (let d = dir; ; d = path.dirname(d)) {\n\t\tif (fs.existsSync(path.join(d, \".git\"))) return d;\n\t\tif (!project && fs.existsSync(path.join(d, \"package.json\"))) project = d;\n\t\tif (path.dirname(d) === d) return project ?? dir;\n\t}\n}\n\n/** Applies one change from the page and says what it was. */\nasync function changeNote(\n\tnotes: NoteStore,\n\tbody: {\n\t\top?: string;\n\t\tid?: number;\n\t\tdone?: boolean;\n\t\ttext?: string;\n\t\tnote?: NewNote;\n\t},\n\tfps: number,\n\tsay: (line: string) => void,\n) {\n\tif (body.op === \"add\" && body.note && String(body.note.text).trim()) {\n\t\tconst note = await notes.add(body.note);\n\t\tconst files = [\n\t\t\tnote.image && `frame with the spot marked: ${note.image}`,\n\t\t\t`all notes: ${notes.file}`,\n\t\t];\n\t\tsay(`${describeNote(note, fps)}\\n ${files.filter(Boolean).join(\" · \")}`);\n\t\treturn note;\n\t}\n\tconst id = Number(body.id);\n\tif (body.op === \"update\") {\n\t\tconst note = await notes.update(id, body);\n\t\tif (note && typeof body.done === \"boolean\")\n\t\t\tsay(`note ${id} marked ${note.done ? \"done\" : \"open\"}`);\n\t\treturn note;\n\t}\n\tif (body.op === \"remove\") {\n\t\tconst note = await notes.remove(id);\n\t\tif (note) say(`note ${id} deleted`);\n\t\treturn note;\n\t}\n\treturn undefined;\n}\n\nfunction errorText(err: unknown): string {\n\treturn err instanceof Error ? err.message : String(err);\n}\n\nfunction readBody(req: http.IncomingMessage, limit: number): Promise<string> {\n\treturn new Promise((resolve, reject) => {\n\t\tlet body = \"\";\n\t\treq.setEncoding(\"utf8\");\n\t\treq.on(\"data\", (chunk: string) => {\n\t\t\tbody += chunk;\n\t\t\tif (body.length > limit) {\n\t\t\t\treject(new Error(\"request body too large\"));\n\t\t\t\treq.destroy();\n\t\t\t}\n\t\t});\n\t\treq.on(\"end\", () => resolve(body));\n\t\treq.on(\"error\", reject);\n\t});\n}\n\nfunction isInside(root: string, file: string): boolean {\n\tconst rel = path.relative(root, file);\n\treturn !!rel && !rel.startsWith(\"..\") && !path.isAbsolute(rel);\n}\n\nasync function isFile(file: string): Promise<boolean> {\n\ttry {\n\t\treturn (await fs.promises.stat(file)).isFile();\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nconst CONTENT_TYPES: Record<string, string> = {\n\t\".png\": \"image/png\",\n\t\".jpg\": \"image/jpeg\",\n\t\".jpeg\": \"image/jpeg\",\n\t\".webp\": \"image/webp\",\n\t\".gif\": \"image/gif\",\n\t\".svg\": \"image/svg+xml\",\n\t\".mp4\": \"video/mp4\",\n\t\".mov\": \"video/quicktime\",\n\t\".webm\": \"video/webm\",\n\t\".mp3\": \"audio/mpeg\",\n\t\".wav\": \"audio/wav\",\n\t\".m4a\": \"audio/mp4\",\n\t\".ogg\": \"audio/ogg\",\n\t\".ttf\": \"font/ttf\",\n\t\".otf\": \"font/otf\",\n\t\".woff\": \"font/woff\",\n\t\".woff2\": \"font/woff2\",\n\t\".json\": \"application/json\",\n\t\".js\": \"text/javascript; charset=utf-8\",\n\t\".mjs\": \"text/javascript; charset=utf-8\",\n\t\".wasm\": \"application/wasm\",\n\t\".onnx\": \"application/octet-stream\",\n\t\".srt\": \"text/plain; charset=utf-8\",\n\t\".txt\": \"text/plain; charset=utf-8\",\n};\n\n/** Serves a file, with byte ranges so video can seek without loading it whole. */\nasync function sendFile(\n\treq: http.IncomingMessage,\n\tres: http.ServerResponse,\n\tfile: string,\n) {\n\tconst { size } = await fs.promises.stat(file);\n\tconst type =\n\t\tCONTENT_TYPES[path.extname(file).toLowerCase()] ??\n\t\t\"application/octet-stream\";\n\tconst range = /^bytes=(\\d*)-(\\d*)$/.exec(req.headers.range ?? \"\");\n\tlet start = 0;\n\tlet end = size - 1;\n\tif (range && (range[1] || range[2])) {\n\t\tif (range[1]) {\n\t\t\tstart = Number(range[1]);\n\t\t\tif (range[2]) end = Math.min(end, Number(range[2]));\n\t\t} else {\n\t\t\tstart = Math.max(0, size - Number(range[2]));\n\t\t}\n\t\tif (start > end || start >= size) {\n\t\t\tres.writeHead(416, { \"content-range\": `bytes */${size}` });\n\t\t\tres.end();\n\t\t\treturn;\n\t\t}\n\t}\n\tres.writeHead(range ? 206 : 200, {\n\t\t\"content-type\": type,\n\t\t\"content-length\": end - start + 1,\n\t\t\"accept-ranges\": \"bytes\",\n\t\t\"cache-control\": \"no-cache\",\n\t\t...(range && { \"content-range\": `bytes ${start}-${end}/${size}` }),\n\t});\n\tif (req.method === \"HEAD\") {\n\t\tres.end();\n\t\treturn;\n\t}\n\tfs.createReadStream(file, { start, end }).pipe(res);\n}\n\nasync function resolveMeta(\n\ttarget: PreviewTarget,\n\ttitle: string,\n\tsession: string,\n\tnotesDir: string | null,\n): Promise<PreviewMeta> {\n\tconst fps = target.fps ?? 24;\n\tconst durationMs =\n\t\ttarget.durationMs ?? (await target.computeDuration?.()) ?? 5000;\n\treturn {\n\t\tsession,\n\t\tnotesDir,\n\t\ttitle,\n\t\tfps,\n\t\tframeCount: Math.max(1, Math.round((durationMs / 1000) * fps)),\n\t\twidth: target.width ?? 1920,\n\t\theight: target.height ?? 1080,\n\t};\n}\n\n/** Same project, same port: 41000–41899, from the working directory. */\nfunction stablePort(): number {\n\tconst digest = createHash(\"sha1\").update(process.cwd()).digest();\n\treturn 41000 + (digest.readUInt16BE(0) % 900);\n}\n\n/** Asks a preview already on `port` to shut down; false if something else is there. */\nasync function requestTakeover(host: string, port: number): Promise<boolean> {\n\t// A fresh connection (no agent): a pooled keep-alive socket may point at a\n\t// server that is already gone.\n\tconst ok = await new Promise<boolean>((resolve) => {\n\t\tconst req = http.request(\n\t\t\t{\n\t\t\t\thost,\n\t\t\t\tport,\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tpath: \"/takeover\",\n\t\t\t\tagent: false,\n\t\t\t\theaders: { [TAKEOVER_HEADER]: \"1\" },\n\t\t\t\ttimeout: 1500,\n\t\t\t},\n\t\t\t(res) => {\n\t\t\t\tres.resume();\n\t\t\t\tresolve(res.statusCode === 200);\n\t\t\t},\n\t\t);\n\t\treq.on(\"timeout\", () => req.destroy());\n\t\treq.on(\"error\", () => resolve(false));\n\t\treq.end();\n\t});\n\t// Give it a moment to release the socket.\n\tif (ok) await new Promise((r) => setTimeout(r, 150));\n\treturn ok;\n}\n\n/** Max absolute amplitude of both channels per bin, 0..1. */\nfunction computePeaks(\n\tleft: Float32Array,\n\tright: Float32Array,\n\tbins: number,\n): number[] {\n\tconst peaks = new Array<number>(bins).fill(0);\n\tif (left.length === 0) return peaks;\n\tconst per = left.length / bins;\n\tfor (let b = 0; b < bins; b++) {\n\t\tconst start = Math.floor(b * per);\n\t\tconst end = Math.min(left.length, Math.floor((b + 1) * per));\n\t\tlet max = 0;\n\t\tfor (let i = start; i < end; i++) {\n\t\t\tconst v = Math.max(Math.abs(left[i] ?? 0), Math.abs(right[i] ?? 0));\n\t\t\tif (v > max) max = v;\n\t\t}\n\t\tpeaks[b] = Math.min(1, max);\n\t}\n\treturn peaks;\n}\n\nfunction sendJson(res: http.ServerResponse, status: number, body: unknown) {\n\tres.writeHead(status, {\n\t\t\"content-type\": \"application/json\",\n\t\t\"cache-control\": \"no-store\",\n\t});\n\tres.end(JSON.stringify(body));\n}\n\nfunction openBrowser(url: string) {\n\tconst [cmd, args] =\n\t\tprocess.platform === \"darwin\"\n\t\t\t? [\"open\", [url]]\n\t\t\t: process.platform === \"win32\"\n\t\t\t\t? [\"cmd\", [\"/c\", \"start\", \"\", url]]\n\t\t\t\t: [\"xdg-open\", [url]];\n\ttry {\n\t\tspawn(cmd, args, { stdio: \"ignore\", detached: true })\n\t\t\t.on(\"error\", () => {})\n\t\t\t.unref();\n\t} catch {}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"preview-DZTbEON9.mjs","names":["url","resolve"],"sources":["../src/preview/bundle.ts","../src/preview/notes.ts","../src/preview/page.ts","../src/preview/index.ts"],"sourcesContent":["/**\n * The preview page runs the project's composition code and renders it with\n * WebGPU. Two bundles make that work:\n *\n * - The engine (`buildEngine`): framefields and its browser player, built once\n * for the browser. Published packages ship it in `dist/preview-engine`;\n * running from source, it is built on first use.\n * - The project (`bundleProject`): only the user's own files, built on each\n * preview. Its `framefields` imports point at the engine's files, so the\n * composition and the player share one engine (signals, clock, caches).\n *\n * Node APIs that compositions use at build time are shimmed: `path`, `url`,\n * `os`, `import.meta.url` (each file keeps its own `file://` URL), and `fs`\n * reads, which fetch the file from the preview server. Absolute file paths\n * double as URLs there, so assets referenced by path load unchanged.\n * Node-only packages become stubs that throw when used.\n */\nimport fs from \"node:fs\";\nimport { createRequire, isBuiltin } from \"node:module\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\nimport type { BuildOptions, PluginBuild } from \"esbuild\";\n\nconst HERE = path.dirname(fileURLToPath(import.meta.url));\n// Spelled in two parts so this file survives its own rewrite.\nconst META_URL = [\"import\", \"meta\", \"url\"].join(\".\");\nconst require = createRequire(import.meta.url);\n\n/** URL prefix the server serves the engine's files under. */\nexport const ENGINE_URL = \"/@framefields/engine\";\n\n/** Public entry points a composition may import, and their sources. */\nconst ENTRIES: Record<string, string> = {\n\tframefields: \"index.ts\",\n\t\"framefields/effects\": \"effects/index.ts\",\n\t\"framefields/audio\": \"audio/index.ts\",\n\t\"framefields/signals\": \"signals/index.ts\",\n\t\"framefields/fonts\": \"fonts/index.ts\",\n};\n\n/** Node built-ins with a browser version (shim file names); others are stubbed. */\nconst SHIMS: Record<string, string> = {\n\tpath: \"path\",\n\t\"path/posix\": \"path\",\n\turl: \"url\",\n\tos: \"os\",\n\tfs: \"fs\",\n\t\"fs/promises\": \"fs-promises\",\n\tmodule: \"module\",\n};\n\n/** Packages that only work in Node; the browser never needs them to draw. */\nconst NODE_ONLY = new Set([\n\t\"sharp\",\n\t\"skia-canvas\",\n\t\"node-av\",\n\t\"onnxruntime-node\",\n\t\"@resvg/resvg-js\",\n\t\"@napi-rs/webcodecs\",\n\t\"@mediabunny/server\",\n\t\"@mediabunny/mp3-encoder\",\n\t\"webgpu\",\n\t\"pino\",\n\t\"pino-pretty\",\n\t\"dotenv\",\n\t\"esbuild\",\n\t\"@framefields/renderer\",\n\t\"@framefields/server-utils\",\n\t\"framefields/renderer\",\n\t\"framefields/preview\",\n]);\n\n// Reported as an ES module so named imports (`import { homedir } from \"os\"`)\n// read through the proxy instead of being copied off it.\nconst STUB = `\nconst stub = () => new Proxy(function () {}, {\n\tget: (_t, k) => (k === \"__esModule\" ? true : k === Symbol.toPrimitive ? () => \"\" : stub()),\n\tapply: () => stub(),\n\tconstruct: () => stub(),\n});\nmodule.exports = stub();\n`;\n\n/**\n * Builds framefields for the browser into `outDir`: one module per public entry,\n * the player and the shims, with shared code split into chunks (node renderers\n * load only when a composition uses them).\n */\nexport async function buildEngine(outDir: string): Promise<void> {\n\tconst { build } = await import(\"esbuild\");\n\tconst src = path.resolve(HERE, \"..\");\n\tconst shims = path.join(HERE, \"shims\");\n\tconst entryPoints: Record<string, string> = {\n\t\tplayer: path.join(HERE, \"player.ts\"),\n\t};\n\tfor (const [spec, file] of Object.entries(ENTRIES)) {\n\t\tentryPoints[engineName(spec)] = path.join(src, file);\n\t}\n\tfor (const name of new Set(Object.values(SHIMS))) {\n\t\tentryPoints[`shims/${name}`] = path.join(shims, `${name}.ts`);\n\t}\n\n\tawait fs.promises.rm(outDir, { recursive: true, force: true });\n\tawait build({\n\t\t...BROWSER,\n\t\tentryPoints,\n\t\toutdir: outDir,\n\t\toutExtension: { \".js\": \".mjs\" },\n\t\tsplitting: true,\n\t\tminify: true,\n\t\tconditions: [\"development\", \"browser\", \"import\"],\n\t\tdefine: { \"process.env.NODE_ENV\": '\"production\"', global: \"globalThis\" },\n\t\tinject: [path.join(shims, \"process.ts\")],\n\t\tplugins: [\n\t\t\t{\n\t\t\t\tname: \"framefields-engine\",\n\t\t\t\tsetup(b) {\n\t\t\t\t\tb.onResolve({ filter: /^framefields-preview:renderers$/ }, () => ({\n\t\t\t\t\t\tpath: builtinRenderersModule(),\n\t\t\t\t\t}));\n\t\t\t\t\tresolveNode(b, (name) => ({ path: path.join(shims, `${name}.ts`) }));\n\t\t\t\t},\n\t\t\t},\n\t\t],\n\t});\n}\n\nexport interface ProjectBundle {\n\tcode: string;\n\t/** The project's source files, to watch for changes. */\n\tinputs: string[];\n}\n\n/** Bundles the module that builds the composition, and what it imports. */\nexport async function bundleProject(options: {\n\t/** Module that exports the composition factory. */\n\tentry: string;\n\t/** Name of the export; a function returning a Composition (or one itself). */\n\texportName: string;\n\t/** Where cached vision models are, and where onnxruntime-web loads from. */\n\tmodelsDir: string;\n\tortBase: string;\n}): Promise<ProjectBundle> {\n\tconst { build } = await import(\"esbuild\");\n\tconst entry = path.resolve(options.entry);\n\tconst fromEngine = (file: string) => ({\n\t\tpath: `${ENGINE_URL}/${file}.mjs`,\n\t\texternal: true,\n\t});\n\tconst settings = { modelsDir: options.modelsDir, ortBase: options.ortBase };\n\n\tconst result = await build({\n\t\t...BROWSER,\n\t\tentryPoints: [\"framefields-preview:entry\"],\n\t\twrite: false,\n\t\tmetafile: true,\n\t\tsourcemap: \"inline\",\n\t\tplugins: [\n\t\t\t{\n\t\t\t\tname: \"framefields-project\",\n\t\t\t\tsetup(b) {\n\t\t\t\t\tb.onResolve({ filter: /^framefields-preview:entry$/ }, () => ({\n\t\t\t\t\t\tpath: \"entry\",\n\t\t\t\t\t\tnamespace: \"framefields-preview\",\n\t\t\t\t\t}));\n\t\t\t\t\tb.onLoad({ filter: /.*/, namespace: \"framefields-preview\" }, () => ({\n\t\t\t\t\t\tcontents: [\n\t\t\t\t\t\t\t`import { startPlayer } from ${JSON.stringify(`${ENGINE_URL}/player.mjs`)};`,\n\t\t\t\t\t\t\t`import * as mod from ${JSON.stringify(entry)};`,\n\t\t\t\t\t\t\t`startPlayer(mod, ${JSON.stringify(options.exportName)}, ${JSON.stringify(settings)});`,\n\t\t\t\t\t\t].join(\"\\n\"),\n\t\t\t\t\t\tresolveDir: path.dirname(entry),\n\t\t\t\t\t\tloader: \"js\",\n\t\t\t\t\t}));\n\t\t\t\t\tb.onResolve({ filter: /^\\/@framefields\\/engine\\// }, (args) => ({\n\t\t\t\t\t\tpath: args.path,\n\t\t\t\t\t\texternal: true,\n\t\t\t\t\t}));\n\t\t\t\t\tb.onResolve({ filter: /^framefields(\\/|$)/ }, (args) =>\n\t\t\t\t\t\tENTRIES[args.path] ? fromEngine(engineName(args.path)) : undefined,\n\t\t\t\t\t);\n\t\t\t\t\tresolveNode(b, (name) => fromEngine(`shims/${name}`));\n\t\t\t\t\trewriteMetaUrl(b);\n\t\t\t\t},\n\t\t\t},\n\t\t],\n\t});\n\n\tconst inputs = Object.keys(result.metafile?.inputs ?? {})\n\t\t.filter((p) => !p.includes(\":\") && !p.includes(\"node_modules\"))\n\t\t.map((p) => path.resolve(p));\n\treturn { code: result.outputFiles[0].text, inputs };\n}\n\nlet engineBuild: Promise<string> | undefined;\n\n/**\n * The prebuilt engine shipped beside a compiled bundle, if there is one. The\n * bundle lands either at the package's dist root or beside dist/preview,\n * depending on how the bundler splits it, so both are checked.\n */\nexport function prebuiltEngineDir(here: string): string | undefined {\n\treturn [here, path.join(here, \"..\")]\n\t\t.map((base) => path.join(base, \"preview-engine\"))\n\t\t.find((dir) => fs.existsSync(path.join(dir, \"player.mjs\")));\n}\n\n/**\n * The engine to serve: prebuilt in the package's dist, or, running from\n * source, built once per process (so engine edits show without a rebuild).\n */\nexport function engineDir(): Promise<string> {\n\t// From source HERE is src/preview, so this misses and the engine is built.\n\tconst prebuilt = prebuiltEngineDir(HERE);\n\tif (prebuilt) {\n\t\treturn Promise.resolve(prebuilt);\n\t}\n\tengineBuild ??= (async () => {\n\t\tconst dir = path.join(\n\t\t\tos.tmpdir(),\n\t\t\t`framefields-preview-engine-${process.pid}`,\n\t\t);\n\t\tawait buildEngine(dir);\n\t\treturn dir;\n\t})();\n\tengineBuild.catch(() => {\n\t\tengineBuild = undefined;\n\t});\n\treturn engineBuild;\n}\n\n/** Where vision models are cached, as `@framefields/vision` resolves it. */\nexport function visionModelsDir(): string {\n\treturn process.env.FRAMEFIELDS_MODELS_DIR\n\t\t? path.resolve(process.env.FRAMEFIELDS_MODELS_DIR)\n\t\t: path.join(os.homedir(), \".cache\", \"framefields\", \"models\");\n}\n\nconst ORT_WEB_VERSION = \"1.30.0\";\n\n/**\n * onnxruntime-web, for vision effects: the project's own install when it has\n * one (served from disk), otherwise a pinned CDN build. Loaded only when a\n * vision effect runs, so framefields doesn't ship its WebAssembly.\n */\nexport function onnxRuntimeWeb(fromDir: string): {\n\tbase: string;\n\tdir?: string;\n} {\n\tfor (const from of [fromDir, HERE]) {\n\t\ttry {\n\t\t\tconst main = createRequire(path.join(from, \"noop.js\")).resolve(\n\t\t\t\t\"onnxruntime-web\",\n\t\t\t);\n\t\t\tconst dir = path.dirname(main);\n\t\t\treturn { base: `/@fs${dir}/`, dir };\n\t\t} catch {}\n\t}\n\treturn {\n\t\tbase: `https://cdn.jsdelivr.net/npm/onnxruntime-web@${ORT_WEB_VERSION}/dist/`,\n\t};\n}\n\n// ── shared ──\n\nconst BROWSER: BuildOptions = {\n\tbundle: true,\n\tformat: \"esm\",\n\tplatform: \"browser\",\n\ttarget: \"es2022\",\n\tmainFields: [\"browser\", \"module\", \"main\"],\n\tlogLevel: \"silent\",\n};\n\nfunction engineName(spec: string): string {\n\treturn spec === \"framefields\"\n\t\t? \"framefields\"\n\t\t: spec.slice(\"framefields/\".length);\n}\n\n/** Node built-ins to shims, the rest of Node to stubs. */\nfunction resolveNode(\n\tb: PluginBuild,\n\tshim: (name: string) => { path: string; external?: boolean },\n): void {\n\tb.onResolve({ filter: /^(node:)?[a-z_/]+$/ }, (args) => {\n\t\tconst name = args.path.replace(/^node:/, \"\");\n\t\tif (SHIMS[name]) return shim(SHIMS[name]);\n\t\tif (args.path.startsWith(\"node:\") || isBuiltin(name))\n\t\t\treturn { path: name, namespace: \"framefields-stub\" };\n\t\treturn undefined;\n\t});\n\tb.onResolve({ filter: /.*/ }, (args) => {\n\t\tconst pkg = packageName(args.path);\n\t\tif (pkg && (NODE_ONLY.has(pkg) || NODE_ONLY.has(args.path)))\n\t\t\treturn { path: args.path, namespace: \"framefields-stub\" };\n\t\treturn undefined;\n\t});\n\tb.onLoad({ filter: /.*/, namespace: \"framefields-stub\" }, () => ({\n\t\tcontents: STUB,\n\t\tloader: \"js\",\n\t}));\n}\n\n/** Each project file keeps its own URL, as `fileURLToPath(import.meta.url)` expects. */\nfunction rewriteMetaUrl(b: PluginBuild): void {\n\tb.onLoad({ filter: /\\.[cm]?[jt]sx?$/ }, async (args) => {\n\t\tif (args.path.includes(`${path.sep}node_modules${path.sep}`))\n\t\t\treturn undefined;\n\t\tconst source = await fs.promises.readFile(args.path, \"utf8\");\n\t\tif (!source.includes(META_URL)) return undefined;\n\t\treturn {\n\t\t\tcontents: `const __framefieldsFileUrl = ${JSON.stringify(pathToFileURL(args.path).href)};\\n${source.replaceAll(META_URL, \"__framefieldsFileUrl\")}`,\n\t\t\tloader: loaderFor(args.path),\n\t\t};\n\t});\n}\n\n/** The generated list of node renderers (needed only to build the engine). */\nfunction builtinRenderersModule(): string {\n\tconst dist = require.resolve(\"@framefields/renderer\");\n\tconst file = path.join(\n\t\tpath.dirname(dist),\n\t\t\"..\",\n\t\t\"src\",\n\t\t\"generated\",\n\t\t\"node-renderers.ts\",\n\t);\n\tif (!fs.existsSync(file))\n\t\tthrow new Error(`[framefields] node renderer list not found at ${file}`);\n\treturn file;\n}\n\nfunction packageName(spec: string): string | undefined {\n\tif (spec.startsWith(\".\") || spec.startsWith(\"/\")) return undefined;\n\tconst parts = spec.split(\"/\");\n\treturn spec.startsWith(\"@\") ? parts.slice(0, 2).join(\"/\") : parts[0];\n}\n\nfunction loaderFor(file: string): \"ts\" | \"tsx\" | \"js\" | \"jsx\" {\n\tif (file.endsWith(\".tsx\")) return \"tsx\";\n\tif (file.endsWith(\".jsx\")) return \"jsx\";\n\treturn /\\.[cm]?ts$/.test(file) ? \"ts\" : \"js\";\n}\n","/**\n * Notes the user pins on the preview: a moment, a spot (or an area) on the\n * frame, and what should change there. They are kept as files so whoever\n * edits the film (a person, or an agent) can read them: `notes.json`, plus the\n * frame with the spot marked as `note-<id>.jpg`. The folder ignores itself in\n * git. `notes.json` is read on every change, so it can be edited by hand\n * (marking a note done, say) while the preview runs.\n */\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nexport interface PreviewNote {\n\tid: number;\n\tframe: number;\n\t/** Seconds into the film. */\n\ttime: number;\n\t/** The spot, as fractions of the frame's width and height (0..1). */\n\tx: number;\n\ty: number;\n\t/** Set when an area was marked: its size, from the spot at its top left. */\n\tw?: number;\n\th?: number;\n\ttext: string;\n\t/** The frame with the spot marked (absolute path), when it was captured. */\n\timage?: string;\n\tdone: boolean;\n\tcreated: string;\n}\n\nexport interface NewNote {\n\tframe: number;\n\ttime: number;\n\tx: number;\n\ty: number;\n\tw?: number;\n\th?: number;\n\ttext: string;\n\t/** JPEG as a base64 data URL. */\n\timage?: string;\n}\n\nexport class NoteStore {\n\treadonly file: string;\n\n\tconstructor(readonly dir: string) {\n\t\tthis.file = path.join(dir, \"notes.json\");\n\t}\n\n\tasync list(): Promise<PreviewNote[]> {\n\t\ttry {\n\t\t\tconst notes = JSON.parse(await fs.promises.readFile(this.file, \"utf8\"));\n\t\t\treturn Array.isArray(notes) ? notes : [];\n\t\t} catch {\n\t\t\treturn [];\n\t\t}\n\t}\n\n\tasync add(input: NewNote): Promise<PreviewNote> {\n\t\tconst notes = await this.list();\n\t\tconst id = notes.reduce((max, n) => Math.max(max, n.id), 0) + 1;\n\t\tconst note: PreviewNote = {\n\t\t\tid,\n\t\t\tframe: Math.max(0, Math.round(Number(input.frame) || 0)),\n\t\t\ttime: round(Number(input.time) || 0, 3),\n\t\t\tx: round(clamp01(input.x), 4),\n\t\t\ty: round(clamp01(input.y), 4),\n\t\t\t...(input.w !== undefined &&\n\t\t\t\tinput.h !== undefined && {\n\t\t\t\t\tw: round(clamp01(input.w), 4),\n\t\t\t\t\th: round(clamp01(input.h), 4),\n\t\t\t\t}),\n\t\t\ttext: String(input.text ?? \"\").trim(),\n\t\t\tdone: false,\n\t\t\tcreated: new Date().toISOString(),\n\t\t};\n\t\tawait this.ensureDir();\n\t\tconst jpeg = /^data:image\\/jpeg;base64,(.+)$/.exec(input.image ?? \"\");\n\t\tif (jpeg) {\n\t\t\tnote.image = path.join(this.dir, `note-${id}.jpg`);\n\t\t\tawait fs.promises.writeFile(note.image, Buffer.from(jpeg[1], \"base64\"));\n\t\t}\n\t\tawait this.save([...notes, note]);\n\t\treturn note;\n\t}\n\n\tasync update(\n\t\tid: number,\n\t\tpatch: { done?: boolean; text?: string },\n\t): Promise<PreviewNote | undefined> {\n\t\tconst notes = await this.list();\n\t\tconst note = notes.find((n) => n.id === id);\n\t\tif (!note) return undefined;\n\t\tif (typeof patch.done === \"boolean\") note.done = patch.done;\n\t\tif (typeof patch.text === \"string\" && patch.text.trim())\n\t\t\tnote.text = patch.text.trim();\n\t\tawait this.save(notes);\n\t\treturn note;\n\t}\n\n\tasync remove(id: number): Promise<PreviewNote | undefined> {\n\t\tconst notes = await this.list();\n\t\tconst note = notes.find((n) => n.id === id);\n\t\tif (!note) return undefined;\n\t\tif (note.image) await fs.promises.rm(note.image, { force: true });\n\t\tawait this.save(notes.filter((n) => n !== note));\n\t\treturn note;\n\t}\n\n\tprivate async save(notes: PreviewNote[]): Promise<void> {\n\t\tawait this.ensureDir();\n\t\tconst tmp = `${this.file}.${process.pid}.tmp`;\n\t\tawait fs.promises.writeFile(tmp, `${JSON.stringify(notes, null, \"\\t\")}\\n`);\n\t\tawait fs.promises.rename(tmp, this.file);\n\t}\n\n\tprivate async ensureDir(): Promise<void> {\n\t\tawait fs.promises.mkdir(this.dir, { recursive: true });\n\t\t// Notes are working material, not part of the project.\n\t\tconst ignore = path.join(path.dirname(this.dir), \".gitignore\");\n\t\tif (!fs.existsSync(ignore)) await fs.promises.writeFile(ignore, \"*\\n\");\n\t}\n}\n\n/** One line for a terminal (or an agent reading one). */\nexport function describeNote(note: PreviewNote, fps: number): string {\n\tconst pct = (v: number) => `${Math.round(v * 100)}%`;\n\tconst where =\n\t\tnote.w !== undefined && note.h !== undefined\n\t\t\t? `area ${pct(note.x)},${pct(note.y)} to ${pct(note.x + note.w)},${pct(note.y + note.h)}`\n\t\t\t: `spot ${pct(note.x)},${pct(note.y)}`;\n\treturn `note ${note.id} at ${timecode(note.frame, fps)} (frame ${note.frame}), ${where}: ${note.text}`;\n}\n\nfunction timecode(frame: number, fps: number): string {\n\tconst s = frame / fps;\n\tconst m = Math.floor(s / 60);\n\treturn `${m}:${(s - m * 60).toFixed(2).padStart(5, \"0\")}`;\n}\n\nfunction clamp01(v: unknown): number {\n\tconst n = Number(v);\n\treturn Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : 0;\n}\n\nfunction round(v: number, digits: number): number {\n\treturn Number(v.toFixed(digits));\n}\n","/**\n * Single-file preview UI served by `startPreview`. No framework, no build step.\n * The page script avoids template literals so it can live inside this one.\n */\nexport const previewPage = /* html */ `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>framefields preview</title>\n<link rel=\"icon\" href=\"data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 512 512%22%3E%3Cg fill=%22none%22 stroke=%22%233B82F6%22 stroke-linecap=%22round%22%3E%3Cpath d=%22M184 56H86q-30 0-30 30v98M328 56h98q30 0 30 30v98M184 456H86q-30 0-30-30v-98M328 456h98q30 0 30-30v-98%22 stroke-width=%2234%22/%3E%3Cpath d=%22M176 182v148m0-148c0 74 60 74 100 74%22 stroke-width=%2228%22/%3E%3C/g%3E%3Cg fill=%22%233B82F6%22%3E%3Ccircle cx=%22176%22 cy=%22172%22 r=%2232%22/%3E%3Ccircle cx=%22176%22 cy=%22340%22 r=%2232%22/%3E%3Cpath d=%22M284 200l78 56-78 56z%22 stroke=%22%233B82F6%22 stroke-width=%2234%22 stroke-linejoin=%22round%22/%3E%3C/g%3E%3C/svg%3E\">\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap\" rel=\"stylesheet\">\n<style>\n :root {\n --bg: #0e0f11; --surface: #16171a; --surface-2: #1d1e22; --surface-3: #26282d;\n --line: #2a2c31; --line-2: #3a3c43;\n --ink: #ededf0; --ink-2: #a6a8b0; --ink-3: #74777f;\n --stage: #060607;\n --primary-bg: #ededf0; --primary-ink: #0e0f11;\n --brand: #3b82f6; --focus: #5eaefc; --ok: #4fbf7a; --bad: #f27d8d;\n --wave: #4b4e57;\n --radius: 10px;\n --font: \"Inter\", ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", sans-serif;\n color-scheme: dark;\n }\n @media (prefers-color-scheme: light) {\n :root {\n --bg: #f5f5f6; --surface: #ffffff; --surface-2: #f0f0f2; --surface-3: #e6e6e9;\n --line: #e2e2e6; --line-2: #cfcfd5;\n --ink: #15161a; --ink-2: #53555d; --ink-3: #85878f;\n --stage: #0b0b0c;\n --primary-bg: #15161a; --primary-ink: #ffffff;\n --brand: #2563eb; --focus: #1c74cc; --ok: #23874a; --bad: #cf4057;\n --wave: #c2c3ca;\n color-scheme: light;\n }\n }\n * { box-sizing: border-box; }\n html, body { height: 100%; margin: 0; overflow: hidden; background: var(--bg); color: var(--ink); }\n body {\n display: flex; flex-direction: column; height: 100dvh;\n font: 13.5px/1.45 var(--font); -webkit-font-smoothing: antialiased;\n font-feature-settings: \"cv11\", \"ss03\";\n }\n button, input { font: inherit; color: inherit; }\n button { cursor: pointer; }\n button:focus-visible, input:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }\n .tnum { font-variant-numeric: tabular-nums; }\n .grow { flex: 1; }\n .spinner {\n width: 12px; height: 12px; border-radius: 50%; flex: none;\n border: 1.5px solid var(--line-2); border-top-color: var(--ink); animation: spin .8s linear infinite;\n }\n @keyframes spin { to { transform: rotate(360deg); } }\n .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }\n\n /* ── Top bar ── */\n .top { flex: none; display: flex; align-items: center; gap: 10px; padding: 0 20px; height: 50px; border-bottom: 1px solid var(--line); min-width: 0; }\n .logo { width: 22px; height: 22px; flex: none; color: var(--brand); }\n .sep { width: 1px; height: 18px; background: var(--line-2); flex: none; margin: 0 2px; }\n .top h1 { font-size: 14px; font-weight: 600; margin: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n .spec { color: var(--ink-3); font-size: 12px; white-space: nowrap; }\n @media (max-width: 640px) { .spec { display: none; } }\n .chip {\n display: inline-flex; align-items: center; gap: 8px; height: 26px; padding: 0 10px; white-space: nowrap;\n border: 1px solid var(--line); border-radius: 999px; color: var(--ink-2); font-size: 12px; background: var(--surface);\n }\n .chip[data-state=\"ready\"] .dot { color: var(--ok); }\n .chip[data-state=\"none\"] .dot, .chip[data-state=\"offline\"] .dot { color: var(--ink-3); }\n .chip[data-state=\"error\"] { color: var(--bad); }\n\n /* ── Player: one screen, no page scroll; the picture takes the space left ── */\n .player {\n flex: 1; min-height: 0; display: flex; flex-direction: column; gap: 10px;\n width: 100%; max-width: 1600px; margin: 0 auto; padding: 16px 20px 20px;\n }\n @media (max-width: 640px) { .player { padding: 12px; } }\n .stage-wrap { flex: 1 1 0; min-height: 96px; container-type: size; display: grid; place-items: center; }\n .stage {\n position: relative; background: var(--stage); border-radius: var(--radius); overflow: hidden;\n box-shadow: 0 0 0 1px var(--line); user-select: none; -webkit-user-select: none;\n aspect-ratio: var(--ar, 16 / 9); width: min(100cqw, calc(100cqh * var(--ar-n, 1.7778)));\n }\n .stage canvas { display: block; width: 100%; height: 100%; }\n .stage-msg {\n position: absolute; inset: 0; display: none; place-items: center; padding: 24px; text-align: center;\n color: #fff; background: rgb(0 0 0 / .72); font-size: 13px; line-height: 1.5; white-space: pre-wrap; overflow: auto;\n }\n .stage-msg.on { display: grid; }\n /* Buffering: a centred spinning arc over the picture, like YouTube's. It\n fades in after a beat so brief waits don't flash it. */\n .buffering {\n position: absolute; left: 50%; top: 50%; width: clamp(36px, 9cqmin, 64px); aspect-ratio: 1;\n transform: translate(-50%, -50%); pointer-events: none; opacity: 0; visibility: hidden;\n transition: opacity .15s, visibility 0s .15s; filter: drop-shadow(0 0 3px rgb(0 0 0 / .45));\n }\n .buffering.on { opacity: 1; visibility: visible; transition: opacity .2s .25s, visibility 0s .25s; }\n .buffering svg { width: 100%; height: 100%; animation: yt-rotate 1.4s linear infinite; }\n .buffering circle {\n fill: none; stroke: #fff; stroke-width: 3.6; stroke-linecap: round;\n stroke-dasharray: 1 150; stroke-dashoffset: 0; animation: yt-dash 1.4s ease-in-out infinite;\n }\n @keyframes yt-rotate { to { transform: rotate(360deg); } }\n @keyframes yt-dash {\n 0% { stroke-dasharray: 1 150; stroke-dashoffset: 0; }\n 50% { stroke-dasharray: 90 150; stroke-dashoffset: -35; }\n 100% { stroke-dasharray: 90 150; stroke-dashoffset: -124; }\n }\n @media (prefers-reduced-motion: reduce) { .buffering circle, .play-btn .spin circle { animation: none; stroke-dasharray: 70 150; } }\n\n /* ── Transport ── */\n .transport { flex: none; display: flex; align-items: center; gap: 4px; flex-wrap: wrap; row-gap: 6px; }\n .icon-btn {\n width: 32px; height: 32px; display: inline-grid; place-items: center; border-radius: 8px;\n border: 0; background: transparent; color: var(--ink-2); flex: none;\n }\n .icon-btn:hover:not(:disabled) { background: var(--surface-2); color: var(--ink); }\n .icon-btn:disabled { opacity: .35; cursor: default; }\n .icon-btn svg { width: 16px; height: 16px; }\n .play-btn {\n width: 36px; height: 36px; border-radius: 50%; border: 0; display: inline-grid; place-items: center; flex: none;\n background: var(--primary-bg); color: var(--primary-ink); margin: 0 2px;\n }\n .play-btn:hover:not(:disabled) { filter: brightness(.9); }\n .play-btn:disabled { opacity: .4; cursor: default; }\n .play-btn svg { width: 14px; height: 14px; }\n .play-btn .spin { display: none; width: 20px; height: 20px; animation: yt-rotate 1.4s linear infinite; }\n .play-btn .spin circle {\n fill: none; stroke: currentColor; stroke-width: 5; stroke-linecap: round;\n stroke-dasharray: 1 150; animation: yt-dash 1.4s ease-in-out infinite;\n }\n .play-btn.loading .spin { display: block; }\n .play-btn.loading svg:not(.spin) { display: none !important; }\n .timecode { display: flex; align-items: baseline; gap: 5px; margin-left: 8px; font-size: 13px; white-space: nowrap; }\n .timecode .dim { color: var(--ink-3); }\n .timecode .frame { color: var(--ink-3); font-size: 11.5px; margin-left: 6px; }\n .fps { margin-left: 10px; color: var(--ink-3); font-size: 11.5px; white-space: nowrap; }\n input[type=\"range\"].slider { -webkit-appearance: none; appearance: none; width: 72px; height: 4px; border-radius: 2px; background: var(--line-2); margin: 0 8px 0 0; }\n input[type=\"range\"].slider::-webkit-slider-thumb { -webkit-appearance: none; width: 12px; height: 12px; border-radius: 50%; background: var(--ink); border: 0; }\n input[type=\"range\"].slider::-moz-range-thumb { width: 12px; height: 12px; border-radius: 50%; background: var(--ink); border: 0; }\n input[type=\"range\"].slider:disabled { opacity: .35; }\n\n /* ── Timeline ── */\n .timeline {\n flex: none; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface);\n padding: 0 14px; user-select: none; -webkit-user-select: none; touch-action: none; cursor: pointer;\n }\n .tl-inner { position: relative; }\n .ruler { position: relative; height: 22px; border-bottom: 1px solid var(--line); }\n .tick { position: absolute; bottom: 0; width: 1px; height: 4px; background: var(--line-2); }\n .tick.major { height: 8px; background: var(--ink-3); }\n .tick span { position: absolute; bottom: 9px; left: 4px; font-size: 10.5px; line-height: 1; color: var(--ink-3); white-space: nowrap; font-variant-numeric: tabular-nums; }\n .wave-lane { position: relative; height: 48px; }\n @media (max-height: 600px) { .wave-lane { height: 30px; } }\n .wave-lane canvas { position: absolute; inset: 0; width: 100%; height: 100%; }\n .wave-state { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: 8px; color: var(--ink-3); font-size: 12px; }\n .wave-state.shimmer {\n background: linear-gradient(90deg, transparent 0%, color-mix(in srgb, var(--ink) 6%, transparent) 50%, transparent 100%);\n background-size: 200% 100%; animation: shimmer 1.4s linear infinite;\n }\n @keyframes shimmer { from { background-position: 100% 0; } to { background-position: -100% 0; } }\n .playhead { position: absolute; top: 0; bottom: 0; width: 0; pointer-events: none; z-index: 3; }\n .playhead::before { content: \"\"; position: absolute; top: 0; bottom: 0; left: -0.5px; width: 1px; background: var(--ink); }\n .playhead::after { content: \"\"; position: absolute; top: 0; left: -5px; width: 10px; height: 11px; background: var(--ink); clip-path: polygon(0 0, 100% 0, 100% 55%, 50% 100%, 0 55%); }\n .hover-line { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--line-2); display: none; pointer-events: none; }\n .hover-time {\n position: absolute; top: -28px; transform: translateX(-50%); display: none; pointer-events: none;\n font-size: 11px; line-height: 1; background: var(--surface-3); color: var(--ink); padding: 5px 7px; border-radius: 5px;\n white-space: nowrap; z-index: 4; font-variant-numeric: tabular-nums;\n }\n /* ── Notes: pinned to a spot (or an area) and a moment ── */\n .stage.can-note { cursor: crosshair; }\n .overlay { position: absolute; inset: 0; pointer-events: none; }\n .pin {\n position: absolute; width: 22px; height: 22px; margin: -11px 0 0 -11px; padding: 0; border-radius: 50%;\n display: grid; place-items: center; pointer-events: auto; cursor: pointer;\n background: var(--brand); color: #fff; font-size: 11px; font-weight: 600; line-height: 1;\n border: 2px solid #fff; box-shadow: 0 1px 5px rgb(0 0 0 / .55); transition: transform .12s;\n }\n .pin:hover, .pin.active { transform: scale(1.18); }\n .pin.done { background: #6b6e76; }\n .pin.draft { animation: pin-pulse 1.5s ease-out infinite; }\n .pin svg { width: 11px; height: 11px; }\n @keyframes pin-pulse {\n from { box-shadow: 0 0 0 0 rgb(59 130 246 / .6), 0 1px 5px rgb(0 0 0 / .55); }\n to { box-shadow: 0 0 0 14px rgb(59 130 246 / 0), 0 1px 5px rgb(0 0 0 / .55); }\n }\n .area {\n position: absolute; border: 1.5px solid var(--brand); border-radius: 3px;\n background: rgb(59 130 246 / .12); box-shadow: 0 0 0 1px rgb(0 0 0 / .35);\n }\n .area.done { border-color: #9a9da5; background: rgb(255 255 255 / .05); }\n .marquee { position: absolute; display: none; border: 1.5px dashed #fff; background: rgb(59 130 246 / .16); border-radius: 3px; pointer-events: none; }\n .stage-hint, .toast {\n position: absolute; left: 50%; transform: translateX(-50%); pointer-events: none; white-space: nowrap;\n padding: 6px 11px; border-radius: 999px; font-size: 12px; color: #fff; background: rgb(0 0 0 / .62);\n backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); opacity: 0; transition: opacity .15s;\n }\n .stage-hint { top: 12px; }\n .stage.can-note.hint:hover .stage-hint { opacity: 1; }\n .toast { bottom: 14px; }\n .toast.on { opacity: 1; }\n\n .pop {\n position: fixed; z-index: 20; width: 280px; display: none; padding: 10px 10px 8px;\n background: var(--surface); border: 1px solid var(--line-2); border-radius: 12px;\n box-shadow: 0 12px 32px rgb(0 0 0 / .35);\n }\n .pop.on { display: block; }\n .pop-head { display: flex; align-items: center; gap: 8px; font-size: 11.5px; color: var(--ink-3); margin-bottom: 6px; }\n .badge {\n min-width: 18px; height: 18px; padding: 0 5px; border-radius: 9px; display: inline-grid; place-items: center; flex: none;\n background: var(--brand); color: #fff; font-size: 10.5px; font-weight: 600;\n }\n .badge.done { background: var(--ink-3); }\n .pop textarea {\n display: block; width: 100%; min-height: 54px; max-height: 180px; resize: none; padding: 2px 2px 0;\n border: 0; outline: none; background: transparent; color: var(--ink); font: inherit; line-height: 1.45;\n }\n .pop textarea::placeholder { color: var(--ink-3); }\n .pop-text { white-space: pre-wrap; overflow-wrap: anywhere; padding: 0 2px; max-height: 200px; overflow: auto; }\n .pop-foot { display: flex; align-items: center; gap: 6px; margin-top: 8px; font-size: 11.5px; color: var(--ink-3); }\n .pop-foot .err { color: var(--bad); }\n .btn {\n height: 26px; padding: 0 10px; border-radius: 7px; border: 0; font-size: 12px; font-weight: 500;\n display: inline-flex; align-items: center; gap: 6px; background: transparent; color: var(--ink-2); white-space: nowrap;\n }\n .btn:hover:not(:disabled) { background: var(--surface-2); color: var(--ink); }\n .btn.primary { background: var(--primary-bg); color: var(--primary-ink); }\n .btn.primary:hover:not(:disabled) { background: var(--primary-bg); filter: brightness(.9); }\n .btn.danger:hover:not(:disabled) { color: var(--bad); }\n .btn:disabled { opacity: .4; cursor: default; }\n .btn svg { width: 13px; height: 13px; }\n .x-btn { width: 22px; height: 22px; padding: 0; justify-content: center; margin-right: -4px; }\n\n .transport { position: relative; }\n .notes-btn { height: 32px; padding: 0 9px 0 8px; margin-right: 6px; font-size: 12.5px; }\n .notes-btn svg { width: 15px; height: 15px; }\n .notes-btn .badge { background: var(--surface-3); color: var(--ink-2); }\n .notes-btn .badge.open { background: var(--brand); color: #fff; }\n .notes-panel {\n position: absolute; right: 0; bottom: calc(100% + 8px); z-index: 15; width: min(360px, calc(100vw - 24px));\n max-height: min(440px, 62vh); display: none; flex-direction: column; overflow: hidden;\n background: var(--surface); border: 1px solid var(--line-2); border-radius: 12px; box-shadow: 0 12px 32px rgb(0 0 0 / .35);\n }\n .notes-panel.on { display: flex; }\n .np-head { flex: none; display: flex; align-items: center; gap: 8px; padding: 8px 8px 8px 14px; border-bottom: 1px solid var(--line); }\n .np-head h2 { margin: 0; font-size: 13px; font-weight: 600; }\n .np-list { overflow: auto; padding: 4px; }\n .np-empty { padding: 22px 18px; text-align: center; color: var(--ink-3); font-size: 12.5px; }\n .np-foot { flex: none; padding: 8px 14px; border-top: 1px solid var(--line); color: var(--ink-3); font-size: 11.5px; overflow-wrap: anywhere; }\n .note-row {\n display: grid; grid-template-columns: auto 1fr auto; gap: 10px; align-items: start; padding: 8px 8px 8px 10px;\n border-radius: 8px; cursor: pointer;\n }\n .note-row:hover { background: var(--surface-2); }\n .note-row .badge { margin-top: 1px; }\n .note-row .when { font-size: 11.5px; color: var(--ink-3); }\n .note-row .text { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; overflow-wrap: anywhere; }\n .note-row.done .text { color: var(--ink-3); text-decoration: line-through; }\n .check {\n width: 20px; height: 20px; border-radius: 50%; padding: 0; display: grid; place-items: center; margin-top: 1px;\n border: 1.5px solid var(--line-2); background: transparent; color: transparent;\n }\n .check:hover { border-color: var(--ok); color: var(--ok); }\n .check.on { background: var(--ok); border-color: var(--ok); color: #fff; }\n .check svg { width: 11px; height: 11px; }\n\n .note-marks { position: absolute; left: 0; right: 0; top: 22px; bottom: 0; pointer-events: none; z-index: 2; }\n .note-mark {\n position: absolute; top: 5px; width: 14px; height: 14px; margin-left: -7px; padding: 0; border: 0; border-radius: 50%;\n display: grid; place-items: center; pointer-events: auto; cursor: pointer;\n background: var(--brand); color: #fff; font-size: 8.5px; font-weight: 700; box-shadow: 0 0 0 2px var(--surface);\n }\n .note-mark::after { content: \"\"; position: absolute; top: 14px; left: 6.5px; width: 1px; height: 30px; background: var(--brand); opacity: .45; }\n .note-mark.done { background: var(--ink-3); }\n .note-mark.stacked::after { display: none; }\n .note-mark.done::after { background: var(--ink-3); }\n .note-mark:hover { transform: scale(1.2); }\n</style>\n</head>\n<body>\n<header class=\"top\">\n <svg class=\"logo\" viewBox=\"0 0 512 512\" aria-label=\"framefields\" role=\"img\"><g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\"><path d=\"M184 56H86q-30 0-30 30v98M328 56h98q30 0 30 30v98M184 456H86q-30 0-30-30v-98M328 456h98q30 0 30-30v-98\" stroke-width=\"34\" stroke-linejoin=\"round\"/><path d=\"M176 182v148m0-148c0 74 60 74 100 74\" stroke-width=\"28\"/></g><g fill=\"currentColor\"><circle cx=\"176\" cy=\"172\" r=\"32\"/><circle cx=\"176\" cy=\"340\" r=\"32\"/><path d=\"M284 200l78 56-78 56z\" stroke=\"currentColor\" stroke-width=\"34\" stroke-linejoin=\"round\"/></g></svg>\n <span class=\"sep\" aria-hidden=\"true\"></span>\n <h1 id=\"title\">framefields preview</h1>\n <span class=\"grow\"></span>\n <span class=\"spec tnum\" id=\"spec\"></span>\n <span class=\"chip\" id=\"chip\" data-state=\"mixing\" role=\"status\" aria-live=\"polite\"><span class=\"spinner\"></span><span>Mixing audio</span></span>\n</header>\n\n<main class=\"player\" aria-label=\"Player\">\n <div class=\"stage-wrap\">\n <div class=\"stage\" id=\"stage\">\n <canvas id=\"canvas\" aria-label=\"Rendered frame\"></canvas>\n <div class=\"stage-msg\" id=\"stageMsg\" role=\"alert\"></div>\n <div class=\"overlay\" id=\"pins\"></div>\n <div class=\"marquee\" id=\"marquee\"></div>\n <div class=\"stage-hint\" id=\"stageHint\">Click to pin a note · drag to mark an area</div>\n <div class=\"toast\" id=\"toast\" role=\"status\" aria-live=\"polite\"></div>\n <div class=\"buffering\" id=\"buffering\" role=\"status\" aria-label=\"Loading\"><svg viewBox=\"0 0 50 50\"><circle cx=\"25\" cy=\"25\" r=\"20\"/></svg></div>\n </div>\n </div>\n\n <div class=\"transport\">\n <button class=\"icon-btn\" id=\"prev\" title=\"Previous frame\" aria-label=\"Previous frame\">\n <svg viewBox=\"0 0 16 16\" fill=\"currentColor\"><path d=\"M3 3h1.6v10H3zM13 3.6v8.8a.6.6 0 0 1-.92.5L5.8 8.5a.6.6 0 0 1 0-1l6.28-4.4a.6.6 0 0 1 .92.5z\"/></svg>\n </button>\n <button class=\"play-btn\" id=\"play\" title=\"Play\" aria-label=\"Play\" disabled>\n <svg id=\"playIcon\" viewBox=\"0 0 16 16\" fill=\"currentColor\"><path d=\"M4.5 2.9v10.2a.7.7 0 0 0 1.06.6l8.1-5.1a.7.7 0 0 0 0-1.2l-8.1-5.1a.7.7 0 0 0-1.06.6z\"/></svg>\n <svg class=\"spin\" viewBox=\"0 0 50 50\" aria-hidden=\"true\"><circle cx=\"25\" cy=\"25\" r=\"18\"/></svg>\n <svg id=\"pauseIcon\" viewBox=\"0 0 16 16\" fill=\"currentColor\" style=\"display:none\"><rect x=\"3.5\" y=\"2.5\" width=\"3.2\" height=\"11\" rx=\".8\"/><rect x=\"9.3\" y=\"2.5\" width=\"3.2\" height=\"11\" rx=\".8\"/></svg>\n </button>\n <button class=\"icon-btn\" id=\"next\" title=\"Next frame\" aria-label=\"Next frame\">\n <svg viewBox=\"0 0 16 16\" fill=\"currentColor\"><path d=\"M11.4 3H13v10h-1.6zM3 3.6v8.8a.6.6 0 0 0 .92.5l6.28-4.4a.6.6 0 0 0 0-1L3.92 3.1A.6.6 0 0 0 3 3.6z\"/></svg>\n </button>\n <div class=\"timecode tnum\">\n <span id=\"tcNow\">0:00.00</span><span class=\"dim\">/</span><span class=\"dim\" id=\"tcDur\">0:00.00</span>\n <span class=\"frame\" id=\"tcFrame\">frame 0</span>\n </div>\n <span class=\"fps tnum\" id=\"fps\" role=\"status\" hidden></span>\n <span class=\"grow\"></span>\n <button class=\"btn notes-btn\" id=\"notesBtn\" title=\"Notes\" aria-haspopup=\"dialog\" aria-expanded=\"false\" hidden>\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linejoin=\"round\"><path d=\"M3 2.75h10a1.25 1.25 0 0 1 1.25 1.25v6.5A1.25 1.25 0 0 1 13 11.75H7.5L4.5 14v-2.25H3A1.25 1.25 0 0 1 1.75 10.5V4A1.25 1.25 0 0 1 3 2.75z\"/></svg>\n Notes <span class=\"badge\" id=\"notesCount\">0</span>\n </button>\n <div class=\"notes-panel\" id=\"notesPanel\" role=\"dialog\" aria-label=\"Notes\">\n <div class=\"np-head\">\n <h2>Notes</h2><span class=\"grow\"></span>\n <button class=\"btn\" id=\"notesCopy\" title=\"Copy the open notes, to paste into a chat\">\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"><rect x=\"5.25\" y=\"5.25\" width=\"8\" height=\"8\" rx=\"1.5\"/><path d=\"M10.75 3.5V3.25a1.5 1.5 0 0 0-1.5-1.5h-5.5a1.5 1.5 0 0 0-1.5 1.5v5.5a1.5 1.5 0 0 0 1.5 1.5H4\"/></svg>\n <span id=\"notesCopyLabel\">Copy</span>\n </button>\n <button class=\"btn x-btn\" id=\"notesClose\" title=\"Close\" aria-label=\"Close\"><svg viewBox=\"0 0 16 16\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"><path d=\"M4 4l8 8M12 4l-8 8\"/></svg></button>\n </div>\n <div class=\"np-list\" id=\"notesList\"></div>\n <div class=\"np-foot\" id=\"notesFoot\"></div>\n </div>\n <button class=\"icon-btn\" id=\"mute\" title=\"Mute\" aria-label=\"Mute\" disabled>\n <svg id=\"volIcon\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M2.5 6h2.2L8 3.2v9.6L4.7 10H2.5z\" fill=\"currentColor\" stroke=\"none\"/><path d=\"M10.6 5.6a3.4 3.4 0 0 1 0 4.8M12.4 3.9a5.8 5.8 0 0 1 0 8.2\"/></svg>\n <svg id=\"muteIcon\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" style=\"display:none\"><path d=\"M2.5 6h2.2L8 3.2v9.6L4.7 10H2.5z\" fill=\"currentColor\" stroke=\"none\"/><path d=\"M10.5 6l3.5 4M14 6l-3.5 4\"/></svg>\n </button>\n <input class=\"slider\" type=\"range\" id=\"volume\" min=\"0\" max=\"1\" step=\"0.01\" value=\"0.9\" aria-label=\"Volume\" disabled>\n </div>\n\n <div class=\"timeline\" id=\"timeline\" aria-label=\"Timeline\">\n <div class=\"tl-inner\" id=\"tlInner\">\n <div class=\"ruler\" id=\"ruler\"></div>\n <div class=\"note-marks\" id=\"noteMarks\"></div>\n <div class=\"wave-lane\">\n <canvas id=\"wave\"></canvas>\n <div class=\"wave-state shimmer\" id=\"waveState\"><span class=\"spinner\"></span>Mixing audio</div>\n </div>\n <div class=\"hover-line\" id=\"hoverLine\"></div>\n <div class=\"hover-time\" id=\"hoverTime\"></div>\n <div class=\"playhead\" id=\"playhead\"></div>\n </div>\n </div>\n</main>\n\n<div class=\"pop\" id=\"pop\" role=\"dialog\" aria-label=\"Note\"></div>\n\n<script>\n(function () {\n var $ = function (id) { return document.getElementById(id); };\n var stage = $(\"stage\"), timeline = $(\"timeline\"), tlInner = $(\"tlInner\");\n\n var meta = null, session = null, frame = 0, shown = -1, playing = false, playToken = 0;\n var player = null;\n\n // ── Helpers ──\n function fmt(f) {\n var s = f / meta.fps, m = Math.floor(s / 60);\n return m + \":\" + (s - m * 60).toFixed(2).padStart(5, \"0\");\n }\n function last() { return meta.frameCount - 1; }\n function clampFrame(f) { return Math.max(0, Math.min(last(), Math.round(f))); }\n function pct(f) { return meta.frameCount > 1 ? (f / last()) * 100 : 0; }\n function el(tag, cls, text) {\n var e = document.createElement(tag);\n if (cls) e.className = cls;\n if (text != null) e.textContent = text;\n return e;\n }\n function sleep(ms) { return new Promise(function (r) { setTimeout(r, ms); }); }\n\n // ── Reporting: errors and the engine's warnings go to the preview's\n // terminal too, so whoever runs it sees them without opening this tab.\n // Once each; frame numbers aside, playback repeats the same one per frame ──\n var reportedKeys = new Set();\n function errText(x) {\n if (x && x.message) return x.stack && x.stack.indexOf(x.message) >= 0 ? x.stack.split(\"\\\\n\").slice(0, 4).join(\"\\\\n\") : x.message;\n return typeof x === \"string\" ? x : String(x);\n }\n function report(level, parts) {\n var text = parts.map(errText).join(\" \").trim(), key = level + text.replace(/\\\\d+/g, \"#\");\n if (!text || reportedKeys.has(key) || reportedKeys.size > 100) return;\n reportedKeys.add(key);\n fetch(\"/@framefields/log\", {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ level: level, message: text.slice(0, 4000) }),\n keepalive: true\n }).catch(function () {});\n }\n [\"error\", \"warn\"].forEach(function (name) {\n var original = console[name].bind(console);\n console[name] = function () {\n original.apply(null, arguments);\n var args = Array.prototype.slice.call(arguments);\n // Errors from anywhere; warnings only from the engine (fonts, renderers).\n if (name === \"error\" || (typeof args[0] === \"string\" && args[0].indexOf(\"[framefields]\") === 0)) {\n report(name === \"error\" ? \"error\" : \"warning\", args);\n }\n };\n });\n\n // ── Frames: drawn in this tab by the composition's own code ──\n function setTime(f) {\n frame = f;\n $(\"tcNow\").textContent = fmt(f);\n $(\"tcFrame\").textContent = \"frame \" + f;\n $(\"playhead\").style.left = pct(f) + \"%\";\n if (!playing && notesOn) renderPins();\n }\n function show(f) {\n setTime(f);\n if (!player) return Promise.resolve();\n $(\"buffering\").classList.add(\"on\");\n // Calls made while a frame draws collapse to the latest.\n return player.render(f).then(function (drawn) {\n shown = drawn;\n if (!stalled) $(\"buffering\").classList.remove(\"on\");\n });\n }\n function seek(f) {\n stop();\n var done = show(clampFrame(f));\n keepPosition();\n scheduleWarm();\n return done;\n }\n\n // ── Position in the address: #t=12.5 opens there, and a reload (the next\n // preview taking over) comes back to the same moment ──\n function startFrame() {\n var m = /[#&]t=([\\\\d.]+)/.exec(location.hash), n = /[#&]frame=(\\\\d+)/.exec(location.hash);\n if (m) return clampFrame(Number(m[1]) * meta.fps);\n if (n) return clampFrame(Number(n[1]));\n return 0;\n }\n function keepPosition() {\n var at = frame > 0 ? \"#t=\" + Number((frame / meta.fps).toFixed(3)) : location.pathname;\n try { history.replaceState(null, \"\", at); } catch (e) {}\n }\n\n // ── Analysing ahead: while paused, frames after the playhead are drawn\n // off-screen so vision results are cached before they play ──\n var warmTimer = null, warmToken = 0, warmed = new Set();\n function scheduleWarm() {\n clearTimeout(warmTimer);\n warmToken++;\n if (!player || !player.hasVision || playing) return;\n warmTimer = setTimeout(warmAhead, 1000);\n }\n async function warmAhead() {\n var token = ++warmToken;\n var end = Math.min(last(), frame + Math.round(meta.fps * 20));\n for (var f = frame + 1; f <= end && token === warmToken && !playing; f++) {\n if (warmed.has(f)) continue;\n if (!(await player.warm(f))) { scheduleWarm(); return; }\n warmed.add(f);\n await sleep(0);\n }\n }\n function showError(err) {\n report(\"error\", [err]);\n pageState = \"error\";\n $(\"buffering\").classList.remove(\"on\");\n $(\"stageMsg\").textContent = String((err && err.message) || err);\n $(\"stageMsg\").classList.add(\"on\");\n }\n\n // ── Audio ──\n var audio = { state: \"mixing\", error: \"\", buffer: null, peaks: null, ctx: null, gain: null, src: null, muted: false, volume: 0.9 };\n var AUDIO_LABEL = { mixing: \"Mixing audio\", loading: \"Loading audio\", ready: \"Audio ready\", none: \"No audio\", error: \"Audio failed\" };\n var offline = false;\n\n function renderChip() {\n var chip = $(\"chip\"), state, label, busy;\n if (offline) { state = \"offline\"; label = \"Preview stopped\"; busy = false; }\n else { state = audio.state; label = AUDIO_LABEL[audio.state]; busy = state === \"mixing\" || state === \"loading\"; }\n chip.dataset.state = state;\n chip.title = offline ? \"The preview command isn't running. This tab reconnects when it starts again.\" : audio.error || \"\";\n chip.replaceChildren(el(\"span\", busy ? \"spinner\" : \"dot\"), el(\"span\", null, label));\n }\n function setAudioStatus(state, error) {\n audio.state = state;\n audio.error = error || \"\";\n var busy = state === \"mixing\" || state === \"loading\", ws = $(\"waveState\");\n ws.classList.toggle(\"shimmer\", busy);\n ws.style.display = state === \"ready\" ? \"none\" : \"flex\";\n ws.replaceChildren();\n if (busy) ws.append(el(\"span\", \"spinner\"));\n ws.append(document.createTextNode(state === \"none\" ? \"No audio in this composition\" : state === \"error\" ? \"Audio could not be mixed\" : AUDIO_LABEL[state]));\n var usable = state === \"ready\" && !!audio.buffer;\n $(\"mute\").disabled = !usable;\n $(\"volume\").disabled = !usable;\n renderChip();\n drawWave();\n }\n function loadAudio(status) {\n stopClock();\n audio.buffer = null;\n audio.peaks = null;\n if (!status || status.state !== \"ready\") { setAudioStatus(status ? status.state : \"none\", status && status.error); return; }\n audio.peaks = status.peaks;\n setAudioStatus(\"loading\");\n if (!audio.ctx) {\n var Ctx = window.AudioContext || window.webkitAudioContext;\n audio.ctx = new Ctx();\n audio.gain = audio.ctx.createGain();\n audio.gain.connect(audio.ctx.destination);\n applyVolume();\n }\n fetch(\"/audio\")\n .then(function (r) { if (!r.ok) throw new Error(\"HTTP \" + r.status); return r.arrayBuffer(); })\n .then(function (data) { return audio.ctx.decodeAudioData(data); })\n .then(function (buf) { audio.buffer = buf; setAudioStatus(\"ready\"); })\n .catch(function (err) { setAudioStatus(\"error\", String((err && err.message) || err)); });\n }\n function applyVolume() {\n if (audio.gain) audio.gain.gain.value = audio.muted ? 0 : audio.volume;\n $(\"volIcon\").style.display = audio.muted ? \"none\" : \"\";\n $(\"muteIcon\").style.display = audio.muted ? \"\" : \"none\";\n $(\"mute\").title = audio.muted ? \"Unmute\" : \"Mute\";\n $(\"mute\").setAttribute(\"aria-label\", audio.muted ? \"Unmute\" : \"Mute\");\n }\n // The clock for real-time playback: the soundtrack when there is one,\n // otherwise the wall clock. Audio as master keeps picture and sound in sync.\n function startClock(f0) {\n if (audio.buffer && audio.ctx) {\n audio.ctx.resume();\n var src = audio.ctx.createBufferSource();\n src.buffer = audio.buffer;\n src.connect(audio.gain);\n var startAt = audio.ctx.currentTime + 0.06, offset = f0 / meta.fps;\n if (offset < audio.buffer.duration) src.start(startAt, offset);\n audio.src = src;\n return function () { return Math.max(0, audio.ctx.currentTime - startAt); };\n }\n var t0 = performance.now();\n return function () { return (performance.now() - t0) / 1000; };\n }\n function stopClock() {\n if (!audio.src) return;\n try { audio.src.stop(); } catch (e) {}\n audio.src.disconnect();\n audio.src = null;\n }\n\n // ── Playback ──\n var stalled = false;\n // Waiting on frames mid-playback: the play button and the picture both spin.\n function setStalled(on) {\n stalled = on;\n $(\"play\").classList.toggle(\"loading\", on);\n $(\"buffering\").classList.toggle(\"on\", on);\n }\n function setPlayingUi(on) {\n $(\"playIcon\").style.display = on ? \"none\" : \"\";\n $(\"pauseIcon\").style.display = on ? \"\" : \"none\";\n $(\"play\").title = on ? \"Pause\" : \"Play\";\n $(\"play\").setAttribute(\"aria-label\", on ? \"Pause\" : \"Play\");\n }\n function play() {\n if (playing || !player) return;\n playing = true;\n clearTimeout(warmTimer);\n warmToken++;\n var token = ++playToken;\n var alive = function () { return playing && token === playToken; };\n setPlayingUi(true);\n if (openId != null) closePop();\n if (draft && !draft.text.trim()) cancelDraft();\n if (notesOn) renderPins();\n if (frame >= last()) frame = 0;\n playFrames(alive).finally(function () { if (token === playToken && playing) pause(); });\n }\n function stop() {\n playing = false;\n stopClock();\n setStalled(false);\n setPlayingUi(false);\n meter.stop();\n renderFps();\n }\n\n // ── Frame rate reached: distinct frames shown per second while playing,\n // over the last 2 s, stalls included. The film's own rate is in meta ──\n var meter = (function () {\n var started = null, lastFrame = null, samples = [], lastFps = null;\n function read() {\n if (started === null) return { state: \"paused\", fps: lastFps };\n var now = performance.now(), elapsed = Math.min(2000, now - started);\n samples = samples.filter(function (t) { return t > now - 2000; });\n if (elapsed < 1000) return { state: \"measuring\", fps: null };\n // Counting whole frames in the window can overshoot the film's rate.\n return { state: \"playing\", fps: Math.min(meta ? meta.fps : Infinity, (samples.length * 1000) / elapsed) };\n }\n return {\n read: read,\n start: function () { started = performance.now(); lastFrame = null; samples = []; },\n stop: function () {\n if (started === null) return;\n // A playback too short to measure leaves no rate, not an older one.\n var r = read();\n lastFps = r.state === \"playing\" ? r.fps : null;\n started = null;\n samples = [];\n },\n record: function (f) {\n if (started === null || f === lastFrame) return;\n lastFrame = f;\n samples.push(performance.now());\n }\n };\n })();\n function renderFps() {\n var r = meter.read(), out = $(\"fps\");\n out.hidden = r.state === \"paused\" && r.fps === null;\n if (out.hidden) return;\n out.textContent = r.state === \"measuring\" ? \"… fps\" : r.fps.toFixed(1) + \" fps\";\n out.title = (r.state === \"paused\" ? \"Frame rate the last playback reached\" : \"Frame rate playback reaches\") + (meta ? \"; the film runs at \" + meta.fps + \" fps\" : \"\");\n }\n setInterval(function () { if (playing) renderFps(); }, 500);\n // Pausing settles on exactly the frame the timecode shows.\n function pause() { stop(); keepPosition(); show(frame).then(scheduleWarm); }\n function togglePlay() { playing ? pause() : play(); }\n\n // Real time: nothing is rendered ahead. Each turn draws the frame due on\n // the clock straight to the screen, and frames that drawing missed are\n // skipped. Playback starts at once and the sound keeps its speed; a device\n // that can't keep up shows fewer frames, and the frame rate says how many.\n async function playFrames(alive) {\n meter.start();\n renderFps();\n // A soundtrack still downloading would start late: wait for it.\n if (audio.state === \"loading\") {\n setStalled(true);\n while (alive() && audio.state === \"loading\") await sleep(50);\n // A newer playback may be waiting too: only this one's own stall ends here.\n if (!alive()) return;\n setStalled(false);\n }\n var f0 = frame, clock = startClock(f0), presented = -1;\n while (alive()) {\n var due = Math.min(last(), f0 + Math.floor(clock() * meta.fps));\n if (due === presented) { await sleep(4); continue; }\n setTime(due);\n // Each frame is drawn once; caching it would cost a texture and a copy per frame.\n var drawn = await player.render(due, { cache: false });\n if (!alive()) return;\n presented = shown = drawn;\n meter.record(drawn);\n if (drawn >= last()) return;\n }\n }\n\n // ── Timeline ──\n function frameAtX(clientX) {\n var r = tlInner.getBoundingClientRect();\n return clampFrame(Math.min(1, Math.max(0, (clientX - r.left) / r.width)) * last());\n }\n function renderRuler() {\n var ruler = $(\"ruler\"), width = tlInner.clientWidth || 800, dur = last() / meta.fps;\n ruler.replaceChildren();\n if (dur <= 0) return;\n var steps = [0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600];\n var major = steps.find(function (s) { return (s / dur) * width >= 80; }) || 1200, minor = major / 5;\n for (var i = 0; i * minor <= dur + 1e-6; i++) {\n var t = i * minor, isMajor = i % 5 === 0, x = (t / dur) * width;\n var tick = el(\"div\", \"tick\" + (isMajor ? \" major\" : \"\"));\n tick.style.left = (t / dur) * 100 + \"%\";\n if (isMajor && x < width - 34) {\n var m = Math.floor(t / 60), s = Math.round((t - m * 60) * 10) / 10;\n tick.append(el(\"span\", null, m + \":\" + (s < 10 ? \"0\" : \"\") + s));\n }\n ruler.append(tick);\n }\n }\n function drawWave() {\n var canvas = $(\"wave\"), dpr = window.devicePixelRatio || 1;\n var w = canvas.clientWidth, h = canvas.clientHeight;\n canvas.width = Math.round(w * dpr);\n canvas.height = Math.round(h * dpr);\n var ctx = canvas.getContext(\"2d\");\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n ctx.clearRect(0, 0, w, h);\n var peaks = audio.peaks;\n if (!peaks || !peaks.length || !meta) return;\n var videoSec = Math.max(1e-6, last() / meta.fps);\n var audioSec = audio.buffer ? audio.buffer.duration : videoSec;\n var spanPx = Math.min(1, audioSec / videoSec) * w, mid = h / 2;\n ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue(\"--wave\").trim();\n for (var x = 0; x < spanPx; x += 3) {\n var v = Math.max(peaks[Math.floor((x / spanPx) * peaks.length)] || 0, 0.015);\n var bh = Math.max(1, Math.sqrt(v) * (h - 8));\n ctx.fillRect(x, mid - bh / 2, 2, bh);\n }\n }\n\n var dragging = false;\n timeline.addEventListener(\"pointerdown\", function (e) {\n if (!meta || e.button !== 0) return;\n timeline.setPointerCapture(e.pointerId);\n dragging = true;\n seek(frameAtX(e.clientX));\n });\n timeline.addEventListener(\"pointermove\", function (e) {\n if (!meta) return;\n var f = frameAtX(e.clientX), r = tlInner.getBoundingClientRect();\n var x = Math.min(r.width, Math.max(0, e.clientX - r.left));\n $(\"hoverLine\").style.display = \"block\";\n $(\"hoverLine\").style.left = x + \"px\";\n $(\"hoverTime\").style.display = \"block\";\n $(\"hoverTime\").style.left = x + \"px\";\n $(\"hoverTime\").textContent = fmt(f);\n if (dragging && f !== frame) seek(f);\n });\n timeline.addEventListener(\"pointerleave\", function () {\n $(\"hoverLine\").style.display = \"none\";\n $(\"hoverTime\").style.display = \"none\";\n });\n timeline.addEventListener(\"pointerup\", function () { dragging = false; });\n timeline.addEventListener(\"pointercancel\", function () { dragging = false; });\n\n // ── Notes: pause, click the picture (or drag an area) and say what should\n // change. Each is saved with its moment, its spot and a picture of the\n // frame with the spot marked, for whoever edits the film ──\n var notes = [], notesOn = false, draft = null, openId = null, saving = false, press = null;\n var pop = $(\"pop\"), pins = $(\"pins\"), NL = String.fromCharCode(10);\n var ICON_CHECK = '<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3.5 8.5l3 3 6-7\"/></svg>';\n var ICON_X = '<svg viewBox=\"0 0 16 16\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"><path d=\"M4 4l8 8M12 4l-8 8\"/></svg>';\n\n function pct1(v) { return Math.round(v * 100) + \"%\"; }\n function whereText(n) {\n return n.w != null ? \"area \" + pct1(n.x) + \",\" + pct1(n.y) + \" to \" + pct1(n.x + n.w) + \",\" + pct1(n.y + n.h) : \"spot \" + pct1(n.x) + \",\" + pct1(n.y);\n }\n function noteById(id) { return notes.find(function (n) { return n.id === id; }); }\n function openCount() { return notes.filter(function (n) { return !n.done; }).length; }\n\n function renderNotes() {\n renderPins();\n renderMarks();\n renderList();\n var open = openCount(), count = $(\"notesCount\");\n count.textContent = String(open || notes.length);\n count.classList.toggle(\"open\", open > 0);\n $(\"notesBtn\").title = open + \" open of \" + notes.length;\n }\n function renderPins() {\n if (!meta) return;\n stage.classList.toggle(\"can-note\", notesOn && !!player && !playing);\n stage.classList.toggle(\"hint\", notes.length === 0 && !draft);\n pins.replaceChildren();\n if (playing) return;\n var here = notes.filter(function (n) { return n.frame === frame; });\n if (draft && draft.frame === frame) here.push(draft);\n here.forEach(function (n) {\n if (n.w != null) {\n var a = el(\"div\", \"area\" + (n.done ? \" done\" : \"\"));\n a.style.left = n.x * 100 + \"%\"; a.style.top = n.y * 100 + \"%\";\n a.style.width = n.w * 100 + \"%\"; a.style.height = n.h * 100 + \"%\";\n pins.append(a);\n }\n var pin = el(\"button\", \"pin\" + (n.done ? \" done\" : \"\") + (n === draft ? \" draft\" : \"\") + (n.id === openId ? \" active\" : \"\"));\n pin.style.left = n.x * 100 + \"%\"; pin.style.top = n.y * 100 + \"%\";\n if (n === draft) { pin.setAttribute(\"aria-label\", \"New note\"); }\n else {\n if (n.done) pin.innerHTML = ICON_CHECK; else pin.textContent = String(n.id);\n pin.title = n.text;\n pin.setAttribute(\"aria-label\", \"Note \" + n.id);\n pin.addEventListener(\"click\", function (e) { e.stopPropagation(); openCard(n.id); });\n }\n pins.append(pin);\n });\n }\n function renderMarks() {\n var marks = $(\"noteMarks\");\n marks.replaceChildren();\n if (!meta) return;\n // Notes close together stack, up to three high.\n var width = tlInner.clientWidth || 800, lanes = [];\n notes.slice().sort(function (a, b) { return a.frame - b.frame || a.id - b.id; }).forEach(function (n) {\n var x = (pct(n.frame) / 100) * width, lane = lanes.findIndex(function (end) { return x - end >= 16; });\n if (lane < 0) lane = lanes.length < 3 ? lanes.length : 2;\n lanes[lane] = x;\n var m = el(\"button\", \"note-mark\" + (n.done ? \" done\" : \"\") + (lane ? \" stacked\" : \"\"), String(n.id));\n m.style.left = pct(n.frame) + \"%\";\n m.style.top = 4 + lane * 15 + \"px\";\n m.title = n.id + \" · \" + fmt(n.frame) + \" · \" + n.text;\n m.addEventListener(\"pointerdown\", function (e) { e.stopPropagation(); });\n m.addEventListener(\"click\", function (e) { e.stopPropagation(); goToNote(n.id); });\n marks.append(m);\n });\n }\n function renderList() {\n var list = $(\"notesList\");\n list.replaceChildren();\n $(\"notesCopy\").disabled = openCount() === 0;\n if (!notes.length) {\n list.append(el(\"div\", \"np-empty\", \"Pause and click the picture to pin a note to that spot and moment. Drag to mark an area.\"));\n }\n notes.slice().sort(function (a, b) { return a.frame - b.frame || a.id - b.id; }).forEach(function (n) {\n var row = el(\"div\", \"note-row\" + (n.done ? \" done\" : \"\"));\n var body = el(\"div\");\n body.append(el(\"div\", \"when tnum\", fmt(n.frame) + \" · frame \" + n.frame), el(\"div\", \"text\", n.text));\n var check = el(\"button\", \"check\" + (n.done ? \" on\" : \"\"));\n check.innerHTML = ICON_CHECK;\n check.title = n.done ? \"Reopen\" : \"Mark done\";\n check.setAttribute(\"aria-label\", check.title);\n check.addEventListener(\"click\", function (e) { e.stopPropagation(); setDone(n.id, !n.done); });\n row.append(el(\"span\", \"badge\" + (n.done ? \" done\" : \"\"), String(n.id)), body, check);\n row.addEventListener(\"click\", function () { goToNote(n.id); });\n list.append(row);\n });\n var dir = meta && meta.notesDir ? meta.notesDir.split(\"/\").slice(-2).join(\"/\") : \"\";\n $(\"notesFoot\").textContent = \"Saved in \" + dir + \" with a picture of each spot. Ask your agent to go through them.\";\n }\n\n function notesRequest(body) {\n return fetch(\"/@framefields/notes\", { method: \"POST\", headers: { \"content-type\": \"application/json\" }, body: JSON.stringify(body) })\n .then(function (r) { if (!r.ok) throw new Error(\"HTTP \" + r.status); return r.json(); });\n }\n function upsert(note) {\n var i = notes.findIndex(function (n) { return n.id === note.id; });\n if (i >= 0) notes[i] = note; else notes.push(note);\n renderNotes();\n }\n function setDone(id, done) {\n notesRequest({ op: \"update\", id: id, done: done }).then(function (n) {\n upsert(n);\n if (openId === id) openCard(id);\n }, function () { toast(\"Couldn't save. Is the preview running?\"); });\n }\n function removeNote(id) {\n notesRequest({ op: \"remove\", id: id }).then(function () {\n notes = notes.filter(function (n) { return n.id !== id; });\n closePop();\n renderNotes();\n }, function () { toast(\"Couldn't delete. Is the preview running?\"); });\n }\n function goToNote(id) {\n var n = noteById(id);\n if (!n) return;\n $(\"notesPanel\").classList.remove(\"on\");\n $(\"notesBtn\").setAttribute(\"aria-expanded\", \"false\");\n seek(n.frame).then(function () { openCard(id); });\n }\n\n var toastTimer = null;\n function toast(text) {\n var t = $(\"toast\");\n t.textContent = text;\n t.classList.add(\"on\");\n clearTimeout(toastTimer);\n toastTimer = setTimeout(function () { t.classList.remove(\"on\"); }, 2200);\n }\n\n // ── Placing a note on the picture ──\n function fracAt(e) {\n var r = stage.getBoundingClientRect();\n return { x: Math.min(1, Math.max(0, (e.clientX - r.left) / r.width)), y: Math.min(1, Math.max(0, (e.clientY - r.top) / r.height)), r: r };\n }\n stage.addEventListener(\"pointerdown\", function (e) {\n if (e.button !== 0 || !player || e.target.closest(\".pin\")) return;\n if (playing) { pause(); return; }\n if (!notesOn) return;\n if (draft && pop.querySelector(\"textarea\") && pop.querySelector(\"textarea\").value.trim()) {\n pop.querySelector(\"textarea\").focus();\n return;\n }\n cancelDraft();\n var p = fracAt(e);\n press = { x: p.x, y: p.y, cx: e.clientX, cy: e.clientY, area: null };\n stage.setPointerCapture(e.pointerId);\n e.preventDefault();\n });\n stage.addEventListener(\"pointermove\", function (e) {\n if (!press) return;\n var p = fracAt(e);\n if (!press.area && Math.hypot(e.clientX - press.cx, e.clientY - press.cy) < 6) return;\n press.area = { x: Math.min(press.x, p.x), y: Math.min(press.y, p.y), w: Math.abs(p.x - press.x), h: Math.abs(p.y - press.y) };\n var m = $(\"marquee\");\n m.style.display = \"block\";\n m.style.left = press.area.x * 100 + \"%\"; m.style.top = press.area.y * 100 + \"%\";\n m.style.width = press.area.w * 100 + \"%\"; m.style.height = press.area.h * 100 + \"%\";\n });\n function endPress() {\n if (!press) return;\n var a = press.area;\n $(\"marquee\").style.display = \"none\";\n draft = a && a.w > 0.01 && a.h > 0.01\n ? { frame: frame, x: a.x, y: a.y, w: a.w, h: a.h, text: \"\" }\n : { frame: frame, x: press.x, y: press.y, text: \"\" };\n press = null;\n openComposer();\n }\n stage.addEventListener(\"pointerup\", endPress);\n stage.addEventListener(\"pointercancel\", function () { press = null; $(\"marquee\").style.display = \"none\"; });\n\n // ── The popover: writing a note, or reading one ──\n var popAnchor = null;\n function placePop() {\n if (!popAnchor || !pop.classList.contains(\"on\")) return;\n var r = stage.getBoundingClientRect(), n = popAnchor;\n var ax = r.left + (n.x + (n.w || 0)) * r.width, ay = r.top + (n.y + (n.h || 0)) * r.height;\n var w = pop.offsetWidth, h = pop.offsetHeight, gap = 16;\n var left = ax + gap + w <= innerWidth - 8 ? ax + gap : ax - gap - w - (n.w || 0) * r.width;\n var top = ay + gap + h <= innerHeight - 8 ? ay + gap / 2 : ay - h - gap / 2 - (n.h || 0) * r.height;\n pop.style.left = Math.max(8, Math.min(innerWidth - w - 8, left)) + \"px\";\n pop.style.top = Math.max(8, Math.min(innerHeight - h - 8, top)) + \"px\";\n }\n function showPop(anchor) {\n popAnchor = anchor;\n pop.classList.add(\"on\");\n placePop();\n }\n function closePop() {\n pop.classList.remove(\"on\");\n pop.replaceChildren();\n popAnchor = null;\n openId = null;\n renderPins();\n }\n function cancelDraft() {\n if (!draft) return;\n draft = null;\n closePop();\n }\n function openComposer() {\n openId = null;\n var head = el(\"div\", \"pop-head\");\n head.append(el(\"span\", \"badge\", \"+\"), el(\"span\", \"tnum\", fmt(draft.frame) + \" · \" + (draft.w != null ? \"area\" : \"spot\")));\n var ta = document.createElement(\"textarea\");\n ta.placeholder = \"What should change here?\";\n ta.rows = 2;\n ta.setAttribute(\"aria-label\", \"Note\");\n var foot = el(\"div\", \"pop-foot\"), status = el(\"span\", \"grow\", \"Enter to add\");\n var cancel = el(\"button\", \"btn\", \"Cancel\"), add = el(\"button\", \"btn primary\", \"Add note\");\n add.disabled = true;\n foot.append(status, cancel, add);\n pop.replaceChildren(head, ta, foot);\n ta.addEventListener(\"input\", function () {\n draft.text = ta.value;\n add.disabled = !ta.value.trim() || saving;\n ta.style.height = \"auto\";\n ta.style.height = Math.min(180, ta.scrollHeight) + \"px\";\n placePop();\n });\n ta.addEventListener(\"keydown\", function (e) {\n if (e.key === \"Enter\" && !e.shiftKey && !e.isComposing) { e.preventDefault(); saveDraft(ta, add, status); }\n if (e.key === \"Escape\") { e.preventDefault(); cancelDraft(); }\n });\n cancel.addEventListener(\"click\", cancelDraft);\n add.addEventListener(\"click\", function () { saveDraft(ta, add, status); });\n renderPins();\n showPop(draft);\n ta.focus();\n }\n async function saveDraft(ta, add, status) {\n var d = draft, text = ta.value.trim();\n if (!d || !text || saving) return;\n saving = true;\n ta.readOnly = true;\n add.disabled = true;\n status.className = \"grow\";\n status.textContent = \"Saving…\";\n try {\n var image = null;\n try { image = await capture(d); } catch (err) { report(\"warning\", [\"[framefields] no picture for the note:\", err]); }\n var note = await notesRequest({ op: \"add\", note: { frame: d.frame, time: d.frame / meta.fps, x: d.x, y: d.y, w: d.w, h: d.h, text: text, image: image } });\n if (draft === d) draft = null;\n closePop();\n upsert(note);\n toast(\"Note \" + note.id + \" saved\");\n } catch (err) {\n status.className = \"grow err\";\n status.textContent = \"Couldn't save. Is the preview running?\";\n ta.readOnly = false;\n add.disabled = false;\n } finally {\n saving = false;\n }\n }\n // The frame with the spot marked, small enough to read at a glance.\n async function capture(d) {\n var bmp = await player.snapshot(d.frame);\n var scale = Math.min(1, 1600 / bmp.width), w = Math.round(bmp.width * scale), h = Math.round(bmp.height * scale);\n var c = document.createElement(\"canvas\");\n c.width = w; c.height = h;\n var g = c.getContext(\"2d\");\n g.drawImage(bmp, 0, 0, w, h);\n bmp.close();\n var lw = Math.max(3, w / 400);\n function mark(draw) {\n // Dark, white, then blue: it reads on any picture.\n g.lineWidth = lw + 5; g.strokeStyle = \"rgba(0,0,0,0.5)\"; draw();\n g.lineWidth = lw + 2.5; g.strokeStyle = \"#fff\"; draw();\n g.lineWidth = lw; g.strokeStyle = \"#3b82f6\"; draw();\n }\n if (d.w != null) {\n mark(function () { g.strokeRect(d.x * w, d.y * h, d.w * w, d.h * h); });\n } else {\n var rad = Math.max(16, w / 40);\n mark(function () { g.beginPath(); g.arc(d.x * w, d.y * h, rad, 0, Math.PI * 2); g.stroke(); });\n g.fillStyle = \"#3b82f6\";\n g.beginPath(); g.arc(d.x * w, d.y * h, lw, 0, Math.PI * 2); g.fill();\n }\n return c.toDataURL(\"image/jpeg\", 0.88);\n }\n function openCard(id) {\n var n = noteById(id);\n if (!n) return;\n if (draft) cancelDraft();\n openId = id;\n var head = el(\"div\", \"pop-head\");\n var close = el(\"button\", \"btn x-btn\");\n close.innerHTML = ICON_X;\n close.title = \"Close\";\n close.setAttribute(\"aria-label\", \"Close\");\n close.addEventListener(\"click\", closePop);\n head.append(el(\"span\", \"badge\" + (n.done ? \" done\" : \"\"), String(n.id)), el(\"span\", \"tnum grow\", fmt(n.frame) + \" · frame \" + n.frame), close);\n var foot = el(\"div\", \"pop-foot\");\n var done = el(\"button\", \"btn\");\n done.innerHTML = ICON_CHECK;\n done.append(document.createTextNode(n.done ? \"Reopen\" : \"Mark done\"));\n done.addEventListener(\"click\", function () { setDone(n.id, !n.done); });\n var del = el(\"button\", \"btn danger\", \"Delete\");\n del.addEventListener(\"click\", function () { removeNote(n.id); });\n foot.append(done, el(\"span\", \"grow\"), del);\n pop.replaceChildren(head, el(\"div\", \"pop-text\", n.text), foot);\n renderPins();\n showPop(n);\n }\n\n document.addEventListener(\"pointerdown\", function (e) {\n if (pop.classList.contains(\"on\") && !pop.contains(e.target) && !stage.contains(e.target) && !e.target.closest(\".note-mark, .note-row\")) {\n if (openId != null) closePop();\n else if (draft && !draft.text.trim()) cancelDraft();\n }\n var panel = $(\"notesPanel\");\n if (panel.classList.contains(\"on\") && !panel.contains(e.target) && !$(\"notesBtn\").contains(e.target)) {\n panel.classList.remove(\"on\");\n $(\"notesBtn\").setAttribute(\"aria-expanded\", \"false\");\n }\n });\n document.addEventListener(\"keydown\", function (e) {\n if (e.key !== \"Escape\") return;\n if (openId != null) closePop();\n $(\"notesPanel\").classList.remove(\"on\");\n $(\"notesBtn\").setAttribute(\"aria-expanded\", \"false\");\n });\n window.addEventListener(\"resize\", placePop);\n new ResizeObserver(placePop).observe(stage);\n\n $(\"notesBtn\").addEventListener(\"click\", function () {\n var on = $(\"notesPanel\").classList.toggle(\"on\");\n $(\"notesBtn\").setAttribute(\"aria-expanded\", String(on));\n });\n $(\"notesClose\").addEventListener(\"click\", function () {\n $(\"notesPanel\").classList.remove(\"on\");\n $(\"notesBtn\").setAttribute(\"aria-expanded\", \"false\");\n });\n // The open notes as text, for a chat that can't read the notes folder.\n $(\"notesCopy\").addEventListener(\"click\", function () {\n var lines = [\"Notes on the preview of \" + JSON.stringify(meta.title) + \":\"];\n notes.filter(function (n) { return !n.done; }).sort(function (a, b) { return a.frame - b.frame; }).forEach(function (n) {\n lines.push(\"- Note \" + n.id + \" at \" + fmt(n.frame) + \" (frame \" + n.frame + \"), \" + whereText(n) + \": \" + n.text + (n.image ? \" [picture: \" + n.image + \"]\" : \"\"));\n });\n var text = lines.join(NL), label = $(\"notesCopyLabel\");\n var done = function () { label.textContent = \"Copied\"; setTimeout(function () { label.textContent = \"Copy\"; }, 1600); };\n (navigator.clipboard ? navigator.clipboard.writeText(text) : Promise.reject()).then(done, function () {\n var t = document.createElement(\"textarea\");\n t.value = text;\n document.body.append(t);\n t.select();\n try { document.execCommand(\"copy\"); done(); } catch (e) {}\n t.remove();\n });\n });\n\n function loadNotes() {\n if (!notesOn) return;\n fetch(\"/@framefields/notes\").then(function (r) { return r.json(); }).then(function (list) {\n notes = Array.isArray(list) ? list : [];\n renderNotes();\n }, function () {});\n }\n\n // ── Wiring ──\n $(\"play\").addEventListener(\"click\", togglePlay);\n $(\"prev\").addEventListener(\"click\", function () { seek(frame - 1); });\n $(\"next\").addEventListener(\"click\", function () { seek(frame + 1); });\n $(\"mute\").addEventListener(\"click\", function () { audio.muted = !audio.muted; applyVolume(); });\n $(\"volume\").addEventListener(\"input\", function (e) { audio.volume = Number(e.target.value); audio.muted = audio.volume === 0; applyVolume(); });\n\n // A hidden tab's timers are throttled, so playback would stutter: pause.\n document.addEventListener(\"visibilitychange\", function () {\n if (document.hidden && playing) pause();\n });\n\n // ── For scripts and agents driving the page: seek, play, and read the\n // state instead of clicking at coordinates ──\n var pageState = \"loading\";\n window.framefieldsPreview = {\n /** Shows the frame at a time in seconds; resolves once it is drawn. */\n seek: function (sec) { return ready().then(function () { return seek(Number(sec) * meta.fps); }); },\n /** Shows a frame by number; resolves once it is drawn. */\n seekFrame: function (f) { return ready().then(function () { return seek(f); }); },\n play: function () { return ready().then(play); },\n /** Notes pinned on the page, as saved in notes.json. */\n notes: function () { return notes.slice(); },\n pause: function () { if (playing) pause(); },\n /** Where the preview is: loading, ready or error, and what it shows. */\n state: function () {\n var rate = meter.read().fps;\n return {\n state: pageState,\n error: pageState === \"error\" ? $(\"stageMsg\").textContent : null,\n frame: frame,\n time: meta ? Number((frame / meta.fps).toFixed(3)) : 0,\n playing: playing,\n buffering: stalled,\n audio: audio.state,\n connected: !offline,\n // Frames shown per second over the last 2 s of playback (the last\n // playback's while paused), and how long the last frame took to draw.\n playbackFps: rate == null ? null : Number(rate.toFixed(1)),\n renderMs: player ? Math.round(player.lastRenderMs) : null,\n meta: meta && { title: meta.title, width: meta.width, height: meta.height, fps: meta.fps, frameCount: meta.frameCount, duration: Number((last() / meta.fps).toFixed(3)) }\n };\n }\n };\n var readyWaiters = [];\n function ready() {\n if (player) return Promise.resolve();\n if (pageState === \"error\") return Promise.reject(new Error($(\"stageMsg\").textContent));\n return new Promise(function (resolve, reject) { readyWaiters.push([resolve, reject]); });\n }\n function settleReady(err) {\n readyWaiters.splice(0).forEach(function (w) { err ? w[1](err) : w[0](); });\n }\n\n function applyMeta(m) {\n meta = m;\n document.title = m.title + \" · preview\";\n $(\"title\").textContent = m.title;\n $(\"spec\").textContent = m.width + \"×\" + m.height + \" · \" + m.fps + \" fps · \" + fmt(last());\n $(\"tcDur\").textContent = fmt(last());\n stage.style.setProperty(\"--ar\", m.width + \" / \" + m.height);\n stage.style.setProperty(\"--ar-n\", String(m.width / m.height));\n renderRuler();\n }\n\n new ResizeObserver(function () {\n if (!meta) return;\n renderRuler();\n drawWave();\n renderMarks();\n }).observe(tlInner);\n\n // When the preview command ends the event stream drops and the browser keeps\n // retrying. The next preview of this project (same port) says hello with a\n // new session, and this tab reloads into it.\n var events = new EventSource(\"/events\");\n events.addEventListener(\"hello\", function (e) {\n var s = JSON.parse(e.data).session;\n if (session && s !== session) { location.reload(); return; }\n offline = false;\n renderChip();\n });\n events.addEventListener(\"error\", function () {\n if (!meta) return;\n if (playing) { stop(); keepPosition(); }\n offline = true;\n renderChip();\n });\n events.addEventListener(\"notes\", function (e) {\n notes = JSON.parse(e.data);\n if (openId != null && !noteById(openId)) closePop();\n renderNotes();\n });\n events.addEventListener(\"audio\", function (e) {\n if (meta) loadAudio(JSON.parse(e.data));\n });\n\n fetch(\"/meta\").then(function (r) { return r.json(); }).then(function (m) {\n session = m.session;\n applyMeta(m);\n notesOn = !!m.notesDir;\n $(\"notesBtn\").hidden = !notesOn;\n loadNotes();\n loadAudio(m.audio);\n setTime(startFrame());\n $(\"buffering\").classList.add(\"on\");\n whenPlayer();\n });\n\n // The composition's bundle (loaded below) announces its player.\n function whenPlayer() {\n if (!window.framefieldsPlayer) return;\n window.framefieldsPlayer.then(function (p) {\n player = p;\n $(\"play\").disabled = false;\n show(frame).then(function () {\n pageState = \"ready\";\n renderPins();\n document.documentElement.dataset.preview = \"ready\";\n settleReady();\n scheduleWarm();\n });\n }, function (err) {\n showError(err);\n document.documentElement.dataset.preview = \"error\";\n settleReady(err instanceof Error ? err : new Error(String(err)));\n });\n }\n window.addEventListener(\"framefields:player\", function () { if (meta) whenPlayer(); });\n window.addEventListener(\"error\", function (e) {\n if (!player) showError(e.error || e.message);\n else report(\"error\", [e.error || e.message]);\n });\n window.addEventListener(\"unhandledrejection\", function (e) { report(\"error\", [e.reason]); });\n})();\n</script>\n<script type=\"module\" src=\"/@framefields/player.js\"></script>\n</body>\n</html>\n`;\n","import { spawn } from \"node:child_process\";\nimport { createHash, randomUUID } from \"node:crypto\";\nimport fs from \"node:fs\";\nimport http from \"node:http\";\nimport type { AddressInfo } from \"node:net\";\nimport path from \"node:path\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\nimport type { VirtualMediaData } from \"@framefields/core\";\nimport { encodeStereoWav } from \"../audio/index.js\";\nimport { HeadlessMediaRenderer } from \"../renderer/index.js\";\nimport {\n\tbundleProject,\n\tENGINE_URL,\n\tengineDir,\n\tonnxRuntimeWeb,\n\tvisionModelsDir,\n} from \"./bundle.js\";\nimport { describeNote, type NewNote, NoteStore } from \"./notes.js\";\nimport { previewPage } from \"./page.js\";\n\n/** Where the composition comes from: the module that builds it. */\nexport interface PreviewSource {\n\t/** Path or file URL of the module, e.g. `new URL(\"./film.ts\", import.meta.url)`. */\n\tentry: string | URL;\n\t/**\n\t * The export to preview: a function returning a `Composition` (it may be\n\t * async), or a `Composition`. Defaults to `\"default\"`.\n\t */\n\texport?: string;\n}\n\n/** What the server needs from the composition: its size, timing and soundtrack. */\ninterface PreviewTarget {\n\ttoVirtualMedia?(): VirtualMediaData;\n\ttoVirtualMediaAsync?(): Promise<VirtualMediaData>;\n\tcomputeDuration?(): Promise<number>;\n\tfps?: number;\n\tdurationMs?: number;\n\twidth?: number;\n\theight?: number;\n}\n\nexport interface PreviewOptions {\n\t/**\n\t * Port to listen on. Defaults to a port derived from the working directory,\n\t * so every preview of a project lands on the same URL and an open tab picks\n\t * up the next one by itself. A preview already running there is replaced.\n\t */\n\tport?: number;\n\t/** Interface to bind. Defaults to \"127.0.0.1\" so the preview never leaves the machine. */\n\thost?: string;\n\t/**\n\t * Also open the page in the system's default browser. Off by default: the\n\t * caller shows the URL where it suits, e.g. an agent app's own browser\n\t * pane. Skipped when a tab from an earlier preview reconnects on its own.\n\t */\n\topen?: boolean;\n\t/** Title shown in the page header. */\n\ttitle?: string;\n\t/** Mix and play the composition's audio. Defaults to true. */\n\taudio?: boolean;\n\t/** Optional shared renderer instance, used to mix the soundtrack. */\n\trenderer?: HeadlessMediaRenderer;\n\t/**\n\t * Close the server this many milliseconds after the last open tab goes\n\t * away (closed, not reloaded). Defaults to 5000. `false` keeps it serving\n\t * until `close()`. Nothing closes before a tab has connected.\n\t */\n\tidleCloseMs?: number | false;\n\t/**\n\t * Files the page may load by absolute path (the composition's assets).\n\t * Defaults to the project that holds the entry: the nearest `.git`, else\n\t * the nearest `package.json`, else the entry's directory. Pass one path or\n\t * several to allow assets kept outside it.\n\t */\n\troot?: string | string[];\n\t/**\n\t * Where the preview reports what happens out of sight: the page opening,\n\t * errors in the page (a failed build, a frame that threw, no WebGPU), and\n\t * why the server stopped. Defaults to stderr, so whoever runs the preview\n\t * (an agent's background command, say) reads them in its output. `false`\n\t * keeps it quiet.\n\t */\n\tlog?: ((line: string) => void) | false;\n\t/**\n\t * Where notes pinned on the page are kept: `notes.json` and, per note, the\n\t * frame with its spot marked. Defaults to `.framefields/preview-notes` in the\n\t * working directory (`.framefields` ignores itself in git). `false` turns\n\t * notes off.\n\t */\n\tnotesDir?: string | false;\n}\n\n/** Why a preview stopped. */\nexport type PreviewCloseReason =\n\t/** `close()` was called. */\n\t| \"closed\"\n\t/** Its last tab closed (see `idleCloseMs`). */\n\t| \"idle\"\n\t/** A newer preview of the project took over its port. */\n\t| \"replaced\";\n\nexport interface PreviewSession {\n\turl: string;\n\tclose(): Promise<void>;\n\t/** Resolves with the reason when the server stops. */\n\tclosed: Promise<PreviewCloseReason>;\n}\n\ninterface PreviewMeta {\n\tsession: string;\n\t/** Where notes are kept; null when they are off. */\n\tnotesDir: string | null;\n\ttitle: string;\n\tfps: number;\n\tframeCount: number;\n\twidth: number;\n\theight: number;\n}\n\ntype AudioStatus =\n\t| { state: \"mixing\" }\n\t| { state: \"ready\"; durationSec: number; peaks: number[] }\n\t| { state: \"none\" }\n\t| { state: \"error\"; error: string };\n\nconst PEAK_BINS = 1200;\n/** Requests carrying this header come from another preview process, not a page. */\nconst TAKEOVER_HEADER = \"x-framefields-preview\";\nconst PLAYER_PATH = \"/@framefields/player.js\";\n\n/**\n * Serves a localhost player for a composition. The page loads the\n * composition's own code, bundled with the engine, and renders its frames\n * with WebGPU in the browser, like the editor's player; nothing is streamed.\n * The server provides the bundle, the project's files, and the soundtrack,\n * which it mixes with the export engine.\n *\n * The server runs until its tab closes or `close()`. Starting another preview\n * on the same port (the default for a project) stops this one, and open tabs\n * reload into it.\n */\nexport async function startPreview(\n\tsource: PreviewSource,\n\toptions: PreviewOptions = {},\n): Promise<PreviewSession> {\n\tconst entry =\n\t\tsource.entry instanceof URL || String(source.entry).startsWith(\"file:\")\n\t\t\t? fileURLToPath(source.entry)\n\t\t\t: path.resolve(String(source.entry));\n\tconst exportName = source.export ?? \"default\";\n\t// Vision nodes load their cached models, and onnxruntime-web's files when\n\t// the project has it installed.\n\tconst modelsDir = visionModelsDir();\n\tconst ort = onnxRuntimeWeb(path.dirname(entry));\n\tconst roots = [\n\t\t...(options.root === undefined\n\t\t\t? [repoRoot(path.dirname(entry))]\n\t\t\t: Array.isArray(options.root)\n\t\t\t\t? options.root\n\t\t\t\t: [options.root]),\n\t\tmodelsDir,\n\t\tort.dir,\n\t]\n\t\t.filter((r): r is string => !!r)\n\t\t.map((r) => path.resolve(r));\n\tconst sseClients = new Set<http.ServerResponse>();\n\tconst idleCloseMs = options.idleCloseMs ?? 5000;\n\tlet idleTimer: NodeJS.Timeout | undefined;\n\tconst title = options.title ?? \"framefields preview\";\n\tconst session = randomUUID();\n\tconst log =\n\t\toptions.log === false\n\t\t\t? () => {}\n\t\t\t: (options.log ?? ((line: string) => console.error(line)));\n\tconst say = (line: string) => log(`[framefields preview] ${line}`);\n\t// Each distinct page error is reported once; playback repeats them per frame.\n\tconst reported = new Set<string>();\n\tlet pageOpened = false;\n\n\tconst notes =\n\t\toptions.notesDir === false\n\t\t\t? undefined\n\t\t\t: new NoteStore(\n\t\t\t\t\tpath.resolve(\n\t\t\t\t\t\toptions.notesDir ??\n\t\t\t\t\t\t\tpath.join(process.cwd(), \".framefields\", \"preview-notes\"),\n\t\t\t\t\t),\n\t\t\t\t);\n\t// Changes run one at a time: each reads and rewrites notes.json.\n\tlet noteQueue: Promise<unknown> = Promise.resolve();\n\n\tconst target = await loadTarget(entry, exportName);\n\tconst meta = await resolveMeta(target, title, session, notes?.dir ?? null);\n\t// The project's own code, bundled against the browser engine; the engine\n\t// itself is prebuilt (or built once, running from source).\n\tlet bundle: Promise<string> | undefined;\n\tconst playerBundle = () => {\n\t\tif (!bundle) {\n\t\t\tconst built = bundleProject({\n\t\t\t\tentry,\n\t\t\t\texportName,\n\t\t\t\tmodelsDir,\n\t\t\t\tortBase: ort.base,\n\t\t\t}).then((b) => b.code);\n\t\t\tbundle = built;\n\t\t\tbuilt.catch((err: unknown) => {\n\t\t\t\tsay(\n\t\t\t\t\t`could not build ${path.relative(process.cwd(), entry)}:\\n${errorText(err)}`,\n\t\t\t\t);\n\t\t\t\t// Let a later request try again, unless it started its own build.\n\t\t\t\tif (bundle === built) bundle = undefined;\n\t\t\t});\n\t\t}\n\t\treturn bundle;\n\t};\n\tconst engine = engineDir();\n\tengine.catch(() => {});\n\n\t// ── Audio: mixed once, by the export engine ──\n\tlet audio: AudioStatus = {\n\t\tstate: options.audio === false ? \"none\" : \"mixing\",\n\t};\n\tlet audioWav: Buffer | undefined;\n\n\tfunction broadcast(event: string, data: unknown) {\n\t\tfor (const client of sseClients) {\n\t\t\tclient.write(`event: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`);\n\t\t}\n\t}\n\n\tfunction setAudio(next: AudioStatus) {\n\t\taudio = next;\n\t\tbroadcast(\"audio\", audio);\n\t}\n\n\tfunction startAudio() {\n\t\tif (audio.state !== \"mixing\") return;\n\t\tif (!target.toVirtualMediaAsync && !target.toVirtualMedia) {\n\t\t\tsetAudio({ state: \"none\" });\n\t\t\treturn;\n\t\t}\n\t\tconst renderer = options.renderer ?? new HeadlessMediaRenderer();\n\t\t(async () => {\n\t\t\tconst vm = target.toVirtualMediaAsync\n\t\t\t\t? await target.toVirtualMediaAsync()\n\t\t\t\t: (target.toVirtualMedia as () => VirtualMediaData)();\n\t\t\treturn renderer.renderAudio(vm, { fps: meta.fps });\n\t\t})().then(\n\t\t\t({ channels, sampleRate }) => {\n\t\t\t\tconst left = channels[0] ?? new Float32Array(0);\n\t\t\t\tconst right = channels[1] ?? left;\n\t\t\t\tconst peaks = computePeaks(left, right, PEAK_BINS);\n\t\t\t\tif (left.length === 0 || Math.max(...peaks) < 0.001) {\n\t\t\t\t\tsetAudio({ state: \"none\" });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\taudioWav = encodeStereoWav([left, right], { sampleRate });\n\t\t\t\tsetAudio({\n\t\t\t\t\tstate: \"ready\",\n\t\t\t\t\tdurationSec: left.length / sampleRate,\n\t\t\t\t\tpeaks: peaks.map((p) => Math.round(p * 1000) / 1000),\n\t\t\t\t});\n\t\t\t},\n\t\t\t(err: unknown) => {\n\t\t\t\tsetAudio({\n\t\t\t\t\tstate: \"error\",\n\t\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t\t});\n\t\t\t},\n\t\t);\n\t}\n\n\tlet resolveClosed!: (reason: PreviewCloseReason) => void;\n\tconst closed = new Promise<PreviewCloseReason>((r) => {\n\t\tresolveClosed = r;\n\t});\n\tlet closing: Promise<void> | undefined;\n\tfunction close(reason: PreviewCloseReason = \"closed\"): Promise<void> {\n\t\tclosing ??= new Promise<void>((resolve) => {\n\t\t\tclearTimeout(idleTimer);\n\t\t\tif (reason === \"idle\") say(\"stopped: its tab was closed\");\n\t\t\tif (reason === \"replaced\") say(\"stopped: a newer preview took over\");\n\t\t\tfor (const client of sseClients) client.end();\n\t\t\tsseClients.clear();\n\t\t\tserver.close(() => {\n\t\t\t\tresolve();\n\t\t\t\tresolveClosed(reason);\n\t\t\t});\n\t\t\tserver.closeAllConnections();\n\t\t});\n\t\treturn closing;\n\t}\n\n\tconst server = http.createServer(async (req, res) => {\n\t\tconst url = new URL(req.url ?? \"/\", \"http://localhost\");\n\t\t// Cross-origin isolation: lets onnxruntime-web use SharedArrayBuffer and\n\t\t// threads for vision models (single-threaded otherwise). \"credentialless\"\n\t\t// keeps cross-origin assets such as the font stylesheet loading.\n\t\tres.setHeader(\"cross-origin-opener-policy\", \"same-origin\");\n\t\tres.setHeader(\"cross-origin-embedder-policy\", \"credentialless\");\n\t\ttry {\n\t\t\tif (req.method === \"GET\" && url.pathname === \"/\") {\n\t\t\t\tres.writeHead(200, {\n\t\t\t\t\t\"content-type\": \"text/html; charset=utf-8\",\n\t\t\t\t\t\"cache-control\": \"no-store\",\n\t\t\t\t});\n\t\t\t\tres.end(previewPage);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (req.method === \"GET\" && url.pathname === \"/meta\") {\n\t\t\t\tsendJson(res, 200, { ...meta, audio });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (req.method === \"GET\" && url.pathname.startsWith(`${ENGINE_URL}/`)) {\n\t\t\t\tconst dir = await engine;\n\t\t\t\tconst file = path.resolve(\n\t\t\t\t\tdir,\n\t\t\t\t\t`.${url.pathname.slice(ENGINE_URL.length)}`,\n\t\t\t\t);\n\t\t\t\tif (isInside(dir, file) && (await isFile(file))) {\n\t\t\t\t\tawait sendFile(req, res, file);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tsendJson(res, 404, { error: \"not found\" });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (req.method === \"GET\" && url.pathname === PLAYER_PATH) {\n\t\t\t\tlet code: string;\n\t\t\t\ttry {\n\t\t\t\t\tawait engine;\n\t\t\t\t\tcode = await playerBundle();\n\t\t\t\t} catch (err) {\n\t\t\t\t\t// Shown on the page: usually an error in the composition's code.\n\t\t\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tcode = `throw new Error(${JSON.stringify(`Could not build the preview:\\n${message}`)});`;\n\t\t\t\t}\n\t\t\t\tres.writeHead(200, {\n\t\t\t\t\t\"content-type\": \"text/javascript; charset=utf-8\",\n\t\t\t\t\t\"cache-control\": \"no-store\",\n\t\t\t\t});\n\t\t\t\tres.end(code);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// `fs.existsSync` / `statSync` in the page: a missing file is an\n\t\t\t// answer here, not a 404 the browser logs as an error.\n\t\t\tif (req.method === \"GET\" && url.pathname === \"/@framefields/stat\") {\n\t\t\t\tconst file = path.resolve(url.searchParams.get(\"path\") ?? \"\");\n\t\t\t\tlet size = -1;\n\t\t\t\tif (roots.some((r) => isInside(r, file))) {\n\t\t\t\t\tsize = await fs.promises.stat(file).then(\n\t\t\t\t\t\t(st) => (st.isFile() ? st.size : -1),\n\t\t\t\t\t\t() => -1,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tsendJson(res, 200, { exists: size >= 0, size: Math.max(0, size) });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (req.method === \"GET\" && url.pathname === \"/events\") {\n\t\t\t\tres.writeHead(200, {\n\t\t\t\t\t\"content-type\": \"text/event-stream\",\n\t\t\t\t\t\"cache-control\": \"no-store\",\n\t\t\t\t\tconnection: \"keep-alive\",\n\t\t\t\t});\n\t\t\t\t// Reconnect fast: a tab left open finds the next preview's\n\t\t\t\t// server within a second of it starting.\n\t\t\t\tres.write(\"retry: 1000\\n\");\n\t\t\t\tres.write(\n\t\t\t\t\t`event: hello\\ndata: ${JSON.stringify({ session: meta.session })}\\n\\n`,\n\t\t\t\t);\n\t\t\t\tsseClients.add(res);\n\t\t\t\tclearTimeout(idleTimer);\n\t\t\t\tif (!pageOpened) {\n\t\t\t\t\tpageOpened = true;\n\t\t\t\t\tsay(\n\t\t\t\t\t\tidleCloseMs === false\n\t\t\t\t\t\t\t? \"page opened\"\n\t\t\t\t\t\t\t: `page opened; the server stops ${Number((idleCloseMs / 1000).toFixed(1))} s after its last tab closes`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tonClient?.();\n\t\t\t\treq.on(\"close\", () => {\n\t\t\t\t\tsseClients.delete(res);\n\t\t\t\t\t// A reload reconnects within a second; a closed tab doesn't.\n\t\t\t\t\tif (sseClients.size === 0 && idleCloseMs !== false && !closing) {\n\t\t\t\t\t\tidleTimer = setTimeout(() => {\n\t\t\t\t\t\t\tif (sseClients.size === 0) void close(\"idle\");\n\t\t\t\t\t\t}, idleCloseMs);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// A newer preview of the same project wants this port. The custom\n\t\t\t// header keeps web pages from triggering it (it forces a CORS preflight).\n\t\t\tif (\n\t\t\t\treq.method === \"POST\" &&\n\t\t\t\turl.pathname === \"/takeover\" &&\n\t\t\t\treq.headers[TAKEOVER_HEADER] === \"1\"\n\t\t\t) {\n\t\t\t\tsendJson(res, 200, { ok: true });\n\t\t\t\tsetImmediate(() => void close(\"replaced\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Errors in the page, so they reach the terminal (and an agent)\n\t\t\t// without anyone looking at the browser. JSON only: other sites\n\t\t\t// can't send that without a preflight this server doesn't answer.\n\t\t\tif (\n\t\t\t\treq.method === \"POST\" &&\n\t\t\t\turl.pathname === \"/@framefields/log\" &&\n\t\t\t\treq.headers[\"content-type\"] === \"application/json\"\n\t\t\t) {\n\t\t\t\tconst body = await readBody(req, 16 * 1024);\n\t\t\t\tconst { message, level } = JSON.parse(body) as {\n\t\t\t\t\tmessage?: unknown;\n\t\t\t\t\tlevel?: unknown;\n\t\t\t\t};\n\t\t\t\tconst text = String(message ?? \"\").trim();\n\t\t\t\tif (text && !reported.has(text) && reported.size < 200) {\n\t\t\t\t\treported.add(text);\n\t\t\t\t\tsay(\n\t\t\t\t\t\t`${level === \"warning\" ? \"warning\" : \"error\"} in the page: ${text}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tres.writeHead(204).end();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (notes && url.pathname === \"/@framefields/notes\") {\n\t\t\t\tif (req.method === \"GET\") {\n\t\t\t\t\tsendJson(res, 200, await notes.list());\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (\n\t\t\t\t\treq.method === \"POST\" &&\n\t\t\t\t\treq.headers[\"content-type\"] === \"application/json\"\n\t\t\t\t) {\n\t\t\t\t\tconst body = JSON.parse(await readBody(req, 16 * 1024 * 1024)) as {\n\t\t\t\t\t\top?: string;\n\t\t\t\t\t\tid?: number;\n\t\t\t\t\t\tdone?: boolean;\n\t\t\t\t\t\ttext?: string;\n\t\t\t\t\t\tnote?: NewNote;\n\t\t\t\t\t};\n\t\t\t\t\tconst change = noteQueue.then(() =>\n\t\t\t\t\t\tchangeNote(notes, body, meta.fps, say),\n\t\t\t\t\t);\n\t\t\t\t\tnoteQueue = change.catch(() => {});\n\t\t\t\t\tconst note = await change;\n\t\t\t\t\tbroadcast(\"notes\", await notes.list());\n\t\t\t\t\tsendJson(res, note ? 200 : 404, note ?? { error: \"no such note\" });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (req.method === \"GET\" && url.pathname === \"/audio\") {\n\t\t\t\tif (audio.state !== \"ready\" || !audioWav) {\n\t\t\t\t\tsendJson(res, 404, { error: `audio is ${audio.state}` });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tres.writeHead(200, {\n\t\t\t\t\t\"content-type\": \"audio/wav\",\n\t\t\t\t\t\"content-length\": audioWav.length,\n\t\t\t\t\t\"cache-control\": \"no-store\",\n\t\t\t\t});\n\t\t\t\tres.end(audioWav);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Fonts referenced by family only resolve to assets/fonts/<Family>.ttf,\n\t\t\t// read from the project's folder, as the export engine does.\n\t\t\tconst fontMatch = /^\\/assets\\/fonts\\/([^/]+)$/.exec(url.pathname);\n\t\t\tif (fontMatch && (req.method === \"GET\" || req.method === \"HEAD\")) {\n\t\t\t\tconst file = path.join(\n\t\t\t\t\tprocess.cwd(),\n\t\t\t\t\t\"assets\",\n\t\t\t\t\t\"fonts\",\n\t\t\t\t\tdecodeURIComponent(fontMatch[1]),\n\t\t\t\t);\n\t\t\t\tif (await isFile(file)) {\n\t\t\t\t\tawait sendFile(req, res, file);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\t// The composition's files, by absolute path: what the program\n\t\t\t// references loads as-is.\n\t\t\tif (req.method === \"GET\" || req.method === \"HEAD\") {\n\t\t\t\t// The engine asks for local files under Vite's `/@fs/` prefix.\n\t\t\t\tconst file = path.resolve(\n\t\t\t\t\tdecodeURIComponent(url.pathname.replace(/^\\/@fs(?=\\/)/, \"\")),\n\t\t\t\t);\n\t\t\t\tif (roots.some((r) => isInside(r, file)) && (await isFile(file))) {\n\t\t\t\t\tawait sendFile(req, res, file);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\tsendJson(res, 404, { error: \"not found\" });\n\t\t} catch (err) {\n\t\t\tif (!res.headersSent) {\n\t\t\t\tsendJson(res, 500, {\n\t\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tres.end();\n\t\t\t}\n\t\t}\n\t});\n\n\tlet onClient: (() => void) | undefined;\n\tconst host = options.host ?? \"127.0.0.1\";\n\tconst port = options.port ?? stablePort();\n\tconst listen = (p: number) =>\n\t\tnew Promise<void>((resolve, reject) => {\n\t\t\tconst fail = (err: Error) => {\n\t\t\t\tserver.off(\"listening\", ok);\n\t\t\t\treject(err);\n\t\t\t};\n\t\t\tconst ok = () => {\n\t\t\t\tserver.off(\"error\", fail);\n\t\t\t\tresolve();\n\t\t\t};\n\t\t\tserver.once(\"error\", fail);\n\t\t\tserver.once(\"listening\", ok);\n\t\t\tserver.listen(p, host);\n\t\t});\n\ttry {\n\t\tawait listen(port);\n\t} catch (err) {\n\t\tif ((err as NodeJS.ErrnoException).code !== \"EADDRINUSE\") throw err;\n\t\t// Most likely the previous preview of this project: ask it to step aside.\n\t\tif (await requestTakeover(host, port)) {\n\t\t\tawait listen(port).catch(() => listen(0));\n\t\t} else if (options.port === undefined) {\n\t\t\tawait listen(0);\n\t\t} else {\n\t\t\tthrow err;\n\t\t}\n\t}\n\tconst { port: actualPort } = server.address() as AddressInfo;\n\tconst url = `http://${host.includes(\":\") ? `[${host}]` : host}:${actualPort}/`;\n\n\t// Build the page's bundle and mix the soundtrack while the browser opens.\n\tvoid playerBundle().catch(() => {});\n\tstartAudio();\n\tif (options.open === true) {\n\t\t// A tab from the last preview reconnects within ~1 s and reloads itself;\n\t\t// only open a new one when nobody comes back.\n\t\tconst reconnected = await new Promise<boolean>((resolve) => {\n\t\t\tconst timer = setTimeout(() => resolve(false), 2000);\n\t\t\tonClient = () => {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\tresolve(true);\n\t\t\t};\n\t\t\tif (sseClients.size > 0) onClient();\n\t\t});\n\t\tonClient = undefined;\n\t\tif (!reconnected) openBrowser(url);\n\t}\n\n\treturn { url, closed, close: () => close() };\n}\n\nasync function loadTarget(\n\tentry: string,\n\texportName: string,\n): Promise<PreviewTarget> {\n\tconst mod = (await import(pathToFileURL(entry).href)) as Record<\n\t\tstring,\n\t\tunknown\n\t>;\n\tconst exported = mod[exportName];\n\tif (exported === undefined) {\n\t\tthrow new Error(\n\t\t\t`[framefields] ${entry} has no export named \"${exportName}\" to preview.`,\n\t\t);\n\t}\n\treturn (\n\t\ttypeof exported === \"function\"\n\t\t\t? await (exported as () => unknown)()\n\t\t\t: exported\n\t) as PreviewTarget;\n}\n\n/**\n * The project holding `dir`: the nearest `.git`, else the nearest\n * `package.json`, else `dir` itself. A repo wins over a nested package, so a\n * monorepo's shared assets stay reachable.\n */\nfunction repoRoot(dir: string): string {\n\tlet project: string | undefined;\n\tfor (let d = dir; ; d = path.dirname(d)) {\n\t\tif (fs.existsSync(path.join(d, \".git\"))) return d;\n\t\tif (!project && fs.existsSync(path.join(d, \"package.json\"))) project = d;\n\t\tif (path.dirname(d) === d) return project ?? dir;\n\t}\n}\n\n/** Applies one change from the page and says what it was. */\nasync function changeNote(\n\tnotes: NoteStore,\n\tbody: {\n\t\top?: string;\n\t\tid?: number;\n\t\tdone?: boolean;\n\t\ttext?: string;\n\t\tnote?: NewNote;\n\t},\n\tfps: number,\n\tsay: (line: string) => void,\n) {\n\tif (body.op === \"add\" && body.note && String(body.note.text).trim()) {\n\t\tconst note = await notes.add(body.note);\n\t\tconst files = [\n\t\t\tnote.image && `frame with the spot marked: ${note.image}`,\n\t\t\t`all notes: ${notes.file}`,\n\t\t];\n\t\tsay(`${describeNote(note, fps)}\\n ${files.filter(Boolean).join(\" · \")}`);\n\t\treturn note;\n\t}\n\tconst id = Number(body.id);\n\tif (body.op === \"update\") {\n\t\tconst note = await notes.update(id, body);\n\t\tif (note && typeof body.done === \"boolean\")\n\t\t\tsay(`note ${id} marked ${note.done ? \"done\" : \"open\"}`);\n\t\treturn note;\n\t}\n\tif (body.op === \"remove\") {\n\t\tconst note = await notes.remove(id);\n\t\tif (note) say(`note ${id} deleted`);\n\t\treturn note;\n\t}\n\treturn undefined;\n}\n\nfunction errorText(err: unknown): string {\n\treturn err instanceof Error ? err.message : String(err);\n}\n\nfunction readBody(req: http.IncomingMessage, limit: number): Promise<string> {\n\treturn new Promise((resolve, reject) => {\n\t\tlet body = \"\";\n\t\treq.setEncoding(\"utf8\");\n\t\treq.on(\"data\", (chunk: string) => {\n\t\t\tbody += chunk;\n\t\t\tif (body.length > limit) {\n\t\t\t\treject(new Error(\"request body too large\"));\n\t\t\t\treq.destroy();\n\t\t\t}\n\t\t});\n\t\treq.on(\"end\", () => resolve(body));\n\t\treq.on(\"error\", reject);\n\t});\n}\n\nfunction isInside(root: string, file: string): boolean {\n\tconst rel = path.relative(root, file);\n\treturn !!rel && !rel.startsWith(\"..\") && !path.isAbsolute(rel);\n}\n\nasync function isFile(file: string): Promise<boolean> {\n\ttry {\n\t\treturn (await fs.promises.stat(file)).isFile();\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nconst CONTENT_TYPES: Record<string, string> = {\n\t\".png\": \"image/png\",\n\t\".jpg\": \"image/jpeg\",\n\t\".jpeg\": \"image/jpeg\",\n\t\".webp\": \"image/webp\",\n\t\".gif\": \"image/gif\",\n\t\".svg\": \"image/svg+xml\",\n\t\".mp4\": \"video/mp4\",\n\t\".mov\": \"video/quicktime\",\n\t\".webm\": \"video/webm\",\n\t\".mp3\": \"audio/mpeg\",\n\t\".wav\": \"audio/wav\",\n\t\".m4a\": \"audio/mp4\",\n\t\".ogg\": \"audio/ogg\",\n\t\".ttf\": \"font/ttf\",\n\t\".otf\": \"font/otf\",\n\t\".woff\": \"font/woff\",\n\t\".woff2\": \"font/woff2\",\n\t\".json\": \"application/json\",\n\t\".js\": \"text/javascript; charset=utf-8\",\n\t\".mjs\": \"text/javascript; charset=utf-8\",\n\t\".wasm\": \"application/wasm\",\n\t\".onnx\": \"application/octet-stream\",\n\t\".srt\": \"text/plain; charset=utf-8\",\n\t\".txt\": \"text/plain; charset=utf-8\",\n};\n\n/** Serves a file, with byte ranges so video can seek without loading it whole. */\nasync function sendFile(\n\treq: http.IncomingMessage,\n\tres: http.ServerResponse,\n\tfile: string,\n) {\n\tconst { size } = await fs.promises.stat(file);\n\tconst type =\n\t\tCONTENT_TYPES[path.extname(file).toLowerCase()] ??\n\t\t\"application/octet-stream\";\n\tconst range = /^bytes=(\\d*)-(\\d*)$/.exec(req.headers.range ?? \"\");\n\tlet start = 0;\n\tlet end = size - 1;\n\tif (range && (range[1] || range[2])) {\n\t\tif (range[1]) {\n\t\t\tstart = Number(range[1]);\n\t\t\tif (range[2]) end = Math.min(end, Number(range[2]));\n\t\t} else {\n\t\t\tstart = Math.max(0, size - Number(range[2]));\n\t\t}\n\t\tif (start > end || start >= size) {\n\t\t\tres.writeHead(416, { \"content-range\": `bytes */${size}` });\n\t\t\tres.end();\n\t\t\treturn;\n\t\t}\n\t}\n\tres.writeHead(range ? 206 : 200, {\n\t\t\"content-type\": type,\n\t\t\"content-length\": end - start + 1,\n\t\t\"accept-ranges\": \"bytes\",\n\t\t\"cache-control\": \"no-cache\",\n\t\t...(range && { \"content-range\": `bytes ${start}-${end}/${size}` }),\n\t});\n\tif (req.method === \"HEAD\") {\n\t\tres.end();\n\t\treturn;\n\t}\n\tfs.createReadStream(file, { start, end }).pipe(res);\n}\n\nasync function resolveMeta(\n\ttarget: PreviewTarget,\n\ttitle: string,\n\tsession: string,\n\tnotesDir: string | null,\n): Promise<PreviewMeta> {\n\tconst fps = target.fps ?? 24;\n\tconst durationMs =\n\t\ttarget.durationMs ?? (await target.computeDuration?.()) ?? 5000;\n\treturn {\n\t\tsession,\n\t\tnotesDir,\n\t\ttitle,\n\t\tfps,\n\t\tframeCount: Math.max(1, Math.round((durationMs / 1000) * fps)),\n\t\twidth: target.width ?? 1920,\n\t\theight: target.height ?? 1080,\n\t};\n}\n\n/** Same project, same port: 41000–41899, from the working directory. */\nfunction stablePort(): number {\n\tconst digest = createHash(\"sha1\").update(process.cwd()).digest();\n\treturn 41000 + (digest.readUInt16BE(0) % 900);\n}\n\n/** Asks a preview already on `port` to shut down; false if something else is there. */\nasync function requestTakeover(host: string, port: number): Promise<boolean> {\n\t// A fresh connection (no agent): a pooled keep-alive socket may point at a\n\t// server that is already gone.\n\tconst ok = await new Promise<boolean>((resolve) => {\n\t\tconst req = http.request(\n\t\t\t{\n\t\t\t\thost,\n\t\t\t\tport,\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tpath: \"/takeover\",\n\t\t\t\tagent: false,\n\t\t\t\theaders: { [TAKEOVER_HEADER]: \"1\" },\n\t\t\t\ttimeout: 1500,\n\t\t\t},\n\t\t\t(res) => {\n\t\t\t\tres.resume();\n\t\t\t\tresolve(res.statusCode === 200);\n\t\t\t},\n\t\t);\n\t\treq.on(\"timeout\", () => req.destroy());\n\t\treq.on(\"error\", () => resolve(false));\n\t\treq.end();\n\t});\n\t// Give it a moment to release the socket.\n\tif (ok) await new Promise((r) => setTimeout(r, 150));\n\treturn ok;\n}\n\n/** Max absolute amplitude of both channels per bin, 0..1. */\nfunction computePeaks(\n\tleft: Float32Array,\n\tright: Float32Array,\n\tbins: number,\n): number[] {\n\tconst peaks = new Array<number>(bins).fill(0);\n\tif (left.length === 0) return peaks;\n\tconst per = left.length / bins;\n\tfor (let b = 0; b < bins; b++) {\n\t\tconst start = Math.floor(b * per);\n\t\tconst end = Math.min(left.length, Math.floor((b + 1) * per));\n\t\tlet max = 0;\n\t\tfor (let i = start; i < end; i++) {\n\t\t\tconst v = Math.max(Math.abs(left[i] ?? 0), Math.abs(right[i] ?? 0));\n\t\t\tif (v > max) max = v;\n\t\t}\n\t\tpeaks[b] = Math.min(1, max);\n\t}\n\treturn peaks;\n}\n\nfunction sendJson(res: http.ServerResponse, status: number, body: unknown) {\n\tres.writeHead(status, {\n\t\t\"content-type\": \"application/json\",\n\t\t\"cache-control\": \"no-store\",\n\t});\n\tres.end(JSON.stringify(body));\n}\n\nfunction openBrowser(url: string) {\n\tconst [cmd, args] =\n\t\tprocess.platform === \"darwin\"\n\t\t\t? [\"open\", [url]]\n\t\t\t: process.platform === \"win32\"\n\t\t\t\t? [\"cmd\", [\"/c\", \"start\", \"\", url]]\n\t\t\t\t: [\"xdg-open\", [url]];\n\ttry {\n\t\tspawn(cmd, args, { stdio: \"ignore\", detached: true })\n\t\t\t.on(\"error\", () => {})\n\t\t\t.unref();\n\t} catch {}\n}\n"],"mappings":"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"}