framefields 2.6.2 → 2.7.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 (463) hide show
  1. package/dist/audio/index.d.mts +4 -4
  2. package/dist/audio/index.mjs +10 -10
  3. package/dist/{audio-BHuVoNc-.mjs → audio-j3ttoRDX.mjs} +4 -4
  4. package/dist/{audio-BHuVoNc-.mjs.map → audio-j3ttoRDX.mjs.map} +1 -1
  5. package/dist/{audio-WcPMxtcz.d.mts → audio-tL8CcPra.d.mts} +3 -3
  6. package/dist/{audio-WcPMxtcz.d.mts.map → audio-tL8CcPra.d.mts.map} +1 -1
  7. package/dist/components/index.d.mts +10 -10
  8. package/dist/components/index.mjs +44 -44
  9. package/dist/{dist-Wsjx_Rza.mjs → dist-BHTd-EC1.mjs} +92 -19
  10. package/dist/{dist-Wsjx_Rza.mjs.map → dist-BHTd-EC1.mjs.map} +1 -1
  11. package/dist/{dist-CSU_2yUb.mjs → dist-BM0ilHvQ.mjs} +115 -21
  12. package/dist/dist-BM0ilHvQ.mjs.map +1 -0
  13. package/dist/{dist-CL5JOMSp.mjs → dist-BRXsfWKQ.mjs} +424 -8
  14. package/dist/dist-BRXsfWKQ.mjs.map +1 -0
  15. package/dist/{dist-PlaSQJ43.mjs → dist-CPtFZxtu.mjs} +2 -2
  16. package/dist/{dist-PlaSQJ43.mjs.map → dist-CPtFZxtu.mjs.map} +1 -1
  17. package/dist/effects/index.d.mts +4 -4
  18. package/dist/effects/index.mjs +35 -35
  19. package/dist/{effects-BvloJaPQ.mjs → effects-Dy661XS_.mjs} +3 -3
  20. package/dist/{effects-BvloJaPQ.mjs.map → effects-Dy661XS_.mjs.map} +1 -1
  21. package/dist/fonts/index.mjs +2 -2
  22. package/dist/{fonts-DpnBDAqM.mjs → fonts-BcFhAqqO.mjs} +2 -2
  23. package/dist/{fonts-DpnBDAqM.mjs.map → fonts-BcFhAqqO.mjs.map} +1 -1
  24. package/dist/{gpu-BvYDrqOC.mjs → gpu-zIhSz_0X.mjs} +2 -2
  25. package/dist/{gpu-BvYDrqOC.mjs.map → gpu-zIhSz_0X.mjs.map} +1 -1
  26. package/dist/{index-kj5uSzaI.d.mts → index-B1ENegOQ.d.mts} +204 -2
  27. package/dist/index-B1ENegOQ.d.mts.map +1 -0
  28. package/dist/{index-DsGBouQM.d.mts → index-CJRTBLZ4.d.mts} +4 -4
  29. package/dist/{index-DsGBouQM.d.mts.map → index-CJRTBLZ4.d.mts.map} +1 -1
  30. package/dist/{index-DcIR_DgU.d.mts → index-Cg30slle.d.mts} +2 -2
  31. package/dist/{index-DcIR_DgU.d.mts.map → index-Cg30slle.d.mts.map} +1 -1
  32. package/dist/{index-wUhS1_4W.d.mts → index-DeeMmoEA.d.mts} +2 -2
  33. package/dist/{index-wUhS1_4W.d.mts.map → index-DeeMmoEA.d.mts.map} +1 -1
  34. package/dist/{index-C541Ol1N.d.mts → index-ePGQ_C7f.d.mts} +39 -16
  35. package/dist/index-ePGQ_C7f.d.mts.map +1 -0
  36. package/dist/{index-C6ihzCpZ.d.mts → index-l1yRugLc.d.mts} +3 -3
  37. package/dist/{index-C6ihzCpZ.d.mts.map → index-l1yRugLc.d.mts.map} +1 -1
  38. package/dist/{index-BCzXnNpb.d.mts → index-uDZ9Yuv-.d.mts} +5 -8
  39. package/dist/index-uDZ9Yuv-.d.mts.map +1 -0
  40. package/dist/index.d.mts +11 -11
  41. package/dist/index.mjs +53 -53
  42. package/dist/preview/index.d.mts +3 -3
  43. package/dist/preview/index.mjs +10 -10
  44. package/dist/{preview-UMG4Colu.mjs → preview-IpdVFuxv.mjs} +3 -2
  45. package/dist/{preview-UMG4Colu.mjs.map → preview-IpdVFuxv.mjs.map} +1 -1
  46. package/dist/preview-engine/audio.mjs +1 -1
  47. package/dist/preview-engine/{chunk-RF62TV3G.mjs → chunk-25WRUINA.mjs} +1 -1
  48. package/dist/preview-engine/{chunk-MMVFX42U.mjs → chunk-2AEWPT6Y.mjs} +1 -1
  49. package/dist/preview-engine/{chunk-IIOHF7MH.mjs → chunk-2MDTUA3W.mjs} +1 -1
  50. package/dist/preview-engine/{chunk-KWTTANYN.mjs → chunk-2TLABBER.mjs} +1 -1
  51. package/dist/preview-engine/{chunk-MUOJMF7M.mjs → chunk-3QOJVABU.mjs} +1 -1
  52. package/dist/preview-engine/{chunk-RDEQ4LQU.mjs → chunk-4Q3ZUTQC.mjs} +1 -1
  53. package/dist/preview-engine/{chunk-OXXRKNHW.mjs → chunk-5BXCJ2QX.mjs} +1 -1
  54. package/dist/preview-engine/{chunk-JTT3YX7W.mjs → chunk-5R4UF7YK.mjs} +1 -1
  55. package/dist/preview-engine/{chunk-SVE3XWO7.mjs → chunk-6VDYWMP5.mjs} +1 -1
  56. package/dist/preview-engine/{chunk-ULPLHKM2.mjs → chunk-7B7QQDPZ.mjs} +1 -1
  57. package/dist/preview-engine/{chunk-ULVX4VYT.mjs → chunk-7ZA36EZE.mjs} +1 -1
  58. package/dist/preview-engine/{chunk-LYSU6S4O.mjs → chunk-AJ6BVUDB.mjs} +1 -1
  59. package/dist/preview-engine/{chunk-J24VHZS6.mjs → chunk-BDXGUSSF.mjs} +1 -1
  60. package/dist/preview-engine/{chunk-B7J72YSP.mjs → chunk-CC5V7UBF.mjs} +1 -1
  61. package/dist/preview-engine/{chunk-JD4TBC6Z.mjs → chunk-CP42LYD7.mjs} +1 -1
  62. package/dist/preview-engine/{chunk-LN3YAWUD.mjs → chunk-DJWDD3OE.mjs} +1 -1
  63. package/dist/preview-engine/{chunk-V3FODTPI.mjs → chunk-DZEWY4PR.mjs} +22 -22
  64. package/dist/preview-engine/{chunk-RKNV5N7D.mjs → chunk-ENGQ55LR.mjs} +1 -1
  65. package/dist/preview-engine/{chunk-NIILFJKE.mjs → chunk-FBXKVLTB.mjs} +14 -14
  66. package/dist/preview-engine/{chunk-MJCK5FLZ.mjs → chunk-FMEXTHL7.mjs} +1 -1
  67. package/dist/preview-engine/{chunk-UEQIO6AH.mjs → chunk-FQ7UCMWR.mjs} +1 -1
  68. package/dist/preview-engine/{chunk-TPFDAOW7.mjs → chunk-FRIBHQTM.mjs} +1 -1
  69. package/dist/preview-engine/chunk-FT7HJ6IE.mjs +3 -0
  70. package/dist/preview-engine/{chunk-YEPYVASB.mjs → chunk-G3UOUGIE.mjs} +1 -1
  71. package/dist/preview-engine/{chunk-4JX5LBE2.mjs → chunk-H4GDIVX4.mjs} +1 -1
  72. package/dist/preview-engine/{chunk-DDFWN7DI.mjs → chunk-I7QIQURU.mjs} +1 -1
  73. package/dist/preview-engine/{chunk-3KHD67Q3.mjs → chunk-ID344UBN.mjs} +1 -1
  74. package/dist/preview-engine/{chunk-5NT7WWJR.mjs → chunk-IGCXUGHT.mjs} +1 -1
  75. package/dist/preview-engine/{chunk-QPBTLJ5W.mjs → chunk-IK3YA6MC.mjs} +1 -1
  76. package/dist/preview-engine/chunk-J5SKQFJ6.mjs +1 -0
  77. package/dist/preview-engine/{chunk-Q7JN4EHX.mjs → chunk-JOPZNSZJ.mjs} +1 -1
  78. package/dist/preview-engine/{chunk-SQ52PBF3.mjs → chunk-JOSNIMFT.mjs} +1 -1
  79. package/dist/preview-engine/{chunk-SXNCNG4L.mjs → chunk-JR443L5E.mjs} +1 -1
  80. package/dist/preview-engine/{chunk-FZLKPBKS.mjs → chunk-KCAFMWDM.mjs} +1 -1
  81. package/dist/preview-engine/{chunk-QRYCB6E3.mjs → chunk-LAEPNVOE.mjs} +1 -1
  82. package/dist/preview-engine/chunk-MAALUQK3.mjs +1974 -0
  83. package/dist/preview-engine/chunk-MHCXSWK7.mjs +1 -0
  84. package/dist/preview-engine/{chunk-4INGTMZ5.mjs → chunk-MWBARRK2.mjs} +1 -1
  85. package/dist/preview-engine/{chunk-GQIRV466.mjs → chunk-MXPN2GB7.mjs} +1 -1
  86. package/dist/preview-engine/{chunk-W2EZFV4Q.mjs → chunk-NHYB5YC3.mjs} +1 -1
  87. package/dist/preview-engine/{chunk-22BLMBWX.mjs → chunk-PXSKIGAP.mjs} +6 -6
  88. package/dist/preview-engine/{chunk-SP22JWN2.mjs → chunk-PYM2FBFA.mjs} +1 -1
  89. package/dist/preview-engine/chunk-QDTJ4MOJ.mjs +1 -0
  90. package/dist/preview-engine/{chunk-6PMATELR.mjs → chunk-RQU2GFGH.mjs} +1 -1
  91. package/dist/preview-engine/{chunk-UQD2UE6I.mjs → chunk-RWDPBES3.mjs} +1 -1
  92. package/dist/preview-engine/{chunk-QVW5METG.mjs → chunk-S5ONWSZF.mjs} +1 -1
  93. package/dist/preview-engine/{chunk-2YGR5YCN.mjs → chunk-SIU6JU2R.mjs} +1 -1
  94. package/dist/preview-engine/{chunk-NQZRPPUU.mjs → chunk-SJ3Q4AQP.mjs} +1 -1
  95. package/dist/preview-engine/chunk-TEBZRQPG.mjs +1 -0
  96. package/dist/preview-engine/{chunk-WDWHOXSA.mjs → chunk-UUDML2SN.mjs} +1 -1
  97. package/dist/preview-engine/{chunk-RHXZXWJ4.mjs → chunk-VAS4IJZR.mjs} +1 -1
  98. package/dist/preview-engine/{chunk-DOCFOSA6.mjs → chunk-WTDK6H46.mjs} +1 -1
  99. package/dist/preview-engine/chunk-XNMEVVQB.mjs +1 -0
  100. package/dist/preview-engine/{chunk-AKT4YQWL.mjs → chunk-YVGT4NK6.mjs} +2 -2
  101. package/dist/preview-engine/chunk-ZYTXBBIX.mjs +1 -0
  102. package/dist/preview-engine/components.mjs +1 -1
  103. package/dist/preview-engine/effects.mjs +1 -1
  104. package/dist/preview-engine/framefields.mjs +1 -1
  105. package/dist/preview-engine/player.mjs +2 -2
  106. package/dist/preview-engine/{renderers-FC7G7KU7.mjs → renderers-2AKYMAIK.mjs} +1 -1
  107. package/dist/preview-engine/renderers-363CKEIG.mjs +1 -0
  108. package/dist/preview-engine/{renderers-EJGF3PYQ.mjs → renderers-36L24ZVS.mjs} +1 -1
  109. package/dist/preview-engine/renderers-3FMMVDMA.mjs +1 -0
  110. package/dist/preview-engine/{renderers-56TGEFDK.mjs → renderers-44AANAXR.mjs} +1 -1
  111. package/dist/preview-engine/renderers-4UUU62VV.mjs +1 -0
  112. package/dist/preview-engine/renderers-4ZTZFSAR.mjs +1 -0
  113. package/dist/preview-engine/{renderers-PYDOSDEY.mjs → renderers-5ZSB75GY.mjs} +1 -1
  114. package/dist/preview-engine/renderers-6KDD75BM.mjs +1 -0
  115. package/dist/preview-engine/renderers-7BJRD4G2.mjs +1 -0
  116. package/dist/preview-engine/{renderers-HBIBRZD7.mjs → renderers-AIIHGDJM.mjs} +1 -1
  117. package/dist/preview-engine/{renderers-P2FTCDZ7.mjs → renderers-AU746ORP.mjs} +1 -1
  118. package/dist/preview-engine/renderers-BIF2BETK.mjs +1 -0
  119. package/dist/preview-engine/{renderers-FD3ZT73R.mjs → renderers-BTQCHVHV.mjs} +1 -1
  120. package/dist/preview-engine/{renderers-HMNCIEU3.mjs → renderers-CFHBOAZG.mjs} +1 -1
  121. package/dist/preview-engine/renderers-CU5RW5UA.mjs +1 -0
  122. package/dist/preview-engine/renderers-DK5OBTKE.mjs +1 -0
  123. package/dist/preview-engine/renderers-EAKCDGTY.mjs +1 -0
  124. package/dist/preview-engine/{renderers-FYI6QNUH.mjs → renderers-EGBGC7MU.mjs} +1 -1
  125. package/dist/preview-engine/renderers-EVXCU7G7.mjs +1 -0
  126. package/dist/preview-engine/renderers-EXEYEH3F.mjs +1 -0
  127. package/dist/preview-engine/{renderers-RDHHVXRE.mjs → renderers-EXJDPS3T.mjs} +1 -1
  128. package/dist/preview-engine/renderers-F3WALPWI.mjs +1 -0
  129. package/dist/preview-engine/{renderers-4RVSLTCX.mjs → renderers-FS3EOB4J.mjs} +1 -1
  130. package/dist/preview-engine/{renderers-C3YXNKNM.mjs → renderers-GUL2MXDD.mjs} +1 -1
  131. package/dist/preview-engine/{renderers-EQDINUMT.mjs → renderers-H4HC44B5.mjs} +1 -1
  132. package/dist/preview-engine/renderers-H6565HO4.mjs +1 -0
  133. package/dist/preview-engine/renderers-HT5V6OZ2.mjs +1 -0
  134. package/dist/preview-engine/renderers-HXMOARWO.mjs +1 -0
  135. package/dist/preview-engine/renderers-HZ2Q7BGK.mjs +1 -0
  136. package/dist/preview-engine/renderers-I4N2K2KW.mjs +1 -0
  137. package/dist/preview-engine/renderers-I5J7U6CB.mjs +1 -0
  138. package/dist/preview-engine/renderers-IZV6QXR5.mjs +1 -0
  139. package/dist/preview-engine/renderers-JWGAPHWX.mjs +1 -0
  140. package/dist/preview-engine/renderers-KNDSVQTY.mjs +1 -0
  141. package/dist/preview-engine/renderers-KNDUOO6M.mjs +1 -0
  142. package/dist/preview-engine/renderers-KQAO3QEI.mjs +1 -0
  143. package/dist/preview-engine/{renderers-7SW2AHM2.mjs → renderers-LCSQLXRD.mjs} +1 -1
  144. package/dist/preview-engine/renderers-M34LOEHC.mjs +1 -0
  145. package/dist/preview-engine/renderers-ML6GNCWB.mjs +1 -0
  146. package/dist/preview-engine/renderers-NARG274F.mjs +1 -0
  147. package/dist/preview-engine/renderers-NCFPOSWR.mjs +1 -0
  148. package/dist/preview-engine/{renderers-ZAMSS7QN.mjs → renderers-PCOTECQB.mjs} +1 -1
  149. package/dist/preview-engine/renderers-PG5JHI2S.mjs +1 -0
  150. package/dist/preview-engine/{renderers-LTV5RFDJ.mjs → renderers-Q7MNLQJF.mjs} +1 -1
  151. package/dist/preview-engine/{renderers-GHP7SL5M.mjs → renderers-QXVSJYZL.mjs} +1 -1
  152. package/dist/preview-engine/{renderers-H2UI7YEO.mjs → renderers-R3ERZ3ZY.mjs} +1 -1
  153. package/dist/preview-engine/renderers-RXQKV65L.mjs +1 -0
  154. package/dist/preview-engine/renderers-SA73GD2B.mjs +1 -0
  155. package/dist/preview-engine/renderers-SLIXUHNX.mjs +1 -0
  156. package/dist/preview-engine/renderers-T462JZNA.mjs +1 -0
  157. package/dist/preview-engine/{renderers-LV6LLULR.mjs → renderers-TH2XZAYW.mjs} +1 -1
  158. package/dist/preview-engine/{renderers-ART3EGSZ.mjs → renderers-UHZIERVH.mjs} +1 -1
  159. package/dist/preview-engine/renderers-V7CFVGYK.mjs +1 -0
  160. package/dist/preview-engine/renderers-VY5E7BPK.mjs +1 -0
  161. package/dist/preview-engine/{renderers-ZDMDDWLN.mjs → renderers-WKXGELRG.mjs} +1 -1
  162. package/dist/preview-engine/{renderers-GY4R467B.mjs → renderers-WLDIIFR5.mjs} +1 -1
  163. package/dist/preview-engine/{renderers-NTKSN32T.mjs → renderers-WM6WBRH6.mjs} +1 -1
  164. package/dist/preview-engine/renderers-WT5FQZM7.mjs +228 -0
  165. package/dist/preview-engine/{renderers-YK74777T.mjs → renderers-WTKRLNNR.mjs} +1 -1
  166. package/dist/preview-engine/{renderers-BWRNCXR6.mjs → renderers-X6GKEVP7.mjs} +1 -1
  167. package/dist/preview-engine/renderers-Y47N5EYR.mjs +1 -0
  168. package/dist/preview-engine/renderers-Y4CUVZYI.mjs +1 -0
  169. package/dist/preview-engine/renderers-Y6YC7ZAK.mjs +1 -0
  170. package/dist/preview-engine/renderers-Y6ZYUAQG.mjs +1 -0
  171. package/dist/preview-engine/{renderers-YBTWN76S.mjs → renderers-YRWXYT5A.mjs} +1 -1
  172. package/dist/preview-engine/{renderers-SY435YU3.mjs → renderers-YWC4MXPG.mjs} +1 -1
  173. package/dist/preview-engine/{renderers-7GMW52G3.mjs → renderers-ZWNPYZN6.mjs} +1 -1
  174. package/dist/preview-engine/signals.mjs +1 -1
  175. package/dist/{program-B6YvXPhj.mjs → program-KPBfq9tw.mjs} +84 -15
  176. package/dist/program-KPBfq9tw.mjs.map +1 -0
  177. package/dist/registry/index.json +1 -1
  178. package/dist/registry/lower-third.json +1 -1
  179. package/dist/registry/text-reveal.json +2 -2
  180. package/dist/registry/title-card.json +1 -1
  181. package/dist/renderer/index.d.mts +2 -2
  182. package/dist/renderer/index.mjs +2 -2
  183. package/dist/{renderer-DFGSy_ay.mjs → renderer-1-FLx8kd.mjs} +3 -3
  184. package/dist/{renderer-DFGSy_ay.mjs.map → renderer-1-FLx8kd.mjs.map} +1 -1
  185. package/dist/{renderer-Ck74BZ_L.mjs → renderer-47d3etrX.mjs} +4 -4
  186. package/dist/{renderer-Ck74BZ_L.mjs.map → renderer-47d3etrX.mjs.map} +1 -1
  187. package/dist/{renderer-Cs8Od5S8.mjs → renderer-6GCG6JxD.mjs} +3 -3
  188. package/dist/{renderer-Cs8Od5S8.mjs.map → renderer-6GCG6JxD.mjs.map} +1 -1
  189. package/dist/{renderer-BLn9MrDC.mjs → renderer-8BIDdgjt.mjs} +3 -3
  190. package/dist/{renderer-BLn9MrDC.mjs.map → renderer-8BIDdgjt.mjs.map} +1 -1
  191. package/dist/renderer-B-5TlncB.mjs +5 -0
  192. package/dist/{renderer-CkbIz6M-.mjs → renderer-B1yzSbvC.mjs} +4 -4
  193. package/dist/{renderer-CkbIz6M-.mjs.map → renderer-B1yzSbvC.mjs.map} +1 -1
  194. package/dist/{renderer-BYNbtv_d.mjs → renderer-B2YiXpp-.mjs} +2 -2
  195. package/dist/{renderer-BYNbtv_d.mjs.map → renderer-B2YiXpp-.mjs.map} +1 -1
  196. package/dist/{renderer-DXfYdQ2o.mjs → renderer-B55ljMI_.mjs} +2 -2
  197. package/dist/{renderer-DXfYdQ2o.mjs.map → renderer-B55ljMI_.mjs.map} +1 -1
  198. package/dist/{renderer-CopRZnwp.mjs → renderer-B6Tg4UgH.mjs} +4 -4
  199. package/dist/{renderer-CopRZnwp.mjs.map → renderer-B6Tg4UgH.mjs.map} +1 -1
  200. package/dist/{renderer-CuQceSLi.mjs → renderer-BFnf-8Yt.mjs} +3 -3
  201. package/dist/{renderer-CuQceSLi.mjs.map → renderer-BFnf-8Yt.mjs.map} +1 -1
  202. package/dist/{renderer-BmarvBo8.mjs → renderer-BGHHoPiD.mjs} +2 -2
  203. package/dist/{renderer-CLvXiFPk.mjs → renderer-BHUjPi24.mjs} +2 -2
  204. package/dist/{renderer-CLvXiFPk.mjs.map → renderer-BHUjPi24.mjs.map} +1 -1
  205. package/dist/renderer-BHXtDg5_.mjs +3 -0
  206. package/dist/{renderer-CTQTbvWR.mjs → renderer-BIk67-jU.mjs} +2 -2
  207. package/dist/{renderer-CTQTbvWR.mjs.map → renderer-BIk67-jU.mjs.map} +1 -1
  208. package/dist/{renderer-BHjw4EdY.d.mts → renderer-BLEmr0Lt.d.mts} +2 -2
  209. package/dist/{renderer-BHjw4EdY.d.mts.map → renderer-BLEmr0Lt.d.mts.map} +1 -1
  210. package/dist/{renderer-BcH6-FsA.mjs → renderer-BLs5sDdk.mjs} +3 -3
  211. package/dist/{renderer-BcH6-FsA.mjs.map → renderer-BLs5sDdk.mjs.map} +1 -1
  212. package/dist/renderer-BPd3gvEt.mjs +5 -0
  213. package/dist/{renderer-Hs9t2qY-.mjs → renderer-BXul0Wy6.mjs} +3 -3
  214. package/dist/{renderer-Hs9t2qY-.mjs.map → renderer-BXul0Wy6.mjs.map} +1 -1
  215. package/dist/{renderer-BX2tt7zA.mjs → renderer-B_1SLBPa.mjs} +3 -3
  216. package/dist/{renderer-BX2tt7zA.mjs.map → renderer-B_1SLBPa.mjs.map} +1 -1
  217. package/dist/renderer-B_ep98BH.mjs +5 -0
  218. package/dist/renderer-Ba_KUbKW.mjs +5 -0
  219. package/dist/{renderer-v779MqRx.mjs → renderer-BbyNUjE_.mjs} +3 -3
  220. package/dist/{renderer-v779MqRx.mjs.map → renderer-BbyNUjE_.mjs.map} +1 -1
  221. package/dist/{renderer-rLhze-dj.mjs → renderer-BcVr_cIk.mjs} +2 -2
  222. package/dist/{renderer-rLhze-dj.mjs.map → renderer-BcVr_cIk.mjs.map} +1 -1
  223. package/dist/renderer-BeL2hstU.mjs +5 -0
  224. package/dist/renderer-Bj81osPQ.mjs +5 -0
  225. package/dist/{renderer-Cy_rrWEC.mjs → renderer-BjL7sKGU.mjs} +3 -3
  226. package/dist/{renderer-Cy_rrWEC.mjs.map → renderer-BjL7sKGU.mjs.map} +1 -1
  227. package/dist/{renderer-BZ9zGp0Z.mjs → renderer-BjNcuJ4H.mjs} +3 -3
  228. package/dist/{renderer-BZ9zGp0Z.mjs.map → renderer-BjNcuJ4H.mjs.map} +1 -1
  229. package/dist/{renderer-Da5Xcwx5.mjs → renderer-Br8aDmf0.mjs} +2 -2
  230. package/dist/renderer-BvZQngSm.mjs +5 -0
  231. package/dist/renderer-BxgN8oXx.mjs +5 -0
  232. package/dist/{renderer-CptS_UFe.mjs → renderer-ByOvGGzp.mjs} +3 -3
  233. package/dist/{renderer-CptS_UFe.mjs.map → renderer-ByOvGGzp.mjs.map} +1 -1
  234. package/dist/{renderer-C3gJco03.mjs → renderer-C11qLlrz.mjs} +3 -3
  235. package/dist/{renderer-C3gJco03.mjs.map → renderer-C11qLlrz.mjs.map} +1 -1
  236. package/dist/{renderer-D4KKtfTl.mjs → renderer-C5dyPQjG.mjs} +3 -3
  237. package/dist/{renderer-D4KKtfTl.mjs.map → renderer-C5dyPQjG.mjs.map} +1 -1
  238. package/dist/renderer-C5jDrTBc.mjs +5 -0
  239. package/dist/{renderer-Dk88a53K.mjs → renderer-C6TbU8U5.mjs} +3 -3
  240. package/dist/{renderer-Dk88a53K.mjs.map → renderer-C6TbU8U5.mjs.map} +1 -1
  241. package/dist/{renderer-DKkXBsVx.mjs → renderer-C9bcEaBE.mjs} +3 -3
  242. package/dist/{renderer-DKkXBsVx.mjs.map → renderer-C9bcEaBE.mjs.map} +1 -1
  243. package/dist/{renderer-COAYUhv2.mjs → renderer-CAH00Lub.mjs} +2 -2
  244. package/dist/{renderer-GNgSEbBu.mjs → renderer-CAhsBEpU.mjs} +2 -2
  245. package/dist/{renderer-GNgSEbBu.mjs.map → renderer-CAhsBEpU.mjs.map} +1 -1
  246. package/dist/renderer-CBK4M82Y.mjs +5 -0
  247. package/dist/{renderer-876_ArdD.mjs → renderer-CCH0gSF7.mjs} +3 -3
  248. package/dist/{renderer-876_ArdD.mjs.map → renderer-CCH0gSF7.mjs.map} +1 -1
  249. package/dist/{renderer-DNNxsVQh.mjs → renderer-CCbOp0BM.mjs} +3 -3
  250. package/dist/{renderer-DNNxsVQh.mjs.map → renderer-CCbOp0BM.mjs.map} +1 -1
  251. package/dist/{renderer-UPRkHFPp.mjs → renderer-CEb5GjcU.mjs} +3 -3
  252. package/dist/{renderer-UPRkHFPp.mjs.map → renderer-CEb5GjcU.mjs.map} +1 -1
  253. package/dist/{renderer-Cwhm9oQe.mjs → renderer-CIL9zh-a.mjs} +3 -3
  254. package/dist/{renderer-Cwhm9oQe.mjs.map → renderer-CIL9zh-a.mjs.map} +1 -1
  255. package/dist/{renderer-URxsYx_I.mjs → renderer-CIbXIFOb.mjs} +4 -4
  256. package/dist/{renderer-URxsYx_I.mjs.map → renderer-CIbXIFOb.mjs.map} +1 -1
  257. package/dist/renderer-CLHtCctc.mjs +5 -0
  258. package/dist/{renderer-Q6A9dB56.mjs → renderer-CLX33fIK.mjs} +3 -3
  259. package/dist/{renderer-Q6A9dB56.mjs.map → renderer-CLX33fIK.mjs.map} +1 -1
  260. package/dist/{renderer-DJJXvO27.mjs → renderer-CLXpWDqx.mjs} +2 -2
  261. package/dist/{renderer-DJJXvO27.mjs.map → renderer-CLXpWDqx.mjs.map} +1 -1
  262. package/dist/renderer-CLin7MNN.mjs +5 -0
  263. package/dist/{renderer-BBnurLTw.mjs → renderer-CLypsmiH.mjs} +3 -3
  264. package/dist/{renderer-BBnurLTw.mjs.map → renderer-CLypsmiH.mjs.map} +1 -1
  265. package/dist/renderer-CN7TypOB.mjs +5 -0
  266. package/dist/renderer-CV5Jfwq7.mjs +5 -0
  267. package/dist/renderer-Ca5E46WK.mjs +5 -0
  268. package/dist/renderer-CdbOtoOU.mjs +5 -0
  269. package/dist/renderer-CeD_bqWj.mjs +5 -0
  270. package/dist/{renderer-nIKvk_ij.mjs → renderer-CfEuoZ7b.mjs} +3 -3
  271. package/dist/{renderer-nIKvk_ij.mjs.map → renderer-CfEuoZ7b.mjs.map} +1 -1
  272. package/dist/renderer-CfzNNUOd.mjs +5 -0
  273. package/dist/{renderer-I-KtRtuL.mjs → renderer-Cg3h0Mz9.mjs} +3 -3
  274. package/dist/{renderer-I-KtRtuL.mjs.map → renderer-Cg3h0Mz9.mjs.map} +1 -1
  275. package/dist/renderer-Cl8TmdU8.mjs +5 -0
  276. package/dist/{renderer-CW05KUXE.mjs → renderer-ClHqvIps.mjs} +10 -10
  277. package/dist/renderer-ClHqvIps.mjs.map +1 -0
  278. package/dist/renderer-CosBP_IM.mjs +5 -0
  279. package/dist/renderer-CpwVs5Fh.mjs +5 -0
  280. package/dist/renderer-Cq7baxNb.mjs +5 -0
  281. package/dist/{renderer-PTV0M2HP.mjs → renderer-CqMz_iXy.mjs} +2 -2
  282. package/dist/{renderer-PTV0M2HP.mjs.map → renderer-CqMz_iXy.mjs.map} +1 -1
  283. package/dist/{renderer-DGUL7JlO.mjs → renderer-CqSmfrE1.mjs} +3 -3
  284. package/dist/{renderer-DGUL7JlO.mjs.map → renderer-CqSmfrE1.mjs.map} +1 -1
  285. package/dist/{renderer-D1fjdDVu.d.mts → renderer-CrhJJKFY.d.mts} +5 -3
  286. package/dist/renderer-CrhJJKFY.d.mts.map +1 -0
  287. package/dist/{renderer-sQjiRkiF.mjs → renderer-CxxwhUwy.mjs} +3 -3
  288. package/dist/{renderer-sQjiRkiF.mjs.map → renderer-CxxwhUwy.mjs.map} +1 -1
  289. package/dist/{renderer-BsSZdDHQ.mjs → renderer-D24aiXmB.mjs} +3 -3
  290. package/dist/{renderer-BsSZdDHQ.mjs.map → renderer-D24aiXmB.mjs.map} +1 -1
  291. package/dist/renderer-D6GkxwAx.mjs +5 -0
  292. package/dist/renderer-D7SfGdzX.mjs +5 -0
  293. package/dist/renderer-D7wt09_l.mjs +5 -0
  294. package/dist/{renderer-BHJzeFp9.mjs → renderer-DBslE7fc.mjs} +2 -2
  295. package/dist/{renderer-BHJzeFp9.mjs.map → renderer-DBslE7fc.mjs.map} +1 -1
  296. package/dist/{renderer-DfQWeysg.mjs → renderer-DE3Ki96C.mjs} +3 -3
  297. package/dist/{renderer-DfQWeysg.mjs.map → renderer-DE3Ki96C.mjs.map} +1 -1
  298. package/dist/{renderer-C6YcfNbV.mjs → renderer-DJCqc98W.mjs} +3 -3
  299. package/dist/{renderer-C6YcfNbV.mjs.map → renderer-DJCqc98W.mjs.map} +1 -1
  300. package/dist/{renderer-CdxHmvvf.mjs → renderer-DODOyrty.mjs} +3 -3
  301. package/dist/{renderer-CdxHmvvf.mjs.map → renderer-DODOyrty.mjs.map} +1 -1
  302. package/dist/{renderer-B51lS0Zb.mjs → renderer-DRYxcfgi.mjs} +2 -2
  303. package/dist/renderer-DSuDMYqY.mjs +5 -0
  304. package/dist/{renderer-BfYAlY63.mjs → renderer-DYreml_A.mjs} +3 -3
  305. package/dist/{renderer-BfYAlY63.mjs.map → renderer-DYreml_A.mjs.map} +1 -1
  306. package/dist/{renderer-Byp6c5Y4.mjs → renderer-DZJnNISO.mjs} +2 -2
  307. package/dist/{renderer-Byp6c5Y4.mjs.map → renderer-DZJnNISO.mjs.map} +1 -1
  308. package/dist/renderer-DaIstCIs.mjs +5 -0
  309. package/dist/renderer-DbOD3Nc0.mjs +5 -0
  310. package/dist/{renderer-Bpm12fcQ.mjs → renderer-DbenSRjl.mjs} +3 -3
  311. package/dist/{renderer-Bpm12fcQ.mjs.map → renderer-DbenSRjl.mjs.map} +1 -1
  312. package/dist/{renderer-56T9Wf1N.mjs → renderer-DcXxkxds.mjs} +75 -72
  313. package/dist/renderer-DcXxkxds.mjs.map +1 -0
  314. package/dist/renderer-DdRCA1bn.mjs +6 -0
  315. package/dist/{renderer-15a6H6Yg.mjs → renderer-DdUtL-qF.mjs} +3 -3
  316. package/dist/{renderer-15a6H6Yg.mjs.map → renderer-DdUtL-qF.mjs.map} +1 -1
  317. package/dist/renderer-DfO37haf.mjs +5 -0
  318. package/dist/renderer-DjZwzxZ7.mjs +5 -0
  319. package/dist/renderer-DnAvySNy.mjs +5 -0
  320. package/dist/{renderer-IP6Iy2RQ.mjs → renderer-DnbY04Qk.mjs} +3 -3
  321. package/dist/{renderer-IP6Iy2RQ.mjs.map → renderer-DnbY04Qk.mjs.map} +1 -1
  322. package/dist/renderer-DnvEPm5E.mjs +5 -0
  323. package/dist/{renderer-HW-6FmmO.mjs → renderer-Do9mW_-B.mjs} +3 -3
  324. package/dist/{renderer-HW-6FmmO.mjs.map → renderer-Do9mW_-B.mjs.map} +1 -1
  325. package/dist/{renderer-DlboTFM8.mjs → renderer-Dqg8qxjq.mjs} +3 -3
  326. package/dist/{renderer-DlboTFM8.mjs.map → renderer-Dqg8qxjq.mjs.map} +1 -1
  327. package/dist/{renderer-BfuMCx2c.mjs → renderer-DvkRbmb2.mjs} +3 -3
  328. package/dist/{renderer-BfuMCx2c.mjs.map → renderer-DvkRbmb2.mjs.map} +1 -1
  329. package/dist/renderer-DwNpKnWf.mjs +5 -0
  330. package/dist/{renderer-Djpx4utT.mjs → renderer-Dw_xy3zx.mjs} +3 -3
  331. package/dist/{renderer-Djpx4utT.mjs.map → renderer-Dw_xy3zx.mjs.map} +1 -1
  332. package/dist/renderer-Dx3Dghus.mjs +5 -0
  333. package/dist/{renderer-SPq9BbnG.mjs → renderer-DyrNti45.mjs} +3 -3
  334. package/dist/{renderer-SPq9BbnG.mjs.map → renderer-DyrNti45.mjs.map} +1 -1
  335. package/dist/renderer-KvaoEwIk.mjs +5 -0
  336. package/dist/{renderer-BxtbwOSp.mjs → renderer-SwUFJ4aA.mjs} +3 -3
  337. package/dist/{renderer-BxtbwOSp.mjs.map → renderer-SwUFJ4aA.mjs.map} +1 -1
  338. package/dist/{renderer-DF_TswHB.mjs → renderer-XEoQnDs5.mjs} +3 -3
  339. package/dist/{renderer-DF_TswHB.mjs.map → renderer-XEoQnDs5.mjs.map} +1 -1
  340. package/dist/{renderer-QYjSQL80.mjs → renderer-Xw9Zn3pD.mjs} +2 -2
  341. package/dist/{renderer-QYjSQL80.mjs.map → renderer-Xw9Zn3pD.mjs.map} +1 -1
  342. package/dist/{renderer-XU08U1If.mjs → renderer-ZWIDTOLV.mjs} +2 -2
  343. package/dist/{renderer-XU08U1If.mjs.map → renderer-ZWIDTOLV.mjs.map} +1 -1
  344. package/dist/{renderer-8ZxKRprN.mjs → renderer-bGhRdKB7.mjs} +2 -2
  345. package/dist/{renderer-8ZxKRprN.mjs.map → renderer-bGhRdKB7.mjs.map} +1 -1
  346. package/dist/{renderer-DUx7rcc5.mjs → renderer-d4weUlVq.mjs} +2 -2
  347. package/dist/{renderer-DUx7rcc5.mjs.map → renderer-d4weUlVq.mjs.map} +1 -1
  348. package/dist/{renderer-Bf0Bhqot.mjs → renderer-g7CMgD1v.mjs} +3 -3
  349. package/dist/{renderer-Bf0Bhqot.mjs.map → renderer-g7CMgD1v.mjs.map} +1 -1
  350. package/dist/{renderer-BUj6OsWX.mjs → renderer-gjLFqdv-.mjs} +4 -4
  351. package/dist/{renderer-BUj6OsWX.mjs.map → renderer-gjLFqdv-.mjs.map} +1 -1
  352. package/dist/{renderer-DLpjygMO.mjs → renderer-gv0u9XmU.mjs} +3 -3
  353. package/dist/{renderer-DLpjygMO.mjs.map → renderer-gv0u9XmU.mjs.map} +1 -1
  354. package/dist/{renderer-KS_JYan2.mjs → renderer-jJWwrha1.mjs} +3 -3
  355. package/dist/{renderer-KS_JYan2.mjs.map → renderer-jJWwrha1.mjs.map} +1 -1
  356. package/dist/{renderer-C7BxBoHC.mjs → renderer-kl_pAczD.mjs} +3 -3
  357. package/dist/{renderer-C7BxBoHC.mjs.map → renderer-kl_pAczD.mjs.map} +1 -1
  358. package/dist/{renderer-Bxs9zUIN.mjs → renderer-lrKwPhZi.mjs} +2 -2
  359. package/dist/{renderer-Bxs9zUIN.mjs.map → renderer-lrKwPhZi.mjs.map} +1 -1
  360. package/dist/{renderer-DJy8V_nb.mjs → renderer-pnYj1REL.mjs} +3 -3
  361. package/dist/{renderer-DJy8V_nb.mjs.map → renderer-pnYj1REL.mjs.map} +1 -1
  362. package/dist/{renderers-CP8CFWDs-CPfe6IN0.mjs → renderers-CP8CFWDs-CDikhBRO.mjs} +3 -3
  363. package/dist/{renderers-CP8CFWDs-CPfe6IN0.mjs.map → renderers-CP8CFWDs-CDikhBRO.mjs.map} +1 -1
  364. package/dist/signals/index.d.mts +5 -5
  365. package/dist/signals/index.mjs +6 -6
  366. package/dist/{signals-BRgYyspQ.mjs → signals-BeAWA_1Y.mjs} +3 -3
  367. package/dist/{signals-BRgYyspQ.mjs.map → signals-BeAWA_1Y.mjs.map} +1 -1
  368. package/dist/{src-Be7rjknf.mjs → src-LYcH_X8o.mjs} +233 -27
  369. package/dist/src-LYcH_X8o.mjs.map +1 -0
  370. package/package.json +58 -58
  371. package/dist/dist-CL5JOMSp.mjs.map +0 -1
  372. package/dist/dist-CSU_2yUb.mjs.map +0 -1
  373. package/dist/index-BCzXnNpb.d.mts.map +0 -1
  374. package/dist/index-C541Ol1N.d.mts.map +0 -1
  375. package/dist/index-kj5uSzaI.d.mts.map +0 -1
  376. package/dist/preview-engine/chunk-3SR6BI4B.mjs +0 -1
  377. package/dist/preview-engine/chunk-ALSOO22Q.mjs +0 -1
  378. package/dist/preview-engine/chunk-NKGXINCU.mjs +0 -1
  379. package/dist/preview-engine/chunk-PJMX2GFY.mjs +0 -1
  380. package/dist/preview-engine/chunk-SIXIF5QZ.mjs +0 -3
  381. package/dist/preview-engine/chunk-TXX2POD2.mjs +0 -1974
  382. package/dist/preview-engine/renderers-3HFRWF2G.mjs +0 -1
  383. package/dist/preview-engine/renderers-4CRXZJ5G.mjs +0 -1
  384. package/dist/preview-engine/renderers-5QF4OLFQ.mjs +0 -1
  385. package/dist/preview-engine/renderers-5SOW6OVE.mjs +0 -1
  386. package/dist/preview-engine/renderers-6M2HPY2J.mjs +0 -1
  387. package/dist/preview-engine/renderers-7HT5NW7W.mjs +0 -1
  388. package/dist/preview-engine/renderers-ATTGUTYY.mjs +0 -1
  389. package/dist/preview-engine/renderers-AYWCRBQC.mjs +0 -1
  390. package/dist/preview-engine/renderers-BA6ZCPI5.mjs +0 -1
  391. package/dist/preview-engine/renderers-BAS7RXTC.mjs +0 -1
  392. package/dist/preview-engine/renderers-CZTCW4FC.mjs +0 -1
  393. package/dist/preview-engine/renderers-D2XEQ2YY.mjs +0 -1
  394. package/dist/preview-engine/renderers-DFIJYBW2.mjs +0 -1
  395. package/dist/preview-engine/renderers-EJN6M6DO.mjs +0 -1
  396. package/dist/preview-engine/renderers-EK3HQUUF.mjs +0 -1
  397. package/dist/preview-engine/renderers-G34C5J27.mjs +0 -1
  398. package/dist/preview-engine/renderers-GKVTNDVH.mjs +0 -1
  399. package/dist/preview-engine/renderers-HZBYQKIQ.mjs +0 -1
  400. package/dist/preview-engine/renderers-IK7ZJJKJ.mjs +0 -1
  401. package/dist/preview-engine/renderers-ISASPH6T.mjs +0 -1
  402. package/dist/preview-engine/renderers-IW5MWDBI.mjs +0 -1
  403. package/dist/preview-engine/renderers-JBRPKCKF.mjs +0 -228
  404. package/dist/preview-engine/renderers-K2PI4WJS.mjs +0 -1
  405. package/dist/preview-engine/renderers-KESHVFL2.mjs +0 -1
  406. package/dist/preview-engine/renderers-KTZZVAB2.mjs +0 -1
  407. package/dist/preview-engine/renderers-KY5CJEOV.mjs +0 -1
  408. package/dist/preview-engine/renderers-M5W6SUQD.mjs +0 -1
  409. package/dist/preview-engine/renderers-MEM3REMI.mjs +0 -1
  410. package/dist/preview-engine/renderers-O5XMWFPY.mjs +0 -1
  411. package/dist/preview-engine/renderers-QJA35PKC.mjs +0 -1
  412. package/dist/preview-engine/renderers-TJIFTQHV.mjs +0 -1
  413. package/dist/preview-engine/renderers-TN4XN436.mjs +0 -1
  414. package/dist/preview-engine/renderers-UDYLCYCB.mjs +0 -1
  415. package/dist/preview-engine/renderers-WZ7SFIBZ.mjs +0 -1
  416. package/dist/preview-engine/renderers-X6QNDC6R.mjs +0 -1
  417. package/dist/preview-engine/renderers-X6RNTJOV.mjs +0 -1
  418. package/dist/preview-engine/renderers-Y3N7236P.mjs +0 -1
  419. package/dist/preview-engine/renderers-YP3ASXIJ.mjs +0 -1
  420. package/dist/preview-engine/renderers-YVJAG3PD.mjs +0 -1
  421. package/dist/preview-engine/renderers-ZPPZNMKJ.mjs +0 -1
  422. package/dist/program-B6YvXPhj.mjs.map +0 -1
  423. package/dist/renderer-1qAST5qk.mjs +0 -5
  424. package/dist/renderer-2dntY9lQ.mjs +0 -5
  425. package/dist/renderer-56T9Wf1N.mjs.map +0 -1
  426. package/dist/renderer-83Knj3DI.mjs +0 -5
  427. package/dist/renderer-B2GWXOwP.mjs +0 -5
  428. package/dist/renderer-B3LlOjNo.mjs +0 -5
  429. package/dist/renderer-B5cPLblQ.mjs +0 -5
  430. package/dist/renderer-B7uorwdR.mjs +0 -5
  431. package/dist/renderer-BJ6LDZlX.mjs +0 -5
  432. package/dist/renderer-BaQC4ylR.mjs +0 -5
  433. package/dist/renderer-BfQzj_uW.mjs +0 -5
  434. package/dist/renderer-Bl9uy1ra.mjs +0 -6
  435. package/dist/renderer-Bu4FyXDo.mjs +0 -5
  436. package/dist/renderer-BxbJwS3T.mjs +0 -5
  437. package/dist/renderer-C51hhQ7B.mjs +0 -5
  438. package/dist/renderer-C8ZShnSH.mjs +0 -5
  439. package/dist/renderer-CQh1RgN8.mjs +0 -5
  440. package/dist/renderer-CRYaMhGd.mjs +0 -5
  441. package/dist/renderer-CULThR_r.mjs +0 -5
  442. package/dist/renderer-CW05KUXE.mjs.map +0 -1
  443. package/dist/renderer-C_KDGeMJ.mjs +0 -5
  444. package/dist/renderer-Ce4Gam_S.mjs +0 -5
  445. package/dist/renderer-CkZ6xcN6.mjs +0 -5
  446. package/dist/renderer-Co4C29rw.mjs +0 -5
  447. package/dist/renderer-CyReVcsb.mjs +0 -3
  448. package/dist/renderer-CzgPat6y.mjs +0 -5
  449. package/dist/renderer-D-pFBDki.mjs +0 -5
  450. package/dist/renderer-D1fjdDVu.d.mts.map +0 -1
  451. package/dist/renderer-DELCePyB.mjs +0 -5
  452. package/dist/renderer-DWijRKGh.mjs +0 -5
  453. package/dist/renderer-Di-PH-vG.mjs +0 -5
  454. package/dist/renderer-Dn9CR9KL.mjs +0 -5
  455. package/dist/renderer-ICmKc1Bq.mjs +0 -5
  456. package/dist/renderer-IR3rE4TW.mjs +0 -5
  457. package/dist/renderer-Qyc4xLZY.mjs +0 -5
  458. package/dist/renderer-Riuen1UO.mjs +0 -5
  459. package/dist/renderer-YKJt5oPu.mjs +0 -5
  460. package/dist/renderer-asS1sOXb.mjs +0 -5
  461. package/dist/renderer-mAQErqBN.mjs +0 -5
  462. package/dist/renderer-pZazoCK-.mjs +0 -5
  463. package/dist/src-Be7rjknf.mjs.map +0 -1
@@ -316,7 +316,7 @@
316
316
  {
317
317
  "name": "text-reveal",
318
318
  "title": "TextReveal",
319
- "summary": "Headlines and body copy that animate in and out by character, word or line: rise, blur, track, mask, slam, scatter, scramble, type, stream, wipe and flip, each ending on exactly the static text.",
319
+ "summary": "Headlines and body copy that animate in and out by character, word or line: rise, blur, mask, slam, scatter, scramble, type, stream, wipe and flip, each ending on exactly the static text.",
320
320
  "tags": [
321
321
  "text",
322
322
  "typography",
@@ -28,7 +28,7 @@
28
28
  "files": [
29
29
  {
30
30
  "path": "lower-third/index.ts",
31
- "content": "/**\n * LowerThird: the name-and-role bar of interviews and talks. Plates are\n * measured to their text, placed in the safe area's bottom corner, slide in\n * through their own box, and leave on their own after `hold`.\n *\n * Coordinates are the frame's: the root covers the video (default\n * the composition `canvas`, or the given width / height) from 0, 0.\n */\nimport type { LayoutNode } from \"framefields\";\nimport {\n\ttype Animatable,\n\tbindComponent,\n\tdefineComponent,\n\televate,\n\tKeyBook,\n\tparseColor,\n\ttype Rect,\n\tsafeArea,\n\tsurfaceFill,\n\tthemeFrames,\n\tz,\n} from \"framefields/components\";\n\nexport const LOWER_THIRD_LAYOUTS = [\"bar\", \"stacked\", \"tag\", \"line\"] as const;\n\n/** Cap height as a share of the font size (Inter and most UI sans faces). */\nconst CAP_HEIGHT = 0.72;\nexport type LowerThirdLayout = (typeof LOWER_THIRD_LAYOUTS)[number];\n\nexport interface LowerThirdCues {\n\t[name: string]: ReadonlyArray<{ frame: number }>;\n\t/** Fully on screen. */\n\tin: ReadonlyArray<{ frame: number }>;\n\t/** Starts to leave (empty with `stay`). */\n\tout: ReadonlyArray<{ frame: number }>;\n}\n\nexport const lowerThird = /* @__PURE__ */ defineComponent({\n\tname: \"LowerThird\",\n\tprops: () => ({\n\t\tname: z.string().min(1),\n\t\trole: z.string().optional(),\n\t\tlayout: z.enum(LOWER_THIRD_LAYOUTS).default(\"bar\"),\n\t\tside: z.enum([\"left\", \"right\"]).default(\"left\"),\n\t\tsafeArea: z\n\t\t\t.union([\n\t\t\t\tz.enum([\"portrait\", \"landscape\"]),\n\t\t\t\tz.object({\n\t\t\t\t\tx: z.number(),\n\t\t\t\t\ty: z.number(),\n\t\t\t\t\twidth: z.number(),\n\t\t\t\t\theight: z.number(),\n\t\t\t\t}),\n\t\t\t])\n\t\t\t.optional(),\n\t\t/** Stay on screen instead of leaving after `hold`. */\n\t\tstay: z.boolean().default(false),\n\t\taccent: z.string().optional(),\n\t\tplate: z.string().optional(),\n\t\tnameSize: z.number().positive().optional(),\n\t\tfontFamily: z.string().optional(),\n\t}),\n\tdefaults: { enter: false, exit: false, hold: 90 },\n\tbuild(ctx, props) {\n\t\tconst { host, theme, fps } = ctx;\n\t\tconst s = theme.scale;\n\t\tconst W = ctx.requested.width ?? ctx.canvas.width;\n\t\tconst H = ctx.requested.height ?? ctx.canvas.height;\n\t\tconst safe = safeArea({ width: W, height: H }, props.safeArea);\n\t\tconst fontFamily = props.fontFamily ?? theme.font.display;\n\t\tconst nameSize = props.nameSize ?? 44 * s;\n\t\tconst roleSize = nameSize * 0.62;\n\t\tconst accent = props.accent ?? theme.color.accent;\n\t\tconst plateColor = props.plate ?? theme.color.surface;\n\t\tconst right = props.side === \"right\";\n\t\tconst base = themeFrames(theme, \"base\", fps);\n\t\tconst fast = themeFrames(theme, \"fast\", fps);\n\t\tconst enter = theme.motion.ease.enter;\n\n\t\tconst nameM = host.measure(props.name, {\n\t\t\tfontFamily,\n\t\t\tfontSize: nameSize,\n\t\t\tfontWeight: theme.weight.bold,\n\t\t});\n\t\tconst roleM = props.role\n\t\t\t? host.measure(props.role, {\n\t\t\t\t\tfontFamily,\n\t\t\t\t\tfontSize: roleSize,\n\t\t\t\t\tfontWeight: theme.weight.medium,\n\t\t\t\t})\n\t\t\t: null;\n\t\tconst padX = nameSize * 0.55;\n\t\tconst padY = nameSize * 0.36;\n\t\tconst children: LayoutNode[] = [];\n\t\tconst parts: Record<string, Animatable<LayoutNode>> = {};\n\t\t/** Keys per node and prop, merged into one track each at the end. */\n\t\tconst book = new KeyBook<Animatable<LayoutNode>>();\n\t\tconst key = book.key.bind(book);\n\n\t\tconst text = (\n\t\t\tid: string,\n\t\t\tcontent: string,\n\t\t\tsize: number,\n\t\t\tweight: number,\n\t\t\tfill: string,\n\t\t\tx: number,\n\t\t\ty: number,\n\t\t) =>\n\t\t\thost.text(content, {\n\t\t\t\tid: ctx.partId(id),\n\t\t\t\tposition: \"absolute\",\n\t\t\t\tx,\n\t\t\t\ty,\n\t\t\t\tfontFamily,\n\t\t\t\tfontSize: size,\n\t\t\t\tfontWeight: weight,\n\t\t\t\tfill,\n\t\t\t});\n\n\t\t/**\n\t\t * A plate that slides in through its own box: the background travels\n\t\t * in from the side, its text rises a beat later. Returns the timing.\n\t\t */\n\t\tconst plate = (\n\t\t\tid: string,\n\t\t\trect: Rect,\n\t\t\tbackground: string,\n\t\t\tradius: number,\n\t\t\tcontent: Array<Animatable<LayoutNode>>,\n\t\t\tat: number,\n\t\t) => {\n\t\t\tconst fill = host.box({\n\t\t\t\tid: ctx.partId(`${id}-fill`),\n\t\t\t\tposition: \"absolute\",\n\t\t\t\tx: 0,\n\t\t\t\ty: 0,\n\t\t\t\twidth: rect.width,\n\t\t\t\theight: rect.height,\n\t\t\t\tbackground: surfaceFill(background, theme),\n\t\t\t\tborderRadius: radius,\n\t\t\t\t// The slide is fast: a little smear sells it.\n\t\t\t\tmotionBlurShutter: 90,\n\t\t\t});\n\t\t\tconst travel = rect.width * (right ? 1 : -1);\n\t\t\tconst slide = Math.round(base * 1.1);\n\t\t\tkey(fill, \"x\", [\n\t\t\t\t[at, travel],\n\t\t\t\t[at + slide, 0, enter],\n\t\t\t]);\n\t\t\tconst textAt = at + Math.round(fast * 0.7);\n\t\t\tfor (const node of content) {\n\t\t\t\tconst y0 = (node as unknown as { y: number }).y;\n\t\t\t\tkey(node, \"y\", [\n\t\t\t\t\t[textAt, y0 + rect.height * 0.5],\n\t\t\t\t\t[textAt + base, y0, enter],\n\t\t\t\t]);\n\t\t\t\tkey(node, \"opacity\", [\n\t\t\t\t\t[textAt, 0],\n\t\t\t\t\t[textAt + Math.round(base * 0.6), 1, \"power2.out\"],\n\t\t\t\t]);\n\t\t\t}\n\t\t\tconst clip = host.box({\n\t\t\t\tid: ctx.partId(id),\n\t\t\t\tposition: \"absolute\",\n\t\t\t\t...rect,\n\t\t\t\toverflow: \"hidden\",\n\t\t\t\tborderRadius: radius,\n\t\t\t\tchildren: [fill, ...content],\n\t\t\t});\n\t\t\t// A clear plate (the `line` layout) has nothing to lift.\n\t\t\tif (parseColor(background)?.[3] !== 0) elevate(host, clip, theme, \"low\");\n\t\t\tchildren.push(clip);\n\t\t\tparts[id] = clip;\n\t\t\treturn {\n\t\t\t\tclip,\n\t\t\t\tfill,\n\t\t\t\tcontent,\n\t\t\t\ttravel,\n\t\t\t\tsettled: Math.max(at + slide, textAt + base),\n\t\t\t};\n\t\t};\n\n\t\t// ── Layouts ───────────────────────────────────────────────────────\n\t\tconst bottom = safe.y + safe.height;\n\t\tconst xFor = (width: number) =>\n\t\t\tright ? safe.x + safe.width - width : safe.x;\n\t\tconst plates: Array<ReturnType<typeof plate>> = [];\n\t\tlet accentLine: Animatable<LayoutNode> | null = null;\n\t\tconst lineW = Math.max(3, nameSize * 0.09);\n\n\t\tswitch (props.layout) {\n\t\t\tcase \"bar\": {\n\t\t\t\tconst edge = Math.max(4, nameSize * 0.14);\n\t\t\t\tconst innerW = Math.max(nameM.width, roleM?.width ?? 0);\n\t\t\t\tconst h = padY * 2 + nameM.height + (roleM ? roleM.height * 0.95 : 0);\n\t\t\t\tconst w = innerW + padX * 2 + edge;\n\t\t\t\tconst rect = { x: xFor(w), y: bottom - h, width: w, height: h };\n\t\t\t\tconst tx = right ? padX : padX + edge;\n\t\t\t\tconst content = [\n\t\t\t\t\ttext(\n\t\t\t\t\t\t\"name\",\n\t\t\t\t\t\tprops.name,\n\t\t\t\t\t\tnameSize,\n\t\t\t\t\t\ttheme.weight.bold,\n\t\t\t\t\t\ttheme.color.ink,\n\t\t\t\t\t\ttx,\n\t\t\t\t\t\tpadY,\n\t\t\t\t\t),\n\t\t\t\t];\n\t\t\t\tif (props.role && roleM)\n\t\t\t\t\tcontent.push(\n\t\t\t\t\t\ttext(\n\t\t\t\t\t\t\t\"role\",\n\t\t\t\t\t\t\tprops.role,\n\t\t\t\t\t\t\troleSize,\n\t\t\t\t\t\t\ttheme.weight.medium,\n\t\t\t\t\t\t\ttheme.color.muted,\n\t\t\t\t\t\t\ttx,\n\t\t\t\t\t\t\tpadY + nameM.height * 0.95,\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\tconst p = plate(\"plate\", rect, plateColor, theme.radius.sm, content, 0);\n\t\t\t\tconst strip = host.box({\n\t\t\t\t\tid: ctx.partId(\"edge\"),\n\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\tx: right ? rect.x + rect.width - edge : rect.x,\n\t\t\t\t\ty: rect.y + rect.height,\n\t\t\t\t\twidth: edge,\n\t\t\t\t\theight: 0,\n\t\t\t\t\tbackground: accent,\n\t\t\t\t\tborderRadius: theme.radius.sm,\n\t\t\t\t});\n\t\t\t\tconst grow = Math.round(base * 0.8);\n\t\t\t\tkey(strip, \"height\", [\n\t\t\t\t\t[0, 0],\n\t\t\t\t\t[grow, rect.height, enter],\n\t\t\t\t]);\n\t\t\t\tkey(strip, \"y\", [\n\t\t\t\t\t[0, rect.y + rect.height],\n\t\t\t\t\t[grow, rect.y, enter],\n\t\t\t\t]);\n\t\t\t\tchildren.push(strip);\n\t\t\t\tparts.edge = strip;\n\t\t\t\tplates.push(p);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"stacked\": {\n\t\t\t\tconst nameW = nameM.width + padX * 2;\n\t\t\t\tconst nameH = nameM.height + padY * 2;\n\t\t\t\tconst roleH = roleM ? roleM.height + padY * 1.2 : 0;\n\t\t\t\tconst roleW = roleM ? roleM.width + padX * 1.6 : 0;\n\t\t\t\tconst inset = nameSize * 0.4;\n\t\t\t\tconst total = nameH + (roleM ? roleH * 0.9 : 0);\n\t\t\t\tconst top = bottom - total;\n\t\t\t\tplates.push(\n\t\t\t\t\tplate(\n\t\t\t\t\t\t\"name-plate\",\n\t\t\t\t\t\t{ x: xFor(nameW), y: top, width: nameW, height: nameH },\n\t\t\t\t\t\tplateColor,\n\t\t\t\t\t\ttheme.radius.sm,\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\ttext(\n\t\t\t\t\t\t\t\t\"name\",\n\t\t\t\t\t\t\t\tprops.name,\n\t\t\t\t\t\t\t\tnameSize,\n\t\t\t\t\t\t\t\ttheme.weight.bold,\n\t\t\t\t\t\t\t\ttheme.color.ink,\n\t\t\t\t\t\t\t\tpadX,\n\t\t\t\t\t\t\t\tpadY,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t],\n\t\t\t\t\t\t0,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tif (props.role && roleM)\n\t\t\t\t\tplates.push(\n\t\t\t\t\t\tplate(\n\t\t\t\t\t\t\t\"role-plate\",\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tx: right ? xFor(roleW) - inset : xFor(roleW) + inset,\n\t\t\t\t\t\t\t\ty: top + nameH - roleH * 0.1,\n\t\t\t\t\t\t\t\twidth: roleW,\n\t\t\t\t\t\t\t\theight: roleH,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\taccent,\n\t\t\t\t\t\t\ttheme.radius.sm,\n\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\ttext(\n\t\t\t\t\t\t\t\t\t\"role\",\n\t\t\t\t\t\t\t\t\tprops.role,\n\t\t\t\t\t\t\t\t\troleSize,\n\t\t\t\t\t\t\t\t\ttheme.weight.semibold,\n\t\t\t\t\t\t\t\t\ttheme.color.bg,\n\t\t\t\t\t\t\t\t\tpadX * 0.8,\n\t\t\t\t\t\t\t\t\tpadY * 0.6,\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\tMath.round(fast * 0.8),\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"tag\": {\n\t\t\t\t// One line, one node: bold name, a dot, the role a size down.\n\t\t\t\tconst sep = \" · \";\n\t\t\t\tconst sepW = props.role\n\t\t\t\t\t? host.measure(sep, { fontFamily, fontSize: roleSize }).width\n\t\t\t\t\t: 0;\n\t\t\t\tconst h = nameM.height + padY * 1.4;\n\t\t\t\tconst w = nameM.width + (roleM ? sepW + roleM.width : 0) + h;\n\t\t\t\tconst line = host.text(\n\t\t\t\t\tprops.role ? `${props.name}${sep}${props.role}` : props.name,\n\t\t\t\t\t{\n\t\t\t\t\t\tid: ctx.partId(\"name\"),\n\t\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\t\tx: h / 2,\n\t\t\t\t\t\ty: padY * 0.7,\n\t\t\t\t\t\tfontFamily,\n\t\t\t\t\t\tfontSize: nameSize,\n\t\t\t\t\t\tfill: theme.color.ink,\n\t\t\t\t\t\tspans: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\ttext: props.name,\n\t\t\t\t\t\t\t\tfontWeight: theme.weight.bold,\n\t\t\t\t\t\t\t\tfill: theme.color.ink,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t...(props.role\n\t\t\t\t\t\t\t\t? [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\ttext: `${sep}${props.role}`,\n\t\t\t\t\t\t\t\t\t\t\tfontSize: roleSize,\n\t\t\t\t\t\t\t\t\t\t\tfontWeight: theme.weight.medium,\n\t\t\t\t\t\t\t\t\t\t\tfill: theme.color.muted,\n\t\t\t\t\t\t\t\t\t\t\t// Centred on the name's capitals, not on its baseline: one\n\t\t\t\t\t\t\t\t\t\t\t// line in a pill reads as aligned when the caps share a middle.\n\t\t\t\t\t\t\t\t\t\t\tbaselineShift: -(nameSize - roleSize) * CAP_HEIGHT * 0.5,\n\t\t\t\t\t\t\t\t\t\t},\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],\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t\tplates.push(\n\t\t\t\t\tplate(\n\t\t\t\t\t\t\"tag\",\n\t\t\t\t\t\t{ x: xFor(w), y: bottom - h, width: w, height: h },\n\t\t\t\t\t\tplateColor,\n\t\t\t\t\t\ttheme.radius.pill,\n\t\t\t\t\t\t[line],\n\t\t\t\t\t\t0,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"line\": {\n\t\t\t\tconst innerW = Math.max(nameM.width, roleM?.width ?? 0);\n\t\t\t\tconst h = nameM.height + (roleM ? roleM.height : 0);\n\t\t\t\tconst gap = nameSize * 0.4;\n\t\t\t\tconst w = innerW + gap + lineW;\n\t\t\t\tconst x = xFor(w);\n\t\t\t\tconst lineX = right ? x + w - lineW : x;\n\t\t\t\tconst tx = right ? 0 : lineW + gap;\n\t\t\t\taccentLine = host.box({\n\t\t\t\t\tid: ctx.partId(\"line\"),\n\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\tx: lineX,\n\t\t\t\t\ty: bottom,\n\t\t\t\t\twidth: lineW,\n\t\t\t\t\theight: 0,\n\t\t\t\t\tbackground: accent,\n\t\t\t\t\tborderRadius: lineW / 2,\n\t\t\t\t});\n\t\t\t\tconst grow = Math.round(base * 1.1);\n\t\t\t\tkey(accentLine, \"height\", [\n\t\t\t\t\t[0, 0],\n\t\t\t\t\t[grow, h, enter],\n\t\t\t\t]);\n\t\t\t\tkey(accentLine, \"y\", [\n\t\t\t\t\t[0, bottom],\n\t\t\t\t\t[grow, bottom - h, enter],\n\t\t\t\t]);\n\t\t\t\tchildren.push(accentLine);\n\t\t\t\tparts.line = accentLine;\n\t\t\t\tconst content = [\n\t\t\t\t\ttext(\n\t\t\t\t\t\t\"name\",\n\t\t\t\t\t\tprops.name,\n\t\t\t\t\t\tnameSize,\n\t\t\t\t\t\ttheme.weight.bold,\n\t\t\t\t\t\ttheme.color.ink,\n\t\t\t\t\t\ttx,\n\t\t\t\t\t\t0,\n\t\t\t\t\t),\n\t\t\t\t];\n\t\t\t\tif (props.role && roleM)\n\t\t\t\t\tcontent.push(\n\t\t\t\t\t\ttext(\n\t\t\t\t\t\t\t\"role\",\n\t\t\t\t\t\t\tprops.role,\n\t\t\t\t\t\t\troleSize,\n\t\t\t\t\t\t\ttheme.weight.medium,\n\t\t\t\t\t\t\ttheme.color.muted,\n\t\t\t\t\t\t\ttx,\n\t\t\t\t\t\t\tnameM.height,\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t// No plate: the text rises through a clear box.\n\t\t\t\tplates.push(\n\t\t\t\t\tplate(\n\t\t\t\t\t\t\"text\",\n\t\t\t\t\t\t{ x, y: bottom - h, width: w, height: h },\n\t\t\t\t\t\t\"rgba(0,0,0,0)\",\n\t\t\t\t\t\t0,\n\t\t\t\t\t\tcontent,\n\t\t\t\t\t\tMath.round(fast * 0.6),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tconst settled = Math.max(...plates.map((p) => p.settled));\n\t\t// ── Out: text first, then the plates slide back the way they came ──\n\t\tlet afterHold = 0;\n\t\tconst outAt = settled + ctx.hold;\n\t\tif (!props.stay) {\n\t\t\tconst fadeOut = Math.round(fast * 0.9);\n\t\t\tconst slide = Math.round(base * 0.9);\n\t\t\tplates.forEach((p, k) => {\n\t\t\t\tconst at = outAt + k * Math.round(fast * 0.4);\n\t\t\t\tfor (const node of p.content)\n\t\t\t\t\tkey(node, \"opacity\", [\n\t\t\t\t\t\t[at, 1],\n\t\t\t\t\t\t[at + fadeOut, 0, \"power2.in\"],\n\t\t\t\t\t]);\n\t\t\t\tkey(p.fill, \"x\", [\n\t\t\t\t\t[at + fadeOut - 2, 0],\n\t\t\t\t\t[at + fadeOut - 2 + slide, p.travel, theme.motion.ease.exit],\n\t\t\t\t]);\n\t\t\t\tafterHold = Math.max(afterHold, at + fadeOut - 2 + slide - outAt);\n\t\t\t});\n\t\t\tfor (const node of [accentLine, parts.edge])\n\t\t\t\tif (node)\n\t\t\t\t\tkey(node, \"opacity\", [\n\t\t\t\t\t\t[outAt, 1],\n\t\t\t\t\t\t[outAt + afterHold, 0, \"power2.in\"],\n\t\t\t\t\t]);\n\t\t}\n\t\tbook.apply();\n\n\t\treturn {\n\t\t\tchildren,\n\t\t\twidth: W,\n\t\t\theight: H,\n\t\t\tduration: settled,\n\t\t\topenEnded: props.stay,\n\t\t\tafterHold,\n\t\t\torigin: [0, 0],\n\t\t\tcues: {\n\t\t\t\tin: [{ frame: settled }],\n\t\t\t\tout: props.stay ? [] : [{ frame: outAt }],\n\t\t\t} as LowerThirdCues,\n\t\t\tparts,\n\t\t};\n\t},\n});\n\n/**\n * The name-and-role bar of interviews and talks: measured plates in the safe\n * area that slide in and leave on their own.\n */\nexport const LowerThird = /* @__PURE__ */ bindComponent(lowerThird);\n"
31
+ "content": "/**\n * LowerThird: the name-and-role bar of interviews and talks. Plates are\n * measured to their text, placed in the safe area's bottom corner, slide in\n * through their own box, and leave on their own after `hold`.\n *\n * Coordinates are the frame's: the root covers the video (default\n * the composition `canvas`, or the given width / height) from 0, 0.\n */\nimport type { LayoutNode } from \"framefields\";\nimport {\n\ttype Animatable,\n\tbindComponent,\n\tdefineComponent,\n\televate,\n\tKeyBook,\n\tparseColor,\n\ttype Rect,\n\tsafeArea,\n\tsurfaceFill,\n\tthemeFrames,\n\tz,\n} from \"framefields/components\";\n\nexport const LOWER_THIRD_LAYOUTS = [\"bar\", \"stacked\", \"tag\", \"line\"] as const;\n\n/** Cap height as a share of the font size (Inter and most UI sans faces). */\nconst CAP_HEIGHT = 0.72;\nexport type LowerThirdLayout = (typeof LOWER_THIRD_LAYOUTS)[number];\n\nexport interface LowerThirdCues {\n\t[name: string]: ReadonlyArray<{ frame: number }>;\n\t/** Fully on screen. */\n\tin: ReadonlyArray<{ frame: number }>;\n\t/** Starts to leave (empty with `stay`). */\n\tout: ReadonlyArray<{ frame: number }>;\n}\n\nexport const lowerThird = /* @__PURE__ */ defineComponent({\n\tname: \"LowerThird\",\n\tprops: () => ({\n\t\tname: z.string().min(1),\n\t\trole: z.string().optional(),\n\t\tlayout: z.enum(LOWER_THIRD_LAYOUTS).default(\"bar\"),\n\t\tside: z.enum([\"left\", \"right\"]).default(\"left\"),\n\t\tsafeArea: z\n\t\t\t.union([\n\t\t\t\tz.enum([\"portrait\", \"landscape\"]),\n\t\t\t\tz.object({\n\t\t\t\t\tx: z.number(),\n\t\t\t\t\ty: z.number(),\n\t\t\t\t\twidth: z.number(),\n\t\t\t\t\theight: z.number(),\n\t\t\t\t}),\n\t\t\t])\n\t\t\t.optional(),\n\t\t/** Stay on screen instead of leaving after `hold`. */\n\t\tstay: z.boolean().default(false),\n\t\taccent: z.string().optional(),\n\t\tplate: z.string().optional(),\n\t\tnameSize: z.number().positive().optional(),\n\t\tfontFamily: z.string().optional(),\n\t}),\n\tdefaults: { enter: false, exit: false, hold: 90 },\n\tbuild(ctx, props) {\n\t\tconst { host, theme, fps } = ctx;\n\t\tconst s = theme.scale;\n\t\tconst W = ctx.requested.width ?? ctx.canvas.width;\n\t\tconst H = ctx.requested.height ?? ctx.canvas.height;\n\t\tconst safe = safeArea({ width: W, height: H }, props.safeArea);\n\t\tconst fontFamily = props.fontFamily ?? theme.font.display;\n\t\tconst nameSize = props.nameSize ?? 44 * s;\n\t\tconst roleSize = nameSize * 0.62;\n\t\tconst accent = props.accent ?? theme.color.accent;\n\t\tconst plateColor = props.plate ?? theme.color.surface;\n\t\tconst right = props.side === \"right\";\n\t\tconst base = themeFrames(theme, \"base\", fps);\n\t\tconst fast = themeFrames(theme, \"fast\", fps);\n\t\tconst enter = theme.motion.ease.enter;\n\n\t\tconst nameM = host.measure(props.name, {\n\t\t\tfontFamily,\n\t\t\tfontSize: nameSize,\n\t\t\tfontWeight: theme.weight.bold,\n\t\t});\n\t\tconst roleM = props.role\n\t\t\t? host.measure(props.role, {\n\t\t\t\t\tfontFamily,\n\t\t\t\t\tfontSize: roleSize,\n\t\t\t\t\tfontWeight: theme.weight.medium,\n\t\t\t\t})\n\t\t\t: null;\n\t\tconst padX = nameSize * 0.55;\n\t\tconst padY = nameSize * 0.36;\n\t\tconst children: LayoutNode[] = [];\n\t\tconst parts: Record<string, Animatable<LayoutNode>> = {};\n\t\t/** Keys per node and prop, merged into one track each at the end. */\n\t\tconst book = new KeyBook<Animatable<LayoutNode>>();\n\t\tconst key = book.key.bind(book);\n\n\t\tconst text = (\n\t\t\tid: string,\n\t\t\tcontent: string,\n\t\t\tsize: number,\n\t\t\tweight: number,\n\t\t\tfill: string,\n\t\t\tx: number,\n\t\t\ty: number,\n\t\t) =>\n\t\t\thost.text(content, {\n\t\t\t\tid: ctx.partId(id),\n\t\t\t\tposition: \"absolute\",\n\t\t\t\tx,\n\t\t\t\ty,\n\t\t\t\tfontFamily,\n\t\t\t\tfontSize: size,\n\t\t\t\tfontWeight: weight,\n\t\t\t\tfill,\n\t\t\t});\n\n\t\t/**\n\t\t * A plate that slides in through its own box: the background travels\n\t\t * in from the side, its text rises a beat later. Returns the timing.\n\t\t */\n\t\tconst plate = (\n\t\t\tid: string,\n\t\t\trect: Rect,\n\t\t\tbackground: string,\n\t\t\tradius: number,\n\t\t\tcontent: Array<Animatable<LayoutNode>>,\n\t\t\tat: number,\n\t\t) => {\n\t\t\tconst fill = host.box({\n\t\t\t\tid: ctx.partId(`${id}-fill`),\n\t\t\t\tposition: \"absolute\",\n\t\t\t\tx: 0,\n\t\t\t\ty: 0,\n\t\t\t\twidth: rect.width,\n\t\t\t\theight: rect.height,\n\t\t\t\tbackground: surfaceFill(background, theme),\n\t\t\t\tborderRadius: radius,\n\t\t\t\t// The slide is fast: a little smear sells it.\n\t\t\t\tmotionBlurShutter: 90,\n\t\t\t});\n\t\t\tconst travel = rect.width * (right ? 1 : -1);\n\t\t\tconst slide = Math.round(base * 1.1);\n\t\t\tkey(fill, \"x\", [\n\t\t\t\t[at, travel],\n\t\t\t\t[at + slide, 0, enter],\n\t\t\t]);\n\t\t\tconst textAt = at + Math.round(fast * 0.7);\n\t\t\tfor (const node of content) {\n\t\t\t\tconst y0 = (node as unknown as { y: number }).y;\n\t\t\t\tkey(node, \"y\", [\n\t\t\t\t\t[textAt, y0 + rect.height * 0.5],\n\t\t\t\t\t[textAt + base, y0, enter],\n\t\t\t\t]);\n\t\t\t\tkey(node, \"opacity\", [\n\t\t\t\t\t[textAt, 0],\n\t\t\t\t\t[textAt + Math.round(base * 0.6), 1, \"power2.out\"],\n\t\t\t\t]);\n\t\t\t}\n\t\t\tconst clip = host.box({\n\t\t\t\tid: ctx.partId(id),\n\t\t\t\tposition: \"absolute\",\n\t\t\t\t...rect,\n\t\t\t\toverflow: \"hidden\",\n\t\t\t\tborderRadius: radius,\n\t\t\t\tchildren: [fill, ...content],\n\t\t\t});\n\t\t\t// A clear plate (the `line` layout) has nothing to lift.\n\t\t\tif (parseColor(background)?.[3] !== 0) elevate(host, clip, theme, \"low\");\n\t\t\tchildren.push(clip);\n\t\t\tparts[id] = clip;\n\t\t\treturn {\n\t\t\t\tclip,\n\t\t\t\tfill,\n\t\t\t\tcontent,\n\t\t\t\ttravel,\n\t\t\t\tsettled: Math.max(at + slide, textAt + base),\n\t\t\t};\n\t\t};\n\n\t\t// ── Layouts ───────────────────────────────────────────────────────\n\t\tconst bottom = safe.y + safe.height;\n\t\tconst xFor = (width: number) =>\n\t\t\tright ? safe.x + safe.width - width : safe.x;\n\t\tconst plates: Array<ReturnType<typeof plate>> = [];\n\t\tlet accentLine: Animatable<LayoutNode> | null = null;\n\t\tconst lineW = Math.max(3, nameSize * 0.09);\n\n\t\tswitch (props.layout) {\n\t\t\tcase \"bar\": {\n\t\t\t\tconst edge = Math.max(4, nameSize * 0.14);\n\t\t\t\tconst innerW = Math.max(nameM.width, roleM?.width ?? 0);\n\t\t\t\tconst h = padY * 2 + nameM.height + (roleM ? roleM.height * 0.95 : 0);\n\t\t\t\tconst w = innerW + padX * 2 + edge;\n\t\t\t\tconst rect = { x: xFor(w), y: bottom - h, width: w, height: h };\n\t\t\t\tconst tx = right ? padX : padX + edge;\n\t\t\t\tconst content = [\n\t\t\t\t\ttext(\n\t\t\t\t\t\t\"name\",\n\t\t\t\t\t\tprops.name,\n\t\t\t\t\t\tnameSize,\n\t\t\t\t\t\ttheme.weight.bold,\n\t\t\t\t\t\ttheme.color.ink,\n\t\t\t\t\t\ttx,\n\t\t\t\t\t\tpadY,\n\t\t\t\t\t),\n\t\t\t\t];\n\t\t\t\tif (props.role && roleM)\n\t\t\t\t\tcontent.push(\n\t\t\t\t\t\ttext(\n\t\t\t\t\t\t\t\"role\",\n\t\t\t\t\t\t\tprops.role,\n\t\t\t\t\t\t\troleSize,\n\t\t\t\t\t\t\ttheme.weight.medium,\n\t\t\t\t\t\t\ttheme.color.muted,\n\t\t\t\t\t\t\ttx,\n\t\t\t\t\t\t\tpadY + nameM.height * 0.95,\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\tconst p = plate(\"plate\", rect, plateColor, theme.radius.sm, content, 0);\n\t\t\t\tconst strip = host.box({\n\t\t\t\t\tid: ctx.partId(\"edge\"),\n\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\tx: right ? rect.x + rect.width - edge : rect.x,\n\t\t\t\t\ty: rect.y + rect.height,\n\t\t\t\t\twidth: edge,\n\t\t\t\t\theight: 0,\n\t\t\t\t\tbackground: accent,\n\t\t\t\t\tborderRadius: theme.radius.sm,\n\t\t\t\t});\n\t\t\t\tconst grow = Math.round(base * 0.8);\n\t\t\t\tkey(strip, \"height\", [\n\t\t\t\t\t[0, 0],\n\t\t\t\t\t[grow, rect.height, enter],\n\t\t\t\t]);\n\t\t\t\tkey(strip, \"y\", [\n\t\t\t\t\t[0, rect.y + rect.height],\n\t\t\t\t\t[grow, rect.y, enter],\n\t\t\t\t]);\n\t\t\t\tchildren.push(strip);\n\t\t\t\tparts.edge = strip;\n\t\t\t\tplates.push(p);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"stacked\": {\n\t\t\t\tconst nameW = nameM.width + padX * 2;\n\t\t\t\tconst nameH = nameM.height + padY * 2;\n\t\t\t\tconst roleH = roleM ? roleM.height + padY * 1.2 : 0;\n\t\t\t\tconst roleW = roleM ? roleM.width + padX * 1.6 : 0;\n\t\t\t\tconst inset = nameSize * 0.4;\n\t\t\t\tconst total = nameH + (roleM ? roleH * 0.9 : 0);\n\t\t\t\tconst top = bottom - total;\n\t\t\t\tplates.push(\n\t\t\t\t\tplate(\n\t\t\t\t\t\t\"name-plate\",\n\t\t\t\t\t\t{ x: xFor(nameW), y: top, width: nameW, height: nameH },\n\t\t\t\t\t\tplateColor,\n\t\t\t\t\t\ttheme.radius.sm,\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\ttext(\n\t\t\t\t\t\t\t\t\"name\",\n\t\t\t\t\t\t\t\tprops.name,\n\t\t\t\t\t\t\t\tnameSize,\n\t\t\t\t\t\t\t\ttheme.weight.bold,\n\t\t\t\t\t\t\t\ttheme.color.ink,\n\t\t\t\t\t\t\t\tpadX,\n\t\t\t\t\t\t\t\tpadY,\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t],\n\t\t\t\t\t\t0,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tif (props.role && roleM)\n\t\t\t\t\tplates.push(\n\t\t\t\t\t\tplate(\n\t\t\t\t\t\t\t\"role-plate\",\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tx: right ? xFor(roleW) - inset : xFor(roleW) + inset,\n\t\t\t\t\t\t\t\ty: top + nameH - roleH * 0.1,\n\t\t\t\t\t\t\t\twidth: roleW,\n\t\t\t\t\t\t\t\theight: roleH,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\taccent,\n\t\t\t\t\t\t\ttheme.radius.sm,\n\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\ttext(\n\t\t\t\t\t\t\t\t\t\"role\",\n\t\t\t\t\t\t\t\t\tprops.role,\n\t\t\t\t\t\t\t\t\troleSize,\n\t\t\t\t\t\t\t\t\ttheme.weight.semibold,\n\t\t\t\t\t\t\t\t\ttheme.color.bg,\n\t\t\t\t\t\t\t\t\tpadX * 0.8,\n\t\t\t\t\t\t\t\t\tpadY * 0.6,\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\tMath.round(fast * 0.8),\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"tag\": {\n\t\t\t\t// One line, one node: bold name, a dot, the role a size down.\n\t\t\t\t// Too long for the safe area, the whole pill scales down: text\n\t\t\t\t// width is linear in font size, so one ratio fits it exactly.\n\t\t\t\tconst sep = \" · \";\n\t\t\t\tconst sepW = props.role\n\t\t\t\t\t? host.measure(sep, { fontFamily, fontSize: roleSize }).width\n\t\t\t\t\t: 0;\n\t\t\t\tconst h0 = nameM.height + padY * 1.4;\n\t\t\t\tconst w0 = nameM.width + (roleM ? sepW + roleM.width : 0) + h0;\n\t\t\t\tconst k = Math.min(1, safe.width / w0);\n\t\t\t\tconst [h, w] = [h0 * k, w0 * k];\n\t\t\t\tconst line = host.text(\n\t\t\t\t\tprops.role ? `${props.name}${sep}${props.role}` : props.name,\n\t\t\t\t\t{\n\t\t\t\t\t\tid: ctx.partId(\"name\"),\n\t\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\t\tx: h / 2,\n\t\t\t\t\t\ty: padY * 0.7 * k,\n\t\t\t\t\t\tfontFamily,\n\t\t\t\t\t\tfontSize: nameSize * k,\n\t\t\t\t\t\tfill: theme.color.ink,\n\t\t\t\t\t\tspans: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\ttext: props.name,\n\t\t\t\t\t\t\t\tfontWeight: theme.weight.bold,\n\t\t\t\t\t\t\t\tfill: theme.color.ink,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t...(props.role\n\t\t\t\t\t\t\t\t? [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\ttext: `${sep}${props.role}`,\n\t\t\t\t\t\t\t\t\t\t\tfontSize: roleSize * k,\n\t\t\t\t\t\t\t\t\t\t\tfontWeight: theme.weight.medium,\n\t\t\t\t\t\t\t\t\t\t\tfill: theme.color.muted,\n\t\t\t\t\t\t\t\t\t\t\t// Centred on the name's capitals, not on its baseline: one\n\t\t\t\t\t\t\t\t\t\t\t// line in a pill reads as aligned when the caps share a middle.\n\t\t\t\t\t\t\t\t\t\t\tbaselineShift:\n\t\t\t\t\t\t\t\t\t\t\t\t-(nameSize - roleSize) * k * CAP_HEIGHT * 0.5,\n\t\t\t\t\t\t\t\t\t\t},\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],\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t\tplates.push(\n\t\t\t\t\tplate(\n\t\t\t\t\t\t\"tag\",\n\t\t\t\t\t\t{ x: xFor(w), y: bottom - h, width: w, height: h },\n\t\t\t\t\t\tplateColor,\n\t\t\t\t\t\ttheme.radius.pill,\n\t\t\t\t\t\t[line],\n\t\t\t\t\t\t0,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"line\": {\n\t\t\t\tconst innerW = Math.max(nameM.width, roleM?.width ?? 0);\n\t\t\t\tconst h = nameM.height + (roleM ? roleM.height : 0);\n\t\t\t\tconst gap = nameSize * 0.4;\n\t\t\t\tconst w = innerW + gap + lineW;\n\t\t\t\tconst x = xFor(w);\n\t\t\t\tconst lineX = right ? x + w - lineW : x;\n\t\t\t\tconst tx = right ? 0 : lineW + gap;\n\t\t\t\taccentLine = host.box({\n\t\t\t\t\tid: ctx.partId(\"line\"),\n\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\tx: lineX,\n\t\t\t\t\ty: bottom,\n\t\t\t\t\twidth: lineW,\n\t\t\t\t\theight: 0,\n\t\t\t\t\tbackground: accent,\n\t\t\t\t\tborderRadius: lineW / 2,\n\t\t\t\t});\n\t\t\t\tconst grow = Math.round(base * 1.1);\n\t\t\t\tkey(accentLine, \"height\", [\n\t\t\t\t\t[0, 0],\n\t\t\t\t\t[grow, h, enter],\n\t\t\t\t]);\n\t\t\t\tkey(accentLine, \"y\", [\n\t\t\t\t\t[0, bottom],\n\t\t\t\t\t[grow, bottom - h, enter],\n\t\t\t\t]);\n\t\t\t\tchildren.push(accentLine);\n\t\t\t\tparts.line = accentLine;\n\t\t\t\tconst content = [\n\t\t\t\t\ttext(\n\t\t\t\t\t\t\"name\",\n\t\t\t\t\t\tprops.name,\n\t\t\t\t\t\tnameSize,\n\t\t\t\t\t\ttheme.weight.bold,\n\t\t\t\t\t\ttheme.color.ink,\n\t\t\t\t\t\ttx,\n\t\t\t\t\t\t0,\n\t\t\t\t\t),\n\t\t\t\t];\n\t\t\t\tif (props.role && roleM)\n\t\t\t\t\tcontent.push(\n\t\t\t\t\t\ttext(\n\t\t\t\t\t\t\t\"role\",\n\t\t\t\t\t\t\tprops.role,\n\t\t\t\t\t\t\troleSize,\n\t\t\t\t\t\t\ttheme.weight.medium,\n\t\t\t\t\t\t\ttheme.color.muted,\n\t\t\t\t\t\t\ttx,\n\t\t\t\t\t\t\tnameM.height,\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t// No plate: the text rises through a clear box.\n\t\t\t\tplates.push(\n\t\t\t\t\tplate(\n\t\t\t\t\t\t\"text\",\n\t\t\t\t\t\t{ x, y: bottom - h, width: w, height: h },\n\t\t\t\t\t\t\"rgba(0,0,0,0)\",\n\t\t\t\t\t\t0,\n\t\t\t\t\t\tcontent,\n\t\t\t\t\t\tMath.round(fast * 0.6),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tconst settled = Math.max(...plates.map((p) => p.settled));\n\t\t// ── Out: text first, then the plates slide back the way they came ──\n\t\tlet afterHold = 0;\n\t\tconst outAt = settled + ctx.hold;\n\t\tif (!props.stay) {\n\t\t\tconst fadeOut = Math.round(fast * 0.9);\n\t\t\tconst slide = Math.round(base * 0.9);\n\t\t\tplates.forEach((p, k) => {\n\t\t\t\tconst at = outAt + k * Math.round(fast * 0.4);\n\t\t\t\tfor (const node of p.content)\n\t\t\t\t\tkey(node, \"opacity\", [\n\t\t\t\t\t\t[at, 1],\n\t\t\t\t\t\t[at + fadeOut, 0, \"power2.in\"],\n\t\t\t\t\t]);\n\t\t\t\tkey(p.fill, \"x\", [\n\t\t\t\t\t[at + fadeOut - 2, 0],\n\t\t\t\t\t[at + fadeOut - 2 + slide, p.travel, theme.motion.ease.exit],\n\t\t\t\t]);\n\t\t\t\tafterHold = Math.max(afterHold, at + fadeOut - 2 + slide - outAt);\n\t\t\t});\n\t\t\tfor (const node of [accentLine, parts.edge])\n\t\t\t\tif (node)\n\t\t\t\t\tkey(node, \"opacity\", [\n\t\t\t\t\t\t[outAt, 1],\n\t\t\t\t\t\t[outAt + afterHold, 0, \"power2.in\"],\n\t\t\t\t\t]);\n\t\t}\n\t\tbook.apply();\n\n\t\treturn {\n\t\t\tchildren,\n\t\t\twidth: W,\n\t\t\theight: H,\n\t\t\tduration: settled,\n\t\t\topenEnded: props.stay,\n\t\t\tafterHold,\n\t\t\torigin: [0, 0],\n\t\t\tcues: {\n\t\t\t\tin: [{ frame: settled }],\n\t\t\t\tout: props.stay ? [] : [{ frame: outAt }],\n\t\t\t} as LowerThirdCues,\n\t\t\tparts,\n\t\t};\n\t},\n});\n\n/**\n * The name-and-role bar of interviews and talks: measured plates in the safe\n * area that slide in and leave on their own.\n */\nexport const LowerThird = /* @__PURE__ */ bindComponent(lowerThird);\n"
32
32
  }
33
33
  ]
34
34
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "text-reveal",
3
3
  "title": "TextReveal",
4
- "summary": "Headlines and body copy that animate in and out by character, word or line: rise, blur, track, mask, slam, scatter, scramble, type, stream, wipe and flip, each ending on exactly the static text.",
4
+ "summary": "Headlines and body copy that animate in and out by character, word or line: rise, blur, mask, slam, scatter, scramble, type, stream, wipe and flip, each ending on exactly the static text.",
5
5
  "tags": [
6
6
  "text",
7
7
  "typography",
@@ -30,7 +30,7 @@
30
30
  "files": [
31
31
  {
32
32
  "path": "text-reveal/index.ts",
33
- "content": "/**\n * TextReveal: headlines and body copy that animate in (and out) by\n * character, word or line. One timing model, many looks. Glyph styles run on\n * the GPU through baked per-unit schedules, so a paragraph stays one text\n * node and its final frame is exactly the static text.\n */\nimport {\n\ttype AnimatableProp,\n\ttype KeyframeTuple,\n\tLayerAnimation,\n\ttype LayoutNode,\n\ttype TextNode,\n} from \"framefields\";\nimport {\n\ttype Animatable,\n\tbakeSamples,\n\tbindComponent,\n\tconstantExpression,\n\tdefineComponent,\n\tevaluateEase,\n\tfindRange,\n\ttype GlyphAnimator,\n\tglyphAnimator,\n\ttype GlyphProps,\n\ttype GlyphUnit,\n\tlineShift,\n\ttype MeasureOptions,\n\ttype Metrics,\n\trangeRects,\n\ttype Rect,\n\trendererChars,\n\tround as roundTo,\n\tscaleFrames,\n\tscheduleExpression,\n\ttype Theme,\n\tthemeFrames,\n\ttypingFrames,\n\ttype UnitSchedule,\n\tz,\n} from \"framefields/components\";\nimport {\n\tburstStarts,\n\tindexedStarts,\n\tscrambleSet,\n\tunitStarts,\n\tunitsOf,\n} from \"./plan.js\";\n\nexport const REVEAL_STYLES = [\n\t\"rise\",\n\t\"blur\",\n\t\"track\",\n\t\"mask\",\n\t\"slam\",\n\t\"scatter\",\n\t\"scramble\",\n\t\"type\",\n\t\"stream\",\n\t\"wipe\",\n\t\"flip\",\n] as const;\nexport type RevealStyle = (typeof REVEAL_STYLES)[number];\n\n/** Styles that can also play out (`exitStyle`). */\nexport const EXIT_STYLES = [\n\t\"rise\",\n\t\"blur\",\n\t\"track\",\n\t\"mask\",\n\t\"scatter\",\n\t\"type\",\n\t\"stream\",\n\t\"wipe\",\n\t\"flip\",\n] as const;\nexport type ExitStyle = (typeof EXIT_STYLES)[number];\n\nconst ORDERS = [\"forward\", \"reverse\", \"center\", \"edges\", \"random\"] as const;\n\ninterface StyleDefaults {\n\tby: GlyphUnit;\n\t/** Frames between units at 30 fps. */\n\tstagger: number;\n\tspeed: keyof Theme[\"motion\"][\"speed\"];\n\t/** Multiplier on the speed token. */\n\tlength: number;\n\tease: string;\n}\n\nconst STYLE: Record<RevealStyle, StyleDefaults> = {\n\trise: {\n\t\tby: \"word\",\n\t\tstagger: 3,\n\t\tspeed: \"base\",\n\t\tlength: 1.4,\n\t\tease: \"expo.out\",\n\t},\n\tblur: {\n\t\tby: \"char\",\n\t\tstagger: 1.2,\n\t\tspeed: \"slow\",\n\t\tlength: 1,\n\t\tease: \"power3.out\",\n\t},\n\ttrack: {\n\t\tby: \"line\",\n\t\tstagger: 0,\n\t\tspeed: \"slow\",\n\t\tlength: 1.5,\n\t\tease: \"expo.out\",\n\t},\n\tmask: {\n\t\tby: \"line\",\n\t\tstagger: 5,\n\t\tspeed: \"base\",\n\t\tlength: 1.5,\n\t\tease: \"expo.out\",\n\t},\n\tslam: { by: \"word\", stagger: 7, speed: \"fast\", length: 1, ease: \"power3.in\" },\n\tscatter: {\n\t\tby: \"char\",\n\t\tstagger: 0.8,\n\t\tspeed: \"slow\",\n\t\tlength: 1.2,\n\t\tease: \"expo.out\",\n\t},\n\tscramble: {\n\t\tby: \"char\",\n\t\tstagger: 1.5,\n\t\tspeed: \"base\",\n\t\tlength: 1,\n\t\tease: \"none\",\n\t},\n\ttype: { by: \"char\", stagger: 0, speed: \"fast\", length: 1, ease: \"none\" },\n\tstream: {\n\t\tby: \"word\",\n\t\tstagger: 6,\n\t\tspeed: \"slow\",\n\t\tlength: 1,\n\t\tease: \"power2.out\",\n\t},\n\twipe: {\n\t\tby: \"line\",\n\t\tstagger: 0,\n\t\tspeed: \"slow\",\n\t\tlength: 1.4,\n\t\tease: \"power2.inOut\",\n\t},\n\tflip: {\n\t\tby: \"char\",\n\t\tstagger: 1.5,\n\t\tspeed: \"base\",\n\t\tlength: 1.2,\n\t\tease: \"back.out(1.4)\",\n\t},\n};\n\n/** What each glyph style pushes at full weight (font-size units, degrees). */\nfunction glyphFrom(style: RevealStyle, fs: number, exit: boolean): GlyphProps {\n\tswitch (style) {\n\t\tcase \"rise\":\n\t\t\treturn { y: (exit ? -0.32 : 0.42) * fs, opacity: 0 };\n\t\tcase \"blur\":\n\t\t\treturn { blur: 0.16 * fs, scale: 1.18, opacity: 0 };\n\t\tcase \"flip\":\n\t\t\treturn { rotationX: exit ? -90 : 90, opacity: 0 };\n\t\tdefault:\n\t\t\treturn { opacity: 0 };\n\t}\n}\n\nexport interface TextRevealCues {\n\t[name: string]: ReadonlyArray<{ frame: number }>;\n\t/** Per unit: starts moving (`frame`) and comes to rest (`settled`). */\n\tunits: ReadonlyArray<{\n\t\tframe: number;\n\t\tsettled: number;\n\t\tindex: number;\n\t\ttext: string;\n\t}>;\n\t/** The whole text is at rest. */\n\trevealed: ReadonlyArray<{ frame: number }>;\n\t/** The exit starts (empty without `exitStyle`). */\n\texitStart: ReadonlyArray<{ frame: number }>;\n\t/** Each typed character (`type`) or arriving word (`stream`): for key sounds. */\n\tkeystrokes: ReadonlyArray<{ frame: number; index: number }>;\n}\n\nexport interface TextRevealApi {\n\t/**\n\t * Rects (parent coordinates) that a match covers, one per line: hand\n\t * them to `Annotate` or a camera. Without `match`, the whole text.\n\t */\n\trects(match?: string | RegExp, nth?: number): Rect[];\n}\n\ntype TextProps = Partial<\n\tPick<\n\t\tTextNode,\n\t\t| \"fontFamily\"\n\t\t| \"fontSize\"\n\t\t| \"fontWeight\"\n\t\t| \"fill\"\n\t\t| \"align\"\n\t\t| \"lineHeight\"\n\t\t| \"letterSpacing\"\n\t\t| \"direction\"\n\t\t| \"width\"\n\t>\n>;\n\nconst ALIGN = { start: \"left\", center: \"center\", end: \"right\" } as const;\n\nexport const textReveal = /* @__PURE__ */ defineComponent({\n\tname: \"TextReveal\",\n\tprops: () => ({\n\t\ttext: z.string().min(1),\n\t\tstyle: z.enum(REVEAL_STYLES).default(\"rise\"),\n\t\t/** Unit of animation. Default per style. */\n\t\tby: z.enum([\"char\", \"word\", \"line\"]).optional(),\n\t\t/** Frames between units. Default per style. */\n\t\tstagger: z.number().min(0).optional(),\n\t\torder: z.enum(ORDERS).default(\"forward\"),\n\t\t/** Frames one unit's move takes. Default per style, from the theme. */\n\t\tunitDuration: z.number().int().positive().optional(),\n\t\tease: z.string().optional(),\n\t\t/** Play out after `hold`, unit by unit. */\n\t\texitStyle: z.union([z.enum(EXIT_STYLES), z.literal(false)]).default(false),\n\t\t/** Typing speed for `type`, characters a second. */\n\t\tcps: z.number().positive().default(24),\n\t\t/** Caret for `type` and `stream`. */\n\t\tcaret: z.boolean().default(true),\n\t\t/** Words or patterns drawn in the theme's accent. */\n\t\temphasis: z.array(z.union([z.string(), z.instanceof(RegExp)])).default([]),\n\t\taccent: z.string().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\tlineHeight: z.number().positive().optional(),\n\t\tletterSpacing: z.number().optional(),\n\t\tdirection: z.enum([\"ltr\", \"rtl\", \"auto\"]).optional(),\n\t}),\n\tdefaults: { enter: false, exit: false, hold: 30 },\n\tbuild(ctx, props) {\n\t\tconst { host, theme, fps } = ctx;\n\t\tconst text = props.text;\n\t\tconst fs = props.fontSize ?? 96 * theme.scale;\n\t\tconst fontFamily = props.fontFamily ?? theme.font.display;\n\t\tconst registered = host.fonts();\n\t\tif (registered.length > 0 && !registered.includes(fontFamily)) {\n\t\t\tthrow new Error(\n\t\t\t\t`TextReveal: font \"${fontFamily}\" is not registered. Registered: ${registered.join(\", \")}.`,\n\t\t\t);\n\t\t}\n\t\tconst wrap = ctx.requested.width;\n\t\tconst textProps: TextProps = {\n\t\t\tfontFamily,\n\t\t\tfontSize: fs,\n\t\t\tfill: props.fill ?? theme.color.ink,\n\t\t\talign: props.align,\n\t\t\t...(props.fontWeight !== undefined && { fontWeight: props.fontWeight }),\n\t\t\t...(props.lineHeight !== undefined && { lineHeight: props.lineHeight }),\n\t\t\t...(props.letterSpacing !== undefined && {\n\t\t\t\tletterSpacing: props.letterSpacing,\n\t\t\t}),\n\t\t\t...(props.direction !== undefined && { direction: props.direction }),\n\t\t\t...(wrap !== undefined && { width: wrap }),\n\t\t};\n\t\tconst measureOptions: MeasureOptions = {\n\t\t\tfontFamily,\n\t\t\tfontSize: fs,\n\t\t\tfontWeight: props.fontWeight,\n\t\t\tletterSpacing: props.letterSpacing,\n\t\t\tlineHeight: props.lineHeight,\n\t\t\twidth: wrap,\n\t\t\talign: ALIGN[props.align],\n\t\t\tdirection: props.direction,\n\t\t};\n\t\tconst m = host.measure(text, measureOptions);\n\t\tconst W = wrap ?? m.width;\n\t\tconst H = m.height;\n\n\t\tconst style = STYLE[props.style];\n\t\tconst by: GlyphUnit = props.by ?? style.by;\n\t\t// Stagger stays fractional: units may start between frames.\n\t\tconst perFps = (v: number) => (v * fps) / 30;\n\t\tconst stagger = perFps(props.stagger ?? style.stagger);\n\t\tconst duration =\n\t\t\tprops.unitDuration ??\n\t\t\tMath.max(\n\t\t\t\t1,\n\t\t\t\tMath.round(themeFrames(theme, style.speed, fps) * style.length),\n\t\t\t);\n\t\tconst ease = props.ease ?? style.ease;\n\n\t\tconst parts: Record<string, Animatable<LayoutNode>> = {};\n\t\tconst children: LayoutNode[] = [];\n\t\tconst animators: GlyphAnimator[] = [];\n\t\tconst keystrokes: Array<{ frame: number; index: number }> = [];\n\t\tlet unitCues: TextRevealCues[\"units\"] = [];\n\t\tlet revealed = 0;\n\n\t\t// Emphasis is static colour, first in the stack so streaming ink mixes over it.\n\t\tconst emphasised = emphasisWeights(text, props.emphasis);\n\t\tif (emphasised) {\n\t\t\tanimators.push(\n\t\t\t\tglyphAnimator(\n\t\t\t\t\t\"emphasis\",\n\t\t\t\t\tconstantExpression(\"char\", rendererChars(text, emphasised)),\n\t\t\t\t\t{\n\t\t\t\t\t\tfillColor: props.accent ?? theme.color.accent,\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\n\t\tconst units = unitsOf(text, by, m);\n\t\tconst schedule = (starts: number[], dur = duration, e = ease) => ({\n\t\t\tstarts: indexedStarts(text, by, units, starts),\n\t\t\tduration: dur,\n\t\t\tease: e,\n\t\t});\n\t\tconst cuesFor = (starts: number[], dur: number) =>\n\t\t\tunits.map((u, i) => ({\n\t\t\t\tframe: Math.round(starts[i] ?? 0),\n\t\t\t\tsettled: Math.round((starts[i] ?? 0) + dur),\n\t\t\t\tindex: u.index,\n\t\t\t\ttext: u.text,\n\t\t\t}));\n\n\t\t// Tracking widens a line: unwrapped text gets a box with room for it,\n\t\t// placed so `align` holds while it grows.\n\t\tconst tracks =\n\t\t\twrap === undefined &&\n\t\t\t(props.style === \"track\" || props.exitStyle === \"track\");\n\t\tconst room = tracks ? 0.5 * fs * Math.max(0, text.length - 1) : 0;\n\t\tconst shift =\n\t\t\tprops.align === \"center\" ? 0.5 : props.align === \"end\" ? 1 : 0;\n\t\tconst textNode = host.text(text, {\n\t\t\t...textProps,\n\t\t\t...(tracks && { width: W + room }),\n\t\t\tid: ctx.partId(\"text\"),\n\t\t\tposition: \"absolute\",\n\t\t\tx: -room * shift,\n\t\t\ty: 0,\n\t\t});\n\t\tparts.text = textNode;\n\t\t// What carries the glyphs: the text node, or a wrapper around it (null when lines are split).\n\t\tlet main: LayoutNode | null = textNode;\n\t\tlet lines: MaskLine[] = [];\n\t\tlet sweep: ReturnType<typeof wipeMatte> | null = null;\n\n\t\t// ── Enter ────────────────────────────────────────────────────────\n\t\tswitch (props.style) {\n\t\t\tcase \"rise\":\n\t\t\tcase \"blur\":\n\t\t\tcase \"flip\": {\n\t\t\t\tconst starts = unitStarts(units.length, stagger, props.order, ctx.seed);\n\t\t\t\tanimators.push(\n\t\t\t\t\tglyphAnimator(\n\t\t\t\t\t\t\"enter\",\n\t\t\t\t\t\tscheduleExpression(by, schedule(starts), \"enter\"),\n\t\t\t\t\t\tglyphFrom(props.style, fs, false),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tunitCues = cuesFor(starts, duration);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"slam\": {\n\t\t\t\t// Each unit is its own node so it scales around its own centre;\n\t\t\t\t// the paragraph takes over the frame the last one lands.\n\t\t\t\tconst starts = unitStarts(units.length, stagger, props.order, ctx.seed);\n\t\t\t\tunitCues = cuesFor(starts, duration);\n\t\t\t\tconst { width: _w, align: _a, ...unitProps } = textProps;\n\t\t\t\tconst pieces = units.map((u, i) => {\n\t\t\t\t\tconst rect = rangeRects(m, u.start, u.end, measureOptions)[0];\n\t\t\t\t\tconst s = starts[i] ?? 0;\n\t\t\t\t\tconst accented =\n\t\t\t\t\t\temphasised?.slice(u.start, u.end).every((v) => v === 1) ?? false;\n\t\t\t\t\tconst piece = host.text(u.text, {\n\t\t\t\t\t\t...unitProps,\n\t\t\t\t\t\t...(accented && { fill: props.accent ?? theme.color.accent }),\n\t\t\t\t\t\tid: ctx.partId(`unit-${i}`),\n\t\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\t\tx: rect?.x ?? 0,\n\t\t\t\t\t\ty: rect?.y ?? 0,\n\t\t\t\t\t});\n\t\t\t\t\tpiece.animate(\n\t\t\t\t\t\tLayerAnimation.create()\n\t\t\t\t\t\t\t.keys(\"scale\", [\n\t\t\t\t\t\t\t\t[s, 2.4],\n\t\t\t\t\t\t\t\t[s + duration, 1, ease],\n\t\t\t\t\t\t\t])\n\t\t\t\t\t\t\t.keys(\"opacity\", [\n\t\t\t\t\t\t\t\t[s, 0],\n\t\t\t\t\t\t\t\t[s + Math.max(1, Math.round(duration * 0.45)), 1],\n\t\t\t\t\t\t\t]),\n\t\t\t\t\t);\n\t\t\t\t\treturn piece;\n\t\t\t\t});\n\t\t\t\tconst landed = Math.max(0, ...unitCues.map((u) => u.settled));\n\t\t\t\tconst group = host.box({\n\t\t\t\t\tid: ctx.partId(\"units\"),\n\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\tx: 0,\n\t\t\t\t\ty: 0,\n\t\t\t\t\twidth: W,\n\t\t\t\t\theight: H,\n\t\t\t\t\tchildren: pieces,\n\t\t\t\t});\n\t\t\t\tgroup.animate(\n\t\t\t\t\tLayerAnimation.keys(\"opacity\", [\n\t\t\t\t\t\t[0, 1],\n\t\t\t\t\t\t[landed, 0, \"hold\"],\n\t\t\t\t\t]),\n\t\t\t\t);\n\t\t\t\ttextNode.animate(\n\t\t\t\t\tLayerAnimation.keys(\"opacity\", [\n\t\t\t\t\t\t[0, 0],\n\t\t\t\t\t\t[landed, 1, \"hold\"],\n\t\t\t\t\t]),\n\t\t\t\t);\n\t\t\t\t// A short landing shake as each unit hits.\n\t\t\t\tconst shake = host.box({\n\t\t\t\t\tid: ctx.partId(\"shake\"),\n\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\tx: 0,\n\t\t\t\t\ty: 0,\n\t\t\t\t\twidth: W,\n\t\t\t\t\theight: H,\n\t\t\t\t\tchildren: [group, textNode],\n\t\t\t\t});\n\t\t\t\tshake.animate(...slamShake(unitCues, fs, ctx.rng(\"slam\")));\n\t\t\t\tparts.units = group;\n\t\t\t\tparts.shake = shake;\n\t\t\t\tmain = shake;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"scatter\": {\n\t\t\t\tconst starts = unitStarts(units.length, stagger, props.order, ctx.seed);\n\t\t\t\tconst sched = schedule(starts);\n\t\t\t\tconst per = (key: string) => {\n\t\t\t\t\tconst q = ctx.rng(key);\n\t\t\t\t\treturn rendererChars(\n\t\t\t\t\t\ttext,\n\t\t\t\t\t\tArray.from({ length: text.length }, () => q.spread(1)),\n\t\t\t\t\t);\n\t\t\t\t};\n\t\t\t\tconst rx = per(\"scatter-x\");\n\t\t\t\tconst ry = per(\"scatter-y\");\n\t\t\t\tconst rr = per(\"scatter-r\");\n\t\t\t\tanimators.push(\n\t\t\t\t\tglyphAnimator(\"enter-x\", scheduleExpression(by, sched, \"enter\", rx), {\n\t\t\t\t\t\tx: 1.1 * fs,\n\t\t\t\t\t}),\n\t\t\t\t\tglyphAnimator(\"enter-y\", scheduleExpression(by, sched, \"enter\", ry), {\n\t\t\t\t\t\ty: 0.9 * fs,\n\t\t\t\t\t}),\n\t\t\t\t\tglyphAnimator(\"enter-r\", scheduleExpression(by, sched, \"enter\", rr), {\n\t\t\t\t\t\trotation: 120,\n\t\t\t\t\t}),\n\t\t\t\t\tglyphAnimator(\"enter\", scheduleExpression(by, sched, \"enter\"), {\n\t\t\t\t\t\topacity: 0,\n\t\t\t\t\t\tblur: 0.06 * fs,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t\tunitCues = cuesFor(starts, duration);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"stream\": {\n\t\t\t\tconst words = unitsOf(text, \"word\", m);\n\t\t\t\tconst step = scaleFrames(1, fps);\n\t\t\t\tconst starts = burstStarts(\n\t\t\t\t\twords.length,\n\t\t\t\t\tperFps(props.stagger ?? style.stagger),\n\t\t\t\t\tstep,\n\t\t\t\t\tctx.rng(\"stream\"),\n\t\t\t\t);\n\t\t\t\tconst fade = Math.max(2, themeFrames(theme, \"fast\", fps));\n\t\t\t\tconst ink = props.unitDuration ?? themeFrames(theme, \"slow\", fps);\n\t\t\t\tanimators.push(\n\t\t\t\t\tglyphAnimator(\n\t\t\t\t\t\t\"enter\",\n\t\t\t\t\t\tscheduleExpression(\n\t\t\t\t\t\t\t\"word\",\n\t\t\t\t\t\t\t{ starts, duration: fade, ease: \"power2.out\" },\n\t\t\t\t\t\t\t\"enter\",\n\t\t\t\t\t\t),\n\t\t\t\t\t\t{ opacity: 0, y: 0.08 * fs },\n\t\t\t\t\t),\n\t\t\t\t\tglyphAnimator(\n\t\t\t\t\t\t\"ink\",\n\t\t\t\t\t\tscheduleExpression(\n\t\t\t\t\t\t\t\"word\",\n\t\t\t\t\t\t\t{ starts, duration: ink, ease: \"sine.inOut\" },\n\t\t\t\t\t\t\t\"enter\",\n\t\t\t\t\t\t),\n\t\t\t\t\t\t{ fillColor: theme.color.muted },\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tunitCues = words.map((w, i) => ({\n\t\t\t\t\tframe: starts[i] ?? 0,\n\t\t\t\t\tsettled: (starts[i] ?? 0) + ink,\n\t\t\t\t\tindex: w.index,\n\t\t\t\t\ttext: w.text,\n\t\t\t\t}));\n\t\t\t\twords.forEach((w, i) => {\n\t\t\t\t\tkeystrokes.push({ frame: starts[i] ?? 0, index: w.index });\n\t\t\t\t});\n\t\t\t\tif (props.caret) {\n\t\t\t\t\tconst ends = words.map((w) => w.end);\n\t\t\t\t\tconst caret = buildCaret(ctx, m, measureOptions, fs, {\n\t\t\t\t\t\tat: starts,\n\t\t\t\t\t\tcounts: ends,\n\t\t\t\t\t\tuntil: (starts[starts.length - 1] ?? 0) + ink,\n\t\t\t\t\t\tshape: \"block\",\n\t\t\t\t\t});\n\t\t\t\t\tchildren.push(caret);\n\t\t\t\t\tparts.caret = caret;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"type\": {\n\t\t\t\tconst frames = typingFrames(text, props.cps, fps, ctx.rng(\"type\"));\n\t\t\t\ttextNode.animate(\n\t\t\t\t\tLayerAnimation.keys(\n\t\t\t\t\t\t\"text\" as AnimatableProp,\n\t\t\t\t\t\tcountKeys(text.length, frames),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tframes.forEach((f, i) => {\n\t\t\t\t\tif (!/\\s/.test(text[i] ?? \"\"))\n\t\t\t\t\t\tkeystrokes.push({ frame: f, index: i });\n\t\t\t\t});\n\t\t\t\tunitCues = keystrokes.map((k) => ({\n\t\t\t\t\tframe: k.frame,\n\t\t\t\t\tsettled: k.frame,\n\t\t\t\t\tindex: k.index,\n\t\t\t\t\ttext: text[k.index] ?? \"\",\n\t\t\t\t}));\n\t\t\t\trevealed = (frames[frames.length - 1] ?? 0) + 1;\n\t\t\t\tif (props.caret) {\n\t\t\t\t\tconst caret = buildCaret(ctx, m, measureOptions, fs, {\n\t\t\t\t\t\tat: frames,\n\t\t\t\t\t\tcounts: frames.map((_, i) => i + 1),\n\t\t\t\t\t\tuntil: revealed + ctx.hold,\n\t\t\t\t\t\tshape: \"bar\",\n\t\t\t\t\t});\n\t\t\t\t\tchildren.push(caret);\n\t\t\t\t\tparts.caret = caret;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"track\": {\n\t\t\t\tconst ls = props.letterSpacing ?? 0;\n\t\t\t\tconst wide = ls + 0.5 * fs;\n\t\t\t\tconst fade = Math.round(duration * 0.5);\n\t\t\t\tconst keys: KeyframeTuple[] = [\n\t\t\t\t\t[0, wide],\n\t\t\t\t\t[duration, ls, ease],\n\t\t\t\t];\n\t\t\t\tconst anim = LayerAnimation.create()\n\t\t\t\t\t.keys(\"letterSpacing\", keys)\n\t\t\t\t\t.keys(\"opacity\", [\n\t\t\t\t\t\t[0, 0],\n\t\t\t\t\t\t[fade, 1, \"power2.out\"],\n\t\t\t\t\t]);\n\t\t\t\ttextNode.animate(anim);\n\t\t\t\tunitCues = [{ frame: 0, settled: duration, index: 0, text }];\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"mask\": {\n\t\t\t\tlines = maskLines(ctx, text, m, measureOptions, textProps, fs);\n\t\t\t\tconst starts = unitStarts(lines.length, stagger, props.order, ctx.seed);\n\t\t\t\tlines.forEach((line, i) => {\n\t\t\t\t\tconst s = starts[i] ?? 0;\n\t\t\t\t\tline.text.animate(\n\t\t\t\t\t\tLayerAnimation.keys(\"y\", [\n\t\t\t\t\t\t\t[s, line.restY + line.travel],\n\t\t\t\t\t\t\t[s + duration, line.restY, ease],\n\t\t\t\t\t\t]),\n\t\t\t\t\t);\n\t\t\t\t\tchildren.push(line.clip);\n\t\t\t\t\tparts[`line-${i}`] = line.clip;\n\t\t\t\t});\n\t\t\t\tunitCues = lines.map((l, i) => ({\n\t\t\t\t\tframe: Math.round(starts[i] ?? 0),\n\t\t\t\t\tsettled: Math.round((starts[i] ?? 0) + duration),\n\t\t\t\t\tindex: i,\n\t\t\t\t\ttext: l.content,\n\t\t\t\t}));\n\t\t\t\tmain = null;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"wipe\": {\n\t\t\t\tconst w = wipeMatte(ctx, W, H, fs, \"hidden\");\n\t\t\t\tsweep = w;\n\t\t\t\tw.matte.animate(\n\t\t\t\t\tLayerAnimation.keys(\"x\", [\n\t\t\t\t\t\t[0, w.hidden],\n\t\t\t\t\t\t[duration, w.shown, ease],\n\t\t\t\t\t]),\n\t\t\t\t);\n\t\t\t\thost.matte(textNode, w.matte);\n\t\t\t\tparts.matte = w.matte;\n\t\t\t\t// The front trails a little blur over the glyphs it is uncovering.\n\t\t\t\tconst xs = rendererChars(text, glyphCenters(text, m, measureOptions));\n\t\t\t\tconst front = Array.from({ length: duration + 1 }, (_, f) =>\n\t\t\t\t\tw.front(\n\t\t\t\t\t\tw.hidden + (w.shown - w.hidden) * evaluateEase(ease, f / duration),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tanimators.push(\n\t\t\t\t\tglyphAnimator(\n\t\t\t\t\t\t\"wipe-blur\",\n\t\t\t\t\t\t`((X,T)=>{const f=clamp(frame,0,${duration});const k=floor(f);const front=T[k]+((T[k+1]??T[k])-T[k])*(f-k);return clamp(1-(front-(X[textIndex]??0))/${round(w.feather)},0,1);})(${list(xs)},${list(front)})`,\n\t\t\t\t\t\t{ blur: 0.12 * fs },\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tunitCues = [{ frame: 0, settled: duration, index: 0, text }];\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"scramble\": {\n\t\t\t\tconst result = scramble(ctx, text, m, measureOptions, textProps, {\n\t\t\t\t\tstagger,\n\t\t\t\t\tcycle: duration,\n\t\t\t\t\torder: props.order,\n\t\t\t\t});\n\t\t\t\tchildren.push(result.reels);\n\t\t\t\tparts.reels = result.reels;\n\t\t\t\t// The real text takes over the moment the last glyph locks.\n\t\t\t\ttextNode.animate(\n\t\t\t\t\tLayerAnimation.keys(\"opacity\", [\n\t\t\t\t\t\t[0, 0],\n\t\t\t\t\t\t[result.done, 1, \"hold\"],\n\t\t\t\t\t]),\n\t\t\t\t);\n\t\t\t\tunitCues = result.cues;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tif (unitCues.length > 0 && props.style !== \"type\")\n\t\t\trevealed = Math.max(...unitCues.map((u) => u.settled));\n\t\t// The text is at rest only once the last landing shake dies out.\n\t\tif (props.style === \"slam\") revealed += SHAKE.length - 1;\n\n\t\t// ── Exit ─────────────────────────────────────────────────────────\n\t\tlet afterHold = 0;\n\t\tconst exitStart = revealed + ctx.hold;\n\t\tif (props.exitStyle !== false) {\n\t\t\tconst out = props.exitStyle;\n\t\t\tconst outStyle = STYLE[out];\n\t\t\tconst outBy: GlyphUnit = props.by ?? outStyle.by;\n\t\t\tconst outUnits = unitsOf(text, outBy, m);\n\t\t\tconst outStagger = perFps((props.stagger ?? outStyle.stagger) * 0.5);\n\t\t\tconst outDur = Math.max(\n\t\t\t\t1,\n\t\t\t\tMath.round(\n\t\t\t\t\tthemeFrames(theme, outStyle.speed, fps) * outStyle.length * 0.75,\n\t\t\t\t),\n\t\t\t);\n\t\t\tconst outEase = theme.motion.ease.exit;\n\t\t\tconst starts = unitStarts(\n\t\t\t\toutUnits.length,\n\t\t\t\toutStagger,\n\t\t\t\tprops.order,\n\t\t\t\tctx.seed,\n\t\t\t\texitStart,\n\t\t\t);\n\t\t\tconst sched: UnitSchedule = {\n\t\t\t\tstarts: indexedStarts(text, outBy, outUnits, starts),\n\t\t\t\tduration: outDur,\n\t\t\t\tease: outEase,\n\t\t\t};\n\t\t\tafterHold = Math.round(Math.max(...starts) - exitStart + outDur);\n\t\t\tswitch (out) {\n\t\t\t\tcase \"rise\":\n\t\t\t\tcase \"blur\":\n\t\t\t\tcase \"flip\":\n\t\t\t\tcase \"scatter\":\n\t\t\t\tcase \"stream\":\n\t\t\t\t\tanimators.push(\n\t\t\t\t\t\tglyphAnimator(\n\t\t\t\t\t\t\t\"exit\",\n\t\t\t\t\t\t\tscheduleExpression(outBy, sched, \"exit\"),\n\t\t\t\t\t\t\tout === \"scatter\"\n\t\t\t\t\t\t\t\t? { y: -0.6 * fs, opacity: 0, blur: 0.06 * fs }\n\t\t\t\t\t\t\t\t: glyphFrom(out, fs, true),\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"type\": {\n\t\t\t\t\t// Backspace, twice as fast as typing.\n\t\t\t\t\tconst per = Math.max(1, Math.round(fps / (props.cps * 2)));\n\t\t\t\t\tconst keys: KeyframeTuple[] = [];\n\t\t\t\t\tfor (\n\t\t\t\t\t\tlet c = text.length - 1, f = exitStart + per;\n\t\t\t\t\t\tc >= 0;\n\t\t\t\t\t\tc--, f += per\n\t\t\t\t\t)\n\t\t\t\t\t\tkeys.push([f, c === 1 ? 1 / text.length : c, \"hold\"]);\n\t\t\t\t\ttextNode.animate(LayerAnimation.keys(\"text\" as AnimatableProp, keys));\n\t\t\t\t\tafterHold = per * text.length + 1;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase \"track\": {\n\t\t\t\t\tconst ls = props.letterSpacing ?? 0;\n\t\t\t\t\ttextNode.animate(\n\t\t\t\t\t\tLayerAnimation.create()\n\t\t\t\t\t\t\t.keys(\"letterSpacing\", [\n\t\t\t\t\t\t\t\t[exitStart, ls],\n\t\t\t\t\t\t\t\t[exitStart + outDur, ls + 0.4 * fs, outEase],\n\t\t\t\t\t\t\t])\n\t\t\t\t\t\t\t.keys(\"opacity\", [\n\t\t\t\t\t\t\t\t[exitStart, 1],\n\t\t\t\t\t\t\t\t[exitStart + outDur, 0, outEase],\n\t\t\t\t\t\t\t]),\n\t\t\t\t\t);\n\t\t\t\t\tafterHold = outDur;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase \"wipe\": {\n\t\t\t\t\tif (!sweep) {\n\t\t\t\t\t\tsweep = wipeMatte(ctx, W, H, fs, \"shown\");\n\t\t\t\t\t\thost.matte(textNode, sweep.matte);\n\t\t\t\t\t\tparts.matte = sweep.matte;\n\t\t\t\t\t}\n\t\t\t\t\tsweep.matte.animate(\n\t\t\t\t\t\tLayerAnimation.keys(\"x\", [\n\t\t\t\t\t\t\t[exitStart, sweep.shown],\n\t\t\t\t\t\t\t[exitStart + outDur, sweep.gone, outEase],\n\t\t\t\t\t\t]),\n\t\t\t\t\t);\n\t\t\t\t\tafterHold = outDur;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase \"mask\": {\n\t\t\t\t\tif (lines.length === 0)\n\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t'TextReveal: exitStyle \"mask\" needs style \"mask\" (it splits the lines).',\n\t\t\t\t\t\t);\n\t\t\t\t\tconst outStarts = unitStarts(\n\t\t\t\t\t\tlines.length,\n\t\t\t\t\t\toutStagger,\n\t\t\t\t\t\tprops.order,\n\t\t\t\t\t\tctx.seed,\n\t\t\t\t\t\texitStart,\n\t\t\t\t\t);\n\t\t\t\t\tlines.forEach((line, i) => {\n\t\t\t\t\t\tconst s = outStarts[i] ?? exitStart;\n\t\t\t\t\t\tline.text.animate(\n\t\t\t\t\t\t\tLayerAnimation.keys(\"y\", [\n\t\t\t\t\t\t\t\t[s, line.restY],\n\t\t\t\t\t\t\t\t[s + outDur, line.restY - line.travel, outEase],\n\t\t\t\t\t\t\t]),\n\t\t\t\t\t\t);\n\t\t\t\t\t});\n\t\t\t\t\tafterHold = Math.round(Math.max(...outStarts) - exitStart + outDur);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (animators.length > 0)\n\t\t\t(textNode as unknown as { animators: GlyphAnimator[] }).animators =\n\t\t\t\tanimators;\n\t\tif (main) children.unshift(main);\n\n\t\treturn {\n\t\t\tchildren,\n\t\t\twidth: W,\n\t\t\theight: H,\n\t\t\tduration: Math.max(1, revealed),\n\t\t\topenEnded: true,\n\t\t\tafterHold,\n\t\t\tcues: {\n\t\t\t\tunits: unitCues,\n\t\t\t\trevealed: [{ frame: revealed }],\n\t\t\t\texitStart: props.exitStyle === false ? [] : [{ frame: exitStart }],\n\t\t\t\tkeystrokes,\n\t\t\t} as TextRevealCues,\n\t\t\tparts,\n\t\t\tapi: (placement): TextRevealApi => ({\n\t\t\t\trects(match, nth = 0) {\n\t\t\t\t\tconst range: [number, number] | null =\n\t\t\t\t\t\tmatch === undefined\n\t\t\t\t\t\t\t? [0, text.length]\n\t\t\t\t\t\t\t: findRange(text, match, nth);\n\t\t\t\t\tif (!range)\n\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t`TextReveal.rects: \"${String(match)}\" not found in \"${text}\".`,\n\t\t\t\t\t\t);\n\t\t\t\t\treturn rangeRects(m, range[0], range[1], measureOptions).map((q) => ({\n\t\t\t\t\t\t...q,\n\t\t\t\t\t\tx: q.x + placement.x,\n\t\t\t\t\t\ty: q.y + placement.y,\n\t\t\t\t\t}));\n\t\t\t\t},\n\t\t\t}),\n\t\t};\n\t},\n});\n\n// ── Helpers ──────────────────────────────────────────────────────────\n\nconst round = (v: number) => roundTo(v, 4);\nconst list = (values: readonly number[]) => `[${values.map(round).join(\",\")}]`;\n\n/** 1 for characters inside an emphasis match, else 0 (null when nothing matches). */\nfunction emphasisWeights(\n\ttext: string,\n\tpatterns: ReadonlyArray<string | RegExp>,\n): number[] | null {\n\tif (patterns.length === 0) return null;\n\tconst w = new Array<number>(text.length).fill(0);\n\tfor (const p of patterns) {\n\t\tfor (let nth = 0; ; nth++) {\n\t\t\tconst range = findRange(text, p, nth);\n\t\t\tif (!range || range[1] <= range[0]) break;\n\t\t\tfor (let i = range[0]; i < range[1]; i++) w[i] = 1;\n\t\t}\n\t}\n\treturn w.some((v) => v > 0) ? w : null;\n}\n\n/** Text-reveal count keys: ≤ 1 is a fraction, > 1 a character count. */\nfunction countKeys(length: number, frames: readonly number[]): KeyframeTuple[] {\n\tconst keys: KeyframeTuple[] = [[0, 0]];\n\tframes.forEach((f, i) => {\n\t\tconst c = i + 1;\n\t\tkeys.push([f, c === 1 ? 1 / length : c, \"hold\"]);\n\t});\n\treturn keys;\n}\n\n/** Landing shake, per frame after a unit lands (× 0.045 em). */\nconst SHAKE = [0, 1, -0.55, 0.3, -0.12, 0];\n\nfunction slamShake(\n\tunits: ReadonlyArray<{ settled: number }>,\n\tfs: number,\n\tr: { spread(n: number): number },\n): LayerAnimation[] {\n\tconst end = Math.max(0, ...units.map((u) => u.settled)) + 8;\n\tconst xs = new Array<number>(end + 1).fill(0);\n\tconst ys = new Array<number>(end + 1).fill(0);\n\tconst a = 0.045 * fs;\n\tconst profile = SHAKE;\n\tfor (const u of units) {\n\t\tconst dx = r.spread(1);\n\t\tprofile.forEach((p, k) => {\n\t\t\tconst f = u.settled + k;\n\t\t\tif (f <= end) {\n\t\t\t\txs[f] = (xs[f] ?? 0) + a * p * dx;\n\t\t\t\tys[f] = (ys[f] ?? 0) + a * p * 0.6;\n\t\t\t}\n\t\t});\n\t}\n\treturn [\n\t\t// Tight tolerance: the text must come to rest exactly where it belongs.\n\t\tLayerAnimation.keys(\"x\", bakeSamples(xs, 0, { tolerance: 0.005 })),\n\t\tLayerAnimation.keys(\"y\", bakeSamples(ys, 0, { tolerance: 0.005 })),\n\t];\n}\n\ninterface Ctx {\n\thost: import(\"framefields/components\").Host;\n\ttheme: Theme;\n\tfps: number;\n\tseed: number;\n\tpartId(part: string): string;\n\trng(key: string | number): import(\"framefields/components\").Rng;\n}\n\n/** Where the caret sits after `count` characters: x, and the line's top. */\nfunction caretAt(\n\tcount: number,\n\tm: Metrics,\n\toptions: MeasureOptions,\n): [number, number] {\n\tlet last: Metrics[\"glyphs\"][number] | undefined;\n\tfor (const g of m.glyphs)\n\t\tif (g.index < count && (!last || g.index > last.index)) last = g;\n\tif (!last) return [lineShift(m, 0, options), 0];\n\tconst shift = lineShift(m, last.line, options);\n\treturn [\n\t\tshift + last.x + last.width - (options.letterSpacing ?? 0),\n\t\tlast.line * m.lineHeight,\n\t];\n}\n\nfunction buildCaret(\n\tctx: Ctx,\n\tm: Metrics,\n\toptions: MeasureOptions,\n\tfs: number,\n\to: {\n\t\tat: readonly number[];\n\t\tcounts: readonly number[];\n\t\tuntil: number;\n\t\tshape: \"bar\" | \"block\";\n\t},\n): Animatable<LayoutNode> {\n\tconst w = o.shape === \"bar\" ? Math.max(2, 0.06 * fs) : 0.42 * fs;\n\tconst h = o.shape === \"bar\" ? 1.05 * fs : 0.42 * fs;\n\tconst dy =\n\t\to.shape === \"bar\" ? (m.lineHeight - h) / 2 : m.lineHeight * 0.52 - h / 2;\n\tconst gap = o.shape === \"bar\" ? 0.04 * fs : 0.18 * fs;\n\tconst [x0, y0] = caretAt(0, m, options);\n\tconst xs: KeyframeTuple[] = [[0, x0 + (o.shape === \"bar\" ? 0 : -w)]];\n\tconst ys: KeyframeTuple[] = [[0, y0 + dy]];\n\to.at.forEach((f, i) => {\n\t\tconst [x, y] = caretAt(o.counts[i] ?? 0, m, options);\n\t\txs.push([f, x + gap, \"hold\"]);\n\t\tys.push([f, y + dy, \"hold\"]);\n\t});\n\tconst caret = ctx.host.box({\n\t\tid: ctx.partId(\"caret\"),\n\t\tposition: \"absolute\",\n\t\tx: x0,\n\t\ty: y0 + dy,\n\t\twidth: w,\n\t\theight: h,\n\t\tbackground: ctx.theme.color.accent,\n\t\tborderRadius: o.shape === \"bar\" ? w / 2 : w * 0.18,\n\t});\n\t// Solid while text arrives, then a few blinks, then gone.\n\tconst last = o.at[o.at.length - 1] ?? 0;\n\tconst half = Math.max(1, Math.round(ctx.fps * 0.5));\n\tconst blink: KeyframeTuple[] = [[0, 1]];\n\tlet on = false;\n\tfor (let f = last + half; f < o.until; f += half, on = !on)\n\t\tblink.push([f, on ? 1 : 0, \"hold\"]);\n\tblink.push([Math.max(last + 1, o.until), 0, \"hold\"]);\n\tcaret.animate(\n\t\tLayerAnimation.create().keys(\"x\", xs).keys(\"y\", ys).keys(\"opacity\", blink),\n\t);\n\treturn caret;\n}\n\n/** Centre x of every character (spaces included), wrapped and aligned. */\nfunction glyphCenters(\n\ttext: string,\n\tm: Metrics,\n\toptions: MeasureOptions,\n): number[] {\n\tconst xs = new Array<number>(text.length).fill(0);\n\tfor (const g of m.glyphs)\n\t\txs[g.index] = lineShift(m, g.line, options) + g.x + g.width / 2;\n\treturn xs;\n}\n\n/**\n * A soft-edged matte for `wipe`: transparent → opaque → opaque → transparent,\n * `feather` px ramps. `hidden` / `shown` / `gone` are its x for each phase.\n */\nfunction wipeMatte(\n\tctx: Ctx,\n\tW: number,\n\tH: number,\n\tfs: number,\n\tinitial: \"hidden\" | \"shown\",\n) {\n\tconst feather = Math.max(24, 0.9 * fs);\n\tconst pad = fs * 0.5;\n\tconst total = W + feather * 2;\n\tconst a = ((feather / total) * 100).toFixed(3);\n\tconst b = (((feather + W) / total) * 100).toFixed(3);\n\tconst hidden = -total;\n\tconst shown = -feather;\n\tconst matte = ctx.host.box({\n\t\tid: ctx.partId(\"matte\"),\n\t\tposition: \"absolute\",\n\t\tx: initial === \"hidden\" ? hidden : shown,\n\t\ty: -pad,\n\t\twidth: total,\n\t\theight: H + pad * 2,\n\t\tbackground: `linear-gradient(90deg, rgba(255,255,255,0) 0%, #ffffff ${a}%, #ffffff ${b}%, rgba(255,255,255,0) 100%)`,\n\t});\n\treturn {\n\t\tmatte,\n\t\tfeather,\n\t\ttotal,\n\t\thidden,\n\t\tshown,\n\t\tgone: W + feather,\n\t\t/** Where visibility reaches zero, for a matte at `x`. */\n\t\tfront: (x: number) => x + total,\n\t};\n}\n\ninterface MaskLine {\n\tclip: Animatable<LayoutNode>;\n\ttext: Animatable<TextNode>;\n\tcontent: string;\n\trestY: number;\n\ttravel: number;\n}\n\n/** One clipped text node per measured line, each at its wrapped position. */\nfunction maskLines(\n\tctx: Ctx,\n\ttext: string,\n\tm: Metrics,\n\toptions: MeasureOptions,\n\ttextProps: TextProps,\n\tfs: number,\n): MaskLine[] {\n\tconst pad = Math.round(fs * 0.18);\n\tconst { width: _w, align: _a, ...lineProps } = textProps;\n\treturn unitsOf(text, \"line\", m).map((u, i) => {\n\t\tconst first = m.glyphs.find(\n\t\t\t(g) => g.index >= u.start && !/\\s/.test(g.char),\n\t\t);\n\t\tconst x = lineShift(m, i, options) + (first?.x ?? 0);\n\t\tconst content = text.slice(u.start, u.end).trim();\n\t\tconst lineText = ctx.host.text(content, {\n\t\t\t...lineProps,\n\t\t\tid: ctx.partId(`line-${i}-text`),\n\t\t\tposition: \"absolute\",\n\t\t\tx,\n\t\t\ty: pad,\n\t\t});\n\t\tconst clip = ctx.host.box({\n\t\t\tid: ctx.partId(`line-${i}`),\n\t\t\tposition: \"absolute\",\n\t\t\tx: 0,\n\t\t\ty: i * m.lineHeight - pad,\n\t\t\twidth: Math.max(m.width, options.width ?? 0) + fs,\n\t\t\theight: m.lineHeight + pad * 2,\n\t\t\toverflow: \"hidden\",\n\t\t\tchildren: [lineText],\n\t\t});\n\t\treturn {\n\t\t\tclip,\n\t\t\ttext: lineText,\n\t\t\tcontent,\n\t\t\trestY: pad,\n\t\t\ttravel: m.lineHeight + pad,\n\t\t};\n\t});\n}\n\n/**\n * Scramble: each character is a clipped vertical reel of seeded glyphs that\n * steps every couple of frames and lands on the real glyph. Reels are as\n * wide as the real glyph's advance, so the line never jitters.\n */\nfunction scramble(\n\tctx: Ctx,\n\ttext: string,\n\tm: Metrics,\n\toptions: MeasureOptions,\n\ttextProps: TextProps,\n\to: { stagger: number; cycle: number; order: (typeof ORDERS)[number] },\n) {\n\tconst units = unitsOf(text, \"char\", m);\n\tconst starts = unitStarts(units.length, o.stagger, o.order, ctx.seed);\n\tconst step = Math.max(1, scaleFrames(2, ctx.fps));\n\tconst { width: _w, align: _a, letterSpacing: _l, ...glyphProps } = textProps;\n\tconst reels: LayoutNode[] = [];\n\tconst cues: Array<{\n\t\tframe: number;\n\t\tsettled: number;\n\t\tindex: number;\n\t\ttext: string;\n\t}> = [];\n\tlet done = 0;\n\tunits.forEach((u, i) => {\n\t\tconst g = m.glyphs.find((q) => q.index === u.start);\n\t\tif (!g) return;\n\t\tconst appear = Math.round((starts[i] ?? 0) * 0.5);\n\t\tconst lock = Math.round((starts[i] ?? 0) + o.cycle);\n\t\tconst r = ctx.rng(`scramble:${u.start}`);\n\t\tconst set = scrambleSet(u.text);\n\t\tconst count = Math.max(1, Math.floor((lock - appear) / step));\n\t\tconst glyphs = Array.from({ length: count }, () => r.pick([...set]));\n\t\tglyphs.push(u.text);\n\t\tconst advance = g.width - (options.letterSpacing ?? 0);\n\t\tconst reel = ctx.host.text(glyphs.join(\"\\n\"), {\n\t\t\t...glyphProps,\n\t\t\tid: ctx.partId(`reel-${u.start}`),\n\t\t\tposition: \"absolute\",\n\t\t\tx: 0,\n\t\t\ty: 0,\n\t\t\twidth: advance,\n\t\t\talign: \"center\",\n\t\t});\n\t\tconst ys: KeyframeTuple[] = [[0, 0]];\n\t\tfor (let k = 1; k < count; k++)\n\t\t\tys.push([appear + k * step, -k * m.lineHeight, \"hold\"]);\n\t\tys.push([lock, -count * m.lineHeight, \"hold\"]);\n\t\treel.animate(\n\t\t\tLayerAnimation.create()\n\t\t\t\t.keys(\"y\", ys)\n\t\t\t\t.keys(\"opacity\", [\n\t\t\t\t\t[appear, 0],\n\t\t\t\t\t[appear + 3, 1],\n\t\t\t\t]),\n\t\t);\n\t\treels.push(\n\t\t\tctx.host.box({\n\t\t\t\tid: ctx.partId(`cell-${u.start}`),\n\t\t\t\tposition: \"absolute\",\n\t\t\t\tx: lineShift(m, g.line, options) + g.x,\n\t\t\t\ty: g.line * m.lineHeight,\n\t\t\t\twidth: advance,\n\t\t\t\theight: m.lineHeight,\n\t\t\t\toverflow: \"hidden\",\n\t\t\t\tchildren: [reel],\n\t\t\t}),\n\t\t);\n\t\tcues.push({ frame: appear, settled: lock, index: u.index, text: u.text });\n\t\tdone = Math.max(done, lock);\n\t});\n\tconst group = ctx.host.box({\n\t\tid: ctx.partId(\"reels\"),\n\t\tposition: \"absolute\",\n\t\tx: 0,\n\t\ty: 0,\n\t\twidth: m.width,\n\t\theight: m.height,\n\t\tchildren: reels,\n\t});\n\tgroup.animate(\n\t\tLayerAnimation.keys(\"opacity\", [\n\t\t\t[0, 1],\n\t\t\t[done, 0, \"hold\"],\n\t\t]),\n\t);\n\treturn { reels: group, cues, done };\n}\n\n/**\n * Headlines and body copy that animate in and out by character, word or line:\n * rise, blur, track, mask, slam, scatter, scramble, type, stream, wipe and\n * flip, each ending on exactly the static text.\n */\nexport const TextReveal = /* @__PURE__ */ bindComponent(textReveal);\n"
33
+ "content": "/**\n * TextReveal: headlines and body copy that animate in (and out) by\n * character, word or line. One timing model, many looks. Glyph styles run on\n * the GPU through baked per-unit schedules, so a paragraph stays one text\n * node and its final frame is exactly the static text.\n */\nimport {\n\ttype KeyframeTuple,\n\tLayerAnimation,\n\ttype LayoutNode,\n\ttype TextNode,\n} from \"framefields\";\nimport {\n\ttype Animatable,\n\tbakeSamples,\n\tbindComponent,\n\tconstantExpression,\n\tdefineComponent,\n\tevaluateEase,\n\tfindRange,\n\ttype GlyphAnimator,\n\tglyphAnimator,\n\ttype GlyphProps,\n\ttype GlyphUnit,\n\tlineShift,\n\ttype MeasureOptions,\n\ttype Metrics,\n\trangeRects,\n\ttype Rect,\n\trendererChars,\n\tround as roundTo,\n\tscaleFrames,\n\tscheduleExpression,\n\tstepExpression,\n\ttype Theme,\n\tthemeFrames,\n\ttypingFrames,\n\ttype UnitSchedule,\n\tz,\n} from \"framefields/components\";\nimport {\n\tburstStarts,\n\tindexedStarts,\n\tscrambleSet,\n\tunitStarts,\n\tunitsOf,\n} from \"./plan.js\";\n\nexport const REVEAL_STYLES = [\n\t\"rise\",\n\t\"blur\",\n\t\"mask\",\n\t\"slam\",\n\t\"scatter\",\n\t\"scramble\",\n\t\"type\",\n\t\"stream\",\n\t\"wipe\",\n\t\"flip\",\n] as const;\nexport type RevealStyle = (typeof REVEAL_STYLES)[number];\n\n/** Styles that can also play out (`exitStyle`). */\nexport const EXIT_STYLES = [\n\t\"rise\",\n\t\"blur\",\n\t\"mask\",\n\t\"scatter\",\n\t\"type\",\n\t\"stream\",\n\t\"wipe\",\n\t\"flip\",\n] as const;\nexport type ExitStyle = (typeof EXIT_STYLES)[number];\n\nconst ORDERS = [\"forward\", \"reverse\", \"center\", \"edges\", \"random\"] as const;\n\ninterface StyleDefaults {\n\tby: GlyphUnit;\n\t/** Frames between units at 30 fps. */\n\tstagger: number;\n\tspeed: keyof Theme[\"motion\"][\"speed\"];\n\t/** Multiplier on the speed token. */\n\tlength: number;\n\tease: string;\n}\n\nconst STYLE: Record<RevealStyle, StyleDefaults> = {\n\trise: {\n\t\tby: \"word\",\n\t\tstagger: 3,\n\t\tspeed: \"base\",\n\t\tlength: 1.4,\n\t\tease: \"expo.out\",\n\t},\n\tblur: {\n\t\tby: \"char\",\n\t\tstagger: 1.2,\n\t\tspeed: \"slow\",\n\t\tlength: 1,\n\t\tease: \"power3.out\",\n\t},\n\tmask: {\n\t\tby: \"line\",\n\t\tstagger: 5,\n\t\tspeed: \"base\",\n\t\tlength: 1.5,\n\t\tease: \"expo.out\",\n\t},\n\tslam: { by: \"word\", stagger: 7, speed: \"fast\", length: 1, ease: \"power3.in\" },\n\tscatter: {\n\t\tby: \"char\",\n\t\tstagger: 0.8,\n\t\tspeed: \"slow\",\n\t\tlength: 1.2,\n\t\tease: \"expo.out\",\n\t},\n\tscramble: {\n\t\tby: \"char\",\n\t\tstagger: 1.5,\n\t\tspeed: \"base\",\n\t\tlength: 1,\n\t\tease: \"none\",\n\t},\n\ttype: { by: \"char\", stagger: 0, speed: \"fast\", length: 1, ease: \"none\" },\n\tstream: {\n\t\tby: \"word\",\n\t\tstagger: 6,\n\t\tspeed: \"slow\",\n\t\tlength: 1,\n\t\tease: \"power2.out\",\n\t},\n\twipe: {\n\t\tby: \"line\",\n\t\tstagger: 0,\n\t\tspeed: \"slow\",\n\t\tlength: 1.4,\n\t\tease: \"power2.inOut\",\n\t},\n\tflip: {\n\t\tby: \"char\",\n\t\tstagger: 1.5,\n\t\tspeed: \"base\",\n\t\tlength: 1.2,\n\t\tease: \"back.out(1.4)\",\n\t},\n};\n\n/** What each glyph style pushes at full weight (font-size units, degrees). */\nfunction glyphFrom(style: RevealStyle, fs: number, exit: boolean): GlyphProps {\n\tswitch (style) {\n\t\tcase \"rise\":\n\t\t\treturn { y: (exit ? -0.32 : 0.42) * fs, opacity: 0 };\n\t\tcase \"blur\":\n\t\t\treturn { blur: 0.16 * fs, scale: 1.18, opacity: 0 };\n\t\tcase \"flip\":\n\t\t\treturn { rotationX: exit ? -90 : 90, opacity: 0 };\n\t\tdefault:\n\t\t\treturn { opacity: 0 };\n\t}\n}\n\nexport interface TextRevealCues {\n\t[name: string]: ReadonlyArray<{ frame: number }>;\n\t/** Per unit: starts moving (`frame`) and comes to rest (`settled`). */\n\tunits: ReadonlyArray<{\n\t\tframe: number;\n\t\tsettled: number;\n\t\tindex: number;\n\t\ttext: string;\n\t}>;\n\t/** The whole text is at rest. */\n\trevealed: ReadonlyArray<{ frame: number }>;\n\t/** The exit starts (empty without `exitStyle`). */\n\texitStart: ReadonlyArray<{ frame: number }>;\n\t/** Each typed character (`type`) or arriving word (`stream`): for key sounds. */\n\tkeystrokes: ReadonlyArray<{ frame: number; index: number }>;\n}\n\nexport interface TextRevealApi {\n\t/**\n\t * Rects (parent coordinates) that a match covers, one per line: hand\n\t * them to `Annotate` or a camera. Without `match`, the whole text.\n\t */\n\trects(match?: string | RegExp, nth?: number): Rect[];\n}\n\ntype TextProps = Partial<\n\tPick<\n\t\tTextNode,\n\t\t| \"fontFamily\"\n\t\t| \"fontSize\"\n\t\t| \"fontWeight\"\n\t\t| \"fill\"\n\t\t| \"align\"\n\t\t| \"lineHeight\"\n\t\t| \"letterSpacing\"\n\t\t| \"direction\"\n\t\t| \"width\"\n\t>\n>;\n\nconst ALIGN = { start: \"left\", center: \"center\", end: \"right\" } as const;\n\nexport const textReveal = /* @__PURE__ */ defineComponent({\n\tname: \"TextReveal\",\n\tprops: () => ({\n\t\ttext: z.string().min(1),\n\t\tstyle: z.enum(REVEAL_STYLES).default(\"rise\"),\n\t\t/** Unit of animation. Default per style. */\n\t\tby: z.enum([\"char\", \"word\", \"line\"]).optional(),\n\t\t/** Frames between units. Default per style. */\n\t\tstagger: z.number().min(0).optional(),\n\t\torder: z.enum(ORDERS).default(\"forward\"),\n\t\t/** Frames one unit's move takes. Default per style, from the theme. */\n\t\tunitDuration: z.number().int().positive().optional(),\n\t\tease: z.string().optional(),\n\t\t/** Play out after `hold`, unit by unit. */\n\t\texitStyle: z.union([z.enum(EXIT_STYLES), z.literal(false)]).default(false),\n\t\t/** Typing speed for `type`, characters a second. */\n\t\tcps: z.number().positive().default(24),\n\t\t/** Caret for `type` and `stream`. */\n\t\tcaret: z.boolean().default(true),\n\t\t/** Words or patterns drawn in the theme's accent. */\n\t\temphasis: z.array(z.union([z.string(), z.instanceof(RegExp)])).default([]),\n\t\taccent: z.string().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\tlineHeight: z.number().positive().optional(),\n\t\tletterSpacing: z.number().optional(),\n\t\tdirection: z.enum([\"ltr\", \"rtl\", \"auto\"]).optional(),\n\t}),\n\tdefaults: { enter: false, exit: false, hold: 30 },\n\tbuild(ctx, props) {\n\t\tconst { host, theme, fps } = ctx;\n\t\tconst text = props.text;\n\t\tconst fs = props.fontSize ?? 96 * theme.scale;\n\t\tconst fontFamily = props.fontFamily ?? theme.font.display;\n\t\tconst registered = host.fonts();\n\t\tif (registered.length > 0 && !registered.includes(fontFamily)) {\n\t\t\tthrow new Error(\n\t\t\t\t`TextReveal: font \"${fontFamily}\" is not registered. Registered: ${registered.join(\", \")}.`,\n\t\t\t);\n\t\t}\n\t\tconst wrap = ctx.requested.width;\n\t\tconst textProps: TextProps = {\n\t\t\tfontFamily,\n\t\t\tfontSize: fs,\n\t\t\tfill: props.fill ?? theme.color.ink,\n\t\t\talign: props.align,\n\t\t\t...(props.fontWeight !== undefined && { fontWeight: props.fontWeight }),\n\t\t\t...(props.lineHeight !== undefined && { lineHeight: props.lineHeight }),\n\t\t\t...(props.letterSpacing !== undefined && {\n\t\t\t\tletterSpacing: props.letterSpacing,\n\t\t\t}),\n\t\t\t...(props.direction !== undefined && { direction: props.direction }),\n\t\t\t...(wrap !== undefined && { width: wrap }),\n\t\t};\n\t\tconst measureOptions: MeasureOptions = {\n\t\t\tfontFamily,\n\t\t\tfontSize: fs,\n\t\t\tfontWeight: props.fontWeight,\n\t\t\tletterSpacing: props.letterSpacing,\n\t\t\tlineHeight: props.lineHeight,\n\t\t\twidth: wrap,\n\t\t\talign: ALIGN[props.align],\n\t\t\tdirection: props.direction,\n\t\t};\n\t\tconst m = host.measure(text, measureOptions);\n\t\tconst W = wrap ?? m.width;\n\t\tconst H = m.height;\n\n\t\tconst style = STYLE[props.style];\n\t\tconst by: GlyphUnit = props.by ?? style.by;\n\t\t// Stagger stays fractional: units may start between frames.\n\t\tconst perFps = (v: number) => (v * fps) / 30;\n\t\tconst stagger = perFps(props.stagger ?? style.stagger);\n\t\tconst duration =\n\t\t\tprops.unitDuration ??\n\t\t\tMath.max(\n\t\t\t\t1,\n\t\t\t\tMath.round(themeFrames(theme, style.speed, fps) * style.length),\n\t\t\t);\n\t\tconst ease = props.ease ?? style.ease;\n\n\t\tconst parts: Record<string, Animatable<LayoutNode>> = {};\n\t\tconst children: LayoutNode[] = [];\n\t\tconst animators: GlyphAnimator[] = [];\n\t\tconst keystrokes: Array<{ frame: number; index: number }> = [];\n\t\tlet unitCues: TextRevealCues[\"units\"] = [];\n\t\tlet revealed = 0;\n\n\t\t// Emphasis is static colour, first in the stack so streaming ink mixes over it.\n\t\tconst emphasised = emphasisWeights(text, props.emphasis);\n\t\tif (emphasised) {\n\t\t\tanimators.push(\n\t\t\t\tglyphAnimator(\n\t\t\t\t\t\"emphasis\",\n\t\t\t\t\tconstantExpression(\"char\", rendererChars(text, emphasised)),\n\t\t\t\t\t{\n\t\t\t\t\t\tfillColor: props.accent ?? theme.color.accent,\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\n\t\tconst units = unitsOf(text, by, m);\n\t\tconst schedule = (starts: number[], dur = duration, e = ease) => ({\n\t\t\tstarts: indexedStarts(text, by, units, starts),\n\t\t\tduration: dur,\n\t\t\tease: e,\n\t\t});\n\t\tconst cuesFor = (starts: number[], dur: number) =>\n\t\t\tunits.map((u, i) => ({\n\t\t\t\tframe: Math.round(starts[i] ?? 0),\n\t\t\t\tsettled: Math.round((starts[i] ?? 0) + dur),\n\t\t\t\tindex: u.index,\n\t\t\t\ttext: u.text,\n\t\t\t}));\n\n\t\tconst textNode = host.text(text, {\n\t\t\t...textProps,\n\t\t\tid: ctx.partId(\"text\"),\n\t\t\tposition: \"absolute\",\n\t\t\tx: 0,\n\t\t\ty: 0,\n\t\t});\n\t\tparts.text = textNode;\n\t\t// What carries the glyphs: the text node, or a wrapper around it (null when lines are split).\n\t\tlet main: LayoutNode | null = textNode;\n\t\tlet lines: MaskLine[] = [];\n\t\tlet sweep: ReturnType<typeof wipeMatte> | null = null;\n\n\t\t// ── Enter ────────────────────────────────────────────────────────\n\t\tswitch (props.style) {\n\t\t\tcase \"rise\":\n\t\t\tcase \"blur\":\n\t\t\tcase \"flip\": {\n\t\t\t\tconst starts = unitStarts(units.length, stagger, props.order, ctx.seed);\n\t\t\t\tanimators.push(\n\t\t\t\t\tglyphAnimator(\n\t\t\t\t\t\t\"enter\",\n\t\t\t\t\t\tscheduleExpression(by, schedule(starts), \"enter\"),\n\t\t\t\t\t\tglyphFrom(props.style, fs, false),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tunitCues = cuesFor(starts, duration);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"slam\": {\n\t\t\t\t// Each unit is its own node so it scales around its own centre;\n\t\t\t\t// the paragraph takes over the frame the last one lands.\n\t\t\t\tconst starts = unitStarts(units.length, stagger, props.order, ctx.seed);\n\t\t\t\tunitCues = cuesFor(starts, duration);\n\t\t\t\tconst { width: _w, align: _a, ...unitProps } = textProps;\n\t\t\t\tconst pieces = units.map((u, i) => {\n\t\t\t\t\tconst rect = rangeRects(m, u.start, u.end, measureOptions)[0];\n\t\t\t\t\tconst s = starts[i] ?? 0;\n\t\t\t\t\tconst accented =\n\t\t\t\t\t\temphasised?.slice(u.start, u.end).every((v) => v === 1) ?? false;\n\t\t\t\t\tconst piece = host.text(u.text, {\n\t\t\t\t\t\t...unitProps,\n\t\t\t\t\t\t...(accented && { fill: props.accent ?? theme.color.accent }),\n\t\t\t\t\t\tid: ctx.partId(`unit-${i}`),\n\t\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\t\tx: rect?.x ?? 0,\n\t\t\t\t\t\ty: rect?.y ?? 0,\n\t\t\t\t\t});\n\t\t\t\t\tpiece.animate(\n\t\t\t\t\t\tLayerAnimation.create()\n\t\t\t\t\t\t\t.keys(\"scale\", [\n\t\t\t\t\t\t\t\t[s, 2.4],\n\t\t\t\t\t\t\t\t[s + duration, 1, ease],\n\t\t\t\t\t\t\t])\n\t\t\t\t\t\t\t.keys(\"opacity\", [\n\t\t\t\t\t\t\t\t[s, 0],\n\t\t\t\t\t\t\t\t[s + Math.max(1, Math.round(duration * 0.45)), 1],\n\t\t\t\t\t\t\t]),\n\t\t\t\t\t);\n\t\t\t\t\treturn piece;\n\t\t\t\t});\n\t\t\t\tconst landed = Math.max(0, ...unitCues.map((u) => u.settled));\n\t\t\t\t// Words land from 2.4×: neither box may clip them on the way in.\n\t\t\t\tconst group = host.box({\n\t\t\t\t\tid: ctx.partId(\"units\"),\n\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\tx: 0,\n\t\t\t\t\ty: 0,\n\t\t\t\t\twidth: W,\n\t\t\t\t\theight: H,\n\t\t\t\t\toverflow: \"visible\",\n\t\t\t\t\tchildren: pieces,\n\t\t\t\t});\n\t\t\t\tgroup.animate(\n\t\t\t\t\tLayerAnimation.keys(\"opacity\", [\n\t\t\t\t\t\t[0, 1],\n\t\t\t\t\t\t[landed, 0, \"hold\"],\n\t\t\t\t\t]),\n\t\t\t\t);\n\t\t\t\ttextNode.animate(\n\t\t\t\t\tLayerAnimation.keys(\"opacity\", [\n\t\t\t\t\t\t[0, 0],\n\t\t\t\t\t\t[landed, 1, \"hold\"],\n\t\t\t\t\t]),\n\t\t\t\t);\n\t\t\t\t// A short landing shake as each unit hits.\n\t\t\t\tconst shake = host.box({\n\t\t\t\t\tid: ctx.partId(\"shake\"),\n\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\tx: 0,\n\t\t\t\t\ty: 0,\n\t\t\t\t\twidth: W,\n\t\t\t\t\theight: H,\n\t\t\t\t\toverflow: \"visible\",\n\t\t\t\t\tchildren: [group, textNode],\n\t\t\t\t});\n\t\t\t\tshake.animate(...slamShake(unitCues, fs, ctx.rng(\"slam\")));\n\t\t\t\tparts.units = group;\n\t\t\t\tparts.shake = shake;\n\t\t\t\tmain = shake;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"scatter\": {\n\t\t\t\tconst starts = unitStarts(units.length, stagger, props.order, ctx.seed);\n\t\t\t\tconst sched = schedule(starts);\n\t\t\t\tconst per = (key: string) => {\n\t\t\t\t\tconst q = ctx.rng(key);\n\t\t\t\t\treturn rendererChars(\n\t\t\t\t\t\ttext,\n\t\t\t\t\t\tArray.from({ length: text.length }, () => q.spread(1)),\n\t\t\t\t\t);\n\t\t\t\t};\n\t\t\t\tconst rx = per(\"scatter-x\");\n\t\t\t\tconst ry = per(\"scatter-y\");\n\t\t\t\tconst rr = per(\"scatter-r\");\n\t\t\t\tanimators.push(\n\t\t\t\t\tglyphAnimator(\"enter-x\", scheduleExpression(by, sched, \"enter\", rx), {\n\t\t\t\t\t\tx: 1.1 * fs,\n\t\t\t\t\t}),\n\t\t\t\t\tglyphAnimator(\"enter-y\", scheduleExpression(by, sched, \"enter\", ry), {\n\t\t\t\t\t\ty: 0.9 * fs,\n\t\t\t\t\t}),\n\t\t\t\t\tglyphAnimator(\"enter-r\", scheduleExpression(by, sched, \"enter\", rr), {\n\t\t\t\t\t\trotation: 120,\n\t\t\t\t\t}),\n\t\t\t\t\tglyphAnimator(\"enter\", scheduleExpression(by, sched, \"enter\"), {\n\t\t\t\t\t\topacity: 0,\n\t\t\t\t\t\tblur: 0.06 * fs,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t\tunitCues = cuesFor(starts, duration);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"stream\": {\n\t\t\t\tconst words = unitsOf(text, \"word\", m);\n\t\t\t\tconst step = scaleFrames(1, fps);\n\t\t\t\tconst starts = burstStarts(\n\t\t\t\t\twords.length,\n\t\t\t\t\tperFps(props.stagger ?? style.stagger),\n\t\t\t\t\tstep,\n\t\t\t\t\tctx.rng(\"stream\"),\n\t\t\t\t);\n\t\t\t\tconst fade = Math.max(2, themeFrames(theme, \"fast\", fps));\n\t\t\t\tconst ink = props.unitDuration ?? themeFrames(theme, \"slow\", fps);\n\t\t\t\tanimators.push(\n\t\t\t\t\tglyphAnimator(\n\t\t\t\t\t\t\"enter\",\n\t\t\t\t\t\tscheduleExpression(\n\t\t\t\t\t\t\t\"word\",\n\t\t\t\t\t\t\t{ starts, duration: fade, ease: \"power2.out\" },\n\t\t\t\t\t\t\t\"enter\",\n\t\t\t\t\t\t),\n\t\t\t\t\t\t{ opacity: 0, y: 0.08 * fs },\n\t\t\t\t\t),\n\t\t\t\t\tglyphAnimator(\n\t\t\t\t\t\t\"ink\",\n\t\t\t\t\t\tscheduleExpression(\n\t\t\t\t\t\t\t\"word\",\n\t\t\t\t\t\t\t{ starts, duration: ink, ease: \"sine.inOut\" },\n\t\t\t\t\t\t\t\"enter\",\n\t\t\t\t\t\t),\n\t\t\t\t\t\t{ fillColor: theme.color.muted },\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tunitCues = words.map((w, i) => ({\n\t\t\t\t\tframe: starts[i] ?? 0,\n\t\t\t\t\tsettled: (starts[i] ?? 0) + ink,\n\t\t\t\t\tindex: w.index,\n\t\t\t\t\ttext: w.text,\n\t\t\t\t}));\n\t\t\t\twords.forEach((w, i) => {\n\t\t\t\t\tkeystrokes.push({ frame: starts[i] ?? 0, index: w.index });\n\t\t\t\t});\n\t\t\t\tif (props.caret) {\n\t\t\t\t\tconst ends = words.map((w) => w.end);\n\t\t\t\t\tconst caret = buildCaret(ctx, m, measureOptions, fs, {\n\t\t\t\t\t\tat: starts,\n\t\t\t\t\t\tcounts: ends,\n\t\t\t\t\t\tuntil: (starts[starts.length - 1] ?? 0) + ink,\n\t\t\t\t\t\tshape: \"block\",\n\t\t\t\t\t});\n\t\t\t\t\tchildren.push(caret);\n\t\t\t\t\tparts.caret = caret;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"type\": {\n\t\t\t\t// Each glyph switches on at its keystroke inside the full layout,\n\t\t\t\t// so centred or wrapped lines never reflow while typing.\n\t\t\t\tconst frames = typingFrames(text, props.cps, fps, ctx.rng(\"type\"));\n\t\t\t\tanimators.push(\n\t\t\t\t\tglyphAnimator(\n\t\t\t\t\t\t\"enter\",\n\t\t\t\t\t\tstepExpression(\"char\", rendererChars(text, frames)),\n\t\t\t\t\t\t{ opacity: 0 },\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tframes.forEach((f, i) => {\n\t\t\t\t\tif (!/\\s/.test(text[i] ?? \"\"))\n\t\t\t\t\t\tkeystrokes.push({ frame: f, index: i });\n\t\t\t\t});\n\t\t\t\tunitCues = keystrokes.map((k) => ({\n\t\t\t\t\tframe: k.frame,\n\t\t\t\t\tsettled: k.frame,\n\t\t\t\t\tindex: k.index,\n\t\t\t\t\ttext: text[k.index] ?? \"\",\n\t\t\t\t}));\n\t\t\t\trevealed = (frames[frames.length - 1] ?? 0) + 1;\n\t\t\t\tif (props.caret) {\n\t\t\t\t\tconst caret = buildCaret(ctx, m, measureOptions, fs, {\n\t\t\t\t\t\tat: frames,\n\t\t\t\t\t\tcounts: frames.map((_, i) => i + 1),\n\t\t\t\t\t\tuntil: revealed + ctx.hold,\n\t\t\t\t\t\tshape: \"bar\",\n\t\t\t\t\t});\n\t\t\t\t\tchildren.push(caret);\n\t\t\t\t\tparts.caret = caret;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"mask\": {\n\t\t\t\tlines = maskLines(ctx, text, m, measureOptions, textProps, fs);\n\t\t\t\tconst starts = unitStarts(lines.length, stagger, props.order, ctx.seed);\n\t\t\t\tlines.forEach((line, i) => {\n\t\t\t\t\tconst s = starts[i] ?? 0;\n\t\t\t\t\tline.text.animate(\n\t\t\t\t\t\tLayerAnimation.keys(\"y\", [\n\t\t\t\t\t\t\t[s, line.restY + line.travel],\n\t\t\t\t\t\t\t[s + duration, line.restY, ease],\n\t\t\t\t\t\t]),\n\t\t\t\t\t);\n\t\t\t\t\tchildren.push(line.clip);\n\t\t\t\t\tparts[`line-${i}`] = line.clip;\n\t\t\t\t});\n\t\t\t\tunitCues = lines.map((l, i) => ({\n\t\t\t\t\tframe: Math.round(starts[i] ?? 0),\n\t\t\t\t\tsettled: Math.round((starts[i] ?? 0) + duration),\n\t\t\t\t\tindex: i,\n\t\t\t\t\ttext: l.content,\n\t\t\t\t}));\n\t\t\t\tmain = null;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"wipe\": {\n\t\t\t\tconst w = wipeMatte(ctx, W, H, fs, \"hidden\");\n\t\t\t\tsweep = w;\n\t\t\t\tw.matte.animate(\n\t\t\t\t\tLayerAnimation.keys(\"x\", [\n\t\t\t\t\t\t[0, w.hidden],\n\t\t\t\t\t\t[duration, w.shown, ease],\n\t\t\t\t\t]),\n\t\t\t\t);\n\t\t\t\thost.matte(textNode, w.matte);\n\t\t\t\tparts.matte = w.matte;\n\t\t\t\t// The front trails a little blur over the glyphs it is uncovering.\n\t\t\t\tconst xs = rendererChars(text, glyphCenters(text, m, measureOptions));\n\t\t\t\tconst front = Array.from({ length: duration + 1 }, (_, f) =>\n\t\t\t\t\tw.front(\n\t\t\t\t\t\tw.hidden + (w.shown - w.hidden) * evaluateEase(ease, f / duration),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tanimators.push(\n\t\t\t\t\tglyphAnimator(\n\t\t\t\t\t\t\"wipe-blur\",\n\t\t\t\t\t\t`((X,T)=>{const f=clamp(frame,0,${duration});const k=floor(f);const front=T[k]+((T[k+1]??T[k])-T[k])*(f-k);return clamp(1-(front-(X[textIndex]??0))/${round(w.feather)},0,1);})(${list(xs)},${list(front)})`,\n\t\t\t\t\t\t{ blur: 0.12 * fs },\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tunitCues = [{ frame: 0, settled: duration, index: 0, text }];\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"scramble\": {\n\t\t\t\tconst result = scramble(ctx, text, m, measureOptions, textProps, {\n\t\t\t\t\tstagger,\n\t\t\t\t\tcycle: duration,\n\t\t\t\t\torder: props.order,\n\t\t\t\t});\n\t\t\t\tchildren.push(result.reels);\n\t\t\t\tparts.reels = result.reels;\n\t\t\t\t// The real text takes over the moment the last glyph locks.\n\t\t\t\ttextNode.animate(\n\t\t\t\t\tLayerAnimation.keys(\"opacity\", [\n\t\t\t\t\t\t[0, 0],\n\t\t\t\t\t\t[result.done, 1, \"hold\"],\n\t\t\t\t\t]),\n\t\t\t\t);\n\t\t\t\tunitCues = result.cues;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tif (unitCues.length > 0 && props.style !== \"type\")\n\t\t\trevealed = Math.max(...unitCues.map((u) => u.settled));\n\t\t// The text is at rest only once the last landing shake dies out.\n\t\tif (props.style === \"slam\") revealed += SHAKE.length - 1;\n\n\t\t// ── Exit ─────────────────────────────────────────────────────────\n\t\tlet afterHold = 0;\n\t\tconst exitStart = revealed + ctx.hold;\n\t\tif (props.exitStyle !== false) {\n\t\t\tconst out = props.exitStyle;\n\t\t\tconst outStyle = STYLE[out];\n\t\t\tconst outBy: GlyphUnit = props.by ?? outStyle.by;\n\t\t\tconst outUnits = unitsOf(text, outBy, m);\n\t\t\tconst outStagger = perFps((props.stagger ?? outStyle.stagger) * 0.5);\n\t\t\tconst outDur = Math.max(\n\t\t\t\t1,\n\t\t\t\tMath.round(\n\t\t\t\t\tthemeFrames(theme, outStyle.speed, fps) * outStyle.length * 0.75,\n\t\t\t\t),\n\t\t\t);\n\t\t\tconst outEase = theme.motion.ease.exit;\n\t\t\tconst starts = unitStarts(\n\t\t\t\toutUnits.length,\n\t\t\t\toutStagger,\n\t\t\t\tprops.order,\n\t\t\t\tctx.seed,\n\t\t\t\texitStart,\n\t\t\t);\n\t\t\tconst sched: UnitSchedule = {\n\t\t\t\tstarts: indexedStarts(text, outBy, outUnits, starts),\n\t\t\t\tduration: outDur,\n\t\t\t\tease: outEase,\n\t\t\t};\n\t\t\tafterHold = Math.round(Math.max(...starts) - exitStart + outDur);\n\t\t\tswitch (out) {\n\t\t\t\tcase \"rise\":\n\t\t\t\tcase \"blur\":\n\t\t\t\tcase \"flip\":\n\t\t\t\tcase \"scatter\":\n\t\t\t\tcase \"stream\":\n\t\t\t\t\tanimators.push(\n\t\t\t\t\t\tglyphAnimator(\n\t\t\t\t\t\t\t\"exit\",\n\t\t\t\t\t\t\tscheduleExpression(outBy, sched, \"exit\"),\n\t\t\t\t\t\t\tout === \"scatter\"\n\t\t\t\t\t\t\t\t? { y: -0.6 * fs, opacity: 0, blur: 0.06 * fs }\n\t\t\t\t\t\t\t\t: glyphFrom(out, fs, true),\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"type\": {\n\t\t\t\t\t// Backspace, twice as fast as typing: the last glyph goes first.\n\t\t\t\t\tconst per = Math.max(1, Math.round(fps / (props.cps * 2)));\n\t\t\t\t\tconst gone = Array.from(\n\t\t\t\t\t\t{ length: text.length },\n\t\t\t\t\t\t(_, c) => exitStart + per * (text.length - c),\n\t\t\t\t\t);\n\t\t\t\t\tanimators.push(\n\t\t\t\t\t\tglyphAnimator(\n\t\t\t\t\t\t\t\"exit\",\n\t\t\t\t\t\t\t`(1-${stepExpression(\"char\", rendererChars(text, gone))})`,\n\t\t\t\t\t\t\t{ opacity: 0 },\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t\tafterHold = per * text.length + 1;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase \"wipe\": {\n\t\t\t\t\tif (!sweep) {\n\t\t\t\t\t\tsweep = wipeMatte(ctx, W, H, fs, \"shown\");\n\t\t\t\t\t\thost.matte(textNode, sweep.matte);\n\t\t\t\t\t\tparts.matte = sweep.matte;\n\t\t\t\t\t}\n\t\t\t\t\tsweep.matte.animate(\n\t\t\t\t\t\tLayerAnimation.keys(\"x\", [\n\t\t\t\t\t\t\t[exitStart, sweep.shown],\n\t\t\t\t\t\t\t[exitStart + outDur, sweep.gone, outEase],\n\t\t\t\t\t\t]),\n\t\t\t\t\t);\n\t\t\t\t\tafterHold = outDur;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase \"mask\": {\n\t\t\t\t\tif (lines.length === 0)\n\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t'TextReveal: exitStyle \"mask\" needs style \"mask\" (it splits the lines).',\n\t\t\t\t\t\t);\n\t\t\t\t\tconst outStarts = unitStarts(\n\t\t\t\t\t\tlines.length,\n\t\t\t\t\t\toutStagger,\n\t\t\t\t\t\tprops.order,\n\t\t\t\t\t\tctx.seed,\n\t\t\t\t\t\texitStart,\n\t\t\t\t\t);\n\t\t\t\t\tlines.forEach((line, i) => {\n\t\t\t\t\t\tconst s = outStarts[i] ?? exitStart;\n\t\t\t\t\t\tline.text.animate(\n\t\t\t\t\t\t\tLayerAnimation.keys(\"y\", [\n\t\t\t\t\t\t\t\t[s, line.restY],\n\t\t\t\t\t\t\t\t[s + outDur, line.restY - line.travel, outEase],\n\t\t\t\t\t\t\t]),\n\t\t\t\t\t\t);\n\t\t\t\t\t});\n\t\t\t\t\tafterHold = Math.round(Math.max(...outStarts) - exitStart + outDur);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (animators.length > 0)\n\t\t\t(textNode as unknown as { animators: GlyphAnimator[] }).animators =\n\t\t\t\tanimators;\n\t\tif (main) children.unshift(main);\n\n\t\treturn {\n\t\t\tchildren,\n\t\t\twidth: W,\n\t\t\theight: H,\n\t\t\tduration: Math.max(1, revealed),\n\t\t\topenEnded: true,\n\t\t\tafterHold,\n\t\t\tcues: {\n\t\t\t\tunits: unitCues,\n\t\t\t\trevealed: [{ frame: revealed }],\n\t\t\t\texitStart: props.exitStyle === false ? [] : [{ frame: exitStart }],\n\t\t\t\tkeystrokes,\n\t\t\t} as TextRevealCues,\n\t\t\tparts,\n\t\t\tapi: (placement): TextRevealApi => ({\n\t\t\t\trects(match, nth = 0) {\n\t\t\t\t\tconst range: [number, number] | null =\n\t\t\t\t\t\tmatch === undefined\n\t\t\t\t\t\t\t? [0, text.length]\n\t\t\t\t\t\t\t: findRange(text, match, nth);\n\t\t\t\t\tif (!range)\n\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t`TextReveal.rects: \"${String(match)}\" not found in \"${text}\".`,\n\t\t\t\t\t\t);\n\t\t\t\t\treturn rangeRects(m, range[0], range[1], measureOptions).map((q) => ({\n\t\t\t\t\t\t...q,\n\t\t\t\t\t\tx: q.x + placement.x,\n\t\t\t\t\t\ty: q.y + placement.y,\n\t\t\t\t\t}));\n\t\t\t\t},\n\t\t\t}),\n\t\t};\n\t},\n});\n\n// ── Helpers ──────────────────────────────────────────────────────────\n\nconst round = (v: number) => roundTo(v, 4);\nconst list = (values: readonly number[]) => `[${values.map(round).join(\",\")}]`;\n\n/** 1 for characters inside an emphasis match, else 0 (null when nothing matches). */\nfunction emphasisWeights(\n\ttext: string,\n\tpatterns: ReadonlyArray<string | RegExp>,\n): number[] | null {\n\tif (patterns.length === 0) return null;\n\tconst w = new Array<number>(text.length).fill(0);\n\tfor (const p of patterns) {\n\t\tfor (let nth = 0; ; nth++) {\n\t\t\tconst range = findRange(text, p, nth);\n\t\t\tif (!range || range[1] <= range[0]) break;\n\t\t\tfor (let i = range[0]; i < range[1]; i++) w[i] = 1;\n\t\t}\n\t}\n\treturn w.some((v) => v > 0) ? w : null;\n}\n\n/** Landing shake, per frame after a unit lands (× 0.045 em). */\nconst SHAKE = [0, 1, -0.55, 0.3, -0.12, 0];\n\nfunction slamShake(\n\tunits: ReadonlyArray<{ settled: number }>,\n\tfs: number,\n\tr: { spread(n: number): number },\n): LayerAnimation[] {\n\tconst end = Math.max(0, ...units.map((u) => u.settled)) + 8;\n\tconst xs = new Array<number>(end + 1).fill(0);\n\tconst ys = new Array<number>(end + 1).fill(0);\n\tconst a = 0.045 * fs;\n\tconst profile = SHAKE;\n\tfor (const u of units) {\n\t\tconst dx = r.spread(1);\n\t\tprofile.forEach((p, k) => {\n\t\t\tconst f = u.settled + k;\n\t\t\tif (f <= end) {\n\t\t\t\txs[f] = (xs[f] ?? 0) + a * p * dx;\n\t\t\t\tys[f] = (ys[f] ?? 0) + a * p * 0.6;\n\t\t\t}\n\t\t});\n\t}\n\treturn [\n\t\t// Tight tolerance: the text must come to rest exactly where it belongs.\n\t\tLayerAnimation.keys(\"x\", bakeSamples(xs, 0, { tolerance: 0.005 })),\n\t\tLayerAnimation.keys(\"y\", bakeSamples(ys, 0, { tolerance: 0.005 })),\n\t];\n}\n\ninterface Ctx {\n\thost: import(\"framefields/components\").Host;\n\ttheme: Theme;\n\tfps: number;\n\tseed: number;\n\tpartId(part: string): string;\n\trng(key: string | number): import(\"framefields/components\").Rng;\n}\n\n/** Where the caret sits after `count` characters: x, and the line's top. */\nfunction caretAt(\n\tcount: number,\n\tm: Metrics,\n\toptions: MeasureOptions,\n): [number, number] {\n\tlet last: Metrics[\"glyphs\"][number] | undefined;\n\tfor (const g of m.glyphs)\n\t\tif (g.index < count && (!last || g.index > last.index)) last = g;\n\tif (!last) return [lineShift(m, 0, options), 0];\n\tconst shift = lineShift(m, last.line, options);\n\treturn [\n\t\tshift + last.x + last.width - (options.letterSpacing ?? 0),\n\t\tlast.line * m.lineHeight,\n\t];\n}\n\nfunction buildCaret(\n\tctx: Ctx,\n\tm: Metrics,\n\toptions: MeasureOptions,\n\tfs: number,\n\to: {\n\t\tat: readonly number[];\n\t\tcounts: readonly number[];\n\t\tuntil: number;\n\t\tshape: \"bar\" | \"block\";\n\t},\n): Animatable<LayoutNode> {\n\tconst w = o.shape === \"bar\" ? Math.max(2, 0.06 * fs) : 0.42 * fs;\n\tconst h = o.shape === \"bar\" ? 1.05 * fs : 0.42 * fs;\n\tconst dy =\n\t\to.shape === \"bar\" ? (m.lineHeight - h) / 2 : m.lineHeight * 0.52 - h / 2;\n\tconst gap = o.shape === \"bar\" ? 0.04 * fs : 0.18 * fs;\n\tconst [x0, y0] = caretAt(0, m, options);\n\tconst xs: KeyframeTuple[] = [[0, x0 + (o.shape === \"bar\" ? 0 : -w)]];\n\tconst ys: KeyframeTuple[] = [[0, y0 + dy]];\n\to.at.forEach((f, i) => {\n\t\tconst [x, y] = caretAt(o.counts[i] ?? 0, m, options);\n\t\txs.push([f, x + gap, \"hold\"]);\n\t\tys.push([f, y + dy, \"hold\"]);\n\t});\n\tconst caret = ctx.host.box({\n\t\tid: ctx.partId(\"caret\"),\n\t\tposition: \"absolute\",\n\t\tx: x0,\n\t\ty: y0 + dy,\n\t\twidth: w,\n\t\theight: h,\n\t\tbackground: ctx.theme.color.accent,\n\t\tborderRadius: o.shape === \"bar\" ? w / 2 : w * 0.18,\n\t});\n\t// Solid while text arrives, then a few blinks, then gone.\n\tconst last = o.at[o.at.length - 1] ?? 0;\n\tconst half = Math.max(1, Math.round(ctx.fps * 0.5));\n\tconst blink: KeyframeTuple[] = [[0, 1]];\n\tlet on = false;\n\tfor (let f = last + half; f < o.until; f += half, on = !on)\n\t\tblink.push([f, on ? 1 : 0, \"hold\"]);\n\tblink.push([Math.max(last + 1, o.until), 0, \"hold\"]);\n\tcaret.animate(\n\t\tLayerAnimation.create().keys(\"x\", xs).keys(\"y\", ys).keys(\"opacity\", blink),\n\t);\n\treturn caret;\n}\n\n/** Centre x of every character (spaces included), wrapped and aligned. */\nfunction glyphCenters(\n\ttext: string,\n\tm: Metrics,\n\toptions: MeasureOptions,\n): number[] {\n\tconst xs = new Array<number>(text.length).fill(0);\n\tfor (const g of m.glyphs)\n\t\txs[g.index] = lineShift(m, g.line, options) + g.x + g.width / 2;\n\treturn xs;\n}\n\n/**\n * A soft-edged matte for `wipe`: transparent → opaque → opaque → transparent,\n * `feather` px ramps. `hidden` / `shown` / `gone` are its x for each phase.\n */\nfunction wipeMatte(\n\tctx: Ctx,\n\tW: number,\n\tH: number,\n\tfs: number,\n\tinitial: \"hidden\" | \"shown\",\n) {\n\tconst feather = Math.max(24, 0.9 * fs);\n\tconst pad = fs * 0.5;\n\tconst total = W + feather * 2;\n\tconst a = ((feather / total) * 100).toFixed(3);\n\tconst b = (((feather + W) / total) * 100).toFixed(3);\n\tconst hidden = -total;\n\tconst shown = -feather;\n\tconst matte = ctx.host.box({\n\t\tid: ctx.partId(\"matte\"),\n\t\tposition: \"absolute\",\n\t\tx: initial === \"hidden\" ? hidden : shown,\n\t\ty: -pad,\n\t\twidth: total,\n\t\theight: H + pad * 2,\n\t\tbackground: `linear-gradient(90deg, rgba(255,255,255,0) 0%, #ffffff ${a}%, #ffffff ${b}%, rgba(255,255,255,0) 100%)`,\n\t});\n\treturn {\n\t\tmatte,\n\t\tfeather,\n\t\ttotal,\n\t\thidden,\n\t\tshown,\n\t\tgone: W + feather,\n\t\t/** Where visibility reaches zero, for a matte at `x`. */\n\t\tfront: (x: number) => x + total,\n\t};\n}\n\ninterface MaskLine {\n\tclip: Animatable<LayoutNode>;\n\ttext: Animatable<TextNode>;\n\tcontent: string;\n\trestY: number;\n\ttravel: number;\n}\n\n/** One clipped text node per measured line, each at its wrapped position. */\nfunction maskLines(\n\tctx: Ctx,\n\ttext: string,\n\tm: Metrics,\n\toptions: MeasureOptions,\n\ttextProps: TextProps,\n\tfs: number,\n): MaskLine[] {\n\tconst pad = Math.round(fs * 0.18);\n\tconst { width: _w, align: _a, ...lineProps } = textProps;\n\treturn unitsOf(text, \"line\", m).map((u, i) => {\n\t\tconst first = m.glyphs.find(\n\t\t\t(g) => g.index >= u.start && !/\\s/.test(g.char),\n\t\t);\n\t\tconst x = lineShift(m, i, options) + (first?.x ?? 0);\n\t\tconst content = text.slice(u.start, u.end).trim();\n\t\tconst lineText = ctx.host.text(content, {\n\t\t\t...lineProps,\n\t\t\tid: ctx.partId(`line-${i}-text`),\n\t\t\tposition: \"absolute\",\n\t\t\tx,\n\t\t\ty: pad,\n\t\t});\n\t\tconst clip = ctx.host.box({\n\t\t\tid: ctx.partId(`line-${i}`),\n\t\t\tposition: \"absolute\",\n\t\t\tx: 0,\n\t\t\ty: i * m.lineHeight - pad,\n\t\t\twidth: Math.max(m.width, options.width ?? 0) + fs,\n\t\t\theight: m.lineHeight + pad * 2,\n\t\t\toverflow: \"hidden\",\n\t\t\tchildren: [lineText],\n\t\t});\n\t\treturn {\n\t\t\tclip,\n\t\t\ttext: lineText,\n\t\t\tcontent,\n\t\t\trestY: pad,\n\t\t\ttravel: m.lineHeight + pad,\n\t\t};\n\t});\n}\n\n/**\n * Scramble: each character is a clipped vertical reel of seeded glyphs that\n * steps every couple of frames and lands on the real glyph. Reels are as\n * wide as the real glyph's advance, so the line never jitters.\n */\nfunction scramble(\n\tctx: Ctx,\n\ttext: string,\n\tm: Metrics,\n\toptions: MeasureOptions,\n\ttextProps: TextProps,\n\to: { stagger: number; cycle: number; order: (typeof ORDERS)[number] },\n) {\n\tconst units = unitsOf(text, \"char\", m);\n\tconst starts = unitStarts(units.length, o.stagger, o.order, ctx.seed);\n\tconst step = Math.max(1, scaleFrames(2, ctx.fps));\n\tconst { width: _w, align: _a, letterSpacing: _l, ...glyphProps } = textProps;\n\tconst reels: LayoutNode[] = [];\n\tconst cues: Array<{\n\t\tframe: number;\n\t\tsettled: number;\n\t\tindex: number;\n\t\ttext: string;\n\t}> = [];\n\tlet done = 0;\n\tunits.forEach((u, i) => {\n\t\tconst g = m.glyphs.find((q) => q.index === u.start);\n\t\tif (!g) return;\n\t\tconst appear = Math.round((starts[i] ?? 0) * 0.5);\n\t\tconst lock = Math.round((starts[i] ?? 0) + o.cycle);\n\t\tconst r = ctx.rng(`scramble:${u.start}`);\n\t\tconst set = scrambleSet(u.text);\n\t\tconst count = Math.max(1, Math.floor((lock - appear) / step));\n\t\tconst glyphs = Array.from({ length: count }, () => r.pick([...set]));\n\t\tglyphs.push(u.text);\n\t\tconst advance = g.width - (options.letterSpacing ?? 0);\n\t\tconst reel = ctx.host.text(glyphs.join(\"\\n\"), {\n\t\t\t...glyphProps,\n\t\t\tid: ctx.partId(`reel-${u.start}`),\n\t\t\tposition: \"absolute\",\n\t\t\tx: 0,\n\t\t\ty: 0,\n\t\t\twidth: advance,\n\t\t\talign: \"center\",\n\t\t});\n\t\tconst ys: KeyframeTuple[] = [[0, 0]];\n\t\tfor (let k = 1; k < count; k++)\n\t\t\tys.push([appear + k * step, -k * m.lineHeight, \"hold\"]);\n\t\tys.push([lock, -count * m.lineHeight, \"hold\"]);\n\t\treel.animate(\n\t\t\tLayerAnimation.create()\n\t\t\t\t.keys(\"y\", ys)\n\t\t\t\t.keys(\"opacity\", [\n\t\t\t\t\t[appear, 0],\n\t\t\t\t\t[appear + 3, 1],\n\t\t\t\t]),\n\t\t);\n\t\treels.push(\n\t\t\tctx.host.box({\n\t\t\t\tid: ctx.partId(`cell-${u.start}`),\n\t\t\t\tposition: \"absolute\",\n\t\t\t\tx: lineShift(m, g.line, options) + g.x,\n\t\t\t\ty: g.line * m.lineHeight,\n\t\t\t\twidth: advance,\n\t\t\t\theight: m.lineHeight,\n\t\t\t\toverflow: \"hidden\",\n\t\t\t\tchildren: [reel],\n\t\t\t}),\n\t\t);\n\t\tcues.push({ frame: appear, settled: lock, index: u.index, text: u.text });\n\t\tdone = Math.max(done, lock);\n\t});\n\tconst group = ctx.host.box({\n\t\tid: ctx.partId(\"reels\"),\n\t\tposition: \"absolute\",\n\t\tx: 0,\n\t\ty: 0,\n\t\twidth: m.width,\n\t\theight: m.height,\n\t\tchildren: reels,\n\t});\n\tgroup.animate(\n\t\tLayerAnimation.keys(\"opacity\", [\n\t\t\t[0, 1],\n\t\t\t[done, 0, \"hold\"],\n\t\t]),\n\t);\n\treturn { reels: group, cues, done };\n}\n\n/**\n * Headlines and body copy that animate in and out by character, word or line:\n * rise, blur, mask, slam, scatter, scramble, type, stream, wipe and flip, each\n * ending on exactly the static text.\n */\nexport const TextReveal = /* @__PURE__ */ bindComponent(textReveal);\n"
34
34
  },
35
35
  {
36
36
  "path": "text-reveal/plan.ts",
@@ -28,7 +28,7 @@
28
28
  "files": [
29
29
  {
30
30
  "path": "title-card/index.ts",
31
- "content": "/**\n * TitleCard: kicker, headline, rule and sub-line in designed layouts, on\n * one timeline. The text parts are `TextReveal`s (the headline in any reveal\n * style), the rule draws, and the whole card can play out.\n */\nimport { LayerAnimation, type LayoutNode } from \"framefields\";\nimport {\n\ttype Animatable,\n\tbind,\n\tbindComponent,\n\tdefineComponent,\n\tthemeFrames,\n\tz,\n} from \"framefields/components\";\nimport {\n\tEXIT_STYLES,\n\tREVEAL_STYLES,\n\ttextReveal,\n} from \"../text-reveal/index.js\";\n\nexport const TITLE_LAYOUTS = [\"center\", \"left\", \"stack\", \"split\"] as const;\nexport type TitleLayout = (typeof TITLE_LAYOUTS)[number];\n\nexport interface TitleCardCues {\n\t[name: string]: ReadonlyArray<{ frame: number }>;\n\t/** The headline is at rest. */\n\trevealed: ReadonlyArray<{ frame: number }>;\n\t/** Every part is at rest. */\n\tsettled: ReadonlyArray<{ frame: number }>;\n\t/** The card starts to play out (empty without `exitStyle`). */\n\texitStart: ReadonlyArray<{ frame: number }>;\n}\n\nexport const titleCard = /* @__PURE__ */ defineComponent({\n\tname: \"TitleCard\",\n\tprops: () => ({\n\t\ttitle: z.string().min(1),\n\t\tkicker: z.string().optional(),\n\t\tsubtitle: z.string().optional(),\n\t\tlayout: z.enum(TITLE_LAYOUTS).default(\"center\"),\n\t\t/** The headline's reveal style. */\n\t\treveal: z.enum(REVEAL_STYLES).default(\"rise\"),\n\t\temphasis: z.array(z.union([z.string(), z.instanceof(RegExp)])).default([]),\n\t\trule: z.boolean().default(true),\n\t\texitStyle: z.union([z.enum(EXIT_STYLES), z.literal(false)]).default(false),\n\t\ttitleSize: z.number().positive().optional(),\n\t\tfontFamily: z.string().optional(),\n\t}),\n\tdefaults: { enter: false, exit: false, hold: 45 },\n\tbuild(ctx, props) {\n\t\tconst { host, theme, fps } = ctx;\n\t\tconst s = theme.scale;\n\t\tconst W = ctx.requested.width ?? 1600 * s;\n\t\tconst size = props.titleSize ?? 120 * s;\n\t\tconst kickerSize = Math.max(16 * s, size * 0.22);\n\t\tconst subSize = Math.max(20 * s, size * 0.34);\n\t\tconst fontFamily = props.fontFamily ?? theme.font.display;\n\t\tconst reveal = bind(host, textReveal);\n\t\tconst fast = themeFrames(theme, \"fast\", fps);\n\t\tconst base = themeFrames(theme, \"base\", fps);\n\t\tconst layout = props.layout;\n\t\tconst centered = layout === \"center\";\n\t\tconst align = centered ? \"center\" : \"start\";\n\n\t\t// Columns: split puts the headline left, the rest right of a rule.\n\t\tconst bar = layout === \"stack\" ? Math.max(4 * s, size * 0.06) : 0;\n\t\tconst barGap = layout === \"stack\" ? size * 0.32 : 0;\n\t\tconst titleW = layout === \"split\" ? W * 0.56 : W - bar - barGap;\n\t\tconst sideX = W * 0.62;\n\t\tconst sideW = W - sideX;\n\t\tconst left = bar + barGap;\n\n\t\tconst measure = (\n\t\t\ttext: string,\n\t\t\tfontSize: number,\n\t\t\twidth: number,\n\t\t\tweight?: number,\n\t\t) =>\n\t\t\thost.measure(text, {\n\t\t\t\tfontFamily,\n\t\t\t\tfontSize,\n\t\t\t\tfontWeight: weight,\n\t\t\t\twidth,\n\t\t\t\talign: centered ? \"center\" : \"left\",\n\t\t\t});\n\t\tconst titleM = measure(props.title, size, titleW, 700);\n\t\tconst kickerText = props.kicker?.toUpperCase();\n\t\t// Measured with its tracking, so a centred kicker is really centred.\n\t\tconst kickerM = kickerText\n\t\t\t? host.measure(kickerText, {\n\t\t\t\t\tfontFamily,\n\t\t\t\t\tfontSize: kickerSize,\n\t\t\t\t\tfontWeight: theme.weight.semibold,\n\t\t\t\t\tletterSpacing: kickerSize * 0.16,\n\t\t\t\t})\n\t\t\t: null;\n\t\tconst subM = props.subtitle\n\t\t\t? measure(\n\t\t\t\t\tprops.subtitle,\n\t\t\t\t\tsubSize,\n\t\t\t\t\tlayout === \"split\" ? sideW : centered ? W * 0.8 : titleW,\n\t\t\t\t)\n\t\t\t: null;\n\n\t\tconst gap = size * 0.28;\n\t\tconst ruleW =\n\t\t\tlayout === \"split\" ? Math.max(2, 3 * s) : Math.min(W * 0.12, 160 * s);\n\t\tconst ruleH = layout === \"split\" ? 0 : Math.max(3, 4 * s);\n\t\tconst ruleGap = props.rule && layout !== \"split\" ? gap : 0;\n\n\t\t// ── Vertical layout ───────────────────────────────────────────────\n\t\tlet y = 0;\n\t\tconst place: Record<\n\t\t\t\"kicker\" | \"title\" | \"rule\" | \"subtitle\",\n\t\t\t{ x: number; y: number }\n\t\t> = {\n\t\t\tkicker: { x: 0, y: 0 },\n\t\t\ttitle: { x: 0, y: 0 },\n\t\t\trule: { x: 0, y: 0 },\n\t\t\tsubtitle: { x: 0, y: 0 },\n\t\t};\n\t\tlet height: number;\n\t\tif (layout === \"split\") {\n\t\t\tplace.title = { x: 0, y: 0 };\n\t\t\tconst side =\n\t\t\t\t(kickerM ? kickerM.height + gap * 0.6 : 0) + (subM?.height ?? 0);\n\t\t\tconst top = Math.max(0, (titleM.height - side) / 2);\n\t\t\tplace.kicker = { x: sideX, y: top };\n\t\t\tplace.subtitle = {\n\t\t\t\tx: sideX,\n\t\t\t\ty: top + (kickerM ? kickerM.height + gap * 0.6 : 0),\n\t\t\t};\n\t\t\tplace.rule = { x: W * 0.59, y: 0 };\n\t\t\theight = Math.max(titleM.height, side);\n\t\t} else {\n\t\t\tif (kickerM) {\n\t\t\t\tplace.kicker = { x: centered ? (W - kickerM.width) / 2 : left, y };\n\t\t\t\ty += kickerM.height + gap * 0.5;\n\t\t\t}\n\t\t\tplace.title = { x: centered ? 0 : left, y };\n\t\t\ty += titleM.height;\n\t\t\tif (props.rule) {\n\t\t\t\ty += ruleGap * 0.6;\n\t\t\t\tplace.rule = { x: centered ? (W - ruleW) / 2 : left, y };\n\t\t\t\ty += ruleH + ruleGap * 0.6;\n\t\t\t} else y += gap * 0.5;\n\t\t\tif (subM) {\n\t\t\t\tplace.subtitle = { x: centered ? W * 0.1 : left, y };\n\t\t\t\ty += subM.height;\n\t\t\t}\n\t\t\theight = y;\n\t\t}\n\n\t\t// ── Timeline ──────────────────────────────────────────────────────\n\t\tconst children: LayoutNode[] = [];\n\t\tconst parts: Record<string, Animatable<LayoutNode>> = {};\n\t\tconst titleAt = kickerText ? Math.round(fast * 1.2) : 0;\n\t\tconst title = reveal({\n\t\t\tid: ctx.partId(\"title\"),\n\t\t\ttext: props.title,\n\t\t\tstyle: props.reveal,\n\t\t\tat: titleAt,\n\t\t\t...place.title,\n\t\t\twidth: titleW,\n\t\t\talign,\n\t\t\tfontFamily,\n\t\t\tfontSize: size,\n\t\t\tfontWeight: theme.weight.bold,\n\t\t\temphasis: props.emphasis,\n\t\t\ttheme,\n\t\t\tseed: ctx.seed,\n\t\t\tfps,\n\t\t});\n\t\tconst revealed = title.cues.revealed[0]?.frame ?? titleAt;\n\t\tconst followAt = Math.max(titleAt, revealed - Math.round(base * 0.6));\n\n\t\tlet settled = revealed;\n\t\tconst fadeable: Array<Animatable<LayoutNode>> = [];\n\t\tif (kickerText && kickerM) {\n\t\t\tconst kicker = reveal({\n\t\t\t\tid: ctx.partId(\"kicker\"),\n\t\t\t\ttext: kickerText,\n\t\t\t\tstyle: \"track\",\n\t\t\t\t...place.kicker,\n\t\t\t\tfontFamily,\n\t\t\t\tfontSize: kickerSize,\n\t\t\t\tfontWeight: theme.weight.semibold,\n\t\t\t\tletterSpacing: kickerSize * 0.16,\n\t\t\t\tfill: theme.color.accent,\n\t\t\t\ttheme,\n\t\t\t\tfps,\n\t\t\t});\n\t\t\tchildren.push(kicker);\n\t\t\tparts.kicker = kicker;\n\t\t\tfadeable.push(kicker);\n\t\t\tsettled = Math.max(settled, kicker.cues.revealed[0]?.frame ?? 0);\n\t\t}\n\t\tchildren.push(title);\n\t\tparts.title = title;\n\n\t\tif (props.rule) {\n\t\t\tconst draw = Math.round(base * 1.3);\n\t\t\tconst rule = host.box({\n\t\t\t\tid: ctx.partId(\"rule\"),\n\t\t\t\tposition: \"absolute\",\n\t\t\t\t...place.rule,\n\t\t\t\twidth: layout === \"split\" ? ruleW : 0,\n\t\t\t\theight: layout === \"split\" ? 0 : ruleH,\n\t\t\t\tbackground: theme.color.accent,\n\t\t\t\tborderRadius: ruleH / 2,\n\t\t\t});\n\t\t\tconst along = layout === \"split\" ? \"height\" : \"width\";\n\t\t\tconst full = layout === \"split\" ? height : ruleW;\n\t\t\trule.animate(\n\t\t\t\tLayerAnimation.keys(along, [\n\t\t\t\t\t[followAt, 0],\n\t\t\t\t\t[followAt + draw, full, theme.motion.ease.enter],\n\t\t\t\t]),\n\t\t\t);\n\t\t\t// Centred rules grow from the middle.\n\t\t\tif (centered)\n\t\t\t\trule.animate(\n\t\t\t\t\tLayerAnimation.keys(\"x\", [\n\t\t\t\t\t\t[followAt, W / 2],\n\t\t\t\t\t\t[followAt + draw, place.rule.x, theme.motion.ease.enter],\n\t\t\t\t\t]),\n\t\t\t\t);\n\t\t\tchildren.push(rule);\n\t\t\tparts.rule = rule;\n\t\t\tfadeable.push(rule);\n\t\t\tsettled = Math.max(settled, followAt + draw);\n\t\t}\n\t\tif (layout === \"stack\") {\n\t\t\tconst draw = Math.round(base * 1.5);\n\t\t\tconst accentBar = host.box({\n\t\t\t\tid: ctx.partId(\"bar\"),\n\t\t\t\tposition: \"absolute\",\n\t\t\t\tx: 0,\n\t\t\t\ty: 0,\n\t\t\t\twidth: bar,\n\t\t\t\theight: 0,\n\t\t\t\tbackground: theme.color.accent,\n\t\t\t\tborderRadius: bar / 2,\n\t\t\t});\n\t\t\taccentBar.animate(\n\t\t\t\tLayerAnimation.keys(\"height\", [\n\t\t\t\t\t[0, 0],\n\t\t\t\t\t[draw, height, theme.motion.ease.enter],\n\t\t\t\t]),\n\t\t\t);\n\t\t\tchildren.push(accentBar);\n\t\t\tparts.bar = accentBar;\n\t\t\tfadeable.push(accentBar);\n\t\t\tsettled = Math.max(settled, draw);\n\t\t}\n\t\tif (props.subtitle && subM) {\n\t\t\tconst sub = reveal({\n\t\t\t\tid: ctx.partId(\"subtitle\"),\n\t\t\t\ttext: props.subtitle,\n\t\t\t\tstyle: \"rise\",\n\t\t\t\tby: \"line\",\n\t\t\t\tat: followAt + Math.round(fast * 0.5),\n\t\t\t\t...place.subtitle,\n\t\t\t\twidth: layout === \"split\" ? sideW : centered ? W * 0.8 : titleW,\n\t\t\t\talign,\n\t\t\t\tfontFamily,\n\t\t\t\tfontSize: subSize,\n\t\t\t\tfill: theme.color.muted,\n\t\t\t\ttheme,\n\t\t\t\tfps,\n\t\t\t});\n\t\t\tchildren.push(sub);\n\t\t\tparts.subtitle = sub;\n\t\t\tfadeable.push(sub);\n\t\t\tsettled = Math.max(settled, sub.cues.revealed[0]?.frame ?? 0);\n\t\t}\n\n\t\t// ── Exit ──────────────────────────────────────────────────────────\n\t\tlet afterHold = 0;\n\t\tconst exitStart = settled + ctx.hold;\n\t\tif (props.exitStyle !== false) {\n\t\t\t// Rebuild the headline with its exit lined up to the card's.\n\t\t\tconst leaving = reveal({\n\t\t\t\tid: ctx.partId(\"title\"),\n\t\t\t\ttext: props.title,\n\t\t\t\tstyle: props.reveal,\n\t\t\t\texitStyle: props.exitStyle,\n\t\t\t\thold: exitStart - revealed,\n\t\t\t\tat: titleAt,\n\t\t\t\t...place.title,\n\t\t\t\twidth: titleW,\n\t\t\t\talign,\n\t\t\t\tfontFamily,\n\t\t\t\tfontSize: size,\n\t\t\t\tfontWeight: theme.weight.bold,\n\t\t\t\temphasis: props.emphasis,\n\t\t\t\ttheme,\n\t\t\t\tseed: ctx.seed,\n\t\t\t\tfps,\n\t\t\t});\n\t\t\tchildren[children.indexOf(title)] = leaving;\n\t\t\tparts.title = leaving;\n\t\t\tafterHold = Math.max(1, leaving.span.end - exitStart);\n\t\t\tconst out = Math.min(afterHold, Math.round(base * 0.8));\n\t\t\tfor (const node of fadeable)\n\t\t\t\tnode.animate(\n\t\t\t\t\tLayerAnimation.keys(\"opacity\", [\n\t\t\t\t\t\t[exitStart, 1],\n\t\t\t\t\t\t[exitStart + out, 0, theme.motion.ease.exit],\n\t\t\t\t\t]),\n\t\t\t\t);\n\t\t}\n\n\t\treturn {\n\t\t\tchildren,\n\t\t\twidth: W,\n\t\t\theight,\n\t\t\tduration: settled,\n\t\t\topenEnded: true,\n\t\t\tafterHold,\n\t\t\tcues: {\n\t\t\t\trevealed: [{ frame: revealed }],\n\t\t\t\tsettled: [{ frame: settled }],\n\t\t\t\texitStart: props.exitStyle === false ? [] : [{ frame: exitStart }],\n\t\t\t} as TitleCardCues,\n\t\t\tparts,\n\t\t};\n\t},\n});\n\n/**\n * Kicker, headline, rule and sub-line in designed layouts (center, left, stack,\n * split), the headline in any reveal style, on one timeline.\n */\nexport const TitleCard = /* @__PURE__ */ bindComponent(titleCard);\n"
31
+ "content": "/**\n * TitleCard: kicker, headline, rule and sub-line in designed layouts, on\n * one timeline. The text parts are `TextReveal`s (the headline in any reveal\n * style), the rule draws, and the whole card can play out.\n */\nimport { LayerAnimation, type LayoutNode } from \"framefields\";\nimport {\n\ttype Animatable,\n\tbind,\n\tbindComponent,\n\tdefineComponent,\n\tthemeFrames,\n\tz,\n} from \"framefields/components\";\nimport {\n\tEXIT_STYLES,\n\tREVEAL_STYLES,\n\ttextReveal,\n} from \"../text-reveal/index.js\";\n\nexport const TITLE_LAYOUTS = [\"center\", \"left\", \"stack\", \"split\"] as const;\nexport type TitleLayout = (typeof TITLE_LAYOUTS)[number];\n\nexport interface TitleCardCues {\n\t[name: string]: ReadonlyArray<{ frame: number }>;\n\t/** The headline is at rest. */\n\trevealed: ReadonlyArray<{ frame: number }>;\n\t/** Every part is at rest. */\n\tsettled: ReadonlyArray<{ frame: number }>;\n\t/** The card starts to play out (empty without `exitStyle`). */\n\texitStart: ReadonlyArray<{ frame: number }>;\n}\n\nexport const titleCard = /* @__PURE__ */ defineComponent({\n\tname: \"TitleCard\",\n\tprops: () => ({\n\t\ttitle: z.string().min(1),\n\t\tkicker: z.string().optional(),\n\t\tsubtitle: z.string().optional(),\n\t\tlayout: z.enum(TITLE_LAYOUTS).default(\"center\"),\n\t\t/** The headline's reveal style. */\n\t\treveal: z.enum(REVEAL_STYLES).default(\"rise\"),\n\t\temphasis: z.array(z.union([z.string(), z.instanceof(RegExp)])).default([]),\n\t\trule: z.boolean().default(true),\n\t\texitStyle: z.union([z.enum(EXIT_STYLES), z.literal(false)]).default(false),\n\t\ttitleSize: z.number().positive().optional(),\n\t\tfontFamily: z.string().optional(),\n\t}),\n\tdefaults: { enter: false, exit: false, hold: 45 },\n\tbuild(ctx, props) {\n\t\tconst { host, theme, fps } = ctx;\n\t\tconst s = theme.scale;\n\t\tconst W = ctx.requested.width ?? 1600 * s;\n\t\tconst size = props.titleSize ?? 120 * s;\n\t\tconst kickerSize = Math.max(16 * s, size * 0.22);\n\t\tconst subSize = Math.max(20 * s, size * 0.34);\n\t\tconst fontFamily = props.fontFamily ?? theme.font.display;\n\t\tconst reveal = bind(host, textReveal);\n\t\tconst fast = themeFrames(theme, \"fast\", fps);\n\t\tconst base = themeFrames(theme, \"base\", fps);\n\t\tconst layout = props.layout;\n\t\tconst centered = layout === \"center\";\n\t\tconst align = centered ? \"center\" : \"start\";\n\n\t\t// Columns: split puts the headline left, the rest right of a rule.\n\t\tconst bar = layout === \"stack\" ? Math.max(4 * s, size * 0.06) : 0;\n\t\tconst barGap = layout === \"stack\" ? size * 0.32 : 0;\n\t\tconst titleW = layout === \"split\" ? W * 0.56 : W - bar - barGap;\n\t\tconst sideX = W * 0.62;\n\t\tconst sideW = W - sideX;\n\t\tconst left = bar + barGap;\n\n\t\tconst measure = (\n\t\t\ttext: string,\n\t\t\tfontSize: number,\n\t\t\twidth: number,\n\t\t\tweight?: number,\n\t\t) =>\n\t\t\thost.measure(text, {\n\t\t\t\tfontFamily,\n\t\t\t\tfontSize,\n\t\t\t\tfontWeight: weight,\n\t\t\t\twidth,\n\t\t\t\talign: centered ? \"center\" : \"left\",\n\t\t\t});\n\t\tconst titleM = measure(props.title, size, titleW, 700);\n\t\tconst kickerText = props.kicker?.toUpperCase();\n\t\t// Measured with its tracking, so a centred kicker is really centred.\n\t\tconst kickerM = kickerText\n\t\t\t? host.measure(kickerText, {\n\t\t\t\t\tfontFamily,\n\t\t\t\t\tfontSize: kickerSize,\n\t\t\t\t\tfontWeight: theme.weight.semibold,\n\t\t\t\t\tletterSpacing: kickerSize * 0.16,\n\t\t\t\t})\n\t\t\t: null;\n\t\tconst subM = props.subtitle\n\t\t\t? measure(\n\t\t\t\t\tprops.subtitle,\n\t\t\t\t\tsubSize,\n\t\t\t\t\tlayout === \"split\" ? sideW : centered ? W * 0.8 : titleW,\n\t\t\t\t)\n\t\t\t: null;\n\n\t\tconst gap = size * 0.28;\n\t\tconst ruleW =\n\t\t\tlayout === \"split\" ? Math.max(2, 3 * s) : Math.min(W * 0.12, 160 * s);\n\t\tconst ruleH = layout === \"split\" ? 0 : Math.max(3, 4 * s);\n\t\tconst ruleGap = props.rule && layout !== \"split\" ? gap : 0;\n\n\t\t// ── Vertical layout ───────────────────────────────────────────────\n\t\tlet y = 0;\n\t\tconst place: Record<\n\t\t\t\"kicker\" | \"title\" | \"rule\" | \"subtitle\",\n\t\t\t{ x: number; y: number }\n\t\t> = {\n\t\t\tkicker: { x: 0, y: 0 },\n\t\t\ttitle: { x: 0, y: 0 },\n\t\t\trule: { x: 0, y: 0 },\n\t\t\tsubtitle: { x: 0, y: 0 },\n\t\t};\n\t\tlet height: number;\n\t\tif (layout === \"split\") {\n\t\t\tplace.title = { x: 0, y: 0 };\n\t\t\tconst side =\n\t\t\t\t(kickerM ? kickerM.height + gap * 0.6 : 0) + (subM?.height ?? 0);\n\t\t\tconst top = Math.max(0, (titleM.height - side) / 2);\n\t\t\tplace.kicker = { x: sideX, y: top };\n\t\t\tplace.subtitle = {\n\t\t\t\tx: sideX,\n\t\t\t\ty: top + (kickerM ? kickerM.height + gap * 0.6 : 0),\n\t\t\t};\n\t\t\tplace.rule = { x: W * 0.59, y: 0 };\n\t\t\theight = Math.max(titleM.height, side);\n\t\t} else {\n\t\t\tif (kickerM) {\n\t\t\t\tplace.kicker = { x: centered ? (W - kickerM.width) / 2 : left, y };\n\t\t\t\ty += kickerM.height + gap * 0.5;\n\t\t\t}\n\t\t\tplace.title = { x: centered ? 0 : left, y };\n\t\t\ty += titleM.height;\n\t\t\tif (props.rule) {\n\t\t\t\ty += ruleGap * 0.6;\n\t\t\t\tplace.rule = { x: centered ? (W - ruleW) / 2 : left, y };\n\t\t\t\ty += ruleH + ruleGap * 0.6;\n\t\t\t} else y += gap * 0.5;\n\t\t\tif (subM) {\n\t\t\t\tplace.subtitle = { x: centered ? W * 0.1 : left, y };\n\t\t\t\ty += subM.height;\n\t\t\t}\n\t\t\theight = y;\n\t\t}\n\n\t\t// ── Timeline ──────────────────────────────────────────────────────\n\t\tconst children: LayoutNode[] = [];\n\t\tconst parts: Record<string, Animatable<LayoutNode>> = {};\n\t\tconst titleAt = kickerText ? Math.round(fast * 1.2) : 0;\n\t\tconst title = reveal({\n\t\t\tid: ctx.partId(\"title\"),\n\t\t\ttext: props.title,\n\t\t\tstyle: props.reveal,\n\t\t\tat: titleAt,\n\t\t\t...place.title,\n\t\t\twidth: titleW,\n\t\t\talign,\n\t\t\tfontFamily,\n\t\t\tfontSize: size,\n\t\t\tfontWeight: theme.weight.bold,\n\t\t\temphasis: props.emphasis,\n\t\t\ttheme,\n\t\t\tseed: ctx.seed,\n\t\t\tfps,\n\t\t});\n\t\tconst revealed = title.cues.revealed[0]?.frame ?? titleAt;\n\t\tconst followAt = Math.max(titleAt, revealed - Math.round(base * 0.6));\n\n\t\tlet settled = revealed;\n\t\tconst fadeable: Array<Animatable<LayoutNode>> = [];\n\t\tif (kickerText && kickerM) {\n\t\t\tconst kicker = reveal({\n\t\t\t\tid: ctx.partId(\"kicker\"),\n\t\t\t\ttext: kickerText,\n\t\t\t\tstyle: \"rise\",\n\t\t\t\t...place.kicker,\n\t\t\t\tfontFamily,\n\t\t\t\tfontSize: kickerSize,\n\t\t\t\tfontWeight: theme.weight.semibold,\n\t\t\t\tletterSpacing: kickerSize * 0.16,\n\t\t\t\tfill: theme.color.accent,\n\t\t\t\ttheme,\n\t\t\t\tfps,\n\t\t\t});\n\t\t\tchildren.push(kicker);\n\t\t\tparts.kicker = kicker;\n\t\t\tfadeable.push(kicker);\n\t\t\tsettled = Math.max(settled, kicker.cues.revealed[0]?.frame ?? 0);\n\t\t}\n\t\tchildren.push(title);\n\t\tparts.title = title;\n\n\t\tif (props.rule) {\n\t\t\tconst draw = Math.round(base * 1.3);\n\t\t\tconst rule = host.box({\n\t\t\t\tid: ctx.partId(\"rule\"),\n\t\t\t\tposition: \"absolute\",\n\t\t\t\t...place.rule,\n\t\t\t\twidth: layout === \"split\" ? ruleW : 0,\n\t\t\t\theight: layout === \"split\" ? 0 : ruleH,\n\t\t\t\tbackground: theme.color.accent,\n\t\t\t\tborderRadius: ruleH / 2,\n\t\t\t});\n\t\t\tconst along = layout === \"split\" ? \"height\" : \"width\";\n\t\t\tconst full = layout === \"split\" ? height : ruleW;\n\t\t\trule.animate(\n\t\t\t\tLayerAnimation.keys(along, [\n\t\t\t\t\t[followAt, 0],\n\t\t\t\t\t[followAt + draw, full, theme.motion.ease.enter],\n\t\t\t\t]),\n\t\t\t);\n\t\t\t// Centred rules grow from the middle.\n\t\t\tif (centered)\n\t\t\t\trule.animate(\n\t\t\t\t\tLayerAnimation.keys(\"x\", [\n\t\t\t\t\t\t[followAt, W / 2],\n\t\t\t\t\t\t[followAt + draw, place.rule.x, theme.motion.ease.enter],\n\t\t\t\t\t]),\n\t\t\t\t);\n\t\t\tchildren.push(rule);\n\t\t\tparts.rule = rule;\n\t\t\tfadeable.push(rule);\n\t\t\tsettled = Math.max(settled, followAt + draw);\n\t\t}\n\t\tif (layout === \"stack\") {\n\t\t\tconst draw = Math.round(base * 1.5);\n\t\t\tconst accentBar = host.box({\n\t\t\t\tid: ctx.partId(\"bar\"),\n\t\t\t\tposition: \"absolute\",\n\t\t\t\tx: 0,\n\t\t\t\ty: 0,\n\t\t\t\twidth: bar,\n\t\t\t\theight: 0,\n\t\t\t\tbackground: theme.color.accent,\n\t\t\t\tborderRadius: bar / 2,\n\t\t\t});\n\t\t\taccentBar.animate(\n\t\t\t\tLayerAnimation.keys(\"height\", [\n\t\t\t\t\t[0, 0],\n\t\t\t\t\t[draw, height, theme.motion.ease.enter],\n\t\t\t\t]),\n\t\t\t);\n\t\t\tchildren.push(accentBar);\n\t\t\tparts.bar = accentBar;\n\t\t\tfadeable.push(accentBar);\n\t\t\tsettled = Math.max(settled, draw);\n\t\t}\n\t\tif (props.subtitle && subM) {\n\t\t\tconst sub = reveal({\n\t\t\t\tid: ctx.partId(\"subtitle\"),\n\t\t\t\ttext: props.subtitle,\n\t\t\t\tstyle: \"rise\",\n\t\t\t\tby: \"line\",\n\t\t\t\tat: followAt + Math.round(fast * 0.5),\n\t\t\t\t...place.subtitle,\n\t\t\t\twidth: layout === \"split\" ? sideW : centered ? W * 0.8 : titleW,\n\t\t\t\talign,\n\t\t\t\tfontFamily,\n\t\t\t\tfontSize: subSize,\n\t\t\t\tfill: theme.color.muted,\n\t\t\t\ttheme,\n\t\t\t\tfps,\n\t\t\t});\n\t\t\tchildren.push(sub);\n\t\t\tparts.subtitle = sub;\n\t\t\tfadeable.push(sub);\n\t\t\tsettled = Math.max(settled, sub.cues.revealed[0]?.frame ?? 0);\n\t\t}\n\n\t\t// ── Exit ──────────────────────────────────────────────────────────\n\t\tlet afterHold = 0;\n\t\tconst exitStart = settled + ctx.hold;\n\t\tif (props.exitStyle !== false) {\n\t\t\t// Rebuild the headline with its exit lined up to the card's.\n\t\t\tconst leaving = reveal({\n\t\t\t\tid: ctx.partId(\"title\"),\n\t\t\t\ttext: props.title,\n\t\t\t\tstyle: props.reveal,\n\t\t\t\texitStyle: props.exitStyle,\n\t\t\t\thold: exitStart - revealed,\n\t\t\t\tat: titleAt,\n\t\t\t\t...place.title,\n\t\t\t\twidth: titleW,\n\t\t\t\talign,\n\t\t\t\tfontFamily,\n\t\t\t\tfontSize: size,\n\t\t\t\tfontWeight: theme.weight.bold,\n\t\t\t\temphasis: props.emphasis,\n\t\t\t\ttheme,\n\t\t\t\tseed: ctx.seed,\n\t\t\t\tfps,\n\t\t\t});\n\t\t\tchildren[children.indexOf(title)] = leaving;\n\t\t\tparts.title = leaving;\n\t\t\tafterHold = Math.max(1, leaving.span.end - exitStart);\n\t\t\tconst out = Math.min(afterHold, Math.round(base * 0.8));\n\t\t\tfor (const node of fadeable)\n\t\t\t\tnode.animate(\n\t\t\t\t\tLayerAnimation.keys(\"opacity\", [\n\t\t\t\t\t\t[exitStart, 1],\n\t\t\t\t\t\t[exitStart + out, 0, theme.motion.ease.exit],\n\t\t\t\t\t]),\n\t\t\t\t);\n\t\t}\n\n\t\treturn {\n\t\t\tchildren,\n\t\t\twidth: W,\n\t\t\theight,\n\t\t\tduration: settled,\n\t\t\topenEnded: true,\n\t\t\tafterHold,\n\t\t\tcues: {\n\t\t\t\trevealed: [{ frame: revealed }],\n\t\t\t\tsettled: [{ frame: settled }],\n\t\t\t\texitStart: props.exitStyle === false ? [] : [{ frame: exitStart }],\n\t\t\t} as TitleCardCues,\n\t\t\tparts,\n\t\t};\n\t},\n});\n\n/**\n * Kicker, headline, rule and sub-line in designed layouts (center, left, stack,\n * split), the headline in any reveal style, on one timeline.\n */\nexport const TitleCard = /* @__PURE__ */ bindComponent(titleCard);\n"
32
32
  }
33
33
  ]
34
34
  }
@@ -1,3 +1,3 @@
1
- import { F as describeGpu, L as initHeadlessWebGPU, p as GpuAdapterDescription } from "../index-kj5uSzaI.mjs";
2
- import { _ as renderSemaphore, a as renderFrameGrid, c as HeadlessMediaRenderer, d as QaSegment, f as RenderProgress, g as formatQaReport, h as VideoQaStats, i as computeGridLayout, l as HeadlessWebGPURenderer, m as VideoQaReport, n as FrameGridRenderable, o as resolveSamplePoints, p as VideoQaOptions, r as FrameSamplePoint, s as AudioQaStats, t as FrameGridOptions, u as QaIssue, v as resetRenderCaches } from "../index-DcIR_DgU.mjs";
1
+ import { F as describeGpu, L as initHeadlessWebGPU, p as GpuAdapterDescription } from "../index-B1ENegOQ.mjs";
2
+ import { _ as renderSemaphore, a as renderFrameGrid, c as HeadlessMediaRenderer, d as QaSegment, f as RenderProgress, g as formatQaReport, h as VideoQaStats, i as computeGridLayout, l as HeadlessWebGPURenderer, m as VideoQaReport, n as FrameGridRenderable, o as resolveSamplePoints, p as VideoQaOptions, r as FrameSamplePoint, s as AudioQaStats, t as FrameGridOptions, u as QaIssue, v as resetRenderCaches } from "../index-Cg30slle.mjs";
3
3
  export { AudioQaStats, FrameGridOptions, FrameGridRenderable, FrameSamplePoint, GpuAdapterDescription, HeadlessMediaRenderer, HeadlessWebGPURenderer, QaIssue, QaSegment, RenderProgress, VideoQaOptions, VideoQaReport, VideoQaStats, computeGridLayout, describeGpu, formatQaReport, initHeadlessWebGPU, renderFrameGrid, renderSemaphore, resetRenderCaches, resolveSamplePoints };
@@ -1,5 +1,5 @@
1
1
  import "../chunk-D7gtBK74.mjs";
2
- import { R as initHeadlessWebGPU, x as describeGpu } from "../dist-Wsjx_Rza.mjs";
3
- import { a as HeadlessMediaRenderer, c as renderSemaphore, i as resetRenderCaches, n as renderFrameGrid, o as HeadlessWebGPURenderer, r as resolveSamplePoints, s as formatQaReport, t as computeGridLayout } from "../renderer-56T9Wf1N.mjs";
2
+ import { R as initHeadlessWebGPU, x as describeGpu } from "../dist-BHTd-EC1.mjs";
3
+ import { a as HeadlessMediaRenderer, c as renderSemaphore, i as resetRenderCaches, n as renderFrameGrid, o as HeadlessWebGPURenderer, r as resolveSamplePoints, s as formatQaReport, t as computeGridLayout } from "../renderer-DcXxkxds.mjs";
4
4
 
5
5
  export { HeadlessMediaRenderer, HeadlessWebGPURenderer, computeGridLayout, describeGpu, formatQaReport, initHeadlessWebGPU, renderFrameGrid, renderSemaphore, resetRenderCaches, resolveSamplePoints };
@@ -1,7 +1,7 @@
1
1
  import "./chunk-D7gtBK74.mjs";
2
2
  import { n as defineRenderer } from "./renderer-BgmO1Dzw.mjs";
3
- import "./dist-Wsjx_Rza.mjs";
4
- import { c as drawTextureToTarget, l as fragmentBindGroupLayout, o as drawChildToTexture, s as drawFullscreenPass, t as FULLSCREEN_QUAD_WGSL, u as perDevice } from "./gpu-BvYDrqOC.mjs";
3
+ import "./dist-BHTd-EC1.mjs";
4
+ import { c as drawTextureToTarget, l as fragmentBindGroupLayout, o as drawChildToTexture, s as drawFullscreenPass, t as FULLSCREEN_QUAD_WGSL, u as perDevice } from "./gpu-zIhSz_0X.mjs";
5
5
 
6
6
  //#region ../../nodes/node-rgb-split/dist/renderers-BrKktWQX.mjs
7
7
  const OP = "RGBSplit";
@@ -188,4 +188,4 @@ var renderers_default = defineRenderer({ WebGPURenderer: RGBSplitWebGPURenderer
188
188
 
189
189
  //#endregion
190
190
  export { RGBSplitWebGPURenderer, renderers_default as default };
191
- //# sourceMappingURL=renderer-DFGSy_ay.mjs.map
191
+ //# sourceMappingURL=renderer-1-FLx8kd.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"renderer-DFGSy_ay.mjs","names":[],"sources":["../../../nodes/node-rgb-split/dist/renderers-BrKktWQX.mjs"],"sourcesContent":["import { defineRenderer } from \"@framefields/node-sdk/renderer\";\nimport { FULLSCREEN_QUAD_WGSL, drawChildToTexture, drawFullscreenPass, drawTextureToTarget, fragmentBindGroupLayout, perDevice } from \"@framefields/node-sdk/gpu\";\n\n//#region src/renderers/webgpu-renderer.ts\nconst OP = \"RGBSplit\";\nconst UNIFORM_FLOATS = 8;\n/** One fragment shader per pass; pass 0 reads the layer, later passes read the previous pass. */\nconst PASSES = [`struct U { v : array<vec4<f32>, 2> };\n@group(0) @binding(0) var<uniform> u : U;\n@group(0) @binding(1) var src : texture_2d<f32>;\n@group(0) @binding(2) var prev : texture_2d<f32>;\n@group(0) @binding(3) var samp : sampler;\n${FULLSCREEN_QUAD_WGSL}\n\nfn straight(c : vec4<f32>) -> vec4<f32> {\n\tif (c.a <= 0.0001) { return vec4<f32>(0.0); }\n\treturn vec4<f32>(c.rgb / c.a, c.a);\n}\nfn premul(c : vec4<f32>) -> vec4<f32> { return vec4<f32>(c.rgb * c.a, c.a); }\nfn res() -> vec2<f32> { return vec2<f32>(textureDimensions(src)); }\n/** Straight-alpha color of the layer at a uv (filtered). */\nfn sampleS(uv : vec2<f32>) -> vec4<f32> { return straight(textureSampleLevel(src, samp, uv, 0.0)); }\n/** Premultiplied color of the layer at a uv (filtered). */\nfn sampleP(uv : vec2<f32>) -> vec4<f32> { return textureSampleLevel(src, samp, uv, 0.0); }\n/** Straight-alpha color of the layer's texel at a pixel position (nearest). */\nfn texelS(p : vec2<f32>) -> vec4<f32> {\n\tlet d = vec2<i32>(textureDimensions(src));\n\tlet q = clamp(vec2<i32>(floor(p)), vec2<i32>(0), d - vec2<i32>(1));\n\treturn straight(textureLoad(src, q, 0));\n}\nfn luma(c : vec3<f32>) -> f32 { return dot(c, vec3<f32>(0.2126, 0.7152, 0.0722)); }\nfn hash12(p : vec2<f32>) -> f32 {\n\tvar p3 = fract(vec3<f32>(p.xyx) * 0.1031);\n\tp3 += dot(p3, p3.yzx + 33.33);\n\treturn fract((p3.x + p3.y) * p3.z);\n}\nfn vnoise(p : vec2<f32>) -> f32 {\n\tlet i = floor(p);\n\tlet f = fract(p);\n\tlet u = f * f * (3.0 - 2.0 * f);\n\treturn mix(mix(hash12(i), hash12(i + vec2<f32>(1.0, 0.0)), u.x),\n\t\tmix(hash12(i + vec2<f32>(0.0, 1.0)), hash12(i + vec2<f32>(1.0, 1.0)), u.x), u.y);\n}\n/** 8x8 ordered-dither threshold, 0..1. */\nfn bayer8(p : vec2<f32>) -> f32 {\n\tlet x = u32(p.x) & 7u;\n\tlet y = u32(p.y) & 7u;\n\tvar v = 0u;\n\tfor (var k = 0u; k < 3u; k++) {\n\t\tlet bx = (x >> k) & 1u;\n\t\tlet by = (y >> k) & 1u;\n\t\tlet m = ((bx ^ by) << 1u) | by;\n\t\tv += m * (1u << (2u * (2u - k)));\n\t}\n\treturn (f32(v) + 0.5) / 64.0;\n}\nfn rot2(a : f32) -> mat2x2<f32> { let c = cos(a); let s = sin(a); return mat2x2<f32>(c, s, -s, c); }\n@fragment fn fs(in : VSOut) -> @location(0) vec4<f32> {\n\tlet time = u.v[0].x;\n\tlet frame = u.v[0].y;\n\tlet amount = u.v[1].x;\n\tlet angle = u.v[1].y;\n\tlet radial = u.v[1].z;\n\n\tlet R = res();\n\tlet a = radians(angle);\n\tvar d = vec2<f32>(cos(a), sin(a)) * amount / R;\n\tif (radial > 0.5) { d = (in.uv - 0.5) * 2.0 * amount / R; }\n\tlet cr = sampleP(in.uv + d);\n\tlet cg = sampleP(in.uv);\n\tlet cb = sampleP(in.uv - d);\n\treturn vec4<f32>(cr.r, cg.g, cb.b, max(max(cr.a, cg.a), cb.a));\n}\n`];\nconst resources = perDevice((device) => ({\n\tlayout: fragmentBindGroupLayout(device, [\n\t\t\"uniform\",\n\t\t\"texture\",\n\t\t\"texture\",\n\t\t\"sampler\"\n\t], `${OP}_layout`),\n\tpipelines: /* @__PURE__ */ new Map(),\n\tsampler: device.createSampler({\n\t\tmagFilter: \"linear\",\n\t\tminFilter: \"linear\",\n\t\taddressModeU: \"clamp-to-edge\",\n\t\taddressModeV: \"clamp-to-edge\"\n\t})\n}));\nfunction pipelinesFor(device, format) {\n\tconst r = resources(device);\n\tlet list = r.pipelines.get(format);\n\tif (!list) {\n\t\tlist = PASSES.map((code, i) => {\n\t\t\tconst module = device.createShaderModule({\n\t\t\t\tcode,\n\t\t\t\tlabel: `${OP}_${i}.wgsl`\n\t\t\t});\n\t\t\treturn device.createRenderPipeline({\n\t\t\t\tlabel: `${OP}_${i}`,\n\t\t\t\tlayout: device.createPipelineLayout({ bindGroupLayouts: [r.layout] }),\n\t\t\t\tvertex: {\n\t\t\t\t\tmodule,\n\t\t\t\t\tentryPoint: \"vs\"\n\t\t\t\t},\n\t\t\t\tfragment: {\n\t\t\t\t\tmodule,\n\t\t\t\t\tentryPoint: \"fs\",\n\t\t\t\t\ttargets: [{ format }]\n\t\t\t\t},\n\t\t\t\tprimitive: { topology: \"triangle-strip\" }\n\t\t\t});\n\t\t});\n\t\tr.pipelines.set(format, list);\n\t}\n\treturn list;\n}\nfunction num(v, fallback) {\n\tconst n = typeof v === \"number\" ? v : Number(v);\n\treturn Number.isFinite(n) ? n : fallback;\n}\n/** Packs the effect's resolved props into the uniform block the shaders read. */\nfunction pack(raw, data, time, frame) {\n\tdata.fill(0);\n\tdata[0] = time;\n\tdata[1] = frame;\n\tdata[4] = num(raw.amount, 8);\n\tdata[5] = num(raw.angle, 0);\n\tdata[6] = raw.radial === void 0 ? 0 : raw.radial ? 1 : 0;\n}\nconst RGBSplitWebGPURenderer = async (args) => {\n\tconst { ctx, encoder, pass, targetTexture, targetWidth, targetHeight, props } = args;\n\tconst op = props.virtualMedia?.operation;\n\tif (op?.op !== OP) return;\n\tconst child = props.virtualMedia?.children?.[0];\n\tif (!child) return;\n\tpass.end();\n\tconst fps = props.fps || 30;\n\tconst frame = Math.round(props.compositionFrame ?? props.frame ?? 0);\n\tconst raw = op.effect?.resolveUniforms ? op.effect.resolveUniforms({\n\t\tframe,\n\t\tfps,\n\t\ttime: frame / fps\n\t}) : op;\n\tconst data = new Float32Array(UNIFORM_FLOATS);\n\tpack(raw, data, frame / fps, frame);\n\tconst { texture: childTex } = await drawChildToTexture(args, child);\n\tconst device = ctx.device;\n\tconst r = resources(device);\n\tconst pipelines = pipelinesFor(device, ctx.renderer.format);\n\tconst ubo = ctx.renderer.getTemporaryBuffer(data);\n\tlet prev = childTex;\n\tfor (const pipeline of pipelines) {\n\t\tconst out = ctx.renderer.getTemporaryTexture(targetWidth, targetHeight, [\n\t\t\tchildTex,\n\t\t\tprev,\n\t\t\ttargetTexture,\n\t\t\t...props.excludeTextures || []\n\t\t]);\n\t\tdrawFullscreenPass(encoder, out.createView(), pipeline, [device.createBindGroup({\n\t\t\tlayout: r.layout,\n\t\t\tentries: [\n\t\t\t\t{\n\t\t\t\t\tbinding: 0,\n\t\t\t\t\tresource: { buffer: ubo }\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tbinding: 1,\n\t\t\t\t\tresource: childTex.createView()\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tbinding: 2,\n\t\t\t\t\tresource: prev.createView()\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tbinding: 3,\n\t\t\t\t\tresource: r.sampler\n\t\t\t\t}\n\t\t\t]\n\t\t})]);\n\t\tprev = out;\n\t}\n\tdrawTextureToTarget(args, prev);\n};\n\n//#endregion\n//#region src/renderers/index.ts\nvar renderers_default = defineRenderer({ WebGPURenderer: RGBSplitWebGPURenderer });\n\n//#endregion\nexport { RGBSplitWebGPURenderer as n, renderers_default as t };\n//# sourceMappingURL=renderers-BrKktWQX.mjs.map"],"mappings":"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"}
1
+ {"version":3,"file":"renderer-1-FLx8kd.mjs","names":[],"sources":["../../../nodes/node-rgb-split/dist/renderers-BrKktWQX.mjs"],"sourcesContent":["import { defineRenderer } from \"@framefields/node-sdk/renderer\";\nimport { FULLSCREEN_QUAD_WGSL, drawChildToTexture, drawFullscreenPass, drawTextureToTarget, fragmentBindGroupLayout, perDevice } from \"@framefields/node-sdk/gpu\";\n\n//#region src/renderers/webgpu-renderer.ts\nconst OP = \"RGBSplit\";\nconst UNIFORM_FLOATS = 8;\n/** One fragment shader per pass; pass 0 reads the layer, later passes read the previous pass. */\nconst PASSES = [`struct U { v : array<vec4<f32>, 2> };\n@group(0) @binding(0) var<uniform> u : U;\n@group(0) @binding(1) var src : texture_2d<f32>;\n@group(0) @binding(2) var prev : texture_2d<f32>;\n@group(0) @binding(3) var samp : sampler;\n${FULLSCREEN_QUAD_WGSL}\n\nfn straight(c : vec4<f32>) -> vec4<f32> {\n\tif (c.a <= 0.0001) { return vec4<f32>(0.0); }\n\treturn vec4<f32>(c.rgb / c.a, c.a);\n}\nfn premul(c : vec4<f32>) -> vec4<f32> { return vec4<f32>(c.rgb * c.a, c.a); }\nfn res() -> vec2<f32> { return vec2<f32>(textureDimensions(src)); }\n/** Straight-alpha color of the layer at a uv (filtered). */\nfn sampleS(uv : vec2<f32>) -> vec4<f32> { return straight(textureSampleLevel(src, samp, uv, 0.0)); }\n/** Premultiplied color of the layer at a uv (filtered). */\nfn sampleP(uv : vec2<f32>) -> vec4<f32> { return textureSampleLevel(src, samp, uv, 0.0); }\n/** Straight-alpha color of the layer's texel at a pixel position (nearest). */\nfn texelS(p : vec2<f32>) -> vec4<f32> {\n\tlet d = vec2<i32>(textureDimensions(src));\n\tlet q = clamp(vec2<i32>(floor(p)), vec2<i32>(0), d - vec2<i32>(1));\n\treturn straight(textureLoad(src, q, 0));\n}\nfn luma(c : vec3<f32>) -> f32 { return dot(c, vec3<f32>(0.2126, 0.7152, 0.0722)); }\nfn hash12(p : vec2<f32>) -> f32 {\n\tvar p3 = fract(vec3<f32>(p.xyx) * 0.1031);\n\tp3 += dot(p3, p3.yzx + 33.33);\n\treturn fract((p3.x + p3.y) * p3.z);\n}\nfn vnoise(p : vec2<f32>) -> f32 {\n\tlet i = floor(p);\n\tlet f = fract(p);\n\tlet u = f * f * (3.0 - 2.0 * f);\n\treturn mix(mix(hash12(i), hash12(i + vec2<f32>(1.0, 0.0)), u.x),\n\t\tmix(hash12(i + vec2<f32>(0.0, 1.0)), hash12(i + vec2<f32>(1.0, 1.0)), u.x), u.y);\n}\n/** 8x8 ordered-dither threshold, 0..1. */\nfn bayer8(p : vec2<f32>) -> f32 {\n\tlet x = u32(p.x) & 7u;\n\tlet y = u32(p.y) & 7u;\n\tvar v = 0u;\n\tfor (var k = 0u; k < 3u; k++) {\n\t\tlet bx = (x >> k) & 1u;\n\t\tlet by = (y >> k) & 1u;\n\t\tlet m = ((bx ^ by) << 1u) | by;\n\t\tv += m * (1u << (2u * (2u - k)));\n\t}\n\treturn (f32(v) + 0.5) / 64.0;\n}\nfn rot2(a : f32) -> mat2x2<f32> { let c = cos(a); let s = sin(a); return mat2x2<f32>(c, s, -s, c); }\n@fragment fn fs(in : VSOut) -> @location(0) vec4<f32> {\n\tlet time = u.v[0].x;\n\tlet frame = u.v[0].y;\n\tlet amount = u.v[1].x;\n\tlet angle = u.v[1].y;\n\tlet radial = u.v[1].z;\n\n\tlet R = res();\n\tlet a = radians(angle);\n\tvar d = vec2<f32>(cos(a), sin(a)) * amount / R;\n\tif (radial > 0.5) { d = (in.uv - 0.5) * 2.0 * amount / R; }\n\tlet cr = sampleP(in.uv + d);\n\tlet cg = sampleP(in.uv);\n\tlet cb = sampleP(in.uv - d);\n\treturn vec4<f32>(cr.r, cg.g, cb.b, max(max(cr.a, cg.a), cb.a));\n}\n`];\nconst resources = perDevice((device) => ({\n\tlayout: fragmentBindGroupLayout(device, [\n\t\t\"uniform\",\n\t\t\"texture\",\n\t\t\"texture\",\n\t\t\"sampler\"\n\t], `${OP}_layout`),\n\tpipelines: /* @__PURE__ */ new Map(),\n\tsampler: device.createSampler({\n\t\tmagFilter: \"linear\",\n\t\tminFilter: \"linear\",\n\t\taddressModeU: \"clamp-to-edge\",\n\t\taddressModeV: \"clamp-to-edge\"\n\t})\n}));\nfunction pipelinesFor(device, format) {\n\tconst r = resources(device);\n\tlet list = r.pipelines.get(format);\n\tif (!list) {\n\t\tlist = PASSES.map((code, i) => {\n\t\t\tconst module = device.createShaderModule({\n\t\t\t\tcode,\n\t\t\t\tlabel: `${OP}_${i}.wgsl`\n\t\t\t});\n\t\t\treturn device.createRenderPipeline({\n\t\t\t\tlabel: `${OP}_${i}`,\n\t\t\t\tlayout: device.createPipelineLayout({ bindGroupLayouts: [r.layout] }),\n\t\t\t\tvertex: {\n\t\t\t\t\tmodule,\n\t\t\t\t\tentryPoint: \"vs\"\n\t\t\t\t},\n\t\t\t\tfragment: {\n\t\t\t\t\tmodule,\n\t\t\t\t\tentryPoint: \"fs\",\n\t\t\t\t\ttargets: [{ format }]\n\t\t\t\t},\n\t\t\t\tprimitive: { topology: \"triangle-strip\" }\n\t\t\t});\n\t\t});\n\t\tr.pipelines.set(format, list);\n\t}\n\treturn list;\n}\nfunction num(v, fallback) {\n\tconst n = typeof v === \"number\" ? v : Number(v);\n\treturn Number.isFinite(n) ? n : fallback;\n}\n/** Packs the effect's resolved props into the uniform block the shaders read. */\nfunction pack(raw, data, time, frame) {\n\tdata.fill(0);\n\tdata[0] = time;\n\tdata[1] = frame;\n\tdata[4] = num(raw.amount, 8);\n\tdata[5] = num(raw.angle, 0);\n\tdata[6] = raw.radial === void 0 ? 0 : raw.radial ? 1 : 0;\n}\nconst RGBSplitWebGPURenderer = async (args) => {\n\tconst { ctx, encoder, pass, targetTexture, targetWidth, targetHeight, props } = args;\n\tconst op = props.virtualMedia?.operation;\n\tif (op?.op !== OP) return;\n\tconst child = props.virtualMedia?.children?.[0];\n\tif (!child) return;\n\tpass.end();\n\tconst fps = props.fps || 30;\n\tconst frame = Math.round(props.compositionFrame ?? props.frame ?? 0);\n\tconst raw = op.effect?.resolveUniforms ? op.effect.resolveUniforms({\n\t\tframe,\n\t\tfps,\n\t\ttime: frame / fps\n\t}) : op;\n\tconst data = new Float32Array(UNIFORM_FLOATS);\n\tpack(raw, data, frame / fps, frame);\n\tconst { texture: childTex } = await drawChildToTexture(args, child);\n\tconst device = ctx.device;\n\tconst r = resources(device);\n\tconst pipelines = pipelinesFor(device, ctx.renderer.format);\n\tconst ubo = ctx.renderer.getTemporaryBuffer(data);\n\tlet prev = childTex;\n\tfor (const pipeline of pipelines) {\n\t\tconst out = ctx.renderer.getTemporaryTexture(targetWidth, targetHeight, [\n\t\t\tchildTex,\n\t\t\tprev,\n\t\t\ttargetTexture,\n\t\t\t...props.excludeTextures || []\n\t\t]);\n\t\tdrawFullscreenPass(encoder, out.createView(), pipeline, [device.createBindGroup({\n\t\t\tlayout: r.layout,\n\t\t\tentries: [\n\t\t\t\t{\n\t\t\t\t\tbinding: 0,\n\t\t\t\t\tresource: { buffer: ubo }\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tbinding: 1,\n\t\t\t\t\tresource: childTex.createView()\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tbinding: 2,\n\t\t\t\t\tresource: prev.createView()\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tbinding: 3,\n\t\t\t\t\tresource: r.sampler\n\t\t\t\t}\n\t\t\t]\n\t\t})]);\n\t\tprev = out;\n\t}\n\tdrawTextureToTarget(args, prev);\n};\n\n//#endregion\n//#region src/renderers/index.ts\nvar renderers_default = defineRenderer({ WebGPURenderer: RGBSplitWebGPURenderer });\n\n//#endregion\nexport { RGBSplitWebGPURenderer as n, renderers_default as t };\n//# sourceMappingURL=renderers-BrKktWQX.mjs.map"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { n as defineRenderer } from "./renderer-BgmO1Dzw.mjs";
2
- import { i as VideoResultSchema, n as ImageResultSchema } from "./dist-PlaSQJ43.mjs";
3
- import { b as colorMatrixWgsl } from "./dist-Wsjx_Rza.mjs";
4
- import { a as createFullscreenPipeline, c as drawTextureToTarget, l as fragmentBindGroupLayout, o as drawChildToTexture, s as drawFullscreenPass } from "./gpu-BvYDrqOC.mjs";
2
+ import { i as VideoResultSchema, n as ImageResultSchema } from "./dist-CPtFZxtu.mjs";
3
+ import { b as colorMatrixWgsl } from "./dist-BHTd-EC1.mjs";
4
+ import { a as createFullscreenPipeline, c as drawTextureToTarget, l as fragmentBindGroupLayout, o as drawChildToTexture, s as drawFullscreenPass } from "./gpu-zIhSz_0X.mjs";
5
5
  import { z } from "zod";
6
6
 
7
7
  //#region ../../nodes/node-levels/dist/renderers-DdSgwTj2.mjs
@@ -173,4 +173,4 @@ var renderers_default = defineRenderer({ WebGPURenderer: LevelsWebGPURenderer })
173
173
 
174
174
  //#endregion
175
175
  export { renderers_default as t };
176
- //# sourceMappingURL=renderer-Ck74BZ_L.mjs.map
176
+ //# sourceMappingURL=renderer-47d3etrX.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"renderer-Ck74BZ_L.mjs","names":[],"sources":["../../../nodes/node-levels/dist/renderers-DdSgwTj2.mjs"],"sourcesContent":["import { ImageResultSchema, VideoResultSchema } from \"@framefields/node-sdk\";\nimport { z } from \"zod\";\nimport { defineRenderer } from \"@framefields/node-sdk/renderer\";\nimport { createFullscreenPipeline, drawChildToTexture, drawFullscreenPass, drawTextureToTarget, fragmentBindGroupLayout } from \"@framefields/node-sdk/gpu\";\nimport { colorMatrixWgsl } from \"@framefields/webgpu-renderers\";\n\n//#region src/shared/config.ts\nconst LevelChannelSchema = z.object({\n\tinBlack: z.number().min(0).max(1).default(0),\n\tinWhite: z.number().min(0).max(1).default(1),\n\toutBlack: z.number().min(0).max(1).default(0),\n\toutWhite: z.number().min(0).max(1).default(1)\n}).strict().superRefine((val, ctx) => {\n\tif (val.inBlack >= val.inWhite) ctx.addIssue({\n\t\tcode: z.ZodIssueCode.custom,\n\t\tpath: [\"inWhite\"],\n\t\tmessage: \"inWhite must be greater than inBlack\"\n\t});\n\tif (val.outBlack >= val.outWhite) ctx.addIssue({\n\t\tcode: z.ZodIssueCode.custom,\n\t\tpath: [\"outWhite\"],\n\t\tmessage: \"outWhite must be greater than outBlack\"\n\t});\n});\nconst LevelsNodeConfigSchema = z.object({\n\tmaster: LevelChannelSchema.default({\n\t\tinBlack: 0,\n\t\tinWhite: 1,\n\t\toutBlack: 0,\n\t\toutWhite: 1\n\t}),\n\tred: LevelChannelSchema.default({\n\t\tinBlack: 0,\n\t\tinWhite: 1,\n\t\toutBlack: 0,\n\t\toutWhite: 1\n\t}),\n\tgreen: LevelChannelSchema.default({\n\t\tinBlack: 0,\n\t\tinWhite: 1,\n\t\toutBlack: 0,\n\t\toutWhite: 1\n\t}),\n\tblue: LevelChannelSchema.default({\n\t\tinBlack: 0,\n\t\tinWhite: 1,\n\t\toutBlack: 0,\n\t\toutWhite: 1\n\t})\n}).strict();\nconst LevelsResultSchema = z.union([ImageResultSchema, VideoResultSchema]);\nconst LevelsOperationSchema = LevelsNodeConfigSchema.extend({\n\top: z.literal(\"Levels\"),\n\tmetadata: z.unknown().optional()\n});\nconst defaultLevelChannel = {\n\tinBlack: 0,\n\tinWhite: 1,\n\toutBlack: 0,\n\toutWhite: 1\n};\nconst defaultLevelsConfig = {\n\tmaster: { ...defaultLevelChannel },\n\tred: { ...defaultLevelChannel },\n\tgreen: { ...defaultLevelChannel },\n\tblue: { ...defaultLevelChannel }\n};\nconst LEVELS_OUTPUT_TYPE_MAP = {\n\tVideo: \"Video\",\n\tLottie: \"Video\",\n\tGIF: \"GIF\",\n\tImage: \"Image\",\n\tSVG: \"Image\"\n};\n\n//#endregion\n//#region src/renderers/webgpu-renderer.ts\nconst deviceResourceCache = /* @__PURE__ */ new WeakMap();\nconst colorMatrixData = new Float32Array(20);\nfunction getColorMatrixResources(device, format) {\n\tlet res = deviceResourceCache.get(device);\n\tif (!res) {\n\t\tconst colorMatrixUniformLayout = fragmentBindGroupLayout(device, [\"uniform\"]);\n\t\tconst singleTextureLayout = fragmentBindGroupLayout(device, [\"texture\", \"sampler\"]);\n\t\tres = {\n\t\t\tcolorMatrixPipeline: createFullscreenPipeline(device, {\n\t\t\t\tlabel: \"ColorMatrixPipeline\",\n\t\t\t\tmodule: device.createShaderModule({\n\t\t\t\t\tlabel: \"color-matrix.wgsl\",\n\t\t\t\t\tcode: colorMatrixWgsl\n\t\t\t\t}),\n\t\t\t\tlayouts: [colorMatrixUniformLayout, singleTextureLayout],\n\t\t\t\tformat\n\t\t\t}),\n\t\t\tcolorMatrixUniformLayout,\n\t\t\tsingleTextureLayout\n\t\t};\n\t\tdeviceResourceCache.set(device, res);\n\t}\n\treturn res;\n}\nfunction getTransform(ch) {\n\tconst inRange = Math.max(ch.inWhite - ch.inBlack, 1e-5);\n\tconst scale = (ch.outWhite - ch.outBlack) / inRange;\n\treturn {\n\t\tscale,\n\t\tbias: ch.outBlack - ch.inBlack * scale\n\t};\n}\nconst LevelsWebGPURenderer = async (args) => {\n\tconst { ctx, encoder, pass, targetTexture, targetWidth, targetHeight, props } = args;\n\tconst op = props.virtualMedia?.operation;\n\tif (op?.op !== \"Levels\" || !props.virtualMedia?.children?.[0]) return;\n\tpass.end();\n\tconst childMedia = props.virtualMedia.children[0];\n\tconst m = getTransform(op.master ?? defaultLevelChannel);\n\tconst r = getTransform(op.red ?? defaultLevelChannel);\n\tconst g = getTransform(op.green ?? defaultLevelChannel);\n\tconst b = getTransform(op.blue ?? defaultLevelChannel);\n\tconst matrix = [\n\t\tr.scale * m.scale,\n\t\t0,\n\t\t0,\n\t\t0,\n\t\tr.bias * m.scale + m.bias,\n\t\t0,\n\t\tg.scale * m.scale,\n\t\t0,\n\t\t0,\n\t\tg.bias * m.scale + m.bias,\n\t\t0,\n\t\t0,\n\t\tb.scale * m.scale,\n\t\t0,\n\t\tb.bias * m.scale + m.bias,\n\t\t0,\n\t\t0,\n\t\t0,\n\t\t1,\n\t\t0\n\t];\n\tconst width = targetWidth;\n\tconst height = targetHeight;\n\tconst { texture: tmpTex } = await drawChildToTexture(args, childMedia);\n\tconst { colorMatrixPipeline: pipeline, colorMatrixUniformLayout: uLayout, singleTextureLayout: tLayout } = getColorMatrixResources(ctx.device, ctx.renderer.format);\n\tconst sampler = ctx.renderer.samplerCache.getSampler(ctx.device);\n\tcolorMatrixData[0] = matrix[0];\n\tcolorMatrixData[1] = matrix[1];\n\tcolorMatrixData[2] = matrix[2];\n\tcolorMatrixData[3] = matrix[3];\n\tcolorMatrixData[4] = matrix[5];\n\tcolorMatrixData[5] = matrix[6];\n\tcolorMatrixData[6] = matrix[7];\n\tcolorMatrixData[7] = matrix[8];\n\tcolorMatrixData[8] = matrix[10];\n\tcolorMatrixData[9] = matrix[11];\n\tcolorMatrixData[10] = matrix[12];\n\tcolorMatrixData[11] = matrix[13];\n\tcolorMatrixData[12] = matrix[15];\n\tcolorMatrixData[13] = matrix[16];\n\tcolorMatrixData[14] = matrix[17];\n\tcolorMatrixData[15] = matrix[18];\n\tcolorMatrixData[16] = matrix[4];\n\tcolorMatrixData[17] = matrix[9];\n\tcolorMatrixData[18] = matrix[14];\n\tcolorMatrixData[19] = matrix[19];\n\tconst buffer = ctx.renderer.getTemporaryBuffer(colorMatrixData);\n\tconst outTex = ctx.renderer.getTemporaryTexture(width, height, [\n\t\ttmpTex,\n\t\ttargetTexture,\n\t\t...props.excludeTextures || []\n\t]);\n\tdrawFullscreenPass(encoder, outTex.createView(), pipeline, [ctx.device.createBindGroup({\n\t\tlayout: uLayout,\n\t\tentries: [{\n\t\t\tbinding: 0,\n\t\t\tresource: { buffer }\n\t\t}]\n\t}), ctx.renderer.bindGroupCache.getBindGroup(ctx.device, tLayout, tmpTex, sampler)]);\n\tdrawTextureToTarget(args, outTex, props.opacity ?? 1);\n};\n\n//#endregion\n//#region src/renderers/index.ts\nvar renderers_default = defineRenderer({ WebGPURenderer: LevelsWebGPURenderer });\n\n//#endregion\nexport { LevelsOperationSchema as a, defaultLevelsConfig as c, LevelsNodeConfigSchema as i, LEVELS_OUTPUT_TYPE_MAP as n, LevelsResultSchema as o, LevelChannelSchema as r, defaultLevelChannel as s, renderers_default as t };\n//# sourceMappingURL=renderers-DdSgwTj2.mjs.map"],"mappings":"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"}
1
+ {"version":3,"file":"renderer-47d3etrX.mjs","names":[],"sources":["../../../nodes/node-levels/dist/renderers-DdSgwTj2.mjs"],"sourcesContent":["import { ImageResultSchema, VideoResultSchema } from \"@framefields/node-sdk\";\nimport { z } from \"zod\";\nimport { defineRenderer } from \"@framefields/node-sdk/renderer\";\nimport { createFullscreenPipeline, drawChildToTexture, drawFullscreenPass, drawTextureToTarget, fragmentBindGroupLayout } from \"@framefields/node-sdk/gpu\";\nimport { colorMatrixWgsl } from \"@framefields/webgpu-renderers\";\n\n//#region src/shared/config.ts\nconst LevelChannelSchema = z.object({\n\tinBlack: z.number().min(0).max(1).default(0),\n\tinWhite: z.number().min(0).max(1).default(1),\n\toutBlack: z.number().min(0).max(1).default(0),\n\toutWhite: z.number().min(0).max(1).default(1)\n}).strict().superRefine((val, ctx) => {\n\tif (val.inBlack >= val.inWhite) ctx.addIssue({\n\t\tcode: z.ZodIssueCode.custom,\n\t\tpath: [\"inWhite\"],\n\t\tmessage: \"inWhite must be greater than inBlack\"\n\t});\n\tif (val.outBlack >= val.outWhite) ctx.addIssue({\n\t\tcode: z.ZodIssueCode.custom,\n\t\tpath: [\"outWhite\"],\n\t\tmessage: \"outWhite must be greater than outBlack\"\n\t});\n});\nconst LevelsNodeConfigSchema = z.object({\n\tmaster: LevelChannelSchema.default({\n\t\tinBlack: 0,\n\t\tinWhite: 1,\n\t\toutBlack: 0,\n\t\toutWhite: 1\n\t}),\n\tred: LevelChannelSchema.default({\n\t\tinBlack: 0,\n\t\tinWhite: 1,\n\t\toutBlack: 0,\n\t\toutWhite: 1\n\t}),\n\tgreen: LevelChannelSchema.default({\n\t\tinBlack: 0,\n\t\tinWhite: 1,\n\t\toutBlack: 0,\n\t\toutWhite: 1\n\t}),\n\tblue: LevelChannelSchema.default({\n\t\tinBlack: 0,\n\t\tinWhite: 1,\n\t\toutBlack: 0,\n\t\toutWhite: 1\n\t})\n}).strict();\nconst LevelsResultSchema = z.union([ImageResultSchema, VideoResultSchema]);\nconst LevelsOperationSchema = LevelsNodeConfigSchema.extend({\n\top: z.literal(\"Levels\"),\n\tmetadata: z.unknown().optional()\n});\nconst defaultLevelChannel = {\n\tinBlack: 0,\n\tinWhite: 1,\n\toutBlack: 0,\n\toutWhite: 1\n};\nconst defaultLevelsConfig = {\n\tmaster: { ...defaultLevelChannel },\n\tred: { ...defaultLevelChannel },\n\tgreen: { ...defaultLevelChannel },\n\tblue: { ...defaultLevelChannel }\n};\nconst LEVELS_OUTPUT_TYPE_MAP = {\n\tVideo: \"Video\",\n\tLottie: \"Video\",\n\tGIF: \"GIF\",\n\tImage: \"Image\",\n\tSVG: \"Image\"\n};\n\n//#endregion\n//#region src/renderers/webgpu-renderer.ts\nconst deviceResourceCache = /* @__PURE__ */ new WeakMap();\nconst colorMatrixData = new Float32Array(20);\nfunction getColorMatrixResources(device, format) {\n\tlet res = deviceResourceCache.get(device);\n\tif (!res) {\n\t\tconst colorMatrixUniformLayout = fragmentBindGroupLayout(device, [\"uniform\"]);\n\t\tconst singleTextureLayout = fragmentBindGroupLayout(device, [\"texture\", \"sampler\"]);\n\t\tres = {\n\t\t\tcolorMatrixPipeline: createFullscreenPipeline(device, {\n\t\t\t\tlabel: \"ColorMatrixPipeline\",\n\t\t\t\tmodule: device.createShaderModule({\n\t\t\t\t\tlabel: \"color-matrix.wgsl\",\n\t\t\t\t\tcode: colorMatrixWgsl\n\t\t\t\t}),\n\t\t\t\tlayouts: [colorMatrixUniformLayout, singleTextureLayout],\n\t\t\t\tformat\n\t\t\t}),\n\t\t\tcolorMatrixUniformLayout,\n\t\t\tsingleTextureLayout\n\t\t};\n\t\tdeviceResourceCache.set(device, res);\n\t}\n\treturn res;\n}\nfunction getTransform(ch) {\n\tconst inRange = Math.max(ch.inWhite - ch.inBlack, 1e-5);\n\tconst scale = (ch.outWhite - ch.outBlack) / inRange;\n\treturn {\n\t\tscale,\n\t\tbias: ch.outBlack - ch.inBlack * scale\n\t};\n}\nconst LevelsWebGPURenderer = async (args) => {\n\tconst { ctx, encoder, pass, targetTexture, targetWidth, targetHeight, props } = args;\n\tconst op = props.virtualMedia?.operation;\n\tif (op?.op !== \"Levels\" || !props.virtualMedia?.children?.[0]) return;\n\tpass.end();\n\tconst childMedia = props.virtualMedia.children[0];\n\tconst m = getTransform(op.master ?? defaultLevelChannel);\n\tconst r = getTransform(op.red ?? defaultLevelChannel);\n\tconst g = getTransform(op.green ?? defaultLevelChannel);\n\tconst b = getTransform(op.blue ?? defaultLevelChannel);\n\tconst matrix = [\n\t\tr.scale * m.scale,\n\t\t0,\n\t\t0,\n\t\t0,\n\t\tr.bias * m.scale + m.bias,\n\t\t0,\n\t\tg.scale * m.scale,\n\t\t0,\n\t\t0,\n\t\tg.bias * m.scale + m.bias,\n\t\t0,\n\t\t0,\n\t\tb.scale * m.scale,\n\t\t0,\n\t\tb.bias * m.scale + m.bias,\n\t\t0,\n\t\t0,\n\t\t0,\n\t\t1,\n\t\t0\n\t];\n\tconst width = targetWidth;\n\tconst height = targetHeight;\n\tconst { texture: tmpTex } = await drawChildToTexture(args, childMedia);\n\tconst { colorMatrixPipeline: pipeline, colorMatrixUniformLayout: uLayout, singleTextureLayout: tLayout } = getColorMatrixResources(ctx.device, ctx.renderer.format);\n\tconst sampler = ctx.renderer.samplerCache.getSampler(ctx.device);\n\tcolorMatrixData[0] = matrix[0];\n\tcolorMatrixData[1] = matrix[1];\n\tcolorMatrixData[2] = matrix[2];\n\tcolorMatrixData[3] = matrix[3];\n\tcolorMatrixData[4] = matrix[5];\n\tcolorMatrixData[5] = matrix[6];\n\tcolorMatrixData[6] = matrix[7];\n\tcolorMatrixData[7] = matrix[8];\n\tcolorMatrixData[8] = matrix[10];\n\tcolorMatrixData[9] = matrix[11];\n\tcolorMatrixData[10] = matrix[12];\n\tcolorMatrixData[11] = matrix[13];\n\tcolorMatrixData[12] = matrix[15];\n\tcolorMatrixData[13] = matrix[16];\n\tcolorMatrixData[14] = matrix[17];\n\tcolorMatrixData[15] = matrix[18];\n\tcolorMatrixData[16] = matrix[4];\n\tcolorMatrixData[17] = matrix[9];\n\tcolorMatrixData[18] = matrix[14];\n\tcolorMatrixData[19] = matrix[19];\n\tconst buffer = ctx.renderer.getTemporaryBuffer(colorMatrixData);\n\tconst outTex = ctx.renderer.getTemporaryTexture(width, height, [\n\t\ttmpTex,\n\t\ttargetTexture,\n\t\t...props.excludeTextures || []\n\t]);\n\tdrawFullscreenPass(encoder, outTex.createView(), pipeline, [ctx.device.createBindGroup({\n\t\tlayout: uLayout,\n\t\tentries: [{\n\t\t\tbinding: 0,\n\t\t\tresource: { buffer }\n\t\t}]\n\t}), ctx.renderer.bindGroupCache.getBindGroup(ctx.device, tLayout, tmpTex, sampler)]);\n\tdrawTextureToTarget(args, outTex, props.opacity ?? 1);\n};\n\n//#endregion\n//#region src/renderers/index.ts\nvar renderers_default = defineRenderer({ WebGPURenderer: LevelsWebGPURenderer });\n\n//#endregion\nexport { LevelsOperationSchema as a, defaultLevelsConfig as c, LevelsNodeConfigSchema as i, LEVELS_OUTPUT_TYPE_MAP as n, LevelsResultSchema as o, LevelChannelSchema as r, defaultLevelChannel as s, renderers_default as t };\n//# sourceMappingURL=renderers-DdSgwTj2.mjs.map"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { n as defineRenderer, s as resolveHandleParam } from "./renderer-BgmO1Dzw.mjs";
2
- import { a as configBuilder, i as VideoResultSchema, n as ImageResultSchema } from "./dist-PlaSQJ43.mjs";
3
- import { a as createFullscreenPipeline, c as drawTextureToTarget, d as signalTextureView, f as signalTiming, l as fragmentBindGroupLayout, o as drawChildToTexture, p as texturesLayout, s as drawFullscreenPass, t as FULLSCREEN_QUAD_WGSL } from "./gpu-BvYDrqOC.mjs";
2
+ import { a as configBuilder, i as VideoResultSchema, n as ImageResultSchema } from "./dist-CPtFZxtu.mjs";
3
+ import { a as createFullscreenPipeline, c as drawTextureToTarget, d as signalTextureView, f as signalTiming, l as fragmentBindGroupLayout, o as drawChildToTexture, p as texturesLayout, s as drawFullscreenPass, t as FULLSCREEN_QUAD_WGSL } from "./gpu-zIhSz_0X.mjs";
4
4
  import { z } from "zod";
5
5
 
6
6
  //#region ../../nodes/node-high-pass/dist/renderers-DUL8Peaw.mjs
@@ -253,4 +253,4 @@ var renderers_default = defineRenderer({ WebGPURenderer });
253
253
 
254
254
  //#endregion
255
255
  export { renderers_default as t };
256
- //# sourceMappingURL=renderer-Cs8Od5S8.mjs.map
256
+ //# sourceMappingURL=renderer-6GCG6JxD.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"renderer-Cs8Od5S8.mjs","names":[],"sources":["../../../nodes/node-high-pass/dist/renderers-DUL8Peaw.mjs"],"sourcesContent":["import { defineRenderer } from \"@framefields/node-sdk/renderer\";\nimport { ImageResultSchema, VideoResultSchema, configBuilder, resolveHandleParam } from \"@framefields/node-sdk\";\nimport { FULLSCREEN_QUAD_WGSL, createFullscreenPipeline, drawChildToTexture, drawFullscreenPass, drawTextureToTarget, fragmentBindGroupLayout, signalTextureView, signalTiming, texturesLayout } from \"@framefields/node-sdk/gpu\";\nimport { z } from \"zod\";\n\n//#region src/shared/config.ts\nconst MAX_RADIUS = 250;\nconst highPassConfig = configBuilder().field(\"radius\", z.number().min(.1).max(MAX_RADIUS).default(3), {\n\tbindable: true,\n\tdataTypes: [\"Number\", \"Signal\"],\n\tlabel: \"Radius Signal\",\n\tdescription: \"The cutoff radius in pixels. Frequencies smaller than this radius are preserved.\"\n}).field(\"contrastBoost\", z.number().multipleOf(.01).min(1).max(10).default(1), {\n\tbindable: true,\n\tdataTypes: [\"Number\", \"Signal\"],\n\tlabel: \"Contrast Boost Signal\",\n\tdescription: \"Multiplier applied to the extracted high-frequency detail delta.\"\n}).field(\"monochrome\", z.boolean().default(true), { description: \"When enabled, high-pass delta is converted to luminance before centering on 50% neutral gray.\" }).build();\nconst HighPassNodeConfigSchema = highPassConfig.schema;\nconst HighPassResultSchema = z.union([ImageResultSchema, VideoResultSchema]);\nconst HIGH_PASS_OUTPUT_TYPE_MAP = {\n\tVideo: \"Video\",\n\tLottie: \"Video\",\n\tGIF: \"GIF\",\n\tImage: \"Image\",\n\tSVG: \"Image\"\n};\n\n//#endregion\n//#region src/renderers/webgpu-renderer.ts\nconst WGSL_HIGHPASS_UNIFORMS = `\nstruct HighPassUniforms {\n\tdirX : f32,\n\tdirY : f32,\n\tradius : f32,\n\tcontrastBoost : f32,\n\n\tmonochrome : f32,\n\thasRadiusSig : f32,\n\thasContrastSig : f32,\n\tisSecondPass : f32,\n};\n\n@group(0) @binding(0) var<uniform> u : HighPassUniforms;\n@group(1) @binding(0) var tex : texture_2d<f32>;\n@group(1) @binding(1) var samp : sampler;\n@group(1) @binding(2) var origTex : texture_2d<f32>;\n\n@group(2) @binding(0) var radiusSigTex : texture_2d<f32>;\n@group(2) @binding(1) var contrastSigTex : texture_2d<f32>;\n@group(2) @binding(2) var signalSamp : sampler;\n${FULLSCREEN_QUAD_WGSL}\n\n@fragment fn fs(in : VSOut) -> @location(0) vec4<f32> {\n\tlet dimensions = vec2<f32>(textureDimensions(tex));\n\tvar radius = u.radius;\n\tif (u.hasRadiusSig > 0.5) {\n\t\tradius = textureSampleLevel(radiusSigTex, signalSamp, vec2<f32>(0.5, 0.5), 0.0).r;\n\t}\n\tvar contrastBoost = u.contrastBoost;\n\tif (u.hasContrastSig > 0.5) {\n\t\tcontrastBoost = textureSampleLevel(contrastSigTex, signalSamp, vec2<f32>(0.5, 0.5), 0.0).r;\n\t}\n\n\tlet origCol = textureSampleLevel(origTex, samp, in.uv, 0.0);\n\tif (radius <= 0.0) {\n\t\tif (u.isSecondPass > 0.5) {\n\t\t\treturn vec4<f32>(vec3<f32>(0.5) * origCol.a, origCol.a);\n\t\t}\n\t\treturn origCol;\n\t}\n\n\tlet texelSize = 1.0 / dimensions;\n\tlet spatial_sigma = radius;\n\tlet stepScale = max(1.0, spatial_sigma / 20.0);\n\tlet effectiveSigma = spatial_sigma / stepScale;\n\tlet sampleRadius = min(i32(ceil(3.0 * effectiveSigma)), 64);\n\tlet direction = vec2<f32>(u.dirX, u.dirY);\n\n\tvar blurred = vec4<f32>(0.0);\n\tvar totalWeight = 0.0;\n\n\tfor (var i = -sampleRadius; i <= sampleRadius; i++) {\n\t\tlet offset = vec2<f32>(f32(i) * stepScale) * direction * texelSize;\n\t\tlet x = f32(i) * stepScale;\n\t\tlet weight = exp(-0.5 * (x / spatial_sigma) * (x / spatial_sigma));\n\t\tblurred += textureSampleLevel(tex, samp, in.uv + offset, 0.0) * weight;\n\t\ttotalWeight += weight;\n\t}\n\n\tlet blurResult = blurred / max(totalWeight, 1e-5);\n\n\tif (u.isSecondPass <= 0.5) {\n\t\t// Pass 1: Intermediate horizontal blur\n\t\treturn blurResult;\n\t}\n\n\t// Pass 2: Vertical blur complete -> Calculate High Pass difference against original\n\tif (origCol.a < 1e-5) {\n\t\treturn vec4<f32>(0.0, 0.0, 0.0, 0.0);\n\t}\n\n\tlet origUnpremult = origCol.rgb / origCol.a;\n\tlet blurUnpremult = blurResult.rgb / max(blurResult.a, 1e-5);\n\tlet diff = origUnpremult - blurUnpremult;\n\n\tvar highPassRgb : vec3<f32>;\n\tif (u.monochrome > 0.5) {\n\t\tlet lumaDiff = dot(diff, vec3<f32>(0.2126, 0.7152, 0.0722));\n\t\thighPassRgb = vec3<f32>(0.5) + vec3<f32>(lumaDiff * contrastBoost);\n\t} else {\n\t\thighPassRgb = vec3<f32>(0.5) + diff * contrastBoost;\n\t}\n\n\tlet clampedRgb = clamp(highPassRgb, vec3<f32>(0.0), vec3<f32>(1.0));\n\treturn vec4<f32>(clampedRgb * origCol.a, origCol.a);\n}\n`;\nconst deviceResourceCache = /* @__PURE__ */ new WeakMap();\nconst highPassUniformData = new Float32Array(8);\nfunction getHighPassResources(device, targetFormat) {\n\tconst cached = deviceResourceCache.get(device);\n\tif (cached) return cached;\n\tconst highPassUniformLayout = fragmentBindGroupLayout(device, [\"uniform\"]);\n\tconst singleTextureLayout = fragmentBindGroupLayout(device, [\n\t\t\"texture\",\n\t\t\"sampler\",\n\t\t\"texture\"\n\t]);\n\tconst signalTextureLayout = texturesLayout(device, 2);\n\tconst res = {\n\t\thighPassUniformLayout,\n\t\tsingleTextureLayout,\n\t\tsignalTextureLayout,\n\t\thighPassPipeline: createFullscreenPipeline(device, {\n\t\t\tmodule: device.createShaderModule({ code: WGSL_HIGHPASS_UNIFORMS }),\n\t\t\tlayouts: device.createPipelineLayout({ bindGroupLayouts: [\n\t\t\t\thighPassUniformLayout,\n\t\t\t\tsingleTextureLayout,\n\t\t\t\tsignalTextureLayout\n\t\t\t] }),\n\t\t\tformat: targetFormat\n\t\t})\n\t};\n\tdeviceResourceCache.set(device, res);\n\treturn res;\n}\nconst WebGPURenderer = async (args) => {\n\tconst { ctx, encoder, pass, targetTexture, targetWidth, targetHeight, props } = args;\n\tconst op = props.virtualMedia?.operation;\n\tif (op?.op !== \"HighPass\" || !op) return;\n\tconst childMedia = props.virtualMedia.children?.[0];\n\tif (!childMedia) return;\n\tpass.end();\n\tconst frame = props.frame ?? 0;\n\tconst fps = props.fps || 30;\n\tconst width = targetWidth;\n\tconst height = targetHeight;\n\tconst nodeId = op.inputs ? Object.keys(op.inputs)[0] : \"highpass_node\";\n\tconst resolveField = (handleId, fallbackVal, clampMin = 0, clampMax = 1e3) => resolveHandleParam(op.inputs, handleId, fallbackVal, clampMin, clampMax);\n\tconst radiusRes = resolveField(op.radiusHandleId, Number(op.radius ?? 3), .1, MAX_RADIUS);\n\tconst contrastRes = resolveField(op.contrastBoostHandleId, Number(op.contrastBoost ?? 1), 1, 10);\n\tconst { texture: tmpTex } = await drawChildToTexture(args, childMedia);\n\tconst { highPassPipeline: pipeline, highPassUniformLayout: uLayout, singleTextureLayout: tLayout, signalTextureLayout: sigLayout } = getHighPassResources(ctx.device, ctx.renderer.format);\n\tconst sampler = ctx.renderer.samplerCache.getSampler(ctx.device);\n\tconst { elapsedSeconds, durationSeconds } = signalTiming(props, frame, fps);\n\tconst signalContext = {\n\t\targs,\n\t\twidth,\n\t\theight,\n\t\tframe,\n\t\tfps,\n\t\telapsedSeconds,\n\t\tdurationSeconds\n\t};\n\tconst getSignalView = (res, suffix) => signalTextureView(signalContext, res, `${nodeId}_${suffix}`);\n\tconst radiusView = getSignalView(radiusRes, \"radius_sig\");\n\tconst contrastView = getSignalView(contrastRes, \"contrast_sig\");\n\tconst createHighPassBindGroup1 = (inputTex, origTexture) => ctx.device.createBindGroup({\n\t\tlayout: tLayout,\n\t\tentries: [\n\t\t\t{\n\t\t\t\tbinding: 0,\n\t\t\t\tresource: inputTex.createView()\n\t\t\t},\n\t\t\t{\n\t\t\t\tbinding: 1,\n\t\t\t\tresource: sampler\n\t\t\t},\n\t\t\t{\n\t\t\t\tbinding: 2,\n\t\t\t\tresource: origTexture.createView()\n\t\t\t}\n\t\t]\n\t});\n\thighPassUniformData[2] = radiusRes.val;\n\thighPassUniformData[3] = contrastRes.val;\n\thighPassUniformData[4] = op.monochrome ?? true ? 1 : 0;\n\thighPassUniformData[5] = radiusRes.hasStaticSig ? 1 : 0;\n\thighPassUniformData[6] = contrastRes.hasStaticSig ? 1 : 0;\n\thighPassUniformData[7] = 0;\n\tconst outTex1 = ctx.renderer.getTemporaryTexture(width, height, [\n\t\ttmpTex,\n\t\ttargetTexture,\n\t\t...props.excludeTextures || []\n\t]);\n\tconst outTex2 = ctx.renderer.getTemporaryTexture(width, height, [\n\t\ttmpTex,\n\t\toutTex1,\n\t\ttargetTexture,\n\t\t...props.excludeTextures || []\n\t]);\n\tconst sigBindGroup = ctx.device.createBindGroup({\n\t\tlayout: sigLayout,\n\t\tentries: [\n\t\t\t{\n\t\t\t\tbinding: 0,\n\t\t\t\tresource: radiusView\n\t\t\t},\n\t\t\t{\n\t\t\t\tbinding: 1,\n\t\t\t\tresource: contrastView\n\t\t\t},\n\t\t\t{\n\t\t\t\tbinding: 2,\n\t\t\t\tresource: sampler\n\t\t\t}\n\t\t]\n\t});\n\thighPassUniformData[0] = 1;\n\thighPassUniformData[1] = 0;\n\thighPassUniformData[7] = 0;\n\tconst hBuffer = ctx.renderer.getTemporaryBuffer(highPassUniformData);\n\tdrawFullscreenPass(encoder, outTex1.createView(), pipeline, [\n\t\tctx.device.createBindGroup({\n\t\t\tlayout: uLayout,\n\t\t\tentries: [{\n\t\t\t\tbinding: 0,\n\t\t\t\tresource: { buffer: hBuffer }\n\t\t\t}]\n\t\t}),\n\t\tcreateHighPassBindGroup1(tmpTex, tmpTex),\n\t\tsigBindGroup\n\t]);\n\thighPassUniformData[0] = 0;\n\thighPassUniformData[1] = 1;\n\thighPassUniformData[7] = 1;\n\tconst vBuffer = ctx.renderer.getTemporaryBuffer(highPassUniformData);\n\tdrawFullscreenPass(encoder, outTex2.createView(), pipeline, [\n\t\tctx.device.createBindGroup({\n\t\t\tlayout: uLayout,\n\t\t\tentries: [{\n\t\t\t\tbinding: 0,\n\t\t\t\tresource: { buffer: vBuffer }\n\t\t\t}]\n\t\t}),\n\t\tcreateHighPassBindGroup1(outTex1, tmpTex),\n\t\tsigBindGroup\n\t]);\n\tdrawTextureToTarget(args, outTex2, op.opacity ?? 1);\n};\n\n//#endregion\n//#region src/renderers/index.ts\nvar renderers_default = defineRenderer({ WebGPURenderer });\n\n//#endregion\nexport { MAX_RADIUS as a, HighPassResultSchema as i, HIGH_PASS_OUTPUT_TYPE_MAP as n, highPassConfig as o, HighPassNodeConfigSchema as r, renderers_default as t };\n//# sourceMappingURL=renderers-DUL8Peaw.mjs.map"],"mappings":"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"}
1
+ {"version":3,"file":"renderer-6GCG6JxD.mjs","names":[],"sources":["../../../nodes/node-high-pass/dist/renderers-DUL8Peaw.mjs"],"sourcesContent":["import { defineRenderer } from \"@framefields/node-sdk/renderer\";\nimport { ImageResultSchema, VideoResultSchema, configBuilder, resolveHandleParam } from \"@framefields/node-sdk\";\nimport { FULLSCREEN_QUAD_WGSL, createFullscreenPipeline, drawChildToTexture, drawFullscreenPass, drawTextureToTarget, fragmentBindGroupLayout, signalTextureView, signalTiming, texturesLayout } from \"@framefields/node-sdk/gpu\";\nimport { z } from \"zod\";\n\n//#region src/shared/config.ts\nconst MAX_RADIUS = 250;\nconst highPassConfig = configBuilder().field(\"radius\", z.number().min(.1).max(MAX_RADIUS).default(3), {\n\tbindable: true,\n\tdataTypes: [\"Number\", \"Signal\"],\n\tlabel: \"Radius Signal\",\n\tdescription: \"The cutoff radius in pixels. Frequencies smaller than this radius are preserved.\"\n}).field(\"contrastBoost\", z.number().multipleOf(.01).min(1).max(10).default(1), {\n\tbindable: true,\n\tdataTypes: [\"Number\", \"Signal\"],\n\tlabel: \"Contrast Boost Signal\",\n\tdescription: \"Multiplier applied to the extracted high-frequency detail delta.\"\n}).field(\"monochrome\", z.boolean().default(true), { description: \"When enabled, high-pass delta is converted to luminance before centering on 50% neutral gray.\" }).build();\nconst HighPassNodeConfigSchema = highPassConfig.schema;\nconst HighPassResultSchema = z.union([ImageResultSchema, VideoResultSchema]);\nconst HIGH_PASS_OUTPUT_TYPE_MAP = {\n\tVideo: \"Video\",\n\tLottie: \"Video\",\n\tGIF: \"GIF\",\n\tImage: \"Image\",\n\tSVG: \"Image\"\n};\n\n//#endregion\n//#region src/renderers/webgpu-renderer.ts\nconst WGSL_HIGHPASS_UNIFORMS = `\nstruct HighPassUniforms {\n\tdirX : f32,\n\tdirY : f32,\n\tradius : f32,\n\tcontrastBoost : f32,\n\n\tmonochrome : f32,\n\thasRadiusSig : f32,\n\thasContrastSig : f32,\n\tisSecondPass : f32,\n};\n\n@group(0) @binding(0) var<uniform> u : HighPassUniforms;\n@group(1) @binding(0) var tex : texture_2d<f32>;\n@group(1) @binding(1) var samp : sampler;\n@group(1) @binding(2) var origTex : texture_2d<f32>;\n\n@group(2) @binding(0) var radiusSigTex : texture_2d<f32>;\n@group(2) @binding(1) var contrastSigTex : texture_2d<f32>;\n@group(2) @binding(2) var signalSamp : sampler;\n${FULLSCREEN_QUAD_WGSL}\n\n@fragment fn fs(in : VSOut) -> @location(0) vec4<f32> {\n\tlet dimensions = vec2<f32>(textureDimensions(tex));\n\tvar radius = u.radius;\n\tif (u.hasRadiusSig > 0.5) {\n\t\tradius = textureSampleLevel(radiusSigTex, signalSamp, vec2<f32>(0.5, 0.5), 0.0).r;\n\t}\n\tvar contrastBoost = u.contrastBoost;\n\tif (u.hasContrastSig > 0.5) {\n\t\tcontrastBoost = textureSampleLevel(contrastSigTex, signalSamp, vec2<f32>(0.5, 0.5), 0.0).r;\n\t}\n\n\tlet origCol = textureSampleLevel(origTex, samp, in.uv, 0.0);\n\tif (radius <= 0.0) {\n\t\tif (u.isSecondPass > 0.5) {\n\t\t\treturn vec4<f32>(vec3<f32>(0.5) * origCol.a, origCol.a);\n\t\t}\n\t\treturn origCol;\n\t}\n\n\tlet texelSize = 1.0 / dimensions;\n\tlet spatial_sigma = radius;\n\tlet stepScale = max(1.0, spatial_sigma / 20.0);\n\tlet effectiveSigma = spatial_sigma / stepScale;\n\tlet sampleRadius = min(i32(ceil(3.0 * effectiveSigma)), 64);\n\tlet direction = vec2<f32>(u.dirX, u.dirY);\n\n\tvar blurred = vec4<f32>(0.0);\n\tvar totalWeight = 0.0;\n\n\tfor (var i = -sampleRadius; i <= sampleRadius; i++) {\n\t\tlet offset = vec2<f32>(f32(i) * stepScale) * direction * texelSize;\n\t\tlet x = f32(i) * stepScale;\n\t\tlet weight = exp(-0.5 * (x / spatial_sigma) * (x / spatial_sigma));\n\t\tblurred += textureSampleLevel(tex, samp, in.uv + offset, 0.0) * weight;\n\t\ttotalWeight += weight;\n\t}\n\n\tlet blurResult = blurred / max(totalWeight, 1e-5);\n\n\tif (u.isSecondPass <= 0.5) {\n\t\t// Pass 1: Intermediate horizontal blur\n\t\treturn blurResult;\n\t}\n\n\t// Pass 2: Vertical blur complete -> Calculate High Pass difference against original\n\tif (origCol.a < 1e-5) {\n\t\treturn vec4<f32>(0.0, 0.0, 0.0, 0.0);\n\t}\n\n\tlet origUnpremult = origCol.rgb / origCol.a;\n\tlet blurUnpremult = blurResult.rgb / max(blurResult.a, 1e-5);\n\tlet diff = origUnpremult - blurUnpremult;\n\n\tvar highPassRgb : vec3<f32>;\n\tif (u.monochrome > 0.5) {\n\t\tlet lumaDiff = dot(diff, vec3<f32>(0.2126, 0.7152, 0.0722));\n\t\thighPassRgb = vec3<f32>(0.5) + vec3<f32>(lumaDiff * contrastBoost);\n\t} else {\n\t\thighPassRgb = vec3<f32>(0.5) + diff * contrastBoost;\n\t}\n\n\tlet clampedRgb = clamp(highPassRgb, vec3<f32>(0.0), vec3<f32>(1.0));\n\treturn vec4<f32>(clampedRgb * origCol.a, origCol.a);\n}\n`;\nconst deviceResourceCache = /* @__PURE__ */ new WeakMap();\nconst highPassUniformData = new Float32Array(8);\nfunction getHighPassResources(device, targetFormat) {\n\tconst cached = deviceResourceCache.get(device);\n\tif (cached) return cached;\n\tconst highPassUniformLayout = fragmentBindGroupLayout(device, [\"uniform\"]);\n\tconst singleTextureLayout = fragmentBindGroupLayout(device, [\n\t\t\"texture\",\n\t\t\"sampler\",\n\t\t\"texture\"\n\t]);\n\tconst signalTextureLayout = texturesLayout(device, 2);\n\tconst res = {\n\t\thighPassUniformLayout,\n\t\tsingleTextureLayout,\n\t\tsignalTextureLayout,\n\t\thighPassPipeline: createFullscreenPipeline(device, {\n\t\t\tmodule: device.createShaderModule({ code: WGSL_HIGHPASS_UNIFORMS }),\n\t\t\tlayouts: device.createPipelineLayout({ bindGroupLayouts: [\n\t\t\t\thighPassUniformLayout,\n\t\t\t\tsingleTextureLayout,\n\t\t\t\tsignalTextureLayout\n\t\t\t] }),\n\t\t\tformat: targetFormat\n\t\t})\n\t};\n\tdeviceResourceCache.set(device, res);\n\treturn res;\n}\nconst WebGPURenderer = async (args) => {\n\tconst { ctx, encoder, pass, targetTexture, targetWidth, targetHeight, props } = args;\n\tconst op = props.virtualMedia?.operation;\n\tif (op?.op !== \"HighPass\" || !op) return;\n\tconst childMedia = props.virtualMedia.children?.[0];\n\tif (!childMedia) return;\n\tpass.end();\n\tconst frame = props.frame ?? 0;\n\tconst fps = props.fps || 30;\n\tconst width = targetWidth;\n\tconst height = targetHeight;\n\tconst nodeId = op.inputs ? Object.keys(op.inputs)[0] : \"highpass_node\";\n\tconst resolveField = (handleId, fallbackVal, clampMin = 0, clampMax = 1e3) => resolveHandleParam(op.inputs, handleId, fallbackVal, clampMin, clampMax);\n\tconst radiusRes = resolveField(op.radiusHandleId, Number(op.radius ?? 3), .1, MAX_RADIUS);\n\tconst contrastRes = resolveField(op.contrastBoostHandleId, Number(op.contrastBoost ?? 1), 1, 10);\n\tconst { texture: tmpTex } = await drawChildToTexture(args, childMedia);\n\tconst { highPassPipeline: pipeline, highPassUniformLayout: uLayout, singleTextureLayout: tLayout, signalTextureLayout: sigLayout } = getHighPassResources(ctx.device, ctx.renderer.format);\n\tconst sampler = ctx.renderer.samplerCache.getSampler(ctx.device);\n\tconst { elapsedSeconds, durationSeconds } = signalTiming(props, frame, fps);\n\tconst signalContext = {\n\t\targs,\n\t\twidth,\n\t\theight,\n\t\tframe,\n\t\tfps,\n\t\telapsedSeconds,\n\t\tdurationSeconds\n\t};\n\tconst getSignalView = (res, suffix) => signalTextureView(signalContext, res, `${nodeId}_${suffix}`);\n\tconst radiusView = getSignalView(radiusRes, \"radius_sig\");\n\tconst contrastView = getSignalView(contrastRes, \"contrast_sig\");\n\tconst createHighPassBindGroup1 = (inputTex, origTexture) => ctx.device.createBindGroup({\n\t\tlayout: tLayout,\n\t\tentries: [\n\t\t\t{\n\t\t\t\tbinding: 0,\n\t\t\t\tresource: inputTex.createView()\n\t\t\t},\n\t\t\t{\n\t\t\t\tbinding: 1,\n\t\t\t\tresource: sampler\n\t\t\t},\n\t\t\t{\n\t\t\t\tbinding: 2,\n\t\t\t\tresource: origTexture.createView()\n\t\t\t}\n\t\t]\n\t});\n\thighPassUniformData[2] = radiusRes.val;\n\thighPassUniformData[3] = contrastRes.val;\n\thighPassUniformData[4] = op.monochrome ?? true ? 1 : 0;\n\thighPassUniformData[5] = radiusRes.hasStaticSig ? 1 : 0;\n\thighPassUniformData[6] = contrastRes.hasStaticSig ? 1 : 0;\n\thighPassUniformData[7] = 0;\n\tconst outTex1 = ctx.renderer.getTemporaryTexture(width, height, [\n\t\ttmpTex,\n\t\ttargetTexture,\n\t\t...props.excludeTextures || []\n\t]);\n\tconst outTex2 = ctx.renderer.getTemporaryTexture(width, height, [\n\t\ttmpTex,\n\t\toutTex1,\n\t\ttargetTexture,\n\t\t...props.excludeTextures || []\n\t]);\n\tconst sigBindGroup = ctx.device.createBindGroup({\n\t\tlayout: sigLayout,\n\t\tentries: [\n\t\t\t{\n\t\t\t\tbinding: 0,\n\t\t\t\tresource: radiusView\n\t\t\t},\n\t\t\t{\n\t\t\t\tbinding: 1,\n\t\t\t\tresource: contrastView\n\t\t\t},\n\t\t\t{\n\t\t\t\tbinding: 2,\n\t\t\t\tresource: sampler\n\t\t\t}\n\t\t]\n\t});\n\thighPassUniformData[0] = 1;\n\thighPassUniformData[1] = 0;\n\thighPassUniformData[7] = 0;\n\tconst hBuffer = ctx.renderer.getTemporaryBuffer(highPassUniformData);\n\tdrawFullscreenPass(encoder, outTex1.createView(), pipeline, [\n\t\tctx.device.createBindGroup({\n\t\t\tlayout: uLayout,\n\t\t\tentries: [{\n\t\t\t\tbinding: 0,\n\t\t\t\tresource: { buffer: hBuffer }\n\t\t\t}]\n\t\t}),\n\t\tcreateHighPassBindGroup1(tmpTex, tmpTex),\n\t\tsigBindGroup\n\t]);\n\thighPassUniformData[0] = 0;\n\thighPassUniformData[1] = 1;\n\thighPassUniformData[7] = 1;\n\tconst vBuffer = ctx.renderer.getTemporaryBuffer(highPassUniformData);\n\tdrawFullscreenPass(encoder, outTex2.createView(), pipeline, [\n\t\tctx.device.createBindGroup({\n\t\t\tlayout: uLayout,\n\t\t\tentries: [{\n\t\t\t\tbinding: 0,\n\t\t\t\tresource: { buffer: vBuffer }\n\t\t\t}]\n\t\t}),\n\t\tcreateHighPassBindGroup1(outTex1, tmpTex),\n\t\tsigBindGroup\n\t]);\n\tdrawTextureToTarget(args, outTex2, op.opacity ?? 1);\n};\n\n//#endregion\n//#region src/renderers/index.ts\nvar renderers_default = defineRenderer({ WebGPURenderer });\n\n//#endregion\nexport { MAX_RADIUS as a, HighPassResultSchema as i, HIGH_PASS_OUTPUT_TYPE_MAP as n, highPassConfig as o, HighPassNodeConfigSchema as r, renderers_default as t };\n//# sourceMappingURL=renderers-DUL8Peaw.mjs.map"],"mappings":"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"}