mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
feat(engine): add HDR two-pass compositing — DOM layer + native HLG video (#288)
## Summary Compositions with HDR video AND DOM overlays (text, graphics, SDR video) couldn't render both correctly — either HDR data was lost (Chrome captures sRGB only) or DOM overlays were missing (FFmpeg pass-through skips Chrome). This PR adds in-memory alpha compositing that combines both. ## What it does **Per-frame two-pass capture:** 1. **DOM pass** — Chrome screenshots the page with a transparent background (CDP alpha). HDR videos are hidden, leaving transparent holes where they go. 2. **HDR pass** — Pre-extracted native HLG/PQ frames (16-bit PNG from FFmpeg) are read from disk. 3. **Composite** — DOM pixels (sRGB RGBA8) are alpha-composited over HDR pixels (rgb48le) in Node.js memory, with sRGB→HLG/PQ conversion via a 256-entry lookup table. **Key components:** - `decodePng()` / `decodePngToRgb48le()` — Pure Node.js PNG decoders (no native dependencies). Support all 5 PNG filter types. - `blitRgba8OverRgb48le()` — Alpha composite with per-pixel sRGB→HDR LUT conversion. Fast paths for alpha=0 (skip) and alpha=255 (overwrite). - `initTransparentBackground()` + `captureAlphaPng()` — Split CDP transparent background setup (once) from per-frame screenshot capture (eliminates 2 CDP round-trips per frame). - Single-pass FFmpeg extraction — All HDR frames extracted in one sequential FFmpeg run (avoids duplicate frames from per-frame `-ss` fast seek). ## Key design decisions | Decision | Why | |----------|-----| | In-memory compositing (not FFmpeg overlay) | Eliminates ~2400 process spawns + temp files per render. Pure pixel math is 10x faster. | | 16-bit PNG intermediate | Raw `-f rawvideo` loses color metadata, causing moiré artifacts. PNG is self-describing. | | sRGB→HLG LUT (256 entries) | DOM content is sRGB. Without conversion, it appears orange-shifted in HLG stream. | | Native HDR detection before extraction | `extractAllVideoFrames` converts SDR→HDR. Pre-extraction probe identifies original HDR sources so only truly-HDR videos get native extraction. | ## Files changed | File | What changed | |------|-------------| | `packages/engine/src/utils/alphaBlit.ts` | **NEW** — PNG decode, sRGB→HDR LUT, alpha compositing (14 tests) | | `packages/engine/src/services/screenshotService.ts` | Transparent background CDP, `captureAlphaPng()` | | `packages/engine/src/services/videoFrameInjector.ts` | `hideVideoElements()` / `showVideoElements()` | | `packages/engine/src/services/streamingEncoder.ts` | Input color space tags for rgb48le | | `packages/producer/src/services/renderOrchestrator.ts` | Two-pass HDR capture loop, native HDR detection | ## How to test Render a composition with an HDR video background and text overlays. Both should be visible — HDR video at full quality, text crisp with correct colors (not orange-shifted). ## Stack position **3 of 6** — Stacked on #265 (HDR output pipeline). This is the foundation for all layered compositing that follows. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 3.6 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 200 KiB |
@@ -0,0 +1,142 @@
|
||||
[
|
||||
{
|
||||
"text": "the",
|
||||
"start": 0.1,
|
||||
"end": 0.31
|
||||
},
|
||||
{
|
||||
"text": "makers",
|
||||
"start": 0.31,
|
||||
"end": 0.86
|
||||
},
|
||||
{
|
||||
"text": "of",
|
||||
"start": 0.94,
|
||||
"end": 1.13
|
||||
},
|
||||
{
|
||||
"text": "Subnautica,",
|
||||
"start": 1.13,
|
||||
"end": 2.28
|
||||
},
|
||||
{
|
||||
"text": "and",
|
||||
"start": 2.28,
|
||||
"end": 2.44
|
||||
},
|
||||
{
|
||||
"text": "he",
|
||||
"start": 2.44,
|
||||
"end": 2.54
|
||||
},
|
||||
{
|
||||
"text": "promised",
|
||||
"start": 2.54,
|
||||
"end": 2.99
|
||||
},
|
||||
{
|
||||
"text": "the",
|
||||
"start": 2.99,
|
||||
"end": 3.12
|
||||
},
|
||||
{
|
||||
"text": "founders",
|
||||
"start": 3.12,
|
||||
"end": 3.65
|
||||
},
|
||||
{
|
||||
"text": "$250",
|
||||
"start": 3.75,
|
||||
"end": 4.71
|
||||
},
|
||||
{
|
||||
"text": "million",
|
||||
"start": 4.71,
|
||||
"end": 5.4
|
||||
},
|
||||
{
|
||||
"text": "if",
|
||||
"start": 5.44,
|
||||
"end": 5.6
|
||||
},
|
||||
{
|
||||
"text": "their",
|
||||
"start": 5.6,
|
||||
"end": 5.95
|
||||
},
|
||||
{
|
||||
"text": "next",
|
||||
"start": 5.95,
|
||||
"end": 6.23
|
||||
},
|
||||
{
|
||||
"text": "game",
|
||||
"start": 6.24,
|
||||
"end": 6.64
|
||||
},
|
||||
{
|
||||
"text": "hits",
|
||||
"start": 6.64,
|
||||
"end": 6.91
|
||||
},
|
||||
{
|
||||
"text": "certain",
|
||||
"start": 6.98,
|
||||
"end": 7.36
|
||||
},
|
||||
{
|
||||
"text": "targets.",
|
||||
"start": 7.36,
|
||||
"end": 7.98
|
||||
},
|
||||
{
|
||||
"text": "And",
|
||||
"start": 8.06,
|
||||
"end": 8.14
|
||||
},
|
||||
{
|
||||
"text": "at",
|
||||
"start": 8.14,
|
||||
"end": 8.25
|
||||
},
|
||||
{
|
||||
"text": "the",
|
||||
"start": 8.25,
|
||||
"end": 8.41
|
||||
},
|
||||
{
|
||||
"text": "time,",
|
||||
"start": 8.41,
|
||||
"end": 8.72
|
||||
},
|
||||
{
|
||||
"text": "he",
|
||||
"start": 8.72,
|
||||
"end": 8.82
|
||||
},
|
||||
{
|
||||
"text": "thought",
|
||||
"start": 8.83,
|
||||
"end": 9.19
|
||||
},
|
||||
{
|
||||
"text": "there",
|
||||
"start": 9.24,
|
||||
"end": 9.48
|
||||
},
|
||||
{
|
||||
"text": "was",
|
||||
"start": 9.48,
|
||||
"end": 9.63
|
||||
},
|
||||
{
|
||||
"text": "no",
|
||||
"start": 9.64,
|
||||
"end": 9.75
|
||||
},
|
||||
{
|
||||
"text": "way-",
|
||||
"start": 9.75,
|
||||
"end": 9.98
|
||||
}
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "HDR Regression — HDR PQ (Data Drift)",
|
||||
"description": "1920x1080 native HDR composition with HDR video (BT.2020 PQ 10-bit), 16-bit HDR PNG photo, audio, captions, the Refik-Anadol-inspired Data Drift visual identity, and the @hyperframes/shader-transitions package driving Domain Warp / Gravitational Lens / Cross-Warp Morph handoffs. Renders with `--hdr` to exercise the rgb48le shader transition compositor, the PQ encode path, and the HDR side-data writer.",
|
||||
"tags": [
|
||||
"hdr-regression",
|
||||
"hdr",
|
||||
"pq",
|
||||
"bt2020",
|
||||
"10bit",
|
||||
"video",
|
||||
"audio",
|
||||
"image",
|
||||
"captions",
|
||||
"shader-transitions",
|
||||
"data-drift"
|
||||
],
|
||||
|
||||
"minPsnr": 26,
|
||||
"maxFrameFailures": 0,
|
||||
|
||||
"minAudioCorrelation": 0.85,
|
||||
"maxAudioLagWindows": 120,
|
||||
|
||||
"renderConfig": {
|
||||
"fps": 30
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
<template id="captions-template">
|
||||
<div
|
||||
data-composition-id="captions"
|
||||
data-start="0"
|
||||
data-duration="10.0"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="captions-container"></div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
[data-composition-id="captions"] {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
padding-bottom: 130px;
|
||||
pointer-events: none;
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
}
|
||||
|
||||
#captions-container {
|
||||
position: relative;
|
||||
width: 1280px;
|
||||
min-height: 110px;
|
||||
text-align: center;
|
||||
font-weight: 300;
|
||||
font-size: 44px;
|
||||
letter-spacing: 0.01em;
|
||||
color: rgba(244, 244, 245, 0.95);
|
||||
text-shadow:
|
||||
0 2px 18px rgba(0, 0, 0, 0.85),
|
||||
0 0 24px rgba(124, 58, 237, 0.18);
|
||||
}
|
||||
|
||||
.dd-chunk {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 4px 14px;
|
||||
}
|
||||
|
||||
.dd-word {
|
||||
opacity: 0;
|
||||
display: inline-block;
|
||||
transition: none;
|
||||
color: rgba(244, 244, 245, 0.95);
|
||||
will-change: opacity, color, transform;
|
||||
}
|
||||
|
||||
.dd-word.is-active {
|
||||
background: linear-gradient(120deg, #b794f4 0%, #06b6d4 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
const TRANSCRIPT = [
|
||||
{ "text": "the", "start": 0.1, "end": 0.31 },
|
||||
{ "text": "makers", "start": 0.31, "end": 0.86 },
|
||||
{ "text": "of", "start": 0.94, "end": 1.13 },
|
||||
{ "text": "Subnautica,", "start": 1.13, "end": 2.28 },
|
||||
{ "text": "and", "start": 2.28, "end": 2.44 },
|
||||
{ "text": "he", "start": 2.44, "end": 2.54 },
|
||||
{ "text": "promised", "start": 2.54, "end": 2.99 },
|
||||
{ "text": "the", "start": 2.99, "end": 3.12 },
|
||||
{ "text": "founders", "start": 3.12, "end": 3.65 },
|
||||
{ "text": "$250", "start": 3.75, "end": 4.71 },
|
||||
{ "text": "million", "start": 4.71, "end": 5.4 },
|
||||
{ "text": "if", "start": 5.44, "end": 5.6 },
|
||||
{ "text": "their", "start": 5.6, "end": 5.95 },
|
||||
{ "text": "next", "start": 5.95, "end": 6.23 },
|
||||
{ "text": "game", "start": 6.24, "end": 6.64 },
|
||||
{ "text": "hits", "start": 6.64, "end": 6.91 },
|
||||
{ "text": "certain", "start": 6.98, "end": 7.36 },
|
||||
{ "text": "targets.", "start": 7.36, "end": 7.98 },
|
||||
{ "text": "And", "start": 8.06, "end": 8.14 },
|
||||
{ "text": "at", "start": 8.14, "end": 8.25 },
|
||||
{ "text": "the", "start": 8.25, "end": 8.41 },
|
||||
{ "text": "time,", "start": 8.41, "end": 8.72 },
|
||||
{ "text": "he", "start": 8.72, "end": 8.82 },
|
||||
{ "text": "thought", "start": 8.83, "end": 9.19 },
|
||||
{ "text": "there", "start": 9.24, "end": 9.48 },
|
||||
{ "text": "was", "start": 9.48, "end": 9.63 },
|
||||
{ "text": "no", "start": 9.64, "end": 9.75 },
|
||||
{ "text": "way-", "start": 9.75, "end": 9.98 }
|
||||
];
|
||||
|
||||
const container = document.getElementById("captions-container");
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
const chunks = [];
|
||||
let current = [];
|
||||
for (let i = 0; i < TRANSCRIPT.length; i++) {
|
||||
current.push(TRANSCRIPT[i]);
|
||||
if (current.length >= 4 || i === TRANSCRIPT.length - 1) {
|
||||
chunks.push(current);
|
||||
current = [];
|
||||
}
|
||||
}
|
||||
|
||||
chunks.forEach((chunk) => {
|
||||
const start = chunk[0].start;
|
||||
const end = chunk[chunk.length - 1].end;
|
||||
|
||||
const div = document.createElement("div");
|
||||
div.className = "dd-chunk";
|
||||
container.appendChild(div);
|
||||
|
||||
tl.fromTo(
|
||||
div,
|
||||
{ opacity: 0, y: 18, filter: "blur(8px)" },
|
||||
{ opacity: 1, y: 0, filter: "blur(0px)", duration: 0.4, ease: "power2.out" },
|
||||
start - 0.12,
|
||||
);
|
||||
|
||||
chunk.forEach((wordData, wordIndex) => {
|
||||
const w = document.createElement("span");
|
||||
w.className = "dd-word";
|
||||
w.innerText = wordData.text;
|
||||
div.appendChild(w);
|
||||
|
||||
tl.to(
|
||||
w,
|
||||
{ opacity: 1, duration: 0.18, ease: "sine.out" },
|
||||
wordData.start,
|
||||
);
|
||||
|
||||
tl.add(() => w.classList.add("is-active"), wordData.start);
|
||||
|
||||
const offTime =
|
||||
wordIndex < chunk.length - 1 ? chunk[wordIndex + 1].start : end;
|
||||
tl.add(() => w.classList.remove("is-active"), offTime);
|
||||
});
|
||||
|
||||
tl.to(
|
||||
div,
|
||||
{ opacity: 0, y: -12, filter: "blur(8px)", duration: 0.35, ease: "sine.in" },
|
||||
end + 0.05,
|
||||
);
|
||||
tl.set(div, { opacity: 0, visibility: "hidden" }, end + 0.4);
|
||||
});
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["captions"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,160 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>HDR Regression — HDR PQ (Data Drift)</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@hyperframes/shader-transitions@0.4.4/dist/index.global.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="main"
|
||||
data-composition-id="main-comp"
|
||||
data-start="0"
|
||||
data-duration="10.0"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<!-- ============================================================
|
||||
Data Drift — five scenes, ~10s, GPU shader handoffs.
|
||||
Scene timings (after each shader transition completes):
|
||||
0.0 – 1.6 intro card
|
||||
2.1 – 4.0 HDR video A (transition 1.6, 0.5s)
|
||||
4.5 – 6.2 HDR still photograph (transition 4.0, 0.5s)
|
||||
6.7 – 8.4 HDR video B (transition 6.2, 0.5s)
|
||||
8.9 – 10.0 outro card (transition 8.4, 0.5s)
|
||||
Shader picks (Data Drift): Domain Warp, Gravitational
|
||||
Lens, Cross-Warp Morph, Domain Warp.
|
||||
============================================================ -->
|
||||
|
||||
<!-- Persistent ambient layers — sit behind the .scene stack so
|
||||
every shader capture inherits the Data Drift atmosphere -->
|
||||
<div class="drift-field"></div>
|
||||
<div class="scan-line top"></div>
|
||||
<div class="scan-line bottom"></div>
|
||||
|
||||
<!-- Scene 1: cold open title card -->
|
||||
<section id="scene-intro" class="scene title-card">
|
||||
<div class="eyebrow">Hyperframes · HDR Regression Suite</div>
|
||||
<h1 class="display">
|
||||
A study in <span class="accent">light</span>,<br />unbounded.
|
||||
</h1>
|
||||
<div class="colophon">
|
||||
<span>Volume II</span>
|
||||
<span>HDR · BT.2020 · PQ</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Scene 2: HDR video A -->
|
||||
<section id="scene-video-a" class="scene media-frame">
|
||||
<video
|
||||
id="vid-hdr-1"
|
||||
class="clip"
|
||||
src="../assets/hdr-source-1.mp4"
|
||||
data-start="2.1"
|
||||
data-duration="1.9"
|
||||
muted
|
||||
playsinline
|
||||
></video>
|
||||
<div class="iridescent-frame"></div>
|
||||
<div class="hud top-left">
|
||||
<div class="accent-cyan">Plate I</div>
|
||||
<div>HDR · 10-bit</div>
|
||||
</div>
|
||||
<div class="hud top-right">
|
||||
<div class="accent-purple">SMPTE 2084</div>
|
||||
<div>BT.2020 / PQ</div>
|
||||
</div>
|
||||
<div class="hud-readout">
|
||||
1000<span class="unit">Peak nits</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Scene 3: HDR still photograph (16-bit PNG) -->
|
||||
<section id="scene-still" class="scene media-frame">
|
||||
<img
|
||||
id="img-hdr"
|
||||
src="../assets/photo-hdr.png"
|
||||
alt="HDR reference photograph"
|
||||
/>
|
||||
<div class="iridescent-frame"></div>
|
||||
<div class="hud top-left">
|
||||
<div class="accent-cyan">Plate II</div>
|
||||
<div>16-bit RGB</div>
|
||||
</div>
|
||||
<div class="hud top-right">
|
||||
<div class="accent-purple">PNG · linear-light</div>
|
||||
<div>HDR reference</div>
|
||||
</div>
|
||||
<div class="hud-readout">
|
||||
16<span class="unit">bits per channel</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Scene 4: HDR video B -->
|
||||
<section id="scene-video-b" class="scene media-frame">
|
||||
<video
|
||||
id="vid-hdr-2"
|
||||
class="clip"
|
||||
src="../assets/hdr-source-2.mp4"
|
||||
data-start="6.7"
|
||||
data-duration="1.7"
|
||||
muted
|
||||
playsinline
|
||||
></video>
|
||||
<div class="iridescent-frame"></div>
|
||||
<div class="hud top-left">
|
||||
<div class="accent-cyan">Plate III</div>
|
||||
<div>HDR · 10-bit</div>
|
||||
</div>
|
||||
<div class="hud top-right">
|
||||
<div class="accent-purple">Studio cut</div>
|
||||
<div>BT.2020 / PQ</div>
|
||||
</div>
|
||||
<div class="hud-readout">
|
||||
∞<span class="unit">specular highlights</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Scene 5: closing card -->
|
||||
<section id="scene-outro" class="scene title-card">
|
||||
<div class="eyebrow">Composition · Data Drift</div>
|
||||
<h1 class="display">
|
||||
<span class="accent">HDR</span> ·<br />the wider gamut.
|
||||
</h1>
|
||||
<div class="colophon">
|
||||
<span>BT.2020 · PQ · 10-bit</span>
|
||||
<span>End of Volume II</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
Voiceover — single audio track for the entire composition
|
||||
============================================================ -->
|
||||
<audio
|
||||
id="vo"
|
||||
src="../assets/voiceover.mp3"
|
||||
data-start="0"
|
||||
data-duration="10.0"
|
||||
></audio>
|
||||
|
||||
<!-- ============================================================
|
||||
Captions — sub-composition, lower-third, Data Drift styling
|
||||
============================================================ -->
|
||||
<div
|
||||
id="captions-comp"
|
||||
data-composition-id="captions"
|
||||
data-composition-src="compositions/captions.html"
|
||||
data-start="0"
|
||||
data-duration="10.0"
|
||||
></div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
</script>
|
||||
<script src="script.js"></script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,158 @@
|
||||
/* Data Drift — five-scene HDR PQ composition.
|
||||
|
||||
Scene visibility / handoff is owned entirely by the
|
||||
@hyperframes/shader-transitions package. We build a paused
|
||||
timeline of *internal* scene motion only (text reveals, video
|
||||
Ken-Burns, HUD drifts) and hand it to HyperShader.init() —
|
||||
the library appends the GL transitions and exposes the
|
||||
shader metadata on window.__hf.transitions so the HDR engine
|
||||
can reproduce each transition in rgb48le pixel space.
|
||||
|
||||
Motion rules: sine.inOut and power2.out only. Nothing snaps,
|
||||
everything drifts. */
|
||||
|
||||
(function () {
|
||||
const tl = gsap.timeline({ paused: true, defaults: { ease: "sine.inOut" } });
|
||||
|
||||
/* ---------- Scene 1 · Intro (0.00 – 1.60) ---------- */
|
||||
const introBrow = document.querySelector("#scene-intro .eyebrow");
|
||||
const introDisplay = document.querySelector("#scene-intro .display");
|
||||
const introFoot = document.querySelector("#scene-intro .colophon");
|
||||
if (introBrow && introDisplay && introFoot) {
|
||||
tl.from(
|
||||
introBrow,
|
||||
{ y: 18, opacity: 0, duration: 0.85, ease: "power2.out" },
|
||||
0.05,
|
||||
);
|
||||
tl.from(
|
||||
introDisplay,
|
||||
{ y: 36, opacity: 0, filter: "blur(12px)", duration: 1.05, ease: "power2.out" },
|
||||
0.22,
|
||||
);
|
||||
tl.from(
|
||||
introFoot,
|
||||
{ y: 12, opacity: 0, duration: 0.7, ease: "power2.out" },
|
||||
0.55,
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Scene 2 · HDR video A (2.10 – 4.00) ----------
|
||||
The video starts playing 0.5s after the shader cuts in
|
||||
(the lib transitions 1.6 → 2.1) so the scene becomes visible
|
||||
just as the first frame plays. Subtle Ken Burns push-in. */
|
||||
const vidA = document.getElementById("vid-hdr-1");
|
||||
if (vidA) {
|
||||
tl.fromTo(
|
||||
vidA,
|
||||
{ scale: 1.07 },
|
||||
{ scale: 1.0, duration: 1.9, ease: "sine.out" },
|
||||
2.1,
|
||||
);
|
||||
}
|
||||
const hudA = document.querySelectorAll("#scene-video-a .hud, #scene-video-a .hud-readout");
|
||||
if (hudA.length) {
|
||||
tl.from(
|
||||
hudA,
|
||||
{ opacity: 0, y: 8, duration: 0.6, ease: "power2.out", stagger: 0.08 },
|
||||
2.25,
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Scene 3 · HDR still (4.50 – 6.20) ----------
|
||||
16-bit PNG. Slow horizontal drift to give the still motion. */
|
||||
const still = document.getElementById("img-hdr");
|
||||
if (still) {
|
||||
tl.fromTo(
|
||||
still,
|
||||
{ scale: 1.1, x: -22 },
|
||||
{ scale: 1.02, x: 16, duration: 1.7, ease: "sine.inOut" },
|
||||
4.5,
|
||||
);
|
||||
}
|
||||
const hudB = document.querySelectorAll("#scene-still .hud, #scene-still .hud-readout");
|
||||
if (hudB.length) {
|
||||
tl.from(
|
||||
hudB,
|
||||
{ opacity: 0, y: 8, duration: 0.55, ease: "power2.out", stagger: 0.08 },
|
||||
4.65,
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Scene 4 · HDR video B (6.70 – 8.40) ----------
|
||||
Pull-back, opposite Ken Burns. */
|
||||
const vidB = document.getElementById("vid-hdr-2");
|
||||
if (vidB) {
|
||||
tl.fromTo(
|
||||
vidB,
|
||||
{ scale: 1.0 },
|
||||
{ scale: 1.07, duration: 1.7, ease: "sine.inOut" },
|
||||
6.7,
|
||||
);
|
||||
}
|
||||
const hudC = document.querySelectorAll("#scene-video-b .hud, #scene-video-b .hud-readout");
|
||||
if (hudC.length) {
|
||||
tl.from(
|
||||
hudC,
|
||||
{ opacity: 0, y: 8, duration: 0.55, ease: "power2.out", stagger: 0.08 },
|
||||
6.85,
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Scene 5 · Outro (8.90 – 10.00) ---------- */
|
||||
const outroBrow = document.querySelector("#scene-outro .eyebrow");
|
||||
const outroDisplay = document.querySelector("#scene-outro .display");
|
||||
const outroFoot = document.querySelector("#scene-outro .colophon");
|
||||
if (outroBrow && outroDisplay && outroFoot) {
|
||||
tl.from(
|
||||
outroBrow,
|
||||
{ y: 14, opacity: 0, duration: 0.65, ease: "power2.out" },
|
||||
8.95,
|
||||
);
|
||||
tl.from(
|
||||
outroDisplay,
|
||||
{ y: 28, opacity: 0, filter: "blur(10px)", duration: 0.85, ease: "power2.out" },
|
||||
9.05,
|
||||
);
|
||||
tl.from(
|
||||
outroFoot,
|
||||
{ y: 10, opacity: 0, duration: 0.55, ease: "power2.out" },
|
||||
9.45,
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Shader transitions ----------
|
||||
init() reads __hf if present (set by the HDR engine), wires
|
||||
transitions into the timeline at the requested times, and
|
||||
registers the timeline on window.__timelines under the
|
||||
compositionId we pass. */
|
||||
if (typeof HyperShader !== "undefined" && HyperShader.init) {
|
||||
HyperShader.init({
|
||||
bgColor: "#0a0a0a",
|
||||
accentColor: "#7c3aed",
|
||||
compositionId: "main-comp",
|
||||
scenes: [
|
||||
"scene-intro",
|
||||
"scene-video-a",
|
||||
"scene-still",
|
||||
"scene-video-b",
|
||||
"scene-outro",
|
||||
],
|
||||
transitions: [
|
||||
{ time: 1.6, shader: "domain-warp", duration: 0.5, ease: "sine.inOut" },
|
||||
{ time: 4.0, shader: "gravitational-lens", duration: 0.5, ease: "sine.inOut" },
|
||||
{ time: 6.2, shader: "cross-warp-morph", duration: 0.5, ease: "sine.inOut" },
|
||||
{ time: 8.4, shader: "domain-warp", duration: 0.5, ease: "sine.inOut" },
|
||||
],
|
||||
timeline: tl,
|
||||
});
|
||||
}
|
||||
|
||||
/* When init() runs above it registers the timeline for us. If
|
||||
HyperShader is unavailable (e.g. CDN miss), fall back to a
|
||||
manual registration so the scene-internal motion still
|
||||
plays — the engine will then composite scenes as hard cuts. */
|
||||
window.__timelines = window.__timelines || {};
|
||||
if (!window.__timelines["main-comp"]) {
|
||||
window.__timelines["main-comp"] = tl;
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,277 @@
|
||||
/* ============================================================
|
||||
Data Drift — Refik Anadol-inspired
|
||||
Deep black backdrop, electric purple + cyan gradients,
|
||||
floating weightless typography. The composition is built
|
||||
for shader transitions so each scene handoff is owned by
|
||||
the GL canvas, not by CSS animation.
|
||||
============================================================ */
|
||||
|
||||
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500&family=JetBrains+Mono:wght@300;500&display=swap");
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #0a0a0a;
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
font-weight: 300;
|
||||
color: #f4f4f5;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#main {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
background: #0a0a0a;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Persistent ambient backdrop visible through every scene's gaps.
|
||||
Two soft radial gradients sit behind everything — purple in the
|
||||
upper-left and cyan in the lower-right — establishing the Data
|
||||
Drift palette before any text appears. */
|
||||
#main::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at 22% 28%,
|
||||
rgba(124, 58, 237, 0.45) 0%,
|
||||
rgba(124, 58, 237, 0.0) 55%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 78% 76%,
|
||||
rgba(6, 182, 212, 0.40) 0%,
|
||||
rgba(6, 182, 212, 0.0) 55%
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ---------- Scene container baseline ---------- */
|
||||
|
||||
.scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
/* All scenes start hidden; the shader-transitions library will
|
||||
temporarily reveal them for capture, then promote each to
|
||||
opacity:1 as its transition completes. The intro overrides
|
||||
this below so frame 0 is non-empty. */
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#scene-intro {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ---------- Title cards (intro + outro) ---------- */
|
||||
|
||||
.title-card {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
padding: 0 200px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.42em;
|
||||
text-transform: uppercase;
|
||||
color: #06b6d4;
|
||||
margin-bottom: 56px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.eyebrow::before,
|
||||
.eyebrow::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 90px;
|
||||
height: 1px;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
transparent,
|
||||
rgba(6, 182, 212, 0.7),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.eyebrow::before {
|
||||
right: calc(100% + 28px);
|
||||
}
|
||||
|
||||
.eyebrow::after {
|
||||
left: calc(100% + 28px);
|
||||
}
|
||||
|
||||
.display {
|
||||
font-family: "Inter", sans-serif;
|
||||
font-size: 132px;
|
||||
font-weight: 200;
|
||||
line-height: 1.04;
|
||||
letter-spacing: -0.02em;
|
||||
color: #fafafa;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.display .accent {
|
||||
background: linear-gradient(120deg, #7c3aed 0%, #06b6d4 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.colophon {
|
||||
margin-top: 64px;
|
||||
display: flex;
|
||||
gap: 64px;
|
||||
justify-content: center;
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 12px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(244, 244, 245, 0.55);
|
||||
}
|
||||
|
||||
/* Soft outer scan lines (decorative HUD) */
|
||||
.scan-line {
|
||||
position: absolute;
|
||||
left: 80px;
|
||||
right: 80px;
|
||||
height: 1px;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
transparent,
|
||||
rgba(124, 58, 237, 0.35),
|
||||
rgba(6, 182, 212, 0.35),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.scan-line.top {
|
||||
top: 80px;
|
||||
}
|
||||
|
||||
.scan-line.bottom {
|
||||
bottom: 80px;
|
||||
}
|
||||
|
||||
/* ---------- Media-frame scenes ---------- */
|
||||
|
||||
.media-frame {
|
||||
background: #050505;
|
||||
}
|
||||
|
||||
.media-frame video,
|
||||
.media-frame img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
/* Subtle iridescent edge vignette so the HDR media reads as a
|
||||
"framed" plate inside the composition, not a fullscreen cut. */
|
||||
.iridescent-frame {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(124, 58, 237, 0.18),
|
||||
inset 0 0 220px rgba(0, 0, 0, 0.55),
|
||||
inset 0 0 60px rgba(6, 182, 212, 0.10);
|
||||
}
|
||||
|
||||
/* HUD overlays — minimal, weightless, JetBrains Mono. */
|
||||
.hud {
|
||||
position: absolute;
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 11px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(244, 244, 245, 0.78);
|
||||
}
|
||||
|
||||
.hud.top-left {
|
||||
top: 56px;
|
||||
left: 56px;
|
||||
}
|
||||
|
||||
.hud.top-right {
|
||||
top: 56px;
|
||||
right: 56px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.hud.bottom-left {
|
||||
bottom: 56px;
|
||||
left: 56px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.hud .accent-cyan {
|
||||
color: #06b6d4;
|
||||
}
|
||||
|
||||
.hud .accent-purple {
|
||||
color: #b794f4;
|
||||
}
|
||||
|
||||
.hud-readout {
|
||||
position: absolute;
|
||||
bottom: 56px;
|
||||
right: 56px;
|
||||
text-align: right;
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 56px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 0.04em;
|
||||
color: #fafafa;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.hud-readout .unit {
|
||||
display: block;
|
||||
margin-top: 14px;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.32em;
|
||||
color: rgba(6, 182, 212, 0.85);
|
||||
}
|
||||
|
||||
/* ---------- Particle field (CSS only, deterministic) ---------- */
|
||||
/* A drifting dot pattern on top of the ambient gradient. We keep
|
||||
this as an inline SVG background so it serializes cleanly into
|
||||
html2canvas captures the shader transition will sample. */
|
||||
|
||||
.drift-field {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
opacity: 0.45;
|
||||
background-image: radial-gradient(
|
||||
rgba(124, 58, 237, 0.55) 1px,
|
||||
transparent 1.5px
|
||||
),
|
||||
radial-gradient(rgba(6, 182, 212, 0.45) 1px, transparent 1.5px);
|
||||
background-size: 96px 96px, 168px 168px;
|
||||
background-position: 0 0, 40px 40px;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
../../hdr-pq/assets/hdr-source-2.mp4
|
||||
@@ -0,0 +1 @@
|
||||
../../hdr-pq/assets/hdr-source-1.mp4
|
||||
@@ -0,0 +1 @@
|
||||
../../hdr-pq/assets/photo-hdr.png
|
||||
@@ -0,0 +1 @@
|
||||
../../sdr-baseline/assets/photo-sdr.jpg
|
||||
@@ -0,0 +1 @@
|
||||
../../sdr-baseline/assets/sdr-source-2.mp4
|
||||
@@ -0,0 +1 @@
|
||||
../../sdr-baseline/assets/sdr-source-1.mp4
|
||||
@@ -0,0 +1,142 @@
|
||||
[
|
||||
{
|
||||
"text": "the",
|
||||
"start": 0.1,
|
||||
"end": 0.31
|
||||
},
|
||||
{
|
||||
"text": "makers",
|
||||
"start": 0.31,
|
||||
"end": 0.86
|
||||
},
|
||||
{
|
||||
"text": "of",
|
||||
"start": 0.94,
|
||||
"end": 1.13
|
||||
},
|
||||
{
|
||||
"text": "Subnautica,",
|
||||
"start": 1.13,
|
||||
"end": 2.28
|
||||
},
|
||||
{
|
||||
"text": "and",
|
||||
"start": 2.28,
|
||||
"end": 2.44
|
||||
},
|
||||
{
|
||||
"text": "he",
|
||||
"start": 2.44,
|
||||
"end": 2.54
|
||||
},
|
||||
{
|
||||
"text": "promised",
|
||||
"start": 2.54,
|
||||
"end": 2.99
|
||||
},
|
||||
{
|
||||
"text": "the",
|
||||
"start": 2.99,
|
||||
"end": 3.12
|
||||
},
|
||||
{
|
||||
"text": "founders",
|
||||
"start": 3.12,
|
||||
"end": 3.65
|
||||
},
|
||||
{
|
||||
"text": "$250",
|
||||
"start": 3.75,
|
||||
"end": 4.71
|
||||
},
|
||||
{
|
||||
"text": "million",
|
||||
"start": 4.71,
|
||||
"end": 5.4
|
||||
},
|
||||
{
|
||||
"text": "if",
|
||||
"start": 5.44,
|
||||
"end": 5.6
|
||||
},
|
||||
{
|
||||
"text": "their",
|
||||
"start": 5.6,
|
||||
"end": 5.95
|
||||
},
|
||||
{
|
||||
"text": "next",
|
||||
"start": 5.95,
|
||||
"end": 6.23
|
||||
},
|
||||
{
|
||||
"text": "game",
|
||||
"start": 6.24,
|
||||
"end": 6.64
|
||||
},
|
||||
{
|
||||
"text": "hits",
|
||||
"start": 6.64,
|
||||
"end": 6.91
|
||||
},
|
||||
{
|
||||
"text": "certain",
|
||||
"start": 6.98,
|
||||
"end": 7.36
|
||||
},
|
||||
{
|
||||
"text": "targets.",
|
||||
"start": 7.36,
|
||||
"end": 7.98
|
||||
},
|
||||
{
|
||||
"text": "And",
|
||||
"start": 8.06,
|
||||
"end": 8.14
|
||||
},
|
||||
{
|
||||
"text": "at",
|
||||
"start": 8.14,
|
||||
"end": 8.25
|
||||
},
|
||||
{
|
||||
"text": "the",
|
||||
"start": 8.25,
|
||||
"end": 8.41
|
||||
},
|
||||
{
|
||||
"text": "time,",
|
||||
"start": 8.41,
|
||||
"end": 8.72
|
||||
},
|
||||
{
|
||||
"text": "he",
|
||||
"start": 8.72,
|
||||
"end": 8.82
|
||||
},
|
||||
{
|
||||
"text": "thought",
|
||||
"start": 8.83,
|
||||
"end": 9.19
|
||||
},
|
||||
{
|
||||
"text": "there",
|
||||
"start": 9.24,
|
||||
"end": 9.48
|
||||
},
|
||||
{
|
||||
"text": "was",
|
||||
"start": 9.48,
|
||||
"end": 9.63
|
||||
},
|
||||
{
|
||||
"text": "no",
|
||||
"start": 9.64,
|
||||
"end": 9.75
|
||||
},
|
||||
{
|
||||
"text": "way-",
|
||||
"start": 9.75,
|
||||
"end": 9.98
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
../../sdr-baseline/assets/voiceover.mp3
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "HDR Regression — Mixed SDR + HDR (Shadow Cut)",
|
||||
"description": "1920x1080 composition that mixes SDR (BT.709 8-bit) and HDR (BT.2020 PQ 10-bit) sources in the same scene stack — two SDR videos, two HDR videos, an SDR JPEG and a 16-bit HDR PNG, plus voiceover and captions. Renders twice through the regression harness: once SDR (BT.709 output, every HDR source must be tone-mapped down) and once with `--hdr` (BT.2020 PQ output, every SDR source must be promoted up). Exercises the engine's per-asset transfer auto-detection, the rgb48le cross-transfer compositor, and the Domain Warp shader transition in both SDR and HDR pixel spaces. Visual identity is Hans-Hillmann-inspired Shadow Cut.",
|
||||
"tags": [
|
||||
"hdr-regression",
|
||||
"mixed",
|
||||
"sdr",
|
||||
"hdr",
|
||||
"pq",
|
||||
"bt2020",
|
||||
"bt709",
|
||||
"auto-detect",
|
||||
"cross-transfer",
|
||||
"video",
|
||||
"audio",
|
||||
"image",
|
||||
"captions",
|
||||
"shader-transitions",
|
||||
"shadow-cut"
|
||||
],
|
||||
|
||||
"minPsnr": 26,
|
||||
"maxFrameFailures": 0,
|
||||
|
||||
"minAudioCorrelation": 0.85,
|
||||
"maxAudioLagWindows": 120,
|
||||
|
||||
"renderConfig": {
|
||||
"fps": 30
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
<template id="captions-template">
|
||||
<div
|
||||
data-composition-id="captions"
|
||||
data-start="0"
|
||||
data-duration="10.0"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="captions-container"></div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
[data-composition-id="captions"] {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
padding-bottom: 150px;
|
||||
pointer-events: none;
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
}
|
||||
|
||||
#captions-container {
|
||||
position: relative;
|
||||
width: 1320px;
|
||||
min-height: 130px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
font-size: 46px;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
color: #fafafa;
|
||||
text-shadow:
|
||||
0 4px 22px rgba(0, 0, 0, 0.95),
|
||||
0 0 36px rgba(0, 0, 0, 0.85),
|
||||
0 0 1px rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
|
||||
.sc-chunk {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 6px 18px;
|
||||
}
|
||||
|
||||
.sc-word {
|
||||
opacity: 0;
|
||||
display: inline-block;
|
||||
transition: none;
|
||||
color: rgba(244, 244, 244, 0.92);
|
||||
will-change: opacity, color, transform;
|
||||
}
|
||||
|
||||
.sc-word.is-active {
|
||||
color: #C1121F;
|
||||
text-shadow:
|
||||
0 4px 22px rgba(0, 0, 0, 0.95),
|
||||
0 0 24px rgba(193, 18, 31, 0.55),
|
||||
0 0 1px rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
const TRANSCRIPT = [
|
||||
{ "text": "the", "start": 0.1, "end": 0.31 },
|
||||
{ "text": "makers", "start": 0.31, "end": 0.86 },
|
||||
{ "text": "of", "start": 0.94, "end": 1.13 },
|
||||
{ "text": "Subnautica,", "start": 1.13, "end": 2.28 },
|
||||
{ "text": "and", "start": 2.28, "end": 2.44 },
|
||||
{ "text": "he", "start": 2.44, "end": 2.54 },
|
||||
{ "text": "promised", "start": 2.54, "end": 2.99 },
|
||||
{ "text": "the", "start": 2.99, "end": 3.12 },
|
||||
{ "text": "founders", "start": 3.12, "end": 3.65 },
|
||||
{ "text": "$250", "start": 3.75, "end": 4.71 },
|
||||
{ "text": "million", "start": 4.71, "end": 5.4 },
|
||||
{ "text": "if", "start": 5.44, "end": 5.6 },
|
||||
{ "text": "their", "start": 5.6, "end": 5.95 },
|
||||
{ "text": "next", "start": 5.95, "end": 6.23 },
|
||||
{ "text": "game", "start": 6.24, "end": 6.64 },
|
||||
{ "text": "hits", "start": 6.64, "end": 6.91 },
|
||||
{ "text": "certain", "start": 6.98, "end": 7.36 },
|
||||
{ "text": "targets.", "start": 7.36, "end": 7.98 },
|
||||
{ "text": "And", "start": 8.06, "end": 8.14 },
|
||||
{ "text": "at", "start": 8.14, "end": 8.25 },
|
||||
{ "text": "the", "start": 8.25, "end": 8.41 },
|
||||
{ "text": "time,", "start": 8.41, "end": 8.72 },
|
||||
{ "text": "he", "start": 8.72, "end": 8.82 },
|
||||
{ "text": "thought", "start": 8.83, "end": 9.19 },
|
||||
{ "text": "there", "start": 9.24, "end": 9.48 },
|
||||
{ "text": "was", "start": 9.48, "end": 9.63 },
|
||||
{ "text": "no", "start": 9.64, "end": 9.75 },
|
||||
{ "text": "way-", "start": 9.75, "end": 9.98 }
|
||||
];
|
||||
|
||||
const container = document.getElementById("captions-container");
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
const chunks = [];
|
||||
let current = [];
|
||||
for (let i = 0; i < TRANSCRIPT.length; i++) {
|
||||
current.push(TRANSCRIPT[i]);
|
||||
if (current.length >= 4 || i === TRANSCRIPT.length - 1) {
|
||||
chunks.push(current);
|
||||
current = [];
|
||||
}
|
||||
}
|
||||
|
||||
chunks.forEach((chunk) => {
|
||||
const start = chunk[0].start;
|
||||
const end = chunk[chunk.length - 1].end;
|
||||
|
||||
const div = document.createElement("div");
|
||||
div.className = "sc-chunk";
|
||||
container.appendChild(div);
|
||||
|
||||
tl.fromTo(
|
||||
div,
|
||||
{ opacity: 0, y: 16, filter: "blur(6px)" },
|
||||
{ opacity: 1, y: 0, filter: "blur(0px)", duration: 0.32, ease: "power3.out" },
|
||||
start - 0.1,
|
||||
);
|
||||
|
||||
chunk.forEach((wordData, wordIndex) => {
|
||||
const w = document.createElement("span");
|
||||
w.className = "sc-word";
|
||||
w.innerText = wordData.text;
|
||||
div.appendChild(w);
|
||||
|
||||
tl.to(
|
||||
w,
|
||||
{ opacity: 1, duration: 0.16, ease: "power2.out" },
|
||||
wordData.start,
|
||||
);
|
||||
|
||||
tl.add(() => w.classList.add("is-active"), wordData.start);
|
||||
|
||||
const offTime =
|
||||
wordIndex < chunk.length - 1 ? chunk[wordIndex + 1].start : end;
|
||||
tl.add(() => w.classList.remove("is-active"), offTime);
|
||||
});
|
||||
|
||||
tl.to(
|
||||
div,
|
||||
{ opacity: 0, y: -10, filter: "blur(6px)", duration: 0.3, ease: "power4.in" },
|
||||
end + 0.05,
|
||||
);
|
||||
tl.set(div, { opacity: 0, visibility: "hidden" }, end + 0.36);
|
||||
});
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["captions"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,206 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>HDR Regression — Mixed SDR + HDR (Shadow Cut)</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@hyperframes/shader-transitions@0.4.4/dist/index.global.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="main"
|
||||
data-composition-id="main-comp"
|
||||
data-start="0"
|
||||
data-duration="10.0"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<!-- ============================================================
|
||||
Shadow Cut — six scenes, ~10s, every handoff is Domain Warp.
|
||||
This composition deliberately interleaves SDR and HDR sources
|
||||
so the engine has to auto-detect each plate's transfer fn
|
||||
and cross-convert into the working space:
|
||||
|
||||
SDR render → all HDR plates tonemap PQ → BT.709
|
||||
--hdr render → all SDR plates promote BT.709 → PQ
|
||||
|
||||
Scene timings (after each shader transition completes):
|
||||
0.00 – 1.40 intro card
|
||||
1.85 – 3.20 SDR video A (cross-warp: into PQ space)
|
||||
3.65 – 5.00 HDR video A (native PQ)
|
||||
5.45 – 6.50 SDR still (BT.709 promoted)
|
||||
6.95 – 8.00 HDR still (16-bit PNG, native HDR)
|
||||
8.45 – 10.00 outro card
|
||||
|
||||
Shader picks (Shadow Cut): Domain Warp on every handoff.
|
||||
============================================================ -->
|
||||
|
||||
<!-- Persistent darkness layers — sit behind the .scene stack so
|
||||
every shader capture inherits the noir atmosphere. -->
|
||||
<div class="bar top"></div>
|
||||
<div class="bar bottom"></div>
|
||||
|
||||
<!-- Scene 1: cold open title card -->
|
||||
<section id="scene-intro" class="scene title-card">
|
||||
<div class="classification">Restricted · HDR Regression</div>
|
||||
<h1 class="display">
|
||||
THE <span class="accent">SDR / HDR</span><br />HANDOFF
|
||||
</h1>
|
||||
<div class="subdisplay">A study in mixed transfer functions</div>
|
||||
<div class="colophon">
|
||||
<span>Case File · Volume III</span>
|
||||
<span>BT.709 ⇄ BT.2020 PQ</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Scene 2: SDR video A -->
|
||||
<section id="scene-sdr-vid" class="scene media-frame">
|
||||
<video
|
||||
id="vid-sdr"
|
||||
class="clip"
|
||||
src="../assets/sdr.mp4"
|
||||
data-start="1.85"
|
||||
data-duration="1.35"
|
||||
muted
|
||||
playsinline
|
||||
></video>
|
||||
<div class="shadow-vignette"></div>
|
||||
<div class="corner-tick top-left"></div>
|
||||
<div class="corner-tick top-right"></div>
|
||||
<div class="corner-tick bottom-left"></div>
|
||||
<div class="corner-tick bottom-right"></div>
|
||||
<div class="evidence top-left">
|
||||
<div class="red">Plate I · Source A</div>
|
||||
<div>SDR · 8-bit · BT.709</div>
|
||||
</div>
|
||||
<div class="evidence top-right">
|
||||
<div class="red">Transfer / sRGB</div>
|
||||
<div>Auto-detected</div>
|
||||
</div>
|
||||
<div class="case-label">
|
||||
709<span class="label">REC. ITU-R</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Scene 3: HDR video A -->
|
||||
<section id="scene-hdr-vid" class="scene media-frame">
|
||||
<video
|
||||
id="vid-hdr"
|
||||
class="clip"
|
||||
src="../assets/hdr.mp4"
|
||||
data-start="3.65"
|
||||
data-duration="1.35"
|
||||
muted
|
||||
playsinline
|
||||
></video>
|
||||
<div class="shadow-vignette"></div>
|
||||
<div class="corner-tick top-left"></div>
|
||||
<div class="corner-tick top-right"></div>
|
||||
<div class="corner-tick bottom-left"></div>
|
||||
<div class="corner-tick bottom-right"></div>
|
||||
<div class="evidence top-left">
|
||||
<div class="red">Plate II · Source B</div>
|
||||
<div>HDR · 10-bit · BT.2020</div>
|
||||
</div>
|
||||
<div class="evidence top-right">
|
||||
<div class="red">Transfer / SMPTE 2084</div>
|
||||
<div>Auto-detected</div>
|
||||
</div>
|
||||
<div class="case-label">
|
||||
2084<span class="label">SMPTE PQ</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Scene 4: SDR still photograph -->
|
||||
<section id="scene-sdr-still" class="scene media-frame">
|
||||
<img
|
||||
id="img-sdr"
|
||||
src="../assets/photo-sdr.jpg"
|
||||
alt="SDR reference photograph"
|
||||
/>
|
||||
<div class="shadow-vignette"></div>
|
||||
<div class="corner-tick top-left"></div>
|
||||
<div class="corner-tick top-right"></div>
|
||||
<div class="corner-tick bottom-left"></div>
|
||||
<div class="corner-tick bottom-right"></div>
|
||||
<div class="evidence top-left">
|
||||
<div class="red">Plate III · Stills</div>
|
||||
<div>JPEG · 8-bit · sRGB</div>
|
||||
</div>
|
||||
<div class="evidence top-right">
|
||||
<div class="red">Source / archival</div>
|
||||
<div>Auto-detected</div>
|
||||
</div>
|
||||
<div class="case-label">
|
||||
JPEG<span class="label">8-BIT sRGB</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Scene 5: HDR still photograph (16-bit PNG) -->
|
||||
<section id="scene-hdr-still" class="scene media-frame">
|
||||
<img
|
||||
id="img-hdr"
|
||||
src="../assets/photo-hdr.png"
|
||||
alt="HDR reference photograph"
|
||||
/>
|
||||
<div class="shadow-vignette"></div>
|
||||
<div class="corner-tick top-left"></div>
|
||||
<div class="corner-tick top-right"></div>
|
||||
<div class="corner-tick bottom-left"></div>
|
||||
<div class="corner-tick bottom-right"></div>
|
||||
<div class="evidence top-left">
|
||||
<div class="red">Plate IV · Stills</div>
|
||||
<div>PNG · 16-bit · linear</div>
|
||||
</div>
|
||||
<div class="evidence top-right">
|
||||
<div class="red">Source / reference</div>
|
||||
<div>HDR · BT.2020</div>
|
||||
</div>
|
||||
<div class="case-label">
|
||||
PNG16<span class="label">LINEAR LIGHT</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Scene 6: closing card -->
|
||||
<section id="scene-outro" class="scene title-card">
|
||||
<div class="classification">End of File · Shadow Cut</div>
|
||||
<h1 class="display">
|
||||
ONE PIPELINE.<br /><span class="accent">TWO LIGHTS.</span>
|
||||
</h1>
|
||||
<div class="subdisplay">Auto-detected. Cross-converted. Composited.</div>
|
||||
<div class="colophon">
|
||||
<span>Volume III</span>
|
||||
<span>End of Case File</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
Voiceover — one audio track for the full composition
|
||||
============================================================ -->
|
||||
<audio
|
||||
id="vo"
|
||||
src="../assets/voiceover.mp3"
|
||||
data-start="0"
|
||||
data-duration="10.0"
|
||||
></audio>
|
||||
|
||||
<!-- ============================================================
|
||||
Captions — sub-composition, lower-third, Shadow Cut styling
|
||||
============================================================ -->
|
||||
<div
|
||||
id="captions-comp"
|
||||
data-composition-id="captions"
|
||||
data-composition-src="compositions/captions.html"
|
||||
data-start="0"
|
||||
data-duration="10.0"
|
||||
></div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
</script>
|
||||
<script src="script.js"></script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,252 @@
|
||||
/* Shadow Cut — six-scene mixed-transfer composition.
|
||||
|
||||
Scene visibility / handoffs are owned entirely by the
|
||||
@hyperframes/shader-transitions library (Domain Warp on every
|
||||
cut, per the Shadow Cut style guide). We build a paused
|
||||
timeline of *internal* scene motion only — slow creeping
|
||||
push-ins, classification text reveals, dramatic bottom-up
|
||||
case-label entrances — and hand it to HyperShader.init().
|
||||
|
||||
The library appends GL transitions and exposes the shader
|
||||
metadata on window.__hf.transitions so the HDR engine can
|
||||
reproduce each Domain Warp transition in rgb48le pixel
|
||||
space, cross-converting any SDR → PQ or HDR → BT.709 as the
|
||||
render target requires.
|
||||
|
||||
Motion rules (Shadow Cut): power3.out for reveals, power4.in
|
||||
for exits. The pause before the hit matters. Nothing bouncy. */
|
||||
|
||||
(function () {
|
||||
const tl = gsap.timeline({ paused: true, defaults: { ease: "power3.out" } });
|
||||
|
||||
/* ---------- Scene 1 · Intro (0.00 – 1.40) ---------- */
|
||||
const introClass = document.querySelector("#scene-intro .classification");
|
||||
const introDisplay = document.querySelector("#scene-intro .display");
|
||||
const introSub = document.querySelector("#scene-intro .subdisplay");
|
||||
const introFoot = document.querySelector("#scene-intro .colophon");
|
||||
if (introClass && introDisplay && introSub && introFoot) {
|
||||
tl.from(
|
||||
introClass,
|
||||
{ y: 12, opacity: 0, duration: 0.6, ease: "power3.out" },
|
||||
0.05,
|
||||
);
|
||||
tl.from(
|
||||
introDisplay,
|
||||
{ y: 30, opacity: 0, scale: 1.04, duration: 0.85, ease: "power3.out" },
|
||||
0.18,
|
||||
);
|
||||
tl.from(
|
||||
introSub,
|
||||
{ y: 10, opacity: 0, duration: 0.55, ease: "power3.out" },
|
||||
0.55,
|
||||
);
|
||||
tl.from(
|
||||
introFoot,
|
||||
{ y: 8, opacity: 0, duration: 0.5, ease: "power3.out" },
|
||||
0.78,
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Scene 2 · SDR video A (1.85 – 3.20) ----------
|
||||
Slow creeping push-in. The cross-transfer happens inside
|
||||
the engine when it composites this scene's frames into the
|
||||
PQ working space (or leaves them as-is for SDR output). */
|
||||
const vidSdr = document.getElementById("vid-sdr");
|
||||
if (vidSdr) {
|
||||
tl.fromTo(
|
||||
vidSdr,
|
||||
{ scale: 1.08 },
|
||||
{ scale: 1.0, duration: 1.35, ease: "power3.out" },
|
||||
1.85,
|
||||
);
|
||||
}
|
||||
const evSdr = document.querySelectorAll(
|
||||
"#scene-sdr-vid .evidence, #scene-sdr-vid .case-label",
|
||||
);
|
||||
if (evSdr.length) {
|
||||
tl.from(
|
||||
evSdr,
|
||||
{ opacity: 0, y: 8, duration: 0.5, ease: "power3.out", stagger: 0.07 },
|
||||
2.0,
|
||||
);
|
||||
}
|
||||
const ticksSdr = document.querySelectorAll("#scene-sdr-vid .corner-tick");
|
||||
if (ticksSdr.length) {
|
||||
tl.from(
|
||||
ticksSdr,
|
||||
{ opacity: 0, scale: 0.6, duration: 0.4, ease: "power3.out", stagger: 0.04 },
|
||||
1.95,
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Scene 3 · HDR video A (3.65 – 5.00) ----------
|
||||
Pull-back rather than push-in to vary the rhythm. Native
|
||||
PQ source — the engine reads this with its 10-bit reader. */
|
||||
const vidHdr = document.getElementById("vid-hdr");
|
||||
if (vidHdr) {
|
||||
tl.fromTo(
|
||||
vidHdr,
|
||||
{ scale: 1.0 },
|
||||
{ scale: 1.06, duration: 1.35, ease: "power3.out" },
|
||||
3.65,
|
||||
);
|
||||
}
|
||||
const evHdr = document.querySelectorAll(
|
||||
"#scene-hdr-vid .evidence, #scene-hdr-vid .case-label",
|
||||
);
|
||||
if (evHdr.length) {
|
||||
tl.from(
|
||||
evHdr,
|
||||
{ opacity: 0, y: 8, duration: 0.5, ease: "power3.out", stagger: 0.07 },
|
||||
3.8,
|
||||
);
|
||||
}
|
||||
const ticksHdr = document.querySelectorAll("#scene-hdr-vid .corner-tick");
|
||||
if (ticksHdr.length) {
|
||||
tl.from(
|
||||
ticksHdr,
|
||||
{ opacity: 0, scale: 0.6, duration: 0.4, ease: "power3.out", stagger: 0.04 },
|
||||
3.75,
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Scene 4 · SDR still (5.45 – 6.50) ----------
|
||||
Slow horizontal drift to give the still life. */
|
||||
const stillSdr = document.getElementById("img-sdr");
|
||||
if (stillSdr) {
|
||||
tl.fromTo(
|
||||
stillSdr,
|
||||
{ scale: 1.1, x: -22 },
|
||||
{ scale: 1.04, x: 14, duration: 1.05, ease: "power3.out" },
|
||||
5.45,
|
||||
);
|
||||
}
|
||||
const evStillSdr = document.querySelectorAll(
|
||||
"#scene-sdr-still .evidence, #scene-sdr-still .case-label",
|
||||
);
|
||||
if (evStillSdr.length) {
|
||||
tl.from(
|
||||
evStillSdr,
|
||||
{ opacity: 0, y: 8, duration: 0.45, ease: "power3.out", stagger: 0.07 },
|
||||
5.55,
|
||||
);
|
||||
}
|
||||
const ticksStillSdr = document.querySelectorAll(
|
||||
"#scene-sdr-still .corner-tick",
|
||||
);
|
||||
if (ticksStillSdr.length) {
|
||||
tl.from(
|
||||
ticksStillSdr,
|
||||
{ opacity: 0, scale: 0.6, duration: 0.35, ease: "power3.out", stagger: 0.04 },
|
||||
5.5,
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Scene 5 · HDR still (6.95 – 8.00) ----------
|
||||
Counter-direction drift so the rhythm doesn't feel mechanical. */
|
||||
const stillHdr = document.getElementById("img-hdr");
|
||||
if (stillHdr) {
|
||||
tl.fromTo(
|
||||
stillHdr,
|
||||
{ scale: 1.04, x: 18 },
|
||||
{ scale: 1.1, x: -18, duration: 1.05, ease: "power3.out" },
|
||||
6.95,
|
||||
);
|
||||
}
|
||||
const evStillHdr = document.querySelectorAll(
|
||||
"#scene-hdr-still .evidence, #scene-hdr-still .case-label",
|
||||
);
|
||||
if (evStillHdr.length) {
|
||||
tl.from(
|
||||
evStillHdr,
|
||||
{ opacity: 0, y: 8, duration: 0.45, ease: "power3.out", stagger: 0.07 },
|
||||
7.05,
|
||||
);
|
||||
}
|
||||
const ticksStillHdr = document.querySelectorAll(
|
||||
"#scene-hdr-still .corner-tick",
|
||||
);
|
||||
if (ticksStillHdr.length) {
|
||||
tl.from(
|
||||
ticksStillHdr,
|
||||
{ opacity: 0, scale: 0.6, duration: 0.35, ease: "power3.out", stagger: 0.04 },
|
||||
7.0,
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Scene 6 · Outro (8.45 – 10.00) ---------- */
|
||||
const outroClass = document.querySelector("#scene-outro .classification");
|
||||
const outroDisplay = document.querySelector("#scene-outro .display");
|
||||
const outroSub = document.querySelector("#scene-outro .subdisplay");
|
||||
const outroFoot = document.querySelector("#scene-outro .colophon");
|
||||
if (outroClass && outroDisplay && outroSub && outroFoot) {
|
||||
tl.from(
|
||||
outroClass,
|
||||
{ y: 12, opacity: 0, duration: 0.55, ease: "power3.out" },
|
||||
8.5,
|
||||
);
|
||||
tl.from(
|
||||
outroDisplay,
|
||||
{ y: 28, opacity: 0, scale: 1.04, duration: 0.85, ease: "power3.out" },
|
||||
8.62,
|
||||
);
|
||||
tl.from(
|
||||
outroSub,
|
||||
{ y: 10, opacity: 0, duration: 0.55, ease: "power3.out" },
|
||||
9.0,
|
||||
);
|
||||
tl.from(
|
||||
outroFoot,
|
||||
{ y: 8, opacity: 0, duration: 0.5, ease: "power3.out" },
|
||||
9.25,
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- Shader transitions ----------
|
||||
Domain Warp on every handoff (Shadow Cut style guide).
|
||||
init() reads __hf if present (set by the HDR engine), wires
|
||||
transitions into the timeline at the requested times, and
|
||||
registers the timeline on window.__timelines under the
|
||||
compositionId we pass.
|
||||
|
||||
Transition layout (5 cuts, 0.45s each):
|
||||
1.40 → 1.85: intro → sdr-vid
|
||||
3.20 → 3.65: sdr-vid → hdr-vid (BT.709 → PQ jump)
|
||||
5.00 → 5.45: hdr-vid → sdr-still (PQ → BT.709 jump)
|
||||
6.50 → 6.95: sdr-still → hdr-still (BT.709 → PQ jump)
|
||||
8.00 → 8.45: hdr-still → outro
|
||||
*/
|
||||
if (typeof HyperShader !== "undefined" && HyperShader.init) {
|
||||
HyperShader.init({
|
||||
bgColor: "#0a0a0a",
|
||||
accentColor: "#C1121F",
|
||||
compositionId: "main-comp",
|
||||
scenes: [
|
||||
"scene-intro",
|
||||
"scene-sdr-vid",
|
||||
"scene-hdr-vid",
|
||||
"scene-sdr-still",
|
||||
"scene-hdr-still",
|
||||
"scene-outro",
|
||||
],
|
||||
transitions: [
|
||||
{ time: 1.4, shader: "domain-warp", duration: 0.45, ease: "power3.out" },
|
||||
{ time: 3.2, shader: "domain-warp", duration: 0.45, ease: "power3.out" },
|
||||
{ time: 5.0, shader: "domain-warp", duration: 0.45, ease: "power3.out" },
|
||||
{ time: 6.5, shader: "domain-warp", duration: 0.45, ease: "power3.out" },
|
||||
{ time: 8.0, shader: "domain-warp", duration: 0.45, ease: "power3.out" },
|
||||
],
|
||||
timeline: tl,
|
||||
});
|
||||
}
|
||||
|
||||
/* When init() runs above with a custom timeline it does NOT
|
||||
auto-register on window.__timelines — registration only
|
||||
happens when the library owns the timeline. So we always
|
||||
register the timeline ourselves; this also covers the
|
||||
fallback case where HyperShader is unavailable (CDN miss),
|
||||
in which case the engine renders scenes as hard cuts but
|
||||
internal scene motion still plays. */
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["main-comp"] = tl;
|
||||
})();
|
||||
@@ -0,0 +1,314 @@
|
||||
/* ============================================================
|
||||
Shadow Cut — Hans Hillmann inspired film-noir card style.
|
||||
Near-monochrome stack: deep black, cold grey, stark white,
|
||||
plus one accent — blood red (#C1121F). Every reveal emerges
|
||||
from darkness; the shader transition (Domain Warp) is what
|
||||
dissolves reality before the next plate appears.
|
||||
============================================================ */
|
||||
|
||||
@import url("https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Inter:wght@300;400;600&family=JetBrains+Mono:wght@400;600&display=swap");
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #0a0a0a;
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
font-weight: 400;
|
||||
color: #f4f4f4;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#main {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
background: #0a0a0a;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Persistent darkness — a faint cold-grey vignette that lives
|
||||
behind every scene so even when html2canvas captures an
|
||||
"empty" backdrop for a transition it carries the mood. */
|
||||
#main::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
background:
|
||||
radial-gradient(
|
||||
ellipse at 50% 50%,
|
||||
rgba(58, 58, 58, 0.18) 0%,
|
||||
rgba(10, 10, 10, 0.0) 65%
|
||||
),
|
||||
linear-gradient(
|
||||
180deg,
|
||||
#050505 0%,
|
||||
#0a0a0a 50%,
|
||||
#050505 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Faint horizontal interrogation bars — top + bottom. Lend a
|
||||
noir "redacted document" feel without competing with copy. */
|
||||
.bar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 28px;
|
||||
background: #050505;
|
||||
border-top: 1px solid rgba(193, 18, 31, 0.35);
|
||||
border-bottom: 1px solid rgba(193, 18, 31, 0.35);
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.bar.top {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.bar.bottom {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
/* ---------- Scene container baseline ----------
|
||||
Shadow Cut uses shader transitions, so the library will set
|
||||
non-target scenes to opacity:0 during transition and promote
|
||||
the target to opacity:1 on completion. We mirror that — only
|
||||
the intro is visible at frame 0. */
|
||||
|
||||
.scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#scene-intro {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ---------- Title cards (intro + outro) ---------- */
|
||||
|
||||
.title-card {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
padding: 0 180px;
|
||||
}
|
||||
|
||||
.classification {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.36em;
|
||||
text-transform: uppercase;
|
||||
color: #C1121F;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
.classification::before,
|
||||
.classification::after {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 38px;
|
||||
height: 1px;
|
||||
background: #C1121F;
|
||||
}
|
||||
|
||||
.display {
|
||||
font-family: "Bebas Neue", "Oswald", sans-serif;
|
||||
font-size: 184px;
|
||||
font-weight: 400;
|
||||
line-height: 0.94;
|
||||
letter-spacing: 0.012em;
|
||||
color: #fafafa;
|
||||
text-shadow:
|
||||
0 6px 36px rgba(0, 0, 0, 0.85),
|
||||
0 0 1px rgba(255, 255, 255, 0.6);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.display .accent {
|
||||
color: #C1121F;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.subdisplay {
|
||||
margin-top: 48px;
|
||||
font-family: "Inter", sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 0.42em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(244, 244, 244, 0.55);
|
||||
}
|
||||
|
||||
.colophon {
|
||||
margin-top: 72px;
|
||||
display: flex;
|
||||
gap: 72px;
|
||||
justify-content: center;
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.28em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(193, 18, 31, 0.85);
|
||||
}
|
||||
|
||||
/* ---------- Media-frame scenes ---------- */
|
||||
|
||||
.media-frame {
|
||||
background: #050505;
|
||||
}
|
||||
|
||||
.media-frame video,
|
||||
.media-frame img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
/* Heavy shadow vignette so plates look like they're emerging
|
||||
from darkness — Shadow Cut's whole reveal-from-black thing. */
|
||||
.shadow-vignette {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(193, 18, 31, 0.22),
|
||||
inset 0 0 220px rgba(0, 0, 0, 0.78),
|
||||
inset 0 0 80px rgba(0, 0, 0, 0.65);
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
/* Evidence-stamp HUD overlays in the corners. Mono, condensed. */
|
||||
.evidence {
|
||||
position: absolute;
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.28em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(244, 244, 244, 0.85);
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.evidence .red {
|
||||
color: #C1121F;
|
||||
}
|
||||
|
||||
.evidence.top-left {
|
||||
top: 64px;
|
||||
left: 64px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.evidence.top-right {
|
||||
top: 64px;
|
||||
right: 64px;
|
||||
text-align: right;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.evidence.bottom-left {
|
||||
bottom: 64px;
|
||||
left: 64px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* Bottom-right "case label" — large mono number. */
|
||||
.case-label {
|
||||
position: absolute;
|
||||
bottom: 56px;
|
||||
right: 64px;
|
||||
text-align: right;
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-weight: 600;
|
||||
font-size: 64px;
|
||||
letter-spacing: 0.04em;
|
||||
color: #fafafa;
|
||||
line-height: 1;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.case-label .label {
|
||||
display: block;
|
||||
margin-top: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.32em;
|
||||
color: rgba(193, 18, 31, 0.95);
|
||||
}
|
||||
|
||||
/* Diagonal corner ticks — 4 small marks at the inside edge of
|
||||
the plate, like a film slate. */
|
||||
.corner-tick {
|
||||
position: absolute;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.corner-tick::before,
|
||||
.corner-tick::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
background: rgba(244, 244, 244, 0.78);
|
||||
}
|
||||
|
||||
.corner-tick::before {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 22px;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.corner-tick::after {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.corner-tick.top-left {
|
||||
top: 56px;
|
||||
left: 56px;
|
||||
}
|
||||
|
||||
.corner-tick.top-right {
|
||||
top: 56px;
|
||||
right: 56px;
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.corner-tick.bottom-left {
|
||||
bottom: 56px;
|
||||
left: 56px;
|
||||
transform: scaleY(-1);
|
||||
}
|
||||
|
||||
.corner-tick.bottom-right {
|
||||
bottom: 56px;
|
||||
right: 56px;
|
||||
transform: scale(-1, -1);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 250 KiB |
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,142 @@
|
||||
[
|
||||
{
|
||||
"text": "the",
|
||||
"start": 0.1,
|
||||
"end": 0.31
|
||||
},
|
||||
{
|
||||
"text": "makers",
|
||||
"start": 0.31,
|
||||
"end": 0.86
|
||||
},
|
||||
{
|
||||
"text": "of",
|
||||
"start": 0.94,
|
||||
"end": 1.13
|
||||
},
|
||||
{
|
||||
"text": "Subnautica,",
|
||||
"start": 1.13,
|
||||
"end": 2.28
|
||||
},
|
||||
{
|
||||
"text": "and",
|
||||
"start": 2.28,
|
||||
"end": 2.44
|
||||
},
|
||||
{
|
||||
"text": "he",
|
||||
"start": 2.44,
|
||||
"end": 2.54
|
||||
},
|
||||
{
|
||||
"text": "promised",
|
||||
"start": 2.54,
|
||||
"end": 2.99
|
||||
},
|
||||
{
|
||||
"text": "the",
|
||||
"start": 2.99,
|
||||
"end": 3.12
|
||||
},
|
||||
{
|
||||
"text": "founders",
|
||||
"start": 3.12,
|
||||
"end": 3.65
|
||||
},
|
||||
{
|
||||
"text": "$250",
|
||||
"start": 3.75,
|
||||
"end": 4.71
|
||||
},
|
||||
{
|
||||
"text": "million",
|
||||
"start": 4.71,
|
||||
"end": 5.4
|
||||
},
|
||||
{
|
||||
"text": "if",
|
||||
"start": 5.44,
|
||||
"end": 5.6
|
||||
},
|
||||
{
|
||||
"text": "their",
|
||||
"start": 5.6,
|
||||
"end": 5.95
|
||||
},
|
||||
{
|
||||
"text": "next",
|
||||
"start": 5.95,
|
||||
"end": 6.23
|
||||
},
|
||||
{
|
||||
"text": "game",
|
||||
"start": 6.24,
|
||||
"end": 6.64
|
||||
},
|
||||
{
|
||||
"text": "hits",
|
||||
"start": 6.64,
|
||||
"end": 6.91
|
||||
},
|
||||
{
|
||||
"text": "certain",
|
||||
"start": 6.98,
|
||||
"end": 7.36
|
||||
},
|
||||
{
|
||||
"text": "targets.",
|
||||
"start": 7.36,
|
||||
"end": 7.98
|
||||
},
|
||||
{
|
||||
"text": "And",
|
||||
"start": 8.06,
|
||||
"end": 8.14
|
||||
},
|
||||
{
|
||||
"text": "at",
|
||||
"start": 8.14,
|
||||
"end": 8.25
|
||||
},
|
||||
{
|
||||
"text": "the",
|
||||
"start": 8.25,
|
||||
"end": 8.41
|
||||
},
|
||||
{
|
||||
"text": "time,",
|
||||
"start": 8.41,
|
||||
"end": 8.72
|
||||
},
|
||||
{
|
||||
"text": "he",
|
||||
"start": 8.72,
|
||||
"end": 8.82
|
||||
},
|
||||
{
|
||||
"text": "thought",
|
||||
"start": 8.83,
|
||||
"end": 9.19
|
||||
},
|
||||
{
|
||||
"text": "there",
|
||||
"start": 9.24,
|
||||
"end": 9.48
|
||||
},
|
||||
{
|
||||
"text": "was",
|
||||
"start": 9.48,
|
||||
"end": 9.63
|
||||
},
|
||||
{
|
||||
"text": "no",
|
||||
"start": 9.64,
|
||||
"end": 9.75
|
||||
},
|
||||
{
|
||||
"text": "way-",
|
||||
"start": 9.75,
|
||||
"end": 9.98
|
||||
}
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "HDR Regression — SDR Baseline (Velvet Standard)",
|
||||
"description": "Baseline 1920x1080 SDR composition with all media types (video, audio, images), CSS/GSAP transitions, captions, and the Velvet Standard visual identity. Used to confirm that introducing the HDR pipeline does not regress SDR output.",
|
||||
"tags": ["hdr-regression", "sdr", "video", "audio", "image", "captions", "css-transitions", "velvet-standard"],
|
||||
|
||||
"minPsnr": 28,
|
||||
"maxFrameFailures": 0,
|
||||
|
||||
"minAudioCorrelation": 0.85,
|
||||
"maxAudioLagWindows": 120,
|
||||
|
||||
"renderConfig": {
|
||||
"fps": 30
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
<template id="captions-template">
|
||||
<div
|
||||
data-composition-id="captions"
|
||||
data-start="0"
|
||||
data-duration="10.0"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="captions-container"></div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
[data-composition-id="captions"] {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
padding-bottom: 96px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#captions-container {
|
||||
position: relative;
|
||||
width: 1280px;
|
||||
min-height: 120px;
|
||||
text-align: center;
|
||||
font-family: "Cormorant Garamond", "Playfair Display", "Times New Roman", Times, serif;
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
font-size: 56px;
|
||||
color: #fffaf0;
|
||||
text-shadow: 0 4px 20px rgba(0, 0, 0, 0.7);
|
||||
letter-spacing: -0.005em;
|
||||
}
|
||||
|
||||
.vs-chunk {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0 18px;
|
||||
}
|
||||
|
||||
.vs-word {
|
||||
opacity: 0;
|
||||
display: inline-block;
|
||||
transition: none;
|
||||
color: #fffaf0;
|
||||
}
|
||||
|
||||
.vs-word.is-active {
|
||||
color: #c9a55a;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
const TRANSCRIPT = [
|
||||
{ "text": "the", "start": 0.1, "end": 0.31 },
|
||||
{ "text": "makers", "start": 0.31, "end": 0.86 },
|
||||
{ "text": "of", "start": 0.94, "end": 1.13 },
|
||||
{ "text": "Subnautica,", "start": 1.13, "end": 2.28 },
|
||||
{ "text": "and", "start": 2.28, "end": 2.44 },
|
||||
{ "text": "he", "start": 2.44, "end": 2.54 },
|
||||
{ "text": "promised", "start": 2.54, "end": 2.99 },
|
||||
{ "text": "the", "start": 2.99, "end": 3.12 },
|
||||
{ "text": "founders", "start": 3.12, "end": 3.65 },
|
||||
{ "text": "$250", "start": 3.75, "end": 4.71 },
|
||||
{ "text": "million", "start": 4.71, "end": 5.4 },
|
||||
{ "text": "if", "start": 5.44, "end": 5.6 },
|
||||
{ "text": "their", "start": 5.6, "end": 5.95 },
|
||||
{ "text": "next", "start": 5.95, "end": 6.23 },
|
||||
{ "text": "game", "start": 6.24, "end": 6.64 },
|
||||
{ "text": "hits", "start": 6.64, "end": 6.91 },
|
||||
{ "text": "certain", "start": 6.98, "end": 7.36 },
|
||||
{ "text": "targets.", "start": 7.36, "end": 7.98 },
|
||||
{ "text": "And", "start": 8.06, "end": 8.14 },
|
||||
{ "text": "at", "start": 8.14, "end": 8.25 },
|
||||
{ "text": "the", "start": 8.25, "end": 8.41 },
|
||||
{ "text": "time,", "start": 8.41, "end": 8.72 },
|
||||
{ "text": "he", "start": 8.72, "end": 8.82 },
|
||||
{ "text": "thought", "start": 8.83, "end": 9.19 },
|
||||
{ "text": "there", "start": 9.24, "end": 9.48 },
|
||||
{ "text": "was", "start": 9.48, "end": 9.63 },
|
||||
{ "text": "no", "start": 9.64, "end": 9.75 },
|
||||
{ "text": "way-", "start": 9.75, "end": 9.98 }
|
||||
];
|
||||
|
||||
const container = document.getElementById("captions-container");
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// Group into chunks of 4 words
|
||||
const chunks = [];
|
||||
let current = [];
|
||||
for (let i = 0; i < TRANSCRIPT.length; i++) {
|
||||
current.push(TRANSCRIPT[i]);
|
||||
if (current.length >= 4 || i === TRANSCRIPT.length - 1) {
|
||||
chunks.push(current);
|
||||
current = [];
|
||||
}
|
||||
}
|
||||
|
||||
chunks.forEach((chunk) => {
|
||||
const start = chunk[0].start;
|
||||
const end = chunk[chunk.length - 1].end;
|
||||
|
||||
const div = document.createElement("div");
|
||||
div.className = "vs-chunk";
|
||||
container.appendChild(div);
|
||||
|
||||
// Reveal chunk container
|
||||
tl.fromTo(
|
||||
div,
|
||||
{ opacity: 0, y: 14 },
|
||||
{ opacity: 1, y: 0, duration: 0.35, ease: "sine.out" },
|
||||
start - 0.1,
|
||||
);
|
||||
|
||||
chunk.forEach((wordData, wordIndex) => {
|
||||
const w = document.createElement("span");
|
||||
w.className = "vs-word";
|
||||
w.innerText = wordData.text;
|
||||
div.appendChild(w);
|
||||
|
||||
// Word entrance
|
||||
tl.to(
|
||||
w,
|
||||
{ opacity: 1, duration: 0.18, ease: "sine.out" },
|
||||
wordData.start,
|
||||
);
|
||||
|
||||
// Highlight on (gold)
|
||||
tl.to(
|
||||
w,
|
||||
{ color: "#c9a55a", duration: 0.08 },
|
||||
wordData.start,
|
||||
);
|
||||
|
||||
// Highlight off when next word fires (or chunk end)
|
||||
const offTime =
|
||||
wordIndex < chunk.length - 1 ? chunk[wordIndex + 1].start : end;
|
||||
tl.to(
|
||||
w,
|
||||
{ color: "#fffaf0", duration: 0.08 },
|
||||
offTime,
|
||||
);
|
||||
});
|
||||
|
||||
// Hide chunk after last word
|
||||
tl.to(
|
||||
div,
|
||||
{ opacity: 0, y: -10, duration: 0.35, ease: "sine.in" },
|
||||
end + 0.05,
|
||||
);
|
||||
// Deterministic kill — the karaoke word tweens can race the exit fade
|
||||
tl.set(div, { opacity: 0, visibility: "hidden" }, end + 0.4);
|
||||
});
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["captions"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,126 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>HDR Regression — SDR Baseline</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="main"
|
||||
data-composition-id="main-comp"
|
||||
data-start="0"
|
||||
data-duration="10.0"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<!-- ============================================================
|
||||
Velvet Standard — six scenes, ~10s, all hand-staged with
|
||||
CSS/GSAP transitions (no shaders). Tests SDR baseline path.
|
||||
============================================================ -->
|
||||
|
||||
<!-- Scene 1: cold open title card (0.0 – 1.6s) -->
|
||||
<section id="scene-intro" class="scene title-card">
|
||||
<div class="eyebrow">Hyperframes · HDR Regression Suite</div>
|
||||
<h1 class="display">
|
||||
A study in <span class="accent">light</span>,<br />precisely composed.
|
||||
</h1>
|
||||
<div class="colophon">
|
||||
<span>Volume I</span>
|
||||
<span>SDR · BT.709</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Scene 2: SDR video A (1.6 – 4.0s) -->
|
||||
<section id="scene-video-a" class="scene media-frame">
|
||||
<video
|
||||
id="vid-sdr-1"
|
||||
src="../assets/sdr-source-1.mp4"
|
||||
data-start="1.6"
|
||||
data-duration="2.4"
|
||||
muted
|
||||
playsinline
|
||||
></video>
|
||||
<div class="vignette"></div>
|
||||
<div class="glyph">Plate I</div>
|
||||
<div class="lower-third">
|
||||
<span class="kicker">Plate I · Subnautica</span>
|
||||
<span class="label">Saturation, restrained.</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Scene 3: SDR still photograph (4.0 – 5.8s) -->
|
||||
<section id="scene-still" class="scene media-frame">
|
||||
<img
|
||||
id="img-sdr"
|
||||
src="../assets/photo-sdr.jpg"
|
||||
alt="SDR reference photograph"
|
||||
/>
|
||||
<div class="vignette"></div>
|
||||
<div class="glyph">Plate II</div>
|
||||
<div class="lower-third">
|
||||
<span class="kicker">Plate II · Reference</span>
|
||||
<span class="label">Still life, sRGB.</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Scene 4: SDR video B (5.8 – 8.0s) -->
|
||||
<section id="scene-video-b" class="scene media-frame">
|
||||
<video
|
||||
id="vid-sdr-2"
|
||||
src="../assets/sdr-source-2.mp4"
|
||||
data-start="5.8"
|
||||
data-duration="2.2"
|
||||
muted
|
||||
playsinline
|
||||
></video>
|
||||
<div class="vignette"></div>
|
||||
<div class="glyph">Plate III</div>
|
||||
<div class="lower-third">
|
||||
<span class="kicker">Plate III · Studio cut</span>
|
||||
<span class="label">Movement, in measure.</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Scene 5: closing title (8.0 – 10.05s) -->
|
||||
<section id="scene-outro" class="scene title-card">
|
||||
<div class="eyebrow">Composition · Velvet Standard</div>
|
||||
<h1 class="display">
|
||||
<span class="accent">SDR</span> ·<br />the control plate.
|
||||
</h1>
|
||||
<div class="colophon">
|
||||
<span>BT.709 · 8-bit · Rec.709</span>
|
||||
<span>End of Volume I</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
Voiceover — single audio track for the entire composition
|
||||
============================================================ -->
|
||||
<audio
|
||||
id="vo"
|
||||
src="../assets/voiceover.mp3"
|
||||
data-start="0"
|
||||
data-duration="10.0"
|
||||
></audio>
|
||||
|
||||
<!-- ============================================================
|
||||
Captions — sub-composition, lower-third, 5–word chunks
|
||||
============================================================ -->
|
||||
<div
|
||||
id="captions-comp"
|
||||
data-composition-id="captions"
|
||||
data-composition-src="compositions/captions.html"
|
||||
data-start="0"
|
||||
data-duration="10.0"
|
||||
></div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
</script>
|
||||
<script src="script.js"></script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,112 @@
|
||||
/* Velvet Standard — six-scene SDR baseline.
|
||||
Motion rules: sine.inOut for all morphs, power1.out for entrances,
|
||||
long sustains, slow cross-warp morphs. No bouncing, no overshoot. */
|
||||
|
||||
(function () {
|
||||
const tl = gsap.timeline({ paused: true, defaults: { ease: "sine.inOut" } });
|
||||
|
||||
/* The whole comp is 10s. Six scenes, each with a slow Velvet handoff:
|
||||
0.00 – 1.60 intro card (1.60s on screen, 0.45s morph)
|
||||
1.60 – 4.00 sdr video A (2.40s)
|
||||
4.00 – 5.80 sdr still (1.80s)
|
||||
5.80 – 8.00 sdr video B (2.20s)
|
||||
8.00 – 10.00 outro card (2.00s)
|
||||
*/
|
||||
|
||||
const FADE = 0.55;
|
||||
|
||||
const scenes = [
|
||||
{ id: "scene-intro", in: 0.0, out: 1.6 },
|
||||
{ id: "scene-video-a", in: 1.6, out: 4.0 },
|
||||
{ id: "scene-still", in: 4.0, out: 5.8 },
|
||||
{ id: "scene-video-b", in: 5.8, out: 8.0 },
|
||||
{ id: "scene-outro", in: 8.0, out: 10.0 },
|
||||
];
|
||||
|
||||
scenes.forEach((s) => {
|
||||
const el = document.getElementById(s.id);
|
||||
|
||||
tl.fromTo(
|
||||
el,
|
||||
{ opacity: 0, scale: 1.04, filter: "blur(8px) brightness(0.85)" },
|
||||
{
|
||||
opacity: 1,
|
||||
scale: 1.0,
|
||||
filter: "blur(0px) brightness(1)",
|
||||
duration: FADE,
|
||||
ease: "power1.out",
|
||||
},
|
||||
s.in,
|
||||
);
|
||||
|
||||
if (s.out < 10.0) {
|
||||
tl.to(
|
||||
el,
|
||||
{
|
||||
opacity: 0,
|
||||
scale: 0.985,
|
||||
filter: "blur(6px) brightness(0.92)",
|
||||
duration: FADE,
|
||||
ease: "sine.inOut",
|
||||
},
|
||||
s.out - FADE,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/* ----- Inner motion per scene ----- */
|
||||
|
||||
// Intro: serif headline glides up + gold rule sweeps in
|
||||
const introCard = document.querySelector("#scene-intro .display");
|
||||
const introBrow = document.querySelector("#scene-intro .eyebrow");
|
||||
if (introCard && introBrow) {
|
||||
tl.from(introBrow, { y: 14, opacity: 0, duration: 0.7, ease: "power1.out" }, 0.05);
|
||||
tl.from(introCard, { y: 26, opacity: 0, duration: 0.95, ease: "power1.out" }, 0.18);
|
||||
}
|
||||
|
||||
// SDR Video A: subtle Ken Burns push-in
|
||||
const vidA = document.getElementById("vid-sdr-1");
|
||||
if (vidA) {
|
||||
tl.fromTo(
|
||||
vidA,
|
||||
{ scale: 1.06 },
|
||||
{ scale: 1.0, duration: 2.4, ease: "sine.out" },
|
||||
1.6,
|
||||
);
|
||||
}
|
||||
|
||||
// Still: slow horizontal drift to give photo motion
|
||||
const still = document.getElementById("img-sdr");
|
||||
if (still) {
|
||||
tl.fromTo(
|
||||
still,
|
||||
{ scale: 1.08, x: -20 },
|
||||
{ scale: 1.02, x: 18, duration: 1.8, ease: "sine.inOut" },
|
||||
4.0,
|
||||
);
|
||||
}
|
||||
|
||||
// SDR Video B: pull-back, opposite Ken Burns
|
||||
const vidB = document.getElementById("vid-sdr-2");
|
||||
if (vidB) {
|
||||
tl.fromTo(
|
||||
vidB,
|
||||
{ scale: 1.0 },
|
||||
{ scale: 1.06, duration: 2.2, ease: "sine.inOut" },
|
||||
5.8,
|
||||
);
|
||||
}
|
||||
|
||||
// Outro: gold rule sweeps in, headline lifts
|
||||
const outroDisplay = document.querySelector("#scene-outro .display");
|
||||
const outroBrow = document.querySelector("#scene-outro .eyebrow");
|
||||
const outroFoot = document.querySelector("#scene-outro .colophon");
|
||||
if (outroDisplay && outroBrow && outroFoot) {
|
||||
tl.from(outroBrow, { y: 12, opacity: 0, duration: 0.7, ease: "power1.out" }, 8.05);
|
||||
tl.from(outroDisplay, { y: 20, opacity: 0, duration: 0.85, ease: "power1.out" }, 8.18);
|
||||
tl.from(outroFoot, { y: 8, opacity: 0, duration: 0.6, ease: "power1.out" }, 8.55);
|
||||
}
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["main-comp"] = tl;
|
||||
})();
|
||||
@@ -0,0 +1,171 @@
|
||||
/* Velvet Standard — Massimo Vignelli editorial reduction.
|
||||
Deep navy + cream, generous whitespace, Bodoni-style serif headlines. */
|
||||
|
||||
:root {
|
||||
--ink: #fffaf0; /* warm cream */
|
||||
--paper: #0b0d1a; /* near-black with navy bias */
|
||||
--navy: #1a237e; /* deep editorial navy */
|
||||
--gold: #c9a55a; /* aged gold accent */
|
||||
--rule: rgba(255, 250, 240, 0.18);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
font-family: "Cormorant Garamond", "Playfair Display", "Times New Roman", Times, serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#main {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
background: var(--paper);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
.scene video,
|
||||
.scene img {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* ----- title cards (Velvet Standard layout) ----- */
|
||||
|
||||
.title-card {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--paper);
|
||||
display: grid;
|
||||
grid-template-rows: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
padding: 96px 144px;
|
||||
}
|
||||
|
||||
.title-card .eyebrow {
|
||||
font-family: "Inter", "Helvetica Neue", Arial, sans-serif;
|
||||
font-weight: 300;
|
||||
font-size: 22px;
|
||||
letter-spacing: 0.42em;
|
||||
text-transform: uppercase;
|
||||
color: var(--gold);
|
||||
align-self: end;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
padding-bottom: 24px;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.title-card .display {
|
||||
font-family: "Cormorant Garamond", "Playfair Display", "Times New Roman", Times, serif;
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
font-size: 152px;
|
||||
line-height: 1.02;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--ink);
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.title-card .display .accent {
|
||||
color: var(--gold);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.title-card .colophon {
|
||||
font-family: "Inter", "Helvetica Neue", Arial, sans-serif;
|
||||
font-weight: 300;
|
||||
font-size: 18px;
|
||||
letter-spacing: 0.3em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ink);
|
||||
opacity: 0.65;
|
||||
align-self: end;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
/* ----- media frames ----- */
|
||||
|
||||
.media-frame .vignette {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
rgba(0, 0, 0, 0) 55%,
|
||||
rgba(11, 13, 26, 0.55) 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.media-frame .lower-third {
|
||||
position: absolute;
|
||||
left: 96px;
|
||||
bottom: 96px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 20px 28px;
|
||||
border-left: 2px solid var(--gold);
|
||||
}
|
||||
|
||||
.media-frame .lower-third .kicker {
|
||||
font-family: "Inter", sans-serif;
|
||||
font-weight: 300;
|
||||
font-size: 18px;
|
||||
letter-spacing: 0.32em;
|
||||
text-transform: uppercase;
|
||||
color: var(--gold);
|
||||
}
|
||||
|
||||
.media-frame .lower-third .label {
|
||||
font-family: "Cormorant Garamond", serif;
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
font-size: 56px;
|
||||
line-height: 1;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
/* ----- corner glyph (rule signature) ----- */
|
||||
|
||||
.glyph {
|
||||
position: absolute;
|
||||
top: 56px;
|
||||
right: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
font-family: "Inter", sans-serif;
|
||||
font-weight: 300;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.4em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ink);
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.glyph::before {
|
||||
content: "";
|
||||
width: 38px;
|
||||
height: 1px;
|
||||
background: var(--gold);
|
||||
}
|
||||
Reference in New Issue
Block a user