test(producer): add hdr-regression and hdr-hlg-regression test suites (#365)

## Summary

Replace the trivial `hdr-pq` and `hdr-image-only` tests with two consolidated, time-windowed regression suites that exercise the full HDR pipeline. These goldens are the safety net for every other PR in this stack.

## Why

The pre-existing HDR tests covered only a single full-bleed video or image with a static text label — none of the features that the HDR pipeline has to handle differently from SDR (opacity animation, z-ordered multi-layer compositing, transforms, border-radius clipping, shader transitions, multiple HDR sources, object-fit modes, mixed HDR+SDR layering, HLG transfer). This PR builds the missing safety net first so every subsequent fix can be proven correct.

## What changed

- New `packages/producer/tests/hdr-regression/` (PQ, BT.2020, ~20 s, 1080p, 8 windows A–H):
  - A: static baseline (HDR video + DOM overlay)
  - B: wrapper-opacity fade
  - C: direct-on-`<video>` opacity tween (documents the Chunk 1 bug)
  - D: z-order sandwich (DOM → HDR → DOM)
  - E: two HDR videos side-by-side (pins PR #289)
  - F: rotation + scale + border-radius (documents the Chunk 4 bug)
  - G: `object-fit: contain`
  - H: shader crossfade between HDR video and HDR image
- New `packages/producer/tests/hdr-hlg-regression/` (HLG, ARIB STD-B67, ~5 s, 2 windows A–B) — exercises the separate HLG LUT/OETF code path that previously had **zero** coverage.
- New `scripts/generate-hdr-photo-pq.py` synthesizes `hdr-photo-pq.png` with a cICP chunk for BT.2020/PQ/full.
- Removed `tests/hdr-pq/` and `tests/hdr-image-only/`.
- Updated `.github/workflows/regression.yml` HDR shard to run the new pair sequentially.
- All compositions follow the documented timed-element pattern (`data-start`, `data-duration`, `class="clip"` directly on each timed leaf — no wrapper inheritance).

## Test plan

- [x] Goldens generated with `bun run test:update --sequential`.
- [x] `ffprobe` confirms HEVC/yuv420p10le/bt2020nc/smpte2084 (PQ) and arib-std-b67 (HLG).
- [x] Suite green with `maxFrameFailures` budgets that absorb the documented Chunk 1 / Chunk 4 known-fails — tightened in follow-up PRs in this stack.

## Stack

Foundational PR for the HDR follow-ups stack (Chunk 0 of `plans/hdr-followups.md`). Every subsequent PR builds on this safety net.
This commit is contained in:
Vance Ingalls
2026-04-22 15:43:04 -07:00
committed by GitHub
parent ed62894d01
commit d7c1050e44
25 changed files with 815 additions and 368 deletions
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:70a055b4946a6cb486198438f2c18372e852e4cd16ebecea3356da84fd3daf5e
size 1019741
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

@@ -0,0 +1,244 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>HDR PQ Regression Suite</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@hyperframes/shader-transitions@0.4.11/dist/index.global.js"></script>
<style>
html,
body {
margin: 0;
padding: 0;
background: #000;
}
#main {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
background: #000;
}
.hdr-video {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.label {
position: absolute;
padding: 14px 22px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 36px;
font-weight: 700;
color: #ffffff;
background: rgba(0, 0, 0, 0.6);
border-radius: 8px;
letter-spacing: 0.04em;
z-index: 100;
}
.label-tl {
top: 48px;
left: 64px;
}
/* Window B: z-order sandwich */
#window-b-bg {
position: absolute;
inset: 0;
background: #cc6600;
z-index: 1;
}
#wb-video {
z-index: 5;
}
#window-b-overlay {
position: absolute;
top: 64px;
right: 64px;
bottom: 64px;
left: 64px;
background: rgba(0, 80, 200, 0.35);
border: 4px solid rgba(0, 120, 255, 0.6);
z-index: 10;
}
/* Window C: transform + border-radius */
#window-c-bg {
position: absolute;
inset: 0;
background: #111;
z-index: 1;
}
#wc-video {
position: absolute;
top: 50%;
left: 50%;
width: 1280px;
height: 720px;
margin-top: -360px;
margin-left: -640px;
border-radius: 24px;
overflow: hidden;
object-fit: cover;
display: block;
transform-origin: 50% 50%;
z-index: 5;
}
/* Window D: shader scenes */
#window-d-scene-a,
#window-d-scene-b {
position: absolute;
inset: 0;
overflow: hidden;
}
#window-d-scene-a {
opacity: 1;
}
#window-d-scene-b {
opacity: 0;
}
#window-d-scene-b img.hdr-image {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
</style>
</head>
<body>
<div
id="main"
data-composition-id="hdr-regression"
data-start="0"
data-duration="6"
data-width="1920"
data-height="1080"
>
<!-- Window A · Baseline HDR + direct opacity tween · 0.02.5s -->
<video
id="wa-video"
class="clip hdr-video"
data-start="0"
data-duration="1.5"
data-track-index="0"
src="hdr-clip.mp4"
muted
playsinline
data-has-audio="false"
></video>
<div class="label label-tl clip" data-start="0" data-duration="1.5">
A · Baseline HDR + opacity tween
</div>
<!-- Window B · Z-order sandwich (DOM → HDR → DOM) · 1.53.0s -->
<div id="window-b-bg" class="clip" data-start="1.5" data-duration="1.5"></div>
<video
id="wb-video"
class="clip hdr-video"
data-start="1.5"
data-duration="1.5"
data-track-index="0"
src="hdr-clip.mp4"
muted
playsinline
data-has-audio="false"
></video>
<div id="window-b-overlay" class="clip" data-start="1.5" data-duration="1.5"></div>
<div class="label label-tl clip" data-start="1.5" data-duration="1.5">
B · DOM → HDR → DOM stack
</div>
<!-- Window C · Transform + border-radius · 3.04.5s -->
<div id="window-c-bg" class="clip" data-start="3" data-duration="1.5"></div>
<video
id="wc-video"
class="clip"
data-start="3"
data-duration="1.5"
data-track-index="0"
src="hdr-clip.mp4"
muted
playsinline
data-has-audio="false"
></video>
<div class="label label-tl clip" data-start="3" data-duration="1.5">
C · Rotate + scale + radius
</div>
<!-- Window D · Shader transition (HDR video → HDR image) · 4.56.0s -->
<div id="window-d-scene-a" class="scene">
<video
id="wd-video"
class="clip hdr-video"
data-start="4.5"
data-duration="1.5"
data-track-index="0"
src="hdr-clip.mp4"
muted
playsinline
data-has-audio="false"
></video>
<div class="label label-tl clip" data-start="4.5" data-duration="1.5">
D · Scene A (HDR video)
</div>
</div>
<div id="window-d-scene-b" class="scene">
<img
id="wd-image"
class="clip hdr-image"
src="hdr-photo-pq.png"
alt="HDR PQ photo"
data-start="4.5"
data-duration="1.5"
/>
<div class="label label-tl clip" data-start="4.5" data-duration="1.5">
D · Scene B (HDR image)
</div>
</div>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
// Window A · direct opacity tween on the video element (01.5s)
tl.to("#wa-video", { opacity: 0.15, duration: 0.5, ease: "power2.inOut" }, 0.3);
tl.to("#wa-video", { opacity: 1.0, duration: 0.5, ease: "power2.inOut" }, 0.8);
// Window C · rotate + scale on the video itself (3.04.5s)
tl.set("#wc-video", { rotation: 0, scale: 1 }, 0);
tl.to(
"#wc-video",
{ rotation: 15, scale: 0.8, duration: 1.2, ease: "power2.out" },
3.0,
);
// Window D · shader transition: cross-warp-morph between Scene A and Scene B (4.56.0s)
HyperShader.init({
bgColor: "#000000",
scenes: ["window-d-scene-a", "window-d-scene-b"],
transitions: [{ time: 5.5, shader: "cross-warp-morph", duration: 0.7 }],
timeline: tl,
});
window.__timelines["hdr-regression"] = tl;
</script>
</body>
</html>