hyperframes 0.7.57 → 0.7.59
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/cli.js +4218 -2643
- package/dist/commands/contrast-audit.browser.js +69 -19
- package/dist/commands/layout-audit.browser.js +168 -35
- package/dist/hyperframe-runtime.js +27 -27
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +27 -27
- package/dist/hyperframes-player.global.js +1 -1
- package/dist/skills/hyperframes/SKILL.md +103 -124
- package/dist/skills/hyperframes/references/capability-menu.md +47 -0
- package/dist/skills/hyperframes/references/pitch-round.md +40 -0
- package/dist/skills/hyperframes/references/route-briefs.md +85 -0
- package/dist/skills/hyperframes/references/skill-lifecycle.md +33 -0
- package/dist/skills/hyperframes/references/workflow-catalog.md +70 -0
- package/dist/skills/hyperframes-cli/SKILL.md +99 -109
- package/dist/skills/hyperframes-cli/references/beats.md +19 -0
- package/dist/skills/hyperframes-cli/references/cloud.md +8 -3
- package/dist/skills/hyperframes-cli/references/cloudrun.md +62 -0
- package/dist/skills/hyperframes-cli/references/compare-and-batch.md +105 -0
- package/dist/skills/hyperframes-cli/references/lambda.md +77 -24
- package/dist/skills/hyperframes-cli/references/lint-validate-inspect.md +3 -3
- package/dist/skills/hyperframes-cli/references/preview-render.md +7 -3
- package/dist/skills/hyperframes-cli/references/upgrade-info-misc.md +2 -0
- package/dist/studio/assets/{hyperframes-player--Z69cEkE.js → hyperframes-player-Bm07FLkl.js} +1 -1
- package/dist/studio/assets/{index-KsfE1bUu.js → index-B995FG46.js} +1 -1
- package/dist/studio/assets/index-CrkAdJkb.js +426 -0
- package/dist/studio/assets/index-Dj5p8U_A.css +1 -0
- package/dist/studio/assets/{index-Bf1x1y8H.js → index-FvzmPhfG.js} +1 -1
- package/dist/studio/{chunk-5QSIMBEJ.js → chunk-OBAG3GWK.js} +33 -21
- package/dist/studio/chunk-OBAG3GWK.js.map +1 -0
- package/dist/studio/{domEditingLayers-6LQGKPOI.js → domEditingLayers-2CKWGEHS.js} +2 -2
- package/dist/studio/index.d.ts +27 -7
- package/dist/studio/index.html +2 -2
- package/dist/studio/index.js +18029 -11563
- package/dist/studio/index.js.map +1 -1
- package/dist/studio/styles/tailwind-preset.d.ts +5 -0
- package/dist/studio/styles/tailwind-preset.js +8 -1
- package/dist/studio/styles/tailwind-preset.js.map +1 -1
- package/dist/templates/_shared/AGENTS.md +5 -4
- package/dist/templates/_shared/CLAUDE.md +5 -4
- package/package.json +2 -2
- package/dist/studio/assets/index-DfmYkU44.js +0 -423
- package/dist/studio/assets/index-_pqzyxB1.css +0 -1
- package/dist/studio/chunk-5QSIMBEJ.js.map +0 -1
- /package/dist/studio/{domEditingLayers-6LQGKPOI.js.map → domEditingLayers-2CKWGEHS.js.map} +0 -0
|
@@ -58,28 +58,29 @@ window.__contrastAuditPrepare = function () {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
function parseColor(c) {
|
|
61
|
-
var m = c.match(
|
|
62
|
-
if (!m)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
61
|
+
var m = (c || "").match(/^rgba?\(([^)]+)\)$/i);
|
|
62
|
+
if (!m) {
|
|
63
|
+
var mix = (c || "").match(
|
|
64
|
+
/^color-mix\(\s*in\s+srgb\s*,\s*(rgba?\([^)]+\))\s+(\d+(?:\.\d+)?|\.\d+)%\s*,\s*(rgba?\([^)]+\))\s+(\d+(?:\.\d+)?|\.\d+)%\s*\)$/i,
|
|
65
|
+
);
|
|
66
|
+
if (!mix) return null;
|
|
67
|
+
var first = parseColor(mix[1]);
|
|
68
|
+
var second = parseColor(mix[3]);
|
|
69
|
+
var firstWeight = parseFloat(mix[2]);
|
|
70
|
+
var secondWeight = parseFloat(mix[4]);
|
|
71
|
+
var weightTotal = firstWeight + secondWeight;
|
|
72
|
+
if (!first || !second || !isFinite(weightTotal) || weightTotal <= 0) return null;
|
|
73
|
+
return first.map(function (channel, index) {
|
|
74
|
+
return (channel * firstWeight + second[index] * secondWeight) / weightTotal;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
77
|
var p = m[1].split(",").map(function (s) {
|
|
78
78
|
return parseFloat(s.trim());
|
|
79
79
|
});
|
|
80
80
|
if (
|
|
81
|
+
(p.length !== 3 && p.length !== 4) ||
|
|
81
82
|
p.some(function (v) {
|
|
82
|
-
return
|
|
83
|
+
return !isFinite(v);
|
|
83
84
|
})
|
|
84
85
|
)
|
|
85
86
|
return null;
|
|
@@ -218,8 +219,12 @@ window.__contrastAuditPrepare = function () {
|
|
|
218
219
|
// `color` rather than crashing parseColor or reporting a fabricated
|
|
219
220
|
// black.
|
|
220
221
|
var isSvgText = isSvgTextElement(el);
|
|
221
|
-
var fg = isSvgText ?
|
|
222
|
+
var fg = isSvgText ? parseColor(cs.fill) || parseColor(cs.color) : parseColor(cs.color);
|
|
223
|
+
if (!fg) continue;
|
|
222
224
|
if (fg[3] <= 0.01) continue;
|
|
225
|
+
var strokeWidth = parseFloat(cs.webkitTextStrokeWidth || "0");
|
|
226
|
+
var stroke = strokeWidth > 0 ? parseColor(cs.webkitTextStrokeColor || "") : null;
|
|
227
|
+
if (stroke && stroke[3] <= 0.01) stroke = null;
|
|
223
228
|
|
|
224
229
|
var fontSize = parseFloat(cs.fontSize);
|
|
225
230
|
var fontWeight = Number(cs.fontWeight) || 400;
|
|
@@ -251,6 +256,13 @@ window.__contrastAuditPrepare = function () {
|
|
|
251
256
|
origFillPriority = el.style.getPropertyPriority("fill");
|
|
252
257
|
el.style.setProperty("fill", "transparent", "important");
|
|
253
258
|
}
|
|
259
|
+
var origStrokeColor = null,
|
|
260
|
+
origStrokeColorPriority = null;
|
|
261
|
+
if (stroke) {
|
|
262
|
+
origStrokeColor = el.style.getPropertyValue("-webkit-text-stroke-color");
|
|
263
|
+
origStrokeColorPriority = el.style.getPropertyPriority("-webkit-text-stroke-color");
|
|
264
|
+
el.style.setProperty("-webkit-text-stroke-color", "transparent", "important");
|
|
265
|
+
}
|
|
254
266
|
restores.push({
|
|
255
267
|
el: el,
|
|
256
268
|
origTransition: origTransition,
|
|
@@ -259,6 +271,9 @@ window.__contrastAuditPrepare = function () {
|
|
|
259
271
|
origColorPriority: origColorPriority,
|
|
260
272
|
origFill: origFill,
|
|
261
273
|
origFillPriority: origFillPriority,
|
|
274
|
+
origStrokeColor: origStrokeColor,
|
|
275
|
+
origStrokeColorPriority: origStrokeColorPriority,
|
|
276
|
+
hasStroke: !!stroke,
|
|
262
277
|
isSvgText: isSvgText,
|
|
263
278
|
});
|
|
264
279
|
|
|
@@ -266,6 +281,7 @@ window.__contrastAuditPrepare = function () {
|
|
|
266
281
|
selector: selectorOf(el),
|
|
267
282
|
text: (el.textContent || "").trim().slice(0, 50),
|
|
268
283
|
fg: fg,
|
|
284
|
+
stroke: stroke,
|
|
269
285
|
fontSize: fontSize,
|
|
270
286
|
fontWeight: fontWeight,
|
|
271
287
|
large: large,
|
|
@@ -287,6 +303,15 @@ function __contrastAuditRestoreAll() {
|
|
|
287
303
|
if (r.origFill) r.el.style.setProperty("fill", r.origFill, r.origFillPriority);
|
|
288
304
|
else r.el.style.removeProperty("fill");
|
|
289
305
|
}
|
|
306
|
+
if (r.hasStroke) {
|
|
307
|
+
if (r.origStrokeColor)
|
|
308
|
+
r.el.style.setProperty(
|
|
309
|
+
"-webkit-text-stroke-color",
|
|
310
|
+
r.origStrokeColor,
|
|
311
|
+
r.origStrokeColorPriority,
|
|
312
|
+
);
|
|
313
|
+
else r.el.style.removeProperty("-webkit-text-stroke-color");
|
|
314
|
+
}
|
|
290
315
|
if (r.origTransition)
|
|
291
316
|
r.el.style.setProperty("transition", r.origTransition, r.origTransitionPriority);
|
|
292
317
|
else r.el.style.removeProperty("transition");
|
|
@@ -371,17 +396,25 @@ window.__contrastAuditFinish = async function (imgBase64, time, candidates) {
|
|
|
371
396
|
var stepY = Math.max(1, Math.floor((y1 - y0) / 6));
|
|
372
397
|
var rr = [],
|
|
373
398
|
gg = [],
|
|
374
|
-
bb = []
|
|
399
|
+
bb = [],
|
|
400
|
+
aa = [];
|
|
375
401
|
for (var y = y0; y <= y1; y += stepY) {
|
|
376
402
|
for (var x = x0; x <= x1; x += stepX) {
|
|
377
403
|
var idx = (y * w + x) * 4;
|
|
378
404
|
rr.push(px[idx]);
|
|
379
405
|
gg.push(px[idx + 1]);
|
|
380
406
|
bb.push(px[idx + 2]);
|
|
407
|
+
aa.push(px[idx + 3]);
|
|
381
408
|
}
|
|
382
409
|
}
|
|
383
410
|
if (rr.length === 0) continue;
|
|
384
411
|
|
|
412
|
+
// A transparent sampled backdrop is supplied by the downstream editor,
|
|
413
|
+
// not this composition. WCAG contrast cannot be inferred until that live
|
|
414
|
+
// video/image is known, so do not invent an opaque browser default and
|
|
415
|
+
// hard-fail an otherwise valid alpha overlay.
|
|
416
|
+
if (median(aa) < 255) continue;
|
|
417
|
+
|
|
385
418
|
var bgR = median(rr),
|
|
386
419
|
bgG = median(gg),
|
|
387
420
|
bgB = median(bb);
|
|
@@ -394,6 +427,23 @@ window.__contrastAuditFinish = async function (imgBase64, time, candidates) {
|
|
|
394
427
|
|
|
395
428
|
var ratio = +wcagRatio(compR, compG, compB, bgR, bgG, bgB).toFixed(2);
|
|
396
429
|
|
|
430
|
+
// A solid text stroke is part of the visible glyph paint. Use whichever
|
|
431
|
+
// of fill or stroke provides the stronger edge contrast, rather than
|
|
432
|
+
// failing readable outlined captions based on fill alone.
|
|
433
|
+
if (c.stroke) {
|
|
434
|
+
var stroke = c.stroke;
|
|
435
|
+
var strokeR = Math.round(stroke[0] * stroke[3] + bgR * (1 - stroke[3]));
|
|
436
|
+
var strokeG = Math.round(stroke[1] * stroke[3] + bgG * (1 - stroke[3]));
|
|
437
|
+
var strokeB = Math.round(stroke[2] * stroke[3] + bgB * (1 - stroke[3]));
|
|
438
|
+
var strokeRatio = +wcagRatio(strokeR, strokeG, strokeB, bgR, bgG, bgB).toFixed(2);
|
|
439
|
+
if (strokeRatio > ratio) {
|
|
440
|
+
compR = strokeR;
|
|
441
|
+
compG = strokeG;
|
|
442
|
+
compB = strokeB;
|
|
443
|
+
ratio = strokeRatio;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
397
447
|
out.push({
|
|
398
448
|
time: time,
|
|
399
449
|
selector: c.selector,
|
|
@@ -105,6 +105,10 @@
|
|
|
105
105
|
return !!element.closest("[data-layout-allow-overflow]");
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
function hasTextClipOptOut(element) {
|
|
109
|
+
return hasAllowOverflowFlag(element) || element.hasAttribute("data-layout-bleed");
|
|
110
|
+
}
|
|
111
|
+
|
|
108
112
|
function opacityChain(element) {
|
|
109
113
|
let opacity = 1;
|
|
110
114
|
for (let current = element; current; current = current.parentElement) {
|
|
@@ -215,10 +219,10 @@
|
|
|
215
219
|
const text = textContentFor(element, directOnly);
|
|
216
220
|
if (!text) return false;
|
|
217
221
|
if (directOnly) return true;
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
return true;
|
|
222
|
+
// Aggregate text may come exclusively from descendants (including hidden
|
|
223
|
+
// captions). The container itself does not paint that text and must not be
|
|
224
|
+
// audited as though it did.
|
|
225
|
+
return textContentFor(element, true).length > 0;
|
|
222
226
|
}
|
|
223
227
|
|
|
224
228
|
function textClientRects(element, directOnly) {
|
|
@@ -394,6 +398,7 @@
|
|
|
394
398
|
}
|
|
395
399
|
|
|
396
400
|
function clippedTextIssue(element, time, tolerance) {
|
|
401
|
+
if (hasTextClipOptOut(element)) return null;
|
|
397
402
|
const style = getComputedStyle(element);
|
|
398
403
|
if (!clipsOverflow(style)) return null;
|
|
399
404
|
const overflowX = element.scrollWidth - element.clientWidth;
|
|
@@ -431,9 +436,9 @@
|
|
|
431
436
|
}
|
|
432
437
|
|
|
433
438
|
function textOverflowIssues(element, root, rootRect, time, tolerance) {
|
|
434
|
-
const textRect = textRectFor(element);
|
|
439
|
+
const textRect = textRectFor(element, true);
|
|
435
440
|
if (!textRect) return [];
|
|
436
|
-
const text = textContentFor(element);
|
|
441
|
+
const text = textContentFor(element, true);
|
|
437
442
|
const selector = selectorFor(element);
|
|
438
443
|
const issues = [];
|
|
439
444
|
|
|
@@ -455,7 +460,7 @@
|
|
|
455
460
|
const containerOverflow = overflowFor(textRect, containerRect, tolerance, verticalTolerance);
|
|
456
461
|
if (
|
|
457
462
|
containerOverflow &&
|
|
458
|
-
!
|
|
463
|
+
!hasTextClipOptOut(element) &&
|
|
459
464
|
!clippedByAncestor(element, container)
|
|
460
465
|
) {
|
|
461
466
|
const style = elementStyle;
|
|
@@ -481,7 +486,7 @@
|
|
|
481
486
|
}
|
|
482
487
|
|
|
483
488
|
const canvasOverflow = overflowFor(textRect, rootRect, tolerance);
|
|
484
|
-
if (canvasOverflow && !
|
|
489
|
+
if (canvasOverflow && !hasTextClipOptOut(element)) {
|
|
485
490
|
issues.push({
|
|
486
491
|
code: "canvas_overflow",
|
|
487
492
|
severity: "info",
|
|
@@ -566,7 +571,7 @@
|
|
|
566
571
|
// (low colour alpha) is decorative and exempt, as are elements opted out with
|
|
567
572
|
// data-layout-allow-overlap.
|
|
568
573
|
function isSolidTextBlock(element) {
|
|
569
|
-
if (!isVisibleElement(element) || !hasOwnTextCandidate(element)) return false;
|
|
574
|
+
if (!isVisibleElement(element) || !hasOwnTextCandidate(element, true)) return false;
|
|
570
575
|
if (hasAllowOverlapFlag(element)) return false;
|
|
571
576
|
return colorAlpha(getComputedStyle(element).color) >= 0.35;
|
|
572
577
|
}
|
|
@@ -575,8 +580,9 @@
|
|
|
575
580
|
const blocks = [];
|
|
576
581
|
for (const element of Array.from(root.querySelectorAll("*"))) {
|
|
577
582
|
if (!isSolidTextBlock(element)) continue;
|
|
578
|
-
const
|
|
579
|
-
|
|
583
|
+
const rects = textClientRects(element, true);
|
|
584
|
+
const rect = textRectFor(element, true);
|
|
585
|
+
if (rect) blocks.push({ element, rect, rects });
|
|
580
586
|
}
|
|
581
587
|
return blocks;
|
|
582
588
|
}
|
|
@@ -591,6 +597,18 @@
|
|
|
591
597
|
return overlapX > 0 && overlapY > 0 ? overlapX * overlapY : 0;
|
|
592
598
|
}
|
|
593
599
|
|
|
600
|
+
function rectsArea(rects) {
|
|
601
|
+
return rects.reduce((total, rect) => total + rectArea(rect), 0);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function fragmentIntersectionArea(a, b) {
|
|
605
|
+
let total = 0;
|
|
606
|
+
for (const aRect of a) {
|
|
607
|
+
for (const bRect of b) total += intersectionArea(aRect, bRect);
|
|
608
|
+
}
|
|
609
|
+
return total;
|
|
610
|
+
}
|
|
611
|
+
|
|
594
612
|
function isNested(a, b) {
|
|
595
613
|
return a.contains(b) || b.contains(a);
|
|
596
614
|
}
|
|
@@ -626,8 +644,8 @@
|
|
|
626
644
|
function overlapIssue(a, b, time) {
|
|
627
645
|
if (isNested(a.element, b.element)) return null;
|
|
628
646
|
if (isManagedFlowOverlap(a.element, b.element)) return null;
|
|
629
|
-
const area =
|
|
630
|
-
if (area <= Math.min(
|
|
647
|
+
const area = fragmentIntersectionArea(a.rects, b.rects);
|
|
648
|
+
if (area <= Math.min(rectsArea(a.rects), rectsArea(b.rects)) * 0.2) return null;
|
|
631
649
|
return {
|
|
632
650
|
// Warning at the per-sample level: a single-sample overlap is usually an
|
|
633
651
|
// entrance/exit transient (two blocks crossing mid-animation), not a real
|
|
@@ -730,13 +748,117 @@
|
|
|
730
748
|
|
|
731
749
|
const RASTER_TAGS = new Set(["IMG", "VIDEO", "CANVAS"]);
|
|
732
750
|
const FRAME_MEDIA_TAGS = new Set([...RASTER_TAGS, "SVG"]);
|
|
751
|
+
const imageAlphaCanvases = new WeakMap();
|
|
752
|
+
|
|
753
|
+
function objectPositionOffset(value, freeSpace) {
|
|
754
|
+
const token = String(value || "50%")
|
|
755
|
+
.trim()
|
|
756
|
+
.split(/\s+/)[0];
|
|
757
|
+
if (token === "left" || token === "top") return 0;
|
|
758
|
+
if (token === "right" || token === "bottom") return freeSpace;
|
|
759
|
+
if (token === "center") return freeSpace / 2;
|
|
760
|
+
if (token.endsWith("%")) return (freeSpace * parseFloat(token)) / 100;
|
|
761
|
+
const pixels = parseFloat(token);
|
|
762
|
+
return Number.isFinite(pixels) ? pixels : freeSpace / 2;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function objectPositionOffsets(value, freeX, freeY) {
|
|
766
|
+
const tokens = String(value || "50% 50%")
|
|
767
|
+
.trim()
|
|
768
|
+
.split(/\s+/)
|
|
769
|
+
.slice(0, 2);
|
|
770
|
+
let x = "50%";
|
|
771
|
+
let y = "50%";
|
|
772
|
+
if (tokens.length === 1) {
|
|
773
|
+
if (tokens[0] === "top" || tokens[0] === "bottom") y = tokens[0];
|
|
774
|
+
else x = tokens[0];
|
|
775
|
+
} else {
|
|
776
|
+
for (const token of tokens) {
|
|
777
|
+
if (token === "top" || token === "bottom") y = token;
|
|
778
|
+
else if (token === "left" || token === "right") x = token;
|
|
779
|
+
else if (x === "50%") x = token;
|
|
780
|
+
else y = token;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return { x: objectPositionOffset(x, freeX), y: objectPositionOffset(y, freeY) };
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Return the alpha painted by an <img> at a viewport point. `null` means the
|
|
787
|
+
// browser would not let us inspect the image (not loaded or cross-origin), in
|
|
788
|
+
// which case callers preserve the conservative opaque fallback.
|
|
789
|
+
function imageAlphaAt(element, x, y) {
|
|
790
|
+
const sourceWidth = element.naturalWidth;
|
|
791
|
+
const sourceHeight = element.naturalHeight;
|
|
792
|
+
const rect = element.getBoundingClientRect();
|
|
793
|
+
if (!sourceWidth || !sourceHeight || !rect.width || !rect.height) return null;
|
|
794
|
+
|
|
795
|
+
const style = getComputedStyle(element);
|
|
796
|
+
const fit = style.objectFit || "fill";
|
|
797
|
+
let scaleX = rect.width / sourceWidth;
|
|
798
|
+
let scaleY = rect.height / sourceHeight;
|
|
799
|
+
if (fit !== "fill") {
|
|
800
|
+
const contain = Math.min(scaleX, scaleY);
|
|
801
|
+
const cover = Math.max(scaleX, scaleY);
|
|
802
|
+
const scale =
|
|
803
|
+
fit === "cover"
|
|
804
|
+
? cover
|
|
805
|
+
: fit === "none"
|
|
806
|
+
? 1
|
|
807
|
+
: fit === "scale-down"
|
|
808
|
+
? Math.min(1, contain)
|
|
809
|
+
: contain;
|
|
810
|
+
scaleX = scale;
|
|
811
|
+
scaleY = scale;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const paintedWidth = sourceWidth * scaleX;
|
|
815
|
+
const paintedHeight = sourceHeight * scaleY;
|
|
816
|
+
const offsets = objectPositionOffsets(
|
|
817
|
+
style.objectPosition,
|
|
818
|
+
rect.width - paintedWidth,
|
|
819
|
+
rect.height - paintedHeight,
|
|
820
|
+
);
|
|
821
|
+
const localX = x - rect.left - offsets.x;
|
|
822
|
+
const localY = y - rect.top - offsets.y;
|
|
823
|
+
if (localX < 0 || localY < 0 || localX >= paintedWidth || localY >= paintedHeight) return 0;
|
|
824
|
+
|
|
825
|
+
try {
|
|
826
|
+
let cached = imageAlphaCanvases.get(element);
|
|
827
|
+
const source = element.currentSrc || element.src;
|
|
828
|
+
if (
|
|
829
|
+
!cached ||
|
|
830
|
+
cached.width !== sourceWidth ||
|
|
831
|
+
cached.height !== sourceHeight ||
|
|
832
|
+
cached.source !== source
|
|
833
|
+
) {
|
|
834
|
+
const canvas = document.createElement("canvas");
|
|
835
|
+
canvas.width = sourceWidth;
|
|
836
|
+
canvas.height = sourceHeight;
|
|
837
|
+
const context = canvas.getContext("2d", { willReadFrequently: true });
|
|
838
|
+
if (!context) return null;
|
|
839
|
+
context.drawImage(element, 0, 0, sourceWidth, sourceHeight);
|
|
840
|
+
cached = { context, width: sourceWidth, height: sourceHeight, source };
|
|
841
|
+
imageAlphaCanvases.set(element, cached);
|
|
842
|
+
}
|
|
843
|
+
const sourceX = Math.min(sourceWidth - 1, Math.max(0, Math.floor(localX / scaleX)));
|
|
844
|
+
const sourceY = Math.min(sourceHeight - 1, Math.max(0, Math.floor(localY / scaleY)));
|
|
845
|
+
return cached.context.getImageData(sourceX, sourceY, 1, 1).data[3] / 255;
|
|
846
|
+
} catch {
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
733
850
|
|
|
734
851
|
// An element hides text beneath it when it paints opaque pixels at near-full
|
|
735
852
|
// opacity: raster content (img/video/canvas), a background image, or a solid
|
|
736
853
|
// background colour. Low-opacity overlays (grain, scrims) do not occlude.
|
|
737
|
-
function isOpaqueOccluder(element) {
|
|
738
|
-
|
|
854
|
+
function isOpaqueOccluder(element, x, y) {
|
|
855
|
+
const opacity = opacityChain(element);
|
|
856
|
+
if (opacity < 0.6) return false;
|
|
739
857
|
if (IGNORE_TAGS.has(element.tagName)) return false;
|
|
858
|
+
if (element.tagName === "IMG") {
|
|
859
|
+
const alpha = imageAlphaAt(element, x, y);
|
|
860
|
+
if (alpha !== null) return alpha * opacity >= 0.6;
|
|
861
|
+
}
|
|
740
862
|
if (RASTER_TAGS.has(element.tagName)) return true;
|
|
741
863
|
return hasOpaqueBackground(getComputedStyle(element));
|
|
742
864
|
}
|
|
@@ -798,7 +920,7 @@
|
|
|
798
920
|
// Pair-specific exemptions excuse this hit only; keep walking for deeper occluders.
|
|
799
921
|
if (sharedPreserve3d(element, hit)) continue;
|
|
800
922
|
if (isCrossSceneTransitionOverlap(element, hit)) continue;
|
|
801
|
-
if (isOpaqueOccluder(hit)) return hit;
|
|
923
|
+
if (isOpaqueOccluder(hit, x, y)) return hit;
|
|
802
924
|
}
|
|
803
925
|
return null;
|
|
804
926
|
}
|
|
@@ -819,25 +941,32 @@
|
|
|
819
941
|
return text.length > 0 && text.length <= ATOMIC_LABEL_MAX_CHARS && !/\s/.test(text);
|
|
820
942
|
}
|
|
821
943
|
|
|
822
|
-
// Sweep a grid across
|
|
823
|
-
// overlays covering only part of a multi-line block are caught).
|
|
944
|
+
// Sweep a grid across each painted text fragment (three rows, not just the
|
|
945
|
+
// mid-line, so overlays covering only part of a multi-line block are caught).
|
|
946
|
+
// Sampling fragments instead of their union avoids probing empty line gaps.
|
|
947
|
+
// Unlike a
|
|
824
948
|
// first-hit scan, this keeps sampling every point so it can report what
|
|
825
949
|
// fraction of the box is actually covered — a corner nibble on a paragraph
|
|
826
950
|
// reads very differently from a label buried under an overlay. Still
|
|
827
951
|
// returns the first opaque element found, for `containerSelector`.
|
|
828
|
-
function occlusionCoverage(element,
|
|
952
|
+
function occlusionCoverage(element, textRects) {
|
|
829
953
|
let occluder = null;
|
|
830
954
|
let hits = 0;
|
|
831
|
-
for (const
|
|
832
|
-
const
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
955
|
+
for (const textRect of textRects) {
|
|
956
|
+
for (const yFraction of OCCLUSION_PROBE_Y_FRACTIONS) {
|
|
957
|
+
const y = textRect.top + textRect.height * yFraction;
|
|
958
|
+
for (const xFraction of OCCLUSION_PROBE_X_FRACTIONS) {
|
|
959
|
+
const hit = occluderAt(element, textRect.left + textRect.width * xFraction, y);
|
|
960
|
+
if (!hit) continue;
|
|
961
|
+
hits += 1;
|
|
962
|
+
if (!occluder) occluder = hit;
|
|
963
|
+
}
|
|
838
964
|
}
|
|
839
965
|
}
|
|
840
|
-
return {
|
|
966
|
+
return {
|
|
967
|
+
occluder,
|
|
968
|
+
coveredFraction: round(hits / (OCCLUSION_GRID_POINTS * textRects.length)),
|
|
969
|
+
};
|
|
841
970
|
}
|
|
842
971
|
|
|
843
972
|
// pointer-events:none hides elements from elementFromPoint — both probed text AND occluders.
|
|
@@ -874,10 +1003,14 @@
|
|
|
874
1003
|
function occludedTextIssue(element, time) {
|
|
875
1004
|
if (hasAllowOcclusionFlag(element)) return null;
|
|
876
1005
|
if (!hasVisibleTextInk(element)) return null;
|
|
877
|
-
const textRect = textRectFor(element);
|
|
1006
|
+
const textRect = textRectFor(element, true);
|
|
878
1007
|
if (!textRect) return null;
|
|
879
|
-
const
|
|
880
|
-
const
|
|
1008
|
+
const textRects = textClientRects(element, true);
|
|
1009
|
+
const text = textContentFor(element, true);
|
|
1010
|
+
const { occluder, coveredFraction } = occlusionCoverage(
|
|
1011
|
+
element,
|
|
1012
|
+
textRects.length > 0 ? textRects : [textRect],
|
|
1013
|
+
);
|
|
881
1014
|
if (!occluder) return null;
|
|
882
1015
|
if (!isAtomicLabel(text) && coveredFraction < PROSE_COVERAGE_FLOOR) return null;
|
|
883
1016
|
return {
|
|
@@ -907,9 +1040,9 @@
|
|
|
907
1040
|
// paints the glyphs; a `background-clip: text` with no gradient/image and no
|
|
908
1041
|
// opaque background-color paints nothing, so it stays reportable.
|
|
909
1042
|
function invisibleTextIssue(element, time) {
|
|
910
|
-
const textRect = textRectFor(element);
|
|
1043
|
+
const textRect = textRectFor(element, true);
|
|
911
1044
|
if (!textRect) return null;
|
|
912
|
-
const text = textContentFor(element);
|
|
1045
|
+
const text = textContentFor(element, true);
|
|
913
1046
|
if (!text) return null;
|
|
914
1047
|
const cs = getComputedStyle(element);
|
|
915
1048
|
// Vendor computed-style props are read by property (camelCase), matching
|
|
@@ -1032,8 +1165,8 @@
|
|
|
1032
1165
|
if (escapedElements.has(element)) continue;
|
|
1033
1166
|
// Ownership is geometric and strict-mutex: any text breach past canvas_overflow's own
|
|
1034
1167
|
// tolerance cedes the element to canvas_overflow; in-bounds text leaves the panel finding.
|
|
1035
|
-
if (hasOwnTextCandidate(element)) {
|
|
1036
|
-
const textRect = textRectFor(element);
|
|
1168
|
+
if (hasOwnTextCandidate(element, true)) {
|
|
1169
|
+
const textRect = textRectFor(element, true);
|
|
1037
1170
|
if (textRect && overflowFor(textRect, rootRect, tolerance)) continue;
|
|
1038
1171
|
}
|
|
1039
1172
|
const rect = toRect(element.getBoundingClientRect());
|
|
@@ -1263,7 +1396,7 @@
|
|
|
1263
1396
|
document.body;
|
|
1264
1397
|
const rootRect = rootRectFor(root);
|
|
1265
1398
|
const elements = Array.from(root.querySelectorAll("*")).filter((element) =>
|
|
1266
|
-
isVisibleElement(element),
|
|
1399
|
+
isVisibleElement(element, 0.05),
|
|
1267
1400
|
);
|
|
1268
1401
|
const issues = [];
|
|
1269
1402
|
|