Skip to content

validate and contrast-report.mjs emit null:1 / NaN:1 for text elements whose bbox falls outside the captured viewport #588

@sidorovanthon

Description

@sidorovanthon

Describe the bug

Both npx hyperframes validate and the bundled scripts/contrast-report.mjs emit null:1 (validate) / NaN:1 (contrast-report) contrast warnings for text elements whose bounding box falls outside the captured frame buffer.

Root cause is in sampleRingMedian (in dist/skills/hyperframes/scripts/contrast-report.mjs, the same logic backs the validate command). When bbox.y > frameHeight (or bbox.x > frameWidth), the ring-sampling loop calls pushPixel(x, y) with coordinates beyond the buffer's bounds. pushPixel does not bounds-check before reading raw[i], so the channel arrays fill with undefined. median([undefined, undefined, ...]) returns undefined, which serializes to null in JSON and renders as NaN in the human report. The element then "fails" WCAG with a meaningless ratio.

This couples with the snapshot/capture-viewport bug (#587 — sibling): for portrait compositions (data-width=1080, data-height=1920), the captured frame is hardcoded 1920×1080, so every element with bbox.y > 1080 (i.e. roughly the bottom half of the composition) produces a false-positive warning.

In a typical multi-clip 1080×1920 narrative composition, this can mean dozens of bogus warnings — the production composition that surfaced this had 33 NaN entries out of 45 total samples drowning out the real findings.

Link to reproduction

https://github.com/sidorovanthon/hyperframes-repro-contrast-out-of-clip

Steps to reproduce

  1. Clone the repo, npm install.
  2. npx hyperframes validate
    • Expected output includes lines like:
      · #t-bottom "BOTTOM" — null:1 (need 3:1, t=0.5s)
      
  3. node node_modules/hyperframes/dist/skills/hyperframes/scripts/contrast-report.mjs .
    • Expected: 10/20 entries are NaN:1 for #t-bottom (positioned at top: 1500px in the 1080×1920 portrait composition; falls outside the 1920×1080 captured frame).

Expected behavior

One of the following:

  1. Bounds-check before sampling. In sampleRingMedian / pushPixel, skip pixels whose x or y is outside [0, width) / [0, height). If the resulting channel arrays are empty, mark the element as "off-frame, skipped" instead of returning null ratio. Treat off-frame elements as informational, not as WCAG failures.
  2. Filter elements at probe time. In probeTextElements, skip elements whose getBoundingClientRect() is entirely outside the viewport.
  3. Ideal: combine with fixing the upstream snapshot/capture viewport (hyperframes snapshot ignores root data-width/data-height, defaults to 1920×1080 viewport #587) so portrait compositions are captured at their declared dimensions, and these elements never end up off-frame in the first place.

Actual behavior

⚠ WCAG AA contrast warnings (10):
  · #t-bottom "BOTTOM" — null:1 (need 3:1, t=0.5s)
  · #t-bottom "BOTTOM" — null:1 (need 3:1, t=1.5s)
  ...

contrast-report.json entries for off-frame elements have "bg": [null, null, null, 1] and "ratio": null.

Suggested fix

In dist/skills/hyperframes/scripts/contrast-report.mjs, modify pushPixel (around line 188) to bounds-check:

const pushPixel = (x, y) => {
  if (x < 0 || x >= width || y < 0 || y >= height) return;
  const i = (y * width + x) * channels;
  r.push(raw[i]);
  g.push(raw[i + 1]);
  b.push(raw[i + 2]);
};

Then in the caller, if r.length === 0 after sampling, return a sentinel (e.g. null-with-reason, or skip the element from the warnings list entirely with a skippedReason: "off-frame" marker).

Environment

  • hyperframes 0.4.41
  • Windows 11, Node 20.x

Related

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions