framefields 2.6.1 → 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.
- package/dist/audio/index.d.mts +4 -4
- package/dist/audio/index.mjs +10 -10
- package/dist/{audio-CYrO9q9P.mjs → audio-j3ttoRDX.mjs} +4 -4
- package/dist/{audio-CYrO9q9P.mjs.map → audio-j3ttoRDX.mjs.map} +1 -1
- package/dist/{audio-B5mdR8vv.d.mts → audio-tL8CcPra.d.mts} +3 -3
- package/dist/{audio-B5mdR8vv.d.mts.map → audio-tL8CcPra.d.mts.map} +1 -1
- package/dist/components/index.d.mts +10 -10
- package/dist/components/index.mjs +44 -44
- package/dist/{dist-cC6nOaMR.mjs → dist-BHTd-EC1.mjs} +164 -34
- package/dist/dist-BHTd-EC1.mjs.map +1 -0
- package/dist/{dist-DSKhnhDk.mjs → dist-BM0ilHvQ.mjs} +115 -21
- package/dist/dist-BM0ilHvQ.mjs.map +1 -0
- package/dist/{dist-D021IFfp.mjs → dist-BRXsfWKQ.mjs} +444 -8
- package/dist/dist-BRXsfWKQ.mjs.map +1 -0
- package/dist/{dist-BPOi5xb_.mjs → dist-CPtFZxtu.mjs} +2 -2
- package/dist/{dist-BPOi5xb_.mjs.map → dist-CPtFZxtu.mjs.map} +1 -1
- package/dist/effects/index.d.mts +4 -4
- package/dist/effects/index.mjs +35 -35
- package/dist/{effects-Dvdwndk0.mjs → effects-Dy661XS_.mjs} +3 -3
- package/dist/{effects-Dvdwndk0.mjs.map → effects-Dy661XS_.mjs.map} +1 -1
- package/dist/fonts/index.mjs +2 -2
- package/dist/{fonts-B6a4CyoM.mjs → fonts-BcFhAqqO.mjs} +2 -2
- package/dist/{fonts-B6a4CyoM.mjs.map → fonts-BcFhAqqO.mjs.map} +1 -1
- package/dist/{gpu-CrQADlcf.mjs → gpu-zIhSz_0X.mjs} +2 -2
- package/dist/{gpu-CrQADlcf.mjs.map → gpu-zIhSz_0X.mjs.map} +1 -1
- package/dist/{index-ocZc12ZS.d.mts → index-B1ENegOQ.d.mts} +224 -2
- package/dist/index-B1ENegOQ.d.mts.map +1 -0
- package/dist/{index-D-Gnyffj.d.mts → index-CJRTBLZ4.d.mts} +4 -4
- package/dist/{index-D-Gnyffj.d.mts.map → index-CJRTBLZ4.d.mts.map} +1 -1
- package/dist/{index-BhzlG_1Q.d.mts → index-Cg30slle.d.mts} +2 -2
- package/dist/{index-BhzlG_1Q.d.mts.map → index-Cg30slle.d.mts.map} +1 -1
- package/dist/{index-B4o6F1JC.d.mts → index-DeeMmoEA.d.mts} +2 -2
- package/dist/{index-B4o6F1JC.d.mts.map → index-DeeMmoEA.d.mts.map} +1 -1
- package/dist/{index-CWG19cm-.d.mts → index-ePGQ_C7f.d.mts} +39 -16
- package/dist/index-ePGQ_C7f.d.mts.map +1 -0
- package/dist/{index-o8LxWdwz.d.mts → index-l1yRugLc.d.mts} +3 -3
- package/dist/{index-o8LxWdwz.d.mts.map → index-l1yRugLc.d.mts.map} +1 -1
- package/dist/{index-3Vdef2KE.d.mts → index-uDZ9Yuv-.d.mts} +5 -8
- package/dist/index-uDZ9Yuv-.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +53 -53
- package/dist/preview/index.d.mts +3 -3
- package/dist/preview/index.mjs +10 -10
- package/dist/{preview-OTsGYyOU.mjs → preview-IpdVFuxv.mjs} +3 -2
- package/dist/{preview-OTsGYyOU.mjs.map → preview-IpdVFuxv.mjs.map} +1 -1
- package/dist/preview-engine/audio.mjs +1 -1
- package/dist/preview-engine/{chunk-RF62TV3G.mjs → chunk-25WRUINA.mjs} +1 -1
- package/dist/preview-engine/{chunk-MMVFX42U.mjs → chunk-2AEWPT6Y.mjs} +1 -1
- package/dist/preview-engine/{chunk-IIOHF7MH.mjs → chunk-2MDTUA3W.mjs} +1 -1
- package/dist/preview-engine/{chunk-KWTTANYN.mjs → chunk-2TLABBER.mjs} +1 -1
- package/dist/preview-engine/{chunk-MUOJMF7M.mjs → chunk-3QOJVABU.mjs} +1 -1
- package/dist/preview-engine/{chunk-RDEQ4LQU.mjs → chunk-4Q3ZUTQC.mjs} +1 -1
- package/dist/preview-engine/{chunk-OXXRKNHW.mjs → chunk-5BXCJ2QX.mjs} +1 -1
- package/dist/preview-engine/{chunk-JTT3YX7W.mjs → chunk-5R4UF7YK.mjs} +1 -1
- package/dist/preview-engine/{chunk-4C26NVQY.mjs → chunk-6THE6J3A.mjs} +1 -1
- package/dist/preview-engine/{chunk-SVE3XWO7.mjs → chunk-6VDYWMP5.mjs} +1 -1
- package/dist/preview-engine/{chunk-ULPLHKM2.mjs → chunk-7B7QQDPZ.mjs} +1 -1
- package/dist/preview-engine/{chunk-ULVX4VYT.mjs → chunk-7ZA36EZE.mjs} +1 -1
- package/dist/preview-engine/{chunk-LYSU6S4O.mjs → chunk-AJ6BVUDB.mjs} +1 -1
- package/dist/preview-engine/{chunk-J24VHZS6.mjs → chunk-BDXGUSSF.mjs} +1 -1
- package/dist/preview-engine/{chunk-B7J72YSP.mjs → chunk-CC5V7UBF.mjs} +1 -1
- package/dist/preview-engine/{chunk-JD4TBC6Z.mjs → chunk-CP42LYD7.mjs} +1 -1
- package/dist/preview-engine/{chunk-LN3YAWUD.mjs → chunk-DJWDD3OE.mjs} +1 -1
- package/dist/preview-engine/{chunk-V3FODTPI.mjs → chunk-DZEWY4PR.mjs} +22 -22
- package/dist/preview-engine/{chunk-RKNV5N7D.mjs → chunk-ENGQ55LR.mjs} +1 -1
- package/dist/preview-engine/{chunk-AGLAJGQJ.mjs → chunk-FBXKVLTB.mjs} +14 -14
- package/dist/preview-engine/{chunk-MJCK5FLZ.mjs → chunk-FMEXTHL7.mjs} +1 -1
- package/dist/preview-engine/{chunk-UEQIO6AH.mjs → chunk-FQ7UCMWR.mjs} +1 -1
- package/dist/preview-engine/{chunk-TPFDAOW7.mjs → chunk-FRIBHQTM.mjs} +1 -1
- package/dist/preview-engine/chunk-FT7HJ6IE.mjs +3 -0
- package/dist/preview-engine/{chunk-YEPYVASB.mjs → chunk-G3UOUGIE.mjs} +1 -1
- package/dist/preview-engine/{chunk-4JX5LBE2.mjs → chunk-H4GDIVX4.mjs} +1 -1
- package/dist/preview-engine/{chunk-LHCEQ77S.mjs → chunk-HU73APZL.mjs} +1 -1
- package/dist/preview-engine/{chunk-DDFWN7DI.mjs → chunk-I7QIQURU.mjs} +1 -1
- package/dist/preview-engine/{chunk-3KHD67Q3.mjs → chunk-ID344UBN.mjs} +1 -1
- package/dist/preview-engine/{chunk-5NT7WWJR.mjs → chunk-IGCXUGHT.mjs} +1 -1
- package/dist/preview-engine/{chunk-QPBTLJ5W.mjs → chunk-IK3YA6MC.mjs} +1 -1
- package/dist/preview-engine/chunk-J5SKQFJ6.mjs +1 -0
- package/dist/preview-engine/{chunk-Q7JN4EHX.mjs → chunk-JOPZNSZJ.mjs} +1 -1
- package/dist/preview-engine/{chunk-SQ52PBF3.mjs → chunk-JOSNIMFT.mjs} +1 -1
- package/dist/preview-engine/{chunk-SXNCNG4L.mjs → chunk-JR443L5E.mjs} +1 -1
- package/dist/preview-engine/{chunk-FZLKPBKS.mjs → chunk-KCAFMWDM.mjs} +1 -1
- package/dist/preview-engine/{chunk-QRYCB6E3.mjs → chunk-LAEPNVOE.mjs} +1 -1
- package/dist/preview-engine/chunk-MAALUQK3.mjs +1974 -0
- package/dist/preview-engine/chunk-MHCXSWK7.mjs +1 -0
- package/dist/preview-engine/{chunk-4INGTMZ5.mjs → chunk-MWBARRK2.mjs} +1 -1
- package/dist/preview-engine/chunk-MXK2PP6K.mjs +1 -0
- package/dist/preview-engine/{chunk-GQIRV466.mjs → chunk-MXPN2GB7.mjs} +1 -1
- package/dist/preview-engine/{chunk-W2EZFV4Q.mjs → chunk-NHYB5YC3.mjs} +1 -1
- package/dist/preview-engine/{chunk-K3IMPV6T.mjs → chunk-PXSKIGAP.mjs} +6 -6
- package/dist/preview-engine/{chunk-SP22JWN2.mjs → chunk-PYM2FBFA.mjs} +1 -1
- package/dist/preview-engine/chunk-QDTJ4MOJ.mjs +1 -0
- package/dist/preview-engine/{chunk-6PMATELR.mjs → chunk-RQU2GFGH.mjs} +1 -1
- package/dist/preview-engine/{chunk-UQD2UE6I.mjs → chunk-RWDPBES3.mjs} +1 -1
- package/dist/preview-engine/{chunk-QVW5METG.mjs → chunk-S5ONWSZF.mjs} +1 -1
- package/dist/preview-engine/{chunk-2YGR5YCN.mjs → chunk-SIU6JU2R.mjs} +1 -1
- package/dist/preview-engine/{chunk-NQZRPPUU.mjs → chunk-SJ3Q4AQP.mjs} +1 -1
- package/dist/preview-engine/{chunk-Q6CRERSJ.mjs → chunk-T36SKUWK.mjs} +9 -9
- package/dist/preview-engine/chunk-TEBZRQPG.mjs +1 -0
- package/dist/preview-engine/{chunk-WDWHOXSA.mjs → chunk-UUDML2SN.mjs} +1 -1
- package/dist/preview-engine/{chunk-RHXZXWJ4.mjs → chunk-VAS4IJZR.mjs} +1 -1
- package/dist/preview-engine/{chunk-DOCFOSA6.mjs → chunk-WTDK6H46.mjs} +1 -1
- package/dist/preview-engine/chunk-XNMEVVQB.mjs +1 -0
- package/dist/preview-engine/{chunk-AKT4YQWL.mjs → chunk-YVGT4NK6.mjs} +2 -2
- package/dist/preview-engine/{chunk-ZGMA27IG.mjs → chunk-ZR4QFIJR.mjs} +1 -1
- package/dist/preview-engine/chunk-ZYTXBBIX.mjs +1 -0
- package/dist/preview-engine/components.mjs +1 -1
- package/dist/preview-engine/effects.mjs +1 -1
- package/dist/preview-engine/fonts.mjs +1 -1
- package/dist/preview-engine/framefields.mjs +1 -1
- package/dist/preview-engine/player.mjs +2 -2
- package/dist/preview-engine/{renderers-FC7G7KU7.mjs → renderers-2AKYMAIK.mjs} +1 -1
- package/dist/preview-engine/renderers-363CKEIG.mjs +1 -0
- package/dist/preview-engine/{renderers-EJGF3PYQ.mjs → renderers-36L24ZVS.mjs} +1 -1
- package/dist/preview-engine/renderers-3FMMVDMA.mjs +1 -0
- package/dist/preview-engine/{renderers-56TGEFDK.mjs → renderers-44AANAXR.mjs} +1 -1
- package/dist/preview-engine/renderers-4UUU62VV.mjs +1 -0
- package/dist/preview-engine/renderers-4ZTZFSAR.mjs +1 -0
- package/dist/preview-engine/{renderers-PYDOSDEY.mjs → renderers-5ZSB75GY.mjs} +1 -1
- package/dist/preview-engine/renderers-6KDD75BM.mjs +1 -0
- package/dist/preview-engine/renderers-7BJRD4G2.mjs +1 -0
- package/dist/preview-engine/{renderers-HBIBRZD7.mjs → renderers-AIIHGDJM.mjs} +1 -1
- package/dist/preview-engine/{renderers-P2FTCDZ7.mjs → renderers-AU746ORP.mjs} +1 -1
- package/dist/preview-engine/renderers-BIF2BETK.mjs +1 -0
- package/dist/preview-engine/{renderers-FD3ZT73R.mjs → renderers-BTQCHVHV.mjs} +1 -1
- package/dist/preview-engine/{renderers-HMNCIEU3.mjs → renderers-CFHBOAZG.mjs} +1 -1
- package/dist/preview-engine/renderers-CU5RW5UA.mjs +1 -0
- package/dist/preview-engine/renderers-DK5OBTKE.mjs +1 -0
- package/dist/preview-engine/renderers-EAKCDGTY.mjs +1 -0
- package/dist/preview-engine/{renderers-FYI6QNUH.mjs → renderers-EGBGC7MU.mjs} +1 -1
- package/dist/preview-engine/renderers-EVXCU7G7.mjs +1 -0
- package/dist/preview-engine/renderers-EXEYEH3F.mjs +1 -0
- package/dist/preview-engine/{renderers-RDHHVXRE.mjs → renderers-EXJDPS3T.mjs} +1 -1
- package/dist/preview-engine/renderers-F3WALPWI.mjs +1 -0
- package/dist/preview-engine/{renderers-4RVSLTCX.mjs → renderers-FS3EOB4J.mjs} +1 -1
- package/dist/preview-engine/{renderers-C3YXNKNM.mjs → renderers-GUL2MXDD.mjs} +1 -1
- package/dist/preview-engine/{renderers-EQDINUMT.mjs → renderers-H4HC44B5.mjs} +1 -1
- package/dist/preview-engine/renderers-H6565HO4.mjs +1 -0
- package/dist/preview-engine/renderers-HT5V6OZ2.mjs +1 -0
- package/dist/preview-engine/renderers-HXMOARWO.mjs +1 -0
- package/dist/preview-engine/renderers-HZ2Q7BGK.mjs +1 -0
- package/dist/preview-engine/renderers-I4N2K2KW.mjs +1 -0
- package/dist/preview-engine/renderers-I5J7U6CB.mjs +1 -0
- package/dist/preview-engine/renderers-IZV6QXR5.mjs +1 -0
- package/dist/preview-engine/renderers-JWGAPHWX.mjs +1 -0
- package/dist/preview-engine/renderers-KNDSVQTY.mjs +1 -0
- package/dist/preview-engine/renderers-KNDUOO6M.mjs +1 -0
- package/dist/preview-engine/renderers-KQAO3QEI.mjs +1 -0
- package/dist/preview-engine/{renderers-7SW2AHM2.mjs → renderers-LCSQLXRD.mjs} +1 -1
- package/dist/preview-engine/renderers-M34LOEHC.mjs +1 -0
- package/dist/preview-engine/renderers-ML6GNCWB.mjs +1 -0
- package/dist/preview-engine/renderers-NARG274F.mjs +1 -0
- package/dist/preview-engine/renderers-NCFPOSWR.mjs +1 -0
- package/dist/preview-engine/{renderers-ZAMSS7QN.mjs → renderers-PCOTECQB.mjs} +1 -1
- package/dist/preview-engine/renderers-PG5JHI2S.mjs +1 -0
- package/dist/preview-engine/{renderers-LTV5RFDJ.mjs → renderers-Q7MNLQJF.mjs} +1 -1
- package/dist/preview-engine/{renderers-GHP7SL5M.mjs → renderers-QXVSJYZL.mjs} +1 -1
- package/dist/preview-engine/{renderers-H2UI7YEO.mjs → renderers-R3ERZ3ZY.mjs} +1 -1
- package/dist/preview-engine/renderers-RXQKV65L.mjs +1 -0
- package/dist/preview-engine/renderers-SA73GD2B.mjs +1 -0
- package/dist/preview-engine/renderers-SLIXUHNX.mjs +1 -0
- package/dist/preview-engine/renderers-T462JZNA.mjs +1 -0
- package/dist/preview-engine/{renderers-LV6LLULR.mjs → renderers-TH2XZAYW.mjs} +1 -1
- package/dist/preview-engine/{renderers-ART3EGSZ.mjs → renderers-UHZIERVH.mjs} +1 -1
- package/dist/preview-engine/renderers-V7CFVGYK.mjs +1 -0
- package/dist/preview-engine/renderers-VY5E7BPK.mjs +1 -0
- package/dist/preview-engine/{renderers-ZDMDDWLN.mjs → renderers-WKXGELRG.mjs} +1 -1
- package/dist/preview-engine/{renderers-GY4R467B.mjs → renderers-WLDIIFR5.mjs} +1 -1
- package/dist/preview-engine/{renderers-NTKSN32T.mjs → renderers-WM6WBRH6.mjs} +1 -1
- package/dist/preview-engine/renderers-WT5FQZM7.mjs +228 -0
- package/dist/preview-engine/{renderers-YK74777T.mjs → renderers-WTKRLNNR.mjs} +1 -1
- package/dist/preview-engine/{renderers-BWRNCXR6.mjs → renderers-X6GKEVP7.mjs} +1 -1
- package/dist/preview-engine/renderers-Y47N5EYR.mjs +1 -0
- package/dist/preview-engine/renderers-Y4CUVZYI.mjs +1 -0
- package/dist/preview-engine/renderers-Y6YC7ZAK.mjs +1 -0
- package/dist/preview-engine/renderers-Y6ZYUAQG.mjs +1 -0
- package/dist/preview-engine/{renderers-YBTWN76S.mjs → renderers-YRWXYT5A.mjs} +1 -1
- package/dist/preview-engine/{renderers-SY435YU3.mjs → renderers-YWC4MXPG.mjs} +1 -1
- package/dist/preview-engine/{renderers-7GMW52G3.mjs → renderers-ZWNPYZN6.mjs} +1 -1
- package/dist/preview-engine/signals.mjs +1 -1
- package/dist/{program-W4CWO2bp.mjs → program-KPBfq9tw.mjs} +95 -21
- package/dist/program-KPBfq9tw.mjs.map +1 -0
- package/dist/registry/index.json +1 -1
- package/dist/registry/lower-third.json +1 -1
- package/dist/registry/terminal.json +1 -1
- package/dist/registry/text-reveal.json +2 -2
- package/dist/registry/title-card.json +1 -1
- package/dist/renderer/index.d.mts +2 -2
- package/dist/renderer/index.mjs +2 -2
- package/dist/{renderer-CX4rvqm9.mjs → renderer-1-FLx8kd.mjs} +3 -3
- package/dist/{renderer-CX4rvqm9.mjs.map → renderer-1-FLx8kd.mjs.map} +1 -1
- package/dist/{renderer-BIMm7lYG.mjs → renderer-47d3etrX.mjs} +4 -4
- package/dist/{renderer-BIMm7lYG.mjs.map → renderer-47d3etrX.mjs.map} +1 -1
- package/dist/{renderer-CPtyQrJI.mjs → renderer-6GCG6JxD.mjs} +3 -3
- package/dist/{renderer-CPtyQrJI.mjs.map → renderer-6GCG6JxD.mjs.map} +1 -1
- package/dist/{renderer-BE6K88VR.mjs → renderer-8BIDdgjt.mjs} +3 -3
- package/dist/{renderer-BE6K88VR.mjs.map → renderer-8BIDdgjt.mjs.map} +1 -1
- package/dist/renderer-B-5TlncB.mjs +5 -0
- package/dist/{renderer-FgP_1OgD.mjs → renderer-B1yzSbvC.mjs} +4 -4
- package/dist/{renderer-FgP_1OgD.mjs.map → renderer-B1yzSbvC.mjs.map} +1 -1
- package/dist/{renderer-158rymL0.mjs → renderer-B2YiXpp-.mjs} +2 -2
- package/dist/{renderer-158rymL0.mjs.map → renderer-B2YiXpp-.mjs.map} +1 -1
- package/dist/{renderer-eQP0fvgc.mjs → renderer-B55ljMI_.mjs} +2 -2
- package/dist/{renderer-eQP0fvgc.mjs.map → renderer-B55ljMI_.mjs.map} +1 -1
- package/dist/{renderer-Duorbrb8.mjs → renderer-B6Tg4UgH.mjs} +4 -4
- package/dist/{renderer-Duorbrb8.mjs.map → renderer-B6Tg4UgH.mjs.map} +1 -1
- package/dist/{renderer-DmrP4dsn.mjs → renderer-BFnf-8Yt.mjs} +3 -3
- package/dist/{renderer-DmrP4dsn.mjs.map → renderer-BFnf-8Yt.mjs.map} +1 -1
- package/dist/{renderer-CLP6Lm7y.mjs → renderer-BGHHoPiD.mjs} +2 -2
- package/dist/{renderer-BO5u8eFS.mjs → renderer-BHUjPi24.mjs} +2 -2
- package/dist/{renderer-BO5u8eFS.mjs.map → renderer-BHUjPi24.mjs.map} +1 -1
- package/dist/renderer-BHXtDg5_.mjs +3 -0
- package/dist/{renderer-HPBahXjt.mjs → renderer-BIk67-jU.mjs} +2 -2
- package/dist/{renderer-HPBahXjt.mjs.map → renderer-BIk67-jU.mjs.map} +1 -1
- package/dist/{renderer-C-dcd14F.d.mts → renderer-BLEmr0Lt.d.mts} +2 -2
- package/dist/{renderer-C-dcd14F.d.mts.map → renderer-BLEmr0Lt.d.mts.map} +1 -1
- package/dist/{renderer-tYmQUp4d.mjs → renderer-BLs5sDdk.mjs} +3 -3
- package/dist/{renderer-tYmQUp4d.mjs.map → renderer-BLs5sDdk.mjs.map} +1 -1
- package/dist/renderer-BPd3gvEt.mjs +5 -0
- package/dist/{renderer-BiH29MS6.mjs → renderer-BXul0Wy6.mjs} +3 -3
- package/dist/{renderer-BiH29MS6.mjs.map → renderer-BXul0Wy6.mjs.map} +1 -1
- package/dist/{renderer-DVdRzDwX.mjs → renderer-B_1SLBPa.mjs} +3 -3
- package/dist/{renderer-DVdRzDwX.mjs.map → renderer-B_1SLBPa.mjs.map} +1 -1
- package/dist/renderer-B_ep98BH.mjs +5 -0
- package/dist/renderer-Ba_KUbKW.mjs +5 -0
- package/dist/{renderer-dX7Fy1NR.mjs → renderer-BbyNUjE_.mjs} +3 -3
- package/dist/{renderer-dX7Fy1NR.mjs.map → renderer-BbyNUjE_.mjs.map} +1 -1
- package/dist/{renderer-0n4A8P2_.mjs → renderer-BcVr_cIk.mjs} +2 -2
- package/dist/{renderer-0n4A8P2_.mjs.map → renderer-BcVr_cIk.mjs.map} +1 -1
- package/dist/renderer-BeL2hstU.mjs +5 -0
- package/dist/renderer-Bj81osPQ.mjs +5 -0
- package/dist/{renderer-ZM2jwJbX.mjs → renderer-BjL7sKGU.mjs} +3 -3
- package/dist/{renderer-ZM2jwJbX.mjs.map → renderer-BjL7sKGU.mjs.map} +1 -1
- package/dist/{renderer-Bh7d3RSt.mjs → renderer-BjNcuJ4H.mjs} +3 -3
- package/dist/{renderer-Bh7d3RSt.mjs.map → renderer-BjNcuJ4H.mjs.map} +1 -1
- package/dist/{renderer-Sdsy27LH.mjs → renderer-Br8aDmf0.mjs} +2 -2
- package/dist/renderer-BvZQngSm.mjs +5 -0
- package/dist/renderer-BxgN8oXx.mjs +5 -0
- package/dist/{renderer-BQ-pHSPI.mjs → renderer-ByOvGGzp.mjs} +3 -3
- package/dist/{renderer-BQ-pHSPI.mjs.map → renderer-ByOvGGzp.mjs.map} +1 -1
- package/dist/{renderer-KkDYCAkn.mjs → renderer-C11qLlrz.mjs} +3 -3
- package/dist/{renderer-KkDYCAkn.mjs.map → renderer-C11qLlrz.mjs.map} +1 -1
- package/dist/{renderer-D52KWsb8.mjs → renderer-C5dyPQjG.mjs} +3 -3
- package/dist/{renderer-D52KWsb8.mjs.map → renderer-C5dyPQjG.mjs.map} +1 -1
- package/dist/renderer-C5jDrTBc.mjs +5 -0
- package/dist/{renderer-DTKtkgTO.mjs → renderer-C6TbU8U5.mjs} +3 -3
- package/dist/{renderer-DTKtkgTO.mjs.map → renderer-C6TbU8U5.mjs.map} +1 -1
- package/dist/{renderer-aT6MUYPr.mjs → renderer-C9bcEaBE.mjs} +3 -3
- package/dist/{renderer-aT6MUYPr.mjs.map → renderer-C9bcEaBE.mjs.map} +1 -1
- package/dist/{renderer-CsxO_wBG.mjs → renderer-CAH00Lub.mjs} +2 -2
- package/dist/{renderer-Cb74SQHm.mjs → renderer-CAhsBEpU.mjs} +2 -2
- package/dist/{renderer-Cb74SQHm.mjs.map → renderer-CAhsBEpU.mjs.map} +1 -1
- package/dist/renderer-CBK4M82Y.mjs +5 -0
- package/dist/{renderer-CFSh2eG-.mjs → renderer-CCH0gSF7.mjs} +3 -3
- package/dist/{renderer-CFSh2eG-.mjs.map → renderer-CCH0gSF7.mjs.map} +1 -1
- package/dist/{renderer-CHvEgLHy.mjs → renderer-CCbOp0BM.mjs} +3 -3
- package/dist/{renderer-CHvEgLHy.mjs.map → renderer-CCbOp0BM.mjs.map} +1 -1
- package/dist/{renderer-DrrWvz8l.mjs → renderer-CEb5GjcU.mjs} +3 -3
- package/dist/{renderer-DrrWvz8l.mjs.map → renderer-CEb5GjcU.mjs.map} +1 -1
- package/dist/{renderer-1KfQICGK.mjs → renderer-CIL9zh-a.mjs} +3 -3
- package/dist/{renderer-1KfQICGK.mjs.map → renderer-CIL9zh-a.mjs.map} +1 -1
- package/dist/{renderer-CEvj79Wg.mjs → renderer-CIbXIFOb.mjs} +4 -4
- package/dist/{renderer-CEvj79Wg.mjs.map → renderer-CIbXIFOb.mjs.map} +1 -1
- package/dist/renderer-CLHtCctc.mjs +5 -0
- package/dist/{renderer-DxOJv_G7.mjs → renderer-CLX33fIK.mjs} +3 -3
- package/dist/{renderer-DxOJv_G7.mjs.map → renderer-CLX33fIK.mjs.map} +1 -1
- package/dist/{renderer-BcaT2j3R.mjs → renderer-CLXpWDqx.mjs} +2 -2
- package/dist/{renderer-BcaT2j3R.mjs.map → renderer-CLXpWDqx.mjs.map} +1 -1
- package/dist/renderer-CLin7MNN.mjs +5 -0
- package/dist/{renderer-COcFh5bK.mjs → renderer-CLypsmiH.mjs} +3 -3
- package/dist/{renderer-COcFh5bK.mjs.map → renderer-CLypsmiH.mjs.map} +1 -1
- package/dist/renderer-CN7TypOB.mjs +5 -0
- package/dist/renderer-CV5Jfwq7.mjs +5 -0
- package/dist/renderer-Ca5E46WK.mjs +5 -0
- package/dist/renderer-CdbOtoOU.mjs +5 -0
- package/dist/renderer-CeD_bqWj.mjs +5 -0
- package/dist/{renderer-BnpMAub0.mjs → renderer-CfEuoZ7b.mjs} +3 -3
- package/dist/{renderer-BnpMAub0.mjs.map → renderer-CfEuoZ7b.mjs.map} +1 -1
- package/dist/renderer-CfzNNUOd.mjs +5 -0
- package/dist/{renderer-0W-Ty55h.mjs → renderer-Cg3h0Mz9.mjs} +3 -3
- package/dist/{renderer-0W-Ty55h.mjs.map → renderer-Cg3h0Mz9.mjs.map} +1 -1
- package/dist/renderer-Cl8TmdU8.mjs +5 -0
- package/dist/{renderer-CoaDDIS6.mjs → renderer-ClHqvIps.mjs} +10 -10
- package/dist/renderer-ClHqvIps.mjs.map +1 -0
- package/dist/renderer-CosBP_IM.mjs +5 -0
- package/dist/renderer-CpwVs5Fh.mjs +5 -0
- package/dist/renderer-Cq7baxNb.mjs +5 -0
- package/dist/{renderer-B_Oz9L3g.mjs → renderer-CqMz_iXy.mjs} +2 -2
- package/dist/{renderer-B_Oz9L3g.mjs.map → renderer-CqMz_iXy.mjs.map} +1 -1
- package/dist/{renderer-DORydJHm.mjs → renderer-CqSmfrE1.mjs} +3 -3
- package/dist/{renderer-DORydJHm.mjs.map → renderer-CqSmfrE1.mjs.map} +1 -1
- package/dist/{renderer-WTvo3wo9.d.mts → renderer-CrhJJKFY.d.mts} +5 -3
- package/dist/renderer-CrhJJKFY.d.mts.map +1 -0
- package/dist/{renderer-Bt3F1tmP.mjs → renderer-CxxwhUwy.mjs} +3 -3
- package/dist/{renderer-Bt3F1tmP.mjs.map → renderer-CxxwhUwy.mjs.map} +1 -1
- package/dist/{renderer-Cba208C9.mjs → renderer-D24aiXmB.mjs} +3 -3
- package/dist/{renderer-Cba208C9.mjs.map → renderer-D24aiXmB.mjs.map} +1 -1
- package/dist/renderer-D6GkxwAx.mjs +5 -0
- package/dist/renderer-D7SfGdzX.mjs +5 -0
- package/dist/renderer-D7wt09_l.mjs +5 -0
- package/dist/{renderer-CdJoOfQw.mjs → renderer-DBslE7fc.mjs} +2 -2
- package/dist/{renderer-CdJoOfQw.mjs.map → renderer-DBslE7fc.mjs.map} +1 -1
- package/dist/{renderer-Df-rf7RC.mjs → renderer-DE3Ki96C.mjs} +3 -3
- package/dist/{renderer-Df-rf7RC.mjs.map → renderer-DE3Ki96C.mjs.map} +1 -1
- package/dist/{renderer-DqIrYB0P.mjs → renderer-DJCqc98W.mjs} +3 -3
- package/dist/{renderer-DqIrYB0P.mjs.map → renderer-DJCqc98W.mjs.map} +1 -1
- package/dist/{renderer-E54Vv1_W.mjs → renderer-DODOyrty.mjs} +3 -3
- package/dist/{renderer-E54Vv1_W.mjs.map → renderer-DODOyrty.mjs.map} +1 -1
- package/dist/{renderer-DuFwDHHF.mjs → renderer-DRYxcfgi.mjs} +2 -2
- package/dist/renderer-DSuDMYqY.mjs +5 -0
- package/dist/{renderer-BGDKxfsM.mjs → renderer-DYreml_A.mjs} +3 -3
- package/dist/{renderer-BGDKxfsM.mjs.map → renderer-DYreml_A.mjs.map} +1 -1
- package/dist/{renderer-BGnMRdUK.mjs → renderer-DZJnNISO.mjs} +2 -2
- package/dist/{renderer-BGnMRdUK.mjs.map → renderer-DZJnNISO.mjs.map} +1 -1
- package/dist/renderer-DaIstCIs.mjs +5 -0
- package/dist/renderer-DbOD3Nc0.mjs +5 -0
- package/dist/{renderer-B3gQT1uq.mjs → renderer-DbenSRjl.mjs} +3 -3
- package/dist/{renderer-B3gQT1uq.mjs.map → renderer-DbenSRjl.mjs.map} +1 -1
- package/dist/{renderer-DGsWlXjl.mjs → renderer-DcXxkxds.mjs} +75 -72
- package/dist/renderer-DcXxkxds.mjs.map +1 -0
- package/dist/renderer-DdRCA1bn.mjs +6 -0
- package/dist/{renderer-PDs21Gsq.mjs → renderer-DdUtL-qF.mjs} +3 -3
- package/dist/{renderer-PDs21Gsq.mjs.map → renderer-DdUtL-qF.mjs.map} +1 -1
- package/dist/renderer-DfO37haf.mjs +5 -0
- package/dist/renderer-DjZwzxZ7.mjs +5 -0
- package/dist/renderer-DnAvySNy.mjs +5 -0
- package/dist/{renderer-BwnQRuce.mjs → renderer-DnbY04Qk.mjs} +3 -3
- package/dist/{renderer-BwnQRuce.mjs.map → renderer-DnbY04Qk.mjs.map} +1 -1
- package/dist/renderer-DnvEPm5E.mjs +5 -0
- package/dist/{renderer-C_-hWBF1.mjs → renderer-Do9mW_-B.mjs} +3 -3
- package/dist/{renderer-C_-hWBF1.mjs.map → renderer-Do9mW_-B.mjs.map} +1 -1
- package/dist/{renderer-PDAlUGKG.mjs → renderer-Dqg8qxjq.mjs} +3 -3
- package/dist/{renderer-PDAlUGKG.mjs.map → renderer-Dqg8qxjq.mjs.map} +1 -1
- package/dist/{renderer-wrXFfvpJ.mjs → renderer-DvkRbmb2.mjs} +3 -3
- package/dist/{renderer-wrXFfvpJ.mjs.map → renderer-DvkRbmb2.mjs.map} +1 -1
- package/dist/renderer-DwNpKnWf.mjs +5 -0
- package/dist/{renderer-Cv6od7Be.mjs → renderer-Dw_xy3zx.mjs} +3 -3
- package/dist/{renderer-Cv6od7Be.mjs.map → renderer-Dw_xy3zx.mjs.map} +1 -1
- package/dist/renderer-Dx3Dghus.mjs +5 -0
- package/dist/{renderer-lP23pU1s.mjs → renderer-DyrNti45.mjs} +3 -3
- package/dist/{renderer-lP23pU1s.mjs.map → renderer-DyrNti45.mjs.map} +1 -1
- package/dist/renderer-KvaoEwIk.mjs +5 -0
- package/dist/{renderer-Cw7R1U43.mjs → renderer-SwUFJ4aA.mjs} +3 -3
- package/dist/{renderer-Cw7R1U43.mjs.map → renderer-SwUFJ4aA.mjs.map} +1 -1
- package/dist/{renderer-C4Jfgjm5.mjs → renderer-XEoQnDs5.mjs} +3 -3
- package/dist/{renderer-C4Jfgjm5.mjs.map → renderer-XEoQnDs5.mjs.map} +1 -1
- package/dist/{renderer-BXGr426R.mjs → renderer-Xw9Zn3pD.mjs} +2 -2
- package/dist/{renderer-BXGr426R.mjs.map → renderer-Xw9Zn3pD.mjs.map} +1 -1
- package/dist/{renderer-BEpA2g1_.mjs → renderer-ZWIDTOLV.mjs} +2 -2
- package/dist/{renderer-BEpA2g1_.mjs.map → renderer-ZWIDTOLV.mjs.map} +1 -1
- package/dist/{renderer-D36Pjp0B.mjs → renderer-bGhRdKB7.mjs} +2 -2
- package/dist/{renderer-D36Pjp0B.mjs.map → renderer-bGhRdKB7.mjs.map} +1 -1
- package/dist/{renderer-ZarULMvJ.mjs → renderer-d4weUlVq.mjs} +2 -2
- package/dist/{renderer-ZarULMvJ.mjs.map → renderer-d4weUlVq.mjs.map} +1 -1
- package/dist/{renderer-YEDoDnsE.mjs → renderer-g7CMgD1v.mjs} +3 -3
- package/dist/{renderer-YEDoDnsE.mjs.map → renderer-g7CMgD1v.mjs.map} +1 -1
- package/dist/{renderer-CZGpETnc.mjs → renderer-gjLFqdv-.mjs} +4 -4
- package/dist/{renderer-CZGpETnc.mjs.map → renderer-gjLFqdv-.mjs.map} +1 -1
- package/dist/{renderer-H-Adf8Za.mjs → renderer-gv0u9XmU.mjs} +3 -3
- package/dist/{renderer-H-Adf8Za.mjs.map → renderer-gv0u9XmU.mjs.map} +1 -1
- package/dist/{renderer-FPYVmSf6.mjs → renderer-jJWwrha1.mjs} +3 -3
- package/dist/{renderer-FPYVmSf6.mjs.map → renderer-jJWwrha1.mjs.map} +1 -1
- package/dist/{renderer-BKhdqykZ.mjs → renderer-kl_pAczD.mjs} +3 -3
- package/dist/{renderer-BKhdqykZ.mjs.map → renderer-kl_pAczD.mjs.map} +1 -1
- package/dist/{renderer-BKL3UEzN.mjs → renderer-lrKwPhZi.mjs} +2 -2
- package/dist/{renderer-BKL3UEzN.mjs.map → renderer-lrKwPhZi.mjs.map} +1 -1
- package/dist/{renderer-C2O4nK9x.mjs → renderer-pnYj1REL.mjs} +3 -3
- package/dist/{renderer-C2O4nK9x.mjs.map → renderer-pnYj1REL.mjs.map} +1 -1
- package/dist/{renderers-CP8CFWDs-CZJ7NRsl.mjs → renderers-CP8CFWDs-CDikhBRO.mjs} +3 -3
- package/dist/{renderers-CP8CFWDs-CZJ7NRsl.mjs.map → renderers-CP8CFWDs-CDikhBRO.mjs.map} +1 -1
- package/dist/signals/index.d.mts +5 -5
- package/dist/signals/index.mjs +6 -6
- package/dist/{signals-BrNF9l9s.mjs → signals-BeAWA_1Y.mjs} +3 -3
- package/dist/{signals-BrNF9l9s.mjs.map → signals-BeAWA_1Y.mjs.map} +1 -1
- package/dist/{src-_l81YkMD.mjs → src-LYcH_X8o.mjs} +233 -27
- package/dist/src-LYcH_X8o.mjs.map +1 -0
- package/package.json +58 -58
- package/dist/dist-D021IFfp.mjs.map +0 -1
- package/dist/dist-DSKhnhDk.mjs.map +0 -1
- package/dist/dist-cC6nOaMR.mjs.map +0 -1
- package/dist/index-3Vdef2KE.d.mts.map +0 -1
- package/dist/index-CWG19cm-.d.mts.map +0 -1
- package/dist/index-ocZc12ZS.d.mts.map +0 -1
- package/dist/preview-engine/chunk-3SR6BI4B.mjs +0 -1
- package/dist/preview-engine/chunk-5NLDETDV.mjs +0 -1974
- package/dist/preview-engine/chunk-ALSOO22Q.mjs +0 -1
- package/dist/preview-engine/chunk-NKGXINCU.mjs +0 -1
- package/dist/preview-engine/chunk-PJMX2GFY.mjs +0 -1
- package/dist/preview-engine/chunk-SIXIF5QZ.mjs +0 -3
- package/dist/preview-engine/renderers-4CRXZJ5G.mjs +0 -1
- package/dist/preview-engine/renderers-5QF4OLFQ.mjs +0 -1
- package/dist/preview-engine/renderers-5SOW6OVE.mjs +0 -1
- package/dist/preview-engine/renderers-6M2HPY2J.mjs +0 -1
- package/dist/preview-engine/renderers-7HT5NW7W.mjs +0 -1
- package/dist/preview-engine/renderers-ATTGUTYY.mjs +0 -1
- package/dist/preview-engine/renderers-AYWCRBQC.mjs +0 -1
- package/dist/preview-engine/renderers-BA6ZCPI5.mjs +0 -1
- package/dist/preview-engine/renderers-BAS7RXTC.mjs +0 -1
- package/dist/preview-engine/renderers-CZTCW4FC.mjs +0 -1
- package/dist/preview-engine/renderers-D2XEQ2YY.mjs +0 -1
- package/dist/preview-engine/renderers-DFIJYBW2.mjs +0 -1
- package/dist/preview-engine/renderers-EJN6M6DO.mjs +0 -1
- package/dist/preview-engine/renderers-EK3HQUUF.mjs +0 -1
- package/dist/preview-engine/renderers-G34C5J27.mjs +0 -1
- package/dist/preview-engine/renderers-GKVTNDVH.mjs +0 -1
- package/dist/preview-engine/renderers-HZBYQKIQ.mjs +0 -1
- package/dist/preview-engine/renderers-IK7ZJJKJ.mjs +0 -1
- package/dist/preview-engine/renderers-ISASPH6T.mjs +0 -1
- package/dist/preview-engine/renderers-IT5ISWSA.mjs +0 -228
- package/dist/preview-engine/renderers-IW5MWDBI.mjs +0 -1
- package/dist/preview-engine/renderers-K2PI4WJS.mjs +0 -1
- package/dist/preview-engine/renderers-KESHVFL2.mjs +0 -1
- package/dist/preview-engine/renderers-KTZZVAB2.mjs +0 -1
- package/dist/preview-engine/renderers-KY5CJEOV.mjs +0 -1
- package/dist/preview-engine/renderers-M5W6SUQD.mjs +0 -1
- package/dist/preview-engine/renderers-MEM3REMI.mjs +0 -1
- package/dist/preview-engine/renderers-N36R42RL.mjs +0 -1
- package/dist/preview-engine/renderers-O5XMWFPY.mjs +0 -1
- package/dist/preview-engine/renderers-QJA35PKC.mjs +0 -1
- package/dist/preview-engine/renderers-TJIFTQHV.mjs +0 -1
- package/dist/preview-engine/renderers-TN4XN436.mjs +0 -1
- package/dist/preview-engine/renderers-UDYLCYCB.mjs +0 -1
- package/dist/preview-engine/renderers-WZ7SFIBZ.mjs +0 -1
- package/dist/preview-engine/renderers-X6QNDC6R.mjs +0 -1
- package/dist/preview-engine/renderers-X6RNTJOV.mjs +0 -1
- package/dist/preview-engine/renderers-Y3N7236P.mjs +0 -1
- package/dist/preview-engine/renderers-YP3ASXIJ.mjs +0 -1
- package/dist/preview-engine/renderers-YVJAG3PD.mjs +0 -1
- package/dist/preview-engine/renderers-ZPPZNMKJ.mjs +0 -1
- package/dist/program-W4CWO2bp.mjs.map +0 -1
- package/dist/renderer-8HnHWqYj.mjs +0 -5
- package/dist/renderer-8itujues.mjs +0 -5
- package/dist/renderer-BFods-7v.mjs +0 -5
- package/dist/renderer-BbQygRwc.mjs +0 -5
- package/dist/renderer-Bq_sPaCg.mjs +0 -5
- package/dist/renderer-BrkDrvqS.mjs +0 -5
- package/dist/renderer-C1diVT5Q.mjs +0 -5
- package/dist/renderer-C8E7ajTR.mjs +0 -5
- package/dist/renderer-CAz8JW5c.mjs +0 -5
- package/dist/renderer-CKfrUdGw.mjs +0 -5
- package/dist/renderer-CL0rqBA4.mjs +0 -5
- package/dist/renderer-COyavIEv.mjs +0 -5
- package/dist/renderer-CUrShvUZ.mjs +0 -5
- package/dist/renderer-CWvJ_GaL.mjs +0 -5
- package/dist/renderer-Ck0mGIo9.mjs +0 -5
- package/dist/renderer-CoaDDIS6.mjs.map +0 -1
- package/dist/renderer-CqIXqdf0.mjs +0 -5
- package/dist/renderer-Csod6biL.mjs +0 -5
- package/dist/renderer-CueO924-.mjs +0 -5
- package/dist/renderer-D1vM4obC.mjs +0 -5
- package/dist/renderer-DB7QUt90.mjs +0 -5
- package/dist/renderer-DGsWlXjl.mjs.map +0 -1
- package/dist/renderer-DLMDnKlA.mjs +0 -6
- package/dist/renderer-DNs7yWMr.mjs +0 -5
- package/dist/renderer-DOrtuoa9.mjs +0 -5
- package/dist/renderer-D_JgOawb.mjs +0 -5
- package/dist/renderer-DdLo1uoa.mjs +0 -5
- package/dist/renderer-DeX3JEZV.mjs +0 -5
- package/dist/renderer-DfgZVACG.mjs +0 -5
- package/dist/renderer-DoYG7Pn6.mjs +0 -5
- package/dist/renderer-Jwqa-BUN.mjs +0 -5
- package/dist/renderer-WTvo3wo9.d.mts.map +0 -1
- package/dist/renderer-WyaqnVMz.mjs +0 -3
- package/dist/renderer-Z2Vs-IqP.mjs +0 -5
- package/dist/renderer-ZnyCpdWE.mjs +0 -5
- package/dist/renderer-emCnsnTH.mjs +0 -5
- package/dist/renderer-ioxa4-Dy.mjs +0 -5
- package/dist/renderer-iyIwrDWO.mjs +0 -5
- package/dist/renderer-tuudA_RZ.mjs +0 -5
- package/dist/renderer-yyn8S821.mjs +0 -5
- package/dist/src-_l81YkMD.mjs.map +0 -1
package/dist/registry/index.json
CHANGED
|
@@ -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,
|
|
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
|
}
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
42
|
"path": "terminal/plan.ts",
|
|
43
|
-
"content": "/**\n * Terminal planning, kept pure: run the script on a clock and record what\n * appears where and when. Nodes are built from this plan; tests read it.\n */\nimport { type Rng, typingFrames } from \"framefields/components\";\n\nexport type Step =\n\t| { cmd: string; prompt?: string }\n\t| { output: string | string[] }\n\t| { spinner: string; for: number; done?: string }\n\t| { progress: string; for: number }\n\t| { pause: number }\n\t| { clear: true };\n\n/** A run of text in one colour (a palette name) and weight. */\nexport interface Span {\n\ttext: string;\n\tcolor?: string;\n\tbold?: boolean;\n}\n\nexport interface PlannedLine {\n\trow: number;\n\t/** Frame the line appears. */\n\tappear: number;\n\t/** Frame it stops being drawn (scrolled out or cleared); Infinity if never. */\n\tuntil: number;\n\tspans: Span[];\n\tstep: number;\n\tkind: \"cmd\" | \"output\" | \"spinner\" | \"done\" | \"progress\" | \"prompt\";\n\t/** Columns the text starts in (room for a spinner or check mark). */\n\tindent?: number;\n\t/** For commands: characters already shown (the prompt) and each keystroke's frame. */\n\ttyping?: { prompt: number; frames: number[] };\n}\n\nexport interface TerminalPlan {\n\tlines: PlannedLine[];\n\t/** Rotating spinners: row, from frame, to frame. */\n\tspinners: Array<{ row: number; from: number; to: number; step: number }>;\n\t/** Check marks drawn at `at` on `row`, before the text from column 2. */\n\tchecks: Array<{ row: number; at: number; step: number }>;\n\tprogress: Array<{\n\t\trow: number;\n\t\tfrom: number;\n\t\tto: number;\n\t\tlabel: string;\n\t\tstep: number;\n\t}>;\n\t/** First visible row from each frame on; `jump` (a clear) snaps instead of gliding. */\n\tscroll: Array<[frame: number, top: number, jump?: boolean]>;\n\t/** Where the caret is, from each frame on (null = hidden). */\n\tcaret: Array<[frame: number, at: { row: number; col: number } | null]>;\n\tkeystrokes: Array<{ frame: number; index: number; step: number }>;\n\tsubmits: Array<{ frame: number; step: number }>;\n\toutputs: Array<{ frame: number; end: number; step: number }>;\n\tdone: number;\n\t/** Longest line, in characters. */\n\twidest: number;\n}\n\nexport interface PlanOptions {\n\tprompt: string;\n\tcps: number;\n\t/** Lines a second, or every output line at once. */\n\toutputRate: number | \"instant\";\n\trows: number;\n\tfps: number;\n\tmarkup: boolean;\n\trng: Rng;\n}\n\nconst COLORS = new Set([\n\t\"black\",\n\t\"red\",\n\t\"green\",\n\t\"yellow\",\n\t\"blue\",\n\t\"magenta\",\n\t\"cyan\",\n\t\"white\",\n\t\"gray\",\n\t\"dim\",\n\t\"accent\",\n]);\n\n/** `{green}ok{/} done` → spans. `{bold}` sets weight; unknown tags stay literal. */\nexport function parseMarkup(source: string, markup = true): Span[] {\n\tif (!markup) return [{ text: source }];\n\tconst spans: Span[] = [];\n\tconst stack: Array<{ color?: string; bold?: boolean }> = [{}];\n\tlet buffer = \"\";\n\tconst flush = () => {\n\t\tif (!buffer) return;\n\t\tconst top = stack[stack.length - 1] ?? {};\n\t\tspans.push({ text: buffer, ...top });\n\t\tbuffer = \"\";\n\t};\n\tconst re = /\\{(\\/|[a-z]+)\\}/g;\n\tlet last = 0;\n\tfor (const m of source.matchAll(re)) {\n\t\tconst tag = m[1] as string;\n\t\tconst index = m.index ?? 0;\n\t\tbuffer += source.slice(last, index);\n\t\tlast = index + m[0].length;\n\t\tif (tag === \"/\") {\n\t\t\tflush();\n\t\t\tif (stack.length > 1) stack.pop();\n\t\t} else if (tag === \"bold\" || COLORS.has(tag)) {\n\t\t\tflush();\n\t\t\tconst top = stack[stack.length - 1] ?? {};\n\t\t\tstack.push(\n\t\t\t\ttag === \"bold\" ? { ...top, bold: true } : { ...top, color: tag },\n\t\t\t);\n\t\t} else buffer += m[0];\n\t}\n\tbuffer += source.slice(last);\n\tflush();\n\treturn spans;\n}\n\nexport const spansText = (spans: readonly Span[]) =>\n\tspans.map((s) => s.text).join(\"\");\n\n/** Frames between Enter and the first line of output. */\nconst SUBMIT_BEAT = 0.3;\n/** Frames the scroll takes to glide one or more lines. */\nexport const SCROLL_GLIDE = 4;\n/** A check mark replaces these leading glyphs (fonts often lack them). */\nconst DONE_MARK = /^\\s*[✔✓√]\\s*/;\n\nexport function planTerminal(\n\tscript: readonly Step[],\n\to: PlanOptions,\n): TerminalPlan {\n\tconst plan: TerminalPlan = {\n\t\tlines: [],\n\t\tspinners: [],\n\t\tchecks: [],\n\t\tprogress: [],\n\t\tscroll: [[0, 0]],\n\t\tcaret: [[0, null]],\n\t\tkeystrokes: [],\n\t\tsubmits: [],\n\t\toutputs: [],\n\t\tdone: 0,\n\t\twidest: 0,\n\t};\n\tlet t = 0;\n\tlet row = 0;\n\tlet top = 0;\n\tlet typed = 0;\n\tconst perLine = o.outputRate === \"instant\" ? 0 : o.fps / o.outputRate;\n\n\tconst show = (line: Omit<PlannedLine, \"until\" | \"row\">): PlannedLine => {\n\t\tif (row - top >= o.rows) {\n\t\t\ttop = row - o.rows + 1;\n\t\t\tplan.scroll.push([line.appear, top]);\n\t\t}\n\t\tconst planned = { ...line, row, until: Number.POSITIVE_INFINITY };\n\t\tplan.lines.push(planned);\n\t\tplan.widest = Math.max(\n\t\t\tplan.widest,\n\t\t\tspansText(line.spans).length + (line.indent ?? 0),\n\t\t);\n\t\trow++;\n\t\treturn planned;\n\t};\n\tconst caretAt = (frame: number, at: { row: number; col: number } | null) =>\n\t\tplan.caret.push([frame, at]);\n\n\tscript.forEach((step, s) => {\n\t\tif (\"cmd\" in step) {\n\t\t\tconst prompt = step.prompt ?? o.prompt;\n\t\t\tconst line = show({\n\t\t\t\tappear: t,\n\t\t\t\tspans: [{ text: prompt, color: \"accent\" }, { text: step.cmd }],\n\t\t\t\tstep: s,\n\t\t\t\tkind: \"cmd\",\n\t\t\t});\n\t\t\tconst frames = typingFrames(\n\t\t\t\tstep.cmd,\n\t\t\t\to.cps,\n\t\t\t\to.fps,\n\t\t\t\to.rng,\n\t\t\t\tt + Math.round(o.fps * 0.25),\n\t\t\t);\n\t\t\tline.typing = { prompt: prompt.length, frames };\n\t\t\tcaretAt(t, { row: line.row, col: prompt.length });\n\t\t\tframes.forEach((f, i) => {\n\t\t\t\tplan.keystrokes.push({ frame: f, index: typed++, step: s });\n\t\t\t\tcaretAt(f, { row: line.row, col: prompt.length + i + 1 });\n\t\t\t});\n\t\t\tconst submit =\n\t\t\t\t(frames[frames.length - 1] ?? t) + Math.round(o.fps * SUBMIT_BEAT);\n\t\t\tplan.submits.push({ frame: submit, step: s });\n\t\t\tcaretAt(submit, null);\n\t\t\tt = submit + 1;\n\t\t} else if (\"output\" in step) {\n\t\t\tconst lines = Array.isArray(step.output)\n\t\t\t\t? step.output\n\t\t\t\t: step.output.split(\"\\n\");\n\t\t\tconst first = Math.round(t);\n\t\t\tlet at = t;\n\t\t\tfor (const text of lines) {\n\t\t\t\tshow({\n\t\t\t\t\tappear: Math.round(at),\n\t\t\t\t\tspans: parseMarkup(text, o.markup),\n\t\t\t\t\tstep: s,\n\t\t\t\t\tkind: \"output\",\n\t\t\t\t});\n\t\t\t\tat += perLine;\n\t\t\t}\n\t\t\tplan.outputs.push({\n\t\t\t\tframe: first,\n\t\t\t\tend: Math.round(Math.max(t, at - perLine)),\n\t\t\t\tstep: s,\n\t\t\t});\n\t\t\tt = Math.round(at);\n\t\t} else if (\"spinner\" in step) {\n\t\t\tconst from = Math.round(t);\n\t\t\tconst to = from + step.for;\n\t\t\tconst spinning = show({\n\t\t\t\tappear: from,\n\t\t\t\tspans: [{ text: step.spinner }],\n\t\t\t\tindent: 2,\n\t\t\t\tstep: s,\n\t\t\t\tkind: \"spinner\",\n\t\t\t});\n\t\t\tspinning.until = to;\n\t\t\tplan.spinners.push({ row: spinning.row, from, to, step: s });\n\t\t\trow--;\n\t\t\
|
|
43
|
+
"content": "/**\n * Terminal planning, kept pure: run the script on a clock and record what\n * appears where and when. Nodes are built from this plan; tests read it.\n */\nimport { type Rng, typingFrames } from \"framefields/components\";\n\nexport type Step =\n\t| { cmd: string; prompt?: string }\n\t| { output: string | string[] }\n\t| { spinner: string; for: number; done?: string }\n\t| { progress: string; for: number }\n\t| { pause: number }\n\t| { clear: true };\n\n/** A run of text in one colour (a palette name) and weight. */\nexport interface Span {\n\ttext: string;\n\tcolor?: string;\n\tbold?: boolean;\n}\n\nexport interface PlannedLine {\n\trow: number;\n\t/** Frame the line appears. */\n\tappear: number;\n\t/** Frame it stops being drawn (scrolled out or cleared); Infinity if never. */\n\tuntil: number;\n\tspans: Span[];\n\tstep: number;\n\tkind: \"cmd\" | \"output\" | \"spinner\" | \"done\" | \"progress\" | \"prompt\";\n\t/** Columns the text starts in (room for a spinner or check mark). */\n\tindent?: number;\n\t/** For commands: characters already shown (the prompt) and each keystroke's frame. */\n\ttyping?: { prompt: number; frames: number[] };\n}\n\nexport interface TerminalPlan {\n\tlines: PlannedLine[];\n\t/** Rotating spinners: row, from frame, to frame. */\n\tspinners: Array<{ row: number; from: number; to: number; step: number }>;\n\t/** Check marks drawn at `at` on `row`, before the text from column 2. */\n\tchecks: Array<{ row: number; at: number; step: number }>;\n\tprogress: Array<{\n\t\trow: number;\n\t\tfrom: number;\n\t\tto: number;\n\t\tlabel: string;\n\t\tstep: number;\n\t}>;\n\t/** First visible row from each frame on; `jump` (a clear) snaps instead of gliding. */\n\tscroll: Array<[frame: number, top: number, jump?: boolean]>;\n\t/** Where the caret is, from each frame on (null = hidden). */\n\tcaret: Array<[frame: number, at: { row: number; col: number } | null]>;\n\tkeystrokes: Array<{ frame: number; index: number; step: number }>;\n\tsubmits: Array<{ frame: number; step: number }>;\n\toutputs: Array<{ frame: number; end: number; step: number }>;\n\tdone: number;\n\t/** Longest line, in characters. */\n\twidest: number;\n}\n\nexport interface PlanOptions {\n\tprompt: string;\n\tcps: number;\n\t/** Lines a second, or every output line at once. */\n\toutputRate: number | \"instant\";\n\trows: number;\n\tfps: number;\n\tmarkup: boolean;\n\trng: Rng;\n}\n\nconst COLORS = new Set([\n\t\"black\",\n\t\"red\",\n\t\"green\",\n\t\"yellow\",\n\t\"blue\",\n\t\"magenta\",\n\t\"cyan\",\n\t\"white\",\n\t\"gray\",\n\t\"dim\",\n\t\"accent\",\n]);\n\n/** `{green}ok{/} done` → spans. `{bold}` sets weight; unknown tags stay literal. */\nexport function parseMarkup(source: string, markup = true): Span[] {\n\tif (!markup) return [{ text: source }];\n\tconst spans: Span[] = [];\n\tconst stack: Array<{ color?: string; bold?: boolean }> = [{}];\n\tlet buffer = \"\";\n\tconst flush = () => {\n\t\tif (!buffer) return;\n\t\tconst top = stack[stack.length - 1] ?? {};\n\t\tspans.push({ text: buffer, ...top });\n\t\tbuffer = \"\";\n\t};\n\tconst re = /\\{(\\/|[a-z]+)\\}/g;\n\tlet last = 0;\n\tfor (const m of source.matchAll(re)) {\n\t\tconst tag = m[1] as string;\n\t\tconst index = m.index ?? 0;\n\t\tbuffer += source.slice(last, index);\n\t\tlast = index + m[0].length;\n\t\tif (tag === \"/\") {\n\t\t\tflush();\n\t\t\tif (stack.length > 1) stack.pop();\n\t\t} else if (tag === \"bold\" || COLORS.has(tag)) {\n\t\t\tflush();\n\t\t\tconst top = stack[stack.length - 1] ?? {};\n\t\t\tstack.push(\n\t\t\t\ttag === \"bold\" ? { ...top, bold: true } : { ...top, color: tag },\n\t\t\t);\n\t\t} else buffer += m[0];\n\t}\n\tbuffer += source.slice(last);\n\tflush();\n\treturn spans;\n}\n\nexport const spansText = (spans: readonly Span[]) =>\n\tspans.map((s) => s.text).join(\"\");\n\n/** Frames between Enter and the first line of output. */\nconst SUBMIT_BEAT = 0.3;\n/** Frames the scroll takes to glide one or more lines. */\nexport const SCROLL_GLIDE = 4;\n/** A check mark replaces these leading glyphs (fonts often lack them). */\nconst DONE_MARK = /^\\s*[✔✓√]\\s*/;\n\n/**\n * The spans without a leading check mark, found in the first span with\n * visible text, so `{green}✓{/} done` loses it as `✓ done` does (the\n * drawn check takes its place).\n */\nexport function withoutDoneMark(spans: Span[]): Span[] {\n\tconst first = spans.findIndex((sp) => sp.text.trim() !== \"\");\n\tconst span = spans[first];\n\tif (!span || !DONE_MARK.test(span.text)) return spans;\n\tconst rest = [\n\t\t{ ...span, text: span.text.replace(DONE_MARK, \"\") },\n\t\t...spans.slice(first + 1),\n\t];\n\t// The mark's trailing space goes with it, even when it's in the next span.\n\twhile (rest[0] && rest[0].text.trimStart() === \"\") rest.shift();\n\tif (rest[0]) rest[0] = { ...rest[0], text: rest[0].text.trimStart() };\n\treturn rest;\n}\n\nexport function planTerminal(\n\tscript: readonly Step[],\n\to: PlanOptions,\n): TerminalPlan {\n\tconst plan: TerminalPlan = {\n\t\tlines: [],\n\t\tspinners: [],\n\t\tchecks: [],\n\t\tprogress: [],\n\t\tscroll: [[0, 0]],\n\t\tcaret: [[0, null]],\n\t\tkeystrokes: [],\n\t\tsubmits: [],\n\t\toutputs: [],\n\t\tdone: 0,\n\t\twidest: 0,\n\t};\n\tlet t = 0;\n\tlet row = 0;\n\tlet top = 0;\n\tlet typed = 0;\n\tconst perLine = o.outputRate === \"instant\" ? 0 : o.fps / o.outputRate;\n\n\tconst show = (line: Omit<PlannedLine, \"until\" | \"row\">): PlannedLine => {\n\t\tif (row - top >= o.rows) {\n\t\t\ttop = row - o.rows + 1;\n\t\t\tplan.scroll.push([line.appear, top]);\n\t\t}\n\t\tconst planned = { ...line, row, until: Number.POSITIVE_INFINITY };\n\t\tplan.lines.push(planned);\n\t\tplan.widest = Math.max(\n\t\t\tplan.widest,\n\t\t\tspansText(line.spans).length + (line.indent ?? 0),\n\t\t);\n\t\trow++;\n\t\treturn planned;\n\t};\n\tconst caretAt = (frame: number, at: { row: number; col: number } | null) =>\n\t\tplan.caret.push([frame, at]);\n\n\tscript.forEach((step, s) => {\n\t\tif (\"cmd\" in step) {\n\t\t\tconst prompt = step.prompt ?? o.prompt;\n\t\t\tconst line = show({\n\t\t\t\tappear: t,\n\t\t\t\tspans: [{ text: prompt, color: \"accent\" }, { text: step.cmd }],\n\t\t\t\tstep: s,\n\t\t\t\tkind: \"cmd\",\n\t\t\t});\n\t\t\tconst frames = typingFrames(\n\t\t\t\tstep.cmd,\n\t\t\t\to.cps,\n\t\t\t\to.fps,\n\t\t\t\to.rng,\n\t\t\t\tt + Math.round(o.fps * 0.25),\n\t\t\t);\n\t\t\tline.typing = { prompt: prompt.length, frames };\n\t\t\tcaretAt(t, { row: line.row, col: prompt.length });\n\t\t\tframes.forEach((f, i) => {\n\t\t\t\tplan.keystrokes.push({ frame: f, index: typed++, step: s });\n\t\t\t\tcaretAt(f, { row: line.row, col: prompt.length + i + 1 });\n\t\t\t});\n\t\t\tconst submit =\n\t\t\t\t(frames[frames.length - 1] ?? t) + Math.round(o.fps * SUBMIT_BEAT);\n\t\t\tplan.submits.push({ frame: submit, step: s });\n\t\t\tcaretAt(submit, null);\n\t\t\tt = submit + 1;\n\t\t} else if (\"output\" in step) {\n\t\t\tconst lines = Array.isArray(step.output)\n\t\t\t\t? step.output\n\t\t\t\t: step.output.split(\"\\n\");\n\t\t\tconst first = Math.round(t);\n\t\t\tlet at = t;\n\t\t\tfor (const text of lines) {\n\t\t\t\tshow({\n\t\t\t\t\tappear: Math.round(at),\n\t\t\t\t\tspans: parseMarkup(text, o.markup),\n\t\t\t\t\tstep: s,\n\t\t\t\t\tkind: \"output\",\n\t\t\t\t});\n\t\t\t\tat += perLine;\n\t\t\t}\n\t\t\tplan.outputs.push({\n\t\t\t\tframe: first,\n\t\t\t\tend: Math.round(Math.max(t, at - perLine)),\n\t\t\t\tstep: s,\n\t\t\t});\n\t\t\tt = Math.round(at);\n\t\t} else if (\"spinner\" in step) {\n\t\t\tconst from = Math.round(t);\n\t\t\tconst to = from + step.for;\n\t\t\tconst spinning = show({\n\t\t\t\tappear: from,\n\t\t\t\tspans: [{ text: step.spinner }],\n\t\t\t\tindent: 2,\n\t\t\t\tstep: s,\n\t\t\t\tkind: \"spinner\",\n\t\t\t});\n\t\t\tspinning.until = to;\n\t\t\tplan.spinners.push({ row: spinning.row, from, to, step: s });\n\t\t\trow--;\n\t\t\tshow({\n\t\t\t\tappear: to,\n\t\t\t\tspans: withoutDoneMark(\n\t\t\t\t\tparseMarkup(step.done ?? step.spinner, o.markup),\n\t\t\t\t),\n\t\t\t\tindent: 2,\n\t\t\t\tstep: s,\n\t\t\t\tkind: \"done\",\n\t\t\t});\n\t\t\tplan.checks.push({ row: spinning.row, at: to, step: s });\n\t\t\tt = to;\n\t\t} else if (\"progress\" in step) {\n\t\t\tconst from = Math.round(t);\n\t\t\tconst line = show({\n\t\t\t\tappear: from,\n\t\t\t\tspans: [{ text: step.progress }],\n\t\t\t\tstep: s,\n\t\t\t\tkind: \"progress\",\n\t\t\t});\n\t\t\tplan.progress.push({\n\t\t\t\trow: line.row,\n\t\t\t\tfrom,\n\t\t\t\tto: from + step.for,\n\t\t\t\tlabel: step.progress,\n\t\t\t\tstep: s,\n\t\t\t});\n\t\t\tplan.widest = Math.max(plan.widest, step.progress.length + 32);\n\t\t\tt = from + step.for;\n\t\t} else if (\"pause\" in step) {\n\t\t\tt += step.pause;\n\t\t} else {\n\t\t\tconst at = Math.round(t);\n\t\t\tfor (const line of plan.lines) line.until = Math.min(line.until, at);\n\t\t\ttop = row;\n\t\t\tplan.scroll.push([at, top, true]);\n\t\t}\n\t});\n\n\tplan.done = Math.round(t);\n\t// A fresh prompt waits at the end.\n\tconst last = show({\n\t\tappear: plan.done,\n\t\tspans: [{ text: o.prompt, color: \"accent\" }],\n\t\tstep: script.length,\n\t\tkind: \"prompt\",\n\t});\n\tcaretAt(plan.done, { row: last.row, col: o.prompt.length });\n\n\t// Lines stop drawing once scrolled out (after the glide), so only a\n\t// screenful of nodes is ever live.\n\tfor (const line of plan.lines) {\n\t\tconst gone = plan.scroll.find(([, top]) => top > line.row);\n\t\tif (gone) line.until = Math.min(line.until, gone[0] + SCROLL_GLIDE);\n\t}\n\treturn plan;\n}\n"
|
|
44
44
|
}
|
|
45
45
|
]
|
|
46
46
|
}
|
|
@@ -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,
|
|
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-
|
|
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-
|
|
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 };
|
package/dist/renderer/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import "../chunk-D7gtBK74.mjs";
|
|
2
|
-
import { R as initHeadlessWebGPU, x as describeGpu } from "../dist-
|
|
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-
|
|
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-
|
|
4
|
-
import { c as drawTextureToTarget, l as fragmentBindGroupLayout, o as drawChildToTexture, s as drawFullscreenPass, t as FULLSCREEN_QUAD_WGSL, u as perDevice } from "./gpu-
|
|
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-
|
|
191
|
+
//# sourceMappingURL=renderer-1-FLx8kd.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderer-
|
|
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":";;;;;;AAIA,MAAM,KAAK;AACX,MAAM,iBAAiB;;AAEvB,MAAM,SAAS,CAAC;;;;;EAKd,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA6DrB;AACF,MAAM,YAAY,WAAW,YAAY;CACxC,QAAQ,wBAAwB,QAAQ;EACvC;EACA;EACA;EACA;EACA,EAAE,GAAG,GAAG,SAAS;CAClB,2BAA2B,IAAI,KAAK;CACpC,SAAS,OAAO,cAAc;EAC7B,WAAW;EACX,WAAW;EACX,cAAc;EACd,cAAc;EACd,CAAC;CACF,EAAE;AACH,SAAS,aAAa,QAAQ,QAAQ;CACrC,MAAM,IAAI,UAAU,OAAO;CAC3B,IAAI,OAAO,EAAE,UAAU,IAAI,OAAO;AAClC,KAAI,CAAC,MAAM;AACV,SAAO,OAAO,KAAK,MAAM,MAAM;GAC9B,MAAM,SAAS,OAAO,mBAAmB;IACxC;IACA,OAAO,GAAG,GAAG,GAAG,EAAE;IAClB,CAAC;AACF,UAAO,OAAO,qBAAqB;IAClC,OAAO,GAAG,GAAG,GAAG;IAChB,QAAQ,OAAO,qBAAqB,EAAE,kBAAkB,CAAC,EAAE,OAAO,EAAE,CAAC;IACrE,QAAQ;KACP;KACA,YAAY;KACZ;IACD,UAAU;KACT;KACA,YAAY;KACZ,SAAS,CAAC,EAAE,QAAQ,CAAC;KACrB;IACD,WAAW,EAAE,UAAU,kBAAkB;IACzC,CAAC;IACD;AACF,IAAE,UAAU,IAAI,QAAQ,KAAK;;AAE9B,QAAO;;AAER,SAAS,IAAI,GAAG,UAAU;CACzB,MAAM,IAAI,OAAO,MAAM,WAAW,IAAI,OAAO,EAAE;AAC/C,QAAO,OAAO,SAAS,EAAE,GAAG,IAAI;;;AAGjC,SAAS,KAAK,KAAK,MAAM,MAAM,OAAO;AACrC,MAAK,KAAK,EAAE;AACZ,MAAK,KAAK;AACV,MAAK,KAAK;AACV,MAAK,KAAK,IAAI,IAAI,QAAQ,EAAE;AAC5B,MAAK,KAAK,IAAI,IAAI,OAAO,EAAE;AAC3B,MAAK,KAAK,IAAI,WAAW,KAAK,IAAI,IAAI,IAAI,SAAS,IAAI;;AAExD,MAAM,yBAAyB,OAAO,SAAS;CAC9C,MAAM,EAAE,KAAK,SAAS,MAAM,eAAe,aAAa,cAAc,UAAU;CAChF,MAAM,KAAK,MAAM,cAAc;AAC/B,KAAI,IAAI,OAAO,GAAI;CACnB,MAAM,QAAQ,MAAM,cAAc,WAAW;AAC7C,KAAI,CAAC,MAAO;AACZ,MAAK,KAAK;CACV,MAAM,MAAM,MAAM,OAAO;CACzB,MAAM,QAAQ,KAAK,MAAM,MAAM,oBAAoB,MAAM,SAAS,EAAE;CACpE,MAAM,MAAM,GAAG,QAAQ,kBAAkB,GAAG,OAAO,gBAAgB;EAClE;EACA;EACA,MAAM,QAAQ;EACd,CAAC,GAAG;CACL,MAAM,OAAO,IAAI,aAAa,eAAe;AAC7C,MAAK,KAAK,MAAM,QAAQ,KAAK,MAAM;CACnC,MAAM,EAAE,SAAS,aAAa,MAAM,mBAAmB,MAAM,MAAM;CACnE,MAAM,SAAS,IAAI;CACnB,MAAM,IAAI,UAAU,OAAO;CAC3B,MAAM,YAAY,aAAa,QAAQ,IAAI,SAAS,OAAO;CAC3D,MAAM,MAAM,IAAI,SAAS,mBAAmB,KAAK;CACjD,IAAI,OAAO;AACX,MAAK,MAAM,YAAY,WAAW;EACjC,MAAM,MAAM,IAAI,SAAS,oBAAoB,aAAa,cAAc;GACvE;GACA;GACA;GACA,GAAG,MAAM,mBAAmB,EAAE;GAC9B,CAAC;AACF,qBAAmB,SAAS,IAAI,YAAY,EAAE,UAAU,CAAC,OAAO,gBAAgB;GAC/E,QAAQ,EAAE;GACV,SAAS;IACR;KACC,SAAS;KACT,UAAU,EAAE,QAAQ,KAAK;KACzB;IACD;KACC,SAAS;KACT,UAAU,SAAS,YAAY;KAC/B;IACD;KACC,SAAS;KACT,UAAU,KAAK,YAAY;KAC3B;IACD;KACC,SAAS;KACT,UAAU,EAAE;KACZ;IACD;GACD,CAAC,CAAC,CAAC;AACJ,SAAO;;AAER,qBAAoB,MAAM,KAAK;;AAKhC,IAAI,oBAAoB,eAAe,EAAE,gBAAgB,wBAAwB,CAAC"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { n as defineRenderer } from "./renderer-BgmO1Dzw.mjs";
|
|
2
|
-
import { i as VideoResultSchema, n as ImageResultSchema } from "./dist-
|
|
3
|
-
import { b as colorMatrixWgsl } from "./dist-
|
|
4
|
-
import { a as createFullscreenPipeline, c as drawTextureToTarget, l as fragmentBindGroupLayout, o as drawChildToTexture, s as drawFullscreenPass } from "./gpu-
|
|
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-
|
|
176
|
+
//# sourceMappingURL=renderer-47d3etrX.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderer-
|
|
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":";;;;;;;AAOA,MAAM,qBAAqB,EAAE,OAAO;CACnC,SAAS,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE;CAC5C,SAAS,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE;CAC5C,UAAU,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE;CAC7C,UAAU,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE;CAC7C,CAAC,CAAC,QAAQ,CAAC,aAAa,KAAK,QAAQ;AACrC,KAAI,IAAI,WAAW,IAAI,QAAS,KAAI,SAAS;EAC5C,MAAM,EAAE,aAAa;EACrB,MAAM,CAAC,UAAU;EACjB,SAAS;EACT,CAAC;AACF,KAAI,IAAI,YAAY,IAAI,SAAU,KAAI,SAAS;EAC9C,MAAM,EAAE,aAAa;EACrB,MAAM,CAAC,WAAW;EAClB,SAAS;EACT,CAAC;EACD;AACF,MAAM,yBAAyB,EAAE,OAAO;CACvC,QAAQ,mBAAmB,QAAQ;EAClC,SAAS;EACT,SAAS;EACT,UAAU;EACV,UAAU;EACV,CAAC;CACF,KAAK,mBAAmB,QAAQ;EAC/B,SAAS;EACT,SAAS;EACT,UAAU;EACV,UAAU;EACV,CAAC;CACF,OAAO,mBAAmB,QAAQ;EACjC,SAAS;EACT,SAAS;EACT,UAAU;EACV,UAAU;EACV,CAAC;CACF,MAAM,mBAAmB,QAAQ;EAChC,SAAS;EACT,SAAS;EACT,UAAU;EACV,UAAU;EACV,CAAC;CACF,CAAC,CAAC,QAAQ;AACX,MAAM,qBAAqB,EAAE,MAAM,CAAC,mBAAmB,kBAAkB,CAAC;AAC1E,MAAM,wBAAwB,uBAAuB,OAAO;CAC3D,IAAI,EAAE,QAAQ,SAAS;CACvB,UAAU,EAAE,SAAS,CAAC,UAAU;CAChC,CAAC;AACF,MAAM,sBAAsB;CAC3B,SAAS;CACT,SAAS;CACT,UAAU;CACV,UAAU;CACV;AACD,MAAM,sBAAsB;CAC3B,QAAQ,EAAE,GAAG,qBAAqB;CAClC,KAAK,EAAE,GAAG,qBAAqB;CAC/B,OAAO,EAAE,GAAG,qBAAqB;CACjC,MAAM,EAAE,GAAG,qBAAqB;CAChC;AAWD,MAAM,sCAAsC,IAAI,SAAS;AACzD,MAAM,kBAAkB,IAAI,aAAa,GAAG;AAC5C,SAAS,wBAAwB,QAAQ,QAAQ;CAChD,IAAI,MAAM,oBAAoB,IAAI,OAAO;AACzC,KAAI,CAAC,KAAK;EACT,MAAM,2BAA2B,wBAAwB,QAAQ,CAAC,UAAU,CAAC;EAC7E,MAAM,sBAAsB,wBAAwB,QAAQ,CAAC,WAAW,UAAU,CAAC;AACnF,QAAM;GACL,qBAAqB,yBAAyB,QAAQ;IACrD,OAAO;IACP,QAAQ,OAAO,mBAAmB;KACjC,OAAO;KACP,MAAM;KACN,CAAC;IACF,SAAS,CAAC,0BAA0B,oBAAoB;IACxD;IACA,CAAC;GACF;GACA;GACA;AACD,sBAAoB,IAAI,QAAQ,IAAI;;AAErC,QAAO;;AAER,SAAS,aAAa,IAAI;CACzB,MAAM,UAAU,KAAK,IAAI,GAAG,UAAU,GAAG,SAAS,KAAK;CACvD,MAAM,SAAS,GAAG,WAAW,GAAG,YAAY;AAC5C,QAAO;EACN;EACA,MAAM,GAAG,WAAW,GAAG,UAAU;EACjC;;AAEF,MAAM,uBAAuB,OAAO,SAAS;CAC5C,MAAM,EAAE,KAAK,SAAS,MAAM,eAAe,aAAa,cAAc,UAAU;CAChF,MAAM,KAAK,MAAM,cAAc;AAC/B,KAAI,IAAI,OAAO,YAAY,CAAC,MAAM,cAAc,WAAW,GAAI;AAC/D,MAAK,KAAK;CACV,MAAM,aAAa,MAAM,aAAa,SAAS;CAC/C,MAAM,IAAI,aAAa,GAAG,UAAU,oBAAoB;CACxD,MAAM,IAAI,aAAa,GAAG,OAAO,oBAAoB;CACrD,MAAM,IAAI,aAAa,GAAG,SAAS,oBAAoB;CACvD,MAAM,IAAI,aAAa,GAAG,QAAQ,oBAAoB;CACtD,MAAM,SAAS;EACd,EAAE,QAAQ,EAAE;EACZ;EACA;EACA;EACA,EAAE,OAAO,EAAE,QAAQ,EAAE;EACrB;EACA,EAAE,QAAQ,EAAE;EACZ;EACA;EACA,EAAE,OAAO,EAAE,QAAQ,EAAE;EACrB;EACA;EACA,EAAE,QAAQ,EAAE;EACZ;EACA,EAAE,OAAO,EAAE,QAAQ,EAAE;EACrB;EACA;EACA;EACA;EACA;EACA;CACD,MAAM,QAAQ;CACd,MAAM,SAAS;CACf,MAAM,EAAE,SAAS,WAAW,MAAM,mBAAmB,MAAM,WAAW;CACtE,MAAM,EAAE,qBAAqB,UAAU,0BAA0B,SAAS,qBAAqB,YAAY,wBAAwB,IAAI,QAAQ,IAAI,SAAS,OAAO;CACnK,MAAM,UAAU,IAAI,SAAS,aAAa,WAAW,IAAI,OAAO;AAChE,iBAAgB,KAAK,OAAO;AAC5B,iBAAgB,KAAK,OAAO;AAC5B,iBAAgB,KAAK,OAAO;AAC5B,iBAAgB,KAAK,OAAO;AAC5B,iBAAgB,KAAK,OAAO;AAC5B,iBAAgB,KAAK,OAAO;AAC5B,iBAAgB,KAAK,OAAO;AAC5B,iBAAgB,KAAK,OAAO;AAC5B,iBAAgB,KAAK,OAAO;AAC5B,iBAAgB,KAAK,OAAO;AAC5B,iBAAgB,MAAM,OAAO;AAC7B,iBAAgB,MAAM,OAAO;AAC7B,iBAAgB,MAAM,OAAO;AAC7B,iBAAgB,MAAM,OAAO;AAC7B,iBAAgB,MAAM,OAAO;AAC7B,iBAAgB,MAAM,OAAO;AAC7B,iBAAgB,MAAM,OAAO;AAC7B,iBAAgB,MAAM,OAAO;AAC7B,iBAAgB,MAAM,OAAO;AAC7B,iBAAgB,MAAM,OAAO;CAC7B,MAAM,SAAS,IAAI,SAAS,mBAAmB,gBAAgB;CAC/D,MAAM,SAAS,IAAI,SAAS,oBAAoB,OAAO,QAAQ;EAC9D;EACA;EACA,GAAG,MAAM,mBAAmB,EAAE;EAC9B,CAAC;AACF,oBAAmB,SAAS,OAAO,YAAY,EAAE,UAAU,CAAC,IAAI,OAAO,gBAAgB;EACtF,QAAQ;EACR,SAAS,CAAC;GACT,SAAS;GACT,UAAU,EAAE,QAAQ;GACpB,CAAC;EACF,CAAC,EAAE,IAAI,SAAS,eAAe,aAAa,IAAI,QAAQ,SAAS,QAAQ,QAAQ,CAAC,CAAC;AACpF,qBAAoB,MAAM,QAAQ,MAAM,WAAW,EAAE;;AAKtD,IAAI,oBAAoB,eAAe,EAAE,gBAAgB,sBAAsB,CAAC"}
|
|
@@ -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-
|
|
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-
|
|
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-
|
|
256
|
+
//# sourceMappingURL=renderer-6GCG6JxD.mjs.map
|