@ignix-social/editor 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (586) hide show
  1. package/LICENSE +190 -0
  2. package/NOTICE +54 -0
  3. package/README.md +23 -0
  4. package/bin/ignix-editor.mjs +8 -0
  5. package/package.json +53 -0
  6. package/patches/index.mjs +53 -0
  7. package/scripts/build-ffmpeg-macos.sh +35 -0
  8. package/scripts/build-whisper-macos.sh +21 -0
  9. package/scripts/vendor.mjs +41 -0
  10. package/skills/ignix-editor/SKILL.md +132 -0
  11. package/src/audio.mjs +31 -0
  12. package/src/binaries.json +46 -0
  13. package/src/binaries.mjs +83 -0
  14. package/src/cli.mjs +254 -0
  15. package/src/prepare.mjs +74 -0
  16. package/src/runtime/ignix-frame.js +92 -0
  17. package/src/transcribe.mjs +71 -0
  18. package/src/validate.mjs +71 -0
  19. package/templates/blank/index.html +41 -0
  20. package/vendor/hyperframes/README.md +181 -0
  21. package/vendor/hyperframes/UPSTREAM.json +12 -0
  22. package/vendor/hyperframes/bin/hyperframes-localize-fonts.mjs +12 -0
  23. package/vendor/hyperframes/bin/hyperframes.mjs +11 -0
  24. package/vendor/hyperframes/dist/add-LR3ZR2A5.js +79 -0
  25. package/vendor/hyperframes/dist/agentPromptGenerator-VIIPZCJ3.js +143 -0
  26. package/vendor/hyperframes/dist/animatedGifPrep-P2T6YM7Z.js +34 -0
  27. package/vendor/hyperframes/dist/asset-BE2DOV72.js +26 -0
  28. package/vendor/hyperframes/dist/assetCataloger-VBLPHX24.js +340 -0
  29. package/vendor/hyperframes/dist/auth-RCQB2ZT7.js +60 -0
  30. package/vendor/hyperframes/dist/autoUpdate-RJAX3DAZ.js +268 -0
  31. package/vendor/hyperframes/dist/backgroundChecks-H2LCUIMJ.js +61 -0
  32. package/vendor/hyperframes/dist/backgroundChecksWorker.js +26 -0
  33. package/vendor/hyperframes/dist/batchRender-FT7AP7XH.js +355 -0
  34. package/vendor/hyperframes/dist/beat-analyzer.global.js +338 -0
  35. package/vendor/hyperframes/dist/beats-Y2EOP72P.js +309 -0
  36. package/vendor/hyperframes/dist/benchmark-MZ7GY3JW.js +231 -0
  37. package/vendor/hyperframes/dist/browser-HUTAMRQE.js +237 -0
  38. package/vendor/hyperframes/dist/bundleWithLocalizedFonts-5QOPOB5D.js +52 -0
  39. package/vendor/hyperframes/dist/capture-RLZA6EMR.js +4079 -0
  40. package/vendor/hyperframes/dist/capture-SZZ4IYJK.js +290 -0
  41. package/vendor/hyperframes/dist/captureFile-LYOJOUIE.js +15 -0
  42. package/vendor/hyperframes/dist/captureTimeout-PA4FYWT5.js +27 -0
  43. package/vendor/hyperframes/dist/catalog-KYU55EWR.js +1001 -0
  44. package/vendor/hyperframes/dist/check-TCG4PXLR.js +1697 -0
  45. package/vendor/hyperframes/dist/checkBrowser-RPMO4OW5.js +1032 -0
  46. package/vendor/hyperframes/dist/chunk-2EAXUCCE.js +88 -0
  47. package/vendor/hyperframes/dist/chunk-2EGFTPWD.js +1077 -0
  48. package/vendor/hyperframes/dist/chunk-2PSPARWP.js +6058 -0
  49. package/vendor/hyperframes/dist/chunk-2YN64IN5.js +5747 -0
  50. package/vendor/hyperframes/dist/chunk-3GXQPCJC.js +682 -0
  51. package/vendor/hyperframes/dist/chunk-3ZP5DXE7.js +15 -0
  52. package/vendor/hyperframes/dist/chunk-4NWOGZZS.js +1990 -0
  53. package/vendor/hyperframes/dist/chunk-4PFZPP2N.js +587 -0
  54. package/vendor/hyperframes/dist/chunk-4THIAPBL.js +128 -0
  55. package/vendor/hyperframes/dist/chunk-4YYJOFKS.js +28 -0
  56. package/vendor/hyperframes/dist/chunk-54HJ63Q2.js +29 -0
  57. package/vendor/hyperframes/dist/chunk-5ETJX6WM.js +89 -0
  58. package/vendor/hyperframes/dist/chunk-5IAKAQA7.js +389 -0
  59. package/vendor/hyperframes/dist/chunk-5Z63TBTX.js +37 -0
  60. package/vendor/hyperframes/dist/chunk-6BEFEWFB.js +25 -0
  61. package/vendor/hyperframes/dist/chunk-6CSYCFQV.js +68 -0
  62. package/vendor/hyperframes/dist/chunk-6CTIGJY7.js +154 -0
  63. package/vendor/hyperframes/dist/chunk-7HG4BFIN.js +93 -0
  64. package/vendor/hyperframes/dist/chunk-7JZ4FXP2.js +118 -0
  65. package/vendor/hyperframes/dist/chunk-A2Z4Z4VL.js +2961 -0
  66. package/vendor/hyperframes/dist/chunk-AMKC3AXA.js +25 -0
  67. package/vendor/hyperframes/dist/chunk-AVUUN2XD.js +4847 -0
  68. package/vendor/hyperframes/dist/chunk-AWDFDFI3.js +46 -0
  69. package/vendor/hyperframes/dist/chunk-AWNBM3MZ.js +94 -0
  70. package/vendor/hyperframes/dist/chunk-BXCMXMJQ.js +73 -0
  71. package/vendor/hyperframes/dist/chunk-BZ56MTDX.js +6 -0
  72. package/vendor/hyperframes/dist/chunk-CFKU7HTK.js +171 -0
  73. package/vendor/hyperframes/dist/chunk-CWPVM2QV.js +308 -0
  74. package/vendor/hyperframes/dist/chunk-DPVXBVLL.js +31292 -0
  75. package/vendor/hyperframes/dist/chunk-DQBY7FRR.js +397 -0
  76. package/vendor/hyperframes/dist/chunk-E6RPWJVC.js +213 -0
  77. package/vendor/hyperframes/dist/chunk-EISRN4E6.js +294 -0
  78. package/vendor/hyperframes/dist/chunk-EOVYQ3IG.js +143 -0
  79. package/vendor/hyperframes/dist/chunk-F3WZGU7C.js +479 -0
  80. package/vendor/hyperframes/dist/chunk-FCSIGXXP.js +366 -0
  81. package/vendor/hyperframes/dist/chunk-FI4I7CME.js +56 -0
  82. package/vendor/hyperframes/dist/chunk-FLWXXDNS.js +15 -0
  83. package/vendor/hyperframes/dist/chunk-GK4AIFIH.js +11171 -0
  84. package/vendor/hyperframes/dist/chunk-GMECXFAN.js +57 -0
  85. package/vendor/hyperframes/dist/chunk-GULOKZLI.js +155 -0
  86. package/vendor/hyperframes/dist/chunk-H4YZZ2VQ.js +68 -0
  87. package/vendor/hyperframes/dist/chunk-H6QMKUYA.js +477 -0
  88. package/vendor/hyperframes/dist/chunk-HBBJFK6I.js +273 -0
  89. package/vendor/hyperframes/dist/chunk-HRUGC23L.js +212 -0
  90. package/vendor/hyperframes/dist/chunk-HVW7ABXD.js +19 -0
  91. package/vendor/hyperframes/dist/chunk-HW6OYASV.js +344 -0
  92. package/vendor/hyperframes/dist/chunk-HWKCIWQT.js +3578 -0
  93. package/vendor/hyperframes/dist/chunk-I465DBM6.js +1214 -0
  94. package/vendor/hyperframes/dist/chunk-IGLEPPTN.js +24 -0
  95. package/vendor/hyperframes/dist/chunk-IGNAAHS3.js +433 -0
  96. package/vendor/hyperframes/dist/chunk-IHFUKJOR.js +85 -0
  97. package/vendor/hyperframes/dist/chunk-IQ3OUQ5Z.js +213 -0
  98. package/vendor/hyperframes/dist/chunk-ISN5GRYI.js +20 -0
  99. package/vendor/hyperframes/dist/chunk-IUZFXWSV.js +446 -0
  100. package/vendor/hyperframes/dist/chunk-JH556Q42.js +647 -0
  101. package/vendor/hyperframes/dist/chunk-JTHMTOMQ.js +601 -0
  102. package/vendor/hyperframes/dist/chunk-K2EQN4VU.js +405 -0
  103. package/vendor/hyperframes/dist/chunk-K2FTXJYH.js +16044 -0
  104. package/vendor/hyperframes/dist/chunk-K4WIOZFP.js +32 -0
  105. package/vendor/hyperframes/dist/chunk-K4WKGXRC.js +18 -0
  106. package/vendor/hyperframes/dist/chunk-KBJUXVOJ.js +172 -0
  107. package/vendor/hyperframes/dist/chunk-KC5XXX3X.js +590 -0
  108. package/vendor/hyperframes/dist/chunk-KNU5COPB.js +29 -0
  109. package/vendor/hyperframes/dist/chunk-KZI44OPA.js +185 -0
  110. package/vendor/hyperframes/dist/chunk-L3YOGIPG.js +48 -0
  111. package/vendor/hyperframes/dist/chunk-LAOCA3MQ.js +189 -0
  112. package/vendor/hyperframes/dist/chunk-LCSMOLGX.js +312 -0
  113. package/vendor/hyperframes/dist/chunk-LFGJWOVA.js +61 -0
  114. package/vendor/hyperframes/dist/chunk-LHSR3M4M.js +498 -0
  115. package/vendor/hyperframes/dist/chunk-LYNI3FOL.js +123 -0
  116. package/vendor/hyperframes/dist/chunk-M2WBOQ2Q.js +54 -0
  117. package/vendor/hyperframes/dist/chunk-MRF4AMKD.js +104 -0
  118. package/vendor/hyperframes/dist/chunk-MWB3ZIFM.js +852 -0
  119. package/vendor/hyperframes/dist/chunk-NGKM64BP.js +39 -0
  120. package/vendor/hyperframes/dist/chunk-NXMYHJAL.js +43 -0
  121. package/vendor/hyperframes/dist/chunk-OAF4XJOW.js +192 -0
  122. package/vendor/hyperframes/dist/chunk-OFQWTKPW.js +117 -0
  123. package/vendor/hyperframes/dist/chunk-P6LJ4Y32.js +4662 -0
  124. package/vendor/hyperframes/dist/chunk-PBN6GBJE.js +108 -0
  125. package/vendor/hyperframes/dist/chunk-PDAMK4Q5.js +28 -0
  126. package/vendor/hyperframes/dist/chunk-PFCFR3KZ.js +20 -0
  127. package/vendor/hyperframes/dist/chunk-PJ3HDUCY.js +108 -0
  128. package/vendor/hyperframes/dist/chunk-PMRBLBUP.js +152 -0
  129. package/vendor/hyperframes/dist/chunk-PPQUYWTW.js +240 -0
  130. package/vendor/hyperframes/dist/chunk-Q5X2B5WT.js +29 -0
  131. package/vendor/hyperframes/dist/chunk-Q7IU7MOR.js +351 -0
  132. package/vendor/hyperframes/dist/chunk-QC4FJVLM.js +371 -0
  133. package/vendor/hyperframes/dist/chunk-QNYYOUU3.js +1318 -0
  134. package/vendor/hyperframes/dist/chunk-RDNNAKEP.js +161 -0
  135. package/vendor/hyperframes/dist/chunk-RMHUQGRK.js +657 -0
  136. package/vendor/hyperframes/dist/chunk-RQVDTPGD.js +6 -0
  137. package/vendor/hyperframes/dist/chunk-RVCWIIZ7.js +68 -0
  138. package/vendor/hyperframes/dist/chunk-S3D6GE5W.js +72 -0
  139. package/vendor/hyperframes/dist/chunk-S3S4TGEH.js +424 -0
  140. package/vendor/hyperframes/dist/chunk-S7NPLFWV.js +11395 -0
  141. package/vendor/hyperframes/dist/chunk-SHB5VDMH.js +51 -0
  142. package/vendor/hyperframes/dist/chunk-TCLGPL65.js +860 -0
  143. package/vendor/hyperframes/dist/chunk-THXFSPLV.js +40 -0
  144. package/vendor/hyperframes/dist/chunk-TJSTGSAW.js +354 -0
  145. package/vendor/hyperframes/dist/chunk-TQFG7HTI.js +1024 -0
  146. package/vendor/hyperframes/dist/chunk-TVCRUG2S.js +265 -0
  147. package/vendor/hyperframes/dist/chunk-TWWSYAKJ.js +24 -0
  148. package/vendor/hyperframes/dist/chunk-U7CZBDMV.js +27 -0
  149. package/vendor/hyperframes/dist/chunk-UEZXMWG3.js +139 -0
  150. package/vendor/hyperframes/dist/chunk-UYEZTK4U.js +61 -0
  151. package/vendor/hyperframes/dist/chunk-VC76R6W2.js +27 -0
  152. package/vendor/hyperframes/dist/chunk-VFCKXTBB.js +159 -0
  153. package/vendor/hyperframes/dist/chunk-VWG7OQKU.js +22 -0
  154. package/vendor/hyperframes/dist/chunk-VXVRPIJA.js +105 -0
  155. package/vendor/hyperframes/dist/chunk-WBLXWOYK.js +15 -0
  156. package/vendor/hyperframes/dist/chunk-WERLJGIC.js +20 -0
  157. package/vendor/hyperframes/dist/chunk-WMQNIQDJ.js +2057 -0
  158. package/vendor/hyperframes/dist/chunk-WPHMNO44.js +4528 -0
  159. package/vendor/hyperframes/dist/chunk-X5HWXQPU.js +113 -0
  160. package/vendor/hyperframes/dist/chunk-X7MTZ4T2.js +6 -0
  161. package/vendor/hyperframes/dist/chunk-XH4DSOS5.js +59 -0
  162. package/vendor/hyperframes/dist/chunk-XS4EJSKU.js +23 -0
  163. package/vendor/hyperframes/dist/chunk-XT5P4QMO.js +181 -0
  164. package/vendor/hyperframes/dist/chunk-XT7X6QCP.js +6 -0
  165. package/vendor/hyperframes/dist/chunk-XUHOIDKG.js +6 -0
  166. package/vendor/hyperframes/dist/chunk-XUXGIA7H.js +485 -0
  167. package/vendor/hyperframes/dist/chunk-XZABUQTX.js +113 -0
  168. package/vendor/hyperframes/dist/chunk-Y6W4DVPS.js +356 -0
  169. package/vendor/hyperframes/dist/chunk-YEIFI443.js +44 -0
  170. package/vendor/hyperframes/dist/chunk-YQZNILVH.js +375 -0
  171. package/vendor/hyperframes/dist/chunk-Z27A7NUB.js +65 -0
  172. package/vendor/hyperframes/dist/chunk-Z6B37SNM.js +3081 -0
  173. package/vendor/hyperframes/dist/chunk-ZJUKFZ2P.js +10954 -0
  174. package/vendor/hyperframes/dist/chunk-ZKDMAOFU.js +32 -0
  175. package/vendor/hyperframes/dist/chunk-ZMXUQLI2.js +17219 -0
  176. package/vendor/hyperframes/dist/chunk-ZTZ7NCJT.js +58 -0
  177. package/vendor/hyperframes/dist/chunk-ZTZA4UWZ.js +108 -0
  178. package/vendor/hyperframes/dist/clean-W57E5YQ3.js +293 -0
  179. package/vendor/hyperframes/dist/cli.js +495 -0
  180. package/vendor/hyperframes/dist/cloud-FFTJGZ24.js +70 -0
  181. package/vendor/hyperframes/dist/cloudrun-AJUTHUEA.js +763 -0
  182. package/vendor/hyperframes/dist/colors-HKWSZCGK.js +15 -0
  183. package/vendor/hyperframes/dist/commands/contrast-audit.browser.js +459 -0
  184. package/vendor/hyperframes/dist/commands/layout-audit.browser.js +2102 -0
  185. package/vendor/hyperframes/dist/commands/motion-sample.browser.js +74 -0
  186. package/vendor/hyperframes/dist/commands/motion-signature.browser.js +374 -0
  187. package/vendor/hyperframes/dist/compare-AYCTT55F.js +421 -0
  188. package/vendor/hyperframes/dist/compiler-QO36EXLS.js +121 -0
  189. package/vendor/hyperframes/dist/component-VFFR2O7T.js +176 -0
  190. package/vendor/hyperframes/dist/compositions-QTVN3U2A.js +228 -0
  191. package/vendor/hyperframes/dist/contactSheet-27SRU7A4.js +22 -0
  192. package/vendor/hyperframes/dist/delete-ZAKYCB7I.js +114 -0
  193. package/vendor/hyperframes/dist/deploy-MX76RZ36.js +101 -0
  194. package/vendor/hyperframes/dist/destroy-H2TZBYHH.js +42 -0
  195. package/vendor/hyperframes/dist/deterministicFonts-HAEMV4TM.js +50 -0
  196. package/vendor/hyperframes/dist/dist-EWXJGN63.js +129 -0
  197. package/vendor/hyperframes/dist/dist-FNNGCKFS.js +29 -0
  198. package/vendor/hyperframes/dist/dist-S3PJNPXQ.js +392 -0
  199. package/vendor/hyperframes/dist/docker/Dockerfile.render +65 -0
  200. package/vendor/hyperframes/dist/docs/compositions.md +67 -0
  201. package/vendor/hyperframes/dist/docs/data-attributes.md +27 -0
  202. package/vendor/hyperframes/dist/docs/examples.md +19 -0
  203. package/vendor/hyperframes/dist/docs/gsap.md +26 -0
  204. package/vendor/hyperframes/dist/docs/rendering.md +35 -0
  205. package/vendor/hyperframes/dist/docs/troubleshooting.md +29 -0
  206. package/vendor/hyperframes/dist/docs-QAPJEGLS.js +131 -0
  207. package/vendor/hyperframes/dist/doctor-AI7C3FON.js +67 -0
  208. package/vendor/hyperframes/dist/dom-IROKGVJI.js +14 -0
  209. package/vendor/hyperframes/dist/dotEnv-AECLXQSO.js +14 -0
  210. package/vendor/hyperframes/dist/events-7LALYNKZ.js +92 -0
  211. package/vendor/hyperframes/dist/events-FQY3HGKZ.js +88 -0
  212. package/vendor/hyperframes/dist/feedback-WCMDMSTK.js +472 -0
  213. package/vendor/hyperframes/dist/ffmpeg-QIUFEFTY.js +35 -0
  214. package/vendor/hyperframes/dist/figma-64WY462P.js +63 -0
  215. package/vendor/hyperframes/dist/fontData.generated-NXAOI4GH.js +13 -0
  216. package/vendor/hyperframes/dist/fontLocalizeCli.js +132 -0
  217. package/vendor/hyperframes/dist/get-FPOMJMZB.js +103 -0
  218. package/vendor/hyperframes/dist/gpuPolicy-2ZUZE5D6.js +38 -0
  219. package/vendor/hyperframes/dist/grade-compare-E2CKPST4.js +647 -0
  220. package/vendor/hyperframes/dist/gsapParser-DIYYX6JT.js +2846 -0
  221. package/vendor/hyperframes/dist/gsapParserAcorn-E3GEIRZB.js +28 -0
  222. package/vendor/hyperframes/dist/help-YRQIDDML.js +197 -0
  223. package/vendor/hyperframes/dist/history-PP5NDRV3.js +571 -0
  224. package/vendor/hyperframes/dist/hyperframe-runtime.js +1081 -0
  225. package/vendor/hyperframes/dist/hyperframe.manifest.json +22 -0
  226. package/vendor/hyperframes/dist/hyperframe.runtime.iife.js +1081 -0
  227. package/vendor/hyperframes/dist/hyperframes-player.global.js +474 -0
  228. package/vendor/hyperframes/dist/hyperframes-slideshow.global.js +149 -0
  229. package/vendor/hyperframes/dist/info-SEX3B7JL.js +153 -0
  230. package/vendor/hyperframes/dist/init-U3AKHO5B.js +1147 -0
  231. package/vendor/hyperframes/dist/inspect-Q3K6EDEK.js +97 -0
  232. package/vendor/hyperframes/dist/installer-K6O5WF44.js +37 -0
  233. package/vendor/hyperframes/dist/keyframes-DC64JCI3.js +2602 -0
  234. package/vendor/hyperframes/dist/lambda-D7UMN4DJ.js +451 -0
  235. package/vendor/hyperframes/dist/layout-XSPPDHOO.js +86 -0
  236. package/vendor/hyperframes/dist/lib-TPG2ZMAX.js +128 -0
  237. package/vendor/hyperframes/dist/lint-7DQZE7L3.js +164 -0
  238. package/vendor/hyperframes/dist/lintProject-OHBBM3DJ.js +27 -0
  239. package/vendor/hyperframes/dist/list-4K5MVDQX.js +162 -0
  240. package/vendor/hyperframes/dist/login-NTX4OWXL.js +377 -0
  241. package/vendor/hyperframes/dist/logout-Q654A3SI.js +100 -0
  242. package/vendor/hyperframes/dist/manager-67P2XFXQ.js +34 -0
  243. package/vendor/hyperframes/dist/manager-CT3D67CG.js +38 -0
  244. package/vendor/hyperframes/dist/manager-GFN75DM4.js +30 -0
  245. package/vendor/hyperframes/dist/media-treatment-5FYAOPNL.js +972 -0
  246. package/vendor/hyperframes/dist/media-use-4XXGABIL.js +113 -0
  247. package/vendor/hyperframes/dist/models-C4E4IWJD.js +137 -0
  248. package/vendor/hyperframes/dist/motionShot-S4MIGMT5.js +614 -0
  249. package/vendor/hyperframes/dist/normalize-O76AC4HE.js +333 -0
  250. package/vendor/hyperframes/dist/normalize-audio-CSFTXLXD.js +276 -0
  251. package/vendor/hyperframes/dist/open-2ZD6BHZG.js +115 -0
  252. package/vendor/hyperframes/dist/pipeline-TNGWUZQC.js +54 -0
  253. package/vendor/hyperframes/dist/play-CTMUXX6E.js +349 -0
  254. package/vendor/hyperframes/dist/policies-FSDZVS2E.js +292 -0
  255. package/vendor/hyperframes/dist/preflight-SMNV6RW2.js +45 -0
  256. package/vendor/hyperframes/dist/present-WQVHV5CR.js +350 -0
  257. package/vendor/hyperframes/dist/preview-JOJXTH75.js +1820 -0
  258. package/vendor/hyperframes/dist/producer-ECLEN5RP.js +13 -0
  259. package/vendor/hyperframes/dist/progress-FBIEEB7Y.js +73 -0
  260. package/vendor/hyperframes/dist/projectConfig-HYJMJ5BY.js +56 -0
  261. package/vendor/hyperframes/dist/publication-HQ5Y3KBM.js +29 -0
  262. package/vendor/hyperframes/dist/publish-VHDYQASC.js +430 -0
  263. package/vendor/hyperframes/dist/refresh-K24ETHMA.js +53 -0
  264. package/vendor/hyperframes/dist/remove-background-W2YX2NVH.js +253 -0
  265. package/vendor/hyperframes/dist/render-AO5VX3YI.js +177 -0
  266. package/vendor/hyperframes/dist/render-LNO62LEL.js +2367 -0
  267. package/vendor/hyperframes/dist/render-TTQI7TGN.js +796 -0
  268. package/vendor/hyperframes/dist/render-batch-YH7CTWNY.js +284 -0
  269. package/vendor/hyperframes/dist/renderCancellation-JPIHE47X.js +35 -0
  270. package/vendor/hyperframes/dist/renderSetupWorker.js +79 -0
  271. package/vendor/hyperframes/dist/resolver-IZ647GBZ.js +38 -0
  272. package/vendor/hyperframes/dist/runtimeVersion.js +18 -0
  273. package/vendor/hyperframes/dist/screenshotCapture-ECBTLCU6.js +179 -0
  274. package/vendor/hyperframes/dist/shaderTransitionWorker.js +44 -0
  275. package/vendor/hyperframes/dist/sherpa-KKLNW6U2.js +314 -0
  276. package/vendor/hyperframes/dist/sherpaWorker.js +77 -0
  277. package/vendor/hyperframes/dist/sites-TNVKZS73.js +52 -0
  278. package/vendor/hyperframes/dist/skill-ZHVLH7KX.js +15 -0
  279. package/vendor/hyperframes/dist/skills/hyperframes/SKILL.md +152 -0
  280. package/vendor/hyperframes/dist/skills/hyperframes/references/brief-contract.md +108 -0
  281. package/vendor/hyperframes/dist/skills/hyperframes/references/brief-format.md +72 -0
  282. package/vendor/hyperframes/dist/skills/hyperframes/references/capability-menu.md +47 -0
  283. package/vendor/hyperframes/dist/skills/hyperframes/references/frame-worker-core.md +80 -0
  284. package/vendor/hyperframes/dist/skills/hyperframes/references/intent-interview.md +73 -0
  285. package/vendor/hyperframes/dist/skills/hyperframes/references/pitch-round.md +40 -0
  286. package/vendor/hyperframes/dist/skills/hyperframes/references/plugin-installation.md +43 -0
  287. package/vendor/hyperframes/dist/skills/hyperframes/references/production-loop.md +26 -0
  288. package/vendor/hyperframes/dist/skills/hyperframes/references/review-loop.md +37 -0
  289. package/vendor/hyperframes/dist/skills/hyperframes/references/route-briefs.md +11 -0
  290. package/vendor/hyperframes/dist/skills/hyperframes/references/routes/embedded-captions.md +11 -0
  291. package/vendor/hyperframes/dist/skills/hyperframes/references/routes/faceless-explainer.md +12 -0
  292. package/vendor/hyperframes/dist/skills/hyperframes/references/routes/general-video.md +12 -0
  293. package/vendor/hyperframes/dist/skills/hyperframes/references/routes/motion-graphics.md +11 -0
  294. package/vendor/hyperframes/dist/skills/hyperframes/references/routes/music-to-video.md +12 -0
  295. package/vendor/hyperframes/dist/skills/hyperframes/references/routes/pr-to-video.md +28 -0
  296. package/vendor/hyperframes/dist/skills/hyperframes/references/routes/product-launch-video.md +13 -0
  297. package/vendor/hyperframes/dist/skills/hyperframes/references/routes/remotion-to-hyperframes.md +9 -0
  298. package/vendor/hyperframes/dist/skills/hyperframes/references/routes/slideshow.md +11 -0
  299. package/vendor/hyperframes/dist/skills/hyperframes/references/routes/talking-head-recut.md +11 -0
  300. package/vendor/hyperframes/dist/skills/hyperframes/references/script-format.md +52 -0
  301. package/vendor/hyperframes/dist/skills/hyperframes/references/skill-lifecycle.md +35 -0
  302. package/vendor/hyperframes/dist/skills/hyperframes/references/storyboard-format.md +99 -0
  303. package/vendor/hyperframes/dist/skills/hyperframes/references/subagent-dispatch.md +35 -0
  304. package/vendor/hyperframes/dist/skills/hyperframes/references/workflow-catalog.md +10 -0
  305. package/vendor/hyperframes/dist/skills/hyperframes/scripts/lib/frame-packets-core.mjs +194 -0
  306. package/vendor/hyperframes/dist/skills/hyperframes/scripts/plugin-cli.mjs +88 -0
  307. package/vendor/hyperframes/dist/skills/hyperframes/scripts/plugin-cli.test.mjs +117 -0
  308. package/vendor/hyperframes/dist/skills/hyperframes-cli/SKILL.md +189 -0
  309. package/vendor/hyperframes/dist/skills/hyperframes-cli/references/beats.md +19 -0
  310. package/vendor/hyperframes/dist/skills/hyperframes-cli/references/cloud.md +163 -0
  311. package/vendor/hyperframes/dist/skills/hyperframes-cli/references/cloudrun.md +62 -0
  312. package/vendor/hyperframes/dist/skills/hyperframes-cli/references/compare-and-batch.md +105 -0
  313. package/vendor/hyperframes/dist/skills/hyperframes-cli/references/doctor-browser.md +57 -0
  314. package/vendor/hyperframes/dist/skills/hyperframes-cli/references/init-and-scaffold.md +74 -0
  315. package/vendor/hyperframes/dist/skills/hyperframes-cli/references/lambda.md +185 -0
  316. package/vendor/hyperframes/dist/skills/hyperframes-cli/references/lint-validate-inspect.md +132 -0
  317. package/vendor/hyperframes/dist/skills/hyperframes-cli/references/preview-render.md +193 -0
  318. package/vendor/hyperframes/dist/skills/hyperframes-cli/references/upgrade-info-misc.md +134 -0
  319. package/vendor/hyperframes/dist/skills/media-use/SKILL.md +100 -0
  320. package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/CREDITS.md +35 -0
  321. package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/chime.mp3 +0 -0
  322. package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/click-soft.mp3 +0 -0
  323. package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/click.mp3 +0 -0
  324. package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/error.mp3 +0 -0
  325. package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/glitch-1.mp3 +0 -0
  326. package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/glitch-2.mp3 +0 -0
  327. package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/glitch-3.mp3 +0 -0
  328. package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/impact-bass-1.mp3 +0 -0
  329. package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/impact-bass-2.mp3 +0 -0
  330. package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/key-press.mp3 +0 -0
  331. package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/manifest.json +97 -0
  332. package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/notification.mp3 +0 -0
  333. package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/ping.mp3 +0 -0
  334. package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/pop.mp3 +0 -0
  335. package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/riser.mp3 +0 -0
  336. package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/sparkle.mp3 +0 -0
  337. package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/typing.mp3 +0 -0
  338. package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/whoosh-cinematic.mp3 +0 -0
  339. package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/whoosh-short.mp3 +0 -0
  340. package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/whoosh.mp3 +0 -0
  341. package/vendor/hyperframes/dist/skills/media-use/audio/references/bgm.md +76 -0
  342. package/vendor/hyperframes/dist/skills/media-use/audio/references/captions/authoring.md +163 -0
  343. package/vendor/hyperframes/dist/skills/media-use/audio/references/captions/motion.md +87 -0
  344. package/vendor/hyperframes/dist/skills/media-use/audio/references/captions/transcript-handling.md +97 -0
  345. package/vendor/hyperframes/dist/skills/media-use/audio/references/remove-background.md +143 -0
  346. package/vendor/hyperframes/dist/skills/media-use/audio/references/requirements.md +29 -0
  347. package/vendor/hyperframes/dist/skills/media-use/audio/references/sfx.md +42 -0
  348. package/vendor/hyperframes/dist/skills/media-use/audio/references/transcribe.md +52 -0
  349. package/vendor/hyperframes/dist/skills/media-use/audio/references/tts-to-captions.md +35 -0
  350. package/vendor/hyperframes/dist/skills/media-use/audio/references/tts.md +260 -0
  351. package/vendor/hyperframes/dist/skills/media-use/audio/scripts/audio.mjs +311 -0
  352. package/vendor/hyperframes/dist/skills/media-use/audio/scripts/audio.test.mjs +159 -0
  353. package/vendor/hyperframes/dist/skills/media-use/audio/scripts/gemini-pipeline.test.mjs +131 -0
  354. package/vendor/hyperframes/dist/skills/media-use/audio/scripts/heygen-tts.mjs +122 -0
  355. package/vendor/hyperframes/dist/skills/media-use/audio/scripts/heygen-tts.test.mjs +38 -0
  356. package/vendor/hyperframes/dist/skills/media-use/audio/scripts/heygen-voice.mjs +98 -0
  357. package/vendor/hyperframes/dist/skills/media-use/audio/scripts/heygen-voice.test.mjs +233 -0
  358. package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/audio-meta.mjs +39 -0
  359. package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/audio-meta.test.mjs +136 -0
  360. package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/bgm-volume.mjs +4 -0
  361. package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/bgm.mjs +257 -0
  362. package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/bgm.test.mjs +74 -0
  363. package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/concurrency.mjs +14 -0
  364. package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/concurrency.test.mjs +41 -0
  365. package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/gemini-auth.mjs +43 -0
  366. package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/gemini-auth.py +52 -0
  367. package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/gemini-auth.test.mjs +86 -0
  368. package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/gemini-auth_test.py +90 -0
  369. package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/gemini-tts.mjs +122 -0
  370. package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/gemini-tts.test.mjs +243 -0
  371. package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/heygen.mjs +197 -0
  372. package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/heygen.test.mjs +185 -0
  373. package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/media-record.mjs +114 -0
  374. package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/media-record.test.mjs +205 -0
  375. package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/python.mjs +77 -0
  376. package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/python.test.mjs +136 -0
  377. package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/sfx.mjs +165 -0
  378. package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/sfx.test.mjs +121 -0
  379. package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/tts.mjs +389 -0
  380. package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/tts.spawn.test.mjs +158 -0
  381. package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/tts.test.mjs +230 -0
  382. package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lyria-recipe.py +136 -0
  383. package/vendor/hyperframes/dist/skills/media-use/audio/scripts/wait-bgm.mjs +175 -0
  384. package/vendor/hyperframes/dist/skills/media-use/audio/scripts/wait-bgm.test.mjs +108 -0
  385. package/vendor/hyperframes/dist/skills/media-use/luts/README.md +28 -0
  386. package/vendor/hyperframes/dist/skills/media-use/luts/index.json +57 -0
  387. package/vendor/hyperframes/dist/skills/media-use/references/audio.md +20 -0
  388. package/vendor/hyperframes/dist/skills/media-use/references/grading.md +160 -0
  389. package/vendor/hyperframes/dist/skills/media-use/references/media-treatment-recipes.md +929 -0
  390. package/vendor/hyperframes/dist/skills/media-use/references/media-treatments.md +202 -0
  391. package/vendor/hyperframes/dist/skills/media-use/references/memory.md +37 -0
  392. package/vendor/hyperframes/dist/skills/media-use/references/meta.md +61 -0
  393. package/vendor/hyperframes/dist/skills/media-use/references/operations.md +327 -0
  394. package/vendor/hyperframes/dist/skills/media-use/references/resolve.md +150 -0
  395. package/vendor/hyperframes/dist/skills/media-use/references/setup-providers.md +80 -0
  396. package/vendor/hyperframes/dist/skills/media-use/references/telemetry-dashboard.md +49 -0
  397. package/vendor/hyperframes/dist/skills/media-use/scripts/audio-duck.mjs +113 -0
  398. package/vendor/hyperframes/dist/skills/media-use/scripts/compatibility.test.mjs +96 -0
  399. package/vendor/hyperframes/dist/skills/media-use/scripts/dither.mjs +328 -0
  400. package/vendor/hyperframes/dist/skills/media-use/scripts/dither.test.mjs +176 -0
  401. package/vendor/hyperframes/dist/skills/media-use/scripts/eval.mjs +369 -0
  402. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/adopt.mjs +128 -0
  403. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/adopt.test.mjs +95 -0
  404. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/bgm-provider.mjs +20 -0
  405. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/brand-provider.mjs +59 -0
  406. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/bundled-sfx-provider.mjs +119 -0
  407. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/bundled-sfx-provider.test.mjs +139 -0
  408. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/cache.mjs +148 -0
  409. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/candidates.mjs +91 -0
  410. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/candidates.test.mjs +155 -0
  411. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/codex-provider.mjs +161 -0
  412. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/codex-provider.test.mjs +34 -0
  413. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/config-lock.d.mts +3 -0
  414. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/config-lock.mjs +45 -0
  415. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/config-lock.test.mjs +79 -0
  416. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/coverage.test.mjs +136 -0
  417. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/cube-build.mjs +181 -0
  418. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/cube-build.test.mjs +80 -0
  419. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/cube-validate.mjs +198 -0
  420. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/cube-validate.test.mjs +125 -0
  421. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/cutlist.mjs +184 -0
  422. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/cutlist.test.mjs +162 -0
  423. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/duck.mjs +95 -0
  424. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/duck.test.mjs +153 -0
  425. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/error-diffusion.mjs +230 -0
  426. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/error-diffusion.test.mjs +102 -0
  427. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/ff-binaries.mjs +37 -0
  428. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/freeze.mjs +72 -0
  429. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/freeze.test.mjs +138 -0
  430. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/grade-analyzer.mjs +265 -0
  431. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/grade-analyzer.test.mjs +285 -0
  432. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/heygen-cli.mjs +191 -0
  433. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/heygen-cli.test.mjs +301 -0
  434. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/heygen-search.mjs +45 -0
  435. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/heygen-search.test.mjs +49 -0
  436. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/heygen-video-provider.mjs +138 -0
  437. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/heygen-video-provider.test.mjs +435 -0
  438. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/image-provider.mjs +44 -0
  439. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/index-gen.mjs +63 -0
  440. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/local-media-search.mjs +32 -0
  441. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/local-models.mjs +318 -0
  442. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/local-models.test.mjs +220 -0
  443. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/local-run.mjs +78 -0
  444. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/local-run.test.mjs +121 -0
  445. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/logo-provider.mjs +244 -0
  446. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/logo-provider.test.mjs +298 -0
  447. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/ltx-video-provider.mjs +94 -0
  448. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/ltx-video-provider.test.mjs +316 -0
  449. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/lut-preset-provider.mjs +271 -0
  450. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/lut-preset-provider.test.mjs +199 -0
  451. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/manifest.d.mts +19 -0
  452. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/manifest.mjs +279 -0
  453. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/manifest.test.mjs +397 -0
  454. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/match.mjs +47 -0
  455. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/media-fetch.mjs +85 -0
  456. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/media-fetch.test.mjs +197 -0
  457. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/media-home.mjs +20 -0
  458. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/media-home.test.mjs +61 -0
  459. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/media-search.d.mts +9 -0
  460. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/media-search.mjs +107 -0
  461. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/mflux-provider.mjs +128 -0
  462. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/mflux-provider.test.mjs +174 -0
  463. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/misses.mjs +49 -0
  464. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/misses.test.mjs +89 -0
  465. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/npx-sync.mjs +31 -0
  466. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/npx-sync.test.mjs +50 -0
  467. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/parakeet-words.mjs +27 -0
  468. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/parakeet-words.test.mjs +44 -0
  469. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/prefs-store.mjs +180 -0
  470. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/prefs-store.test.mjs +189 -0
  471. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/probe.mjs +41 -0
  472. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/probe.test.mjs +148 -0
  473. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/providers.mjs +5 -0
  474. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/recipe-store.mjs +367 -0
  475. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/recipe-store.test.mjs +367 -0
  476. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/registry.mjs +212 -0
  477. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/registry.test.mjs +262 -0
  478. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/search.mjs +24 -0
  479. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/search.test.mjs +32 -0
  480. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/sfx-provider.mjs +23 -0
  481. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/specs.mjs +132 -0
  482. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/specs.test.mjs +157 -0
  483. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/stats.mjs +117 -0
  484. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/stats.test.mjs +165 -0
  485. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/svg-sanitize.mjs +21 -0
  486. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/telemetry.mjs +210 -0
  487. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/telemetry.test.mjs +437 -0
  488. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/test-home.test.mjs +10 -0
  489. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/transcriptCutFade.mjs +21 -0
  490. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/transcriptCutFade.test.mjs +33 -0
  491. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/tts-local-provider.mjs +94 -0
  492. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/tts-local-provider.test.mjs +72 -0
  493. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/usage.mjs +57 -0
  494. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/usage.test.mjs +54 -0
  495. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/voice-provider.mjs +57 -0
  496. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/voice-provider.test.mjs +46 -0
  497. package/vendor/hyperframes/dist/skills/media-use/scripts/lib/words.mjs +18 -0
  498. package/vendor/hyperframes/dist/skills/media-use/scripts/prefs.mjs +70 -0
  499. package/vendor/hyperframes/dist/skills/media-use/scripts/recipe.mjs +103 -0
  500. package/vendor/hyperframes/dist/skills/media-use/scripts/resolve-plugin.test.mjs +62 -0
  501. package/vendor/hyperframes/dist/skills/media-use/scripts/resolve.mjs +1369 -0
  502. package/vendor/hyperframes/dist/skills/media-use/scripts/transcribe.mjs +194 -0
  503. package/vendor/hyperframes/dist/skills/media-use/scripts/transcribe.test.mjs +112 -0
  504. package/vendor/hyperframes/dist/skills/media-use/scripts/transcript-cut.mjs +270 -0
  505. package/vendor/hyperframes/dist/skills/media-use/scripts/transcript-cut.test.mjs +117 -0
  506. package/vendor/hyperframes/dist/skills-MJ2J4GEM.js +808 -0
  507. package/vendor/hyperframes/dist/skillsUpdateCheck-TKTYQN5R.js +26 -0
  508. package/vendor/hyperframes/dist/slideshow-EJ4IMY6P.js +24 -0
  509. package/vendor/hyperframes/dist/snapshot-KIVDUUMN.js +875 -0
  510. package/vendor/hyperframes/dist/src-6YQBJGWR.js +202 -0
  511. package/vendor/hyperframes/dist/src-PVYUYV3M.js +471 -0
  512. package/vendor/hyperframes/dist/state-KAXGHF77.js +26 -0
  513. package/vendor/hyperframes/dist/staticProjectServer-7EU666GS.js +49 -0
  514. package/vendor/hyperframes/dist/status-YM52BQ6S.js +311 -0
  515. package/vendor/hyperframes/dist/studio/assets/hyperframes-player-Cp8e_Zst.js +473 -0
  516. package/vendor/hyperframes/dist/studio/assets/index-BX3KHhGX.js +71 -0
  517. package/vendor/hyperframes/dist/studio/assets/index-CASEBNy2.js +1 -0
  518. package/vendor/hyperframes/dist/studio/assets/index-D11atkBt.css +1 -0
  519. package/vendor/hyperframes/dist/studio/assets/index-DMTt9nTf.js +531 -0
  520. package/vendor/hyperframes/dist/studio/assets/index-oE1pOdAy.js +399 -0
  521. package/vendor/hyperframes/dist/studio/chunk-5VSXRGIX.js +948 -0
  522. package/vendor/hyperframes/dist/studio/chunk-5VSXRGIX.js.map +1 -0
  523. package/vendor/hyperframes/dist/studio/chunk-7UQWVN52.js +938 -0
  524. package/vendor/hyperframes/dist/studio/chunk-7UQWVN52.js.map +1 -0
  525. package/vendor/hyperframes/dist/studio/chunk-EEXSYY5F.js +190 -0
  526. package/vendor/hyperframes/dist/studio/chunk-EEXSYY5F.js.map +1 -0
  527. package/vendor/hyperframes/dist/studio/chunk-FOEID72M.js +1441 -0
  528. package/vendor/hyperframes/dist/studio/chunk-FOEID72M.js.map +1 -0
  529. package/vendor/hyperframes/dist/studio/chunk-PZ5AY32C.js +10 -0
  530. package/vendor/hyperframes/dist/studio/chunk-PZ5AY32C.js.map +1 -0
  531. package/vendor/hyperframes/dist/studio/chunk-TDWGXBUT.js +1103 -0
  532. package/vendor/hyperframes/dist/studio/chunk-TDWGXBUT.js.map +1 -0
  533. package/vendor/hyperframes/dist/studio/domEditingLayers-IEVLO54Z.js +47 -0
  534. package/vendor/hyperframes/dist/studio/domEditingLayers-IEVLO54Z.js.map +1 -0
  535. package/vendor/hyperframes/dist/studio/favicon.svg +14 -0
  536. package/vendor/hyperframes/dist/studio/icons/timeline/audio.svg +7 -0
  537. package/vendor/hyperframes/dist/studio/icons/timeline/captions.svg +5 -0
  538. package/vendor/hyperframes/dist/studio/icons/timeline/composition.svg +12 -0
  539. package/vendor/hyperframes/dist/studio/icons/timeline/image.svg +18 -0
  540. package/vendor/hyperframes/dist/studio/icons/timeline/music.svg +10 -0
  541. package/vendor/hyperframes/dist/studio/icons/timeline/text.svg +3 -0
  542. package/vendor/hyperframes/dist/studio/index.css +165 -0
  543. package/vendor/hyperframes/dist/studio/index.css.map +1 -0
  544. package/vendor/hyperframes/dist/studio/index.d.ts +4296 -0
  545. package/vendor/hyperframes/dist/studio/index.html +23 -0
  546. package/vendor/hyperframes/dist/studio/index.js +99693 -0
  547. package/vendor/hyperframes/dist/studio/index.js.map +1 -0
  548. package/vendor/hyperframes/dist/studio/player.d.ts +30 -0
  549. package/vendor/hyperframes/dist/studio/player.js +10 -0
  550. package/vendor/hyperframes/dist/studio/player.js.map +1 -0
  551. package/vendor/hyperframes/dist/studio/styles/tailwind-preset.d.ts +39 -0
  552. package/vendor/hyperframes/dist/studio/styles/tailwind-preset.js +50 -0
  553. package/vendor/hyperframes/dist/studio/styles/tailwind-preset.js.map +1 -0
  554. package/vendor/hyperframes/dist/studio/styles.css +1 -0
  555. package/vendor/hyperframes/dist/studio/ui.d.ts +296 -0
  556. package/vendor/hyperframes/dist/studio/ui.js +66 -0
  557. package/vendor/hyperframes/dist/studio/ui.js.map +1 -0
  558. package/vendor/hyperframes/dist/studio-api-PNQZRPX5.js +136 -0
  559. package/vendor/hyperframes/dist/studioSelectionClient-IUUPKUHZ.js +25 -0
  560. package/vendor/hyperframes/dist/studioServer-UWRBPIOJ.js +1457 -0
  561. package/vendor/hyperframes/dist/synthesize-K62TIZWI.js +182 -0
  562. package/vendor/hyperframes/dist/system-EZYZNUFD.js +44 -0
  563. package/vendor/hyperframes/dist/telemetry-2YC4SGQU.js +144 -0
  564. package/vendor/hyperframes/dist/telemetry-QMYM4WME.js +95 -0
  565. package/vendor/hyperframes/dist/templates/_shared/AGENTS.md +107 -0
  566. package/vendor/hyperframes/dist/templates/_shared/CLAUDE.md +107 -0
  567. package/vendor/hyperframes/dist/templates/blank/index.html +55 -0
  568. package/vendor/hyperframes/dist/templates/from-file/index.html +75 -0
  569. package/vendor/hyperframes/dist/templates/warm-grain/compositions/captions.html +154 -0
  570. package/vendor/hyperframes/dist/templates/warm-grain/compositions/graphics.html +162 -0
  571. package/vendor/hyperframes/dist/templates/warm-grain/compositions/intro.html +87 -0
  572. package/vendor/hyperframes/dist/templates/warm-grain/index.html +264 -0
  573. package/vendor/hyperframes/dist/templates/warm-grain/registry-item.json +34 -0
  574. package/vendor/hyperframes/dist/timeline-77Z3BBQM.js +1377 -0
  575. package/vendor/hyperframes/dist/tokens-OBGTYE4Z.js +92 -0
  576. package/vendor/hyperframes/dist/transcribe-7NVFHCGA.js +387 -0
  577. package/vendor/hyperframes/dist/transcribe-BNSJRR4M.js +62 -0
  578. package/vendor/hyperframes/dist/tts-P3LIPIL5.js +234 -0
  579. package/vendor/hyperframes/dist/updateCheck-UDWIJ4ZQ.js +39 -0
  580. package/vendor/hyperframes/dist/upgrade-CT3CPO3E.js +231 -0
  581. package/vendor/hyperframes/dist/urlDownloader-H6WRI2UE.js +39 -0
  582. package/vendor/hyperframes/dist/usage-EC3KFUIJ.js +2139 -0
  583. package/vendor/hyperframes/dist/validate-65QDZJWZ.js +70 -0
  584. package/vendor/hyperframes/dist/video-FGSGKRSV.js +261 -0
  585. package/vendor/hyperframes/dist/workspaceBuildCheck-TOLGAI4T.js +133 -0
  586. package/vendor/hyperframes/package.json +80 -0
@@ -0,0 +1,2102 @@
1
+ (function () {
2
+ const IGNORE_TAGS = new Set(["SCRIPT", "STYLE", "TEMPLATE", "NOSCRIPT", "META", "LINK"]);
3
+
4
+ function toRect(rect) {
5
+ return {
6
+ left: round(rect.left),
7
+ top: round(rect.top),
8
+ right: round(rect.right),
9
+ bottom: round(rect.bottom),
10
+ width: round(rect.width),
11
+ height: round(rect.height),
12
+ };
13
+ }
14
+
15
+ function rectFromOrigin(left, top, width, height) {
16
+ return {
17
+ left: round(left),
18
+ top: round(top),
19
+ right: round(left + width),
20
+ bottom: round(top + height),
21
+ width: round(width),
22
+ height: round(height),
23
+ };
24
+ }
25
+
26
+ function round(value) {
27
+ return Math.round(value * 100) / 100;
28
+ }
29
+
30
+ function horizontalOverflow(subject, container, tolerance) {
31
+ if (subject.width <= container.width + tolerance) return null;
32
+ const overflow = overflowFor(subject, container, tolerance);
33
+ if (!overflow) return null;
34
+ const horizontal = {};
35
+ if (overflow.left != null) horizontal.left = overflow.left;
36
+ if (overflow.right != null) horizontal.right = overflow.right;
37
+ return Object.keys(horizontal).length > 0 ? horizontal : null;
38
+ }
39
+
40
+ function overflowFor(subject, container, tolerance, vTolerance) {
41
+ // Horizontal axis uses `tolerance`; vertical axis uses `vTolerance` (defaults to the same).
42
+ // A separate vertical tolerance lets text overflow checks absorb glyph ink that exceeds a
43
+ // snug line-height — see textOverflowIssues.
44
+ if (vTolerance == null) vTolerance = tolerance;
45
+ const overflow = {};
46
+ if (subject.left < container.left - tolerance)
47
+ overflow.left = round(container.left - subject.left);
48
+ if (subject.right > container.right + tolerance)
49
+ overflow.right = round(subject.right - container.right);
50
+ if (subject.top < container.top - vTolerance) overflow.top = round(container.top - subject.top);
51
+ if (subject.bottom > container.bottom + vTolerance)
52
+ overflow.bottom = round(subject.bottom - container.bottom);
53
+ return Object.keys(overflow).length > 0 ? overflow : null;
54
+ }
55
+
56
+ function escapeCss(value) {
57
+ if (window.CSS && typeof window.CSS.escape === "function") return window.CSS.escape(value);
58
+ return value.replace(/[^a-zA-Z0-9_-]/g, "\\$&");
59
+ }
60
+
61
+ function escapeAttr(value) {
62
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
63
+ }
64
+
65
+ function selectorFor(element) {
66
+ if (element.id) return `#${escapeCss(element.id)}`;
67
+ const dataName =
68
+ element.getAttribute("data-layout-name") ||
69
+ element.getAttribute("data-composition-id") ||
70
+ element.getAttribute("data-start");
71
+ if (dataName) {
72
+ const attr = element.hasAttribute("data-layout-name")
73
+ ? "data-layout-name"
74
+ : element.hasAttribute("data-composition-id")
75
+ ? "data-composition-id"
76
+ : "data-start";
77
+ const attrSelector = `[${attr}="${escapeAttr(dataName)}"]`;
78
+ if (document.querySelectorAll(attrSelector).length === 1) return attrSelector;
79
+ return `${element.tagName.toLowerCase()}${attrSelector}`;
80
+ }
81
+ const classes = Array.from(element.classList).slice(0, 2);
82
+ if (classes.length > 0) {
83
+ return `${element.tagName.toLowerCase()}.${classes.map(escapeCss).join(".")}`;
84
+ }
85
+ const parent = element.parentElement;
86
+ if (!parent) return element.tagName.toLowerCase();
87
+ const siblings = Array.from(parent.children).filter(
88
+ (child) => child.tagName === element.tagName,
89
+ );
90
+ const index = siblings.indexOf(element) + 1;
91
+ return `${selectorFor(parent)} > ${element.tagName.toLowerCase()}:nth-of-type(${index})`;
92
+ }
93
+
94
+ function uniqueSelectorFor(element) {
95
+ const preferred = selectorFor(element);
96
+ try {
97
+ if (document.querySelectorAll(preferred).length === 1) return preferred;
98
+ } catch {
99
+ // Fall through to a structural selector.
100
+ }
101
+ const parent = element.parentElement;
102
+ if (!parent) return preferred;
103
+ const siblings = Array.from(parent.children).filter(
104
+ (child) => child.tagName === element.tagName,
105
+ );
106
+ const index = siblings.indexOf(element) + 1;
107
+ return `${uniqueSelectorFor(parent)} > ${element.tagName.toLowerCase()}:nth-of-type(${index})`;
108
+ }
109
+
110
+ function hasIgnoreFlag(element) {
111
+ return !!element.closest("[data-layout-ignore], [data-layout-check='ignore']");
112
+ }
113
+
114
+ function hasAllowOverflowFlag(element) {
115
+ return !!element.closest("[data-layout-allow-overflow]");
116
+ }
117
+
118
+ function hasAllowCaptionZoneFlag(element) {
119
+ return !!element.closest("[data-layout-allow-caption-zone]");
120
+ }
121
+
122
+ function hasTextClipOptOut(element) {
123
+ return hasAllowOverflowFlag(element) || element.hasAttribute("data-layout-bleed");
124
+ }
125
+
126
+ function opacityChain(element) {
127
+ let opacity = 1;
128
+ for (let current = element; current; current = current.parentElement) {
129
+ const parsed = Number.parseFloat(getComputedStyle(current).opacity || "1");
130
+ if (Number.isFinite(parsed)) opacity *= parsed;
131
+ }
132
+ return opacity;
133
+ }
134
+
135
+ function hasOpacityBelow(element, floor) {
136
+ for (let current = element; current; current = current.parentElement) {
137
+ const parsed = Number.parseFloat(getComputedStyle(current).opacity || "1");
138
+ if (Number.isFinite(parsed) && parsed < floor) return true;
139
+ }
140
+ return false;
141
+ }
142
+
143
+ // A clip-path can shrink an element's painted region to nothing (e.g. a
144
+ // typewriter span pre-reveal at `inset(0 100% 0 0)`, or `circle(0px)`) while
145
+ // its layout box, opacity, visibility and display all still read as present.
146
+ // Such an element paints zero pixels, so flagging it for overlap/occlusion is
147
+ // a false positive. clip-path also drives hit-testing, so an element clipped
148
+ // to nothing is unreachable by elementFromPoint anywhere in its box; only run
149
+ // the probe when a clip-path is actually in effect (self or ancestor) to avoid
150
+ // mistaking a genuinely-occluded element for a clipped one.
151
+ function hasClipPath(element) {
152
+ for (let current = element; current; current = current.parentElement) {
153
+ const clip = getComputedStyle(current).clipPath;
154
+ if (clip && clip !== "none") return true;
155
+ }
156
+ return false;
157
+ }
158
+
159
+ const CLIP_PROBE_COLS = [0.05, 0.25, 0.5, 0.75, 0.95];
160
+ const CLIP_PROBE_ROWS = [0.25, 0.5, 0.75];
161
+
162
+ function paintsAnyProbePoint(element, rect) {
163
+ // Probe resolution intentionally treats edge strips narrower than the
164
+ // nearest probe point as clipped away. That avoids noisy reports for
165
+ // typewriter pre-reveal states; if a real visible-strip bug appears, add
166
+ // edge probes here before widening the audit surface.
167
+ for (const fx of CLIP_PROBE_COLS) {
168
+ for (const fy of CLIP_PROBE_ROWS) {
169
+ const hit = document.elementFromPoint(
170
+ rect.left + rect.width * fx,
171
+ rect.top + rect.height * fy,
172
+ );
173
+ if (hit === element || element.contains(hit)) return true;
174
+ }
175
+ }
176
+ return false;
177
+ }
178
+
179
+ function isClippedAway(element) {
180
+ if (typeof document.elementFromPoint !== "function") return false;
181
+ if (!hasClipPath(element)) return false;
182
+ const rect = element.getBoundingClientRect();
183
+ if (rect.width <= 0.5 || rect.height <= 0.5) return false;
184
+ return !paintsAnyProbePoint(element, rect);
185
+ }
186
+
187
+ function isVisibleElement(element, opacityFloor, probeClipPath) {
188
+ if (IGNORE_TAGS.has(element.tagName)) return false;
189
+ if (hasIgnoreFlag(element)) return false;
190
+ if (
191
+ opacityFloor != null &&
192
+ typeof element.checkVisibility === "function" &&
193
+ !element.checkVisibility({
194
+ opacityProperty: true,
195
+ visibilityProperty: true,
196
+ contentVisibilityAuto: true,
197
+ })
198
+ ) {
199
+ return false;
200
+ }
201
+ const style = getComputedStyle(element);
202
+ if (
203
+ style.display === "none" ||
204
+ style.visibility === "hidden" ||
205
+ style.visibility === "collapse"
206
+ ) {
207
+ return false;
208
+ }
209
+ if (
210
+ opacityFloor == null ? opacityChain(element) < 0.2 : hasOpacityBelow(element, opacityFloor)
211
+ ) {
212
+ return false;
213
+ }
214
+ const rect = element.getBoundingClientRect();
215
+ if (rect.width <= 0.5 || rect.height <= 0.5) return false;
216
+ return probeClipPath === false || !isClippedAway(element);
217
+ }
218
+
219
+ function directTextNodes(element) {
220
+ return Array.from(element.childNodes).filter((node) => node.nodeType === 3);
221
+ }
222
+
223
+ function textContentFor(element, ownTextOnly) {
224
+ const content = ownTextOnly
225
+ ? directTextNodes(element)
226
+ .map((node) => node.textContent || "")
227
+ .join("")
228
+ : element.innerText || element.textContent || "";
229
+ return content.replace(/\s+/g, " ").trim();
230
+ }
231
+
232
+ function hasOwnTextCandidate(element, directOnly) {
233
+ const text = textContentFor(element, directOnly);
234
+ if (!text) return false;
235
+ if (directOnly) return true;
236
+ // Aggregate text may come exclusively from descendants (including hidden
237
+ // captions). The container itself does not paint that text and must not be
238
+ // audited as though it did.
239
+ return textContentFor(element, true).length > 0;
240
+ }
241
+
242
+ function textClientRects(element, directOnly) {
243
+ const subjects = directOnly ? directTextNodes(element) : [element];
244
+ const rects = [];
245
+ for (const subject of subjects) {
246
+ const range = document.createRange();
247
+ range.selectNodeContents(subject);
248
+ rects.push(
249
+ ...Array.from(range.getClientRects()).filter(
250
+ (rect) => rect.width > 0.5 && rect.height > 0.5,
251
+ ),
252
+ );
253
+ range.detach();
254
+ }
255
+ return rects;
256
+ }
257
+
258
+ function unionRects(rects) {
259
+ if (rects.length === 0) return null;
260
+ const union = rects.reduce(
261
+ (acc, rect) => ({
262
+ left: Math.min(acc.left, rect.left),
263
+ top: Math.min(acc.top, rect.top),
264
+ right: Math.max(acc.right, rect.right),
265
+ bottom: Math.max(acc.bottom, rect.bottom),
266
+ }),
267
+ {
268
+ left: Number.POSITIVE_INFINITY,
269
+ top: Number.POSITIVE_INFINITY,
270
+ right: Number.NEGATIVE_INFINITY,
271
+ bottom: Number.NEGATIVE_INFINITY,
272
+ },
273
+ );
274
+ return toRect({
275
+ ...union,
276
+ width: union.right - union.left,
277
+ height: union.bottom - union.top,
278
+ });
279
+ }
280
+
281
+ function visibleTextClientRects(element, directOnly) {
282
+ // Range rects stay geometrically present outside an overflow clip. Reduce
283
+ // them in viewport coordinates so overlap measures only paintable text.
284
+ let rects = textClientRects(element, directOnly).map(toRect);
285
+ for (
286
+ let ancestor = element.parentElement;
287
+ ancestor && rects.length > 0;
288
+ ancestor = ancestor.parentElement
289
+ ) {
290
+ const style = getComputedStyle(ancestor);
291
+ const clipX = clipsOverflowValue(style.overflowX || style.overflow);
292
+ const clipY = clipsOverflowValue(style.overflowY || style.overflow);
293
+ if (!clipX && !clipY) continue;
294
+ const clip = toRect(ancestor.getBoundingClientRect());
295
+ rects = rects
296
+ .map((rect) => {
297
+ const left = clipX ? Math.max(rect.left, clip.left) : rect.left;
298
+ const right = clipX ? Math.min(rect.right, clip.right) : rect.right;
299
+ const top = clipY ? Math.max(rect.top, clip.top) : rect.top;
300
+ const bottom = clipY ? Math.min(rect.bottom, clip.bottom) : rect.bottom;
301
+ if (right - left <= 0.5 || bottom - top <= 0.5) return null;
302
+ return toRect({ left, right, top, bottom, width: right - left, height: bottom - top });
303
+ })
304
+ .filter(Boolean);
305
+ }
306
+ return rects;
307
+ }
308
+
309
+ function textRectFor(element, directOnly) {
310
+ return unionRects(textClientRects(element, directOnly));
311
+ }
312
+
313
+ function parsePx(value) {
314
+ const parsed = Number.parseFloat(value);
315
+ return Number.isFinite(parsed) ? parsed : 0;
316
+ }
317
+
318
+ function hasMeaningfulBoxStyle(style) {
319
+ return (
320
+ parsePx(style.paddingTop) +
321
+ parsePx(style.paddingRight) +
322
+ parsePx(style.paddingBottom) +
323
+ parsePx(style.paddingLeft) +
324
+ parsePx(style.borderTopWidth) +
325
+ parsePx(style.borderRightWidth) +
326
+ parsePx(style.borderBottomWidth) +
327
+ parsePx(style.borderLeftWidth) +
328
+ parsePx(style.borderTopLeftRadius) +
329
+ parsePx(style.borderTopRightRadius) +
330
+ parsePx(style.borderBottomRightRadius) +
331
+ parsePx(style.borderBottomLeftRadius) >
332
+ 0
333
+ );
334
+ }
335
+
336
+ function hasPaint(style) {
337
+ const backgroundColor = style.backgroundColor || "";
338
+ const hasBackground =
339
+ backgroundColor !== "" &&
340
+ backgroundColor !== "transparent" &&
341
+ !backgroundColor.endsWith(", 0)") &&
342
+ backgroundColor !== "rgba(0, 0, 0, 0)";
343
+ const hasImage = style.backgroundImage && style.backgroundImage !== "none";
344
+ const hasBorder =
345
+ parsePx(style.borderTopWidth) +
346
+ parsePx(style.borderRightWidth) +
347
+ parsePx(style.borderBottomWidth) +
348
+ parsePx(style.borderLeftWidth) >
349
+ 0;
350
+ const hasRadius =
351
+ parsePx(style.borderTopLeftRadius) +
352
+ parsePx(style.borderTopRightRadius) +
353
+ parsePx(style.borderBottomRightRadius) +
354
+ parsePx(style.borderBottomLeftRadius) >
355
+ 0;
356
+ return hasBackground || hasImage || hasBorder || hasRadius;
357
+ }
358
+
359
+ function clipsOverflowValue(value) {
360
+ return value && value !== "visible" && value !== "clip visible";
361
+ }
362
+
363
+ function clipsOverflow(style) {
364
+ return [style.overflowX, style.overflowY, style.overflow].some((value) =>
365
+ clipsOverflowValue(value),
366
+ );
367
+ }
368
+
369
+ function rootRectFor(root) {
370
+ const measured = toRect(root.getBoundingClientRect());
371
+ const authoredWidth = Number.parseFloat(root.getAttribute("data-width") || "");
372
+ const authoredHeight = Number.parseFloat(root.getAttribute("data-height") || "");
373
+ const hasAuthoredSize =
374
+ Number.isFinite(authoredWidth) &&
375
+ authoredWidth > 0 &&
376
+ Number.isFinite(authoredHeight) &&
377
+ authoredHeight > 0;
378
+
379
+ if (!hasAuthoredSize) return measured;
380
+ if (measured.width > 0.5 && measured.height > 0.5) return measured;
381
+ return rectFromOrigin(measured.left, measured.top, authoredWidth, authoredHeight);
382
+ }
383
+
384
+ function isConstraintCandidate(element, root, rootRect) {
385
+ if (element === root) return true;
386
+ const style = getComputedStyle(element);
387
+ if (clipsOverflow(style)) return true;
388
+ if (element.hasAttribute("data-layout-boundary")) return true;
389
+ if (!hasPaint(style)) return false;
390
+ if (!hasMeaningfulBoxStyle(style)) return false;
391
+ const rect = element.getBoundingClientRect();
392
+ const rootArea = rootRect.width * rootRect.height;
393
+ const area = rect.width * rect.height;
394
+ return area > 0 && area < rootArea * 0.95;
395
+ }
396
+
397
+ function nearestConstraint(element, root, rootRect) {
398
+ for (
399
+ let current = element;
400
+ current && current !== document.body;
401
+ current = current.parentElement
402
+ ) {
403
+ if (!isVisibleElement(current)) continue;
404
+ if (isConstraintCandidate(current, root, rootRect)) return current;
405
+ if (current === root) return current;
406
+ }
407
+ return root;
408
+ }
409
+
410
+ function formatPx(value) {
411
+ return `${Math.round(value)}px`;
412
+ }
413
+
414
+ function maxOverflow(overflow) {
415
+ return Math.max(...Object.values(overflow).filter((value) => typeof value === "number"));
416
+ }
417
+
418
+ function textOverflowFixHint(textRect, containerRect, overflow, fontSize, targetName) {
419
+ const horizontalOverflow = (overflow.left || 0) + (overflow.right || 0);
420
+ const verticalOverflow = (overflow.top || 0) + (overflow.bottom || 0);
421
+ const neededWidth = containerRect.width + horizontalOverflow;
422
+ const neededHeight = containerRect.height + verticalOverflow;
423
+ const widthRatio = containerRect.width > 0 ? containerRect.width / textRect.width : 0;
424
+ const heightRatio = containerRect.height > 0 ? containerRect.height / textRect.height : 0;
425
+ const limitingRatio = Math.min(
426
+ widthRatio > 0 ? widthRatio : Number.POSITIVE_INFINITY,
427
+ heightRatio > 0 ? heightRatio : Number.POSITIVE_INFINITY,
428
+ );
429
+ const shrinkPercent =
430
+ Number.isFinite(limitingRatio) && limitingRatio < 1
431
+ ? Math.ceil((1 - limitingRatio) * 100)
432
+ : 0;
433
+ const targetFont =
434
+ shrinkPercent > 0 && Number.isFinite(fontSize) && fontSize > 0
435
+ ? ` or shrink font-size from ${formatPx(fontSize)} to ~${formatPx(fontSize * limitingRatio)}`
436
+ : "";
437
+ const sizeTarget =
438
+ horizontalOverflow > 0 && verticalOverflow > 0
439
+ ? `resize ${targetName} to at least ~${formatPx(neededWidth)} x ${formatPx(neededHeight)}`
440
+ : horizontalOverflow > 0
441
+ ? `widen ${targetName} to at least ~${formatPx(neededWidth)}`
442
+ : `increase ${targetName} height to at least ~${formatPx(neededHeight)}`;
443
+
444
+ return `Text is ${formatPx(textRect.width)} x ${formatPx(textRect.height)} inside ${formatPx(containerRect.width)} x ${formatPx(containerRect.height)} and overflows by up to ${formatPx(maxOverflow(overflow))}; ${sizeTarget}${targetFont}, or allow wrapping with max-width/fitTextFontSize.`;
445
+ }
446
+
447
+ function clippedTextIssue(element, time, tolerance) {
448
+ if (hasTextClipOptOut(element)) return null;
449
+ const style = getComputedStyle(element);
450
+ if (!clipsOverflow(style)) return null;
451
+ const overflowX = element.scrollWidth - element.clientWidth;
452
+ const overflowY = element.scrollHeight - element.clientHeight;
453
+ if (overflowX <= tolerance && overflowY <= tolerance) return null;
454
+ const overflow = {};
455
+ if (overflowX > tolerance) overflow.right = round(overflowX);
456
+ if (overflowY > tolerance) overflow.bottom = round(overflowY);
457
+ const selector = selectorFor(element);
458
+ const text = textContentFor(element);
459
+ const rect = toRect(element.getBoundingClientRect());
460
+ const fontSize = parsePx(style.fontSize);
461
+ return {
462
+ code: "clipped_text",
463
+ severity: "error",
464
+ time,
465
+ selector,
466
+ text,
467
+ message: "Text content is clipped by its own box.",
468
+ rect,
469
+ overflow,
470
+ fixHint: textOverflowFixHint(rect, rect, overflow, fontSize, "the text box"),
471
+ };
472
+ }
473
+
474
+ function horizontalTextMetrics(element, style) {
475
+ if (style.writingMode && style.writingMode !== "horizontal-tb") return null;
476
+ const context = document.createElement("canvas").getContext("2d");
477
+ if (!context) return null;
478
+ context.font = `${style.fontStyle} ${style.fontWeight} ${style.fontSize} ${style.fontFamily}`;
479
+ let text = textContentFor(element, true);
480
+ if (style.textTransform === "uppercase") text = text.toUpperCase();
481
+ if (style.textTransform === "lowercase") text = text.toLowerCase();
482
+ const metrics = context.measureText(text);
483
+ return metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent > 0 ? metrics : null;
484
+ }
485
+
486
+ function intersectsTextWindow(rect, clip, tolerance) {
487
+ return (
488
+ Math.min(rect.right, clip.right) - Math.max(rect.left, clip.left) > tolerance &&
489
+ Math.min(rect.bottom, clip.bottom) - Math.max(rect.top, clip.top) > tolerance
490
+ );
491
+ }
492
+
493
+ function visibleTextLineRects(element, rects, style, clip, tolerance) {
494
+ const metrics = horizontalTextMetrics(element, style);
495
+ const fontHeight = metrics ? metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent : 0;
496
+ const lineHeight = parsePx(style.lineHeight) || fontHeight;
497
+ return rects.flatMap((rect) => {
498
+ if (!metrics) return intersectsTextWindow(rect, clip, tolerance) ? [rect] : [];
499
+ const scale = rect.height / fontHeight;
500
+ const inkTop =
501
+ rect.top + (metrics.fontBoundingBoxAscent - metrics.actualBoundingBoxAscent) * scale;
502
+ const inkBottom =
503
+ rect.bottom - (metrics.fontBoundingBoxDescent - metrics.actualBoundingBoxDescent) * scale;
504
+ const ink = { ...rect, top: inkTop, bottom: inkBottom };
505
+ if (!intersectsTextWindow(ink, clip, tolerance)) return [];
506
+ // Negative leading belongs outside the used line box. Font metrics scale
507
+ // with the Range rect, so zoomed cards retain the same clipping decision.
508
+ const inset = Math.max(0, (rect.height - lineHeight * scale) / 2);
509
+ return [
510
+ toRect({
511
+ ...rect,
512
+ top: rect.top + inset,
513
+ bottom: rect.bottom - inset,
514
+ height: rect.height - 2 * inset,
515
+ }),
516
+ ];
517
+ });
518
+ }
519
+
520
+ function textOverflowIssues(element, root, rootRect, time, tolerance, clippedIssue) {
521
+ const lineRects = textClientRects(element, true).map(toRect);
522
+ const textRect = unionRects(lineRects);
523
+ if (!textRect) return [];
524
+ const text = textContentFor(element, true);
525
+ const selector = selectorFor(element);
526
+ const issues = [];
527
+
528
+ const container = nearestConstraint(element, root, rootRect);
529
+ const containerRect = container === root ? rootRect : toRect(container.getBoundingClientRect());
530
+ const elementStyle = getComputedStyle(element);
531
+ const containerClips = clipsOverflow(
532
+ container === root ? getComputedStyle(root) : getComputedStyle(container),
533
+ );
534
+ const visibleTextRect = containerClips
535
+ ? unionRects(visibleTextLineRects(element, lineRects, elementStyle, containerRect, tolerance))
536
+ : textRect;
537
+ const verticalTolerance = containerClips
538
+ ? tolerance
539
+ : Math.max(tolerance, parsePx(elementStyle.fontSize) * 0.2);
540
+ const containerOverflow = visibleTextRect
541
+ ? overflowFor(visibleTextRect, containerRect, tolerance, verticalTolerance)
542
+ : null;
543
+ const billedAsClippedText =
544
+ container === element &&
545
+ clippedIssue != null &&
546
+ containerOverflow != null &&
547
+ containerOverflow.left == null &&
548
+ containerOverflow.top == null;
549
+ if (containerOverflow && !billedAsClippedText && !hasTextClipOptOut(element)) {
550
+ const style = elementStyle;
551
+ issues.push({
552
+ code: "text_box_overflow",
553
+ severity: "error",
554
+ time,
555
+ selector,
556
+ containerSelector: selectorFor(container),
557
+ text,
558
+ message: "Text extends outside its nearest visual/container box.",
559
+ rect: visibleTextRect,
560
+ containerRect,
561
+ overflow: containerOverflow,
562
+ fixHint: textOverflowFixHint(
563
+ visibleTextRect,
564
+ containerRect,
565
+ containerOverflow,
566
+ parsePx(style.fontSize),
567
+ "the container",
568
+ ),
569
+ });
570
+ }
571
+
572
+ const canvasOverflow = overflowFor(textRect, rootRect, tolerance);
573
+ if (canvasOverflow && !hasTextClipOptOut(element)) {
574
+ issues.push({
575
+ code: "canvas_overflow",
576
+ severity: "info",
577
+ time,
578
+ selector,
579
+ containerSelector: selectorFor(root),
580
+ text,
581
+ message: "Text extends outside the composition canvas.",
582
+ rect: textRect,
583
+ containerRect: rootRect,
584
+ overflow: canvasOverflow,
585
+ fixHint:
586
+ "Move the text inward, reduce its size, or mark intentional off-canvas animation with data-layout-allow-overflow.",
587
+ });
588
+ }
589
+
590
+ return issues;
591
+ }
592
+
593
+ function isNowrapTextChild(child) {
594
+ if (!isVisibleElement(child) || hasAllowOverflowFlag(child)) return false;
595
+ if (getComputedStyle(child).whiteSpace !== "nowrap") return false;
596
+ return (child.textContent || "").trim().length > 0;
597
+ }
598
+
599
+ function hasNowrapTextChild(element) {
600
+ return Array.from(element.children).some(isNowrapTextChild);
601
+ }
602
+
603
+ function containerOverflowIssues(root, time, tolerance) {
604
+ const issues = [];
605
+ const containers = Array.from(root.querySelectorAll("*")).filter((element) => {
606
+ if (!isVisibleElement(element) || hasAllowOverflowFlag(element)) return false;
607
+ const style = getComputedStyle(element);
608
+ return (
609
+ clipsOverflow(style) ||
610
+ element.hasAttribute("data-layout-boundary") ||
611
+ hasNowrapTextChild(element)
612
+ );
613
+ });
614
+
615
+ for (const container of containers) {
616
+ const style = getComputedStyle(container);
617
+ const checksEveryChild =
618
+ clipsOverflow(style) || container.hasAttribute("data-layout-boundary");
619
+ const containerRect = toRect(container.getBoundingClientRect());
620
+ for (const child of Array.from(container.children)) {
621
+ if (!isVisibleElement(child) || hasAllowOverflowFlag(child)) continue;
622
+ if (!checksEveryChild && !isNowrapTextChild(child)) continue;
623
+ const childRect = toRect(child.getBoundingClientRect());
624
+ const overflow = checksEveryChild
625
+ ? overflowFor(childRect, containerRect, tolerance)
626
+ : horizontalOverflow(childRect, containerRect, tolerance);
627
+ if (!overflow) continue;
628
+ issues.push({
629
+ code: "container_overflow",
630
+ severity: "warning",
631
+ time,
632
+ selector: selectorFor(child),
633
+ containerSelector: selectorFor(container),
634
+ message: checksEveryChild
635
+ ? "Element extends outside a clipping layout container."
636
+ : "Nowrap text is wider than its container.",
637
+ rect: childRect,
638
+ containerRect,
639
+ overflow,
640
+ fixHint:
641
+ "Resize/reposition the child or container, or mark intentional overflow with data-layout-allow-overflow.",
642
+ });
643
+ }
644
+ }
645
+
646
+ return issues;
647
+ }
648
+
649
+ function hasAllowOverlapFlag(element) {
650
+ // Overlap intent belongs to the text block that participates in the
651
+ // layering. Inheriting this marker from a scene/root would turn one local
652
+ // waiver into a blanket exemption for every collision in that subtree.
653
+ return element.hasAttribute("data-layout-allow-overlap");
654
+ }
655
+
656
+ function isTransparentColor(color) {
657
+ return (
658
+ !color || color === "transparent" || color === "rgba(0, 0, 0, 0)" || color.endsWith(", 0)")
659
+ );
660
+ }
661
+
662
+ function alphaFromParts(parts, index) {
663
+ if (parts.length <= index) return 1;
664
+ const raw = parts[index].trim();
665
+ return raw.endsWith("%") ? parsePx(raw) / 100 : parsePx(raw);
666
+ }
667
+
668
+ // Alpha of a CSS colour; 1 when no alpha component is present. Handles both
669
+ // legacy `rgba(r, g, b, a)` and modern `rgb(r g b / a)` syntaxes.
670
+ function colorAlpha(color) {
671
+ const match = (color || "").match(/rgba?\(([^)]+)\)/);
672
+ if (!match) return 1;
673
+ const body = match[1];
674
+ return body.includes(",")
675
+ ? alphaFromParts(body.split(","), 3)
676
+ : alphaFromParts(body.split("/"), 1);
677
+ }
678
+
679
+ // A text block competes for space only when it is solid: watermark-style text
680
+ // (low colour alpha) is decorative and exempt, as are elements opted out with
681
+ // data-layout-allow-overlap.
682
+ function isSolidTextBlock(element) {
683
+ if (!isVisibleElement(element) || !hasOwnTextCandidate(element, true)) return false;
684
+ if (hasAllowOverlapFlag(element)) return false;
685
+ return colorAlpha(getComputedStyle(element).color) >= 0.35;
686
+ }
687
+
688
+ function collectSolidTextBlocks(root) {
689
+ const blocks = [];
690
+ for (const element of Array.from(root.querySelectorAll("*"))) {
691
+ if (!isSolidTextBlock(element)) continue;
692
+ const rects = visibleTextClientRects(element, true);
693
+ const rect = unionRects(rects);
694
+ if (rect) blocks.push({ element, rect, rects });
695
+ }
696
+ return blocks;
697
+ }
698
+
699
+ function rectArea(rect) {
700
+ return rect.width * rect.height;
701
+ }
702
+
703
+ function intersectionArea(a, b) {
704
+ const overlapX = Math.min(a.right, b.right) - Math.max(a.left, b.left);
705
+ const overlapY = Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top);
706
+ return overlapX > 0 && overlapY > 0 ? overlapX * overlapY : 0;
707
+ }
708
+
709
+ function rectsArea(rects) {
710
+ return rects.reduce((total, rect) => total + rectArea(rect), 0);
711
+ }
712
+
713
+ function fragmentIntersectionArea(a, b) {
714
+ let total = 0;
715
+ for (const aRect of a) {
716
+ for (const bRect of b) total += intersectionArea(aRect, bRect);
717
+ }
718
+ return total;
719
+ }
720
+
721
+ function isNested(a, b) {
722
+ return a.contains(b) || b.contains(a);
723
+ }
724
+
725
+ function isInFlow(element) {
726
+ const position = getComputedStyle(element).position;
727
+ return position === "static" || position === "relative" || position === "sticky";
728
+ }
729
+
730
+ function nearestFlexGridAncestor(element) {
731
+ for (let parent = element.parentElement; parent; parent = parent.parentElement) {
732
+ const display = getComputedStyle(parent).display;
733
+ if (display.includes("flex") || display.includes("grid")) return parent;
734
+ }
735
+ return null;
736
+ }
737
+
738
+ // Two in-flow text blocks governed by the same flex/grid container are placed
739
+ // by the layout engine, which reserves space for each — they cannot visually
740
+ // collide. Any measured text-rect overlap between them is line-box / leading
741
+ // slop (tight stacks, number lockups, super/subscript units), not a collision.
742
+ // A real overlap bug needs free positioning (absolute/fixed), which keeps a
743
+ // different formatting context and is still flagged.
744
+ function isManagedFlowOverlap(a, b) {
745
+ if (!isInFlow(a) || !isInFlow(b)) return false;
746
+ const container = nearestFlexGridAncestor(a);
747
+ return !!container && container === nearestFlexGridAncestor(b);
748
+ }
749
+
750
+ // Two solid text blocks whose boxes overlap by more than a fifth of the
751
+ // smaller block read as a collision — unreadable, and invisible to the
752
+ // overflow checks, which only compare an element against its container.
753
+ function overlapIssue(a, b, time) {
754
+ if (isNested(a.element, b.element)) return null;
755
+ if (isManagedFlowOverlap(a.element, b.element)) return null;
756
+ const area = fragmentIntersectionArea(a.rects, b.rects);
757
+ if (area <= Math.min(rectsArea(a.rects), rectsArea(b.rects)) * 0.2) return null;
758
+ return {
759
+ // Warning at the per-sample level: a single-sample overlap is usually an
760
+ // entrance/exit transient (two blocks crossing mid-animation), not a real
761
+ // collision. `collapseStaticLayoutIssues` (utils/layoutAudit.ts) re-promotes
762
+ // this to error once the SAME overlap is held across >= 2 adjacent samples
763
+ // (or ~500ms of timeline) — a persistence-tiered replacement for the old
764
+ // "re-promote once data-layout-allow-overlap is widely adopted" plan (#U10).
765
+ code: "content_overlap",
766
+ severity: "warning",
767
+ time,
768
+ selector: uniqueSelectorFor(a.element),
769
+ containerSelector: uniqueSelectorFor(b.element),
770
+ text: textContentFor(a.element),
771
+ message: "Two text blocks overlap and may render unreadable.",
772
+ rect: a.rect,
773
+ fixHint:
774
+ "Give each block its own zone, or mark intentional layering with data-layout-allow-overlap.",
775
+ };
776
+ }
777
+
778
+ function contentOverlapIssues(root, time) {
779
+ const blocks = collectSolidTextBlocks(root);
780
+ const issues = [];
781
+ for (let i = 0; i < blocks.length; i++) {
782
+ for (let j = i + 1; j < blocks.length; j++) {
783
+ const issue = overlapIssue(blocks[i], blocks[j], time);
784
+ if (issue) issues.push(issue);
785
+ }
786
+ }
787
+ return issues;
788
+ }
789
+
790
+ function hasOpaqueBackground(style) {
791
+ let imageAlpha = 0;
792
+ if (style.backgroundImage && style.backgroundImage !== "none") {
793
+ if (style.backgroundImage.includes("url(")) return true;
794
+ // A gradient only occludes as much as its colours — a 4%-alpha grid/scrim must not count.
795
+ imageAlpha = gradientLayersAlpha(style.backgroundImage);
796
+ }
797
+ const colorValue = isTransparentColor(style.backgroundColor)
798
+ ? 0
799
+ : colorAlpha(style.backgroundColor);
800
+ // Layers composite: a 0.5 gradient over a 0.5 background colour paints at ~0.75.
801
+ return 1 - (1 - imageAlpha) * (1 - colorValue) > 0.6;
802
+ }
803
+
804
+ // background-image layers stack: two 0.5-alpha gradients paint at 1-(1-.5)^2 = .75.
805
+ function gradientLayersAlpha(backgroundImage) {
806
+ let combined = 0;
807
+ for (const layer of splitTopLevelCommas(backgroundImage)) {
808
+ combined = 1 - (1 - combined) * (1 - gradientMaxAlpha(layer));
809
+ }
810
+ return combined;
811
+ }
812
+
813
+ function splitTopLevelCommas(value) {
814
+ const parts = [];
815
+ let depth = 0;
816
+ let start = 0;
817
+ for (let i = 0; i < value.length; i += 1) {
818
+ const ch = value[i];
819
+ if (ch === "(") depth += 1;
820
+ else if (ch === ")") depth -= 1;
821
+ else if (ch === "," && depth === 0) {
822
+ parts.push(value.slice(start, i));
823
+ start = i + 1;
824
+ }
825
+ }
826
+ parts.push(value.slice(start));
827
+ return parts;
828
+ }
829
+
830
+ function gradientMaxAlpha(backgroundImage) {
831
+ // Any colour we cannot score (oklch/lab/named-colour fns/...) counts as opaque so real panels keep flagging.
832
+ const known = backgroundImage
833
+ .replace(/(?:repeating-)?(?:linear|radial|conic)-gradient\(/gi, "(")
834
+ .replace(/rgba?\([^)]*\)/gi, "");
835
+ if (/[a-z][a-z-]+\(/i.test(known)) return 1;
836
+ const colors = backgroundImage.match(/rgba?\([^)]*\)|#[0-9a-fA-F]{3,8}\b|\btransparent\b/g);
837
+ if (!colors) return 1;
838
+ let max = 0;
839
+ for (const color of colors) {
840
+ if (color === "transparent") continue;
841
+ if (color.startsWith("#")) {
842
+ const hex = color.slice(1);
843
+ max = Math.max(
844
+ max,
845
+ hex.length === 4
846
+ ? parseInt(hex[3] + hex[3], 16) / 255
847
+ : hex.length === 8
848
+ ? parseInt(hex.slice(6), 16) / 255
849
+ : 1,
850
+ );
851
+ } else {
852
+ max = Math.max(max, colorAlpha(color));
853
+ }
854
+ }
855
+ return max;
856
+ }
857
+
858
+ const RASTER_TAGS = new Set(["IMG", "VIDEO", "CANVAS"]);
859
+ const FRAME_MEDIA_TAGS = new Set([...RASTER_TAGS, "SVG"]);
860
+ const imageAlphaCanvases = new WeakMap();
861
+
862
+ function objectPositionOffset(value, freeSpace) {
863
+ const token = String(value || "50%")
864
+ .trim()
865
+ .split(/\s+/)[0];
866
+ if (token === "left" || token === "top") return 0;
867
+ if (token === "right" || token === "bottom") return freeSpace;
868
+ if (token === "center") return freeSpace / 2;
869
+ if (token.endsWith("%")) return (freeSpace * parseFloat(token)) / 100;
870
+ const pixels = parseFloat(token);
871
+ return Number.isFinite(pixels) ? pixels : freeSpace / 2;
872
+ }
873
+
874
+ function objectPositionOffsets(value, freeX, freeY) {
875
+ const tokens = String(value || "50% 50%")
876
+ .trim()
877
+ .split(/\s+/)
878
+ .slice(0, 2);
879
+ let x = "50%";
880
+ let y = "50%";
881
+ if (tokens.length === 1) {
882
+ if (tokens[0] === "top" || tokens[0] === "bottom") y = tokens[0];
883
+ else x = tokens[0];
884
+ } else {
885
+ for (const token of tokens) {
886
+ if (token === "top" || token === "bottom") y = token;
887
+ else if (token === "left" || token === "right") x = token;
888
+ else if (x === "50%") x = token;
889
+ else y = token;
890
+ }
891
+ }
892
+ return { x: objectPositionOffset(x, freeX), y: objectPositionOffset(y, freeY) };
893
+ }
894
+
895
+ // Return the alpha painted by an <img> at a viewport point. `null` means the
896
+ // browser would not let us inspect the image (not loaded or cross-origin), in
897
+ // which case callers preserve the conservative opaque fallback.
898
+ function imageAlphaAt(element, x, y) {
899
+ const sourceWidth = element.naturalWidth;
900
+ const sourceHeight = element.naturalHeight;
901
+ const rect = element.getBoundingClientRect();
902
+ if (!sourceWidth || !sourceHeight || !rect.width || !rect.height) return null;
903
+
904
+ const style = getComputedStyle(element);
905
+ const fit = style.objectFit || "fill";
906
+ let scaleX = rect.width / sourceWidth;
907
+ let scaleY = rect.height / sourceHeight;
908
+ if (fit !== "fill") {
909
+ const contain = Math.min(scaleX, scaleY);
910
+ const cover = Math.max(scaleX, scaleY);
911
+ const scale =
912
+ fit === "cover"
913
+ ? cover
914
+ : fit === "none"
915
+ ? 1
916
+ : fit === "scale-down"
917
+ ? Math.min(1, contain)
918
+ : contain;
919
+ scaleX = scale;
920
+ scaleY = scale;
921
+ }
922
+
923
+ const paintedWidth = sourceWidth * scaleX;
924
+ const paintedHeight = sourceHeight * scaleY;
925
+ const offsets = objectPositionOffsets(
926
+ style.objectPosition,
927
+ rect.width - paintedWidth,
928
+ rect.height - paintedHeight,
929
+ );
930
+ const localX = x - rect.left - offsets.x;
931
+ const localY = y - rect.top - offsets.y;
932
+ if (localX < 0 || localY < 0 || localX >= paintedWidth || localY >= paintedHeight) return 0;
933
+
934
+ try {
935
+ let cached = imageAlphaCanvases.get(element);
936
+ const source = element.currentSrc || element.src;
937
+ if (
938
+ !cached ||
939
+ cached.width !== sourceWidth ||
940
+ cached.height !== sourceHeight ||
941
+ cached.source !== source
942
+ ) {
943
+ const canvas = document.createElement("canvas");
944
+ canvas.width = sourceWidth;
945
+ canvas.height = sourceHeight;
946
+ const context = canvas.getContext("2d", { willReadFrequently: true });
947
+ if (!context) return null;
948
+ context.drawImage(element, 0, 0, sourceWidth, sourceHeight);
949
+ cached = { context, width: sourceWidth, height: sourceHeight, source };
950
+ imageAlphaCanvases.set(element, cached);
951
+ }
952
+ const sourceX = Math.min(sourceWidth - 1, Math.max(0, Math.floor(localX / scaleX)));
953
+ const sourceY = Math.min(sourceHeight - 1, Math.max(0, Math.floor(localY / scaleY)));
954
+ return cached.context.getImageData(sourceX, sourceY, 1, 1).data[3] / 255;
955
+ } catch {
956
+ return null;
957
+ }
958
+ }
959
+
960
+ // An element hides text beneath it when it paints opaque pixels at near-full
961
+ // opacity: raster content (img/video/canvas), a background image, or a solid
962
+ // background colour. Low-opacity overlays (grain, scrims) do not occlude.
963
+ function isOpaqueOccluder(element, x, y) {
964
+ const opacity = opacityChain(element);
965
+ if (opacity < 0.6) return false;
966
+ if (IGNORE_TAGS.has(element.tagName)) return false;
967
+ if (element.tagName === "IMG") {
968
+ const alpha = imageAlphaAt(element, x, y);
969
+ if (alpha !== null) return alpha * opacity >= 0.6;
970
+ }
971
+ if (RASTER_TAGS.has(element.tagName)) return true;
972
+ return hasOpaqueBackground(getComputedStyle(element));
973
+ }
974
+
975
+ function hasAllowOcclusionFlag(element) {
976
+ return element.hasAttribute("data-layout-allow-occlusion");
977
+ }
978
+
979
+ // A foreign element is one painted independently of the text — not the text
980
+ // itself, its own subtree, or an ancestor it shares a background with.
981
+ function isForeignElement(element, hit) {
982
+ return !!hit && hit !== element && !element.contains(hit) && !hit.contains(element);
983
+ }
984
+
985
+ // During a scene-to-scene crossfade the incoming scene paints over the
986
+ // outgoing scene's still-visible text at >= 0.6 opacity — and `--at-transitions`
987
+ // samples exactly that midpoint. That overlap is the transition doing its job,
988
+ // not an occlusion bug. Detect it: the occluder lives in a DIFFERENT composition
989
+ // mount ([data-composition-id]) than the text, and at least one of the two scenes
990
+ // is mid-fade (effective opacity < 1). Two fully-settled scenes overlapping
991
+ // (both opacity 1) is NOT suppressed — that is a real layering bug.
992
+ function isCrossSceneTransitionOverlap(textEl, occluder) {
993
+ const textScene = textEl.closest("[data-composition-id]");
994
+ const occluderScene = occluder.closest("[data-composition-id]");
995
+ if (!textScene || !occluderScene || textScene === occluderScene) return false;
996
+ return Math.min(opacityChain(textScene), opacityChain(occluderScene)) < 0.999;
997
+ }
998
+
999
+ // The nearest ancestor establishing a 3D rendering context, or null. Elements
1000
+ // sharing one are depth-sorted in 3D, so a "covering" hit is legitimate
1001
+ // perspective (e.g. the back face of a preserve-3d cube), not a 2D overlap.
1002
+ function preserve3dContext(element) {
1003
+ for (let current = element; current; current = current.parentElement) {
1004
+ const ts = getComputedStyle(current).transformStyle;
1005
+ if (ts === "preserve-3d") return current;
1006
+ }
1007
+ return null;
1008
+ }
1009
+
1010
+ function sharedPreserve3d(a, b) {
1011
+ const ctx = preserve3dContext(a);
1012
+ return !!ctx && ctx === preserve3dContext(b);
1013
+ }
1014
+
1015
+ // The opaque element painted over (x, y), or null when the topmost element
1016
+ // there is related to the text, non-opaque, sharing a 3D context with it, or
1017
+ // part of a transient crossfade overlap.
1018
+ // fallow-ignore-next-line complexity
1019
+ function occluderAt(element, x, y) {
1020
+ // Walk the paint-ordered stack: a transparent layer on top must not mask an opaque one below it.
1021
+ const stack =
1022
+ typeof document.elementsFromPoint === "function"
1023
+ ? document.elementsFromPoint(x, y)
1024
+ : typeof document.elementFromPoint === "function"
1025
+ ? [document.elementFromPoint(x, y)].filter(Boolean)
1026
+ : [];
1027
+ for (const hit of stack) {
1028
+ if (!isForeignElement(element, hit)) return null;
1029
+ // Pair-specific exemptions excuse this hit only; keep walking for deeper occluders.
1030
+ if (sharedPreserve3d(element, hit)) continue;
1031
+ if (isCrossSceneTransitionOverlap(element, hit)) continue;
1032
+ if (isOpaqueOccluder(hit, x, y)) return hit;
1033
+ }
1034
+ return null;
1035
+ }
1036
+
1037
+ const OCCLUSION_PROBE_Y_FRACTIONS = [0.25, 0.5, 0.75];
1038
+ const OCCLUSION_PROBE_X_FRACTIONS = [0.03, 0.1, 0.2, 0.35, 0.5, 0.65, 0.8, 0.9, 0.97];
1039
+ const OCCLUSION_GRID_POINTS =
1040
+ OCCLUSION_PROBE_Y_FRACTIONS.length * OCCLUSION_PROBE_X_FRACTIONS.length;
1041
+
1042
+ // Short, atomic text (a label/button/word, no whitespace) reads as a single
1043
+ // unit — ANY covered probe point changes what it says, so flag at any hit
1044
+ // (the pre-#U10 behaviour). Longer prose survives a nibbled edge; only flag
1045
+ // once a real share of it is covered — see `occludedTextIssue`.
1046
+ const ATOMIC_LABEL_MAX_CHARS = 16;
1047
+ // Default prose floor — callers may lower via auditLayout({ proseCoverageFloor }).
1048
+ const DEFAULT_PROSE_COVERAGE_FLOOR = 0.15;
1049
+
1050
+ function isAtomicLabel(text) {
1051
+ return text.length > 0 && text.length <= ATOMIC_LABEL_MAX_CHARS && !/\s/.test(text);
1052
+ }
1053
+
1054
+ // Sweep a grid across each painted text fragment (three rows, not just the
1055
+ // mid-line, so overlays covering only part of a multi-line block are caught).
1056
+ // Sampling fragments instead of their union avoids probing empty line gaps.
1057
+ // Unlike a
1058
+ // first-hit scan, this keeps sampling every point so it can report what
1059
+ // fraction of the box is actually covered — a corner nibble on a paragraph
1060
+ // reads very differently from a label buried under an overlay. Still
1061
+ // returns the first opaque element found, for `containerSelector`.
1062
+ function occlusionCoverage(element, textRects) {
1063
+ let occluder = null;
1064
+ let hits = 0;
1065
+ for (const textRect of textRects) {
1066
+ for (const yFraction of OCCLUSION_PROBE_Y_FRACTIONS) {
1067
+ const y = textRect.top + textRect.height * yFraction;
1068
+ for (const xFraction of OCCLUSION_PROBE_X_FRACTIONS) {
1069
+ const hit = occluderAt(element, textRect.left + textRect.width * xFraction, y);
1070
+ if (!hit) continue;
1071
+ hits += 1;
1072
+ if (!occluder) occluder = hit;
1073
+ }
1074
+ }
1075
+ }
1076
+ return {
1077
+ occluder,
1078
+ coveredFraction: round(hits / (OCCLUSION_GRID_POINTS * textRects.length)),
1079
+ };
1080
+ }
1081
+
1082
+ // pointer-events:none hides elements from elementFromPoint — both probed text AND occluders.
1083
+ function restoreHitTesting(root) {
1084
+ const restores = [];
1085
+ for (const node of [root, ...root.querySelectorAll("*")]) {
1086
+ if (getComputedStyle(node).pointerEvents !== "none") continue;
1087
+ const previous = node.style.getPropertyValue("pointer-events");
1088
+ const priority = node.style.getPropertyPriority("pointer-events");
1089
+ node.style.setProperty("pointer-events", "auto", "important");
1090
+ restores.push(() => {
1091
+ if (previous) node.style.setProperty("pointer-events", previous, priority);
1092
+ else node.style.removeProperty("pointer-events");
1093
+ });
1094
+ }
1095
+ return () => restores.forEach((restore) => restore());
1096
+ }
1097
+
1098
+ // No text ink is on screen while every non-whitespace text node sits at ~0 opacity (entrance not started).
1099
+ function hasVisibleTextInk(element) {
1100
+ const nodes = [element, ...element.querySelectorAll("*")];
1101
+ for (const node of nodes) {
1102
+ if (!directTextNodes(node).some((textNode) => textNode.textContent.trim())) continue;
1103
+ if (opacityChain(node) >= 0.05) return true;
1104
+ }
1105
+ return false;
1106
+ }
1107
+
1108
+ // text_occluded: atomic labels flag at any hit; prose needs coveredFraction >= proseCoverageFloor (default 0.15).
1109
+ function occludedTextIssue(element, time, proseCoverageFloor) {
1110
+ if (hasAllowOcclusionFlag(element)) return null;
1111
+ if (!hasVisibleTextInk(element)) return null;
1112
+ const textRect = textRectFor(element, true);
1113
+ if (!textRect) return null;
1114
+ const textRects = textClientRects(element, true);
1115
+ const text = textContentFor(element, true);
1116
+ const { occluder, coveredFraction } = occlusionCoverage(
1117
+ element,
1118
+ textRects.length > 0 ? textRects : [textRect],
1119
+ );
1120
+ if (!occluder) return null;
1121
+ if (!isAtomicLabel(text) && coveredFraction < proseCoverageFloor) return null;
1122
+ return {
1123
+ code: "text_occluded",
1124
+ severity: "error",
1125
+ time,
1126
+ selector: uniqueSelectorFor(element),
1127
+ containerSelector: uniqueSelectorFor(occluder),
1128
+ text,
1129
+ message: "Text is hidden beneath an opaque element.",
1130
+ rect: textRect,
1131
+ coveredFraction,
1132
+ fixHint:
1133
+ "Give the text its own zone, raise its stacking order above the covering element, or mark intentional layering with data-layout-allow-occlusion.",
1134
+ };
1135
+ }
1136
+
1137
+ // Text whose glyphs paint with an effectively transparent fill renders
1138
+ // invisibly even though the element, its box, opacity and color all read as
1139
+ // present — so geometry/occlusion/contrast audits miss it (contrast reads
1140
+ // `color`, not the fill that actually paints). `-webkit-text-fill-color`
1141
+ // overrides `color` for the glyph fill AND inherits, so a parent's
1142
+ // `transparent` fill silently blanks descendant text that has its own opaque
1143
+ // `color`. Its computed value already resolves to `color` when unset, so it
1144
+ // is the effective fill directly. Clipped text (`background-clip: text`)
1145
+ // legitimately uses a transparent fill — BUT only when a background actually
1146
+ // paints the glyphs; a `background-clip: text` with no gradient/image and no
1147
+ // opaque background-color paints nothing, so it stays reportable.
1148
+ function invisibleTextIssue(element, time) {
1149
+ const textRect = textRectFor(element, true);
1150
+ if (!textRect) return null;
1151
+ const text = textContentFor(element, true);
1152
+ if (!text) return null;
1153
+ const cs = getComputedStyle(element);
1154
+ // Vendor computed-style props are read by property (camelCase), matching
1155
+ // the rest of this script; `webkitTextFillColor` computes to `color` when
1156
+ // unset, so it is the effective fill directly.
1157
+ const fill = cs.webkitTextFillColor || cs.color;
1158
+ if (colorAlpha(fill) > 0.05) return null;
1159
+ const clip = cs.webkitBackgroundClip || cs.backgroundClip || "";
1160
+ if (/text/i.test(clip)) {
1161
+ const bgImage = cs.backgroundImage || "none";
1162
+ const paintsGlyphs =
1163
+ bgImage !== "none" || colorAlpha(cs.backgroundColor || "rgba(0, 0, 0, 0)") > 0.05;
1164
+ // A usable clipped background fills the glyphs — legitimate gradient/solid
1165
+ // clipped text. If nothing paints, fall through and report it.
1166
+ if (paintsGlyphs) return null;
1167
+ }
1168
+ return {
1169
+ code: "text_not_painted",
1170
+ severity: "error",
1171
+ time,
1172
+ selector: selectorFor(element),
1173
+ text,
1174
+ message:
1175
+ "Text paints with an effectively transparent fill (-webkit-text-fill-color / color), so its glyphs are invisible.",
1176
+ rect: textRect,
1177
+ fixHint:
1178
+ "Set an explicit, opaque `color` on the text — and an explicit `-webkit-text-fill-color` if an ancestor makes the fill transparent. If the transparency is intentional gradient text, add `background-clip: text`.",
1179
+ };
1180
+ }
1181
+
1182
+ // Attachment allowance: callouts/tooltips legitimately hang near (not inside) their anchor.
1183
+ const ESCAPE_INTERSECTION_FRACTION = 0.3;
1184
+ const ESCAPE_MIN_CHILD_AREA = 2500;
1185
+
1186
+ function edgeGap(child, parent) {
1187
+ const dx = Math.max(parent.left - child.right, 0, child.left - parent.right);
1188
+ const dy = Math.max(parent.top - child.bottom, 0, child.top - parent.bottom);
1189
+ return Math.sqrt(dx * dx + dy * dy);
1190
+ }
1191
+
1192
+ // An absolute element rendering far outside its offset parent was positioned in the wrong frame.
1193
+ function escapedContainerIssues(root, time) {
1194
+ const issues = [];
1195
+ const flagged = new Set();
1196
+ for (const element of Array.from(root.querySelectorAll("*"))) {
1197
+ if (!isVisibleElement(element) || hasAllowOverflowFlag(element)) continue;
1198
+ if (getComputedStyle(element).position !== "absolute") continue;
1199
+ const parent = element.offsetParent;
1200
+ if (!parent || parent === document.body || parent === root || !isVisibleElement(parent)) {
1201
+ continue;
1202
+ }
1203
+ const childRect = toRect(element.getBoundingClientRect());
1204
+ if (rectArea(childRect) < ESCAPE_MIN_CHILD_AREA) continue;
1205
+ const parentRect = toRect(parent.getBoundingClientRect());
1206
+ const visible = intersectionArea(childRect, parentRect);
1207
+ if (visible >= rectArea(childRect) * ESCAPE_INTERSECTION_FRACTION) continue;
1208
+ // Fully detached but hugging the parent = a callout/tooltip; touching yet mostly outside = drift.
1209
+ const allowance = Math.max(48, Math.min(childRect.width, childRect.height) / 2);
1210
+ if (visible <= 0 && edgeGap(childRect, parentRect) <= allowance) continue;
1211
+ flagged.add(element);
1212
+ issues.push({
1213
+ code: "escaped_container",
1214
+ severity: "warning",
1215
+ time,
1216
+ selector: selectorFor(element),
1217
+ containerSelector: selectorFor(parent),
1218
+ text: textContentFor(element),
1219
+ message:
1220
+ "Positioned element renders far outside its offset parent — its coordinates were likely computed in a different frame (canvas/viewport pixels).",
1221
+ rect: childRect,
1222
+ containerRect: parentRect,
1223
+ fixHint:
1224
+ "Compute left/top in the offset parent's frame (subtract its rect), or mark intentional placement with data-layout-allow-overflow.",
1225
+ });
1226
+ }
1227
+ return { issues, flagged };
1228
+ }
1229
+
1230
+ // A gradient reads as content when any stop is solid; all-translucent stops are glows/vignettes.
1231
+ function gradientHasOpaqueStop(image) {
1232
+ const colors = image.match(/rgba?\([^)]+\)|#[0-9a-f]{3,8}|\btransparent\b/gi) || [];
1233
+ return colors.some((color) => !/^transparent$/i.test(color) && colorAlpha(color) >= 0.6);
1234
+ }
1235
+
1236
+ function isPaintedPanel(element) {
1237
+ if (FRAME_MEDIA_TAGS.has(element.tagName.toUpperCase())) return false;
1238
+ const style = getComputedStyle(element);
1239
+ const image = style.backgroundImage || "none";
1240
+ if (image.includes("url(")) return true;
1241
+ if (image !== "none" && gradientHasOpaqueStop(image)) return true;
1242
+ if (!isTransparentColor(style.backgroundColor) && colorAlpha(style.backgroundColor) > 0.05) {
1243
+ return true;
1244
+ }
1245
+ return (
1246
+ parsePx(style.borderTopWidth) +
1247
+ parsePx(style.borderRightWidth) +
1248
+ parsePx(style.borderBottomWidth) +
1249
+ parsePx(style.borderLeftWidth) >
1250
+ 0
1251
+ );
1252
+ }
1253
+
1254
+ // Canvas-breach floor: entrance nudges stay quiet; matches the connector threshold scale.
1255
+ const PANEL_BREACH_FLOOR_PX = 24;
1256
+ const PANEL_BREACH_FLOOR_FRACTION = 0.025;
1257
+ // A hero-sized panel stuck on the edge is drift; a small painted bleed is usually decoration.
1258
+ const PANEL_HERO_AREA_FRACTION = 0.1;
1259
+
1260
+ // Painted panels breaching the canvas: text is canvas_overflow's, media is frame_out_of_frame's, panels were nobody's.
1261
+ function panelOutOfCanvasIssues(root, rootRect, time, tolerance, escapedElements) {
1262
+ const issues = [];
1263
+ const floor = Math.max(
1264
+ PANEL_BREACH_FLOOR_PX,
1265
+ Math.min(rootRect.width, rootRect.height) * PANEL_BREACH_FLOOR_FRACTION,
1266
+ );
1267
+ const rootArea = rectArea(rootRect);
1268
+ const flagged = new Set();
1269
+ for (const element of Array.from(root.querySelectorAll("*"))) {
1270
+ if (!isVisibleElement(element) || hasAllowOverflowFlag(element)) continue;
1271
+ if (escapedElements.has(element)) continue;
1272
+ // Ownership is geometric and strict-mutex: any text breach past canvas_overflow's own
1273
+ // tolerance cedes the element to canvas_overflow; in-bounds text leaves the panel finding.
1274
+ if (hasOwnTextCandidate(element, true)) {
1275
+ const textRect = textRectFor(element, true);
1276
+ if (textRect && overflowFor(textRect, rootRect, tolerance)) continue;
1277
+ }
1278
+ const rect = toRect(element.getBoundingClientRect());
1279
+ if (rectArea(rect) >= rootArea * 0.95) continue;
1280
+ // Fully off-canvas paints nothing — that is a parked entrance, not drift.
1281
+ if (intersectionArea(rect, rootRect) <= 0) continue;
1282
+ const overflow = overflowFor(rect, rootRect, floor);
1283
+ if (!overflow || !isPaintedPanel(element)) continue;
1284
+ if (element.parentElement && flagged.has(element.parentElement)) {
1285
+ flagged.add(element);
1286
+ continue;
1287
+ }
1288
+ flagged.add(element);
1289
+ issues.push({
1290
+ code: "panel_out_of_canvas",
1291
+ severity: rectArea(rect) >= rootArea * PANEL_HERO_AREA_FRACTION ? "warning" : "info",
1292
+ time,
1293
+ selector: selectorFor(element),
1294
+ containerSelector: selectorFor(root),
1295
+ text: textContentFor(element).slice(0, 48),
1296
+ message: "Painted panel extends outside the composition canvas.",
1297
+ rect,
1298
+ containerRect: rootRect,
1299
+ overflow,
1300
+ fixHint:
1301
+ "Move the panel inward, or mark intentional off-canvas animation with data-layout-allow-overflow.",
1302
+ });
1303
+ }
1304
+ return issues;
1305
+ }
1306
+
1307
+ // Pixels drawn into a <canvas> have no DOM box, so text the frame edge cuts is invisible to
1308
+ // canvas_overflow. Read a thin band just inside each covered frame edge and count sharp steps.
1309
+ const CANVAS_EDGE_STEP = 96;
1310
+ const CANVAS_EDGE_MIN_STEPS = 2;
1311
+ // Monospace and bitmap glyph cells end in a gap, so the outermost column alone can read empty.
1312
+ const CANVAS_EDGE_BAND_PX = 4;
1313
+
1314
+ function coveredFrameEdges(rect, rootRect, tolerance) {
1315
+ const top = Math.max(rect.top, rootRect.top);
1316
+ const bottom = Math.min(rect.bottom, rootRect.bottom);
1317
+ const left = Math.max(rect.left, rootRect.left);
1318
+ const right = Math.min(rect.right, rootRect.right);
1319
+ if (bottom - top < CANVAS_EDGE_BAND_PX || right - left < CANVAS_EDGE_BAND_PX) return [];
1320
+ const band = CANVAS_EDGE_BAND_PX;
1321
+ const rows = { y: top, height: bottom - top, width: band, vertical: true };
1322
+ const cols = { x: left, width: right - left, height: band, vertical: false };
1323
+ const edges = [];
1324
+ if (rect.left <= rootRect.left + tolerance) edges.push({ side: "left", x: left, ...rows });
1325
+ if (rect.right >= rootRect.right - tolerance)
1326
+ edges.push({ side: "right", x: right - band, ...rows });
1327
+ if (rect.top <= rootRect.top + tolerance) edges.push({ side: "top", y: top, ...cols });
1328
+ if (rect.bottom >= rootRect.bottom - tolerance)
1329
+ edges.push({ side: "bottom", y: bottom - band, ...cols });
1330
+ return edges;
1331
+ }
1332
+
1333
+ // Draw the band into a scratch canvas (never getContext on the film's own canvas, which would
1334
+ // claim it as 2D). A WebGL canvas without preserveDrawingBuffer reads blank: no finding.
1335
+ function canvasEdgeBand(canvas, rect, edge) {
1336
+ const scaleX = canvas.width / rect.width;
1337
+ const scaleY = canvas.height / rect.height;
1338
+ const off = document.createElement("canvas");
1339
+ off.width = Math.max(1, Math.round(edge.width * scaleX));
1340
+ off.height = Math.max(1, Math.round(edge.height * scaleY));
1341
+ const ctx = off.getContext("2d");
1342
+ if (!ctx) return null;
1343
+ const sx = Math.max(0, Math.floor((edge.x - rect.left) * scaleX));
1344
+ const sy = Math.max(0, Math.floor((edge.y - rect.top) * scaleY));
1345
+ ctx.drawImage(canvas, sx, sy, off.width, off.height, 0, 0, off.width, off.height);
1346
+ return {
1347
+ data: ctx.getImageData(0, 0, off.width, off.height).data,
1348
+ width: off.width,
1349
+ height: off.height,
1350
+ };
1351
+ }
1352
+
1353
+ // Collapse the band across its thickness (brightest wins), then count steps along the edge:
1354
+ // luma over black plus alpha, compared two pixels back so anti-aliased glyph edges still step.
1355
+ function sharpSteps(band, vertical) {
1356
+ const length = vertical ? band.height : band.width;
1357
+ const thickness = vertical ? band.width : band.height;
1358
+ const levels = [];
1359
+ for (let along = 0; along < length; along++) {
1360
+ let luma = 0;
1361
+ let alpha = 0;
1362
+ for (let across = 0; across < thickness; across++) {
1363
+ const i = 4 * (vertical ? along * band.width + across : across * band.width + along);
1364
+ const a = band.data[i + 3];
1365
+ const l =
1366
+ (0.299 * band.data[i] + 0.587 * band.data[i + 1] + 0.114 * band.data[i + 2]) * (a / 255);
1367
+ luma = Math.max(luma, l);
1368
+ alpha = Math.max(alpha, a);
1369
+ }
1370
+ levels.push([luma, alpha]);
1371
+ }
1372
+ let steps = 0;
1373
+ for (let i = 2; i < levels.length; i++) {
1374
+ const [luma, alpha] = levels[i];
1375
+ const [backLuma, backAlpha] = levels[i - 2];
1376
+ if (Math.max(Math.abs(luma - backLuma), Math.abs(alpha - backAlpha)) < CANVAS_EDGE_STEP)
1377
+ continue;
1378
+ steps++;
1379
+ i += 2;
1380
+ }
1381
+ return steps;
1382
+ }
1383
+
1384
+ function canvasEdgeHasContent(canvas, rect, edge) {
1385
+ try {
1386
+ const band = canvasEdgeBand(canvas, rect, edge);
1387
+ return !!band && sharpSteps(band, edge.vertical) >= CANVAS_EDGE_MIN_STEPS;
1388
+ } catch {
1389
+ return false;
1390
+ }
1391
+ }
1392
+
1393
+ // ponytail: maps through the bounding box, exact for scale and translate; a rotated or skewed
1394
+ // canvas reads the wrong pixels. Add a matrix inverse if that bites.
1395
+ function canvasEdgeIssues(root, rootRect, time, tolerance) {
1396
+ const issues = [];
1397
+ for (const canvas of Array.from(root.querySelectorAll("canvas"))) {
1398
+ if (!isVisibleElement(canvas) || hasAllowOverflowFlag(canvas)) continue;
1399
+ if (!canvas.width || !canvas.height) continue;
1400
+ const rect = toRect(canvas.getBoundingClientRect());
1401
+ if (!rect.width || !rect.height) continue;
1402
+ const sides = coveredFrameEdges(rect, rootRect, tolerance)
1403
+ .filter((edge) => canvasEdgeHasContent(canvas, rect, edge))
1404
+ .map((edge) => edge.side);
1405
+ if (sides.length === 0) continue;
1406
+ issues.push({
1407
+ code: "canvas_content_at_edge",
1408
+ severity: "warning",
1409
+ time,
1410
+ selector: selectorFor(canvas),
1411
+ containerSelector: selectorFor(root),
1412
+ message: `Canvas content touches the frame edge (${sides.join(", ")}).`,
1413
+ rect,
1414
+ containerRect: rootRect,
1415
+ fixHint:
1416
+ "Keep drawn text inside the visible part of the canvas, or mark intentional full-bleed art with data-layout-allow-overflow.",
1417
+ });
1418
+ }
1419
+ return issues;
1420
+ }
1421
+
1422
+ // Soft prior only — the counterfactual attach test (below) is what makes detachment a finding.
1423
+ const CONNECTOR_NAME = /\b(conn(ector)?|arrow|edge|link|flow|wire)\b/i;
1424
+ const CONNECTOR_SKIP_CONTAINERS = "defs, marker, clipPath, mask, symbol, pattern";
1425
+
1426
+ function connectorNameFor(element) {
1427
+ const className =
1428
+ typeof element.className === "string" ? element.className : element.className.baseVal || "";
1429
+ return `${element.id || ""} ${className}`;
1430
+ }
1431
+
1432
+ function isConnectorPath(svg, path) {
1433
+ if (path.hasAttribute("marker-start") || path.hasAttribute("marker-end")) return true;
1434
+ return (
1435
+ CONNECTOR_NAME.test(connectorNameFor(svg)) || CONNECTOR_NAME.test(connectorNameFor(path))
1436
+ );
1437
+ }
1438
+
1439
+ // Raw `d`-space endpoints (no CTM), as authors paste screen coords into `d`, plus the path length.
1440
+ function pathUserEndpoints(path) {
1441
+ if (typeof path.getTotalLength !== "function" || typeof path.getPointAtLength !== "function") {
1442
+ return null;
1443
+ }
1444
+ let total;
1445
+ try {
1446
+ total = path.getTotalLength();
1447
+ } catch {
1448
+ return null;
1449
+ }
1450
+ if (!Number.isFinite(total) || total <= 0) return null;
1451
+ const start = path.getPointAtLength(0);
1452
+ const end = path.getPointAtLength(total);
1453
+ return { start: { x: start.x, y: start.y }, end: { x: end.x, y: end.y }, total };
1454
+ }
1455
+
1456
+ // Screen endpoints via getScreenCTM (viewBox, preserveAspectRatio, group transforms).
1457
+ function pathScreenEndpoints(svg, path, user) {
1458
+ if (
1459
+ !user ||
1460
+ typeof path.getScreenCTM !== "function" ||
1461
+ typeof svg.createSVGPoint !== "function"
1462
+ ) {
1463
+ return null;
1464
+ }
1465
+ const matrix = path.getScreenCTM();
1466
+ if (!matrix) return null;
1467
+ const toScreen = (local) => {
1468
+ const point = svg.createSVGPoint();
1469
+ point.x = local.x;
1470
+ point.y = local.y;
1471
+ const mapped = point.matrixTransform(matrix);
1472
+ return { x: mapped.x, y: mapped.y };
1473
+ };
1474
+ return { start: toScreen(user.start), end: toScreen(user.end) };
1475
+ }
1476
+
1477
+ function distanceToRect(point, rect) {
1478
+ const dx = Math.max(rect.left - point.x, 0, point.x - rect.right);
1479
+ const dy = Math.max(rect.top - point.y, 0, point.y - rect.bottom);
1480
+ return Math.sqrt(dx * dx + dy * dy);
1481
+ }
1482
+
1483
+ // Solid, compact elements a connector could plausibly anchor to.
1484
+ // Both tiers keep `element` so attachment identity is stable across containment vs near-miss.
1485
+ function connectorAnchorRects(root, rootRect) {
1486
+ const compact = [];
1487
+ const painted = [];
1488
+ const rootArea = rectArea(rootRect);
1489
+ for (const element of Array.from(root.querySelectorAll("*"))) {
1490
+ // Known blind spot: anchors living inside an SVG (<image>, foreignObject) are not counted.
1491
+ if (element.closest("svg") || !isVisibleElement(element)) continue;
1492
+ const opaque =
1493
+ RASTER_TAGS.has(element.tagName) || hasOpaqueBackground(getComputedStyle(element));
1494
+ if (!opaque && !textContentFor(element)) continue;
1495
+ const rect = toRect(element.getBoundingClientRect());
1496
+ const area = rectArea(rect);
1497
+ if (area < 400) continue;
1498
+ // Containment tier: large opaque targets only — a text-bearing wrapper contains its own diagram's endpoints.
1499
+ if (opaque && area <= rootArea * 0.6) painted.push({ rect, element });
1500
+ if (area <= rootArea * 0.15) compact.push({ rect, element });
1501
+ }
1502
+ return { compact, painted };
1503
+ }
1504
+
1505
+ // Attach near-miss tolerance (screen px) for both connector findings; not the glyph chord floor.
1506
+ function connectorAttachThreshold(rootRect) {
1507
+ return Math.max(32, Math.min(rootRect.width, rootRect.height) * 0.02);
1508
+ }
1509
+
1510
+ // The one connector enumeration for connector_detached and connector_orphan. Skips dash-hidden
1511
+ // shafts; reports `painted` for callers to gate on, and `chord`, the rendered span in screen px.
1512
+ function* connectorShafts(root) {
1513
+ for (const svg of Array.from(root.querySelectorAll("svg"))) {
1514
+ if (!isVisibleElement(svg)) continue;
1515
+ for (const path of Array.from(svg.querySelectorAll("path"))) {
1516
+ if (path.closest(CONNECTOR_SKIP_CONTAINERS)) continue;
1517
+ if (!isConnectorPath(svg, path)) continue;
1518
+ const user = pathUserEndpoints(path);
1519
+ if (!user || shaftDashHidden(path, user.total)) continue;
1520
+ const rendered = pathScreenEndpoints(svg, path, user);
1521
+ if (!rendered) continue;
1522
+ const chord = Math.hypot(
1523
+ rendered.end.x - rendered.start.x,
1524
+ rendered.end.y - rendered.start.y,
1525
+ );
1526
+ yield { svg, path, user, rendered, chord, painted: shaftIsPainted(path) };
1527
+ }
1528
+ }
1529
+ }
1530
+
1531
+ // Flag only the documented bug: rendered endpoints miss, but user-space-as-screen would attach.
1532
+ function connectorDetachmentIssues(root, rootRect, time) {
1533
+ const issues = [];
1534
+ let anchors = null;
1535
+ const threshold = connectorAttachThreshold(rootRect);
1536
+ const MIN_CONNECTOR_CHORD_PX = 8;
1537
+ for (const { svg, path, user, rendered, chord } of connectorShafts(root)) {
1538
+ if (chord < MIN_CONNECTOR_CHORD_PX) continue;
1539
+ if (anchors === null) anchors = connectorAnchorRects(root, rootRect);
1540
+ if (anchors.compact.length < 2) return issues;
1541
+ // Stable DOM identity across painted (inside) and compact (near-miss) tiers.
1542
+ const attachmentKey = (point) => {
1543
+ for (const anchor of anchors.painted) {
1544
+ if (!anchor.element.contains(svg) && distanceToRect(point, anchor.rect) === 0) {
1545
+ return anchor.element;
1546
+ }
1547
+ }
1548
+ for (const anchor of anchors.compact) {
1549
+ if (distanceToRect(point, anchor.rect) <= threshold) return anchor.element;
1550
+ }
1551
+ return null;
1552
+ };
1553
+ const attached = (point) => attachmentKey(point) !== null;
1554
+ // Half-attached as drawn is allowed; only full render-miss proceeds.
1555
+ if (attached(rendered.start) || attached(rendered.end)) continue;
1556
+ // Paste-into-`d` bug: both raw endpoints land on distinct anchors as screen pixels.
1557
+ const userStartKey = attachmentKey(user.start);
1558
+ const userEndKey = attachmentKey(user.end);
1559
+ const pasteBug = Boolean(userStartKey && userEndKey && userStartKey !== userEndKey);
1560
+ // Guessed marked shaft: both frames miss. Same-anchor grazes attach in user-space
1561
+ // and must stay skipped. Name-only decorative flow/arrow paths stay skipped.
1562
+ // 80px keeps short marker glyphs (chevrons, tips) out.
1563
+ const markedMiss =
1564
+ chord >= 80 &&
1565
+ !userStartKey &&
1566
+ !userEndKey &&
1567
+ (path.hasAttribute("marker-start") || path.hasAttribute("marker-end"));
1568
+ if (!pasteBug && !markedMiss) continue;
1569
+ const gap = Math.round(
1570
+ Math.min(
1571
+ Math.min(...anchors.compact.map((a) => distanceToRect(rendered.start, a.rect))),
1572
+ Math.min(...anchors.compact.map((a) => distanceToRect(rendered.end, a.rect))),
1573
+ ),
1574
+ );
1575
+ issues.push({
1576
+ code: "connector_detached",
1577
+ severity: "warning",
1578
+ time,
1579
+ selector: selectorFor(path),
1580
+ containerSelector: selectorFor(svg),
1581
+ message: pasteBug
1582
+ ? `Connector path endpoints render ${gap}px from the nearest anchorable element, but the path's user-space coordinates would attach if read as screen pixels — screen/viewport numbers were likely written into SVG \`d\` without inverting the CTM.`
1583
+ : `Connector path endpoints render ${gap}px from the nearest anchorable element — a marked shaft that meets no node.`,
1584
+ rect: toRect({
1585
+ left: Math.min(rendered.start.x, rendered.end.x),
1586
+ top: Math.min(rendered.start.y, rendered.end.y),
1587
+ right: Math.max(rendered.start.x, rendered.end.x),
1588
+ bottom: Math.max(rendered.start.y, rendered.end.y),
1589
+ width: Math.abs(rendered.end.x - rendered.start.x),
1590
+ height: Math.abs(rendered.end.y - rendered.start.y),
1591
+ }),
1592
+ fixHint: pasteBug
1593
+ ? "Convert measured screen coordinates into the SVG's user space (subtract the SVG rect / invert getScreenCTM) before writing path `d`, and keep the SVG a direct child of the stage."
1594
+ : "Measure the settled node boxes and write `d` in the SVG's user space (invert getScreenCTM), or grow a layout-owned shaft from the source node.",
1595
+ });
1596
+ }
1597
+ return issues;
1598
+ }
1599
+
1600
+ function shaftIsPainted(path) {
1601
+ if (IGNORE_TAGS.has(path.tagName) || hasIgnoreFlag(path)) return false;
1602
+ const style = getComputedStyle(path);
1603
+ if (
1604
+ style.display === "none" ||
1605
+ style.visibility === "hidden" ||
1606
+ style.visibility === "collapse"
1607
+ ) {
1608
+ return false;
1609
+ }
1610
+ return opacityChain(path) >= 0.2;
1611
+ }
1612
+
1613
+ // True when the visible dash window [offset, offset + length] sits in one gap (an unstarted
1614
+ // draw-on), allowing 10% overlap. Zero-length dashes paint only with a round or square linecap.
1615
+ // Distances are in `pathLength` units when set; a `pathLength` resolving to 0 paints solid.
1616
+ function shaftDashHidden(path, total) {
1617
+ const style = getComputedStyle(path);
1618
+ const dashes = dashArrayLengths(style.strokeDasharray, path);
1619
+ if (dashes === null) return false;
1620
+ const authored = path.pathLength?.baseVal; // SVGAnimatedNumber; 0 when unset or unparseable
1621
+ if (authored === 0 && path.hasAttribute("pathLength")) return false;
1622
+ const dotsPaint = (style.strokeLinecap || "butt") !== "butt";
1623
+ const dashPaints = (index) => index % 2 === 0 && (dashes[index] > 0 || dotsPaint);
1624
+ if (!dashes.some((_, index) => dashPaints(index))) return true; // only butt-capped dots
1625
+ const period = dashes.reduce((sum, dash) => sum + dash, 0);
1626
+ const length = authored > 0 ? authored : total;
1627
+ const offset = dashLength(style.strokeDashoffset, path); // unparseable reads as 0
1628
+ // Wrap into [0, period); adding the period only to negatives keeps `0.9 % 2` exact.
1629
+ const wrapped = Number.isFinite(offset) ? offset % period : 0;
1630
+ const start = wrapped < 0 ? wrapped + period : wrapped;
1631
+ const end = start + length;
1632
+ let painted = 0;
1633
+ let segmentStart = 0;
1634
+ // Two periods cover any window that starts inside the first.
1635
+ for (let i = 0; i < dashes.length * 2; i++) {
1636
+ const segmentEnd = segmentStart + dashes[i % dashes.length];
1637
+ if (dashPaints(i % dashes.length)) {
1638
+ if (segmentStart >= start && segmentEnd <= end) return false;
1639
+ painted += Math.max(0, Math.min(segmentEnd, end) - Math.max(segmentStart, start));
1640
+ }
1641
+ segmentStart = segmentEnd;
1642
+ }
1643
+ return painted <= length * 0.1;
1644
+ }
1645
+
1646
+ // Computed `stroke-dasharray` as an even-length list of user-unit lengths, or null when the
1647
+ // stroke is solid: `none`, an all-zero list, or any negative/unparseable entry (which the spec
1648
+ // renders as `none`). Odd lists repeat, per spec.
1649
+ function dashArrayLengths(value, path) {
1650
+ const text = String(value || "none").trim();
1651
+ if (text === "none") return null;
1652
+ const lengths = text.split(/[\s,]+/).map((token) => dashLength(token, path));
1653
+ if (lengths.some((length) => !Number.isFinite(length) || length < 0)) return null;
1654
+ if (lengths.every((length) => length === 0)) return null;
1655
+ return lengths.length % 2 === 0 ? lengths : lengths.concat(lengths);
1656
+ }
1657
+
1658
+ // One dash length in user units. Computed lengths are already px; a percentage is relative to
1659
+ // the normalised diagonal of the owning SVG viewport (viewBox when set, else the layout box).
1660
+ function dashLength(token, path) {
1661
+ const text = String(token).trim();
1662
+ const value = Number.parseFloat(text);
1663
+ if (!Number.isFinite(value) || !text.endsWith("%")) return value;
1664
+ const svg = path.ownerSVGElement;
1665
+ if (!svg) return NaN;
1666
+ const box = svg.viewBox && svg.viewBox.baseVal;
1667
+ const { width, height } =
1668
+ box && box.width > 0 && box.height > 0 ? box : svg.getBoundingClientRect();
1669
+ return (value / 100) * (Math.hypot(width, height) / Math.SQRT2);
1670
+ }
1671
+
1672
+ function connectorEndpointCandidates(root, rootRect) {
1673
+ const candidates = [];
1674
+ const rootArea = rectArea(rootRect);
1675
+ for (const element of Array.from(root.querySelectorAll("*"))) {
1676
+ if (element.closest("svg") || IGNORE_TAGS.has(element.tagName) || hasIgnoreFlag(element))
1677
+ continue;
1678
+ const style = getComputedStyle(element);
1679
+ const opaque = RASTER_TAGS.has(element.tagName) || hasOpaqueBackground(style);
1680
+ if (!opaque && !textContentFor(element)) continue;
1681
+ const rect = toRect(element.getBoundingClientRect());
1682
+ const area = rectArea(rect);
1683
+ if (area < 400 || area > rootArea * 0.15) continue;
1684
+ candidates.push({ rect, element });
1685
+ }
1686
+ return candidates;
1687
+ }
1688
+
1689
+ function connectorOrphanIssues(root, rootRect, time) {
1690
+ const issues = [];
1691
+ let candidates = null;
1692
+ const threshold = connectorAttachThreshold(rootRect);
1693
+ for (const { svg, path, rendered, chord, painted } of connectorShafts(root)) {
1694
+ if (!painted) continue;
1695
+ if (chord < 80) continue;
1696
+ if (candidates === null) candidates = connectorEndpointCandidates(root, rootRect);
1697
+ const dark = [];
1698
+ for (const point of [rendered.start, rendered.end]) {
1699
+ let best = null;
1700
+ let attached = false;
1701
+ for (const candidate of candidates) {
1702
+ const gap = distanceToRect(point, candidate.rect);
1703
+ if (gap > threshold) continue;
1704
+ if (isVisibleElement(candidate.element)) {
1705
+ attached = true;
1706
+ break;
1707
+ }
1708
+ if (best === null || gap < best.gap) best = { gap, candidate };
1709
+ }
1710
+ if (!attached && best !== null) dark.push(best.candidate);
1711
+ }
1712
+ if (dark.length === 0) continue;
1713
+ issues.push({
1714
+ code: "connector_orphan",
1715
+ severity: "warning",
1716
+ time,
1717
+ selector: selectorFor(path),
1718
+ containerSelector: selectorFor(svg),
1719
+ message: `Connector shaft is visible while ${dark.length === 2 ? "both endpoints are" : `its endpoint ${selectorFor(dark[0].element)} is`} not on stage.`,
1720
+ rect: toRect({
1721
+ left: Math.min(rendered.start.x, rendered.end.x),
1722
+ top: Math.min(rendered.start.y, rendered.end.y),
1723
+ right: Math.max(rendered.start.x, rendered.end.x),
1724
+ bottom: Math.max(rendered.start.y, rendered.end.y),
1725
+ width: Math.abs(rendered.end.x - rendered.start.x),
1726
+ height: Math.abs(rendered.end.y - rendered.start.y),
1727
+ }),
1728
+ fixHint:
1729
+ "Show the shaft only after both ends are on, and hide it with the earlier exit. Do not give the line its own clock.",
1730
+ });
1731
+ }
1732
+ return issues;
1733
+ }
1734
+
1735
+ function candidateAnchor(element) {
1736
+ const dataAttributes = {};
1737
+ for (const attribute of Array.from(element.attributes)) {
1738
+ if (attribute.name.startsWith("data-")) dataAttributes[attribute.name] = attribute.value;
1739
+ }
1740
+ const source = element
1741
+ .closest("[data-composition-file]")
1742
+ ?.getAttribute("data-composition-file");
1743
+ return {
1744
+ selector: uniqueSelectorFor(element),
1745
+ dataAttributes,
1746
+ sourceFile: source || "index.html",
1747
+ };
1748
+ }
1749
+
1750
+ function geometryCandidate(element, kind, rect, elementRect, rootRect, tolerance) {
1751
+ const tag = element.tagName.toLowerCase();
1752
+ const text = kind === "text" ? textContentFor(element, true) : tag;
1753
+ const overflow = kind === "media" ? overflowFor(elementRect, rootRect, tolerance) : null;
1754
+ return {
1755
+ kind,
1756
+ tag,
1757
+ text,
1758
+ rect,
1759
+ elementRect,
1760
+ ...candidateAnchor(element),
1761
+ ...(overflow ? { overflow } : {}),
1762
+ };
1763
+ }
1764
+
1765
+ window.__hyperframesGeometryCandidates = function collectGeometryCandidates(options) {
1766
+ const includeText = options?.text === true;
1767
+ const includeMedia = options?.media === true;
1768
+ if (!includeText && !includeMedia) return [];
1769
+ const tolerance = typeof options?.tolerance === "number" ? options.tolerance : 2;
1770
+ const root =
1771
+ document.querySelector("[data-composition-id][data-width][data-height]") ||
1772
+ document.querySelector("[data-composition-id]") ||
1773
+ document.body;
1774
+ const rootRect = rootRectFor(root);
1775
+ const candidates = [];
1776
+ for (const element of Array.from(document.querySelectorAll("body *"))) {
1777
+ if (element.closest('[data-composition-id="captions"], .caption-layer, #caption-stage')) {
1778
+ continue;
1779
+ }
1780
+ if (!isVisibleElement(element, 0.05, false)) continue;
1781
+ const elementRect = toRect(element.getBoundingClientRect());
1782
+ if (includeText && hasOwnTextCandidate(element, true) && !hasAllowCaptionZoneFlag(element)) {
1783
+ const rect = textRectFor(element, true);
1784
+ if (rect) {
1785
+ candidates.push(
1786
+ geometryCandidate(element, "text", rect, elementRect, rootRect, tolerance),
1787
+ );
1788
+ }
1789
+ }
1790
+ if (includeMedia && FRAME_MEDIA_TAGS.has(element.tagName.toUpperCase())) {
1791
+ candidates.push(
1792
+ geometryCandidate(element, "media", elementRect, elementRect, rootRect, tolerance),
1793
+ );
1794
+ }
1795
+ }
1796
+ return candidates;
1797
+ };
1798
+
1799
+ window.__hyperframesLayoutAudit = function auditLayout(options) {
1800
+ const time = options && typeof options.time === "number" ? options.time : 0;
1801
+ const tolerance =
1802
+ options && typeof options.tolerance === "number" ? Math.max(0, options.tolerance) : 2;
1803
+ const proseCoverageFloor =
1804
+ options && typeof options.proseCoverageFloor === "number"
1805
+ ? Math.min(1, Math.max(0, options.proseCoverageFloor))
1806
+ : DEFAULT_PROSE_COVERAGE_FLOOR;
1807
+ const root =
1808
+ document.querySelector("[data-composition-id][data-width][data-height]") ||
1809
+ document.querySelector("[data-composition-id]") ||
1810
+ document.body;
1811
+ const rootRect = rootRectFor(root);
1812
+ const elements = Array.from(root.querySelectorAll("*")).filter((element) =>
1813
+ isVisibleElement(element, 0.05),
1814
+ );
1815
+ const issues = [];
1816
+
1817
+ const restoreHits = restoreHitTesting(root);
1818
+ try {
1819
+ for (const element of elements) {
1820
+ if (!hasOwnTextCandidate(element)) continue;
1821
+ const clipped = clippedTextIssue(element, time, tolerance);
1822
+ if (clipped) issues.push(clipped);
1823
+ issues.push(...textOverflowIssues(element, root, rootRect, time, tolerance, clipped));
1824
+ const occluded = occludedTextIssue(element, time, proseCoverageFloor);
1825
+ if (occluded) issues.push(occluded);
1826
+ const invisible = invisibleTextIssue(element, time);
1827
+ if (invisible) issues.push(invisible);
1828
+ }
1829
+ } finally {
1830
+ restoreHits();
1831
+ }
1832
+
1833
+ issues.push(...containerOverflowIssues(root, time, tolerance));
1834
+ issues.push(...contentOverlapIssues(root, time));
1835
+ const escaped = escapedContainerIssues(root, time);
1836
+ issues.push(...escaped.issues);
1837
+ issues.push(...panelOutOfCanvasIssues(root, rootRect, time, tolerance, escaped.flagged));
1838
+ issues.push(...canvasEdgeIssues(root, rootRect, time, tolerance));
1839
+ issues.push(...connectorDetachmentIssues(root, rootRect, time));
1840
+ issues.push(...connectorOrphanIssues(root, rootRect, time));
1841
+ return issues;
1842
+ };
1843
+
1844
+ // Reruns only the overlap detector (same threshold, no new surface) on a fine grid for the dense motion re-sampling pass.
1845
+ window.__hyperframesOverlapAudit = function auditOverlap(options) {
1846
+ const time = options && typeof options.time === "number" ? options.time : 0;
1847
+ const root =
1848
+ document.querySelector("[data-composition-id][data-width][data-height]") ||
1849
+ document.querySelector("[data-composition-id]") ||
1850
+ document.body;
1851
+ return contentOverlapIssues(root, time);
1852
+ };
1853
+
1854
+ // The frozen-sweep fingerprint (window.__hyperframesLayoutGeometry, #U10 in
1855
+ // checkPipeline.ts) lives in motion-signature.browser.js, the single owner
1856
+ // of "what counts as motion" shared with motion-sample.browser.js's
1857
+ // liveness signature.
1858
+
1859
+ // Rotation-pivot sampling (rotation_pivot_drift). Per sample, report every
1860
+ // rotatable candidate's bbox center, size, and current rotation angle. Node
1861
+ // accumulates these across the seek grid and, after the run, flags any
1862
+ // element that spins (angle varies) while its bbox CENTER drifts — the
1863
+ // signature of a wrong transformOrigin/svgOrigin (spokes swinging off-axis
1864
+ // instead of spinning in place). Single frame can't tell spin from pivot
1865
+ // drift, so this is a cross-sample finder, not a per-sample one.
1866
+ function rotationAngleDeg(transform) {
1867
+ if (!transform || transform === "none") return null;
1868
+ const match = transform.match(/matrix(3d)?\(([^)]+)\)/);
1869
+ if (!match) return null;
1870
+ const values = match[2].split(",").map((part) => Number.parseFloat(part));
1871
+ // matrix(a,b,c,d,e,f) → a=values[0], b=values[1]. matrix3d shares the same
1872
+ // leading two entries for the in-plane 2D rotation component.
1873
+ const a = values[0];
1874
+ const b = values[1];
1875
+ if (!Number.isFinite(a) || !Number.isFinite(b)) return null;
1876
+ return (Math.atan2(b, a) * 180) / Math.PI;
1877
+ }
1878
+
1879
+ window.__hyperframesRotationSample = function collectRotationSample() {
1880
+ const root =
1881
+ document.querySelector("[data-composition-id][data-width][data-height]") ||
1882
+ document.querySelector("[data-composition-id]") ||
1883
+ document.body;
1884
+ const samples = [];
1885
+ // Cap the candidate set so a pathological composition can't blow up the
1886
+ // per-sample payload; transformed elements above a minimum area only.
1887
+ const CANDIDATE_CAP = 200;
1888
+ for (const element of Array.from(root.querySelectorAll("*"))) {
1889
+ if (samples.length >= CANDIDATE_CAP) break;
1890
+ // Intended orbits/satellites opt out — their bbox center is SUPPOSED to
1891
+ // travel, so a drift finding there is a false positive.
1892
+ if (element.closest("[data-layout-allow-orbit]")) continue;
1893
+ if (!isVisibleElement(element, 0.05)) continue;
1894
+ const angle = rotationAngleDeg(getComputedStyle(element).transform);
1895
+ if (angle === null) continue; // identity / untransformed — not a candidate
1896
+ const box = element.getBoundingClientRect();
1897
+ if (box.width * box.height <= 400) continue;
1898
+ samples.push({
1899
+ selector: selectorFor(element),
1900
+ cx: round(box.left + box.width / 2),
1901
+ cy: round(box.top + box.height / 2),
1902
+ w: round(box.width),
1903
+ h: round(box.height),
1904
+ angle: round(angle),
1905
+ });
1906
+ }
1907
+ return samples;
1908
+ };
1909
+
1910
+ // Needle-pivot sampling (off_pivot_rotation). A gauge/clock/radar pointer
1911
+ // whose center-of-rotation sits far from the dial hub. bbox-intrinsic measures
1912
+ // can't tell a correct sweep from a broken one (a base-pivoted needle's bbox
1913
+ // center orbits either way), so this records two MATERIAL points on each
1914
+ // elongated rotating SVG figure — mapped through getScreenCTM so the actual
1915
+ // rendered transform is honored regardless of svgOrigin/transform-origin — and
1916
+ // the dial's static hub (the point shared by the most non-rotating circles).
1917
+ // The pipeline fits a rotation to the material-point trajectories to recover
1918
+ // the real center-of-rotation and flags it when it drifts off that hub.
1919
+ function ctmRotationDeg(ctm) {
1920
+ if (!ctm) return null;
1921
+ return (Math.atan2(ctm.b, ctm.a) * 180) / Math.PI;
1922
+ }
1923
+
1924
+ function ctmScale(ctm) {
1925
+ return Math.hypot(ctm.a, ctm.b);
1926
+ }
1927
+
1928
+ function mapPoint(svg, ctm, x, y) {
1929
+ const point = svg.createSVGPoint();
1930
+ point.x = x;
1931
+ point.y = y;
1932
+ const mapped = point.matrixTransform(ctm);
1933
+ return { x: mapped.x, y: mapped.y };
1934
+ }
1935
+
1936
+ // Walks up to (and including) the composition root, NOT just the owner <svg>:
1937
+ // an element spun by a div ancestor above its svg must not be mistaken for a
1938
+ // static hub anchor (else a lone rotating arc becomes its own dial center).
1939
+ function hasRotatedAncestor(element, root) {
1940
+ let node = element;
1941
+ while (node) {
1942
+ const angle = rotationAngleDeg(getComputedStyle(node).transform);
1943
+ if (angle !== null && Math.abs(angle) > 1) return true;
1944
+ if (node === root) break;
1945
+ node = node.parentElement;
1946
+ }
1947
+ return false;
1948
+ }
1949
+
1950
+ // KEEP IN SYNC with `fitCircle` in packages/cli/src/utils/checkPipeline.ts —
1951
+ // this browser copy resolves arc-drawn dial hubs and is injected as a raw
1952
+ // string (no import across the puppeteer boundary), so the Kåsa math is
1953
+ // intentionally duplicated per-language. Any change must land in both copies.
1954
+ function fitCirclePoints(points) {
1955
+ const count = points.length;
1956
+ if (count < 3) return null;
1957
+ const meanX = points.reduce((sum, p) => sum + p.x, 0) / count;
1958
+ const meanY = points.reduce((sum, p) => sum + p.y, 0) / count;
1959
+ let suu = 0,
1960
+ svv = 0,
1961
+ suv = 0,
1962
+ suuu = 0,
1963
+ svvv = 0,
1964
+ suvv = 0,
1965
+ svuu = 0;
1966
+ for (const point of points) {
1967
+ const u = point.x - meanX;
1968
+ const v = point.y - meanY;
1969
+ suu += u * u;
1970
+ svv += v * v;
1971
+ suv += u * v;
1972
+ suuu += u * u * u;
1973
+ svvv += v * v * v;
1974
+ suvv += u * v * v;
1975
+ svuu += v * u * u;
1976
+ }
1977
+ const det = suu * svv - suv * suv;
1978
+ if (Math.abs(det) < 1e-6) return null;
1979
+ const uc = (((suuu + suvv) / 2) * svv - ((svvv + svuu) / 2) * suv) / det;
1980
+ const vc = (((svvv + svuu) / 2) * suu - ((suuu + suvv) / 2) * suv) / det;
1981
+ const cx = uc + meanX;
1982
+ const cy = vc + meanY;
1983
+ const radius = Math.sqrt(uc * uc + vc * vc + (suu + svv) / count);
1984
+ let squaredError = 0;
1985
+ for (const point of points) {
1986
+ const delta = Math.hypot(point.x - cx, point.y - cy) - radius;
1987
+ squaredError += delta * delta;
1988
+ }
1989
+ return { cx, cy, radius, residual: Math.sqrt(squaredError / count) };
1990
+ }
1991
+
1992
+ // Fallback for dials drawn as arc <path> rather than <circle> rings: sample
1993
+ // the largest static, near-circular path and recover its arc center.
1994
+ function arcHubForSvg(svg, root) {
1995
+ let best = null;
1996
+ for (const path of Array.from(svg.querySelectorAll("path"))) {
1997
+ if (hasRotatedAncestor(path, root)) continue;
1998
+ if (typeof path.getTotalLength !== "function") continue;
1999
+ const total = path.getTotalLength();
2000
+ if (total < 200) continue;
2001
+ const ctm = path.getScreenCTM();
2002
+ if (!ctm) continue;
2003
+ const points = [];
2004
+ for (let i = 0; i <= 16; i++) {
2005
+ const local = path.getPointAtLength((total * i) / 16);
2006
+ points.push(mapPoint(svg, ctm, local.x, local.y));
2007
+ }
2008
+ const fit = fitCirclePoints(points);
2009
+ if (!fit || fit.radius < 40) continue;
2010
+ if (fit.residual > 0.05 * fit.radius) continue;
2011
+ if (!best || fit.radius > best.radius) best = fit;
2012
+ }
2013
+ return best ? { hx: best.cx, hy: best.cy, hr: best.radius, count: 2 } : null;
2014
+ }
2015
+
2016
+ function dialHubForSvg(svg, root) {
2017
+ const centers = [];
2018
+ for (const circle of Array.from(svg.querySelectorAll("circle"))) {
2019
+ if (hasRotatedAncestor(circle, root)) continue;
2020
+ const ctm = circle.getScreenCTM();
2021
+ if (!ctm) continue;
2022
+ const cx = Number.parseFloat(circle.getAttribute("cx") || "0");
2023
+ const cy = Number.parseFloat(circle.getAttribute("cy") || "0");
2024
+ const center = mapPoint(svg, ctm, cx, cy);
2025
+ const radius = Number.parseFloat(circle.getAttribute("r") || "0") * ctmScale(ctm);
2026
+ centers.push({ x: center.x, y: center.y, radius });
2027
+ }
2028
+ let best = null;
2029
+ for (const anchor of centers) {
2030
+ const cluster = centers.filter(
2031
+ (other) => Math.hypot(other.x - anchor.x, other.y - anchor.y) <= 8,
2032
+ );
2033
+ if (!best || cluster.length > best.cluster.length) best = { anchor, cluster };
2034
+ }
2035
+ if (best && best.cluster.length >= 2) {
2036
+ const count = best.cluster.length;
2037
+ const hx = best.cluster.reduce((sum, item) => sum + item.x, 0) / count;
2038
+ const hy = best.cluster.reduce((sum, item) => sum + item.y, 0) / count;
2039
+ const hr = best.cluster.reduce((max, item) => Math.max(max, item.radius), 0);
2040
+ return { hx, hy, hr, count };
2041
+ }
2042
+ return arcHubForSvg(svg, root);
2043
+ }
2044
+
2045
+ window.__hyperframesOffPivotRotationSample = function collectOffPivotRotationSample() {
2046
+ const root =
2047
+ document.querySelector("[data-composition-id][data-width][data-height]") ||
2048
+ document.querySelector("[data-composition-id]") ||
2049
+ document.body;
2050
+ const samples = [];
2051
+ const hubCache = new Map();
2052
+ const CANDIDATE_CAP = 60;
2053
+ for (const element of Array.from(
2054
+ root.querySelectorAll("path, polygon, line, rect, polyline, g"),
2055
+ )) {
2056
+ if (samples.length >= CANDIDATE_CAP) break;
2057
+ const svg = element.ownerSVGElement;
2058
+ if (!svg || typeof element.getBBox !== "function") continue;
2059
+ if (element.closest("[data-layout-allow-orbit]")) continue;
2060
+ if (!isVisibleElement(element, 0.05)) continue;
2061
+ const ctm = element.getScreenCTM();
2062
+ const angle = ctmRotationDeg(ctm);
2063
+ if (ctm === null || angle === null) continue;
2064
+ let bbox;
2065
+ try {
2066
+ bbox = element.getBBox();
2067
+ } catch {
2068
+ continue;
2069
+ }
2070
+ const long = Math.max(bbox.width, bbox.height);
2071
+ const short = Math.min(bbox.width, bbox.height);
2072
+ if (short <= 0 || long / short < 3 || long < 40) continue;
2073
+ const vertical = bbox.height >= bbox.width;
2074
+ const midMajor = vertical ? bbox.x + bbox.width / 2 : bbox.y + bbox.height / 2;
2075
+ const a = vertical
2076
+ ? mapPoint(svg, ctm, midMajor, bbox.y)
2077
+ : mapPoint(svg, ctm, bbox.x, midMajor);
2078
+ const b = vertical
2079
+ ? mapPoint(svg, ctm, midMajor, bbox.y + bbox.height)
2080
+ : mapPoint(svg, ctm, bbox.x + bbox.width, midMajor);
2081
+ let hub = hubCache.get(svg);
2082
+ if (hub === undefined) {
2083
+ hub = dialHubForSvg(svg, root);
2084
+ hubCache.set(svg, hub);
2085
+ }
2086
+ samples.push({
2087
+ selector: selectorFor(element),
2088
+ ax: round(a.x),
2089
+ ay: round(a.y),
2090
+ bx: round(b.x),
2091
+ by: round(b.y),
2092
+ len: round(Math.hypot(b.x - a.x, b.y - a.y)),
2093
+ angle: round(angle),
2094
+ hx: hub ? round(hub.hx) : null,
2095
+ hy: hub ? round(hub.hy) : null,
2096
+ hr: hub ? round(hub.hr) : null,
2097
+ hubCount: hub ? hub.count : 0,
2098
+ });
2099
+ }
2100
+ return samples;
2101
+ };
2102
+ })();