framefields 2.4.2 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (504) hide show
  1. package/README.md +26 -1
  2. package/dist/audio/index.d.mts +4 -4
  3. package/dist/audio/index.mjs +13 -13
  4. package/dist/{audio-DJeVoE8b.mjs → audio-BluXgSeG.mjs} +1 -1
  5. package/dist/{audio-DJeVoE8b.mjs.map → audio-BluXgSeG.mjs.map} +1 -1
  6. package/dist/{audio-DHuEMOtQ.d.mts → audio-C1A4N3Wh.d.mts} +3 -3
  7. package/dist/{audio-DHuEMOtQ.d.mts.map → audio-C1A4N3Wh.d.mts.map} +1 -1
  8. package/dist/{audio-Bb31xJuX.mjs → audio-CMHpfOoS.mjs} +4 -4
  9. package/dist/{audio-Bb31xJuX.mjs.map → audio-CMHpfOoS.mjs.map} +1 -1
  10. package/dist/cli.d.mts +1 -0
  11. package/dist/cli.mjs +221 -0
  12. package/dist/cli.mjs.map +1 -0
  13. package/dist/components/index.d.mts +12 -0
  14. package/dist/components/index.mjs +49 -0
  15. package/dist/{dist-CspOG4V1.mjs → dist-BBUrGwqd.mjs} +24 -3
  16. package/dist/{dist-CspOG4V1.mjs.map → dist-BBUrGwqd.mjs.map} +1 -1
  17. package/dist/{dist-SB9qfAko.mjs → dist-CQZ5_60A.mjs} +2 -2
  18. package/dist/{dist-SB9qfAko.mjs.map → dist-CQZ5_60A.mjs.map} +1 -1
  19. package/dist/{dist-0QGroRYZ.mjs → dist-C_SMBmHr.mjs} +1 -1
  20. package/dist/{dist-0QGroRYZ.mjs.map → dist-C_SMBmHr.mjs.map} +1 -1
  21. package/dist/{dist-ClyC_d66.mjs → dist-DFeF-ygz.mjs} +117 -40
  22. package/dist/dist-DFeF-ygz.mjs.map +1 -0
  23. package/dist/{dist-CxGfrigI.mjs → dist-QMVy31gR.mjs} +23 -4
  24. package/dist/dist-QMVy31gR.mjs.map +1 -0
  25. package/dist/effects/index.d.mts +5 -5
  26. package/dist/effects/index.mjs +36 -36
  27. package/dist/{effects-BOk_2kzM.mjs → effects-mRe-rLte.mjs} +71 -3
  28. package/dist/effects-mRe-rLte.mjs.map +1 -0
  29. package/dist/fonts/index.d.mts +1 -1
  30. package/dist/fonts/index.mjs +3 -3
  31. package/dist/{fonts-CsMDfqv9.mjs → fonts-wI3CUYHM.mjs} +2 -2
  32. package/dist/{fonts-CsMDfqv9.mjs.map → fonts-wI3CUYHM.mjs.map} +1 -1
  33. package/dist/{gpu-DtT5I4ME.mjs → gpu-I5fdrflL.mjs} +2 -2
  34. package/dist/{gpu-DtT5I4ME.mjs.map → gpu-I5fdrflL.mjs.map} +1 -1
  35. package/dist/{index-BTPLbjt4.d.mts → index-B-KVpJMb.d.mts} +2 -2
  36. package/dist/{index-BTPLbjt4.d.mts.map → index-B-KVpJMb.d.mts.map} +1 -1
  37. package/dist/index-BJe_GEyj.d.mts +5968 -0
  38. package/dist/index-BJe_GEyj.d.mts.map +1 -0
  39. package/dist/{index-D-l-yCv6.d.mts → index-BpdVOlsV.d.mts} +3 -3
  40. package/dist/{index-D-l-yCv6.d.mts.map → index-BpdVOlsV.d.mts.map} +1 -1
  41. package/dist/index-C2f4Hm-p.d.mts +89 -0
  42. package/dist/index-C2f4Hm-p.d.mts.map +1 -0
  43. package/dist/{index-B-55dXDu.d.mts → index-CalU-mpZ.d.mts} +1 -1
  44. package/dist/{index-B-55dXDu.d.mts.map → index-CalU-mpZ.d.mts.map} +1 -1
  45. package/dist/{index-B2Xesdug.d.mts → index-D6-lH1Mx.d.mts} +477 -327
  46. package/dist/index-D6-lH1Mx.d.mts.map +1 -0
  47. package/dist/{index-B3-gwjK6.d.mts → index-DNKM3to5.d.mts} +3 -3
  48. package/dist/{index-B3-gwjK6.d.mts.map → index-DNKM3to5.d.mts.map} +1 -1
  49. package/dist/{index-DujpgUUq.d.mts → index-fymoeGNW.d.mts} +245 -152
  50. package/dist/index-fymoeGNW.d.mts.map +1 -0
  51. package/dist/index.d.mts +13 -4593
  52. package/dist/index.mjs +59 -6654
  53. package/dist/preview/index.d.mts +4 -90
  54. package/dist/preview/index.mjs +12 -12
  55. package/dist/{preview-DyGw2b3_.mjs → preview--DEgW67P.mjs} +3 -3
  56. package/dist/{preview-DyGw2b3_.mjs.map → preview--DEgW67P.mjs.map} +1 -1
  57. package/dist/preview-engine/audio.mjs +1 -1
  58. package/dist/preview-engine/{chunk-FHKBALVI.mjs → chunk-24H3QVYB.mjs} +1 -1
  59. package/dist/preview-engine/{chunk-SWXVITCQ.mjs → chunk-25T3M6MI.mjs} +1 -1
  60. package/dist/preview-engine/chunk-2AV7HRRL.mjs +174 -0
  61. package/dist/preview-engine/{chunk-HNNIVJ2K.mjs → chunk-4EOS6ZWW.mjs} +1 -1
  62. package/dist/preview-engine/{chunk-67SSYK7D.mjs → chunk-4UI3S3UG.mjs} +1 -1
  63. package/dist/preview-engine/{chunk-LLDIDE5Q.mjs → chunk-5N3GW3NE.mjs} +1 -1
  64. package/dist/preview-engine/{chunk-KAQUNOVB.mjs → chunk-5P764TTF.mjs} +1 -1
  65. package/dist/preview-engine/{chunk-WJJIISQJ.mjs → chunk-7BUKVFWB.mjs} +1 -1
  66. package/dist/preview-engine/{chunk-HUKAC5MJ.mjs → chunk-7EHUT5A3.mjs} +1 -1
  67. package/dist/preview-engine/{chunk-34ACEGNW.mjs → chunk-7HTTYP6T.mjs} +1 -1
  68. package/dist/preview-engine/{chunk-6K7US2WD.mjs → chunk-7JYMNGNK.mjs} +1 -1
  69. package/dist/preview-engine/{chunk-NRAZV5OA.mjs → chunk-A5OUYV3I.mjs} +1 -1
  70. package/dist/preview-engine/{chunk-VONDRBA2.mjs → chunk-AJLPRIXB.mjs} +1 -1
  71. package/dist/preview-engine/{chunk-HDGFNBYK.mjs → chunk-ANZDLSSM.mjs} +1 -1
  72. package/dist/preview-engine/{chunk-FFMPC7QA.mjs → chunk-C2TA65CH.mjs} +1 -1
  73. package/dist/preview-engine/{chunk-7QJUC6YY.mjs → chunk-CBQOJAIS.mjs} +14 -14
  74. package/dist/preview-engine/{chunk-GMGGDADT.mjs → chunk-CEIOI4HO.mjs} +1 -1
  75. package/dist/preview-engine/{chunk-RHRMEBNK.mjs → chunk-DKXKOUQR.mjs} +1 -1
  76. package/dist/preview-engine/chunk-DPIED3EG.mjs +1 -0
  77. package/dist/preview-engine/{chunk-QJZKIY4H.mjs → chunk-ENS4KUJV.mjs} +1 -1
  78. package/dist/preview-engine/{chunk-Z6UKUHPU.mjs → chunk-HXHP3UHZ.mjs} +1 -1
  79. package/dist/preview-engine/chunk-I6E3AKLS.mjs +1 -0
  80. package/dist/preview-engine/{chunk-2DDBSI75.mjs → chunk-ILKBHDFB.mjs} +1 -1
  81. package/dist/preview-engine/{chunk-O6NRIXS5.mjs → chunk-JMOJKT5W.mjs} +1 -1
  82. package/dist/preview-engine/{chunk-PZF6T7DM.mjs → chunk-KKBJ7X3T.mjs} +1 -1
  83. package/dist/preview-engine/{chunk-NNQUIVTE.mjs → chunk-KRVXRP5F.mjs} +1 -1
  84. package/dist/preview-engine/{chunk-FLPSWDX4.mjs → chunk-MJN3LOZ7.mjs} +1 -1
  85. package/dist/preview-engine/{chunk-GQLZXCW3.mjs → chunk-MLW6Y4RN.mjs} +1 -1
  86. package/dist/preview-engine/{chunk-KT35IPCF.mjs → chunk-MRDJNDYV.mjs} +1 -1
  87. package/dist/preview-engine/{chunk-RKVB47IA.mjs → chunk-MZAAJXUL.mjs} +1 -1
  88. package/dist/preview-engine/{chunk-FFGBUS76.mjs → chunk-NB55FW7D.mjs} +1 -1
  89. package/dist/preview-engine/{chunk-JX22B2FS.mjs → chunk-NG5WSR3A.mjs} +1 -1
  90. package/dist/preview-engine/{chunk-XOEEGC4Z.mjs → chunk-NVTPJUNG.mjs} +1 -1
  91. package/dist/preview-engine/{chunk-ZKPQGQE3.mjs → chunk-NXKXOSE6.mjs} +1 -1
  92. package/dist/preview-engine/{chunk-WCZ5DKZ4.mjs → chunk-NYEDWUFS.mjs} +1 -1
  93. package/dist/preview-engine/{chunk-WACWOKEP.mjs → chunk-O6LGKH4S.mjs} +1 -1
  94. package/dist/preview-engine/chunk-OLLY3Q7M.mjs +1 -0
  95. package/dist/preview-engine/{chunk-A5OVKZ52.mjs → chunk-OOJAQBS3.mjs} +1 -1
  96. package/dist/preview-engine/{chunk-6AEU4DC7.mjs → chunk-PJN7DD55.mjs} +3 -3
  97. package/dist/preview-engine/{chunk-R26SY5D4.mjs → chunk-QCUGWPYP.mjs} +1 -1
  98. package/dist/preview-engine/{chunk-LK3T65RB.mjs → chunk-QQJPPGH5.mjs} +1 -1
  99. package/dist/preview-engine/{chunk-3ACCCPBN.mjs → chunk-QVUT3QNF.mjs} +1 -1
  100. package/dist/preview-engine/chunk-RNFYEZLL.mjs +1 -0
  101. package/dist/preview-engine/{chunk-GNG4YH2W.mjs → chunk-TA5OBYRR.mjs} +1 -1
  102. package/dist/preview-engine/{chunk-NQZKBFC6.mjs → chunk-V6DAD4S4.mjs} +14 -14
  103. package/dist/preview-engine/{chunk-5RQHNB6Y.mjs → chunk-VRNN6H6Y.mjs} +1 -1
  104. package/dist/preview-engine/{chunk-C6Z7H2RZ.mjs → chunk-VTE3IISK.mjs} +1 -1
  105. package/dist/preview-engine/{chunk-RDP6QROU.mjs → chunk-VZGDX7II.mjs} +1 -1
  106. package/dist/preview-engine/{chunk-DOCSX45C.mjs → chunk-XJ4CQIZZ.mjs} +1 -1
  107. package/dist/preview-engine/{chunk-7LVOSEGN.mjs → chunk-XYRBFLFF.mjs} +1 -1
  108. package/dist/preview-engine/{chunk-LIQA5OOU.mjs → chunk-YMQMPBTL.mjs} +1 -1
  109. package/dist/preview-engine/{chunk-G4J662EL.mjs → chunk-Z4ZO4RVT.mjs} +1 -1
  110. package/dist/preview-engine/{chunk-2GIWWDE4.mjs → chunk-ZFURUTVW.mjs} +1 -1
  111. package/dist/preview-engine/effects.mjs +1 -1
  112. package/dist/preview-engine/framefields.mjs +30 -28
  113. package/dist/preview-engine/player.mjs +5 -5
  114. package/dist/preview-engine/renderers-2CBJYNOC.mjs +1 -0
  115. package/dist/preview-engine/renderers-37CN7O4S.mjs +1 -0
  116. package/dist/preview-engine/renderers-3AZ5PMGC.mjs +1 -0
  117. package/dist/preview-engine/renderers-3UGG2IMP.mjs +1 -0
  118. package/dist/preview-engine/renderers-3VXNHICC.mjs +1 -0
  119. package/dist/preview-engine/{renderers-BDWVXYTW.mjs → renderers-3ZYXHUSW.mjs} +1 -1
  120. package/dist/preview-engine/renderers-4FONYSBA.mjs +1 -0
  121. package/dist/preview-engine/renderers-54I5SFXR.mjs +1 -0
  122. package/dist/preview-engine/renderers-5LXALKB4.mjs +1 -0
  123. package/dist/preview-engine/renderers-5TICPV2K.mjs +1 -0
  124. package/dist/preview-engine/renderers-5YS5WHAV.mjs +1 -0
  125. package/dist/preview-engine/renderers-6UTRYW7Z.mjs +1 -0
  126. package/dist/preview-engine/{renderers-R4WMM6IX.mjs → renderers-6ZCFR7BL.mjs} +1 -1
  127. package/dist/preview-engine/renderers-AYEWAW7F.mjs +1 -0
  128. package/dist/preview-engine/{renderers-75TL7RLS.mjs → renderers-BOT4H6KJ.mjs} +1 -1
  129. package/dist/preview-engine/{renderers-NJLGPPX7.mjs → renderers-C44CEIYB.mjs} +1 -1
  130. package/dist/preview-engine/{renderers-UDOBX433.mjs → renderers-C4G7X7LD.mjs} +1 -1
  131. package/dist/preview-engine/renderers-CLCPP3TZ.mjs +1 -0
  132. package/dist/preview-engine/{renderers-RKZGYWDG.mjs → renderers-DBRC3SQE.mjs} +1 -1
  133. package/dist/preview-engine/{renderers-HZHEGONW.mjs → renderers-DSICQMRF.mjs} +1 -1
  134. package/dist/preview-engine/renderers-DTZ2AER5.mjs +1 -0
  135. package/dist/preview-engine/renderers-DUF2CTX6.mjs +1 -0
  136. package/dist/preview-engine/{renderers-AZYJFJLO.mjs → renderers-EOIMH7UE.mjs} +1 -1
  137. package/dist/preview-engine/renderers-EOMMSENQ.mjs +1 -0
  138. package/dist/preview-engine/{renderers-CK3ZL3GM.mjs → renderers-GSRKNT75.mjs} +1 -1
  139. package/dist/preview-engine/renderers-GUJTPO37.mjs +1 -0
  140. package/dist/preview-engine/{renderers-TCNPEANL.mjs → renderers-HE42AT5T.mjs} +1 -1
  141. package/dist/preview-engine/{renderers-GUPEJK3O.mjs → renderers-JM7JEVBN.mjs} +1 -1
  142. package/dist/preview-engine/{renderers-FKH5S64C.mjs → renderers-JRU2GNPE.mjs} +1 -1
  143. package/dist/preview-engine/{renderers-IHRFEZNP.mjs → renderers-K6J77OI5.mjs} +1 -1
  144. package/dist/preview-engine/{renderers-IJDQJEE5.mjs → renderers-L7EITUCD.mjs} +1 -1
  145. package/dist/preview-engine/renderers-LGUHDWWB.mjs +1 -0
  146. package/dist/preview-engine/renderers-LGXD24IJ.mjs +1 -0
  147. package/dist/preview-engine/renderers-MCTBJEL7.mjs +1 -0
  148. package/dist/preview-engine/renderers-OBXNUQMP.mjs +1 -0
  149. package/dist/preview-engine/renderers-OZGEXW2X.mjs +1 -0
  150. package/dist/preview-engine/{renderers-7CPYTCNW.mjs → renderers-PJGFJGJS.mjs} +1 -1
  151. package/dist/preview-engine/{renderers-SW7FIEKO.mjs → renderers-PPGOYOID.mjs} +1 -1
  152. package/dist/preview-engine/renderers-PUJRHBUW.mjs +1 -0
  153. package/dist/preview-engine/renderers-PVUROEGR.mjs +1 -0
  154. package/dist/preview-engine/renderers-QBVVTQV6.mjs +1 -0
  155. package/dist/preview-engine/{renderers-K7B5PQBT.mjs → renderers-QPHOG7QR.mjs} +1 -1
  156. package/dist/preview-engine/{renderers-WR6MKT5S.mjs → renderers-RV62K5JC.mjs} +1 -1
  157. package/dist/preview-engine/{renderers-UMPOVFRA.mjs → renderers-SBX64ERA.mjs} +1 -1
  158. package/dist/preview-engine/renderers-SCJTTOEJ.mjs +1 -0
  159. package/dist/preview-engine/{renderers-FM6A5YR4.mjs → renderers-SD35PBEM.mjs} +1 -1
  160. package/dist/preview-engine/{renderers-LVLVJHOL.mjs → renderers-SIHL5GLI.mjs} +1 -1
  161. package/dist/preview-engine/renderers-SNUVYUDE.mjs +1 -0
  162. package/dist/preview-engine/renderers-SPW4SWZ2.mjs +228 -0
  163. package/dist/preview-engine/renderers-SRKCF3LD.mjs +1 -0
  164. package/dist/preview-engine/{renderers-GAIUTII6.mjs → renderers-T3S7QVM4.mjs} +1 -1
  165. package/dist/preview-engine/renderers-TBP2EKJ4.mjs +1 -0
  166. package/dist/preview-engine/renderers-UN6YLFHW.mjs +1 -0
  167. package/dist/preview-engine/renderers-UTWKTBZI.mjs +1 -0
  168. package/dist/preview-engine/{renderers-6JZ4HYIU.mjs → renderers-V5MJJH33.mjs} +1 -1
  169. package/dist/preview-engine/{renderers-XKHNBARD.mjs → renderers-V67L3MCY.mjs} +1 -1
  170. package/dist/preview-engine/{renderers-FADYGSLK.mjs → renderers-VYK2BJID.mjs} +1 -1
  171. package/dist/preview-engine/renderers-W6762MNW.mjs +1 -0
  172. package/dist/preview-engine/renderers-W6ZTVA2I.mjs +1 -0
  173. package/dist/preview-engine/renderers-WDV7ENJA.mjs +1 -0
  174. package/dist/preview-engine/{renderers-MBSOKWKB.mjs → renderers-WPBTAU2E.mjs} +1 -1
  175. package/dist/preview-engine/renderers-XP7YFWBV.mjs +1 -0
  176. package/dist/preview-engine/renderers-XVUOO7B6.mjs +1 -0
  177. package/dist/preview-engine/renderers-YPFM5UJO.mjs +1 -0
  178. package/dist/preview-engine/renderers-YW3LBALW.mjs +1 -0
  179. package/dist/preview-engine/{renderers-4AJWVIJG.mjs → renderers-ZAPIBQZC.mjs} +1 -1
  180. package/dist/preview-engine/renderers-ZRR4TUVQ.mjs +1 -0
  181. package/dist/preview-engine/{renderers-TY7DVI7E.mjs → renderers-ZVKXIQ3E.mjs} +1 -1
  182. package/dist/preview-engine/signals.mjs +1 -1
  183. package/dist/{program-C2cJayA1.mjs → program-CEHCeDpG.mjs} +204 -61
  184. package/dist/program-CEHCeDpG.mjs.map +1 -0
  185. package/dist/registry/annotate.json +30 -0
  186. package/dist/registry/app-frame.json +38 -0
  187. package/dist/registry/backdrop.json +37 -0
  188. package/dist/registry/before-after.json +33 -0
  189. package/dist/registry/callout.json +30 -0
  190. package/dist/registry/captions.json +37 -0
  191. package/dist/registry/card-stack.json +30 -0
  192. package/dist/registry/chat-thread.json +32 -0
  193. package/dist/registry/code-block.json +45 -0
  194. package/dist/registry/confetti.json +29 -0
  195. package/dist/registry/counter.json +36 -0
  196. package/dist/registry/device-frame.json +36 -0
  197. package/dist/registry/focus-zoom.json +32 -0
  198. package/dist/registry/index.json +408 -0
  199. package/dist/registry/logo-reveal.json +37 -0
  200. package/dist/registry/lower-third.json +34 -0
  201. package/dist/registry/meter.json +35 -0
  202. package/dist/registry/mouse-pointer.json +44 -0
  203. package/dist/registry/selection.json +31 -0
  204. package/dist/registry/spotlight.json +28 -0
  205. package/dist/registry/terminal.json +40 -0
  206. package/dist/registry/text-reveal.json +39 -0
  207. package/dist/registry/title-card.json +34 -0
  208. package/dist/registry/toasts.json +28 -0
  209. package/dist/registry/touch-point.json +33 -0
  210. package/dist/registry/waveform.json +29 -0
  211. package/dist/registry/word-cycle.json +32 -0
  212. package/dist/renderer/index.d.mts +2 -2
  213. package/dist/renderer/index.mjs +3 -3
  214. package/dist/{renderer-BMKFzfSQ.mjs → renderer--wMbTRFg.mjs} +4 -4
  215. package/dist/{renderer-BMKFzfSQ.mjs.map → renderer--wMbTRFg.mjs.map} +1 -1
  216. package/dist/{renderer-CDUxAtsk.mjs → renderer-3aLw-la1.mjs} +5 -5
  217. package/dist/{renderer-CDUxAtsk.mjs.map → renderer-3aLw-la1.mjs.map} +1 -1
  218. package/dist/renderer-7vCBWQVP.mjs +5 -0
  219. package/dist/{renderer-DILUmb-e.mjs → renderer-AkRxGnFU.mjs} +5 -5
  220. package/dist/{renderer-DILUmb-e.mjs.map → renderer-AkRxGnFU.mjs.map} +1 -1
  221. package/dist/renderer-B-XgGMCq.mjs +5 -0
  222. package/dist/renderer-B0yabPlf.mjs +5 -0
  223. package/dist/{renderer-CoWfz-9p.mjs → renderer-B1yPBJQc.mjs} +74 -74
  224. package/dist/{renderer-CoWfz-9p.mjs.map → renderer-B1yPBJQc.mjs.map} +1 -1
  225. package/dist/{renderer-BPixWnmY.mjs → renderer-B41jTHFs.mjs} +6 -6
  226. package/dist/{renderer-BPixWnmY.mjs.map → renderer-B41jTHFs.mjs.map} +1 -1
  227. package/dist/{renderer-BTd2sWDC.mjs → renderer-B5HG6Czb.mjs} +4 -4
  228. package/dist/{renderer-BTd2sWDC.mjs.map → renderer-B5HG6Czb.mjs.map} +1 -1
  229. package/dist/{renderer-gRMkQabv.mjs → renderer-B5oJN3A5.mjs} +5 -5
  230. package/dist/{renderer-gRMkQabv.mjs.map → renderer-B5oJN3A5.mjs.map} +1 -1
  231. package/dist/renderer-B9MYevrF.mjs +5 -0
  232. package/dist/renderer-BA2Ko4H1.mjs +3 -0
  233. package/dist/renderer-BCGosndi.mjs +5 -0
  234. package/dist/{renderer-CzRqzp8P.mjs → renderer-BEmDSqXg.mjs} +4 -4
  235. package/dist/{renderer-CzRqzp8P.mjs.map → renderer-BEmDSqXg.mjs.map} +1 -1
  236. package/dist/{renderer-Csss5GFQ.mjs → renderer-BG8jNTHr.mjs} +3 -3
  237. package/dist/{renderer-Csss5GFQ.mjs.map → renderer-BG8jNTHr.mjs.map} +1 -1
  238. package/dist/{renderer-BTigwMQp.mjs → renderer-BMnrBPh1.mjs} +4 -4
  239. package/dist/{renderer-BTigwMQp.mjs.map → renderer-BMnrBPh1.mjs.map} +1 -1
  240. package/dist/{renderer-yyUpCeWb.mjs → renderer-BORzpTQP.mjs} +3 -3
  241. package/dist/{renderer-yyUpCeWb.mjs.map → renderer-BORzpTQP.mjs.map} +1 -1
  242. package/dist/{renderer-RDCax5CK.mjs → renderer-BWtI3wKu.mjs} +4 -4
  243. package/dist/{renderer-RDCax5CK.mjs.map → renderer-BWtI3wKu.mjs.map} +1 -1
  244. package/dist/renderer-BXTHDEnd.mjs +5 -0
  245. package/dist/{renderer-G1pMJH5C.mjs → renderer-Baw30QRI.mjs} +3 -3
  246. package/dist/{renderer-G1pMJH5C.mjs.map → renderer-Baw30QRI.mjs.map} +1 -1
  247. package/dist/renderer-BbJoZ-zL.mjs +5 -0
  248. package/dist/{renderer-Dwu_zfdL.mjs → renderer-BbdgwkDB.mjs} +5 -5
  249. package/dist/{renderer-Dwu_zfdL.mjs.map → renderer-BbdgwkDB.mjs.map} +1 -1
  250. package/dist/{renderer-D6BXR7P4.mjs → renderer-BgOEqtZu.mjs} +3 -3
  251. package/dist/{renderer-D6BXR7P4.mjs.map → renderer-BgOEqtZu.mjs.map} +1 -1
  252. package/dist/{renderer-NizuhHsH.mjs → renderer-Bv0-WmP4.mjs} +4 -4
  253. package/dist/{renderer-NizuhHsH.mjs.map → renderer-Bv0-WmP4.mjs.map} +1 -1
  254. package/dist/renderer-BvcqncI2.mjs +3 -0
  255. package/dist/{renderer-BljMOvry.mjs → renderer-Bvhj-05p.mjs} +4 -4
  256. package/dist/{renderer-BljMOvry.mjs.map → renderer-Bvhj-05p.mjs.map} +1 -1
  257. package/dist/renderer-BxAZKOQy.mjs +5 -0
  258. package/dist/{renderer-92eLJg24.mjs → renderer-BxDhx8KM.mjs} +5 -5
  259. package/dist/{renderer-92eLJg24.mjs.map → renderer-BxDhx8KM.mjs.map} +1 -1
  260. package/dist/renderer-BxgkH__7.mjs +5 -0
  261. package/dist/{renderer-7y5Cj8rV.mjs → renderer-C1XZ0fQ-.mjs} +4 -4
  262. package/dist/{renderer-7y5Cj8rV.mjs.map → renderer-C1XZ0fQ-.mjs.map} +1 -1
  263. package/dist/{renderer-UcdqCarR.mjs → renderer-C8KEfda3.mjs} +4 -4
  264. package/dist/{renderer-UcdqCarR.mjs.map → renderer-C8KEfda3.mjs.map} +1 -1
  265. package/dist/{renderer-BSitV56h.mjs → renderer-C9r0-KY4.mjs} +10 -1
  266. package/dist/{renderer-BSitV56h.mjs.map → renderer-C9r0-KY4.mjs.map} +1 -1
  267. package/dist/{renderer-B3e4LSE6.mjs → renderer-CF25oNd3.mjs} +5 -5
  268. package/dist/{renderer-B3e4LSE6.mjs.map → renderer-CF25oNd3.mjs.map} +1 -1
  269. package/dist/renderer-CFEvikBH.mjs +5 -0
  270. package/dist/{renderer-DzSMeQBC.mjs → renderer-CHFUJyul.mjs} +5 -5
  271. package/dist/{renderer-DzSMeQBC.mjs.map → renderer-CHFUJyul.mjs.map} +1 -1
  272. package/dist/{renderer-ByCX4JYg.mjs → renderer-CKGOKBZi.mjs} +5 -5
  273. package/dist/{renderer-ByCX4JYg.mjs.map → renderer-CKGOKBZi.mjs.map} +1 -1
  274. package/dist/{renderer-If8JOkBi.mjs → renderer-CNs1vBUy.mjs} +4 -4
  275. package/dist/{renderer-If8JOkBi.mjs.map → renderer-CNs1vBUy.mjs.map} +1 -1
  276. package/dist/{index-B1XcyMm8.d.mts → renderer-COXKPofU.d.mts} +18 -18
  277. package/dist/renderer-COXKPofU.d.mts.map +1 -0
  278. package/dist/{renderer-CNuVJ4nG.mjs → renderer-COZCwiIr.mjs} +3 -3
  279. package/dist/{renderer-CNuVJ4nG.mjs.map → renderer-COZCwiIr.mjs.map} +1 -1
  280. package/dist/{renderer-V5uEyDFz.mjs → renderer-CPmoA87K.mjs} +4 -4
  281. package/dist/{renderer-V5uEyDFz.mjs.map → renderer-CPmoA87K.mjs.map} +1 -1
  282. package/dist/{renderer-Cw6Sagih.mjs → renderer-CYZb9uoO.mjs} +4 -4
  283. package/dist/{renderer-Cw6Sagih.mjs.map → renderer-CYZb9uoO.mjs.map} +1 -1
  284. package/dist/renderer-C_u9B-_7.mjs +5 -0
  285. package/dist/{renderer-BwpgDLYO.mjs → renderer-CaUJlGoB.mjs} +5 -5
  286. package/dist/{renderer-BwpgDLYO.mjs.map → renderer-CaUJlGoB.mjs.map} +1 -1
  287. package/dist/{renderer-DjwITRvg.mjs → renderer-CcS3Zntx.mjs} +3 -3
  288. package/dist/{renderer-BDCy8NgT.mjs → renderer-CddpdCL3.mjs} +4 -4
  289. package/dist/{renderer-BDCy8NgT.mjs.map → renderer-CddpdCL3.mjs.map} +1 -1
  290. package/dist/{renderer-BV48PFjX.mjs → renderer-Cg-d78nf.mjs} +192 -27
  291. package/dist/renderer-Cg-d78nf.mjs.map +1 -0
  292. package/dist/renderer-Cgx_qzGT.mjs +5 -0
  293. package/dist/{renderer-DScyMHpJ.mjs → renderer-ChNCtBMR.mjs} +3 -3
  294. package/dist/{renderer-DScyMHpJ.mjs.map → renderer-ChNCtBMR.mjs.map} +1 -1
  295. package/dist/renderer-ClOkY1Ny.mjs +5 -0
  296. package/dist/{renderer-DyyIXLQK.mjs → renderer-CrU0kbiB.mjs} +5 -5
  297. package/dist/{renderer-DyyIXLQK.mjs.map → renderer-CrU0kbiB.mjs.map} +1 -1
  298. package/dist/renderer-CtDub_ig.mjs +5 -0
  299. package/dist/{renderer-ut1GqW89.mjs → renderer-CyhSWLKq.mjs} +4 -4
  300. package/dist/{renderer-ut1GqW89.mjs.map → renderer-CyhSWLKq.mjs.map} +1 -1
  301. package/dist/{renderer-DaSxr9fM.mjs → renderer-D-bmSKv4.mjs} +5 -5
  302. package/dist/{renderer-DaSxr9fM.mjs.map → renderer-D-bmSKv4.mjs.map} +1 -1
  303. package/dist/renderer-D0duAhCo.mjs +5 -0
  304. package/dist/{renderer-BBAbZfBl.mjs → renderer-D2tdNoM6.mjs} +6 -6
  305. package/dist/{renderer-BBAbZfBl.mjs.map → renderer-D2tdNoM6.mjs.map} +1 -1
  306. package/dist/renderer-D4wM1dqg.mjs +6 -0
  307. package/dist/renderer-D5U3d09T.mjs +5 -0
  308. package/dist/{renderer-B1QREs7J.mjs → renderer-D86Y5YYi.mjs} +5 -5
  309. package/dist/{renderer-B1QREs7J.mjs.map → renderer-D86Y5YYi.mjs.map} +1 -1
  310. package/dist/{renderer-Cbsnj9vV.mjs → renderer-D8D7dKxq.mjs} +4 -4
  311. package/dist/{renderer-Cbsnj9vV.mjs.map → renderer-D8D7dKxq.mjs.map} +1 -1
  312. package/dist/{renderer-D0vom99O.mjs → renderer-DATAjI6x.mjs} +3 -3
  313. package/dist/{renderer-D0vom99O.mjs.map → renderer-DATAjI6x.mjs.map} +1 -1
  314. package/dist/{renderer-X0eKSD7v.mjs → renderer-DGhCIVI4.mjs} +5 -5
  315. package/dist/{renderer-X0eKSD7v.mjs.map → renderer-DGhCIVI4.mjs.map} +1 -1
  316. package/dist/{renderer-CJ4bxCld.mjs → renderer-DILjs1tz.mjs} +3 -3
  317. package/dist/{renderer-DjBCWxYy.mjs → renderer-DIyzKouj.mjs} +4 -4
  318. package/dist/{renderer-DjBCWxYy.mjs.map → renderer-DIyzKouj.mjs.map} +1 -1
  319. package/dist/{renderer-BwwZB2mt.mjs → renderer-DJ78Bgv7.mjs} +5 -5
  320. package/dist/{renderer-BwwZB2mt.mjs.map → renderer-DJ78Bgv7.mjs.map} +1 -1
  321. package/dist/renderer-DKqFxYfP.mjs +5 -0
  322. package/dist/renderer-DQZNI1zf.mjs +5 -0
  323. package/dist/{renderer-B4RpSd4A.mjs → renderer-DVnrUnkh.mjs} +3 -3
  324. package/dist/{renderer-B4RpSd4A.mjs.map → renderer-DVnrUnkh.mjs.map} +1 -1
  325. package/dist/{renderer-C1E5Rt5q.mjs → renderer-DWtquP0s.mjs} +4 -4
  326. package/dist/{renderer-C1E5Rt5q.mjs.map → renderer-DWtquP0s.mjs.map} +1 -1
  327. package/dist/renderer-DXITcVbS.mjs +5 -0
  328. package/dist/renderer-DY1QgdDC.mjs +5 -0
  329. package/dist/renderer-DYEz06xn.mjs +5 -0
  330. package/dist/renderer-D_2OrSMc.mjs +5 -0
  331. package/dist/renderer-DcCl9CO6.mjs +5 -0
  332. package/dist/{renderer-rXgqgAjw.mjs → renderer-DetTl6bQ.mjs} +3 -3
  333. package/dist/{renderer-rXgqgAjw.mjs.map → renderer-DetTl6bQ.mjs.map} +1 -1
  334. package/dist/{renderer-BjCHW9Jp.mjs → renderer-DfxwJVjW.mjs} +3 -3
  335. package/dist/{renderer-Cv4lchUP.mjs → renderer-DhQZiRL3.mjs} +5 -5
  336. package/dist/{renderer-Cv4lchUP.mjs.map → renderer-DhQZiRL3.mjs.map} +1 -1
  337. package/dist/{renderer-EZp_IIPK.mjs → renderer-Djn4BYtz.mjs} +5 -5
  338. package/dist/{renderer-EZp_IIPK.mjs.map → renderer-Djn4BYtz.mjs.map} +1 -1
  339. package/dist/{renderer-Bkrk0M0r.mjs → renderer-DkS6cblv.mjs} +5 -5
  340. package/dist/{renderer-Bkrk0M0r.mjs.map → renderer-DkS6cblv.mjs.map} +1 -1
  341. package/dist/{renderer-DKfwlE4e.mjs → renderer-DkeJefRk.mjs} +5 -5
  342. package/dist/{renderer-DKfwlE4e.mjs.map → renderer-DkeJefRk.mjs.map} +1 -1
  343. package/dist/renderer-DotZXhrh.mjs +5 -0
  344. package/dist/renderer-DpYDVTzs.mjs +5 -0
  345. package/dist/renderer-DpZxLVQF.mjs +5 -0
  346. package/dist/{renderer-Za4D9hej.mjs → renderer-DqabtpGN.mjs} +5 -5
  347. package/dist/{renderer-Za4D9hej.mjs.map → renderer-DqabtpGN.mjs.map} +1 -1
  348. package/dist/{renderer-DaqWTx7s.mjs → renderer-DqdD8gzX.mjs} +5 -5
  349. package/dist/{renderer-DaqWTx7s.mjs.map → renderer-DqdD8gzX.mjs.map} +1 -1
  350. package/dist/{renderer-DZJJHCtm.mjs → renderer-DstROEvX.mjs} +4 -4
  351. package/dist/{renderer-DZJJHCtm.mjs.map → renderer-DstROEvX.mjs.map} +1 -1
  352. package/dist/{renderer-BjvNEbML.mjs → renderer-DuawUm8D.mjs} +3 -3
  353. package/dist/{renderer-BjvNEbML.mjs.map → renderer-DuawUm8D.mjs.map} +1 -1
  354. package/dist/{renderer-CDOobs18.mjs → renderer-DvIWhLnY.mjs} +5 -5
  355. package/dist/{renderer-CDOobs18.mjs.map → renderer-DvIWhLnY.mjs.map} +1 -1
  356. package/dist/renderer-DvJbouz0.mjs +5 -0
  357. package/dist/{renderer-apeDrNXM.mjs → renderer-Dw2ZFwDT.mjs} +5 -5
  358. package/dist/{renderer-apeDrNXM.mjs.map → renderer-Dw2ZFwDT.mjs.map} +1 -1
  359. package/dist/{renderer-CxqPeATd.d.mts → renderer-Dx28wTi1.d.mts} +2 -2
  360. package/dist/{renderer-CxqPeATd.d.mts.map → renderer-Dx28wTi1.d.mts.map} +1 -1
  361. package/dist/{renderer-CcoUNzlQ.mjs → renderer-DxLkZGUK.mjs} +4 -4
  362. package/dist/{renderer-CcoUNzlQ.mjs.map → renderer-DxLkZGUK.mjs.map} +1 -1
  363. package/dist/{renderer-CeiCJCg4.mjs → renderer-GY7D1oAn.mjs} +4 -4
  364. package/dist/{renderer-CeiCJCg4.mjs.map → renderer-GY7D1oAn.mjs.map} +1 -1
  365. package/dist/{renderer-D2-V9nQq.mjs → renderer-JaSEubRQ.mjs} +4 -4
  366. package/dist/{renderer-D2-V9nQq.mjs.map → renderer-JaSEubRQ.mjs.map} +1 -1
  367. package/dist/{renderer-CqtIbe5n.mjs → renderer-MrvH4L7b.mjs} +4 -4
  368. package/dist/{renderer-CqtIbe5n.mjs.map → renderer-MrvH4L7b.mjs.map} +1 -1
  369. package/dist/{renderer-DO5r7ZV-.mjs → renderer-PNIgmWft.mjs} +2 -2
  370. package/dist/{renderer-DO5r7ZV-.mjs.map → renderer-PNIgmWft.mjs.map} +1 -1
  371. package/dist/renderer-S305xm67.mjs +5 -0
  372. package/dist/renderer-TgUloP-y.mjs +5 -0
  373. package/dist/{renderer-DFJQ6v0F.mjs → renderer-VcPoZAM2.mjs} +3 -3
  374. package/dist/{renderer-DFJQ6v0F.mjs.map → renderer-VcPoZAM2.mjs.map} +1 -1
  375. package/dist/{renderer-DcBGYN3V.mjs → renderer-ZGJWn-_L.mjs} +4 -4
  376. package/dist/{renderer-DcBGYN3V.mjs.map → renderer-ZGJWn-_L.mjs.map} +1 -1
  377. package/dist/{renderer-DTw-W0Yz.mjs → renderer-aXwxbXcE.mjs} +5 -5
  378. package/dist/{renderer-DTw-W0Yz.mjs.map → renderer-aXwxbXcE.mjs.map} +1 -1
  379. package/dist/renderer-gZ5SMvbU.mjs +5 -0
  380. package/dist/{renderer-CCGzT6bX.mjs → renderer-gniIzAsk.mjs} +3 -3
  381. package/dist/renderer-kEpx7pUy.mjs +5 -0
  382. package/dist/{renderer-BkWd4-YO.mjs → renderer-mSlww_MA.mjs} +4 -4
  383. package/dist/{renderer-BkWd4-YO.mjs.map → renderer-mSlww_MA.mjs.map} +1 -1
  384. package/dist/{renderer-B0gPTXhe.mjs → renderer-mWfgWQy_.mjs} +5 -5
  385. package/dist/{renderer-B0gPTXhe.mjs.map → renderer-mWfgWQy_.mjs.map} +1 -1
  386. package/dist/{renderer-BajIOuZH.mjs → renderer-nLPfvY3-.mjs} +2 -2
  387. package/dist/{renderer-BajIOuZH.mjs.map → renderer-nLPfvY3-.mjs.map} +1 -1
  388. package/dist/renderer-oPWZmzua.mjs +5 -0
  389. package/dist/{renderer--ruTGlPo.mjs → renderer-pcpDwv3K.mjs} +4 -4
  390. package/dist/{renderer--ruTGlPo.mjs.map → renderer-pcpDwv3K.mjs.map} +1 -1
  391. package/dist/renderer-qCNOffLy.mjs +5 -0
  392. package/dist/{renderer-DwVOxdzC.mjs → renderer-vPJRRVNN.mjs} +5 -5
  393. package/dist/{renderer-DwVOxdzC.mjs.map → renderer-vPJRRVNN.mjs.map} +1 -1
  394. package/dist/{renderer-JvcR1vYI.mjs → renderer-vQrX7Qsw.mjs} +3 -3
  395. package/dist/{renderer-JvcR1vYI.mjs.map → renderer-vQrX7Qsw.mjs.map} +1 -1
  396. package/dist/{renderer-Bi6W4RSn.mjs → renderer-wld_ZR0-.mjs} +3 -3
  397. package/dist/{renderer-Bi6W4RSn.mjs.map → renderer-wld_ZR0-.mjs.map} +1 -1
  398. package/dist/renderer-xEVoeq12.mjs +5 -0
  399. package/dist/renderer-ypw8fzVD.mjs +5 -0
  400. package/dist/{renderers-CP8CFWDs-DzGXDs5p.mjs → renderers-CP8CFWDs-DgKdGMvS.mjs} +4 -4
  401. package/dist/{renderers-CP8CFWDs-DzGXDs5p.mjs.map → renderers-CP8CFWDs-DgKdGMvS.mjs.map} +1 -1
  402. package/dist/signals/index.d.mts +6 -6
  403. package/dist/signals/index.mjs +8 -8
  404. package/dist/{signals-C5nOrRmn.mjs → signals-Cf9mfNGE.mjs} +4 -4
  405. package/dist/{signals-C5nOrRmn.mjs.map → signals-Cf9mfNGE.mjs.map} +1 -1
  406. package/dist/src-CU_7ui4I.mjs +8373 -0
  407. package/dist/src-CU_7ui4I.mjs.map +1 -0
  408. package/package.json +68 -58
  409. package/dist/dist-ClyC_d66.mjs.map +0 -1
  410. package/dist/dist-CxGfrigI.mjs.map +0 -1
  411. package/dist/effects-BOk_2kzM.mjs.map +0 -1
  412. package/dist/index-B1XcyMm8.d.mts.map +0 -1
  413. package/dist/index-B2Xesdug.d.mts.map +0 -1
  414. package/dist/index-DujpgUUq.d.mts.map +0 -1
  415. package/dist/index.d.mts.map +0 -1
  416. package/dist/index.mjs.map +0 -1
  417. package/dist/preview/index.d.mts.map +0 -1
  418. package/dist/preview-engine/chunk-AQNTIWTK.mjs +0 -1
  419. package/dist/preview-engine/chunk-CPFPY35C.mjs +0 -1
  420. package/dist/preview-engine/chunk-UZBVCYRY.mjs +0 -174
  421. package/dist/preview-engine/chunk-XDMZP3E2.mjs +0 -1
  422. package/dist/preview-engine/chunk-YKMBRUAE.mjs +0 -1
  423. package/dist/preview-engine/renderers-3CXFGRE7.mjs +0 -1
  424. package/dist/preview-engine/renderers-3FC3WCJN.mjs +0 -1
  425. package/dist/preview-engine/renderers-3S2FM33K.mjs +0 -1
  426. package/dist/preview-engine/renderers-53ZTBI3C.mjs +0 -1
  427. package/dist/preview-engine/renderers-5USBVCMQ.mjs +0 -1
  428. package/dist/preview-engine/renderers-65G6BLEA.mjs +0 -1
  429. package/dist/preview-engine/renderers-7XU7BXE7.mjs +0 -1
  430. package/dist/preview-engine/renderers-BFXM32BC.mjs +0 -1
  431. package/dist/preview-engine/renderers-CDXHCWWK.mjs +0 -1
  432. package/dist/preview-engine/renderers-CLUN7WU6.mjs +0 -1
  433. package/dist/preview-engine/renderers-DTB4HEXU.mjs +0 -1
  434. package/dist/preview-engine/renderers-EFKNRLYF.mjs +0 -1
  435. package/dist/preview-engine/renderers-EI6MQ2EL.mjs +0 -228
  436. package/dist/preview-engine/renderers-G2K7ZA5O.mjs +0 -1
  437. package/dist/preview-engine/renderers-GTUO5LIV.mjs +0 -1
  438. package/dist/preview-engine/renderers-HSJIVP57.mjs +0 -1
  439. package/dist/preview-engine/renderers-IHACBWXI.mjs +0 -1
  440. package/dist/preview-engine/renderers-IO3BVVJ7.mjs +0 -1
  441. package/dist/preview-engine/renderers-IR772VSN.mjs +0 -1
  442. package/dist/preview-engine/renderers-L6TRJOXA.mjs +0 -1
  443. package/dist/preview-engine/renderers-N6H6RSF2.mjs +0 -1
  444. package/dist/preview-engine/renderers-O5NFQYRQ.mjs +0 -1
  445. package/dist/preview-engine/renderers-O6V3OEKX.mjs +0 -1
  446. package/dist/preview-engine/renderers-OODA27UH.mjs +0 -1
  447. package/dist/preview-engine/renderers-RIGW33ZI.mjs +0 -1
  448. package/dist/preview-engine/renderers-RUZRBQ4K.mjs +0 -1
  449. package/dist/preview-engine/renderers-SKB2GGKS.mjs +0 -1
  450. package/dist/preview-engine/renderers-T4D2TASM.mjs +0 -1
  451. package/dist/preview-engine/renderers-T74XV36Q.mjs +0 -1
  452. package/dist/preview-engine/renderers-TOPW2UM3.mjs +0 -1
  453. package/dist/preview-engine/renderers-UO5G3Y2G.mjs +0 -1
  454. package/dist/preview-engine/renderers-UOV7VOJJ.mjs +0 -1
  455. package/dist/preview-engine/renderers-UWKSTSTA.mjs +0 -1
  456. package/dist/preview-engine/renderers-VHPAOUTA.mjs +0 -1
  457. package/dist/preview-engine/renderers-YEQRQJFC.mjs +0 -1
  458. package/dist/preview-engine/renderers-YRZ3WODX.mjs +0 -1
  459. package/dist/preview-engine/renderers-YUTFGVRY.mjs +0 -1
  460. package/dist/preview-engine/renderers-YXOTRKPA.mjs +0 -1
  461. package/dist/preview-engine/renderers-ZOU5ARVD.mjs +0 -1
  462. package/dist/preview-engine/renderers-ZW75LQTR.mjs +0 -1
  463. package/dist/program-C2cJayA1.mjs.map +0 -1
  464. package/dist/renderer-BAhZeUUn.mjs +0 -5
  465. package/dist/renderer-BHAE_dKq.mjs +0 -5
  466. package/dist/renderer-BNYZY3FE.mjs +0 -5
  467. package/dist/renderer-BSvubMvI.mjs +0 -5
  468. package/dist/renderer-BV48PFjX.mjs.map +0 -1
  469. package/dist/renderer-B_M_yhTc.mjs +0 -5
  470. package/dist/renderer-B_xSsTuy.mjs +0 -5
  471. package/dist/renderer-BePHurAU.mjs +0 -5
  472. package/dist/renderer-BfZquJrW.mjs +0 -5
  473. package/dist/renderer-BuY5ypB2.mjs +0 -5
  474. package/dist/renderer-BvOQgFHo.mjs +0 -5
  475. package/dist/renderer-C7NkSvUz.mjs +0 -5
  476. package/dist/renderer-CIuxV2q1.mjs +0 -5
  477. package/dist/renderer-CKp97tSQ.mjs +0 -5
  478. package/dist/renderer-CY89dTFR.mjs +0 -5
  479. package/dist/renderer-C_B_4Bl0.mjs +0 -5
  480. package/dist/renderer-Cd87JOjP.mjs +0 -5
  481. package/dist/renderer-Cev6orIK.mjs +0 -6
  482. package/dist/renderer-Cewf6UPh.mjs +0 -5
  483. package/dist/renderer-CnHsMLIv.mjs +0 -5
  484. package/dist/renderer-CoxuAr7b.mjs +0 -5
  485. package/dist/renderer-CrdG7trA.mjs +0 -5
  486. package/dist/renderer-Cu5MFgbk.mjs +0 -5
  487. package/dist/renderer-CuOjhtoo.mjs +0 -5
  488. package/dist/renderer-D2_0g6Oi.mjs +0 -5
  489. package/dist/renderer-D62FwolD.mjs +0 -5
  490. package/dist/renderer-DDNY9N7I.mjs +0 -5
  491. package/dist/renderer-DF_ONJ3v.mjs +0 -5
  492. package/dist/renderer-DbzrVHEE.mjs +0 -5
  493. package/dist/renderer-DlYchp1v.mjs +0 -5
  494. package/dist/renderer-DqcUNP8J.mjs +0 -5
  495. package/dist/renderer-DqvFhEQj.mjs +0 -5
  496. package/dist/renderer-DuuOlYIG.mjs +0 -3
  497. package/dist/renderer-DxBav2bE.mjs +0 -5
  498. package/dist/renderer-GvSRoQje.mjs +0 -5
  499. package/dist/renderer-KP7Ita9g.mjs +0 -5
  500. package/dist/renderer-Pc8dyNXt.mjs +0 -5
  501. package/dist/renderer-ezRCmU09.mjs +0 -3
  502. package/dist/renderer-h6ev1ogx.mjs +0 -5
  503. /package/dist/{chunk-GBe2k3y8.mjs → chunk-D7gtBK74.mjs} +0 -0
  504. /package/dist/{renderer-B-BcgA6d.mjs → renderer-B7fX0C62.mjs} +0 -0
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "code-block",
3
+ "title": "CodeBlock",
4
+ "summary": "Code that reads like an editor and moves like an explanation: syntax colours, typing, a focus band that walks the lines, and diffs where kept lines glide to their new places.",
5
+ "tags": [
6
+ "code",
7
+ "syntax",
8
+ "diff",
9
+ "developer",
10
+ "tutorial",
11
+ "typing"
12
+ ],
13
+ "example": "CodeBlock({ code, lang: \"ts\", focus: [{ at: 60, lines: [3] }] })",
14
+ "parts": [
15
+ "code",
16
+ "line-<n>",
17
+ "number-<n>",
18
+ "band",
19
+ "caret",
20
+ "window"
21
+ ],
22
+ "cues": [
23
+ "keystrokes",
24
+ "lines",
25
+ "focus",
26
+ "diff"
27
+ ],
28
+ "registryDependencies": [
29
+ "app-frame"
30
+ ],
31
+ "files": [
32
+ {
33
+ "path": "code-block/index.ts",
34
+ "content": "/**\n * CodeBlock: code that reads like an editor and moves like an explanation.\n * Syntax colours, typing, a focus band that walks the lines being\n * discussed, and diffs where kept lines glide to their new places while\n * removed lines fold away and new ones arrive.\n */\nimport {\n\ttype AnimatableProp,\n\ttype KeyframeTuple,\n\tLayerAnimation,\n\ttype LayoutNode,\n\ttype TextSpan,\n} from \"framefields\";\nimport {\n\ttype Animatable,\n\tbind,\n\tbindComponent,\n\tdefineComponent,\n\ttype Theme,\n\tthemeFrames,\n\ttypingFrames,\n\tz,\n} from \"framefields/components\";\nimport { appFrame } from \"../app-frame/index.js\";\nimport { diffLines, expandTabs, focusSet, Track } from \"./plan.js\";\nimport {\n\tCODE_LANGS,\n\ttype Token,\n\ttype TokenKind,\n\ttokenize,\n} from \"./tokenize.js\";\n\nexport {\n\tCODE_LANGS,\n\ttype CodeLang,\n\ttype Token,\n\ttype TokenKind,\n} from \"./tokenize.js\";\n\nexport interface CodeBlockCues {\n\t[name: string]: ReadonlyArray<{ frame: number }>;\n\t/** Every typed character (`typing`): for key sounds. */\n\tkeystrokes: ReadonlyArray<{ frame: number; line: number }>;\n\t/** Each line's first frame on screen (1-based `line`). */\n\tlines: ReadonlyArray<{ frame: number; line: number }>;\n\t/** Each focus step. */\n\tfocus: ReadonlyArray<{ frame: number; lines: number[] }>;\n\t/** The diff, start to settle. */\n\tdiff: ReadonlyArray<{ frame: number; end: number }>;\n}\n\n/** Token colours from theme tokens. */\nfunction palette(theme: Theme): Record<TokenKind, string> {\n\tconst c = theme.color;\n\treturn {\n\t\tplain: c.ink,\n\t\tkeyword: c.accent2,\n\t\tstring: c.positive,\n\t\tnumber: c.highlight,\n\t\tcomment: c.muted,\n\t\tfunction: c.accent,\n\t\ttype: \"#4cc9e0\",\n\t\tproperty: c.ink,\n\t\tpunctuation: c.muted,\n\t\toperator: c.muted,\n\t};\n}\n\n/** `#rrggbb` (or `#rgb`) at `alpha`, for washes and bands. */\nfunction tint(color: string, alpha: number): string {\n\tconst m = color.trim().match(/^#([\\da-f]{3}|[\\da-f]{6})$/i);\n\tif (!m?.[1]) return color;\n\tconst h = m[1].length === 3 ? [...m[1]].map((c) => c + c).join(\"\") : m[1];\n\tconst [r, g, b] = [0, 2, 4].map((i) =>\n\t\tNumber.parseInt(h.slice(i, i + 2), 16),\n\t);\n\treturn `rgba(${r},${g},${b},${alpha})`;\n}\n\nfunction tokenSchema() {\n\treturn z.array(\n\t\tz.array(\n\t\t\tz\n\t\t\t\t.object({\n\t\t\t\t\ttext: z.string(),\n\t\t\t\t\tkind: z\n\t\t\t\t\t\t.enum([\n\t\t\t\t\t\t\t\"plain\",\n\t\t\t\t\t\t\t\"keyword\",\n\t\t\t\t\t\t\t\"string\",\n\t\t\t\t\t\t\t\"number\",\n\t\t\t\t\t\t\t\"comment\",\n\t\t\t\t\t\t\t\"function\",\n\t\t\t\t\t\t\t\"type\",\n\t\t\t\t\t\t\t\"property\",\n\t\t\t\t\t\t\t\"punctuation\",\n\t\t\t\t\t\t\t\"operator\",\n\t\t\t\t\t\t])\n\t\t\t\t\t\t.optional(),\n\t\t\t\t\tcolor: z.string().optional(),\n\t\t\t\t})\n\t\t\t\t.strict(),\n\t\t),\n\t);\n}\n\nexport const codeBlock = /* @__PURE__ */ defineComponent({\n\tname: \"CodeBlock\",\n\tprops: () => ({\n\t\tcode: z.string(),\n\t\tlang: z.enum(CODE_LANGS).default(\"ts\"),\n\t\t/** Your own highlighter's output, one token list per line (overrides `lang`). */\n\t\ttokens: tokenSchema().optional(),\n\t\ttyping: z\n\t\t\t.union([\n\t\t\t\tz.boolean(),\n\t\t\t\tz\n\t\t\t\t\t.object({\n\t\t\t\t\t\tcps: z.number().positive().optional(),\n\t\t\t\t\t\tat: z.number().int().min(0).optional(),\n\t\t\t\t\t})\n\t\t\t\t\t.strict(),\n\t\t\t])\n\t\t\t.default(false),\n\t\t/** 1-based lines, as a list or `{ from, to }`; `[]` clears. */\n\t\tfocus: z\n\t\t\t.array(\n\t\t\t\tz\n\t\t\t\t\t.object({\n\t\t\t\t\t\tat: z.number().int().min(0),\n\t\t\t\t\t\tlines: z.union([\n\t\t\t\t\t\t\tz.array(z.number().int().positive()),\n\t\t\t\t\t\t\tz\n\t\t\t\t\t\t\t\t.object({\n\t\t\t\t\t\t\t\t\tfrom: z.number().int().positive(),\n\t\t\t\t\t\t\t\t\tto: z.number().int().positive(),\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t.strict(),\n\t\t\t\t\t\t]),\n\t\t\t\t\t})\n\t\t\t\t\t.strict(),\n\t\t\t)\n\t\t\t.default([]),\n\t\tdiff: z\n\t\t\t.object({\n\t\t\t\tto: z.string(),\n\t\t\t\tat: z.number().int().min(0),\n\t\t\t\tduration: z.number().int().positive().optional(),\n\t\t\t})\n\t\t\t.strict()\n\t\t\t.optional(),\n\t\tlineNumbers: z.boolean().default(true),\n\t\ttabSize: z.number().int().positive().default(2),\n\t\t/** Wrap in an AppFrame window (with a title such as the file name). */\n\t\tframe: z\n\t\t\t.union([z.boolean(), z.object({ title: z.string().optional() }).strict()])\n\t\t\t.default(false),\n\t\tfontFamily: z.string().optional(),\n\t\tfontSize: z.number().positive().optional(),\n\t}),\n\tdefaults: { enter: false, exit: false },\n\tbuild(ctx, props) {\n\t\tconst { host, theme, fps } = ctx;\n\t\tconst s = theme.scale;\n\t\tconst fs = props.fontSize ?? 24 * s;\n\t\tconst fontFamily = props.fontFamily ?? theme.font.mono;\n\t\tconst LINE_HEIGHT = 1.5;\n\t\tconst m = host.measure(\"M\", {\n\t\t\tfontFamily,\n\t\t\tfontSize: fs,\n\t\t\tlineHeight: LINE_HEIGHT,\n\t\t});\n\t\tconst adv = m.width;\n\t\tconst lh = m.lineHeight;\n\t\tconst colors = palette(theme);\n\n\t\tconst before = expandTabs(props.code, props.tabSize).split(\"\\n\");\n\t\tconst after = props.diff\n\t\t\t? expandTabs(props.diff.to, props.tabSize).split(\"\\n\")\n\t\t\t: null;\n\t\tconst tokensOf = (lines: string[], own?: Token[][]) =>\n\t\t\town ?? tokenize(lines.join(\"\\n\"), props.lang);\n\t\tconst beforeTokens = tokensOf(\n\t\t\tbefore,\n\t\t\tprops.tokens as Token[][] | undefined,\n\t\t);\n\t\tconst afterTokens = after ? tokensOf(after) : null;\n\n\t\t// Fixed size up front: the widest line and the most lines, before and after.\n\t\tconst rows = Math.max(before.length, after?.length ?? 0);\n\t\tconst cols = Math.max(\n\t\t\t1,\n\t\t\t...before.map((l) => l.length),\n\t\t\t...(after ?? []).map((l) => l.length),\n\t\t);\n\t\tconst pad = fs;\n\t\tconst gutter = props.lineNumbers ? (String(rows).length + 2) * adv : 0;\n\t\tconst W = pad * 2 + gutter + cols * adv;\n\t\tconst H = pad * 2 + rows * lh;\n\t\tconst rowY = (i: number) => pad + i * lh;\n\t\tconst textX = pad + gutter;\n\n\t\tconst spans = (line: Token[]): TextSpan[] =>\n\t\t\tline.map((t) => ({\n\t\t\t\ttext: t.text,\n\t\t\t\tfill: t.color ?? colors[t.kind ?? \"plain\"],\n\t\t\t}));\n\t\tconst lineNode = (id: string, tokens: Token[], text: string, row: number) =>\n\t\t\thost.text(text || \" \", {\n\t\t\t\tid: ctx.partId(id),\n\t\t\t\tposition: \"absolute\",\n\t\t\t\tx: textX,\n\t\t\t\ty: rowY(row),\n\t\t\t\tfontFamily,\n\t\t\t\tfontSize: fs,\n\t\t\t\tlineHeight: LINE_HEIGHT,\n\t\t\t\tfill: colors.plain,\n\t\t\t\t...(text && { spans: spans(tokens) }),\n\t\t\t});\n\t\tconst numberNode = (id: string, n: number, row: number) =>\n\t\t\thost.text(String(n), {\n\t\t\t\tid: ctx.partId(id),\n\t\t\t\tposition: \"absolute\",\n\t\t\t\tx: pad,\n\t\t\t\ty: rowY(row),\n\t\t\t\twidth: gutter - adv * 1.5,\n\t\t\t\talign: \"end\",\n\t\t\t\tfontFamily,\n\t\t\t\tfontSize: fs,\n\t\t\t\tlineHeight: LINE_HEIGHT,\n\t\t\t\tfill: theme.color.muted,\n\t\t\t});\n\n\t\tconst parts: Record<string, Animatable<LayoutNode>> = {};\n\t\tconst rowsLayer: LayoutNode[] = [];\n\t\tconst washes: LayoutNode[] = [];\n\t\tconst opacity = new Map<LayoutNode, Track>();\n\t\tconst fade = (node: LayoutNode, initial = 1) => {\n\t\t\tlet t = opacity.get(node);\n\t\t\tif (!t) {\n\t\t\t\tt = new Track(initial);\n\t\t\t\topacity.set(node, t);\n\t\t\t}\n\t\t\treturn t;\n\t\t};\n\t\tconst animations = new Map<LayoutNode, LayerAnimation>();\n\t\tconst animOf = (node: LayoutNode) => {\n\t\t\tlet a = animations.get(node);\n\t\t\tif (!a) {\n\t\t\t\ta = LayerAnimation.create();\n\t\t\t\tanimations.set(node, a);\n\t\t\t}\n\t\t\treturn a;\n\t\t};\n\n\t\tconst beforeNodes = before.map((text, i) =>\n\t\t\tlineNode(`line-${i}`, beforeTokens[i] ?? [], text, i),\n\t\t);\n\t\tconst beforeNumbers = props.lineNumbers\n\t\t\t? before.map((_, i) => numberNode(`number-${i}`, i + 1, i))\n\t\t\t: [];\n\t\trowsLayer.push(...beforeNumbers, ...beforeNodes);\n\n\t\t// ── Typing ────────────────────────────────────────────────────────\n\t\tconst keystrokes: Array<{ frame: number; line: number }> = [];\n\t\tconst lineCues: Array<{ frame: number; line: number }> = before.map(\n\t\t\t(_, i) => ({ frame: 0, line: i + 1 }),\n\t\t);\n\t\tlet end = 0;\n\t\tif (props.typing !== false) {\n\t\t\tconst o = props.typing === true ? {} : props.typing;\n\t\t\tconst r = ctx.rng(\"typing\");\n\t\t\tlet t = o.at ?? 0;\n\t\t\tconst caretKeys: { x: KeyframeTuple[]; y: KeyframeTuple[] } = {\n\t\t\t\tx: [],\n\t\t\t\ty: [],\n\t\t\t};\n\t\t\tbefore.forEach((text, i) => {\n\t\t\t\tconst indent = text.length - text.trimStart().length;\n\t\t\t\tconst node = beforeNodes[i] as Animatable<LayoutNode>;\n\t\t\t\tconst number = beforeNumbers[i];\n\t\t\t\tlineCues[i] = { frame: Math.round(t), line: i + 1 };\n\t\t\t\tif (number) fade(number, 0).to(Math.round(t), Math.round(t), 1);\n\t\t\t\tcaretKeys.x.push([Math.round(t), textX + indent * adv, \"hold\"]);\n\t\t\t\tcaretKeys.y.push([Math.round(t), rowY(i), \"hold\"]);\n\t\t\t\t// Indentation appears with the line (editors auto-indent).\n\t\t\t\tconst typed = text.slice(indent);\n\t\t\t\tconst frames = typed\n\t\t\t\t\t? typingFrames(typed, o.cps ?? 28, fps, r, Math.round(t))\n\t\t\t\t\t: [];\n\t\t\t\tconst count = (c: number) =>\n\t\t\t\t\ttext.length > 1 && c <= 1 ? c / text.length : c;\n\t\t\t\tconst keys: KeyframeTuple[] = [[0, 0]];\n\t\t\t\tif (indent > 0 || !typed)\n\t\t\t\t\tkeys.push([\n\t\t\t\t\t\tMath.round(t),\n\t\t\t\t\t\tcount(Math.max(indent, typed ? indent : 1)),\n\t\t\t\t\t\t\"hold\",\n\t\t\t\t\t]);\n\t\t\t\tframes.forEach((f, k) => {\n\t\t\t\t\tkeys.push([f, count(indent + k + 1), \"hold\"]);\n\t\t\t\t\tkeystrokes.push({ frame: f, line: i + 1 });\n\t\t\t\t\tcaretKeys.x.push([f, textX + (indent + k + 1) * adv, \"hold\"]);\n\t\t\t\t\tcaretKeys.y.push([f, rowY(i), \"hold\"]);\n\t\t\t\t});\n\t\t\t\tanimOf(node).keys(\"text\" as AnimatableProp, ascending(keys));\n\t\t\t\tt = (frames[frames.length - 1] ?? t) + Math.round(fps * 0.18);\n\t\t\t});\n\t\t\tend = Math.round(t);\n\t\t\tconst caret = host.box({\n\t\t\t\tid: ctx.partId(\"caret\"),\n\t\t\t\tposition: \"absolute\",\n\t\t\t\tx: textX,\n\t\t\t\ty: rowY(0),\n\t\t\t\twidth: Math.max(2, fs * 0.09),\n\t\t\t\theight: lh,\n\t\t\t\tbackground: theme.color.accent,\n\t\t\t});\n\t\t\tconst half = Math.max(1, Math.round(fps * 0.5));\n\t\t\tconst blink: KeyframeTuple[] = [[0, 1]];\n\t\t\tfor (\n\t\t\t\tlet f = end + half, on = false;\n\t\t\t\tf < end + half * 6;\n\t\t\t\tf += half, on = !on\n\t\t\t)\n\t\t\t\tblink.push([f, on ? 1 : 0, \"hold\"]);\n\t\t\tblink.push([end + half * 6, 0, \"hold\"]);\n\t\t\tcaret.animate(\n\t\t\t\tLayerAnimation.create()\n\t\t\t\t\t.keys(\"x\", ascending(caretKeys.x))\n\t\t\t\t\t.keys(\"y\", ascending(caretKeys.y))\n\t\t\t\t\t.keys(\"opacity\", blink),\n\t\t\t);\n\t\t\tparts.caret = caret;\n\t\t\trowsLayer.push(caret);\n\t\t}\n\n\t\t// ── Diff ──────────────────────────────────────────────────────────\n\t\tconst diffCue: Array<{ frame: number; end: number }> = [];\n\t\tlet afterNodes: LayoutNode[] = beforeNodes;\n\t\tif (props.diff && after && afterTokens) {\n\t\t\tconst T = props.diff.at;\n\t\t\tconst D =\n\t\t\t\tprops.diff.duration ??\n\t\t\t\tMath.round(themeFrames(theme, \"slow\", fps) * 1.2);\n\t\t\tconst move = theme.motion.ease.move;\n\t\t\tconst plan = diffLines(before, after);\n\t\t\tafterNodes = new Array<LayoutNode>(after.length);\n\t\t\tconst wash = (\n\t\t\t\tid: string,\n\t\t\t\trow: number,\n\t\t\t\tcolor: string,\n\t\t\t\tfrom: number,\n\t\t\t\tto: number,\n\t\t\t) => {\n\t\t\t\tconst box = host.box({\n\t\t\t\t\tid: ctx.partId(id),\n\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\tx: pad * 0.4,\n\t\t\t\t\ty: rowY(row),\n\t\t\t\t\twidth: W - pad * 0.8,\n\t\t\t\t\theight: lh,\n\t\t\t\t\tbackground: tint(color, 0.22),\n\t\t\t\t\tborderRadius: theme.radius.sm,\n\t\t\t\t});\n\t\t\t\tfade(box, 0)\n\t\t\t\t\t.to(from, from + 4, 1)\n\t\t\t\t\t.to(to - 10, to, 0, \"power2.in\");\n\t\t\t\twashes.push(box);\n\t\t\t};\n\t\t\tfor (const [i, j] of plan.kept) {\n\t\t\t\tconst node = beforeNodes[i] as Animatable<LayoutNode>;\n\t\t\t\tlet moving: LayoutNode = node;\n\t\t\t\tif ((before[i] as string) !== (after[j] as string)) {\n\t\t\t\t\t// Re-indented: the new text takes over as it starts to move.\n\t\t\t\t\tconst next = lineNode(\n\t\t\t\t\t\t`line-${i}-to-${j}`,\n\t\t\t\t\t\tafterTokens[j] ?? [],\n\t\t\t\t\t\tafter[j] as string,\n\t\t\t\t\t\ti,\n\t\t\t\t\t);\n\t\t\t\t\tfade(next, 0).to(T, T, 1);\n\t\t\t\t\tfade(node).to(T, T, 0);\n\t\t\t\t\trowsLayer.push(next);\n\t\t\t\t\tmoving = next;\n\t\t\t\t}\n\t\t\t\tif (i !== j)\n\t\t\t\t\tanimOf(moving).keys(\"y\", [\n\t\t\t\t\t\t[T, rowY(i)],\n\t\t\t\t\t\t[T + D, rowY(j), move],\n\t\t\t\t\t]);\n\t\t\t\tafterNodes[j] = moving;\n\t\t\t}\n\t\t\tfor (const i of plan.removed) {\n\t\t\t\tconst node = beforeNodes[i] as LayoutNode;\n\t\t\t\tfade(node).to(T, T + Math.round(D * 0.38), 0, \"power2.in\");\n\t\t\t\tanimOf(node).keys(\"x\", [\n\t\t\t\t\t[T, textX],\n\t\t\t\t\t[T + Math.round(D * 0.38), textX - fs * 0.6, \"power2.in\"],\n\t\t\t\t]);\n\t\t\t\twash(\n\t\t\t\t\t`wash-removed-${i}`,\n\t\t\t\t\ti,\n\t\t\t\t\ttheme.color.negative,\n\t\t\t\t\tT,\n\t\t\t\t\tT + Math.round(D * 0.6),\n\t\t\t\t);\n\t\t\t}\n\t\t\tfor (const j of plan.added) {\n\t\t\t\tconst node = lineNode(\n\t\t\t\t\t`added-${j}`,\n\t\t\t\t\tafterTokens[j] ?? [],\n\t\t\t\t\tafter[j] as string,\n\t\t\t\t\tj,\n\t\t\t\t);\n\t\t\t\tconst from = T + Math.round(D * 0.48);\n\t\t\t\tfade(node, 0).to(from, T + D, 1);\n\t\t\t\tanimOf(node).keys(\"x\", [\n\t\t\t\t\t[from, textX + fs * 0.8],\n\t\t\t\t\t[T + D, textX, theme.motion.ease.enter],\n\t\t\t\t]);\n\t\t\t\twash(\n\t\t\t\t\t`wash-added-${j}`,\n\t\t\t\t\tj,\n\t\t\t\t\ttheme.color.positive,\n\t\t\t\t\tfrom,\n\t\t\t\t\tT + D + Math.round(fps * 0.8),\n\t\t\t\t);\n\t\t\t\trowsLayer.push(node);\n\t\t\t\tafterNodes[j] = node;\n\t\t\t}\n\t\t\tif (props.lineNumbers) {\n\t\t\t\tfor (const n of beforeNumbers) fade(n).to(T, T + 6, 0);\n\t\t\t\tafter.forEach((_, j) => {\n\t\t\t\t\tconst n = numberNode(`number-after-${j}`, j + 1, j);\n\t\t\t\t\tfade(n, 0).to(T + Math.round(D * 0.5), T + D, 1);\n\t\t\t\t\trowsLayer.push(n);\n\t\t\t\t});\n\t\t\t}\n\t\t\tdiffCue.push({ frame: T, end: T + D });\n\t\t\tend = Math.max(end, T + D + Math.round(fps * 0.8));\n\t\t}\n\n\t\t// ── Focus ─────────────────────────────────────────────────────────\n\t\tconst focusCues: Array<{ frame: number; lines: number[] }> = [];\n\t\tconst band = host.box({\n\t\t\tid: ctx.partId(\"band\"),\n\t\t\tposition: \"absolute\",\n\t\t\tx: pad * 0.4,\n\t\t\ty: rowY(0),\n\t\t\twidth: W - pad * 0.8,\n\t\t\theight: lh,\n\t\t\tbackground: tint(theme.color.accent, 0.14),\n\t\t\tborderRadius: theme.radius.sm,\n\t\t});\n\t\tconst bandY = new Track(rowY(0));\n\t\tconst bandH = new Track(lh);\n\t\tconst bandOn = new Track(0);\n\t\tconst glide = Math.max(4, themeFrames(theme, \"base\", fps) / 2);\n\t\tfor (const step of [...props.focus].sort((a, b) => a.at - b.at)) {\n\t\t\tconst afterDiff = props.diff !== undefined && step.at >= props.diff.at;\n\t\t\tconst nodes = afterDiff ? afterNodes : beforeNodes;\n\t\t\tconst set = focusSet(step.lines, nodes.length);\n\t\t\tconst f = step.at;\n\t\t\tnodes.forEach((node, i) => {\n\t\t\t\tfade(node).to(f, f + glide, set.size === 0 || set.has(i) ? 1 : 0.32);\n\t\t\t});\n\t\t\tif (set.size > 0) {\n\t\t\t\tconst lo = Math.min(...set);\n\t\t\t\tconst hi = Math.max(...set);\n\t\t\t\tbandY.to(f, f + glide, rowY(lo) - lh * 0.1, theme.motion.ease.move);\n\t\t\t\tbandH.to(\n\t\t\t\t\tf,\n\t\t\t\t\tf + glide,\n\t\t\t\t\t(hi - lo + 1) * lh + lh * 0.2,\n\t\t\t\t\ttheme.motion.ease.move,\n\t\t\t\t);\n\t\t\t\tbandOn.to(f, f + glide, 1);\n\t\t\t} else bandOn.to(f, f + glide, 0);\n\t\t\tfocusCues.push({ frame: f, lines: [...set].map((i) => i + 1) });\n\t\t\tend = Math.max(end, f + glide);\n\t\t}\n\t\tband.animate(\n\t\t\tLayerAnimation.create()\n\t\t\t\t.keys(\"y\", bandY.keys())\n\t\t\t\t.keys(\"height\", bandH.keys())\n\t\t\t\t.keys(\"opacity\", bandOn.keys()),\n\t\t);\n\t\tparts.band = band;\n\n\t\tfor (const [node, track] of opacity)\n\t\t\tanimOf(node).keys(\"opacity\", track.keys());\n\t\tfor (const [node, anim] of animations)\n\t\t\t(node as Animatable<LayoutNode>).animate(anim);\n\n\t\tconst inner = host.box({\n\t\t\tid: ctx.partId(\"code\"),\n\t\t\tposition: \"absolute\",\n\t\t\tx: 0,\n\t\t\ty: 0,\n\t\t\twidth: W,\n\t\t\theight: H,\n\t\t\toverflow: \"hidden\",\n\t\t\t...(props.frame === false && {\n\t\t\t\tbackground: theme.color.surface,\n\t\t\t\tborderRadius: theme.radius.lg,\n\t\t\t}),\n\t\t\tchildren: [band, ...washes, ...rowsLayer],\n\t\t});\n\t\tparts.code = inner;\n\n\t\tlet root: LayoutNode = inner;\n\t\tlet height = H;\n\t\tif (props.frame !== false) {\n\t\t\tconst o = props.frame === true ? {} : props.frame;\n\t\t\tconst bar = 46 * s;\n\t\t\tconst frameNode = bind(\n\t\t\t\thost,\n\t\t\t\tappFrame,\n\t\t\t)({\n\t\t\t\tid: ctx.partId(\"window\"),\n\t\t\t\tkind: \"window\",\n\t\t\t\twidth: W,\n\t\t\t\theight: H + bar,\n\t\t\t\tcontent: [inner],\n\t\t\t\tcontentBackground: theme.color.surface,\n\t\t\t\tenter: false,\n\t\t\t\ttheme,\n\t\t\t\t...(o.title !== undefined && { title: o.title }),\n\t\t\t});\n\t\t\tparts.window = frameNode;\n\t\t\troot = frameNode;\n\t\t\theight = H + bar;\n\t\t}\n\n\t\treturn {\n\t\t\tchildren: [root],\n\t\t\twidth: W,\n\t\t\theight,\n\t\t\tduration: Math.max(1, end),\n\t\t\topenEnded: true,\n\t\t\tcues: {\n\t\t\t\tkeystrokes,\n\t\t\t\tlines: lineCues,\n\t\t\t\tfocus: focusCues,\n\t\t\t\tdiff: diffCue,\n\t\t\t} as CodeBlockCues,\n\t\t\tparts,\n\t\t};\n\t},\n});\n\n/** Keys must ascend: a later key at the same frame replaces the earlier one. */\nfunction ascending(keys: KeyframeTuple[]): KeyframeTuple[] {\n\tconst out: KeyframeTuple[] = [];\n\tfor (const k of keys) {\n\t\twhile (out.length > 0 && (out[out.length - 1] as KeyframeTuple)[0] >= k[0])\n\t\t\tout.pop();\n\t\tout.push(k);\n\t}\n\treturn out;\n}\n\n/**\n * Code that reads like an editor and moves like an explanation: syntax colours,\n * typing, a focus band that walks the lines, and diffs where kept lines glide\n * to their new places.\n */\nexport const CodeBlock = /* @__PURE__ */ bindComponent(codeBlock);\n"
35
+ },
36
+ {
37
+ "path": "code-block/plan.ts",
38
+ "content": "/**\n * CodeBlock planning, kept pure: tab expansion, the line diff, focus ranges,\n * and opacity tracks built from timed transitions.\n */\nimport type { KeyframeTuple } from \"framefields\";\n\nexport function expandTabs(code: string, size: number): string {\n\treturn code.replace(/\\t/g, \" \".repeat(size));\n}\n\nexport interface LineDiff {\n\t/** [old index, new index] of lines that stay (in order). */\n\tkept: Array<[number, number]>;\n\tremoved: number[];\n\tadded: number[];\n}\n\n/**\n * Longest common subsequence on trimmed lines: re-indented lines count as\n * kept, so they glide instead of being deleted and re-added.\n */\nexport function diffLines(\n\ta: readonly string[],\n\tb: readonly string[],\n): LineDiff {\n\tconst A = a.map((l) => l.trim());\n\tconst B = b.map((l) => l.trim());\n\tconst n = A.length;\n\tconst m = B.length;\n\tconst lcs = Array.from({ length: n + 1 }, () =>\n\t\tnew Array<number>(m + 1).fill(0),\n\t);\n\tfor (let i = n - 1; i >= 0; i--)\n\t\tfor (let j = m - 1; j >= 0; j--)\n\t\t\t(lcs[i] as number[])[j] =\n\t\t\t\tA[i] === B[j]\n\t\t\t\t\t? ((lcs[i + 1] as number[])[j + 1] as number) + 1\n\t\t\t\t\t: Math.max(\n\t\t\t\t\t\t\t(lcs[i + 1] as number[])[j] as number,\n\t\t\t\t\t\t\t(lcs[i] as number[])[j + 1] as number,\n\t\t\t\t\t\t);\n\tconst kept: Array<[number, number]> = [];\n\tlet i = 0;\n\tlet j = 0;\n\twhile (i < n && j < m) {\n\t\tif (A[i] === B[j]) {\n\t\t\tkept.push([i, j]);\n\t\t\ti++;\n\t\t\tj++;\n\t\t} else if (\n\t\t\t((lcs[i + 1] as number[])[j] as number) >=\n\t\t\t((lcs[i] as number[])[j + 1] as number)\n\t\t)\n\t\t\ti++;\n\t\telse j++;\n\t}\n\tconst keptA = new Set(kept.map(([x]) => x));\n\tconst keptB = new Set(kept.map(([, y]) => y));\n\treturn {\n\t\tkept,\n\t\tremoved: a.map((_, k) => k).filter((k) => !keptA.has(k)),\n\t\tadded: b.map((_, k) => k).filter((k) => !keptB.has(k)),\n\t};\n}\n\n/** 1-based lines, as a list or `{ from, to }` → 0-based indices inside the code. */\nexport function focusSet(\n\tlines: readonly number[] | { from: number; to: number },\n\tcount: number,\n): Set<number> {\n\tconst list =\n\t\t\"from\" in lines\n\t\t\t? Array.from(\n\t\t\t\t\t{ length: Math.max(0, lines.to - lines.from + 1) },\n\t\t\t\t\t(_, k) => lines.from + k,\n\t\t\t\t)\n\t\t\t: lines;\n\treturn new Set(list.map((l) => l - 1).filter((l) => l >= 0 && l < count));\n}\n\n/** A value track built from transitions: each eases from wherever the value is to `to`. */\nexport class Track {\n\tprivate changes: Array<{\n\t\tstart: number;\n\t\tend: number;\n\t\tto: number;\n\t\tease: string;\n\t}> = [];\n\n\tconstructor(private readonly initial: number) {}\n\n\tto(start: number, end: number, value: number, ease = \"power2.out\"): this {\n\t\tthis.changes.push({ start, end: Math.max(start, end), to: value, ease });\n\t\treturn this;\n\t}\n\n\t/** Ascending keyframes; a change that starts mid-transition starts from its target. */\n\tkeys(): KeyframeTuple[] {\n\t\tconst keys: KeyframeTuple[] = [[0, this.initial]];\n\t\tlet value = this.initial;\n\t\tfor (const c of [...this.changes].sort((x, y) => x.start - y.start)) {\n\t\t\tconst last = keys[keys.length - 1] as KeyframeTuple;\n\t\t\tconst start = Math.max(c.start, last[0] + (c.end === c.start ? 1 : 0));\n\t\t\tif (start > last[0]) keys.push([start, value]);\n\t\t\tconst end = Math.max(c.end, start);\n\t\t\tif (end === start) keys[keys.length - 1] = [start, c.to, \"hold\"];\n\t\t\telse keys.push([end, c.to, c.ease]);\n\t\t\tvalue = c.to;\n\t\t}\n\t\t// Drop duplicate frames (a later key wins).\n\t\tconst out: KeyframeTuple[] = [];\n\t\tfor (const k of keys) {\n\t\t\twhile (\n\t\t\t\tout.length > 0 &&\n\t\t\t\t(out[out.length - 1] as KeyframeTuple)[0] >= k[0]\n\t\t\t)\n\t\t\t\tout.pop();\n\t\t\tout.push(k);\n\t\t}\n\t\treturn out;\n\t}\n}\n"
39
+ },
40
+ {
41
+ "path": "code-block/tokenize.ts",
42
+ "content": "/**\n * A small syntax tokenizer for code on screen: enough to colour ts / js,\n * json, bash, python and css the way an editor would. Anything else comes\n * from the user's own highlighter through `tokens`.\n */\n\nexport const CODE_LANGS = [\n\t\"ts\",\n\t\"js\",\n\t\"json\",\n\t\"bash\",\n\t\"python\",\n\t\"css\",\n\t\"plain\",\n] as const;\nexport type CodeLang = (typeof CODE_LANGS)[number];\n\nexport type TokenKind =\n\t| \"plain\"\n\t| \"keyword\"\n\t| \"string\"\n\t| \"number\"\n\t| \"comment\"\n\t| \"function\"\n\t| \"type\"\n\t| \"property\"\n\t| \"punctuation\"\n\t| \"operator\";\n\nexport interface Token {\n\ttext: string;\n\tkind?: TokenKind;\n\t/** An explicit colour wins over `kind`. */\n\tcolor?: string;\n}\n\nconst words = (list: string) => new Set(list.split(/\\s+/));\n\nlet keywordSets: Record<CodeLang, Set<string>> | undefined;\n\n/** Keyword sets, built on first use (never at import: that would ship to every bundle). */\nfunction keywords(lang: CodeLang): Set<string> {\n\tkeywordSets ??= buildKeywords();\n\treturn keywordSets[lang];\n}\n\nconst buildKeywords = (): Record<CodeLang, Set<string>> => ({\n\tts: words(\n\t\t\"const let var function return if else for while do switch case break continue new class extends import from export default async await try catch finally throw typeof instanceof in of interface type enum implements public private protected readonly static as satisfies keyof declare true false null undefined this void yield\",\n\t),\n\tjs: words(\n\t\t\"const let var function return if else for while do switch case break continue new class extends import from export default async await try catch finally throw typeof instanceof in of static true false null undefined this void yield\",\n\t),\n\tpython: words(\n\t\t\"def class return if elif else for while in not and or import from as with try except finally raise lambda pass break continue None True False self yield async await is global nonlocal\",\n\t),\n\tbash: words(\n\t\t\"if then else elif fi for do done case esac function export return in while until local echo cd source exit set unset\",\n\t),\n\tjson: words(\"true false null\"),\n\tcss: words(\"important\"),\n\tplain: new Set(),\n});\n\ninterface Rule {\n\tre: RegExp;\n\tkind:\n\t\t| TokenKind\n\t\t| ((match: string, rest: string, before: string) => TokenKind);\n}\n\nconst C_LIKE: Rule[] = [\n\t{ re: /\\/\\/.*/y, kind: \"comment\" },\n\t{ re: /\\/\\*.*?(\\*\\/|$)/y, kind: \"comment\" },\n\t{ re: /`(?:[^`\\\\]|\\\\.)*(`|$)/y, kind: \"string\" },\n\t{ re: /\"(?:[^\"\\\\]|\\\\.)*\"?|'(?:[^'\\\\]|\\\\.)*'?/y, kind: \"string\" },\n\t{\n\t\tre: /\\b\\d[\\d_]*(\\.\\d+)?([eE][+-]?\\d+)?n?\\b|\\b0x[\\da-fA-F]+\\b/y,\n\t\tkind: \"number\",\n\t},\n];\n\n/** Identifier kinds shared by most languages. */\nfunction ident(lang: CodeLang) {\n\treturn (word: string, rest: string, before: string): TokenKind => {\n\t\tif (keywords(lang).has(word)) return \"keyword\";\n\t\tif (/^\\s*\\(/.test(rest)) return \"function\";\n\t\tif (/^[A-Z]/.test(word)) return \"type\";\n\t\tif (/\\.\\s*$/.test(before)) return \"property\";\n\t\treturn \"plain\";\n\t};\n}\n\nfunction rulesFor(lang: CodeLang): Rule[] {\n\tconst id = { re: /[A-Za-z_$][\\w$]*/y, kind: ident(lang) };\n\tconst punct = { re: /[{}()[\\];,.:]/y, kind: \"punctuation\" as const };\n\tconst op = { re: /[=+\\-*/%<>!&|?^~@]+/y, kind: \"operator\" as const };\n\tswitch (lang) {\n\t\tcase \"ts\":\n\t\tcase \"js\":\n\t\t\treturn [...C_LIKE, id, punct, op];\n\t\tcase \"json\":\n\t\t\treturn [\n\t\t\t\t{\n\t\t\t\t\tre: /\"(?:[^\"\\\\]|\\\\.)*\"/y,\n\t\t\t\t\tkind: (_m, rest) => (/^\\s*:/.test(rest) ? \"property\" : \"string\"),\n\t\t\t\t},\n\t\t\t\tC_LIKE[4] as Rule,\n\t\t\t\tid,\n\t\t\t\tpunct,\n\t\t\t];\n\t\tcase \"python\":\n\t\t\treturn [\n\t\t\t\t{ re: /#.*/y, kind: \"comment\" },\n\t\t\t\t{ re: /(\"\"\"|''').*?(\\1|$)/y, kind: \"string\" },\n\t\t\t\t{\n\t\t\t\t\tre: /[rbfRBF]?(\"(?:[^\"\\\\]|\\\\.)*\"?|'(?:[^'\\\\]|\\\\.)*'?)/y,\n\t\t\t\t\tkind: \"string\",\n\t\t\t\t},\n\t\t\t\tC_LIKE[4] as Rule,\n\t\t\t\t{ re: /@[\\w.]+/y, kind: \"function\" },\n\t\t\t\tid,\n\t\t\t\tpunct,\n\t\t\t\top,\n\t\t\t];\n\t\tcase \"bash\":\n\t\t\treturn [\n\t\t\t\t{ re: /#.*/y, kind: \"comment\" },\n\t\t\t\t{ re: /\"(?:[^\"\\\\]|\\\\.)*\"?|'[^']*'?/y, kind: \"string\" },\n\t\t\t\t{ re: /\\$\\{?[\\w@#?*]+\\}?/y, kind: \"type\" },\n\t\t\t\t{ re: /--?[\\w-]+/y, kind: \"property\" },\n\t\t\t\t{ re: /\\b\\d+\\b/y, kind: \"number\" },\n\t\t\t\t{\n\t\t\t\t\tre: /[A-Za-z_][\\w./-]*/y,\n\t\t\t\t\t// The first word of a command is the program.\n\t\t\t\t\tkind: (word, _rest, before) => {\n\t\t\t\t\t\tif (keywords(\"bash\").has(word)) return \"keyword\";\n\t\t\t\t\t\tconst b = before.trimEnd();\n\t\t\t\t\t\treturn b === \"\" || /[|;&(]$/.test(b) ? \"function\" : \"plain\";\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{ re: /[|&;<>()=]+/y, kind: \"operator\" },\n\t\t\t];\n\t\tcase \"css\":\n\t\t\treturn [\n\t\t\t\t{ re: /\\/\\*.*?(\\*\\/|$)/y, kind: \"comment\" },\n\t\t\t\t{ re: /\"[^\"]*\"?|'[^']*'?/y, kind: \"string\" },\n\t\t\t\t{ re: /@[\\w-]+/y, kind: \"keyword\" },\n\t\t\t\t{ re: /#[\\da-fA-F]{3,8}\\b/y, kind: \"number\" },\n\t\t\t\t{ re: /-?\\d*\\.?\\d+(px|em|rem|%|vh|vw|s|ms|deg|fr)?/y, kind: \"number\" },\n\t\t\t\t{\n\t\t\t\t\tre: /[\\w-]+/y,\n\t\t\t\t\tkind: (_w, rest) =>\n\t\t\t\t\t\t/^\\s*:(?!:)/.test(rest)\n\t\t\t\t\t\t\t? \"property\"\n\t\t\t\t\t\t\t: /^\\s*\\(/.test(rest)\n\t\t\t\t\t\t\t\t? \"function\"\n\t\t\t\t\t\t\t\t: \"plain\",\n\t\t\t\t},\n\t\t\t\t{ re: /[{}();:,>+~]/y, kind: \"punctuation\" },\n\t\t\t];\n\t\tcase \"plain\":\n\t\t\treturn [];\n\t}\n}\n\n/**\n * Tokens per line. Block comments, template literals and triple-quoted\n * strings carry across lines.\n */\nexport function tokenize(code: string, lang: CodeLang): Token[][] {\n\tconst lines = code.split(\"\\n\");\n\tif (lang === \"plain\") return lines.map((l) => (l ? [{ text: l }] : []));\n\tconst rules = rulesFor(lang);\n\t/** The closing delimiter of a construct still open from a previous line. */\n\tlet open: { close: RegExp; kind: TokenKind } | null = null;\n\n\treturn lines.map((line) => {\n\t\tconst out: Token[] = [];\n\t\tconst push = (text: string, kind: TokenKind) => {\n\t\t\tconst last = out[out.length - 1];\n\t\t\tif (last && last.kind === kind) last.text += text;\n\t\t\telse out.push({ text, kind });\n\t\t};\n\t\tlet i = 0;\n\t\tif (open) {\n\t\t\tconst m = open.close.exec(line);\n\t\t\tconst end = m ? m.index + m[0].length : line.length;\n\t\t\tpush(line.slice(0, end), open.kind);\n\t\t\ti = end;\n\t\t\tif (m) open = null;\n\t\t}\n\t\twhile (i < line.length) {\n\t\t\tconst ws = /\\s+/y;\n\t\t\tws.lastIndex = i;\n\t\t\tconst space = ws.exec(line);\n\t\t\tif (space) {\n\t\t\t\tpush(space[0], \"plain\");\n\t\t\t\ti += space[0].length;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tlet matched = false;\n\t\t\tfor (const rule of rules) {\n\t\t\t\trule.re.lastIndex = i;\n\t\t\t\tconst m = rule.re.exec(line);\n\t\t\t\tif (!m || m[0].length === 0) continue;\n\t\t\t\tconst text = m[0];\n\t\t\t\tconst kind =\n\t\t\t\t\ttypeof rule.kind === \"function\"\n\t\t\t\t\t\t? rule.kind(text, line.slice(i + text.length), line.slice(0, i))\n\t\t\t\t\t\t: rule.kind;\n\t\t\t\tpush(text, kind);\n\t\t\t\t// A construct that runs off the end of the line continues on the next.\n\t\t\t\tif (kind === \"comment\" && text.startsWith(\"/*\") && !text.endsWith(\"*/\"))\n\t\t\t\t\topen = { close: /\\*\\//, kind };\n\t\t\t\telse if (\n\t\t\t\t\tkind === \"string\" &&\n\t\t\t\t\ttext.startsWith(\"`\") &&\n\t\t\t\t\t(text.length === 1 || !text.endsWith(\"`\"))\n\t\t\t\t)\n\t\t\t\t\topen = { close: /(?<!\\\\)`/, kind };\n\t\t\t\telse if (\n\t\t\t\t\tkind === \"string\" &&\n\t\t\t\t\t/^(\"\"\"|''')/.test(text) &&\n\t\t\t\t\t(text.length < 6 || !text.endsWith(text.slice(0, 3)))\n\t\t\t\t)\n\t\t\t\t\topen = { close: new RegExp(text.slice(0, 3)), kind };\n\t\t\t\ti += text.length;\n\t\t\t\tmatched = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (!matched) {\n\t\t\t\tpush(line[i] as string, \"plain\");\n\t\t\t\ti++;\n\t\t\t}\n\t\t}\n\t\treturn out;\n\t});\n}\n"
43
+ }
44
+ ]
45
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "confetti",
3
+ "title": "Confetti",
4
+ "summary": "Celebration bursts of paper, ribbons and sparkles on the GPU that arc up, flutter down and can settle on a floor; bursts can follow cues.",
5
+ "tags": [
6
+ "confetti",
7
+ "celebration",
8
+ "particles",
9
+ "burst",
10
+ "success",
11
+ "party"
12
+ ],
13
+ "example": "Confetti({ bursts: [{ at: 30, origin: [960, 900] }], floor: true })",
14
+ "parts": [
15
+ "paper-<o>-<n>",
16
+ "ribbons-<o>",
17
+ "sparkles-<o>"
18
+ ],
19
+ "cues": [
20
+ "bursts"
21
+ ],
22
+ "registryDependencies": [],
23
+ "files": [
24
+ {
25
+ "path": "confetti/index.ts",
26
+ "content": "/**\n * Confetti: celebration bursts of paper, ribbons and sparkles that arc up,\n * flutter down and, with a floor, settle. Simulated on the GPU (one particle\n * system per palette colour and origin), deterministic per seed; bursts can\n * come straight from another component's cues.\n *\n * Coordinates are the frame's: the root covers the video (default\n * 1920 × 1080 × theme scale, or the given width / height) from 0, 0.\n */\nimport type { LayoutNode } from \"framefields\";\nimport {\n\ttype Animatable,\n\tbindComponent,\n\tdefineComponent,\n\ttype ParticleOptions,\n\tresolveTarget,\n\ttargetPoint,\n\ttargetSchema,\n\ttype Vec2,\n\tz,\n} from \"framefields/components\";\n\nexport interface ConfettiCues {\n\t[name: string]: ReadonlyArray<{ frame: number }>;\n\tbursts: ReadonlyArray<{ frame: number; origin: Vec2 }>;\n}\n\nexport const confetti = /* @__PURE__ */ defineComponent({\n\tname: \"Confetti\",\n\tprops: () => ({\n\t\tbursts: z\n\t\t\t.array(\n\t\t\t\tz\n\t\t\t\t\t.object({\n\t\t\t\t\t\tat: z.number().int().min(0),\n\t\t\t\t\t\t/** Where it bursts from: a point, rect, node or text. */\n\t\t\t\t\t\torigin: targetSchema(),\n\t\t\t\t\t\t/** Paper pieces in this burst. */\n\t\t\t\t\t\tcount: z.number().int().positive().default(140),\n\t\t\t\t\t\t/** Launch direction, degrees (−90 = up). */\n\t\t\t\t\t\tdirection: z.number().default(-90),\n\t\t\t\t\t\t/** Cone width, degrees. */\n\t\t\t\t\t\tspread: z.number().min(0).max(360).default(70),\n\t\t\t\t\t\t/** Launch strength multiplier. */\n\t\t\t\t\t\tpower: z.number().positive().default(1),\n\t\t\t\t\t})\n\t\t\t\t\t.strict(),\n\t\t\t)\n\t\t\t.min(1),\n\t\t/** Paper colours. Default: the theme's accents. */\n\t\tpalette: z.array(z.string()).min(1).optional(),\n\t\t/** px/s². */\n\t\tgravity: z.number().min(0).optional(),\n\t\t/** A floor that pieces land on: `true` for the frame's bottom, or a y. */\n\t\tfloor: z.union([z.boolean(), z.number()]).default(false),\n\t\t/** Fine streamers among the paper. */\n\t\tribbons: z.boolean().default(true),\n\t\t/** A flash of glints at each burst. */\n\t\tsparkles: z.boolean().default(true),\n\t}),\n\tdefaults: { enter: false, exit: false },\n\tbuild(ctx, props) {\n\t\tconst { host, theme, fps } = ctx;\n\t\tconst s = theme.scale;\n\t\tconst W = ctx.requested.width ?? 1920 * s;\n\t\tconst H = ctx.requested.height ?? 1080 * s;\n\t\tconst c = theme.color;\n\t\tconst palette = props.palette ?? [\n\t\t\tc.accent,\n\t\t\tc.accent2,\n\t\t\tc.highlight,\n\t\t\tc.positive,\n\t\t\tc.negative,\n\t\t];\n\t\tconst gravity = props.gravity ?? 1500 * s;\n\t\tconst floor =\n\t\t\tprops.floor === false\n\t\t\t\t? undefined\n\t\t\t\t: props.floor === true\n\t\t\t\t\t? H - 6 * s\n\t\t\t\t\t: props.floor;\n\t\tconst lifetime: [number, number] =\n\t\t\tfloor === undefined ? [2.6, 4] : [5, 6.5];\n\n\t\t// Bursts that share an origin share systems.\n\t\tconst origins = new Map<\n\t\t\tstring,\n\t\t\t{ point: Vec2; bursts: Array<(typeof props.bursts)[number]> }\n\t\t>();\n\t\tconst cues: Array<{ frame: number; origin: Vec2 }> = [];\n\t\tfor (const b of props.bursts) {\n\t\t\tconst point = targetPoint(\n\t\t\t\tresolveTarget(b.origin, { measure: host.measure, fps }),\n\t\t\t\tb.at,\n\t\t\t);\n\t\t\tconst key = `${point[0].toFixed(1)},${point[1].toFixed(1)},${b.direction},${b.spread},${b.power}`;\n\t\t\tconst entry = origins.get(key) ?? { point, bursts: [] };\n\t\t\tentry.bursts.push(b);\n\t\t\torigins.set(key, entry);\n\t\t\tcues.push({ frame: b.at, origin: point });\n\t\t}\n\n\t\tconst parts: Record<string, Animatable<LayoutNode>> = {};\n\t\tconst children: LayoutNode[] = [];\n\t\tlet n = 0;\n\t\tconst system = (\n\t\t\tname: string,\n\t\t\to: Omit<ParticleOptions, \"x\" | \"y\" | \"width\" | \"height\" | \"id\" | \"seed\">,\n\t\t) => {\n\t\t\tconst node = host.particles({\n\t\t\t\tid: ctx.partId(name),\n\t\t\t\tx: 0,\n\t\t\t\ty: 0,\n\t\t\t\twidth: W,\n\t\t\t\theight: H,\n\t\t\t\tseed: ctx.seed * 101 + n++,\n\t\t\t\t...o,\n\t\t\t});\n\t\t\tparts[name] = node;\n\t\t\tchildren.push(node);\n\t\t};\n\n\t\t[...origins.values()].forEach(({ point, bursts }, o) => {\n\t\t\tconst first = bursts[0] as (typeof bursts)[number];\n\t\t\tconst emitter = (share: number) => ({\n\t\t\t\tshape: \"point\" as const,\n\t\t\t\tx: point[0],\n\t\t\t\ty: point[1],\n\t\t\t\trate: 0,\n\t\t\t\tbursts: bursts.map((b) => ({\n\t\t\t\t\tframe: b.at,\n\t\t\t\t\tcount: Math.max(1, Math.round(b.count * share)),\n\t\t\t\t})),\n\t\t\t});\n\t\t\tconst total = Math.max(...bursts.map((b) => b.count));\n\t\t\tconst launch = {\n\t\t\t\tdirection: first.direction,\n\t\t\t\tspread: first.spread,\n\t\t\t\tspeed: [1100 * s * first.power, 2100 * s * first.power] as [\n\t\t\t\t\tnumber,\n\t\t\t\t\tnumber,\n\t\t\t\t],\n\t\t\t\tgravity,\n\t\t\t\tdrag: 1.1,\n\t\t\t\t// Paper wanders as it falls: curl noise reads as flutter.\n\t\t\t\tnoise: { strength: 260 * s, scale: 0.004 / s, speed: 1.4 },\n\t\t\t\tlifetime,\n\t\t\t\t...(floor !== undefined && { floor, bounce: 0.08 }),\n\t\t\t};\n\t\t\tpalette.forEach((color, k) => {\n\t\t\t\tsystem(`paper-${o}-${k}`, {\n\t\t\t\t\t...launch,\n\t\t\t\t\tmax: Math.ceil(total / palette.length) * bursts.length + 8,\n\t\t\t\t\temitter: emitter(1 / palette.length),\n\t\t\t\t\tshape: \"square\",\n\t\t\t\t\tsize: [13 * s, 11 * s],\n\t\t\t\t\tsizeJitter: 0.45,\n\t\t\t\t\tfade: [0.01, floor === undefined ? 0.25 : 0.12],\n\t\t\t\t\tcolor,\n\t\t\t\t});\n\t\t\t});\n\t\t\tif (props.ribbons)\n\t\t\t\tsystem(`ribbons-${o}`, {\n\t\t\t\t\t...launch,\n\t\t\t\t\tmax: Math.ceil(total * 0.2) * bursts.length + 8,\n\t\t\t\t\temitter: emitter(0.2),\n\t\t\t\t\tshape: \"spark\",\n\t\t\t\t\tstretch: 0.03,\n\t\t\t\t\tsize: [5 * s, 4 * s],\n\t\t\t\t\tsizeJitter: 0.3,\n\t\t\t\t\tfade: [0.01, 0.3],\n\t\t\t\t\tcolor: palette.slice(0, 3),\n\t\t\t\t});\n\t\t\tif (props.sparkles)\n\t\t\t\tsystem(`sparkles-${o}`, {\n\t\t\t\t\tmax: 80 * bursts.length,\n\t\t\t\t\temitter: emitter(0.35),\n\t\t\t\t\tdirection: first.direction,\n\t\t\t\t\tspread: Math.min(360, first.spread * 1.6),\n\t\t\t\t\tspeed: [300 * s, 900 * s],\n\t\t\t\t\tgravity: gravity * 0.15,\n\t\t\t\t\tdrag: 2.4,\n\t\t\t\t\tlifetime: [0.35, 0.8],\n\t\t\t\t\tshape: \"soft\",\n\t\t\t\t\tsize: [14 * s, 0],\n\t\t\t\t\tsizeJitter: 0.5,\n\t\t\t\t\tfade: [0, 0.5],\n\t\t\t\t\tblend: \"add\",\n\t\t\t\t\tcolor: [\"#ffffff\", c.highlight],\n\t\t\t\t});\n\t\t});\n\n\t\tconst last = Math.max(...props.bursts.map((b) => b.at));\n\t\treturn {\n\t\t\tchildren,\n\t\t\twidth: W,\n\t\t\theight: H,\n\t\t\tduration: last + Math.ceil(lifetime[1] * fps),\n\t\t\torigin: [0, 0],\n\t\t\tcues: { bursts: cues } as ConfettiCues,\n\t\t\tparts,\n\t\t};\n\t},\n});\n\n/**\n * Celebration bursts of paper, ribbons and sparkles on the GPU that arc up,\n * flutter down and can settle on a floor; bursts can follow cues.\n */\nexport const Confetti = /* @__PURE__ */ bindComponent(confetti);\n"
27
+ }
28
+ ]
29
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "counter",
3
+ "title": "Counter",
4
+ "summary": "A number that counts to its value on odometer reels (or stepping digits), with format-aware separators, affixes and multi-step values.",
5
+ "tags": [
6
+ "number",
7
+ "counter",
8
+ "stat",
9
+ "odometer",
10
+ "data",
11
+ "price"
12
+ ],
13
+ "example": "Counter({ to: 12480, format: \",d\", prefix: \"$\" })",
14
+ "parts": [
15
+ "digits",
16
+ "slot-<n>",
17
+ "prefix",
18
+ "suffix",
19
+ "minus"
20
+ ],
21
+ "cues": [
22
+ "settled",
23
+ "milestones"
24
+ ],
25
+ "registryDependencies": [],
26
+ "files": [
27
+ {
28
+ "path": "counter/index.ts",
29
+ "content": "/**\n * Counter: numbers that change in a way viewers trust. Every digit place is\n * a clipped reel that spins like an odometer (`roll`) or steps (`count`);\n * separators and affixes are laid out by the format, and the number slides\n * to stay aligned as places appear.\n */\nimport {\n\ttype KeyframeTuple,\n\tLayerAnimation,\n\ttype LayoutNode,\n} from \"framefields\";\nimport {\n\ttype Animatable,\n\tbakeSamples,\n\tbindComponent,\n\tdefineComponent,\n\ttype MeasureOptions,\n\tthemeFrames,\n\tz,\n} from \"framefields/components\";\nimport {\n\ttype CounterFormat,\n\tmilestones,\n\tnumberLayout,\n\tplacesOf,\n\treelPositions,\n\ttype ValueKey,\n\tvalueAt,\n} from \"./plan.js\";\n\nexport type { CounterFormat } from \"./plan.js\";\n\nexport interface CounterCues {\n\t[name: string]: ReadonlyArray<{ frame: number }>;\n\t/** Every reel is at rest on the final value. */\n\tsettled: ReadonlyArray<{ frame: number }>;\n\t/** Crossings of `milestoneEvery` (e.g. for tick sounds). */\n\tmilestones: ReadonlyArray<{ frame: number; value: number }>;\n}\n\n/** A slot in the number: a digit reel, a separator, or a character column. */\ninterface Slot {\n\twidth: number;\n\t/** Visible on each frame. */\n\tvisible: boolean[];\n\tbuild(x: number): Animatable<LayoutNode>;\n}\n\nconst MINUS = \"−\";\n/** Frames a slot takes to fade, and the number to slide, when places change. */\nconst SETTLE = 6;\n\nexport const counter = /* @__PURE__ */ defineComponent({\n\tname: \"Counter\",\n\tprops: () => ({\n\t\tfrom: z.number().default(0),\n\t\tto: z.number().optional(),\n\t\t/** Multi-step counts: `[frame, value, ease?]`, frames ascending. */\n\t\tvalues: z\n\t\t\t.array(\n\t\t\t\tz.union([\n\t\t\t\t\tz.tuple([z.number().int().min(0), z.number()]),\n\t\t\t\t\tz.tuple([z.number().int().min(0), z.number(), z.string()]),\n\t\t\t\t]),\n\t\t\t)\n\t\t\t.min(1)\n\t\t\t.optional(),\n\t\t/** `\",d\"`, `\"$,.2f\"`, `\".1%\"`, Intl options, or `(n) => string`. */\n\t\tformat: z\n\t\t\t.union([\n\t\t\t\tz.string(),\n\t\t\t\tz.custom<(value: number) => string>((v) => typeof v === \"function\"),\n\t\t\t\tz.custom<Intl.NumberFormatOptions>(\n\t\t\t\t\t(v) => typeof v === \"object\" && v !== null,\n\t\t\t\t),\n\t\t\t])\n\t\t\t.default(\",d\"),\n\t\tlocale: z.string().default(\"en-US\"),\n\t\tprefix: z.string().default(\"\"),\n\t\tsuffix: z.string().default(\"\"),\n\t\t/** Affix size relative to the digits (e.g. 0.6 for a small `%`). */\n\t\taffixScale: z.number().min(0.2).max(1).default(1),\n\t\t/** `roll`: odometer reels. `count`: digits step frame by frame. */\n\t\tstyle: z.enum([\"roll\", \"count\"]).default(\"roll\"),\n\t\t/** Frames the count takes (`from` → `to`). Default: twice the theme's slow speed. */\n\t\tcountDuration: z.number().int().positive().optional(),\n\t\tease: z.string().optional(),\n\t\t/** Frames each higher place settles after the one below it (`roll`). */\n\t\tdigitStagger: z.number().min(0).default(2),\n\t\t/** Vertical blur on fast reels (`roll`). */\n\t\tmotionBlur: z.boolean().default(true),\n\t\tmilestoneEvery: z.number().positive().optional(),\n\t\tfontFamily: z.string().optional(),\n\t\tfontSize: z.number().positive().optional(),\n\t\tfontWeight: z.union([z.number(), z.string()]).optional(),\n\t\tfill: z.string().optional(),\n\t\talign: z.enum([\"start\", \"center\", \"end\"]).default(\"start\"),\n\t}),\n\tdefaults: { enter: false, exit: false, hold: 30 },\n\tbuild(ctx, props) {\n\t\tconst { host, theme, fps } = ctx;\n\t\tconst fs = props.fontSize ?? 120 * theme.scale;\n\t\tconst fontFamily = props.fontFamily ?? theme.font.display;\n\t\tconst ease = props.ease ?? theme.motion.ease.move;\n\t\tconst roll = props.style === \"roll\";\n\n\t\tif (props.values === undefined && props.to === undefined)\n\t\t\tthrow new Error(\"Counter: give `to` (or `values`).\");\n\t\tconst keys: ValueKey[] = props.values\n\t\t\t? props.values.map((v) => [...v] as unknown as ValueKey)\n\t\t\t: [\n\t\t\t\t\t[0, props.from],\n\t\t\t\t\t[\n\t\t\t\t\t\tprops.countDuration ?? themeFrames(theme, \"slow\", fps) * 2,\n\t\t\t\t\t\tprops.to as number,\n\t\t\t\t\t],\n\t\t\t\t];\n\t\tkeys.forEach((k, i) => {\n\t\t\tconst prev = keys[i - 1];\n\t\t\tif (prev && k[0] <= prev[0])\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Counter: \\`values\\` frames must ascend (${prev[0]} then ${k[0]}).`,\n\t\t\t\t);\n\t\t});\n\n\t\tconst text = (content: string, size: number, id: string, extra = {}) =>\n\t\t\thost.text(content, {\n\t\t\t\tid: ctx.partId(id),\n\t\t\t\tposition: \"absolute\",\n\t\t\t\tfontFamily,\n\t\t\t\tfontSize: size,\n\t\t\t\tfill: props.fill ?? theme.color.ink,\n\t\t\t\t...(props.fontWeight !== undefined && { fontWeight: props.fontWeight }),\n\t\t\t\t...extra,\n\t\t\t});\n\t\tconst measureWith = (s: string, size = fs) =>\n\t\t\thost.measure(s, {\n\t\t\t\tfontFamily,\n\t\t\t\tfontSize: size,\n\t\t\t\tfontWeight: props.fontWeight,\n\t\t\t} satisfies MeasureOptions);\n\t\tconst lh = measureWith(\"0\").lineHeight;\n\n\t\tconst layout = numberLayout(props.format as CounterFormat, props.locale);\n\t\tconst stagger = roll ? (props.digitStagger * fps) / 30 : 0;\n\t\tconst lastKey = (keys[keys.length - 1] as ValueKey)[0];\n\t\t// Reels count in the last shown digit: 99.5 at two decimals is 9950.\n\t\tconst scale = layout ? layout.scale * 10 ** layout.decimals : 1;\n\t\tconst places = layout\n\t\t\t? Math.max(...keys.map((k) => placesOf(k[1] * scale, layout.decimals)))\n\t\t\t: 1;\n\t\tconst settle = Math.round(lastKey + stagger * (places - 1));\n\t\tconst values = Array.from({ length: settle + 1 }, (_, f) =>\n\t\t\tvalueAt(keys, f, ease),\n\t\t);\n\t\tconst scaled = values.map((v) => Math.round(v * scale));\n\n\t\t// ── Slots, right to left ──────────────────────────────────────────\n\t\tconst slots: Slot[] = [];\n\t\tif (layout) {\n\t\t\tconst cellW = Math.max(\n\t\t\t\t...[...\"0123456789\"].map((d) => measureWith(d).width),\n\t\t\t);\n\t\t\tconst shows = (place: number) =>\n\t\t\t\tscaled.map(\n\t\t\t\t\t(n) =>\n\t\t\t\t\t\tplace <= layout.decimals ||\n\t\t\t\t\t\tMath.floor(Math.abs(n) / 10 ** place) > 0,\n\t\t\t\t);\n\t\t\tfor (let p = 0; p < places; p++) {\n\t\t\t\tif (p === layout.decimals && layout.decimals > 0) {\n\t\t\t\t\tconst mark = layout.decimalMark;\n\t\t\t\t\tslots.push({\n\t\t\t\t\t\twidth: measureWith(mark).width,\n\t\t\t\t\t\tvisible: scaled.map(() => true),\n\t\t\t\t\t\tbuild: (x) => text(mark, fs, \"decimal\", { x, y: 0 }),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tconst q = p - layout.decimals;\n\t\t\t\tif (layout.group && q > 0 && q % 3 === 0) {\n\t\t\t\t\tconst sep = layout.group;\n\t\t\t\t\tslots.push({\n\t\t\t\t\t\twidth: measureWith(sep).width,\n\t\t\t\t\t\tvisible: shows(p),\n\t\t\t\t\t\tbuild: (x) => text(sep, fs, `group-${q}`, { x, y: 0 }),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tconst place = p;\n\t\t\t\tslots.push({\n\t\t\t\t\twidth: cellW,\n\t\t\t\t\tvisible: shows(place),\n\t\t\t\t\tbuild: (x) =>\n\t\t\t\t\t\tdigitCell(\n\t\t\t\t\t\t\tplace,\n\t\t\t\t\t\t\tx,\n\t\t\t\t\t\t\tcellW,\n\t\t\t\t\t\t\troll\n\t\t\t\t\t\t\t\t? reelPositions(keys, place, {\n\t\t\t\t\t\t\t\t\t\tscale,\n\t\t\t\t\t\t\t\t\t\tstagger,\n\t\t\t\t\t\t\t\t\t\tease,\n\t\t\t\t\t\t\t\t\t\tlength: settle,\n\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t: scaled.map((n) => Math.floor(Math.abs(n) / 10 ** place) % 10),\n\t\t\t\t\t\t),\n\t\t\t\t});\n\t\t\t}\n\t\t} else {\n\t\t\t// A formatter function: one column per character from the right.\n\t\t\tconst format = props.format as (n: number) => string;\n\t\t\tconst strings = values.map((v) => format(v));\n\t\t\tconst columns = Math.max(...strings.map((s) => s.length));\n\t\t\tfor (let c = 0; c < columns; c++) {\n\t\t\t\tconst chars = strings.map((s) => s[s.length - 1 - c] ?? \"\");\n\t\t\t\tconst rows = [...new Set(chars)];\n\t\t\t\tconst width = Math.max(\n\t\t\t\t\t0,\n\t\t\t\t\t...rows.map((r) => (r ? measureWith(r).width : 0)),\n\t\t\t\t);\n\t\t\t\tconst col = c;\n\t\t\t\tslots.push({\n\t\t\t\t\twidth,\n\t\t\t\t\tvisible: chars.map((ch) => ch !== \"\"),\n\t\t\t\t\tbuild: (x) => charCell(col, x, width, rows, chars),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tfunction digitCell(\n\t\t\tplace: number,\n\t\t\tx: number,\n\t\t\twidth: number,\n\t\t\tpositions: number[],\n\t\t): Animatable<LayoutNode> {\n\t\t\tconst lo = Math.floor(Math.min(...positions));\n\t\t\tconst hi = Math.ceil(Math.max(...positions));\n\t\t\tconst rows = roll\n\t\t\t\t? Array.from(\n\t\t\t\t\t\t{ length: hi - lo + 1 },\n\t\t\t\t\t\t(_, k) => (((lo + k) % 10) + 10) % 10,\n\t\t\t\t\t)\n\t\t\t\t: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];\n\t\t\tconst base = roll ? lo : 0;\n\t\t\tconst ys = positions.map((pos) => -(pos - base) * lh);\n\t\t\tconst reel = text(rows.join(\"\\n\"), fs, `reel-${place}`, {\n\t\t\t\tx: 0,\n\t\t\t\ty: ys[0] ?? 0,\n\t\t\t\twidth,\n\t\t\t\talign: \"center\",\n\t\t\t\t...(roll && props.motionBlur && { motionBlurShutter: 90 }),\n\t\t\t});\n\t\t\treel.animate(\n\t\t\t\tLayerAnimation.keys(\n\t\t\t\t\t\"y\",\n\t\t\t\t\troll ? bakeSamples(ys, 0, { tolerance: 0.2 }) : stepKeys(ys),\n\t\t\t\t),\n\t\t\t);\n\t\t\treturn host.box({\n\t\t\t\tid: ctx.partId(`cell-${place}`),\n\t\t\t\tposition: \"absolute\",\n\t\t\t\tx,\n\t\t\t\ty: 0,\n\t\t\t\twidth,\n\t\t\t\theight: lh,\n\t\t\t\toverflow: \"hidden\",\n\t\t\t\tchildren: [reel],\n\t\t\t});\n\t\t}\n\n\t\tfunction charCell(\n\t\t\tcolumn: number,\n\t\t\tx: number,\n\t\t\twidth: number,\n\t\t\trows: string[],\n\t\t\tchars: string[],\n\t\t): Animatable<LayoutNode> {\n\t\t\tconst ys = chars.map((ch) => -rows.indexOf(ch) * lh);\n\t\t\tconst reel = text(\n\t\t\t\trows.map((r) => r || \" \").join(\"\\n\"),\n\t\t\t\tfs,\n\t\t\t\t`reel-${column}`,\n\t\t\t\t{\n\t\t\t\t\tx: 0,\n\t\t\t\t\ty: ys[0] ?? 0,\n\t\t\t\t\twidth,\n\t\t\t\t\talign: \"center\",\n\t\t\t\t},\n\t\t\t);\n\t\t\treel.animate(LayerAnimation.keys(\"y\", stepKeys(ys)));\n\t\t\treturn host.box({\n\t\t\t\tid: ctx.partId(`cell-${column}`),\n\t\t\t\tposition: \"absolute\",\n\t\t\t\tx,\n\t\t\t\ty: 0,\n\t\t\t\twidth,\n\t\t\t\theight: lh,\n\t\t\t\toverflow: \"hidden\",\n\t\t\t\tchildren: [reel],\n\t\t\t});\n\t\t}\n\n\t\t// ── Layout over time ──────────────────────────────────────────────\n\t\tconst affix = fs * props.affixScale;\n\t\tconst affixLh = measureWith(\"0\", affix).lineHeight;\n\t\tconst affixY = (lh - affixLh) * 0.78;\n\t\tconst prefix = (layout?.prefix ?? \"\") + props.prefix;\n\t\tconst suffix = props.suffix + (layout?.suffix ?? \"\");\n\t\tconst prefixW = prefix ? measureWith(prefix, affix).width : 0;\n\t\tconst suffixW = suffix ? measureWith(suffix, affix).width : 0;\n\t\tconst negative = scaled.map((n) => n < 0);\n\t\tconst minusW = negative.some(Boolean) ? measureWith(MINUS).width : 0;\n\n\t\tconst digitsW = slots.reduce((a, s) => a + s.width, 0);\n\t\tconst W = prefixW + minusW + digitsW + suffixW;\n\t\tconst shown = scaled.map((_, f) =>\n\t\t\tslots.reduce((a, s) => a + (s.visible[f] ? s.width : 0), 0),\n\t\t);\n\t\tconst content = shown.map(\n\t\t\t(w, f) => prefixW + (negative[f] ? minusW : 0) + w + suffixW,\n\t\t);\n\t\tconst af = props.align === \"center\" ? 0.5 : props.align === \"end\" ? 1 : 0;\n\t\tconst left = content.map((c) => (W - c) * af);\n\n\t\tconst parts: Record<string, Animatable<LayoutNode>> = {};\n\t\tlet cursor = digitsW;\n\t\tconst cells = slots.map((s, i) => {\n\t\t\tcursor -= s.width;\n\t\t\tconst node = s.build(cursor);\n\t\t\tnode.animate(LayerAnimation.keys(\"opacity\", fadeKeys(s.visible)));\n\t\t\tparts[`slot-${i}`] = node;\n\t\t\treturn node;\n\t\t});\n\t\tconst digits = host.box({\n\t\t\tid: ctx.partId(\"digits\"),\n\t\t\tposition: \"absolute\",\n\t\t\tx: 0,\n\t\t\ty: 0,\n\t\t\twidth: digitsW,\n\t\t\theight: lh,\n\t\t\tchildren: cells,\n\t\t});\n\t\tconst digitsX = shown.map(\n\t\t\t(w, f) =>\n\t\t\t\t(left[f] as number) +\n\t\t\t\tprefixW +\n\t\t\t\t(negative[f] ? minusW : 0) +\n\t\t\t\tw -\n\t\t\t\tdigitsW,\n\t\t);\n\t\tdigits.animate(LayerAnimation.keys(\"x\", slideKeys(digitsX)));\n\t\tparts.digits = digits;\n\t\tconst children: LayoutNode[] = [digits];\n\n\t\tif (prefix) {\n\t\t\tconst node = text(prefix, affix, \"prefix\", { x: 0, y: affixY });\n\t\t\tnode.animate(LayerAnimation.keys(\"x\", slideKeys(left)));\n\t\t\tparts.prefix = node;\n\t\t\tchildren.push(node);\n\t\t}\n\t\tif (minusW > 0) {\n\t\t\tconst node = text(MINUS, fs, \"minus\", { x: 0, y: 0 });\n\t\t\tnode.animate(\n\t\t\t\tLayerAnimation.create()\n\t\t\t\t\t.keys(\"x\", slideKeys(left.map((l) => l + prefixW)))\n\t\t\t\t\t.keys(\"opacity\", fadeKeys(negative)),\n\t\t\t);\n\t\t\tparts.minus = node;\n\t\t\tchildren.push(node);\n\t\t}\n\t\tif (suffix) {\n\t\t\tconst node = text(suffix, affix, \"suffix\", { x: 0, y: affixY });\n\t\t\tnode.animate(\n\t\t\t\tLayerAnimation.keys(\"x\", slideKeys(digitsX.map((x) => x + digitsW))),\n\t\t\t);\n\t\t\tparts.suffix = node;\n\t\t\tchildren.push(node);\n\t\t}\n\n\t\treturn {\n\t\t\tchildren,\n\t\t\twidth: W,\n\t\t\theight: lh,\n\t\t\tduration: settle + 1,\n\t\t\topenEnded: true,\n\t\t\tcues: {\n\t\t\t\tsettled: [{ frame: settle }],\n\t\t\t\tmilestones:\n\t\t\t\t\tprops.milestoneEvery === undefined\n\t\t\t\t\t\t? []\n\t\t\t\t\t\t: milestones(values, props.milestoneEvery),\n\t\t\t} as CounterCues,\n\t\t\tparts,\n\t\t};\n\t},\n});\n\n/** Hold keys at every change of a per-frame series. */\nfunction stepKeys(series: readonly number[]): KeyframeTuple[] {\n\tconst keys: KeyframeTuple[] = [[0, series[0] ?? 0]];\n\tfor (let f = 1; f < series.length; f++)\n\t\tif (series[f] !== series[f - 1])\n\t\t\tkeys.push([f, series[f] as number, \"hold\"]);\n\treturn keys;\n}\n\n/** A per-frame target that changes in steps, eased over {@link SETTLE} frames. */\nfunction slideKeys(series: readonly number[]): KeyframeTuple[] {\n\tconst keys: KeyframeTuple[] = [[0, series[0] ?? 0]];\n\tfor (let f = 1; f < series.length; f++) {\n\t\tconst prev = series[f - 1] as number;\n\t\tconst next = series[f] as number;\n\t\tif (Math.abs(next - prev) < 0.01) continue;\n\t\tconst last = keys[keys.length - 1] as KeyframeTuple;\n\t\tif (f - 1 > last[0]) keys.push([f - 1, prev]);\n\t\tkeys.push([f - 1 + SETTLE, next, \"power2.out\"]);\n\t}\n\treturn dedupe(keys);\n}\n\nfunction fadeKeys(visible: readonly boolean[]): KeyframeTuple[] {\n\treturn slideKeys(visible.map((v) => (v ? 1 : 0)));\n}\n\n/** Keys must ascend: a later change replaces anything it overlaps. */\nfunction dedupe(keys: KeyframeTuple[]): KeyframeTuple[] {\n\tconst out: KeyframeTuple[] = [];\n\tfor (const k of keys) {\n\t\twhile (out.length > 0 && (out[out.length - 1] as KeyframeTuple)[0] >= k[0])\n\t\t\tout.pop();\n\t\tout.push(k);\n\t}\n\treturn out;\n}\n\n/**\n * A number that counts to its value on odometer reels (or stepping digits),\n * with format-aware separators, affixes and multi-step values.\n */\nexport const Counter = /* @__PURE__ */ bindComponent(counter);\n"
30
+ },
31
+ {
32
+ "path": "counter/plan.ts",
33
+ "content": "/**\n * Counter planning, kept pure: how a format lays a number out (affixes,\n * separators, fixed decimals), the value over time, and where each digit\n * reel sits on every frame.\n */\nimport { evaluateEase } from \"framefields/components\";\n\n/** How a known format writes any value: digits are laid out from these. */\nexport interface NumberLayout {\n\tdecimals: number;\n\t/** Thousands separator, or null for none. */\n\tgroup: string | null;\n\tdecimalMark: string;\n\tprefix: string;\n\tsuffix: string;\n\t/** 100 for percentages. */\n\tscale: number;\n}\n\nexport type CounterFormat =\n\t| string\n\t| Intl.NumberFormatOptions\n\t| ((value: number) => string);\n\n/**\n * A layout for a d3-style format string (`\",d\"`, `\",.2f\"`, `\".1%\"`,\n * `\"$,.0f\"`) or Intl options. Functions return null: they are laid out per\n * character instead.\n */\nexport function numberLayout(\n\tformat: CounterFormat,\n\tlocale = \"en-US\",\n): NumberLayout | null {\n\tif (typeof format === \"function\") return null;\n\tif (typeof format === \"string\") {\n\t\tconst m = format.match(/^(\\$)?(,)?(?:\\.(\\d+))?([df%]?)$/);\n\t\tif (!m) {\n\t\t\tthrow new Error(\n\t\t\t\t`Counter: unsupported format \"${format}\". Use [$][,][.precision][d|f|%] (e.g. \",d\", \"$,.2f\", \".1%\"), Intl options, or a function.`,\n\t\t\t);\n\t\t}\n\t\tconst type = m[4] ?? \"\";\n\t\treturn {\n\t\t\tdecimals: type === \"d\" ? 0 : Number(m[3] ?? (type === \"%\" ? 0 : 0)),\n\t\t\tgroup: m[2] ? \",\" : null,\n\t\t\tdecimalMark: \".\",\n\t\t\tprefix: m[1] ?? \"\",\n\t\t\tsuffix: type === \"%\" ? \"%\" : \"\",\n\t\t\tscale: type === \"%\" ? 100 : 1,\n\t\t};\n\t}\n\tconst nf = new Intl.NumberFormat(locale, format);\n\tconst resolved = nf.resolvedOptions();\n\tconst parts = nf.formatToParts(1234567.891);\n\tconst first = parts.findIndex((p) => p.type === \"integer\");\n\tconst lastNumeric = parts.reduce(\n\t\t(at, p, i) =>\n\t\t\t[\"integer\", \"group\", \"decimal\", \"fraction\"].includes(p.type) ? i : at,\n\t\t-1,\n\t);\n\treturn {\n\t\tdecimals: resolved.maximumFractionDigits ?? 0,\n\t\tgroup: parts.find((p) => p.type === \"group\")?.value ?? null,\n\t\tdecimalMark: parts.find((p) => p.type === \"decimal\")?.value ?? \".\",\n\t\tprefix: parts\n\t\t\t.slice(0, first)\n\t\t\t.map((p) => p.value)\n\t\t\t.join(\"\"),\n\t\tsuffix: parts\n\t\t\t.slice(lastNumeric + 1)\n\t\t\t.map((p) => p.value)\n\t\t\t.join(\"\"),\n\t\tscale: format.style === \"percent\" ? 100 : 1,\n\t};\n}\n\n/** `[frame, value, ease?]`: the value is reached at `frame`. */\nexport type ValueKey = readonly [frame: number, value: number, ease?: string];\n\n/** The value at any frame of a piecewise trajectory. */\nexport function valueAt(\n\tkeys: readonly ValueKey[],\n\tframe: number,\n\tfallbackEase: string,\n): number {\n\tconst first = keys[0];\n\tif (!first) return 0;\n\tif (frame <= first[0]) return first[1];\n\tfor (let i = 1; i < keys.length; i++) {\n\t\tconst a = keys[i - 1] as ValueKey;\n\t\tconst b = keys[i] as ValueKey;\n\t\tif (frame <= b[0]) {\n\t\t\tconst t = (frame - a[0]) / Math.max(1, b[0] - a[0]);\n\t\t\treturn a[1] + (b[1] - a[1]) * evaluateEase(b[2] ?? fallbackEase, t);\n\t\t}\n\t}\n\treturn (keys[keys.length - 1] as ValueKey)[1];\n}\n\n/** Number of digit places a scaled integer shows (at least `decimals + 1`). */\nexport function placesOf(n: number, decimals: number): number {\n\tconst abs = Math.abs(Math.round(n));\n\treturn Math.max(\n\t\tdecimals + 1,\n\t\tabs === 0 ? 1 : Math.floor(Math.log10(abs)) + 1,\n\t);\n}\n\n/** Most turns a reel makes in one move; the rest is a blur nobody can read. */\nexport const MAX_TURNS = 3;\n\n/**\n * Reel position (in digit steps) of place `p` on every frame `0..length`:\n * each segment moves every place from its old digit to its new one, higher\n * places later by `stagger × p` frames, spinning the way an odometer does\n * (capped at {@link MAX_TURNS} turns). `pos mod 10` is the digit shown.\n */\nexport function reelPositions(\n\tkeys: readonly ValueKey[],\n\tplace: number,\n\to: { scale: number; stagger: number; ease: string; length: number },\n): number[] {\n\tconst unit = 10 ** place;\n\tconst digitsAt = (v: number) =>\n\t\tMath.floor(Math.abs(Math.round(v * o.scale)) / unit);\n\tconst out = new Array<number>(o.length + 1);\n\tlet pos = digitsAt(keys[0]?.[1] ?? 0);\n\tlet free = Number.NEGATIVE_INFINITY;\n\tconst segments = keys.slice(1).map((b, i) => {\n\t\tconst a = keys[i] as ValueKey;\n\t\tconst delta = digitsAt(b[1]) - digitsAt(a[1]);\n\t\tconst turns = Math.abs(delta);\n\t\tconst distance =\n\t\t\tMath.sign(delta) *\n\t\t\t(turns <= MAX_TURNS * 10 ? turns : MAX_TURNS * 10 + (turns % 10));\n\t\t// A place never starts a move before finishing its last one.\n\t\tconst start = Math.max(a[0] + o.stagger * place, free);\n\t\tconst seg = {\n\t\t\tfrom: pos,\n\t\t\tdistance,\n\t\t\tstart,\n\t\t\tend: start + (b[0] - a[0]),\n\t\t\tease: b[2] ?? o.ease,\n\t\t};\n\t\tpos += distance;\n\t\tfree = seg.end;\n\t\treturn seg;\n\t});\n\tfor (let f = 0; f <= o.length; f++) {\n\t\tlet p = digitsAt(keys[0]?.[1] ?? 0);\n\t\tfor (const s of segments) {\n\t\t\tif (f <= s.start) break;\n\t\t\tconst t = Math.min(1, (f - s.start) / Math.max(1, s.end - s.start));\n\t\t\tp = s.from + s.distance * evaluateEase(s.ease, t);\n\t\t}\n\t\tout[f] = p;\n\t}\n\treturn out;\n}\n\n/** Frames where `value` crosses a multiple of `every`, with that multiple. */\nexport function milestones(\n\tvalues: readonly number[],\n\tevery: number,\n): Array<{ frame: number; value: number }> {\n\tconst out: Array<{ frame: number; value: number }> = [];\n\tfor (let f = 1; f < values.length; f++) {\n\t\tconst a = Math.floor((values[f - 1] as number) / every);\n\t\tconst b = Math.floor((values[f] as number) / every);\n\t\tif (a === b) continue;\n\t\tconst dir = Math.sign(b - a);\n\t\tfor (let k = a + (dir > 0 ? 1 : 0); dir > 0 ? k <= b : k > b; k += dir)\n\t\t\tout.push({ frame: f, value: k * every });\n\t}\n\treturn out;\n}\n"
34
+ }
35
+ ]
36
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "device-frame",
3
+ "title": "DeviceFrame",
4
+ "summary": "A phone, tablet or laptop around a screen; content is authored in device points and `at(x, y)` maps them for pointers and touches.",
5
+ "tags": [
6
+ "device",
7
+ "phone",
8
+ "mobile",
9
+ "tablet",
10
+ "laptop",
11
+ "mockup",
12
+ "screen",
13
+ "app"
14
+ ],
15
+ "example": "DeviceFrame({ device: \"phone\", content: \"assets/app.mp4\" })",
16
+ "parts": [
17
+ "body",
18
+ "bezel",
19
+ "screen",
20
+ "content",
21
+ "island",
22
+ "status",
23
+ "buttons",
24
+ "base",
25
+ "shadow",
26
+ "tilt"
27
+ ],
28
+ "cues": [],
29
+ "registryDependencies": [],
30
+ "files": [
31
+ {
32
+ "path": "device-frame/index.ts",
33
+ "content": "/**\n * DeviceFrame: a phone, tablet or laptop around a screen. Content is\n * authored in the device's own points (a phone screen is 393 × 852) and\n * scaled into the screen, so a mock UI made at device size drops in\n * unchanged; `at(x, y)` maps those points to the parent's coordinates.\n */\nimport type { BoxNode, LayoutNode } from \"framefields\";\nimport {\n\ttype Animatable,\n\tbindComponent,\n\tdefineComponent,\n\ttype Host,\n\ttype Vec2,\n\tz,\n} from \"framefields/components\";\n\nexport const DEVICES = [\"phone\", \"tablet\", \"laptop\"] as const;\nexport type Device = (typeof DEVICES)[number];\n\nexport interface DeviceFrameApi {\n\t/** The screen in the parent's coordinates. */\n\treadonly screenRect: { x: number; y: number; width: number; height: number };\n\t/** Screen points → the parent's coordinates (ignores `tilt`). */\n\tat(x: number, y: number): Vec2;\n\t/** Add layers (in screen points) to the screen. */\n\tfill(...nodes: LayoutNode[]): DeviceFrameApi;\n}\n\nexport interface DeviceFrameCues {\n\t[name: string]: ReadonlyArray<{ frame: number }>;\n}\n\nconst POINTS: Record<Device, [number, number]> = {\n\tphone: [393, 852],\n\ttablet: [820, 1180],\n\tlaptop: [1440, 900],\n};\n\n/** Drop shadow per elevation: [opacity, distance, size] at scale 1. */\nconst SHADOWS = [\n\t[0, 0, 0],\n\t[0.3, 10, 26],\n\t[0.45, 26, 56],\n\t[0.6, 46, 90],\n] as const;\n\nconst FINISHES = {\n\tgraphite: { body: \"#2c2e33\", edge: \"#55585f\", button: \"#3a3d43\" },\n\tsilver: { body: \"#d8dade\", edge: \"#f2f3f5\", button: \"#c4c7cd\" },\n\tmidnight: { body: \"#1b2130\", edge: \"#424a5c\", button: \"#262d3d\" },\n} as const;\n\nexport const deviceFrame = /* @__PURE__ */ defineComponent({\n\tname: \"DeviceFrame\",\n\tprops: () => ({\n\t\tdevice: z.enum(DEVICES).default(\"phone\"),\n\t\torientation: z.enum([\"portrait\", \"landscape\"]).default(\"portrait\"),\n\t\t/** Layers in screen points, or a media path that fills the screen. */\n\t\tcontent: z\n\t\t\t.custom<LayoutNode | LayoutNode[] | string>(\n\t\t\t\t(v) =>\n\t\t\t\t\ttypeof v === \"string\" ||\n\t\t\t\t\tArray.isArray(v) ||\n\t\t\t\t\t(typeof v === \"object\" && v !== null),\n\t\t\t\t{ message: \"Expected layers or a media path\" },\n\t\t\t)\n\t\t\t.optional(),\n\t\tfit: z.enum([\"cover\", \"contain\", \"fill\"]).default(\"cover\"),\n\t\t/** Screen size content is authored in. */\n\t\tpoints: z.tuple([z.number().positive(), z.number().positive()]).optional(),\n\t\tfinish: z\n\t\t\t.union([z.enum([\"graphite\", \"silver\", \"midnight\"]), z.string()])\n\t\t\t.default(\"graphite\"),\n\t\tisland: z.boolean().default(true),\n\t\tstatusBar: z\n\t\t\t.union([\n\t\t\t\tz.boolean(),\n\t\t\t\tz\n\t\t\t\t\t.object({\n\t\t\t\t\t\ttime: z.string().optional(),\n\t\t\t\t\t\t/** Dark icons, for light screens. */\n\t\t\t\t\t\tdark: z.boolean().optional(),\n\t\t\t\t\t})\n\t\t\t\t\t.strict(),\n\t\t\t])\n\t\t\t.default(true),\n\t\tscreen: z.string().default(\"#000000\"),\n\t\ttilt: z\n\t\t\t.union([\n\t\t\t\tz.literal(false),\n\t\t\t\tz\n\t\t\t\t\t.object({\n\t\t\t\t\t\trotateX: z.number().default(10),\n\t\t\t\t\t\trotateY: z.number().default(-18),\n\t\t\t\t\t})\n\t\t\t\t\t.strict(),\n\t\t\t])\n\t\t\t.default(false),\n\t\televation: z.number().int().min(0).max(3).default(2),\n\t}),\n\tdefaults: { enter: \"rise\" },\n\tbuild(ctx, props) {\n\t\tconst { host, theme } = ctx;\n\t\tconst s = theme.scale;\n\t\tconst device = props.device;\n\t\tconst laptop = device === \"laptop\";\n\t\tconst landscape = !laptop && props.orientation === \"landscape\";\n\t\tconst native = POINTS[device];\n\t\tconst [pw, ph] =\n\t\t\tprops.points ?? (landscape ? [native[1], native[0]] : native);\n\n\t\t// ── Proportions, in points ──────────────────────────────────────\n\t\tconst short = Math.min(pw, ph);\n\t\tconst bezel =\n\t\t\tdevice === \"phone\"\n\t\t\t\t? short * 0.045\n\t\t\t\t: device === \"tablet\"\n\t\t\t\t\t? short * 0.04\n\t\t\t\t\t: pw * 0.018;\n\t\tconst rim = device === \"laptop\" ? pw * 0.004 : short * 0.012;\n\t\tconst top = laptop ? bezel * 1.6 : bezel;\n\t\tconst bodyWp = pw + bezel * 2 + rim * 2;\n\t\tconst bodyHp = ph + top + bezel + rim * 2;\n\t\tconst baseWp = laptop ? bodyWp * 1.14 : 0;\n\t\tconst baseHp = laptop ? pw * 0.032 : 0;\n\t\tconst buttonP = laptop ? 0 : short * 0.011;\n\t\tconst totalWp = laptop ? baseWp : bodyWp + buttonP * 2;\n\t\tconst totalHp = bodyHp + baseHp;\n\n\t\t// Points → px: from the size asked for (height for handhelds, width for laptops).\n\t\tconst k = laptop\n\t\t\t? (ctx.requested.width ?? 1280 * s) / totalWp\n\t\t\t: (ctx.requested.height ?? 860 * s) / totalHp;\n\t\tconst W = ctx.requested.width ?? totalWp * k;\n\t\tconst H = ctx.requested.height ?? totalHp * k;\n\t\tconst px = (v: number) => v * k;\n\t\tconst bodyX = (W - bodyWp * k) / 2;\n\t\tconst bodyY = 0;\n\t\tconst screenX = bodyX + px(rim + bezel);\n\t\tconst screenY = bodyY + px(rim + top);\n\t\tconst screenW = px(pw);\n\t\tconst screenH = px(ph);\n\n\t\tconst named = FINISHES[props.finish as keyof typeof FINISHES];\n\t\tconst finish = named ?? {\n\t\t\tbody: props.finish,\n\t\t\tedge: props.finish,\n\t\t\tbutton: props.finish,\n\t\t};\n\t\tconst bodyR = laptop\n\t\t\t? px(pw * 0.022)\n\t\t\t: px(short * (device === \"phone\" ? 0.14 : 0.075));\n\t\tconst screenR = laptop\n\t\t\t? px(pw * 0.006)\n\t\t\t: Math.max(0, bodyR - px(rim + bezel));\n\n\t\tconst parts: Record<string, Animatable<LayoutNode>> = {};\n\t\tconst kids: LayoutNode[] = [];\n\t\tconst box = (name: string, o: Partial<BoxNode>) => {\n\t\t\tconst node = host.box({\n\t\t\t\tid: ctx.partId(name),\n\t\t\t\tposition: \"absolute\",\n\t\t\t\t...o,\n\t\t\t});\n\t\t\tparts[name] = node;\n\t\t\treturn node;\n\t\t};\n\n\t\t// ── Shadow, buttons, body ───────────────────────────────────────\n\t\tif (props.elevation > 0) {\n\t\t\tconst e = props.elevation;\n\t\t\tconst plate = box(\"shadow\", {\n\t\t\t\tx: bodyX,\n\t\t\t\ty: bodyY,\n\t\t\t\twidth: px(bodyWp),\n\t\t\t\theight: px(bodyHp + baseHp),\n\t\t\t\tbackground: finish.body,\n\t\t\t\tborderRadius: bodyR,\n\t\t\t});\n\t\t\tconst [opacity, distance, size] = SHADOWS[e] ?? SHADOWS[0];\n\t\t\thost.style(plate, {\n\t\t\t\tdropShadow: {\n\t\t\t\t\tcolor: \"#000000\",\n\t\t\t\t\topacity,\n\t\t\t\t\tangle: 90,\n\t\t\t\t\tdistance: distance * s,\n\t\t\t\t\tsize: size * s,\n\t\t\t\t\tspread: 0,\n\t\t\t\t},\n\t\t\t});\n\t\t\tkids.push(plate);\n\t\t}\n\t\tif (!laptop) {\n\t\t\t// Side buttons stand just proud of the body.\n\t\t\tconst bw = px(buttonP * 1.6);\n\t\t\tconst along = landscape ? px(bodyWp) : px(bodyHp);\n\t\t\tconst spec: Array<[side: -1 | 1, from: number, len: number]> =\n\t\t\t\tdevice === \"phone\"\n\t\t\t\t\t? [\n\t\t\t\t\t\t\t[-1, 0.17, 0.04],\n\t\t\t\t\t\t\t[-1, 0.24, 0.07],\n\t\t\t\t\t\t\t[-1, 0.33, 0.07],\n\t\t\t\t\t\t\t[1, 0.26, 0.11],\n\t\t\t\t\t\t]\n\t\t\t\t\t: [\n\t\t\t\t\t\t\t[1, 0.08, 0.05],\n\t\t\t\t\t\t\t[1, 0.15, 0.05],\n\t\t\t\t\t\t];\n\t\t\tconst buttons = spec.map(([side, from, len], i) =>\n\t\t\t\tlandscape\n\t\t\t\t\t? host.box({\n\t\t\t\t\t\t\tid: ctx.partId(`button-${i}`),\n\t\t\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\t\t\tx: bodyX + from * along,\n\t\t\t\t\t\t\ty: side < 0 ? bodyY - bw / 2 : bodyY + px(bodyHp) - bw / 2,\n\t\t\t\t\t\t\twidth: len * along,\n\t\t\t\t\t\t\theight: bw,\n\t\t\t\t\t\t\tbackground: finish.button,\n\t\t\t\t\t\t\tborderRadius: bw / 2,\n\t\t\t\t\t\t})\n\t\t\t\t\t: host.box({\n\t\t\t\t\t\t\tid: ctx.partId(`button-${i}`),\n\t\t\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\t\t\tx: side < 0 ? bodyX - bw / 2 : bodyX + px(bodyWp) - bw / 2,\n\t\t\t\t\t\t\ty: bodyY + from * along,\n\t\t\t\t\t\t\twidth: bw,\n\t\t\t\t\t\t\theight: len * along,\n\t\t\t\t\t\t\tbackground: finish.button,\n\t\t\t\t\t\t\tborderRadius: bw / 2,\n\t\t\t\t\t\t}),\n\t\t\t);\n\t\t\tconst group = box(\"buttons\", {\n\t\t\t\tx: 0,\n\t\t\t\ty: 0,\n\t\t\t\twidth: W,\n\t\t\t\theight: H,\n\t\t\t\tchildren: buttons,\n\t\t\t});\n\t\t\tkids.push(group);\n\t\t}\n\t\tkids.push(\n\t\t\tbox(\"body\", {\n\t\t\t\tx: bodyX,\n\t\t\t\ty: bodyY,\n\t\t\t\twidth: px(bodyWp),\n\t\t\t\theight: px(bodyHp),\n\t\t\t\tbackground: finish.body,\n\t\t\t\tborderRadius: bodyR,\n\t\t\t\tborderColor: finish.edge,\n\t\t\t\tborderWidth: Math.max(1, px(rim * 0.35)),\n\t\t\t\tstrokeAlign: \"inside\",\n\t\t\t}),\n\t\t\tbox(\"bezel\", {\n\t\t\t\tx: bodyX + px(rim),\n\t\t\t\ty: bodyY + px(rim),\n\t\t\t\twidth: px(bodyWp - rim * 2),\n\t\t\t\theight: px(bodyHp - rim * 2),\n\t\t\t\tbackground: \"#050506\",\n\t\t\t\tborderRadius: Math.max(0, bodyR - px(rim)),\n\t\t\t}),\n\t\t);\n\n\t\t// ── Screen: content in points, scaled in ────────────────────────\n\t\tconst content: Animatable<LayoutNode> & { children?: LayoutNode[] } = box(\n\t\t\t\"content\",\n\t\t\t{\n\t\t\t\tx: 0,\n\t\t\t\ty: 0,\n\t\t\t\twidth: pw,\n\t\t\t\theight: ph,\n\t\t\t\tscale: k,\n\t\t\t\tanchorX: 0,\n\t\t\t\tanchorY: 0,\n\t\t\t\tchildren:\n\t\t\t\t\tprops.content === undefined\n\t\t\t\t\t\t? []\n\t\t\t\t\t\t: typeof props.content === \"string\"\n\t\t\t\t\t\t\t? [\n\t\t\t\t\t\t\t\t\thost.media(props.content, {\n\t\t\t\t\t\t\t\t\t\tid: ctx.partId(\"media\"),\n\t\t\t\t\t\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\t\t\t\t\t\tx: 0,\n\t\t\t\t\t\t\t\t\t\ty: 0,\n\t\t\t\t\t\t\t\t\t\twidth: pw,\n\t\t\t\t\t\t\t\t\t\theight: ph,\n\t\t\t\t\t\t\t\t\t\tfit: props.fit,\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t: Array.isArray(props.content)\n\t\t\t\t\t\t\t\t? props.content\n\t\t\t\t\t\t\t\t: [props.content],\n\t\t\t},\n\t\t);\n\t\tconst overlay: LayoutNode[] = [];\n\t\tconst phone = device === \"phone\";\n\t\tif (phone && props.island && !landscape)\n\t\t\toverlay.push(\n\t\t\t\tbox(\"island\", {\n\t\t\t\t\tx: (pw - 124) / 2,\n\t\t\t\t\ty: 11,\n\t\t\t\t\twidth: 124,\n\t\t\t\t\theight: 36,\n\t\t\t\t\tbackground: \"#000000\",\n\t\t\t\t\tborderRadius: 18,\n\t\t\t\t}),\n\t\t\t);\n\t\tif (props.statusBar !== false && !laptop) {\n\t\t\tconst opts = typeof props.statusBar === \"object\" ? props.statusBar : {};\n\t\t\toverlay.push(\n\t\t\t\tstatusBar(ctx, {\n\t\t\t\t\twidth: pw,\n\t\t\t\t\theight: phone && !landscape ? 54 : 30,\n\t\t\t\t\ttime: opts.time ?? \"9:41\",\n\t\t\t\t\tcolor: opts.dark ? \"#000000\" : \"#ffffff\",\n\t\t\t\t\tcompact: !phone || landscape,\n\t\t\t\t\tfont: theme.font.body,\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\tconst status = overlay.length\n\t\t\t? box(\"overlay\", {\n\t\t\t\t\tx: 0,\n\t\t\t\t\ty: 0,\n\t\t\t\t\twidth: pw,\n\t\t\t\t\theight: ph,\n\t\t\t\t\tscale: k,\n\t\t\t\t\tanchorX: 0,\n\t\t\t\t\tanchorY: 0,\n\t\t\t\t\tchildren: overlay,\n\t\t\t\t})\n\t\t\t: undefined;\n\t\tkids.push(\n\t\t\tbox(\"screen\", {\n\t\t\t\tx: screenX,\n\t\t\t\ty: screenY,\n\t\t\t\twidth: screenW,\n\t\t\t\theight: screenH,\n\t\t\t\tbackground: props.screen,\n\t\t\t\tborderRadius: screenR,\n\t\t\t\toverflow: \"hidden\",\n\t\t\t\tchildren: status ? [content, status] : [content],\n\t\t\t}),\n\t\t);\n\t\tif (laptop) {\n\t\t\tkids.push(\n\t\t\t\tbox(\"base\", {\n\t\t\t\t\tx: (W - px(baseWp)) / 2,\n\t\t\t\t\ty: bodyY + px(bodyHp) - px(baseHp * 0.1),\n\t\t\t\t\twidth: px(baseWp),\n\t\t\t\t\theight: px(baseHp),\n\t\t\t\t\tbackground: finish.button,\n\t\t\t\t\tborderRadius: px(baseHp * 0.5),\n\t\t\t\t\tborderColor: finish.edge,\n\t\t\t\t\tborderWidth: Math.max(1, px(rim * 0.3)),\n\t\t\t\t\tstrokeAlign: \"inside\",\n\t\t\t\t\tchildren: [\n\t\t\t\t\t\thost.box({\n\t\t\t\t\t\t\tid: ctx.partId(\"groove\"),\n\t\t\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\t\t\tx: px(baseWp * 0.42),\n\t\t\t\t\t\t\ty: 0,\n\t\t\t\t\t\t\twidth: px(baseWp * 0.16),\n\t\t\t\t\t\t\theight: px(baseHp * 0.32),\n\t\t\t\t\t\t\tbackground: finish.body,\n\t\t\t\t\t\t\tborderRadius: px(baseHp * 0.16),\n\t\t\t\t\t\t}),\n\t\t\t\t\t],\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\n\t\tconst tilted = props.tilt !== false;\n\t\tconst stack = tilted\n\t\t\t? [\n\t\t\t\t\tbox(\"tilt\", {\n\t\t\t\t\t\tx: 0,\n\t\t\t\t\t\ty: 0,\n\t\t\t\t\t\twidth: W,\n\t\t\t\t\t\theight: H,\n\t\t\t\t\t\tperspective: 1800 * s,\n\t\t\t\t\t\trotateX: (props.tilt as { rotateX: number }).rotateX,\n\t\t\t\t\t\trotateY: (props.tilt as { rotateY: number }).rotateY,\n\t\t\t\t\t\tchildren: kids,\n\t\t\t\t\t}),\n\t\t\t\t]\n\t\t\t: kids;\n\t\treturn {\n\t\t\tchildren: stack,\n\t\t\twidth: W,\n\t\t\theight: H,\n\t\t\tduration: null,\n\t\t\tcues: {} as DeviceFrameCues,\n\t\t\tparts,\n\t\t\tapi: (placement): DeviceFrameApi => {\n\t\t\t\tconst rect = {\n\t\t\t\t\tx: placement.x + screenX,\n\t\t\t\t\ty: placement.y + screenY,\n\t\t\t\t\twidth: screenW,\n\t\t\t\t\theight: screenH,\n\t\t\t\t};\n\t\t\t\tconst api: DeviceFrameApi = {\n\t\t\t\t\tscreenRect: rect,\n\t\t\t\t\tat: (x, y) => [rect.x + x * k, rect.y + y * k],\n\t\t\t\t\tfill: (...nodes) => {\n\t\t\t\t\t\tcontent.children = [...(content.children ?? []), ...nodes];\n\t\t\t\t\t\treturn api;\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t\treturn api;\n\t\t\t},\n\t\t};\n\t},\n});\n\n/** Time, signal, Wi-Fi and battery, drawn from boxes and paths (no icon font). */\nfunction statusBar(\n\tctx: { host: Host; partId(p: string): string },\n\to: {\n\t\twidth: number;\n\t\theight: number;\n\t\ttime: string;\n\t\tcolor: string;\n\t\tcompact: boolean;\n\t\tfont: string;\n\t},\n): LayoutNode {\n\tconst { host } = ctx;\n\tconst mid = o.height / 2 + (o.compact ? 0 : 3);\n\tconst kids: LayoutNode[] = [\n\t\thost.text(o.time, {\n\t\t\tid: ctx.partId(\"status-time\"),\n\t\t\tposition: \"absolute\",\n\t\t\tx: o.compact ? 20 : 48,\n\t\t\ty: mid - 10,\n\t\t\tfontFamily: o.font,\n\t\t\tfontSize: 16,\n\t\t\tfontWeight: 600,\n\t\t\tfill: o.color,\n\t\t}),\n\t];\n\t// From the right edge: battery, Wi-Fi, signal.\n\tconst right = o.width - (o.compact ? 20 : 30);\n\tconst bx = right - 27;\n\tkids.push(\n\t\thost.box({\n\t\t\tid: ctx.partId(\"status-battery\"),\n\t\t\tposition: \"absolute\",\n\t\t\tx: bx,\n\t\t\ty: mid - 6.5,\n\t\t\twidth: 25,\n\t\t\theight: 13,\n\t\t\tborderRadius: 4,\n\t\t\tborderColor: o.color,\n\t\t\tborderWidth: 1.2,\n\t\t\topacity: 0.95,\n\t\t\tchildren: [\n\t\t\t\thost.box({\n\t\t\t\t\tid: ctx.partId(\"status-charge\"),\n\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\tx: 2,\n\t\t\t\t\ty: 2,\n\t\t\t\t\twidth: 18,\n\t\t\t\t\theight: 9,\n\t\t\t\t\tbackground: o.color,\n\t\t\t\t\tborderRadius: 2,\n\t\t\t\t}),\n\t\t\t],\n\t\t}),\n\t\thost.box({\n\t\t\tid: ctx.partId(\"status-nub\"),\n\t\t\tposition: \"absolute\",\n\t\t\tx: bx + 26,\n\t\t\ty: mid - 2.5,\n\t\t\twidth: 1.6,\n\t\t\theight: 5,\n\t\t\tbackground: o.color,\n\t\t\tborderRadius: 1,\n\t\t\topacity: 0.6,\n\t\t}),\n\t);\n\t// Wi-Fi: three arcs and a dot, as one stroked path.\n\tconst wx = bx - 13;\n\tconst wy = mid + 5;\n\t// A quarter arc as one quadratic: the control point sits r·√2 out.\n\tconst arc = (r: number) => {\n\t\tconst c = r * Math.SQRT1_2;\n\t\tconst f = (v: number) => v.toFixed(2);\n\t\treturn `M${f(wx - c)} ${f(wy - c)}Q${f(wx)} ${f(wy - r * Math.SQRT2)} ${f(wx + c)} ${f(wy - c)}`;\n\t};\n\tkids.push(\n\t\thost.shape(\"path\", {\n\t\t\tid: ctx.partId(\"status-wifi\"),\n\t\t\tposition: \"absolute\",\n\t\t\tx: 0,\n\t\t\ty: 0,\n\t\t\twidth: o.width,\n\t\t\theight: o.height,\n\t\t\td: `${arc(11)}${arc(7)}${arc(3)}`,\n\t\t\tfillType: \"none\",\n\t\t\tstrokeColor: o.color,\n\t\t\tstrokeWidth: 2,\n\t\t\tstrokeLineCap: \"round\",\n\t\t}),\n\t);\n\t// Signal: four rising bars.\n\tconst sx = wx - 40;\n\tfor (let i = 0; i < 4; i++)\n\t\tkids.push(\n\t\t\thost.box({\n\t\t\t\tid: ctx.partId(`status-signal-${i}`),\n\t\t\t\tposition: \"absolute\",\n\t\t\t\tx: sx + i * 5,\n\t\t\t\ty: mid + 5 - (4 + i * 2.6),\n\t\t\t\twidth: 3.4,\n\t\t\t\theight: 4 + i * 2.6,\n\t\t\t\tbackground: o.color,\n\t\t\t\tborderRadius: 1,\n\t\t\t}),\n\t\t);\n\treturn host.box({\n\t\tid: ctx.partId(\"status\"),\n\t\tposition: \"absolute\",\n\t\tx: 0,\n\t\ty: 0,\n\t\twidth: o.width,\n\t\theight: o.height,\n\t\tchildren: kids,\n\t});\n}\n\n/**\n * A phone, tablet or laptop around a screen; content is authored in device\n * points and `at(x, y)` maps them for pointers and touches.\n */\nexport const DeviceFrame = /* @__PURE__ */ bindComponent(deviceFrame);\n"
34
+ }
35
+ ]
36
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "focus-zoom",
3
+ "title": "FocusZoom",
4
+ "summary": "A camera over 2D content that pushes in on each burst of pointer clicks (or on given shots) and eases back out, clamped to the content.",
5
+ "tags": [
6
+ "camera",
7
+ "zoom",
8
+ "auto-zoom",
9
+ "screen recording",
10
+ "demo",
11
+ "focus"
12
+ ],
13
+ "example": "FocusZoom({ content: [frame, pointer], follow: pointer })",
14
+ "parts": [
15
+ "clip",
16
+ "camera"
17
+ ],
18
+ "cues": [
19
+ "shots"
20
+ ],
21
+ "registryDependencies": [],
22
+ "files": [
23
+ {
24
+ "path": "focus-zoom/index.ts",
25
+ "content": "/**\n * FocusZoom: a camera over 2D content that pushes in on what matters and\n * eases back out — the auto-zoom of polished product demos. Give it shots,\n * or let it follow a MousePointer and frame each burst of clicks.\n */\nimport { LayerAnimation, type LayoutNode } from \"framefields\";\nimport {\n\tbakeSamples,\n\tbindComponent,\n\tboundsOf,\n\tdefineComponent,\n\tresolveTarget,\n\ttargetPoint,\n\ttargetSchema,\n\ttype Vec2,\n\tz,\n} from \"framefields/components\";\nimport {\n\ttype ClickEvent,\n\ttype ShotWindow,\n\tshotsFromClicks,\n\tsolveCamera,\n} from \"./plan.js\";\n\n/** What `follow` needs from a pointer (a MousePointer node has it). */\nexport interface Followable {\n\tpositionAt(frame: number): Vec2;\n\tcues: { clicks: ReadonlyArray<{ frame: number; point: Vec2 }> };\n}\n\n/** A target, or \"full\" for the whole view. */\nfunction shotTargetSchema() {\n\treturn z.union([z.literal(\"full\"), targetSchema()]);\n}\n\nexport interface FocusZoomCues {\n\t[name: string]: ReadonlyArray<{ frame: number }>;\n\t/** One per shot: push starts (`frame`), lands (`settled`), lets go (`end`). */\n\tshots: ReadonlyArray<{\n\t\tframe: number;\n\t\tsettled: number;\n\t\tend: number;\n\t\tzoom: number;\n\t\tcenter: Vec2;\n\t}>;\n}\n\nexport interface FocusZoomApi {\n\t/** A content point → where it shows on screen (parent coordinates) at a parent frame. */\n\ttoScreen(point: Vec2, frame: number): Vec2;\n\t/** Camera at a parent frame: the content point at the centre, and the zoom. */\n\tcameraAt(frame: number): { center: Vec2; zoom: number };\n}\n\n// Shutter angles. Camera moves scale everything at once, so even a little\n// blur reads strongly: keep the default restrained.\nconst SMEAR = { subtle: 24, standard: 60, heavy: 140 } as const;\n\nexport const focusZoom = /* @__PURE__ */ defineComponent({\n\tname: \"FocusZoom\",\n\tprops: () => ({\n\t\t/** What the camera films, laid out at full size in the view's coordinates. */\n\t\tcontent: z.custom<LayoutNode | LayoutNode[]>(\n\t\t\t(v) => typeof v === \"object\" && v !== null,\n\t\t),\n\t\t/** Manual shots: from `at` until the next shot (or `hold` frames). */\n\t\tshots: z\n\t\t\t.array(\n\t\t\t\tz\n\t\t\t\t\t.object({\n\t\t\t\t\t\tat: z.number().int().min(0),\n\t\t\t\t\t\ttarget: shotTargetSchema(),\n\t\t\t\t\t\tzoom: z.number().min(1).optional(),\n\t\t\t\t\t\thold: z.number().int().positive().optional(),\n\t\t\t\t\t})\n\t\t\t\t\t.strict(),\n\t\t\t)\n\t\t\t.optional(),\n\t\t/** Follow a pointer: push in around each burst of clicks. */\n\t\tfollow: z\n\t\t\t.custom<Followable>(\n\t\t\t\t(v) =>\n\t\t\t\t\ttypeof v === \"object\" &&\n\t\t\t\t\tv !== null &&\n\t\t\t\t\ttypeof (v as Followable).positionAt === \"function\",\n\t\t\t\t{\n\t\t\t\t\tmessage:\n\t\t\t\t\t\t\"Expected a MousePointer (anything with positionAt and cues.clicks)\",\n\t\t\t\t},\n\t\t\t)\n\t\t\t.optional(),\n\t\t/** Zoom for a shot without its own. */\n\t\tzoom: z.number().min(1).default(1.8),\n\t\tmaxZoom: z.number().min(1).default(2.5),\n\t\t/** Frames the push starts before a click (it always lands before the click). Default: about a second. */\n\t\tlead: z.number().int().min(0).optional(),\n\t\t/** Frames without a click before easing back out. */\n\t\tidle: z.number().int().min(0).default(40),\n\t\t/** 0 = quick camera moves, 1 = slow, soft ones. */\n\t\tsmoothing: z.number().min(0).max(1).default(0.5),\n\t\t/** Space kept around click points / targets. */\n\t\tpadding: z.number().min(0).default(160),\n\t\t/** Never show past the content's edges. */\n\t\tclamp: z.boolean().default(true),\n\t\t/** When zoomed, keep the view inside this region (e.g. the app window). */\n\t\tbounds: shotTargetSchema().optional(),\n\t\t/** Motion blur while the camera moves. */\n\t\tsmear: z\n\t\t\t.union([z.literal(false), z.enum([\"subtle\", \"standard\", \"heavy\"])])\n\t\t\t.default(\"subtle\"),\n\t\t/** Frames to solve. Default: through the last shot plus its return. */\n\t\tlength: z.number().int().positive().optional(),\n\t}),\n\tdefaults: { enter: false, exit: false },\n\tbuild(ctx, props) {\n\t\tconst { host, theme, fps } = ctx;\n\t\tconst W = ctx.requested.width ?? 1920 * theme.scale;\n\t\tconst H = ctx.requested.height ?? 1080 * theme.scale;\n\t\t// By default the push starts early enough to run at its natural pace.\n\t\tconst lead =\n\t\t\tprops.lead ?? Math.round((0.55 + 0.75 * props.smoothing) * fps) + 2;\n\n\t\tconst shots: ShotWindow[] = [];\n\t\tif (props.follow) {\n\t\t\tconst pointer = props.follow;\n\t\t\t// Click frames are in the pointer's parent timeline — the content —\n\t\t\t// which starts with this component.\n\t\t\tconst clicks: ClickEvent[] = pointer.cues.clicks.map((c) => ({\n\t\t\t\tframe: c.frame,\n\t\t\t\tpoint: c.point,\n\t\t\t}));\n\t\t\tshots.push(\n\t\t\t\t...shotsFromClicks(clicks, {\n\t\t\t\t\twidth: W,\n\t\t\t\t\theight: H,\n\t\t\t\t\tlead,\n\t\t\t\t\tidle: props.idle,\n\t\t\t\t\tzoom: props.zoom,\n\t\t\t\t\tmaxZoom: props.maxZoom,\n\t\t\t\t\tpadding: props.padding,\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\tconst manual = [...(props.shots ?? [])].sort((a, b) => a.at - b.at);\n\t\tmanual.forEach((shot, i) => {\n\t\t\tconst next = manual[i + 1];\n\t\t\tconst end =\n\t\t\t\tshot.hold !== undefined\n\t\t\t\t\t? shot.at + shot.hold\n\t\t\t\t\t: (next?.at ?? shot.at + Math.round(fps * 3));\n\t\t\tif (shot.target === \"full\") {\n\t\t\t\tshots.push({ start: shot.at, end, zoom: 1, center: [W / 2, H / 2] });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst resolved = resolveTarget(shot.target, {\n\t\t\t\tmeasure: host.measure,\n\t\t\t\tfps,\n\t\t\t});\n\t\t\tif (resolved.kind === \"dynamic\") {\n\t\t\t\tshots.push({\n\t\t\t\t\tstart: shot.at,\n\t\t\t\t\tend,\n\t\t\t\t\tzoom: shot.zoom ?? props.zoom,\n\t\t\t\t\tcenter: (f) => targetPoint(resolved, f),\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst r = boundsOf(resolved.rects);\n\t\t\tconst fit = Math.min(\n\t\t\t\tW / (r.width + props.padding * 2),\n\t\t\t\tH / (r.height + props.padding * 2),\n\t\t\t);\n\t\t\tconst zoom = Math.min(\n\t\t\t\tprops.maxZoom,\n\t\t\t\tshot.zoom ?? Math.max(1, Math.min(props.zoom, fit)),\n\t\t\t);\n\t\t\tshots.push({\n\t\t\t\tstart: shot.at,\n\t\t\t\tend,\n\t\t\t\tzoom,\n\t\t\t\tcenter: [r.x + r.width / 2, r.y + r.height / 2],\n\t\t\t});\n\t\t});\n\t\tshots.sort((a, b) => a.start - b.start);\n\n\t\tconst lastEnd = Math.max(0, ...shots.map((s) => s.end));\n\t\tconst length = props.length ?? lastEnd + Math.round(fps * 1.5) + 1;\n\t\tlet bounds:\n\t\t\t| { x: number; y: number; width: number; height: number }\n\t\t\t| undefined;\n\t\tif (props.bounds !== undefined && props.bounds !== \"full\") {\n\t\t\tconst resolved = resolveTarget(props.bounds, {\n\t\t\t\tmeasure: host.measure,\n\t\t\t\tfps,\n\t\t\t});\n\t\t\tif (resolved.kind !== \"static\")\n\t\t\t\tthrow new Error(\"FocusZoom: `bounds` must be a static target.\");\n\t\t\tbounds = boundsOf(resolved.rects);\n\t\t}\n\t\tconst plan = solveCamera({\n\t\t\twidth: W,\n\t\t\theight: H,\n\t\t\tlength,\n\t\t\tfps,\n\t\t\tshots,\n\t\t\tsmoothing: props.smoothing,\n\t\t\tclamp: props.clamp,\n\t\t\tbounds,\n\t\t});\n\n\t\tconst camera = host.box({\n\t\t\tid: ctx.partId(\"camera\"),\n\t\t\tposition: \"absolute\",\n\t\t\tx: 0,\n\t\t\ty: 0,\n\t\t\twidth: W,\n\t\t\theight: H,\n\t\t\tanchorX: 0,\n\t\t\tanchorY: 0,\n\t\t\t...(props.smear !== false && { motionBlurShutter: SMEAR[props.smear] }),\n\t\t\tchildren: Array.isArray(props.content) ? props.content : [props.content],\n\t\t});\n\t\tcamera.animate(\n\t\t\tLayerAnimation.keys(\"x\", bakeSamples(plan.x, 0, { tolerance: 0.15 })),\n\t\t\tLayerAnimation.keys(\"y\", bakeSamples(plan.y, 0, { tolerance: 0.15 })),\n\t\t\tLayerAnimation.keys(\n\t\t\t\t\"scale\",\n\t\t\t\tbakeSamples(plan.scale, 0, { tolerance: 0.0004 }),\n\t\t\t),\n\t\t);\n\t\tconst clip = host.box({\n\t\t\tid: ctx.partId(\"clip\"),\n\t\t\tposition: \"absolute\",\n\t\t\tx: 0,\n\t\t\ty: 0,\n\t\t\twidth: W,\n\t\t\theight: H,\n\t\t\toverflow: \"hidden\",\n\t\t\tchildren: [camera],\n\t\t});\n\n\t\tconst stateAt = (local: number) =>\n\t\t\tplan.states[\n\t\t\t\tMath.max(0, Math.min(plan.states.length - 1, Math.round(local)))\n\t\t\t] ?? { cx: W / 2, cy: H / 2, zoom: 1 };\n\t\treturn {\n\t\t\tchildren: [clip],\n\t\t\twidth: W,\n\t\t\theight: H,\n\t\t\tduration: null,\n\t\t\torigin: [0, 0],\n\t\t\tcues: { shots: plan.shots } as FocusZoomCues,\n\t\t\tparts: { camera, clip },\n\t\t\tapi: (placement): FocusZoomApi => ({\n\t\t\t\ttoScreen(point, frame) {\n\t\t\t\t\tconst s = stateAt(frame - placement.start);\n\t\t\t\t\treturn [\n\t\t\t\t\t\tplacement.x + W / 2 + (point[0] - s.cx) * s.zoom,\n\t\t\t\t\t\tplacement.y + H / 2 + (point[1] - s.cy) * s.zoom,\n\t\t\t\t\t];\n\t\t\t\t},\n\t\t\t\tcameraAt(frame) {\n\t\t\t\t\tconst s = stateAt(frame - placement.start);\n\t\t\t\t\treturn { center: [s.cx, s.cy], zoom: s.zoom };\n\t\t\t\t},\n\t\t\t}),\n\t\t};\n\t},\n});\n\n/**\n * A camera over 2D content that pushes in on each burst of pointer clicks (or\n * on given shots) and eases back out, clamped to the content.\n */\nexport const FocusZoom = /* @__PURE__ */ bindComponent(focusZoom);\n"
26
+ },
27
+ {
28
+ "path": "focus-zoom/plan.ts",
29
+ "content": "/**\n * The FocusZoom camera solver: which framing each frame wants (shots, or\n * clusters of pointer clicks), and how the camera moves between framings.\n *\n * Moves interpolate the view rectangle — all four edges on one eased curve —\n * between framings that are already clamped. That keeps every content point\n * moving one way on screen (no sideways wiggle while zooming), and a convex\n * blend of two in-bounds views never shows past the bounds, so nothing is\n * clamped mid-move. Smoothing pan and zoom separately and clamping the result\n * does neither.\n */\nimport type { Vec2 } from \"framefields/components\";\n\nexport interface CameraState {\n\t/** Content point at the centre of the view. */\n\tcx: number;\n\tcy: number;\n\tzoom: number;\n}\n\nexport interface ShotWindow {\n\tstart: number;\n\tend: number;\n\tcenter: Vec2 | ((frame: number) => Vec2);\n\tzoom: number;\n\t/** The move must have landed by this frame (e.g. the first click). */\n\tsettleBy?: number;\n}\n\nexport interface CameraInput {\n\twidth: number;\n\theight: number;\n\tlength: number;\n\tfps: number;\n\tshots: ShotWindow[];\n\t/** 0 = quick moves, 1 = slow, soft moves. */\n\tsmoothing: number;\n\tclamp: boolean;\n\t/** The region the view must stay inside when zoomed (default: the whole view). */\n\tbounds?: { x: number; y: number; width: number; height: number };\n}\n\nexport interface CameraPlan {\n\tstates: CameraState[];\n\t/** Wrapper transform per frame (anchor at its top-left). */\n\tx: number[];\n\ty: number[];\n\tscale: number[];\n\tshots: Array<{\n\t\tframe: number;\n\t\tsettled: number;\n\t\tend: number;\n\t\tzoom: number;\n\t\tcenter: Vec2;\n\t}>;\n}\n\n/** The visible content rectangle: left, top, right, bottom. */\ninterface View {\n\tx0: number;\n\ty0: number;\n\tx1: number;\n\ty1: number;\n}\n\n/** Minimum-jerk ease: zero velocity and acceleration at both ends. */\nconst ease = (t: number) => {\n\tconst s = Math.min(1, Math.max(0, t));\n\treturn s * s * s * (10 - 15 * s + 6 * s * s);\n};\n\nexport function solveCamera(input: CameraInput): CameraPlan {\n\tconst { width: W, height: H, fps } = input;\n\tconst smoothing = Math.min(1, Math.max(0, input.smoothing));\n\n\tconst toView = (s: CameraState): View => {\n\t\tconst hw = W / (2 * s.zoom);\n\t\tconst hh = H / (2 * s.zoom);\n\t\treturn { x0: s.cx - hw, y0: s.cy - hh, x1: s.cx + hw, y1: s.cy + hh };\n\t};\n\tconst toState = (v: View): CameraState => ({\n\t\tcx: (v.x0 + v.x1) / 2,\n\t\tcy: (v.y0 + v.y1) / 2,\n\t\tzoom: W / (v.x1 - v.x0),\n\t});\n\tconst lerpView = (a: View, b: View, t: number): View => ({\n\t\tx0: a.x0 + (b.x0 - a.x0) * t,\n\t\ty0: a.y0 + (b.y0 - a.y0) * t,\n\t\tx1: a.x1 + (b.x1 - a.x1) * t,\n\t\ty1: a.y1 + (b.y1 - a.y1) * t,\n\t});\n\n\t/** Clamp a framing (never a frame of motion): inside the view, then the bounds. */\n\tconst clampState = (s: CameraState): CameraState => {\n\t\tif (!input.clamp) return s;\n\t\tconst zoom = Math.max(1, s.zoom);\n\t\tconst hw = W / (2 * zoom);\n\t\tconst hh = H / (2 * zoom);\n\t\tconst fit = (c: number, lo: number, hi: number, half: number) =>\n\t\t\thi - lo >= 2 * half\n\t\t\t\t? Math.min(hi - half, Math.max(lo + half, c))\n\t\t\t\t: (lo + hi) / 2;\n\t\tlet cx = fit(s.cx, 0, W, hw);\n\t\tlet cy = fit(s.cy, 0, H, hh);\n\t\tconst b = input.bounds;\n\t\tif (b && zoom > 1) {\n\t\t\tcx = fit(cx, b.x, b.x + b.width, hw);\n\t\t\tcy = fit(cy, b.y, b.y + b.height, hh);\n\t\t\t// Bounds never push the view past the content itself.\n\t\t\tcx = fit(cx, 0, W, hw);\n\t\t\tcy = fit(cy, 0, H, hh);\n\t\t}\n\t\treturn { cx, cy, zoom };\n\t};\n\n\tconst full: CameraState = { cx: W / 2, cy: H / 2, zoom: 1 };\n\tconst shotAt = (frame: number): number => {\n\t\tlet pick = -1;\n\t\tinput.shots.forEach((s, i) => {\n\t\t\tif (frame >= s.start && frame < s.end) pick = i;\n\t\t});\n\t\treturn pick;\n\t};\n\tconst framing = (index: number, frame: number): CameraState => {\n\t\tconst s = input.shots[index];\n\t\tif (!s) return full;\n\t\tconst c = typeof s.center === \"function\" ? s.center(frame) : s.center;\n\t\treturn clampState({ cx: c[0], cy: c[1], zoom: s.zoom });\n\t};\n\n\t// Move length: a base set by `smoothing`, longer for bigger zoom changes.\n\tconst moveFrames = (from: View, to: View) => {\n\t\tconst ratio = Math.abs(Math.log((to.x1 - to.x0) / (from.x1 - from.x0)));\n\t\tconst base = 0.55 + 0.75 * smoothing;\n\t\tconst seconds = base * (0.75 + 0.25 * Math.min(1, ratio / Math.LN2));\n\t\treturn Math.max(2, Math.round(seconds * fps));\n\t};\n\t// Holding on a moving target: follow it softly (exponential, so it never\n\t// overshoots and never reverses).\n\tconst follow = 1 - Math.exp(-(8 - 5 * smoothing) / fps);\n\n\tlet current = toView(framing(shotAt(0), 0));\n\tlet active = shotAt(0);\n\tlet move: { from: View; start: number; frames: number } | null = null;\n\tconst settledAt = new Map<number, number>();\n\tconst states: CameraState[] = [];\n\n\tfor (let f = 0; f < input.length; f++) {\n\t\tconst index = shotAt(f);\n\t\tconst target = toView(framing(index, f));\n\t\tif (index !== active) {\n\t\t\tconst settleBy = input.shots[index]?.settleBy;\n\t\t\tconst natural = moveFrames(current, target);\n\t\t\tconst frames =\n\t\t\t\tsettleBy === undefined\n\t\t\t\t\t? natural\n\t\t\t\t\t: Math.max(2, Math.min(natural, settleBy - f));\n\t\t\tmove = { from: current, start: f, frames };\n\t\t\tactive = index;\n\t\t}\n\t\tif (move) {\n\t\t\tconst t = (f - move.start) / move.frames;\n\t\t\tcurrent = lerpView(move.from, target, ease(t));\n\t\t\tif (t >= 1) {\n\t\t\t\tmove = null;\n\t\t\t\tif (index >= 0 && !settledAt.has(index)) settledAt.set(index, f);\n\t\t\t}\n\t\t} else {\n\t\t\tcurrent = lerpView(current, target, follow);\n\t\t\tif (index >= 0 && !settledAt.has(index)) settledAt.set(index, f);\n\t\t}\n\t\tstates.push(toState(current));\n\t}\n\n\tconst shots = input.shots.map((s, i) => {\n\t\tconst c = typeof s.center === \"function\" ? s.center(s.start) : s.center;\n\t\treturn {\n\t\t\tframe: s.start,\n\t\t\tsettled: settledAt.get(i) ?? s.end,\n\t\t\tend: s.end,\n\t\t\tzoom: s.zoom,\n\t\t\tcenter: c,\n\t\t};\n\t});\n\n\treturn {\n\t\tstates,\n\t\tx: states.map((s) => W / 2 - s.zoom * s.cx),\n\t\ty: states.map((s) => H / 2 - s.zoom * s.cy),\n\t\tscale: states.map((s) => s.zoom),\n\t\tshots,\n\t};\n}\n\nexport interface ClickEvent {\n\tframe: number;\n\tpoint: Vec2;\n}\n\n/**\n * Shots from pointer clicks: nearby clicks close in time share one push-in\n * that starts `lead` frames before the first and lets go `idle` frames after\n * the last, framed on the cluster. The camera doesn't chase the pointer: a\n * still frame reads calmer and keeps the click where the eye already is.\n */\nexport function shotsFromClicks(\n\tclicks: ClickEvent[],\n\to: {\n\t\twidth: number;\n\t\theight: number;\n\t\tlead: number;\n\t\tidle: number;\n\t\tzoom: number;\n\t\tmaxZoom: number;\n\t\tpadding: number;\n\t},\n): ShotWindow[] {\n\tconst sorted = [...clicks].sort((a, b) => a.frame - b.frame);\n\tconst clusters: ClickEvent[][] = [];\n\tfor (const c of sorted) {\n\t\tconst current = clusters[clusters.length - 1];\n\t\tconst last = current?.[current.length - 1];\n\t\tconst near =\n\t\t\tlast &&\n\t\t\tc.frame - last.frame <= o.idle + o.lead &&\n\t\t\tMath.abs(c.point[0] - last.point[0]) < (o.width / o.zoom) * 0.8 &&\n\t\t\tMath.abs(c.point[1] - last.point[1]) < (o.height / o.zoom) * 0.8;\n\t\tif (current && near) current.push(c);\n\t\telse clusters.push([c]);\n\t}\n\treturn clusters.map((cluster) => {\n\t\tconst xs = cluster.map((c) => c.point[0]);\n\t\tconst ys = cluster.map((c) => c.point[1]);\n\t\tconst x0 = Math.min(...xs) - o.padding;\n\t\tconst x1 = Math.max(...xs) + o.padding;\n\t\tconst y0 = Math.min(...ys) - o.padding;\n\t\tconst y1 = Math.max(...ys) + o.padding;\n\t\tconst fit = Math.min(\n\t\t\to.width / Math.max(1, x1 - x0),\n\t\t\to.height / Math.max(1, y1 - y0),\n\t\t);\n\t\tconst first = cluster[0]?.frame ?? 0;\n\t\tconst last = cluster[cluster.length - 1]?.frame ?? first;\n\t\treturn {\n\t\t\tstart: Math.max(0, first - o.lead),\n\t\t\tend: last + o.idle,\n\t\t\tsettleBy: first - 2,\n\t\t\tzoom: Math.max(1, Math.min(o.zoom, o.maxZoom, fit)),\n\t\t\tcenter: [(x0 + x1) / 2, (y0 + y1) / 2] as Vec2,\n\t\t};\n\t});\n}\n"
30
+ }
31
+ ]
32
+ }