@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.
- package/LICENSE +190 -0
- package/NOTICE +54 -0
- package/README.md +23 -0
- package/bin/ignix-editor.mjs +8 -0
- package/package.json +53 -0
- package/patches/index.mjs +53 -0
- package/scripts/build-ffmpeg-macos.sh +35 -0
- package/scripts/build-whisper-macos.sh +21 -0
- package/scripts/vendor.mjs +41 -0
- package/skills/ignix-editor/SKILL.md +132 -0
- package/src/audio.mjs +31 -0
- package/src/binaries.json +46 -0
- package/src/binaries.mjs +83 -0
- package/src/cli.mjs +254 -0
- package/src/prepare.mjs +74 -0
- package/src/runtime/ignix-frame.js +92 -0
- package/src/transcribe.mjs +71 -0
- package/src/validate.mjs +71 -0
- package/templates/blank/index.html +41 -0
- package/vendor/hyperframes/README.md +181 -0
- package/vendor/hyperframes/UPSTREAM.json +12 -0
- package/vendor/hyperframes/bin/hyperframes-localize-fonts.mjs +12 -0
- package/vendor/hyperframes/bin/hyperframes.mjs +11 -0
- package/vendor/hyperframes/dist/add-LR3ZR2A5.js +79 -0
- package/vendor/hyperframes/dist/agentPromptGenerator-VIIPZCJ3.js +143 -0
- package/vendor/hyperframes/dist/animatedGifPrep-P2T6YM7Z.js +34 -0
- package/vendor/hyperframes/dist/asset-BE2DOV72.js +26 -0
- package/vendor/hyperframes/dist/assetCataloger-VBLPHX24.js +340 -0
- package/vendor/hyperframes/dist/auth-RCQB2ZT7.js +60 -0
- package/vendor/hyperframes/dist/autoUpdate-RJAX3DAZ.js +268 -0
- package/vendor/hyperframes/dist/backgroundChecks-H2LCUIMJ.js +61 -0
- package/vendor/hyperframes/dist/backgroundChecksWorker.js +26 -0
- package/vendor/hyperframes/dist/batchRender-FT7AP7XH.js +355 -0
- package/vendor/hyperframes/dist/beat-analyzer.global.js +338 -0
- package/vendor/hyperframes/dist/beats-Y2EOP72P.js +309 -0
- package/vendor/hyperframes/dist/benchmark-MZ7GY3JW.js +231 -0
- package/vendor/hyperframes/dist/browser-HUTAMRQE.js +237 -0
- package/vendor/hyperframes/dist/bundleWithLocalizedFonts-5QOPOB5D.js +52 -0
- package/vendor/hyperframes/dist/capture-RLZA6EMR.js +4079 -0
- package/vendor/hyperframes/dist/capture-SZZ4IYJK.js +290 -0
- package/vendor/hyperframes/dist/captureFile-LYOJOUIE.js +15 -0
- package/vendor/hyperframes/dist/captureTimeout-PA4FYWT5.js +27 -0
- package/vendor/hyperframes/dist/catalog-KYU55EWR.js +1001 -0
- package/vendor/hyperframes/dist/check-TCG4PXLR.js +1697 -0
- package/vendor/hyperframes/dist/checkBrowser-RPMO4OW5.js +1032 -0
- package/vendor/hyperframes/dist/chunk-2EAXUCCE.js +88 -0
- package/vendor/hyperframes/dist/chunk-2EGFTPWD.js +1077 -0
- package/vendor/hyperframes/dist/chunk-2PSPARWP.js +6058 -0
- package/vendor/hyperframes/dist/chunk-2YN64IN5.js +5747 -0
- package/vendor/hyperframes/dist/chunk-3GXQPCJC.js +682 -0
- package/vendor/hyperframes/dist/chunk-3ZP5DXE7.js +15 -0
- package/vendor/hyperframes/dist/chunk-4NWOGZZS.js +1990 -0
- package/vendor/hyperframes/dist/chunk-4PFZPP2N.js +587 -0
- package/vendor/hyperframes/dist/chunk-4THIAPBL.js +128 -0
- package/vendor/hyperframes/dist/chunk-4YYJOFKS.js +28 -0
- package/vendor/hyperframes/dist/chunk-54HJ63Q2.js +29 -0
- package/vendor/hyperframes/dist/chunk-5ETJX6WM.js +89 -0
- package/vendor/hyperframes/dist/chunk-5IAKAQA7.js +389 -0
- package/vendor/hyperframes/dist/chunk-5Z63TBTX.js +37 -0
- package/vendor/hyperframes/dist/chunk-6BEFEWFB.js +25 -0
- package/vendor/hyperframes/dist/chunk-6CSYCFQV.js +68 -0
- package/vendor/hyperframes/dist/chunk-6CTIGJY7.js +154 -0
- package/vendor/hyperframes/dist/chunk-7HG4BFIN.js +93 -0
- package/vendor/hyperframes/dist/chunk-7JZ4FXP2.js +118 -0
- package/vendor/hyperframes/dist/chunk-A2Z4Z4VL.js +2961 -0
- package/vendor/hyperframes/dist/chunk-AMKC3AXA.js +25 -0
- package/vendor/hyperframes/dist/chunk-AVUUN2XD.js +4847 -0
- package/vendor/hyperframes/dist/chunk-AWDFDFI3.js +46 -0
- package/vendor/hyperframes/dist/chunk-AWNBM3MZ.js +94 -0
- package/vendor/hyperframes/dist/chunk-BXCMXMJQ.js +73 -0
- package/vendor/hyperframes/dist/chunk-BZ56MTDX.js +6 -0
- package/vendor/hyperframes/dist/chunk-CFKU7HTK.js +171 -0
- package/vendor/hyperframes/dist/chunk-CWPVM2QV.js +308 -0
- package/vendor/hyperframes/dist/chunk-DPVXBVLL.js +31292 -0
- package/vendor/hyperframes/dist/chunk-DQBY7FRR.js +397 -0
- package/vendor/hyperframes/dist/chunk-E6RPWJVC.js +213 -0
- package/vendor/hyperframes/dist/chunk-EISRN4E6.js +294 -0
- package/vendor/hyperframes/dist/chunk-EOVYQ3IG.js +143 -0
- package/vendor/hyperframes/dist/chunk-F3WZGU7C.js +479 -0
- package/vendor/hyperframes/dist/chunk-FCSIGXXP.js +366 -0
- package/vendor/hyperframes/dist/chunk-FI4I7CME.js +56 -0
- package/vendor/hyperframes/dist/chunk-FLWXXDNS.js +15 -0
- package/vendor/hyperframes/dist/chunk-GK4AIFIH.js +11171 -0
- package/vendor/hyperframes/dist/chunk-GMECXFAN.js +57 -0
- package/vendor/hyperframes/dist/chunk-GULOKZLI.js +155 -0
- package/vendor/hyperframes/dist/chunk-H4YZZ2VQ.js +68 -0
- package/vendor/hyperframes/dist/chunk-H6QMKUYA.js +477 -0
- package/vendor/hyperframes/dist/chunk-HBBJFK6I.js +273 -0
- package/vendor/hyperframes/dist/chunk-HRUGC23L.js +212 -0
- package/vendor/hyperframes/dist/chunk-HVW7ABXD.js +19 -0
- package/vendor/hyperframes/dist/chunk-HW6OYASV.js +344 -0
- package/vendor/hyperframes/dist/chunk-HWKCIWQT.js +3578 -0
- package/vendor/hyperframes/dist/chunk-I465DBM6.js +1214 -0
- package/vendor/hyperframes/dist/chunk-IGLEPPTN.js +24 -0
- package/vendor/hyperframes/dist/chunk-IGNAAHS3.js +433 -0
- package/vendor/hyperframes/dist/chunk-IHFUKJOR.js +85 -0
- package/vendor/hyperframes/dist/chunk-IQ3OUQ5Z.js +213 -0
- package/vendor/hyperframes/dist/chunk-ISN5GRYI.js +20 -0
- package/vendor/hyperframes/dist/chunk-IUZFXWSV.js +446 -0
- package/vendor/hyperframes/dist/chunk-JH556Q42.js +647 -0
- package/vendor/hyperframes/dist/chunk-JTHMTOMQ.js +601 -0
- package/vendor/hyperframes/dist/chunk-K2EQN4VU.js +405 -0
- package/vendor/hyperframes/dist/chunk-K2FTXJYH.js +16044 -0
- package/vendor/hyperframes/dist/chunk-K4WIOZFP.js +32 -0
- package/vendor/hyperframes/dist/chunk-K4WKGXRC.js +18 -0
- package/vendor/hyperframes/dist/chunk-KBJUXVOJ.js +172 -0
- package/vendor/hyperframes/dist/chunk-KC5XXX3X.js +590 -0
- package/vendor/hyperframes/dist/chunk-KNU5COPB.js +29 -0
- package/vendor/hyperframes/dist/chunk-KZI44OPA.js +185 -0
- package/vendor/hyperframes/dist/chunk-L3YOGIPG.js +48 -0
- package/vendor/hyperframes/dist/chunk-LAOCA3MQ.js +189 -0
- package/vendor/hyperframes/dist/chunk-LCSMOLGX.js +312 -0
- package/vendor/hyperframes/dist/chunk-LFGJWOVA.js +61 -0
- package/vendor/hyperframes/dist/chunk-LHSR3M4M.js +498 -0
- package/vendor/hyperframes/dist/chunk-LYNI3FOL.js +123 -0
- package/vendor/hyperframes/dist/chunk-M2WBOQ2Q.js +54 -0
- package/vendor/hyperframes/dist/chunk-MRF4AMKD.js +104 -0
- package/vendor/hyperframes/dist/chunk-MWB3ZIFM.js +852 -0
- package/vendor/hyperframes/dist/chunk-NGKM64BP.js +39 -0
- package/vendor/hyperframes/dist/chunk-NXMYHJAL.js +43 -0
- package/vendor/hyperframes/dist/chunk-OAF4XJOW.js +192 -0
- package/vendor/hyperframes/dist/chunk-OFQWTKPW.js +117 -0
- package/vendor/hyperframes/dist/chunk-P6LJ4Y32.js +4662 -0
- package/vendor/hyperframes/dist/chunk-PBN6GBJE.js +108 -0
- package/vendor/hyperframes/dist/chunk-PDAMK4Q5.js +28 -0
- package/vendor/hyperframes/dist/chunk-PFCFR3KZ.js +20 -0
- package/vendor/hyperframes/dist/chunk-PJ3HDUCY.js +108 -0
- package/vendor/hyperframes/dist/chunk-PMRBLBUP.js +152 -0
- package/vendor/hyperframes/dist/chunk-PPQUYWTW.js +240 -0
- package/vendor/hyperframes/dist/chunk-Q5X2B5WT.js +29 -0
- package/vendor/hyperframes/dist/chunk-Q7IU7MOR.js +351 -0
- package/vendor/hyperframes/dist/chunk-QC4FJVLM.js +371 -0
- package/vendor/hyperframes/dist/chunk-QNYYOUU3.js +1318 -0
- package/vendor/hyperframes/dist/chunk-RDNNAKEP.js +161 -0
- package/vendor/hyperframes/dist/chunk-RMHUQGRK.js +657 -0
- package/vendor/hyperframes/dist/chunk-RQVDTPGD.js +6 -0
- package/vendor/hyperframes/dist/chunk-RVCWIIZ7.js +68 -0
- package/vendor/hyperframes/dist/chunk-S3D6GE5W.js +72 -0
- package/vendor/hyperframes/dist/chunk-S3S4TGEH.js +424 -0
- package/vendor/hyperframes/dist/chunk-S7NPLFWV.js +11395 -0
- package/vendor/hyperframes/dist/chunk-SHB5VDMH.js +51 -0
- package/vendor/hyperframes/dist/chunk-TCLGPL65.js +860 -0
- package/vendor/hyperframes/dist/chunk-THXFSPLV.js +40 -0
- package/vendor/hyperframes/dist/chunk-TJSTGSAW.js +354 -0
- package/vendor/hyperframes/dist/chunk-TQFG7HTI.js +1024 -0
- package/vendor/hyperframes/dist/chunk-TVCRUG2S.js +265 -0
- package/vendor/hyperframes/dist/chunk-TWWSYAKJ.js +24 -0
- package/vendor/hyperframes/dist/chunk-U7CZBDMV.js +27 -0
- package/vendor/hyperframes/dist/chunk-UEZXMWG3.js +139 -0
- package/vendor/hyperframes/dist/chunk-UYEZTK4U.js +61 -0
- package/vendor/hyperframes/dist/chunk-VC76R6W2.js +27 -0
- package/vendor/hyperframes/dist/chunk-VFCKXTBB.js +159 -0
- package/vendor/hyperframes/dist/chunk-VWG7OQKU.js +22 -0
- package/vendor/hyperframes/dist/chunk-VXVRPIJA.js +105 -0
- package/vendor/hyperframes/dist/chunk-WBLXWOYK.js +15 -0
- package/vendor/hyperframes/dist/chunk-WERLJGIC.js +20 -0
- package/vendor/hyperframes/dist/chunk-WMQNIQDJ.js +2057 -0
- package/vendor/hyperframes/dist/chunk-WPHMNO44.js +4528 -0
- package/vendor/hyperframes/dist/chunk-X5HWXQPU.js +113 -0
- package/vendor/hyperframes/dist/chunk-X7MTZ4T2.js +6 -0
- package/vendor/hyperframes/dist/chunk-XH4DSOS5.js +59 -0
- package/vendor/hyperframes/dist/chunk-XS4EJSKU.js +23 -0
- package/vendor/hyperframes/dist/chunk-XT5P4QMO.js +181 -0
- package/vendor/hyperframes/dist/chunk-XT7X6QCP.js +6 -0
- package/vendor/hyperframes/dist/chunk-XUHOIDKG.js +6 -0
- package/vendor/hyperframes/dist/chunk-XUXGIA7H.js +485 -0
- package/vendor/hyperframes/dist/chunk-XZABUQTX.js +113 -0
- package/vendor/hyperframes/dist/chunk-Y6W4DVPS.js +356 -0
- package/vendor/hyperframes/dist/chunk-YEIFI443.js +44 -0
- package/vendor/hyperframes/dist/chunk-YQZNILVH.js +375 -0
- package/vendor/hyperframes/dist/chunk-Z27A7NUB.js +65 -0
- package/vendor/hyperframes/dist/chunk-Z6B37SNM.js +3081 -0
- package/vendor/hyperframes/dist/chunk-ZJUKFZ2P.js +10954 -0
- package/vendor/hyperframes/dist/chunk-ZKDMAOFU.js +32 -0
- package/vendor/hyperframes/dist/chunk-ZMXUQLI2.js +17219 -0
- package/vendor/hyperframes/dist/chunk-ZTZ7NCJT.js +58 -0
- package/vendor/hyperframes/dist/chunk-ZTZA4UWZ.js +108 -0
- package/vendor/hyperframes/dist/clean-W57E5YQ3.js +293 -0
- package/vendor/hyperframes/dist/cli.js +495 -0
- package/vendor/hyperframes/dist/cloud-FFTJGZ24.js +70 -0
- package/vendor/hyperframes/dist/cloudrun-AJUTHUEA.js +763 -0
- package/vendor/hyperframes/dist/colors-HKWSZCGK.js +15 -0
- package/vendor/hyperframes/dist/commands/contrast-audit.browser.js +459 -0
- package/vendor/hyperframes/dist/commands/layout-audit.browser.js +2102 -0
- package/vendor/hyperframes/dist/commands/motion-sample.browser.js +74 -0
- package/vendor/hyperframes/dist/commands/motion-signature.browser.js +374 -0
- package/vendor/hyperframes/dist/compare-AYCTT55F.js +421 -0
- package/vendor/hyperframes/dist/compiler-QO36EXLS.js +121 -0
- package/vendor/hyperframes/dist/component-VFFR2O7T.js +176 -0
- package/vendor/hyperframes/dist/compositions-QTVN3U2A.js +228 -0
- package/vendor/hyperframes/dist/contactSheet-27SRU7A4.js +22 -0
- package/vendor/hyperframes/dist/delete-ZAKYCB7I.js +114 -0
- package/vendor/hyperframes/dist/deploy-MX76RZ36.js +101 -0
- package/vendor/hyperframes/dist/destroy-H2TZBYHH.js +42 -0
- package/vendor/hyperframes/dist/deterministicFonts-HAEMV4TM.js +50 -0
- package/vendor/hyperframes/dist/dist-EWXJGN63.js +129 -0
- package/vendor/hyperframes/dist/dist-FNNGCKFS.js +29 -0
- package/vendor/hyperframes/dist/dist-S3PJNPXQ.js +392 -0
- package/vendor/hyperframes/dist/docker/Dockerfile.render +65 -0
- package/vendor/hyperframes/dist/docs/compositions.md +67 -0
- package/vendor/hyperframes/dist/docs/data-attributes.md +27 -0
- package/vendor/hyperframes/dist/docs/examples.md +19 -0
- package/vendor/hyperframes/dist/docs/gsap.md +26 -0
- package/vendor/hyperframes/dist/docs/rendering.md +35 -0
- package/vendor/hyperframes/dist/docs/troubleshooting.md +29 -0
- package/vendor/hyperframes/dist/docs-QAPJEGLS.js +131 -0
- package/vendor/hyperframes/dist/doctor-AI7C3FON.js +67 -0
- package/vendor/hyperframes/dist/dom-IROKGVJI.js +14 -0
- package/vendor/hyperframes/dist/dotEnv-AECLXQSO.js +14 -0
- package/vendor/hyperframes/dist/events-7LALYNKZ.js +92 -0
- package/vendor/hyperframes/dist/events-FQY3HGKZ.js +88 -0
- package/vendor/hyperframes/dist/feedback-WCMDMSTK.js +472 -0
- package/vendor/hyperframes/dist/ffmpeg-QIUFEFTY.js +35 -0
- package/vendor/hyperframes/dist/figma-64WY462P.js +63 -0
- package/vendor/hyperframes/dist/fontData.generated-NXAOI4GH.js +13 -0
- package/vendor/hyperframes/dist/fontLocalizeCli.js +132 -0
- package/vendor/hyperframes/dist/get-FPOMJMZB.js +103 -0
- package/vendor/hyperframes/dist/gpuPolicy-2ZUZE5D6.js +38 -0
- package/vendor/hyperframes/dist/grade-compare-E2CKPST4.js +647 -0
- package/vendor/hyperframes/dist/gsapParser-DIYYX6JT.js +2846 -0
- package/vendor/hyperframes/dist/gsapParserAcorn-E3GEIRZB.js +28 -0
- package/vendor/hyperframes/dist/help-YRQIDDML.js +197 -0
- package/vendor/hyperframes/dist/history-PP5NDRV3.js +571 -0
- package/vendor/hyperframes/dist/hyperframe-runtime.js +1081 -0
- package/vendor/hyperframes/dist/hyperframe.manifest.json +22 -0
- package/vendor/hyperframes/dist/hyperframe.runtime.iife.js +1081 -0
- package/vendor/hyperframes/dist/hyperframes-player.global.js +474 -0
- package/vendor/hyperframes/dist/hyperframes-slideshow.global.js +149 -0
- package/vendor/hyperframes/dist/info-SEX3B7JL.js +153 -0
- package/vendor/hyperframes/dist/init-U3AKHO5B.js +1147 -0
- package/vendor/hyperframes/dist/inspect-Q3K6EDEK.js +97 -0
- package/vendor/hyperframes/dist/installer-K6O5WF44.js +37 -0
- package/vendor/hyperframes/dist/keyframes-DC64JCI3.js +2602 -0
- package/vendor/hyperframes/dist/lambda-D7UMN4DJ.js +451 -0
- package/vendor/hyperframes/dist/layout-XSPPDHOO.js +86 -0
- package/vendor/hyperframes/dist/lib-TPG2ZMAX.js +128 -0
- package/vendor/hyperframes/dist/lint-7DQZE7L3.js +164 -0
- package/vendor/hyperframes/dist/lintProject-OHBBM3DJ.js +27 -0
- package/vendor/hyperframes/dist/list-4K5MVDQX.js +162 -0
- package/vendor/hyperframes/dist/login-NTX4OWXL.js +377 -0
- package/vendor/hyperframes/dist/logout-Q654A3SI.js +100 -0
- package/vendor/hyperframes/dist/manager-67P2XFXQ.js +34 -0
- package/vendor/hyperframes/dist/manager-CT3D67CG.js +38 -0
- package/vendor/hyperframes/dist/manager-GFN75DM4.js +30 -0
- package/vendor/hyperframes/dist/media-treatment-5FYAOPNL.js +972 -0
- package/vendor/hyperframes/dist/media-use-4XXGABIL.js +113 -0
- package/vendor/hyperframes/dist/models-C4E4IWJD.js +137 -0
- package/vendor/hyperframes/dist/motionShot-S4MIGMT5.js +614 -0
- package/vendor/hyperframes/dist/normalize-O76AC4HE.js +333 -0
- package/vendor/hyperframes/dist/normalize-audio-CSFTXLXD.js +276 -0
- package/vendor/hyperframes/dist/open-2ZD6BHZG.js +115 -0
- package/vendor/hyperframes/dist/pipeline-TNGWUZQC.js +54 -0
- package/vendor/hyperframes/dist/play-CTMUXX6E.js +349 -0
- package/vendor/hyperframes/dist/policies-FSDZVS2E.js +292 -0
- package/vendor/hyperframes/dist/preflight-SMNV6RW2.js +45 -0
- package/vendor/hyperframes/dist/present-WQVHV5CR.js +350 -0
- package/vendor/hyperframes/dist/preview-JOJXTH75.js +1820 -0
- package/vendor/hyperframes/dist/producer-ECLEN5RP.js +13 -0
- package/vendor/hyperframes/dist/progress-FBIEEB7Y.js +73 -0
- package/vendor/hyperframes/dist/projectConfig-HYJMJ5BY.js +56 -0
- package/vendor/hyperframes/dist/publication-HQ5Y3KBM.js +29 -0
- package/vendor/hyperframes/dist/publish-VHDYQASC.js +430 -0
- package/vendor/hyperframes/dist/refresh-K24ETHMA.js +53 -0
- package/vendor/hyperframes/dist/remove-background-W2YX2NVH.js +253 -0
- package/vendor/hyperframes/dist/render-AO5VX3YI.js +177 -0
- package/vendor/hyperframes/dist/render-LNO62LEL.js +2367 -0
- package/vendor/hyperframes/dist/render-TTQI7TGN.js +796 -0
- package/vendor/hyperframes/dist/render-batch-YH7CTWNY.js +284 -0
- package/vendor/hyperframes/dist/renderCancellation-JPIHE47X.js +35 -0
- package/vendor/hyperframes/dist/renderSetupWorker.js +79 -0
- package/vendor/hyperframes/dist/resolver-IZ647GBZ.js +38 -0
- package/vendor/hyperframes/dist/runtimeVersion.js +18 -0
- package/vendor/hyperframes/dist/screenshotCapture-ECBTLCU6.js +179 -0
- package/vendor/hyperframes/dist/shaderTransitionWorker.js +44 -0
- package/vendor/hyperframes/dist/sherpa-KKLNW6U2.js +314 -0
- package/vendor/hyperframes/dist/sherpaWorker.js +77 -0
- package/vendor/hyperframes/dist/sites-TNVKZS73.js +52 -0
- package/vendor/hyperframes/dist/skill-ZHVLH7KX.js +15 -0
- package/vendor/hyperframes/dist/skills/hyperframes/SKILL.md +152 -0
- package/vendor/hyperframes/dist/skills/hyperframes/references/brief-contract.md +108 -0
- package/vendor/hyperframes/dist/skills/hyperframes/references/brief-format.md +72 -0
- package/vendor/hyperframes/dist/skills/hyperframes/references/capability-menu.md +47 -0
- package/vendor/hyperframes/dist/skills/hyperframes/references/frame-worker-core.md +80 -0
- package/vendor/hyperframes/dist/skills/hyperframes/references/intent-interview.md +73 -0
- package/vendor/hyperframes/dist/skills/hyperframes/references/pitch-round.md +40 -0
- package/vendor/hyperframes/dist/skills/hyperframes/references/plugin-installation.md +43 -0
- package/vendor/hyperframes/dist/skills/hyperframes/references/production-loop.md +26 -0
- package/vendor/hyperframes/dist/skills/hyperframes/references/review-loop.md +37 -0
- package/vendor/hyperframes/dist/skills/hyperframes/references/route-briefs.md +11 -0
- package/vendor/hyperframes/dist/skills/hyperframes/references/routes/embedded-captions.md +11 -0
- package/vendor/hyperframes/dist/skills/hyperframes/references/routes/faceless-explainer.md +12 -0
- package/vendor/hyperframes/dist/skills/hyperframes/references/routes/general-video.md +12 -0
- package/vendor/hyperframes/dist/skills/hyperframes/references/routes/motion-graphics.md +11 -0
- package/vendor/hyperframes/dist/skills/hyperframes/references/routes/music-to-video.md +12 -0
- package/vendor/hyperframes/dist/skills/hyperframes/references/routes/pr-to-video.md +28 -0
- package/vendor/hyperframes/dist/skills/hyperframes/references/routes/product-launch-video.md +13 -0
- package/vendor/hyperframes/dist/skills/hyperframes/references/routes/remotion-to-hyperframes.md +9 -0
- package/vendor/hyperframes/dist/skills/hyperframes/references/routes/slideshow.md +11 -0
- package/vendor/hyperframes/dist/skills/hyperframes/references/routes/talking-head-recut.md +11 -0
- package/vendor/hyperframes/dist/skills/hyperframes/references/script-format.md +52 -0
- package/vendor/hyperframes/dist/skills/hyperframes/references/skill-lifecycle.md +35 -0
- package/vendor/hyperframes/dist/skills/hyperframes/references/storyboard-format.md +99 -0
- package/vendor/hyperframes/dist/skills/hyperframes/references/subagent-dispatch.md +35 -0
- package/vendor/hyperframes/dist/skills/hyperframes/references/workflow-catalog.md +10 -0
- package/vendor/hyperframes/dist/skills/hyperframes/scripts/lib/frame-packets-core.mjs +194 -0
- package/vendor/hyperframes/dist/skills/hyperframes/scripts/plugin-cli.mjs +88 -0
- package/vendor/hyperframes/dist/skills/hyperframes/scripts/plugin-cli.test.mjs +117 -0
- package/vendor/hyperframes/dist/skills/hyperframes-cli/SKILL.md +189 -0
- package/vendor/hyperframes/dist/skills/hyperframes-cli/references/beats.md +19 -0
- package/vendor/hyperframes/dist/skills/hyperframes-cli/references/cloud.md +163 -0
- package/vendor/hyperframes/dist/skills/hyperframes-cli/references/cloudrun.md +62 -0
- package/vendor/hyperframes/dist/skills/hyperframes-cli/references/compare-and-batch.md +105 -0
- package/vendor/hyperframes/dist/skills/hyperframes-cli/references/doctor-browser.md +57 -0
- package/vendor/hyperframes/dist/skills/hyperframes-cli/references/init-and-scaffold.md +74 -0
- package/vendor/hyperframes/dist/skills/hyperframes-cli/references/lambda.md +185 -0
- package/vendor/hyperframes/dist/skills/hyperframes-cli/references/lint-validate-inspect.md +132 -0
- package/vendor/hyperframes/dist/skills/hyperframes-cli/references/preview-render.md +193 -0
- package/vendor/hyperframes/dist/skills/hyperframes-cli/references/upgrade-info-misc.md +134 -0
- package/vendor/hyperframes/dist/skills/media-use/SKILL.md +100 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/CREDITS.md +35 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/chime.mp3 +0 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/click-soft.mp3 +0 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/click.mp3 +0 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/error.mp3 +0 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/glitch-1.mp3 +0 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/glitch-2.mp3 +0 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/glitch-3.mp3 +0 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/impact-bass-1.mp3 +0 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/impact-bass-2.mp3 +0 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/key-press.mp3 +0 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/manifest.json +97 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/notification.mp3 +0 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/ping.mp3 +0 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/pop.mp3 +0 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/riser.mp3 +0 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/sparkle.mp3 +0 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/typing.mp3 +0 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/whoosh-cinematic.mp3 +0 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/whoosh-short.mp3 +0 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/assets/sfx/whoosh.mp3 +0 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/references/bgm.md +76 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/references/captions/authoring.md +163 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/references/captions/motion.md +87 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/references/captions/transcript-handling.md +97 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/references/remove-background.md +143 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/references/requirements.md +29 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/references/sfx.md +42 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/references/transcribe.md +52 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/references/tts-to-captions.md +35 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/references/tts.md +260 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/scripts/audio.mjs +311 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/scripts/audio.test.mjs +159 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/scripts/gemini-pipeline.test.mjs +131 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/scripts/heygen-tts.mjs +122 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/scripts/heygen-tts.test.mjs +38 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/scripts/heygen-voice.mjs +98 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/scripts/heygen-voice.test.mjs +233 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/audio-meta.mjs +39 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/audio-meta.test.mjs +136 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/bgm-volume.mjs +4 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/bgm.mjs +257 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/bgm.test.mjs +74 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/concurrency.mjs +14 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/concurrency.test.mjs +41 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/gemini-auth.mjs +43 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/gemini-auth.py +52 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/gemini-auth.test.mjs +86 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/gemini-auth_test.py +90 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/gemini-tts.mjs +122 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/gemini-tts.test.mjs +243 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/heygen.mjs +197 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/heygen.test.mjs +185 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/media-record.mjs +114 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/media-record.test.mjs +205 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/python.mjs +77 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/python.test.mjs +136 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/sfx.mjs +165 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/sfx.test.mjs +121 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/tts.mjs +389 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/tts.spawn.test.mjs +158 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lib/tts.test.mjs +230 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/scripts/lyria-recipe.py +136 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/scripts/wait-bgm.mjs +175 -0
- package/vendor/hyperframes/dist/skills/media-use/audio/scripts/wait-bgm.test.mjs +108 -0
- package/vendor/hyperframes/dist/skills/media-use/luts/README.md +28 -0
- package/vendor/hyperframes/dist/skills/media-use/luts/index.json +57 -0
- package/vendor/hyperframes/dist/skills/media-use/references/audio.md +20 -0
- package/vendor/hyperframes/dist/skills/media-use/references/grading.md +160 -0
- package/vendor/hyperframes/dist/skills/media-use/references/media-treatment-recipes.md +929 -0
- package/vendor/hyperframes/dist/skills/media-use/references/media-treatments.md +202 -0
- package/vendor/hyperframes/dist/skills/media-use/references/memory.md +37 -0
- package/vendor/hyperframes/dist/skills/media-use/references/meta.md +61 -0
- package/vendor/hyperframes/dist/skills/media-use/references/operations.md +327 -0
- package/vendor/hyperframes/dist/skills/media-use/references/resolve.md +150 -0
- package/vendor/hyperframes/dist/skills/media-use/references/setup-providers.md +80 -0
- package/vendor/hyperframes/dist/skills/media-use/references/telemetry-dashboard.md +49 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/audio-duck.mjs +113 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/compatibility.test.mjs +96 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/dither.mjs +328 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/dither.test.mjs +176 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/eval.mjs +369 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/adopt.mjs +128 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/adopt.test.mjs +95 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/bgm-provider.mjs +20 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/brand-provider.mjs +59 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/bundled-sfx-provider.mjs +119 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/bundled-sfx-provider.test.mjs +139 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/cache.mjs +148 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/candidates.mjs +91 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/candidates.test.mjs +155 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/codex-provider.mjs +161 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/codex-provider.test.mjs +34 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/config-lock.d.mts +3 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/config-lock.mjs +45 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/config-lock.test.mjs +79 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/coverage.test.mjs +136 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/cube-build.mjs +181 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/cube-build.test.mjs +80 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/cube-validate.mjs +198 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/cube-validate.test.mjs +125 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/cutlist.mjs +184 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/cutlist.test.mjs +162 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/duck.mjs +95 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/duck.test.mjs +153 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/error-diffusion.mjs +230 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/error-diffusion.test.mjs +102 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/ff-binaries.mjs +37 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/freeze.mjs +72 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/freeze.test.mjs +138 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/grade-analyzer.mjs +265 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/grade-analyzer.test.mjs +285 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/heygen-cli.mjs +191 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/heygen-cli.test.mjs +301 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/heygen-search.mjs +45 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/heygen-search.test.mjs +49 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/heygen-video-provider.mjs +138 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/heygen-video-provider.test.mjs +435 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/image-provider.mjs +44 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/index-gen.mjs +63 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/local-media-search.mjs +32 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/local-models.mjs +318 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/local-models.test.mjs +220 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/local-run.mjs +78 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/local-run.test.mjs +121 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/logo-provider.mjs +244 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/logo-provider.test.mjs +298 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/ltx-video-provider.mjs +94 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/ltx-video-provider.test.mjs +316 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/lut-preset-provider.mjs +271 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/lut-preset-provider.test.mjs +199 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/manifest.d.mts +19 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/manifest.mjs +279 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/manifest.test.mjs +397 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/match.mjs +47 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/media-fetch.mjs +85 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/media-fetch.test.mjs +197 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/media-home.mjs +20 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/media-home.test.mjs +61 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/media-search.d.mts +9 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/media-search.mjs +107 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/mflux-provider.mjs +128 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/mflux-provider.test.mjs +174 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/misses.mjs +49 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/misses.test.mjs +89 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/npx-sync.mjs +31 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/npx-sync.test.mjs +50 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/parakeet-words.mjs +27 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/parakeet-words.test.mjs +44 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/prefs-store.mjs +180 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/prefs-store.test.mjs +189 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/probe.mjs +41 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/probe.test.mjs +148 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/providers.mjs +5 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/recipe-store.mjs +367 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/recipe-store.test.mjs +367 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/registry.mjs +212 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/registry.test.mjs +262 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/search.mjs +24 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/search.test.mjs +32 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/sfx-provider.mjs +23 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/specs.mjs +132 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/specs.test.mjs +157 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/stats.mjs +117 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/stats.test.mjs +165 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/svg-sanitize.mjs +21 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/telemetry.mjs +210 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/telemetry.test.mjs +437 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/test-home.test.mjs +10 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/transcriptCutFade.mjs +21 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/transcriptCutFade.test.mjs +33 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/tts-local-provider.mjs +94 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/tts-local-provider.test.mjs +72 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/usage.mjs +57 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/usage.test.mjs +54 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/voice-provider.mjs +57 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/voice-provider.test.mjs +46 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/lib/words.mjs +18 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/prefs.mjs +70 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/recipe.mjs +103 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/resolve-plugin.test.mjs +62 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/resolve.mjs +1369 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/transcribe.mjs +194 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/transcribe.test.mjs +112 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/transcript-cut.mjs +270 -0
- package/vendor/hyperframes/dist/skills/media-use/scripts/transcript-cut.test.mjs +117 -0
- package/vendor/hyperframes/dist/skills-MJ2J4GEM.js +808 -0
- package/vendor/hyperframes/dist/skillsUpdateCheck-TKTYQN5R.js +26 -0
- package/vendor/hyperframes/dist/slideshow-EJ4IMY6P.js +24 -0
- package/vendor/hyperframes/dist/snapshot-KIVDUUMN.js +875 -0
- package/vendor/hyperframes/dist/src-6YQBJGWR.js +202 -0
- package/vendor/hyperframes/dist/src-PVYUYV3M.js +471 -0
- package/vendor/hyperframes/dist/state-KAXGHF77.js +26 -0
- package/vendor/hyperframes/dist/staticProjectServer-7EU666GS.js +49 -0
- package/vendor/hyperframes/dist/status-YM52BQ6S.js +311 -0
- package/vendor/hyperframes/dist/studio/assets/hyperframes-player-Cp8e_Zst.js +473 -0
- package/vendor/hyperframes/dist/studio/assets/index-BX3KHhGX.js +71 -0
- package/vendor/hyperframes/dist/studio/assets/index-CASEBNy2.js +1 -0
- package/vendor/hyperframes/dist/studio/assets/index-D11atkBt.css +1 -0
- package/vendor/hyperframes/dist/studio/assets/index-DMTt9nTf.js +531 -0
- package/vendor/hyperframes/dist/studio/assets/index-oE1pOdAy.js +399 -0
- package/vendor/hyperframes/dist/studio/chunk-5VSXRGIX.js +948 -0
- package/vendor/hyperframes/dist/studio/chunk-5VSXRGIX.js.map +1 -0
- package/vendor/hyperframes/dist/studio/chunk-7UQWVN52.js +938 -0
- package/vendor/hyperframes/dist/studio/chunk-7UQWVN52.js.map +1 -0
- package/vendor/hyperframes/dist/studio/chunk-EEXSYY5F.js +190 -0
- package/vendor/hyperframes/dist/studio/chunk-EEXSYY5F.js.map +1 -0
- package/vendor/hyperframes/dist/studio/chunk-FOEID72M.js +1441 -0
- package/vendor/hyperframes/dist/studio/chunk-FOEID72M.js.map +1 -0
- package/vendor/hyperframes/dist/studio/chunk-PZ5AY32C.js +10 -0
- package/vendor/hyperframes/dist/studio/chunk-PZ5AY32C.js.map +1 -0
- package/vendor/hyperframes/dist/studio/chunk-TDWGXBUT.js +1103 -0
- package/vendor/hyperframes/dist/studio/chunk-TDWGXBUT.js.map +1 -0
- package/vendor/hyperframes/dist/studio/domEditingLayers-IEVLO54Z.js +47 -0
- package/vendor/hyperframes/dist/studio/domEditingLayers-IEVLO54Z.js.map +1 -0
- package/vendor/hyperframes/dist/studio/favicon.svg +14 -0
- package/vendor/hyperframes/dist/studio/icons/timeline/audio.svg +7 -0
- package/vendor/hyperframes/dist/studio/icons/timeline/captions.svg +5 -0
- package/vendor/hyperframes/dist/studio/icons/timeline/composition.svg +12 -0
- package/vendor/hyperframes/dist/studio/icons/timeline/image.svg +18 -0
- package/vendor/hyperframes/dist/studio/icons/timeline/music.svg +10 -0
- package/vendor/hyperframes/dist/studio/icons/timeline/text.svg +3 -0
- package/vendor/hyperframes/dist/studio/index.css +165 -0
- package/vendor/hyperframes/dist/studio/index.css.map +1 -0
- package/vendor/hyperframes/dist/studio/index.d.ts +4296 -0
- package/vendor/hyperframes/dist/studio/index.html +23 -0
- package/vendor/hyperframes/dist/studio/index.js +99693 -0
- package/vendor/hyperframes/dist/studio/index.js.map +1 -0
- package/vendor/hyperframes/dist/studio/player.d.ts +30 -0
- package/vendor/hyperframes/dist/studio/player.js +10 -0
- package/vendor/hyperframes/dist/studio/player.js.map +1 -0
- package/vendor/hyperframes/dist/studio/styles/tailwind-preset.d.ts +39 -0
- package/vendor/hyperframes/dist/studio/styles/tailwind-preset.js +50 -0
- package/vendor/hyperframes/dist/studio/styles/tailwind-preset.js.map +1 -0
- package/vendor/hyperframes/dist/studio/styles.css +1 -0
- package/vendor/hyperframes/dist/studio/ui.d.ts +296 -0
- package/vendor/hyperframes/dist/studio/ui.js +66 -0
- package/vendor/hyperframes/dist/studio/ui.js.map +1 -0
- package/vendor/hyperframes/dist/studio-api-PNQZRPX5.js +136 -0
- package/vendor/hyperframes/dist/studioSelectionClient-IUUPKUHZ.js +25 -0
- package/vendor/hyperframes/dist/studioServer-UWRBPIOJ.js +1457 -0
- package/vendor/hyperframes/dist/synthesize-K62TIZWI.js +182 -0
- package/vendor/hyperframes/dist/system-EZYZNUFD.js +44 -0
- package/vendor/hyperframes/dist/telemetry-2YC4SGQU.js +144 -0
- package/vendor/hyperframes/dist/telemetry-QMYM4WME.js +95 -0
- package/vendor/hyperframes/dist/templates/_shared/AGENTS.md +107 -0
- package/vendor/hyperframes/dist/templates/_shared/CLAUDE.md +107 -0
- package/vendor/hyperframes/dist/templates/blank/index.html +55 -0
- package/vendor/hyperframes/dist/templates/from-file/index.html +75 -0
- package/vendor/hyperframes/dist/templates/warm-grain/compositions/captions.html +154 -0
- package/vendor/hyperframes/dist/templates/warm-grain/compositions/graphics.html +162 -0
- package/vendor/hyperframes/dist/templates/warm-grain/compositions/intro.html +87 -0
- package/vendor/hyperframes/dist/templates/warm-grain/index.html +264 -0
- package/vendor/hyperframes/dist/templates/warm-grain/registry-item.json +34 -0
- package/vendor/hyperframes/dist/timeline-77Z3BBQM.js +1377 -0
- package/vendor/hyperframes/dist/tokens-OBGTYE4Z.js +92 -0
- package/vendor/hyperframes/dist/transcribe-7NVFHCGA.js +387 -0
- package/vendor/hyperframes/dist/transcribe-BNSJRR4M.js +62 -0
- package/vendor/hyperframes/dist/tts-P3LIPIL5.js +234 -0
- package/vendor/hyperframes/dist/updateCheck-UDWIJ4ZQ.js +39 -0
- package/vendor/hyperframes/dist/upgrade-CT3CPO3E.js +231 -0
- package/vendor/hyperframes/dist/urlDownloader-H6WRI2UE.js +39 -0
- package/vendor/hyperframes/dist/usage-EC3KFUIJ.js +2139 -0
- package/vendor/hyperframes/dist/validate-65QDZJWZ.js +70 -0
- package/vendor/hyperframes/dist/video-FGSGKRSV.js +261 -0
- package/vendor/hyperframes/dist/workspaceBuildCheck-TOLGAI4T.js +133 -0
- 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
|
+
})();
|