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
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions