hyperframes 0.8.111 → 0.8.113
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{add-M5WN2AM2.js → add-ILIGSLV4.js} +15 -15
- package/dist/{agentPromptGenerator-QL2KAWEP.js → agentPromptGenerator-VIIPZCJ3.js} +1 -1
- package/dist/{animatedGifPrep-NMGFOSIL.js → animatedGifPrep-U7FMC7DB.js} +6 -6
- package/dist/{asset-GUASPIKN.js → asset-OFDEIAGM.js} +2 -2
- package/dist/{assetCataloger-FXEHOUA3.js → assetCataloger-5LCGR5XU.js} +2 -2
- package/dist/{auth-3YMEVUKJ.js → auth-5JDDEX2M.js} +2 -2
- package/dist/{autoUpdate-FYCGHBPC.js → autoUpdate-JXVZVJCV.js} +2 -2
- package/dist/{backgroundChecks-ZLYIRMTS.js → backgroundChecks-BBWGY3V6.js} +3 -3
- package/dist/backgroundChecksWorker.js +3 -3
- package/dist/{batchRender-ZTHFMHZY.js → batchRender-QVJ7NEUH.js} +3 -3
- package/dist/{beats-GXY5ZBPB.js → beats-YEYVQMX6.js} +11 -11
- package/dist/{benchmark-MJFVBVNW.js → benchmark-J2V46VWI.js} +12 -12
- package/dist/{browser-ZDI7IX6H.js → browser-XBWASLD2.js} +11 -11
- package/dist/{bundleWithLocalizedFonts-4Q3ETTHW.js → bundleWithLocalizedFonts-4CVITCYK.js} +2 -2
- package/dist/{capture-4OPH4PMK.js → capture-F2Y7TIHU.js} +41 -43
- package/dist/{capture-OG6532RA.js → capture-GLHK4ST7.js} +3 -3
- package/dist/{captureFile-JOPOCRU7.js → captureFile-LYOJOUIE.js} +3 -1
- package/dist/{catalog-23DP7KCW.js → catalog-3QDMTDK2.js} +15 -15
- package/dist/{check-5X6BJA2P.js → check-O3RR4NWI.js} +52 -40
- package/dist/{checkBrowser-3QHBTMYM.js → checkBrowser-K36DAG4S.js} +32 -31
- package/dist/{chunk-NIGUNLOO.js → chunk-2DBYGR4N.js} +10 -9
- package/dist/{chunk-TIZQC44K.js → chunk-3IUSKIZW.js} +1 -1
- package/dist/{chunk-YLOSZHA2.js → chunk-3XPZJUKM.js} +2 -2
- package/dist/{chunk-32P7FTFC.js → chunk-4KGMIOMY.js} +9 -8
- package/dist/{chunk-N34BYX3E.js → chunk-4PFZPP2N.js} +54 -30
- package/dist/{chunk-JTTAS7N6.js → chunk-7543YIMQ.js} +1 -1
- package/dist/{chunk-AMSBTN6L.js → chunk-7CGIICLA.js} +3 -3
- package/dist/{chunk-LAQEZHOH.js → chunk-BOUDTDBQ.js} +1 -1
- package/dist/chunk-C6AIZOQN.js +3541 -0
- package/dist/{chunk-EE6SV6KW.js → chunk-DDK22K3I.js} +3 -3
- package/dist/{chunk-QXERSWXX.js → chunk-DQBY7FRR.js} +123 -3
- package/dist/{chunk-HQGJLZB6.js → chunk-DR7QW6C5.js} +2 -2
- package/dist/{chunk-E4AYSXMS.js → chunk-E52XEEAA.js} +3 -3
- package/dist/{chunk-J4RPT4QW.js → chunk-E6RPWJVC.js} +1 -1
- package/dist/{chunk-QMAXDSCH.js → chunk-EMZF2TPA.js} +2 -2
- package/dist/{chunk-XYVFPDNU.js → chunk-ENHAGQEP.js} +1 -1
- package/dist/{chunk-B25H4M4V.js → chunk-FCEFKWPM.js} +1 -1
- package/dist/{chunk-BQYGGX3B.js → chunk-FW7K32YT.js} +1 -1
- package/dist/{chunk-6CI65KCP.js → chunk-FXHVADV6.js} +1 -1
- package/dist/{chunk-OF3RKOPU.js → chunk-G2T2X6FZ.js} +189 -111
- package/dist/{chunk-5ON5GH3X.js → chunk-GDJ74GHW.js} +4 -3
- package/dist/{chunk-I5WWEBDG.js → chunk-HTQR7SUZ.js} +1 -1
- package/dist/{chunk-TQKKC4MB.js → chunk-HYXL3VJA.js} +5 -5
- package/dist/{chunk-AWUZ3AUD.js → chunk-IP3XINRB.js} +1 -1
- package/dist/{chunk-HPEHQUJX.js → chunk-IRJBTYJF.js} +1 -1
- package/dist/chunk-K4WKGXRC.js +18 -0
- package/dist/{chunk-CQRF7VU3.js → chunk-L5QASNBN.js} +1 -1
- package/dist/{chunk-ITBEN3QD.js → chunk-L5SYYW2P.js} +2 -2
- package/dist/{chunk-MGFVNTIB.js → chunk-LMC46FK5.js} +2 -2
- package/dist/chunk-MRF4AMKD.js +104 -0
- package/dist/{chunk-U4TDAJZE.js → chunk-OCNCK5FQ.js} +2 -2
- package/dist/{chunk-JWPSBANO.js → chunk-OEA2J2TF.js} +3 -3
- package/dist/{chunk-N32ITJ7L.js → chunk-P3FGUSFK.js} +15 -12
- package/dist/{chunk-2JXJ5SGK.js → chunk-QPR4N2D4.js} +6 -6
- package/dist/{chunk-LFUXJBYA.js → chunk-R4NWAW35.js} +7 -7
- package/dist/{chunk-C4TZBEHY.js → chunk-SIN32SJB.js} +2 -2
- package/dist/{chunk-M224DAMG.js → chunk-T3NMEYGM.js} +1 -1
- package/dist/{chunk-Q2QNAFQV.js → chunk-U4VSY56O.js} +1 -1
- package/dist/{chunk-NH75EKOP.js → chunk-VFCKXTBB.js} +6 -3
- package/dist/{chunk-V4HKT2LV.js → chunk-VIJ35BJ6.js} +10 -10
- package/dist/{chunk-5ER3PYYP.js → chunk-VLC7JS4K.js} +1 -1
- package/dist/{chunk-MR2LOG32.js → chunk-VTP4VSRP.js} +8 -8
- package/dist/{chunk-ZU54D6MI.js → chunk-XFJXFNJF.js} +3 -3
- package/dist/{chunk-YIXEH4TF.js → chunk-XXKBPGPJ.js} +2 -2
- package/dist/{chunk-JUP65S7D.js → chunk-XZM2I47O.js} +3 -3
- package/dist/{chunk-BAVM2XBR.js → chunk-YJEGT5L6.js} +209 -33
- package/dist/{chunk-CFC3EEDQ.js → chunk-ZMMSMDCA.js} +1 -1
- package/dist/{chunk-PFEKMIN5.js → chunk-ZYSS3JED.js} +1 -1
- package/dist/{chunk-ZXSNGIYS.js → chunk-ZZIXIOK2.js} +4 -4
- package/dist/{clean-ATMEGSGP.js → clean-RPB34TT6.js} +16 -16
- package/dist/cli.js +122 -54
- package/dist/{cloud-ZFGUHACY.js → cloud-QECFFPKC.js} +4 -4
- package/dist/{cloudrun-LVZ4OPPC.js → cloudrun-BML6ZG7K.js} +3 -3
- package/dist/commands/layout-audit.browser.js +178 -202
- package/dist/commands/motion-sample.browser.js +24 -71
- package/dist/commands/motion-signature.browser.js +374 -0
- package/dist/{compare-55LEUY7M.js → compare-PF6WHQ4T.js} +26 -26
- package/dist/{compiler-C32VXLE2.js → compiler-ZHVIEP26.js} +3 -3
- package/dist/{component-3EDFANIC.js → component-4ENCNLED.js} +3 -3
- package/dist/{compositions-OPU6U6GM.js → compositions-5UA33RSG.js} +11 -11
- package/dist/{contactSheet-54AJDSR5.js → contactSheet-27SRU7A4.js} +2 -2
- package/dist/{delete-PJE4P3HH.js → delete-SARS5LIK.js} +2 -2
- package/dist/{deploy-4T3RMA5G.js → deploy-MX76RZ36.js} +1 -1
- package/dist/{destroy-EYDBVHGF.js → destroy-H2TZBYHH.js} +1 -1
- package/dist/{dist-FMHQMEMX.js → dist-HRBNHR4M.js} +1 -1
- package/dist/{dist-YUIPB7OX.js → dist-OKRUIHYU.js} +2 -2
- package/dist/docker/Dockerfile.render +1 -1
- package/dist/{doctor-EIVBJJAF.js → doctor-TSAQGYP4.js} +11 -11
- package/dist/{events-NP666TF4.js → events-3HUO73ZE.js} +9 -9
- package/dist/{events-ISMT4RDW.js → events-SZ5NWYWD.js} +8 -8
- package/dist/{feedback-4OSLMKBW.js → feedback-42OTOFC6.js} +10 -10
- package/dist/{figma-Y7ZHFCOU.js → figma-RYNFCYXB.js} +3 -3
- package/dist/fontLocalizeCli.js +8 -8
- package/dist/{get-E4GBRHQK.js → get-IRHLG5OQ.js} +2 -2
- package/dist/{gpuPolicy-CA6KRFQQ.js → gpuPolicy-LRMOZXFU.js} +6 -6
- package/dist/{grade-compare-B2EHEOLR.js → grade-compare-KGJP5HV7.js} +24 -24
- package/dist/{help-ISGUWGOR.js → help-C5HS56RZ.js} +1 -1
- package/dist/{history-FNRU3TL4.js → history-DFJNDM2T.js} +16 -17
- package/dist/hyperframe-runtime.js +54 -54
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +54 -54
- package/dist/hyperframes-player.global.js +1 -1
- package/dist/{info-FO7VR5NC.js → info-26GTDTKN.js} +11 -11
- package/dist/{init-IGW4DTVC.js → init-RXA6NI73.js} +25 -24
- package/dist/{inspect-D4GUGDW7.js → inspect-2S5GAZYC.js} +25 -25
- package/dist/{installer-NBEN3ZIP.js → installer-DGYYDGWJ.js} +4 -4
- package/dist/{keyframes-CXZLEC3R.js → keyframes-WBMAX7XZ.js} +12 -12
- package/dist/{lambda-BFHJMMCG.js → lambda-OQBFCEFA.js} +6 -6
- package/dist/{layout-XEDEOLAH.js → layout-ZRU7PXBG.js} +25 -25
- package/dist/{lint-LLGMCVZ5.js → lint-ATVNZ6FG.js} +12 -12
- package/dist/{lintProject-X7L7EZWZ.js → lintProject-TOLC6HDS.js} +1 -1
- package/dist/{list-LUCWSLK6.js → list-E2GHYPCQ.js} +2 -2
- package/dist/{login-SMWYXJWJ.js → login-LOC4KA7H.js} +4 -4
- package/dist/{manager-NEVLEZK3.js → manager-CT3D67CG.js} +1 -1
- package/dist/{media-treatment-OYTNU6PX.js → media-treatment-L43MVH2V.js} +15 -14
- package/dist/{models-RILNQV3Z.js → models-JF5OOZ5A.js} +7 -7
- package/dist/{motionShot-5F4DCMWC.js → motionShot-BJ5CLYMN.js} +12 -12
- package/dist/{normalize-audio-4LHYKFON.js → normalize-audio-K33FKGAF.js} +11 -11
- package/dist/{pipeline-5UY45ELH.js → pipeline-IWPF5ZMK.js} +6 -6
- package/dist/{play-D4AHRZQD.js → play-PHHXYCQI.js} +20 -20
- package/dist/{preflight-3V5IHGWQ.js → preflight-2GH2Z4GZ.js} +8 -8
- package/dist/{present-VUH7PLOO.js → present-KBFK2YVB.js} +16 -16
- package/dist/{preview-NCFC65VR.js → preview-T3E5TFDC.js} +159 -87
- package/dist/{producer-7JGAPEAF.js → producer-SL66UNEP.js} +1 -1
- package/dist/{projectConfig-JHEWPAA5.js → projectConfig-7AKXZNNR.js} +6 -6
- package/dist/{publication-7XA2H7UB.js → publication-B2OERUY7.js} +2 -2
- package/dist/{publish-257VZMWI.js → publish-ZGKT7KED.js} +8 -8
- package/dist/{remove-background-EV6XVADN.js → remove-background-4LRTT6YL.js} +7 -7
- package/dist/{render-DHCMWKFV.js → render-H6RSJF2V.js} +12 -12
- package/dist/{render-DKXGFQVW.js → render-OKNPNMFS.js} +23 -23
- package/dist/{render-4C3YKVZI.js → render-TGCDBKMP.js} +3 -3
- package/dist/{render-batch-AFDBPDMM.js → render-batch-WRQFW4UC.js} +3 -3
- package/dist/{renderCancellation-VIZELWDD.js → renderCancellation-BFCQYGVV.js} +6 -6
- package/dist/renderSetupWorker.js +2 -2
- package/dist/{resolver-S72XBTEQ.js → resolver-37UOGAMT.js} +4 -4
- package/dist/{screenshotCapture-FA5HPN7Q.js → screenshotCapture-ECBTLCU6.js} +3 -3
- package/dist/{sherpa-BAADWCHB.js → sherpa-SDVKYXCX.js} +7 -7
- package/dist/skills/hyperframes/references/frame-worker-core.md +2 -2
- package/dist/skills/hyperframes-cli/references/lint-validate-inspect.md +2 -2
- package/dist/skills/media-use/audio/references/bgm.md +1 -1
- package/dist/skills/media-use/audio/references/tts.md +1 -1
- package/dist/skills/media-use/audio/scripts/lib/python.mjs +19 -5
- package/dist/skills/media-use/audio/scripts/lib/python.test.mjs +69 -1
- package/dist/{skills-2BGFBUR6.js → skills-O25ZTFTO.js} +40 -14
- package/dist/{skillsUpdateCheck-5LYFYHUF.js → skillsUpdateCheck-Y2V4OMMT.js} +3 -3
- package/dist/{snapshot-Z37DZL4T.js → snapshot-KGM2HZIC.js} +52 -31
- package/dist/{src-GD7RGV5U.js → src-PJJ6NISA.js} +5 -5
- package/dist/{src-GRC63B6L.js → src-PLWCT745.js} +7 -7
- package/dist/{staticProjectServer-SFINO3DE.js → staticProjectServer-OCE7GU5S.js} +14 -14
- package/dist/{status-LXQL3L3H.js → status-LENRPQS5.js} +6 -6
- package/dist/studio/assets/{hyperframes-player-DvJeHBHi.js → hyperframes-player-DzJbtQVO.js} +1 -1
- package/dist/studio/assets/{index-SFktf7O1.js → index-DRhF3v4Z.js} +194 -194
- package/dist/studio/assets/{index-C7apTvpr.css → index-DbfKVTId.css} +1 -1
- package/dist/studio/assets/{index--G6c4Q_x.js → index-DgwDQK5G.js} +1 -1
- package/dist/studio/assets/{index-Bt4vDkfw.js → index-n5FUn1xv.js} +1 -1
- package/dist/studio/{chunk-PQFEBX32.js → chunk-BIUXP3PG.js} +2 -2
- package/dist/studio/{chunk-6LUNLTHR.js → chunk-K47E2ZJA.js} +13 -8
- package/dist/studio/chunk-K47E2ZJA.js.map +1 -0
- package/dist/studio/{chunk-QUXZVHOJ.js → chunk-VWT7STY3.js} +2 -2
- package/dist/studio/{domEditingLayers-JQPQGGT7.js → domEditingLayers-V67MS4WP.js} +3 -3
- package/dist/studio/index.d.ts +80 -46
- package/dist/studio/index.html +2 -2
- package/dist/studio/index.js +7294 -6844
- package/dist/studio/index.js.map +1 -1
- package/dist/studio/player.js +2 -2
- package/dist/studio/styles.css +1 -1
- package/dist/{studio-api-WRSCXPWS.js → studio-api-IEQ5BIHT.js} +8 -8
- package/dist/{studioSelectionClient-A6JCWRRW.js → studioSelectionClient-IUUPKUHZ.js} +1 -2
- package/dist/{studioServer-HTLRGC5G.js → studioServer-LXJ34MGN.js} +43 -43
- package/dist/{system-EEWYEHYX.js → system-4KRSV2B7.js} +6 -6
- package/dist/{telemetry-NLEEMXO2.js → telemetry-R3BPD6VK.js} +9 -9
- package/dist/{timeline-7C4KM3KD.js → timeline-N5UKVS2S.js} +15 -15
- package/dist/{tokens-QQLJXW4G.js → tokens-774UBF2R.js} +2 -2
- package/dist/{transcribe-GCKJF5A6.js → transcribe-3UXXEDXX.js} +7 -7
- package/dist/{transcribe-TGR5CTA7.js → transcribe-IP74EZLM.js} +12 -12
- package/dist/{tts-DO2W6NLB.js → tts-P3LIPIL5.js} +13 -6
- package/dist/{updateCheck-73JM5QR3.js → updateCheck-EXHNAPKI.js} +2 -2
- package/dist/{upgrade-V6E5P53C.js → upgrade-A4M3O2F2.js} +2 -2
- package/dist/{urlDownloader-NYXPEBUN.js → urlDownloader-4KNER64L.js} +5 -5
- package/dist/{validate-INP7WJAK.js → validate-DV75GM6J.js} +15 -15
- package/dist/{video-KWQ5TSVD.js → video-GINAMW5I.js} +17 -9
- package/package.json +1 -1
- package/dist/chunk-5GQAJ43Q.js +0 -31
- package/dist/chunk-VWA4HODG.js +0 -129
- package/dist/chunk-YLVI7VL5.js +0 -3541
- package/dist/studio/chunk-6LUNLTHR.js.map +0 -1
- /package/dist/studio/{chunk-PQFEBX32.js.map → chunk-BIUXP3PG.js.map} +0 -0
- /package/dist/studio/{chunk-QUXZVHOJ.js.map → chunk-VWT7STY3.js.map} +0 -0
- /package/dist/studio/{domEditingLayers-JQPQGGT7.js.map → domEditingLayers-V67MS4WP.js.map} +0 -0
|
@@ -1436,7 +1436,7 @@
|
|
|
1436
1436
|
);
|
|
1437
1437
|
}
|
|
1438
1438
|
|
|
1439
|
-
|
|
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
|
-
//
|
|
1506
|
-
function
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
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 (!
|
|
1520
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
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
|
|
1612
|
-
|
|
1613
|
-
|
|
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 =
|
|
1637
|
-
for (const svg of
|
|
1638
|
-
if (!
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
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 (
|
|
1708
|
+
if (best === null || gap < best.gap) best = { gap, candidate };
|
|
1666
1709
|
}
|
|
1667
|
-
if (
|
|
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
|
-
//
|
|
1811
|
-
//
|
|
1812
|
-
//
|
|
1813
|
-
//
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
};
|