hyperframes 0.8.111 → 0.8.112

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/dist/{add-M5WN2AM2.js → add-NIVC2YMT.js} +15 -15
  2. package/dist/{agentPromptGenerator-QL2KAWEP.js → agentPromptGenerator-VIIPZCJ3.js} +1 -1
  3. package/dist/{animatedGifPrep-NMGFOSIL.js → animatedGifPrep-NWAJKCHG.js} +6 -6
  4. package/dist/{asset-GUASPIKN.js → asset-O6JJKF3Z.js} +2 -2
  5. package/dist/{assetCataloger-FXEHOUA3.js → assetCataloger-DJSPW3RL.js} +2 -2
  6. package/dist/{auth-3YMEVUKJ.js → auth-6XRZQQQS.js} +2 -2
  7. package/dist/{autoUpdate-FYCGHBPC.js → autoUpdate-GZHLQBLS.js} +2 -2
  8. package/dist/{backgroundChecks-ZLYIRMTS.js → backgroundChecks-4QQRGJ5S.js} +3 -3
  9. package/dist/backgroundChecksWorker.js +3 -3
  10. package/dist/{batchRender-ZTHFMHZY.js → batchRender-PJGNXN7F.js} +3 -3
  11. package/dist/{beats-GXY5ZBPB.js → beats-VYR6B4UB.js} +10 -10
  12. package/dist/{benchmark-MJFVBVNW.js → benchmark-CGP2NKIP.js} +12 -12
  13. package/dist/{browser-ZDI7IX6H.js → browser-ZWD4ETXK.js} +9 -9
  14. package/dist/{bundleWithLocalizedFonts-4Q3ETTHW.js → bundleWithLocalizedFonts-JWHKLSZM.js} +2 -2
  15. package/dist/{capture-4OPH4PMK.js → capture-C4MPRLJV.js} +40 -42
  16. package/dist/{capture-OG6532RA.js → capture-IQ7AUK6M.js} +3 -3
  17. package/dist/{captureFile-JOPOCRU7.js → captureFile-LYOJOUIE.js} +3 -1
  18. package/dist/{catalog-23DP7KCW.js → catalog-RZ7PWQMS.js} +15 -15
  19. package/dist/{check-5X6BJA2P.js → check-7UU4SSO6.js} +50 -38
  20. package/dist/{checkBrowser-3QHBTMYM.js → checkBrowser-6KYNYIU7.js} +32 -31
  21. package/dist/{chunk-CFC3EEDQ.js → chunk-2BEOCUAG.js} +1 -1
  22. package/dist/{chunk-6CI65KCP.js → chunk-2GXKCB7J.js} +1 -1
  23. package/dist/{chunk-HQGJLZB6.js → chunk-36XLW2TP.js} +2 -2
  24. package/dist/{chunk-NH75EKOP.js → chunk-3Q7HXIXZ.js} +1 -1
  25. package/dist/{chunk-2JXJ5SGK.js → chunk-4BXTKKRD.js} +6 -6
  26. package/dist/{chunk-XYVFPDNU.js → chunk-5SDXALUQ.js} +1 -1
  27. package/dist/{chunk-ITBEN3QD.js → chunk-7G25WGTA.js} +2 -2
  28. package/dist/{chunk-YIXEH4TF.js → chunk-7OJSZTJK.js} +2 -2
  29. package/dist/{chunk-5ER3PYYP.js → chunk-7VIXEM6Q.js} +1 -1
  30. package/dist/{chunk-HPEHQUJX.js → chunk-A6GVXZZ3.js} +1 -1
  31. package/dist/{chunk-YLOSZHA2.js → chunk-BD3MHTSR.js} +2 -2
  32. package/dist/{chunk-Q2QNAFQV.js → chunk-BLI5XATY.js} +1 -1
  33. package/dist/{chunk-N32ITJ7L.js → chunk-BSBIG74E.js} +15 -12
  34. package/dist/{chunk-ZU54D6MI.js → chunk-BZGWHPRW.js} +3 -3
  35. package/dist/{chunk-B25H4M4V.js → chunk-C4JINRJK.js} +1 -1
  36. package/dist/{chunk-5ON5GH3X.js → chunk-CS4IE5G7.js} +1 -1
  37. package/dist/{chunk-EE6SV6KW.js → chunk-E664L6K3.js} +3 -3
  38. package/dist/{chunk-J4RPT4QW.js → chunk-E6RPWJVC.js} +1 -1
  39. package/dist/{chunk-TQKKC4MB.js → chunk-EA4LCCJI.js} +5 -5
  40. package/dist/{chunk-LFUXJBYA.js → chunk-GODQ5XA4.js} +16 -8
  41. package/dist/{chunk-LAQEZHOH.js → chunk-H3WFEPV2.js} +1 -1
  42. package/dist/{chunk-U4TDAJZE.js → chunk-J2S4MY2M.js} +2 -2
  43. package/dist/{chunk-CQRF7VU3.js → chunk-JRULOVZK.js} +1 -1
  44. package/dist/{chunk-C4TZBEHY.js → chunk-KIIKVKHB.js} +2 -2
  45. package/dist/{chunk-M224DAMG.js → chunk-KPSRL2NB.js} +1 -1
  46. package/dist/{chunk-JTTAS7N6.js → chunk-KQ3VNA6R.js} +1 -1
  47. package/dist/{chunk-ZXSNGIYS.js → chunk-KS5HFREQ.js} +4 -4
  48. package/dist/{chunk-AMSBTN6L.js → chunk-LYBFDHXD.js} +3 -3
  49. package/dist/{chunk-AWUZ3AUD.js → chunk-M4RYK5OF.js} +1 -1
  50. package/dist/chunk-MRF4AMKD.js +104 -0
  51. package/dist/{chunk-JWPSBANO.js → chunk-NOOBRCMC.js} +3 -3
  52. package/dist/{chunk-TIZQC44K.js → chunk-OWN3ZIIK.js} +1 -1
  53. package/dist/{chunk-NIGUNLOO.js → chunk-OXZO4WQI.js} +9 -8
  54. package/dist/{chunk-JUP65S7D.js → chunk-P3AQVFY2.js} +3 -3
  55. package/dist/{chunk-OF3RKOPU.js → chunk-P74PW7KR.js} +189 -111
  56. package/dist/{chunk-PFEKMIN5.js → chunk-POEGJGEA.js} +1 -1
  57. package/dist/{chunk-BQYGGX3B.js → chunk-PRETPA7Z.js} +1 -1
  58. package/dist/{chunk-QMAXDSCH.js → chunk-Q7U7QRC7.js} +2 -2
  59. package/dist/chunk-S6TH5G6Z.js +3541 -0
  60. package/dist/{chunk-MGFVNTIB.js → chunk-TSZU5JMC.js} +2 -2
  61. package/dist/{chunk-MR2LOG32.js → chunk-TXVN43SX.js} +8 -8
  62. package/dist/{chunk-V4HKT2LV.js → chunk-UTXMQVB2.js} +9 -9
  63. package/dist/{chunk-I5WWEBDG.js → chunk-VHRHI3CU.js} +1 -1
  64. package/dist/{chunk-E4AYSXMS.js → chunk-VW6CVKVA.js} +2 -2
  65. package/dist/{chunk-BAVM2XBR.js → chunk-YAFO7V4A.js} +209 -33
  66. package/dist/{chunk-32P7FTFC.js → chunk-YQ5MFXWG.js} +8 -7
  67. package/dist/{clean-ATMEGSGP.js → clean-263PH5CR.js} +15 -15
  68. package/dist/cli.js +50 -50
  69. package/dist/{cloud-ZFGUHACY.js → cloud-STLYEQNN.js} +4 -4
  70. package/dist/{cloudrun-LVZ4OPPC.js → cloudrun-7ZVNB6FY.js} +3 -3
  71. package/dist/commands/layout-audit.browser.js +178 -202
  72. package/dist/commands/motion-sample.browser.js +24 -71
  73. package/dist/commands/motion-signature.browser.js +374 -0
  74. package/dist/{compare-55LEUY7M.js → compare-33JLJ52D.js} +26 -26
  75. package/dist/{compiler-C32VXLE2.js → compiler-H3QAMMC2.js} +3 -3
  76. package/dist/{component-3EDFANIC.js → component-O5JLM26B.js} +3 -3
  77. package/dist/{compositions-OPU6U6GM.js → compositions-CN4ZVT5G.js} +11 -11
  78. package/dist/{contactSheet-54AJDSR5.js → contactSheet-27SRU7A4.js} +2 -2
  79. package/dist/{delete-PJE4P3HH.js → delete-Z23A45R3.js} +2 -2
  80. package/dist/{deploy-4T3RMA5G.js → deploy-PJLSZMUH.js} +1 -1
  81. package/dist/{destroy-EYDBVHGF.js → destroy-IPUDK6YN.js} +1 -1
  82. package/dist/{dist-YUIPB7OX.js → dist-7VEVT4NT.js} +2 -2
  83. package/dist/{dist-FMHQMEMX.js → dist-ICXYT7XK.js} +1 -1
  84. package/dist/docker/Dockerfile.render +1 -1
  85. package/dist/{doctor-EIVBJJAF.js → doctor-6QLXSGPK.js} +10 -10
  86. package/dist/{events-ISMT4RDW.js → events-EAAIWITP.js} +8 -8
  87. package/dist/{events-NP666TF4.js → events-UA4DJUO2.js} +9 -9
  88. package/dist/{feedback-4OSLMKBW.js → feedback-2CIRCBHY.js} +10 -10
  89. package/dist/{figma-Y7ZHFCOU.js → figma-DFCCII66.js} +3 -3
  90. package/dist/fontLocalizeCli.js +8 -8
  91. package/dist/{get-E4GBRHQK.js → get-UQ2AAXWM.js} +2 -2
  92. package/dist/{gpuPolicy-CA6KRFQQ.js → gpuPolicy-CPCLBMS5.js} +6 -6
  93. package/dist/{grade-compare-B2EHEOLR.js → grade-compare-THUUFHO3.js} +24 -24
  94. package/dist/{help-ISGUWGOR.js → help-SR2MRL3U.js} +1 -1
  95. package/dist/{history-FNRU3TL4.js → history-WLI7KFXB.js} +15 -15
  96. package/dist/hyperframe-runtime.js +49 -49
  97. package/dist/hyperframe.manifest.json +1 -1
  98. package/dist/hyperframe.runtime.iife.js +49 -49
  99. package/dist/hyperframes-player.global.js +1 -1
  100. package/dist/{info-FO7VR5NC.js → info-DFJM3YRJ.js} +11 -11
  101. package/dist/{init-IGW4DTVC.js → init-HEAMOTGH.js} +22 -22
  102. package/dist/{inspect-D4GUGDW7.js → inspect-TO2EXJNK.js} +25 -25
  103. package/dist/{installer-NBEN3ZIP.js → installer-REA7ZASD.js} +4 -4
  104. package/dist/{keyframes-CXZLEC3R.js → keyframes-XHEBAJY7.js} +12 -12
  105. package/dist/{lambda-BFHJMMCG.js → lambda-MSWYKDNT.js} +6 -6
  106. package/dist/{layout-XEDEOLAH.js → layout-UQXCTFFV.js} +25 -25
  107. package/dist/{lint-LLGMCVZ5.js → lint-BSMZ526C.js} +12 -12
  108. package/dist/{lintProject-X7L7EZWZ.js → lintProject-VNE322A7.js} +1 -1
  109. package/dist/{list-LUCWSLK6.js → list-RNP5EMTX.js} +2 -2
  110. package/dist/{login-SMWYXJWJ.js → login-S6V75HG5.js} +4 -4
  111. package/dist/{media-treatment-OYTNU6PX.js → media-treatment-IHEEEDUQ.js} +15 -14
  112. package/dist/{models-RILNQV3Z.js → models-OQEBKNJI.js} +7 -7
  113. package/dist/{motionShot-5F4DCMWC.js → motionShot-EAIFVJ2I.js} +10 -10
  114. package/dist/{normalize-audio-4LHYKFON.js → normalize-audio-4ACUR6W6.js} +11 -11
  115. package/dist/{pipeline-5UY45ELH.js → pipeline-SOQHD5YN.js} +6 -6
  116. package/dist/{play-D4AHRZQD.js → play-3WJPS3FE.js} +20 -20
  117. package/dist/{preflight-3V5IHGWQ.js → preflight-ZUADCOLZ.js} +7 -7
  118. package/dist/{present-VUH7PLOO.js → present-MU23WZZA.js} +16 -16
  119. package/dist/{preview-NCFC65VR.js → preview-VOTNRX67.js} +17 -17
  120. package/dist/{producer-7JGAPEAF.js → producer-HKRMTCPU.js} +1 -1
  121. package/dist/{projectConfig-JHEWPAA5.js → projectConfig-H37MQGE6.js} +6 -6
  122. package/dist/{publication-7XA2H7UB.js → publication-QVDKMAKD.js} +2 -2
  123. package/dist/{publish-257VZMWI.js → publish-54IPKU5O.js} +8 -8
  124. package/dist/{remove-background-EV6XVADN.js → remove-background-KQQ2WPKP.js} +7 -7
  125. package/dist/{render-DHCMWKFV.js → render-HJQ44SEI.js} +12 -12
  126. package/dist/{render-DKXGFQVW.js → render-P7727PLY.js} +21 -21
  127. package/dist/{render-4C3YKVZI.js → render-S2T3SLI4.js} +3 -3
  128. package/dist/{render-batch-AFDBPDMM.js → render-batch-DUFAHD6F.js} +3 -3
  129. package/dist/{renderCancellation-VIZELWDD.js → renderCancellation-BSVXXGZM.js} +6 -6
  130. package/dist/renderSetupWorker.js +1 -1
  131. package/dist/{resolver-S72XBTEQ.js → resolver-PHWKDEH2.js} +4 -4
  132. package/dist/{screenshotCapture-FA5HPN7Q.js → screenshotCapture-ECBTLCU6.js} +3 -3
  133. package/dist/{sherpa-BAADWCHB.js → sherpa-GHQOWXFV.js} +7 -7
  134. package/dist/skills/hyperframes/references/frame-worker-core.md +2 -2
  135. package/dist/skills/hyperframes-cli/references/lint-validate-inspect.md +2 -2
  136. package/dist/skills/media-use/audio/references/bgm.md +1 -1
  137. package/dist/skills/media-use/audio/references/tts.md +1 -1
  138. package/dist/skills/media-use/audio/scripts/lib/python.mjs +19 -5
  139. package/dist/skills/media-use/audio/scripts/lib/python.test.mjs +69 -1
  140. package/dist/{skills-2BGFBUR6.js → skills-SOCMD66Y.js} +40 -14
  141. package/dist/{skillsUpdateCheck-5LYFYHUF.js → skillsUpdateCheck-PONPICQM.js} +3 -3
  142. package/dist/{snapshot-Z37DZL4T.js → snapshot-ECXEZ4TY.js} +50 -29
  143. package/dist/{src-GD7RGV5U.js → src-LVBNYMJW.js} +5 -5
  144. package/dist/{src-GRC63B6L.js → src-RQE5HXZK.js} +7 -7
  145. package/dist/{staticProjectServer-SFINO3DE.js → staticProjectServer-TQYP44CW.js} +14 -14
  146. package/dist/{status-LXQL3L3H.js → status-HVI7QZHE.js} +6 -6
  147. package/dist/studio/assets/{hyperframes-player-DvJeHBHi.js → hyperframes-player-CBpg41JM.js} +1 -1
  148. package/dist/studio/assets/{index-Bt4vDkfw.js → index-BFitquwi.js} +1 -1
  149. package/dist/studio/assets/{index-SFktf7O1.js → index-BKy_birQ.js} +194 -194
  150. package/dist/studio/assets/{index--G6c4Q_x.js → index-CZfrSyU9.js} +1 -1
  151. package/dist/studio/assets/{index-C7apTvpr.css → index-DbfKVTId.css} +1 -1
  152. package/dist/studio/{chunk-PQFEBX32.js → chunk-BIUXP3PG.js} +2 -2
  153. package/dist/studio/{chunk-6LUNLTHR.js → chunk-K47E2ZJA.js} +13 -8
  154. package/dist/studio/chunk-K47E2ZJA.js.map +1 -0
  155. package/dist/studio/{chunk-QUXZVHOJ.js → chunk-VWT7STY3.js} +2 -2
  156. package/dist/studio/{domEditingLayers-JQPQGGT7.js → domEditingLayers-V67MS4WP.js} +3 -3
  157. package/dist/studio/index.d.ts +80 -46
  158. package/dist/studio/index.html +2 -2
  159. package/dist/studio/index.js +7294 -6844
  160. package/dist/studio/index.js.map +1 -1
  161. package/dist/studio/player.js +2 -2
  162. package/dist/studio/styles.css +1 -1
  163. package/dist/{studio-api-WRSCXPWS.js → studio-api-EKF6TFDB.js} +8 -8
  164. package/dist/{studioServer-HTLRGC5G.js → studioServer-BSNDGK63.js} +38 -38
  165. package/dist/{system-EEWYEHYX.js → system-NLNIE7PO.js} +6 -6
  166. package/dist/{telemetry-NLEEMXO2.js → telemetry-XNDAM6JU.js} +9 -9
  167. package/dist/{timeline-7C4KM3KD.js → timeline-IKWTTCNY.js} +15 -15
  168. package/dist/{tokens-QQLJXW4G.js → tokens-KOTFKBAK.js} +2 -2
  169. package/dist/{transcribe-GCKJF5A6.js → transcribe-ELJHKA5B.js} +7 -7
  170. package/dist/{transcribe-TGR5CTA7.js → transcribe-STEYOXIO.js} +12 -12
  171. package/dist/{updateCheck-73JM5QR3.js → updateCheck-OJMEQUST.js} +2 -2
  172. package/dist/{upgrade-V6E5P53C.js → upgrade-NIS32YR6.js} +2 -2
  173. package/dist/{urlDownloader-NYXPEBUN.js → urlDownloader-KJSQAFN7.js} +5 -5
  174. package/dist/{validate-INP7WJAK.js → validate-7MCKNH5K.js} +15 -15
  175. package/dist/{video-KWQ5TSVD.js → video-TUXP3VGQ.js} +17 -9
  176. package/package.json +1 -1
  177. package/dist/chunk-5GQAJ43Q.js +0 -31
  178. package/dist/chunk-YLVI7VL5.js +0 -3541
  179. package/dist/studio/chunk-6LUNLTHR.js.map +0 -1
  180. /package/dist/studio/{chunk-PQFEBX32.js.map → chunk-BIUXP3PG.js.map} +0 -0
  181. /package/dist/studio/{chunk-QUXZVHOJ.js.map → chunk-VWT7STY3.js.map} +0 -0
  182. /package/dist/studio/{domEditingLayers-JQPQGGT7.js.map → domEditingLayers-V67MS4WP.js.map} +0 -0
@@ -1436,7 +1436,7 @@
1436
1436
  );
1437
1437
  }
1438
1438
 
1439
- /** Raw `d`-space endpoints (no CTM) — the mapping authors use when they paste screen coords into `d`. */
1439
+ // Raw `d`-space endpoints (no CTM), as authors paste screen coords into `d`, plus the path length.
1440
1440
  function pathUserEndpoints(path) {
1441
1441
  if (typeof path.getTotalLength !== "function" || typeof path.getPointAtLength !== "function") {
1442
1442
  return null;
@@ -1450,7 +1450,7 @@
1450
1450
  if (!Number.isFinite(total) || total <= 0) return null;
1451
1451
  const start = path.getPointAtLength(0);
1452
1452
  const end = path.getPointAtLength(total);
1453
- return { start: { x: start.x, y: start.y }, end: { x: end.x, y: end.y } };
1453
+ return { start: { x: start.x, y: start.y }, end: { x: end.x, y: end.y }, total };
1454
1454
  }
1455
1455
 
1456
1456
  // Screen endpoints via getScreenCTM (viewBox, preserveAspectRatio, group transforms).
@@ -1502,86 +1502,98 @@
1502
1502
  return { compact, painted };
1503
1503
  }
1504
1504
 
1505
- // Flag only the documented bug: rendered endpoints miss, but user-space-as-screen would attach.
1506
- function connectorDetachmentIssues(root, rootRect, time) {
1507
- const issues = [];
1508
- let anchors = null;
1509
- // Attach near-miss tolerance (screen px). Separate from the closed-glyph chord floor.
1510
- const threshold = Math.max(32, Math.min(rootRect.width, rootRect.height) * 0.02);
1511
- const MIN_CONNECTOR_CHORD_PX = 8;
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) {
1512
1513
  for (const svg of Array.from(root.querySelectorAll("svg"))) {
1513
1514
  if (!isVisibleElement(svg)) continue;
1514
1515
  for (const path of Array.from(svg.querySelectorAll("path"))) {
1515
1516
  if (path.closest(CONNECTOR_SKIP_CONTAINERS)) continue;
1516
1517
  if (!isConnectorPath(svg, path)) continue;
1517
1518
  const user = pathUserEndpoints(path);
1519
+ if (!user || shaftDashHidden(path, user.total)) continue;
1518
1520
  const rendered = pathScreenEndpoints(svg, path, user);
1519
- if (!user || !rendered) continue;
1520
- // Closed/glyph paths collapse to one point — compare in screen px (not user units).
1521
- const renderedChord = Math.hypot(
1521
+ if (!rendered) continue;
1522
+ const chord = Math.hypot(
1522
1523
  rendered.end.x - rendered.start.x,
1523
1524
  rendered.end.y - rendered.start.y,
1524
1525
  );
1525
- if (renderedChord < MIN_CONNECTOR_CHORD_PX) continue;
1526
- if (anchors === null) anchors = connectorAnchorRects(root, rootRect);
1527
- if (anchors.compact.length < 2) return issues;
1528
- // Stable DOM identity across painted (inside) and compact (near-miss) tiers.
1529
- const attachmentKey = (point) => {
1530
- for (const anchor of anchors.painted) {
1531
- if (!anchor.element.contains(svg) && distanceToRect(point, anchor.rect) === 0) {
1532
- return anchor.element;
1533
- }
1534
- }
1535
- for (const anchor of anchors.compact) {
1536
- if (distanceToRect(point, anchor.rect) <= threshold) return anchor.element;
1537
- }
1538
- return null;
1539
- };
1540
- const attached = (point) => attachmentKey(point) !== null;
1541
- // Half-attached as drawn is allowed; only full render-miss proceeds.
1542
- if (attached(rendered.start) || attached(rendered.end)) continue;
1543
- // Paste-into-`d` bug: both raw endpoints land on distinct anchors as screen pixels.
1544
- const userStartKey = attachmentKey(user.start);
1545
- const userEndKey = attachmentKey(user.end);
1546
- const pasteBug = Boolean(userStartKey && userEndKey && userStartKey !== userEndKey);
1547
- // Guessed marked shaft: both frames miss. Same-anchor grazes attach in user-space
1548
- // and must stay skipped. Name-only decorative flow/arrow paths stay skipped.
1549
- // 80px keeps short marker glyphs (chevrons, tips) out.
1550
- const markedMiss =
1551
- renderedChord >= 80 &&
1552
- !userStartKey &&
1553
- !userEndKey &&
1554
- (path.hasAttribute("marker-start") || path.hasAttribute("marker-end"));
1555
- if (!pasteBug && !markedMiss) continue;
1556
- const gap = Math.round(
1557
- Math.min(
1558
- Math.min(...anchors.compact.map((a) => distanceToRect(rendered.start, a.rect))),
1559
- Math.min(...anchors.compact.map((a) => distanceToRect(rendered.end, a.rect))),
1560
- ),
1561
- );
1562
- issues.push({
1563
- code: "connector_detached",
1564
- severity: "warning",
1565
- time,
1566
- selector: selectorFor(path),
1567
- containerSelector: selectorFor(svg),
1568
- message: pasteBug
1569
- ? `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.`
1570
- : `Connector path endpoints render ${gap}px from the nearest anchorable element — a marked shaft that meets no node.`,
1571
- rect: toRect({
1572
- left: Math.min(rendered.start.x, rendered.end.x),
1573
- top: Math.min(rendered.start.y, rendered.end.y),
1574
- right: Math.max(rendered.start.x, rendered.end.x),
1575
- bottom: Math.max(rendered.start.y, rendered.end.y),
1576
- width: Math.abs(rendered.end.x - rendered.start.x),
1577
- height: Math.abs(rendered.end.y - rendered.start.y),
1578
- }),
1579
- fixHint: pasteBug
1580
- ? "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."
1581
- : "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.",
1582
- });
1526
+ yield { svg, path, user, rendered, chord, painted: shaftIsPainted(path) };
1583
1527
  }
1584
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
+ }
1585
1597
  return issues;
1586
1598
  }
1587
1599
 
@@ -1598,19 +1610,63 @@
1598
1610
  return opacityChain(path) >= 0.2;
1599
1611
  }
1600
1612
 
1601
- function shaftDashHidden(path) {
1602
- if (typeof path.getTotalLength !== "function") return false;
1603
- let total;
1604
- try {
1605
- total = path.getTotalLength();
1606
- } catch {
1607
- return false;
1608
- }
1609
- if (!Number.isFinite(total) || total <= 0) return false;
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) {
1610
1617
  const style = getComputedStyle(path);
1611
- const offset = Number.parseFloat(style.strokeDashoffset || "0");
1612
- const dash = Number.parseFloat(String(style.strokeDasharray || "").split(/[\s,]+/)[0] || "0");
1613
- return offset >= total * 0.9 && dash >= total * 0.9;
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);
1614
1670
  }
1615
1671
 
1616
1672
  function connectorEndpointCandidates(root, rootRect) {
@@ -1633,57 +1689,45 @@
1633
1689
  function connectorOrphanIssues(root, rootRect, time) {
1634
1690
  const issues = [];
1635
1691
  let candidates = null;
1636
- const threshold = Math.max(32, Math.min(rootRect.width, rootRect.height) * 0.02);
1637
- for (const svg of Array.from(root.querySelectorAll("svg"))) {
1638
- if (!isVisibleElement(svg)) continue;
1639
- for (const path of Array.from(svg.querySelectorAll("path"))) {
1640
- if (path.closest(CONNECTOR_SKIP_CONTAINERS)) continue;
1641
- if (!isConnectorPath(svg, path)) continue;
1642
- if (!shaftIsPainted(path) || shaftDashHidden(path)) continue;
1643
- const user = pathUserEndpoints(path);
1644
- const rendered = pathScreenEndpoints(svg, path, user);
1645
- if (!user || !rendered) continue;
1646
- const renderedChord = Math.hypot(
1647
- rendered.end.x - rendered.start.x,
1648
- rendered.end.y - rendered.start.y,
1649
- );
1650
- if (renderedChord < 80) continue;
1651
- if (candidates === null) candidates = connectorEndpointCandidates(root, rootRect);
1652
- const dark = [];
1653
- for (const point of [rendered.start, rendered.end]) {
1654
- let best = null;
1655
- let attached = false;
1656
- for (const candidate of candidates) {
1657
- const gap = distanceToRect(point, candidate.rect);
1658
- if (gap > threshold) continue;
1659
- if (isVisibleElement(candidate.element)) {
1660
- attached = true;
1661
- break;
1662
- }
1663
- if (best === null || gap < best.gap) best = { gap, candidate };
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;
1664
1707
  }
1665
- if (!attached && best !== null) dark.push(best.candidate);
1708
+ if (best === null || gap < best.gap) best = { gap, candidate };
1666
1709
  }
1667
- if (dark.length === 0) continue;
1668
- issues.push({
1669
- code: "connector_orphan",
1670
- severity: "warning",
1671
- time,
1672
- selector: selectorFor(path),
1673
- containerSelector: selectorFor(svg),
1674
- message: `Connector shaft is visible while ${dark.length === 2 ? "both endpoints are" : `its endpoint ${selectorFor(dark[0].element)} is`} not on stage.`,
1675
- rect: toRect({
1676
- left: Math.min(rendered.start.x, rendered.end.x),
1677
- top: Math.min(rendered.start.y, rendered.end.y),
1678
- right: Math.max(rendered.start.x, rendered.end.x),
1679
- bottom: Math.max(rendered.start.y, rendered.end.y),
1680
- width: Math.abs(rendered.end.x - rendered.start.x),
1681
- height: Math.abs(rendered.end.y - rendered.start.y),
1682
- }),
1683
- fixHint:
1684
- "Show the shaft only after both ends are on, and hide it with the earlier exit. Do not give the line its own clock.",
1685
- });
1710
+ if (!attached && best !== null) dark.push(best.candidate);
1686
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
+ });
1687
1731
  }
1688
1732
  return issues;
1689
1733
  }
@@ -1807,78 +1851,10 @@
1807
1851
  return contentOverlapIssues(root, time);
1808
1852
  };
1809
1853
 
1810
- // Frozen-sweep guard (#U10, checkPipeline.ts): a compact per-sample
1811
- // fingerprint of every visible element's box + opacity, in DOM order. Node
1812
- // calls this once per seeked grid point and compares the strings across the
1813
- // whole run — if every sample produces the identical string, the seek never
1814
- // actually moved anything and the whole audit run is unreliable. Deliberately
1815
- // a single opaque string (not a structured array) since Node only ever needs
1816
- // equality, not per-element diffing.
1817
- // Pixel-only media motion (a 2D/WebGL canvas repainting or a playing video
1818
- // without any element moving) is invisible to a geometry+opacity fingerprint
1819
- // and false-positives sweep_static. Downsample each visible canvas/video to
1820
- // 8x8 and fold its pixels into the fingerprint. Tainted, zero-sized, or
1821
- // unreadable media hashes to a constant — no worse than geometry-only
1822
- // detection and never a new false negative for DOM-motion compositions.
1823
- // Media inside iframes is intentionally outside this fingerprint: it lives
1824
- // in a separate document, and cross-origin frames are inaccessible under SOP.
1825
- function mediaPixelHash(element) {
1826
- try {
1827
- const rect = element.getBoundingClientRect();
1828
- const sourceWidth = element.videoWidth || element.width || rect.width;
1829
- const sourceHeight = element.videoHeight || element.height || rect.height;
1830
- if (!sourceWidth || !sourceHeight) return "x";
1831
- const off = document.createElement("canvas");
1832
- off.width = 8;
1833
- off.height = 8;
1834
- const ctx = off.getContext("2d");
1835
- if (!ctx) return "x";
1836
- ctx.drawImage(element, 0, 0, 8, 8);
1837
- const data = ctx.getImageData(0, 0, 8, 8).data;
1838
- let hash = 0;
1839
- for (let i = 0; i < data.length; i++) hash = (hash * 31 + data[i]) >>> 0;
1840
- return String(hash);
1841
- } catch {
1842
- return "x";
1843
- }
1844
- }
1845
-
1846
- window.__hyperframesLayoutGeometry = function collectLayoutGeometry() {
1847
- const root =
1848
- document.querySelector("[data-composition-id][data-width][data-height]") ||
1849
- document.querySelector("[data-composition-id]") ||
1850
- document.body;
1851
- const elements = Array.from(root.querySelectorAll("*")).filter((element) =>
1852
- isVisibleElement(element),
1853
- );
1854
- const parts = elements.map((element) => {
1855
- const rect = toRect(element.getBoundingClientRect());
1856
- const opacity = round(opacityChain(element));
1857
- // Variable-font axis animation (font-variation-settings) is a real,
1858
- // visible motion channel that moves no geometry and no opacity, so a
1859
- // box+opacity fingerprint reads it as a frozen timeline. Worse in a
1860
- // DUPLEXED face (Recursive holds an identical advance width at every
1861
- // weight by design), where not even the line width shifts — the whole
1862
- // run then false-positives sweep_static. Fold the computed axis string
1863
- // in; it is "normal" for every element that does not use it, so this
1864
- // adds nothing to the fingerprint of an ordinary composition.
1865
- const axes = getComputedStyle(element).fontVariationSettings;
1866
- return `${rect.left},${rect.top},${rect.width},${rect.height},${opacity},${
1867
- axes && axes !== "normal" ? axes : ""
1868
- }`;
1869
- });
1870
- // img shares the same pixel-only-motion blind spot as canvas/video: an
1871
- // equal-size, equal-position opaque src/visibility swap moves no
1872
- // geometry and no opacity. mediaPixelHash already handles it generically
1873
- // (drawImage accepts any CanvasImageSource; width/height fall back to
1874
- // rect.width/height same as canvas/video), so only the element selector
1875
- // needed widening.
1876
- for (const media of root.querySelectorAll("canvas, video, img")) {
1877
- if (!isVisibleElement(media)) continue;
1878
- parts.push(`p:${mediaPixelHash(media)}`);
1879
- }
1880
- return parts.join("|");
1881
- };
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.
1882
1858
 
1883
1859
  // Rotation-pivot sampling (rotation_pivot_drift). Per sample, report every
1884
1860
  // rotatable candidate's bbox center, size, and current rotation angle. Node
@@ -4,88 +4,32 @@
4
4
  // scope it returns a bucketed signature of all visible elements, so the Node-side
5
5
  // evaluator can detect frozen windows by comparing signatures across frames.
6
6
  (function () {
7
- const IGNORE_TAGS = new Set(["SCRIPT", "STYLE", "TEMPLATE", "NOSCRIPT", "META", "LINK"]);
8
-
9
- function round(value) {
10
- return Math.round(value * 100) / 100;
11
- }
7
+ // Visibility, opacity and the liveness signature come from the shared classifier
8
+ // (motion-signature.browser.js), so this sampler and the frozen-sweep guard agree on what moves.
9
+ // Resolved at install, checked at call: an install-time throw would only surface as an opaque
10
+ // "__hyperframesMotionSample is not a function" from the driver's evaluate.
11
+ const shared = window.__hyperframesMotionSignature;
12
12
 
13
13
  function toRect(rect) {
14
14
  return {
15
- left: round(rect.left),
16
- top: round(rect.top),
17
- right: round(rect.right),
18
- bottom: round(rect.bottom),
19
- width: round(rect.width),
20
- height: round(rect.height),
15
+ left: shared.round(rect.left),
16
+ top: shared.round(rect.top),
17
+ right: shared.round(rect.right),
18
+ bottom: shared.round(rect.bottom),
19
+ width: shared.round(rect.width),
20
+ height: shared.round(rect.height),
21
21
  };
22
22
  }
23
23
 
24
- function opacityChain(element) {
25
- let opacity = 1;
26
- for (let current = element; current; current = current.parentElement) {
27
- const parsed = Number.parseFloat(getComputedStyle(current).opacity || "1");
28
- if (Number.isFinite(parsed)) opacity *= parsed;
29
- }
30
- return opacity;
31
- }
32
-
33
- // Mirrors layout-audit.browser.js isVisibleElement.
34
- // fallow-ignore-next-line complexity
35
- function isVisibleElement(element) {
36
- if (IGNORE_TAGS.has(element.tagName)) return false;
37
- const style = getComputedStyle(element);
38
- if (
39
- style.display === "none" ||
40
- style.visibility === "hidden" ||
41
- style.visibility === "collapse"
42
- ) {
43
- return false;
44
- }
45
- if (opacityChain(element) < 0.2) return false;
46
- const rect = element.getBoundingClientRect();
47
- return rect.width > 0.5 && rect.height > 0.5;
48
- }
49
-
50
24
  function sampleElement(element) {
51
25
  const rect = element.getBoundingClientRect();
52
26
  return {
53
27
  rect: toRect(rect),
54
- opacity: round(opacityChain(element)),
55
- visible: isVisibleElement(element),
28
+ opacity: shared.round(shared.opacityChain(element)),
29
+ visible: shared.isVisibleElement(element),
56
30
  };
57
31
  }
58
32
 
59
- function compositionRoot() {
60
- return document.querySelector("[data-composition-id]") || document.body;
61
- }
62
-
63
- // ponytail: bucket position to 2px and opacity to 0.08 so the RFC's "moves ≥2px /
64
- // opacity ≥0.08" thresholds fall out of bucketing. Boundary-straddling moves are
65
- // approximate — good enough for liveness; tighten only if false negatives show up.
66
- function elementSignature(element) {
67
- const rect = element.getBoundingClientRect();
68
- const bx = Math.round(rect.left / 2);
69
- const by = Math.round(rect.top / 2);
70
- const bw = Math.round(rect.width / 2);
71
- const bh = Math.round(rect.height / 2);
72
- const bo = Math.round(opacityChain(element) / 0.08);
73
- return bx + "," + by + "," + bw + "," + bh + "," + bo;
74
- }
75
-
76
- function livenessSignature(root) {
77
- if (!root) return "";
78
- const parts = [];
79
- // ponytail: O(DOM) × MOTION_MAX_SAMPLES (300) — fine for typical compositions;
80
- // narrow selector (e.g. "[id],[class]") if heavy-DOM compositions slow this down.
81
- const all = root.querySelectorAll("*");
82
- for (const element of all) {
83
- if (!isVisibleElement(element)) continue;
84
- parts.push(elementSignature(element));
85
- }
86
- return parts.join("|");
87
- }
88
-
89
33
  function safeQuery(selector) {
90
34
  try {
91
35
  return document.querySelector(selector);
@@ -108,13 +52,22 @@
108
52
  function sampleLiveness(scopes) {
109
53
  const liveness = {};
110
54
  for (const scope of scopes) {
111
- const root = scope === "*" ? compositionRoot() : safeQuery(scope);
112
- liveness[scope] = livenessSignature(root);
55
+ const root = scope === "*" ? shared.compositionRoot() : safeQuery(scope);
56
+ // ponytail: O(DOM) × MOTION_MAX_SAMPLES (300), with three computed-style
57
+ // reads (element + ::before/::after) plus an ancestor walk per box-generating
58
+ // element — fine for typical compositions; narrow the scope if heavy-DOM
59
+ // compositions slow down.
60
+ liveness[scope] = shared.compositionSignature(root, { quantize: true });
113
61
  }
114
62
  return liveness;
115
63
  }
116
64
 
117
65
  window.__hyperframesMotionSample = function motionSample(options) {
66
+ if (!shared) {
67
+ throw new Error(
68
+ "motion-signature.browser.js must be injected before motion-sample.browser.js",
69
+ );
70
+ }
118
71
  const { selectors = [], livenessScopes = [] } = options || {};
119
72
  return { data: sampleSelectors(selectors), liveness: sampleLiveness(livenessScopes) };
120
73
  };