mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
Merge pull request #847 from heygen-com/05-14-test_producer_add_png-sequence_distributed_fixture
test(producer): add png-sequence distributed fixture
This commit is contained in:
@@ -16,6 +16,11 @@ packages/producer/tests/distributed/*/src/*.mp4 filter=lfs diff=lfs merge=lfs -t
|
||||
packages/producer/tests/*/src/*.png filter=lfs diff=lfs merge=lfs -text
|
||||
packages/producer/tests/distributed/*/src/*.png filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
# Golden baseline frames for png-sequence distributed fixtures. Each frame is
|
||||
# a small RGBA PNG (~10-15 KB) but a fixture can carry 60+ of them, and
|
||||
# additional fixtures will grow the set further — LFS keeps the repo lean.
|
||||
packages/producer/tests/distributed/*/output/frames/*.png filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
# GitHub Linguist overrides — HTML files are compositions (user content / templates),
|
||||
# not the framework source. Hide them from the repo language stats so TypeScript,
|
||||
# which is the actual implementation, surfaces as the dominant language.
|
||||
|
||||
@@ -70,7 +70,7 @@ export type DistributedSupportResult = { supported: true } | { supported: false;
|
||||
*/
|
||||
export function checkDistributedSupport(renderConfig: {
|
||||
fps: Fps;
|
||||
format?: "mp4" | "webm";
|
||||
format?: "mp4" | "webm" | "mov" | "png-sequence";
|
||||
hdr?: boolean;
|
||||
}): DistributedSupportResult {
|
||||
if (renderConfig.fps.den !== 1) {
|
||||
|
||||
@@ -46,7 +46,16 @@ type TestMetadata = {
|
||||
* rational at load time so downstream code only sees the structured form.
|
||||
*/
|
||||
fps: import("@hyperframes/core").Fps;
|
||||
format?: "mp4" | "webm"; // Optional: defaults to "mp4"
|
||||
/**
|
||||
* Output container. Defaults to `"mp4"`. `"png-sequence"` makes the
|
||||
* rendered output a directory of zero-padded RGBA PNGs instead of a
|
||||
* single video file — the harness branches its comparison logic
|
||||
* accordingly (per-frame byte equality instead of PSNR). `"mov"` and
|
||||
* `"webm"` are encoded video containers that share the PSNR path with
|
||||
* `"mp4"`. `"webm"` is rejected by the distributed pipeline at plan
|
||||
* time; the in-process renderer accepts it.
|
||||
*/
|
||||
format?: "mp4" | "webm" | "mov" | "png-sequence";
|
||||
workers?: number; // Optional: auto-calculates if omitted
|
||||
/** Force HDR in the harness; omitted/false preserves historical SDR-only test behavior. */
|
||||
hdr?: boolean;
|
||||
@@ -230,8 +239,16 @@ function validateMetadata(meta: unknown): TestMetadata {
|
||||
);
|
||||
}
|
||||
rc.fps = fpsParse.value;
|
||||
if (rc.format !== undefined && rc.format !== "mp4" && rc.format !== "webm") {
|
||||
throw new Error("meta.json: 'renderConfig.format' must be 'mp4' or 'webm' (or omit for mp4)");
|
||||
if (
|
||||
rc.format !== undefined &&
|
||||
rc.format !== "mp4" &&
|
||||
rc.format !== "webm" &&
|
||||
rc.format !== "mov" &&
|
||||
rc.format !== "png-sequence"
|
||||
) {
|
||||
throw new Error(
|
||||
"meta.json: 'renderConfig.format' must be 'mp4', 'webm', 'mov', or 'png-sequence' (or omit for mp4)",
|
||||
);
|
||||
}
|
||||
if (rc.workers !== undefined) {
|
||||
if (typeof rc.workers !== "number" || rc.workers < 1) {
|
||||
@@ -554,7 +571,10 @@ function saveFailureDetails(
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
// Extract images for first 10 failed frames
|
||||
// Extract images for first 10 failed frames. png-sequence outputs are
|
||||
// already directories of PNGs — copy the failing frames directly instead
|
||||
// of running ffmpeg's PSNR frame-selector on a directory (which would
|
||||
// throw "Invalid data found when processing input").
|
||||
const framesToExtract = failedCheckpoints.slice(0, 10);
|
||||
if (framesToExtract.length > 0) {
|
||||
const framesDir = join(failuresDir, "frames");
|
||||
@@ -562,23 +582,52 @@ function saveFailureDetails(
|
||||
mkdirSync(framesDir, { recursive: true });
|
||||
}
|
||||
|
||||
const renderedIsDir =
|
||||
existsSync(renderedVideoPath) && statSync(renderedVideoPath).isDirectory();
|
||||
logPretty(`Extracting ${framesToExtract.length} failed frames...`, "📸");
|
||||
|
||||
for (const checkpoint of framesToExtract) {
|
||||
const timeStr = checkpoint.time.toFixed(2).replace(".", "_");
|
||||
try {
|
||||
extractFrameAsImage(
|
||||
renderedVideoPath,
|
||||
checkpoint.time,
|
||||
join(framesDir, `actual_${timeStr}s.png`),
|
||||
fpsToNumber(suite.meta.renderConfig.fps),
|
||||
);
|
||||
extractFrameAsImage(
|
||||
snapshotVideoPath,
|
||||
checkpoint.time,
|
||||
join(framesDir, `expected_${timeStr}s.png`),
|
||||
fpsToNumber(suite.meta.renderConfig.fps),
|
||||
);
|
||||
if (renderedIsDir) {
|
||||
const frameIndex = Math.max(
|
||||
0,
|
||||
Math.round(checkpoint.time * fpsToNumber(suite.meta.renderConfig.fps)),
|
||||
);
|
||||
const renderedFrames = readdirSync(renderedVideoPath)
|
||||
.filter((n) => n.toLowerCase().endsWith(".png"))
|
||||
.sort();
|
||||
const snapshotFrames = readdirSync(snapshotVideoPath)
|
||||
.filter((n) => n.toLowerCase().endsWith(".png"))
|
||||
.sort();
|
||||
const renderedFrame = renderedFrames[frameIndex];
|
||||
const snapshotFrame = snapshotFrames[frameIndex];
|
||||
if (renderedFrame !== undefined) {
|
||||
copyFileSync(
|
||||
join(renderedVideoPath, renderedFrame),
|
||||
join(framesDir, `actual_${timeStr}s.png`),
|
||||
);
|
||||
}
|
||||
if (snapshotFrame !== undefined) {
|
||||
copyFileSync(
|
||||
join(snapshotVideoPath, snapshotFrame),
|
||||
join(framesDir, `expected_${timeStr}s.png`),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
extractFrameAsImage(
|
||||
renderedVideoPath,
|
||||
checkpoint.time,
|
||||
join(framesDir, `actual_${timeStr}s.png`),
|
||||
fpsToNumber(suite.meta.renderConfig.fps),
|
||||
);
|
||||
extractFrameAsImage(
|
||||
snapshotVideoPath,
|
||||
checkpoint.time,
|
||||
join(framesDir, `expected_${timeStr}s.png`),
|
||||
fpsToNumber(suite.meta.renderConfig.fps),
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
logPretty(` Warning: Could not extract frame at ${checkpoint.time}s`, "⚠️");
|
||||
}
|
||||
@@ -637,13 +686,26 @@ async function runTestSuite(
|
||||
|
||||
const tempDownloadDir = join(tempRoot, "downloads");
|
||||
const outputFormat = suite.meta.renderConfig.format ?? "mp4";
|
||||
const videoExt = outputFormat === "webm" ? ".webm" : ".mp4";
|
||||
const renderedOutputPath = join(tempRoot, `output${videoExt}`);
|
||||
const isPngSequence = outputFormat === "png-sequence";
|
||||
// png-sequence output is a directory; encoded video outputs (mp4/mov/webm)
|
||||
// are single files. `outputSuffix` is appended to the in-temp + baseline
|
||||
// names so both shapes round-trip cleanly.
|
||||
const outputSuffix = isPngSequence
|
||||
? ""
|
||||
: outputFormat === "mp4"
|
||||
? ".mp4"
|
||||
: outputFormat === "mov"
|
||||
? ".mov"
|
||||
: ".webm";
|
||||
const outputBasename = isPngSequence ? "frames" : `output${outputSuffix}`;
|
||||
const renderedOutputPath = join(tempRoot, outputBasename);
|
||||
|
||||
// Snapshot files stored in test's output/ directory
|
||||
// Snapshot files stored in test's output/ directory. For png-sequence the
|
||||
// baseline lives at `output/frames/<frame-N>.png`; for video formats it's
|
||||
// a single `output/output.<ext>` file.
|
||||
const snapshotDir = join(suite.dir, "output");
|
||||
const snapshotCompiledPath = join(snapshotDir, "compiled.html");
|
||||
const snapshotVideoPath = join(snapshotDir, `output${videoExt}`);
|
||||
const snapshotVideoPath = join(snapshotDir, outputBasename);
|
||||
|
||||
console.log(JSON.stringify({ event: "test_start", suite: suite.id, name: suite.meta.name }));
|
||||
logPretty(`Running test: ${suite.meta.name}`, "🧪");
|
||||
@@ -750,17 +812,16 @@ async function runTestSuite(
|
||||
// `checkDistributedSupport` already narrowed fps to {24,30,60} and
|
||||
// rejected webm; the cast surfaces that guarantee to TS.
|
||||
const fpsNum = suite.meta.renderConfig.fps.num as 24 | 30 | 60;
|
||||
// `validateMetadata` only accepts `format: "mp4" | "webm"` in
|
||||
// `renderConfig`, and `checkDistributedSupport` rejected webm above,
|
||||
// so by here only `mp4` (or the unset default) can reach this call.
|
||||
// If the metadata schema grows to accept "mov" / "png-sequence"
|
||||
// someday, narrow this cast accordingly.
|
||||
// `runDistributedSimulatedRender`'s `format` parameter accepts the
|
||||
// distributed-supported set; the harness type allows `"webm"` too
|
||||
// but `checkDistributedSupport` rejected that above. Narrow the cast
|
||||
// accordingly.
|
||||
await runDistributedSimulatedRender({
|
||||
projectDir: tempSrcDir,
|
||||
tempRoot,
|
||||
renderedOutputPath,
|
||||
fps: fpsNum,
|
||||
format: "mp4",
|
||||
format: outputFormat as "mp4" | "mov" | "png-sequence",
|
||||
chunkSize: suite.meta.renderConfig.chunkSize,
|
||||
maxParallelChunks: suite.meta.renderConfig.maxParallelChunks,
|
||||
variables: suite.meta.renderConfig.variables,
|
||||
@@ -788,12 +849,21 @@ async function runTestSuite(
|
||||
if (!existsSync(snapshotDir)) {
|
||||
mkdirSync(snapshotDir, { recursive: true });
|
||||
}
|
||||
copyFileSync(renderedOutputPath, snapshotVideoPath);
|
||||
if (isPngSequence) {
|
||||
// Frames directory — recursive copy so every PNG lands at
|
||||
// `<snapshotDir>/frames/<frame-N>.png`.
|
||||
if (existsSync(snapshotVideoPath)) {
|
||||
rmSync(snapshotVideoPath, { recursive: true, force: true });
|
||||
}
|
||||
cpSync(renderedOutputPath, snapshotVideoPath, { recursive: true });
|
||||
} else {
|
||||
copyFileSync(renderedOutputPath, snapshotVideoPath);
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
event: "snapshot_updated",
|
||||
suite: suite.id,
|
||||
file: `output/output${videoExt}`,
|
||||
file: `output/${outputBasename}`,
|
||||
}),
|
||||
);
|
||||
result.visual = { passed: true, failedFrames: 0, checkpoints: [] };
|
||||
@@ -807,42 +877,108 @@ async function runTestSuite(
|
||||
throw new Error(`Snapshot not found: ${snapshotVideoPath}. Run with --update to create it.`);
|
||||
}
|
||||
|
||||
// Visual comparison (100 frames, 1 per 1% of video duration)
|
||||
logPretty("Comparing visual quality (100 checkpoints)...", "🔍");
|
||||
const videoMetadata = await extractMediaMetadata(renderedOutputPath);
|
||||
const snapshotMetadata = await extractMediaMetadata(snapshotVideoPath);
|
||||
// Sample at the common duration. Container duration can drift between
|
||||
// rendered and snapshot when encoder/mux flags change (e.g. -avoid_negative_ts
|
||||
// can shift the first audio sample, extending reported duration without
|
||||
// changing video frame count). Using the rendered duration alone makes the
|
||||
// last checkpoint land on a frame index that may not exist in the snapshot,
|
||||
// which causes ffmpeg's PSNR filter to emit no `average:` line.
|
||||
const videoDuration = Math.min(videoMetadata.durationSeconds, snapshotMetadata.durationSeconds);
|
||||
|
||||
const minPsnrForMode = resolveMinPsnrForMode(options.mode, suite.meta.minPsnr);
|
||||
let visualPassed: boolean;
|
||||
let failedFrames: number;
|
||||
const visualCheckpoints: Array<{ time: number; psnr: number; passed: boolean }> = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const time = (videoDuration * i) / 100;
|
||||
const psnr = psnrAtCheckpoint(
|
||||
renderedOutputPath,
|
||||
snapshotVideoPath,
|
||||
time,
|
||||
fpsToNumber(suite.meta.renderConfig.fps),
|
||||
);
|
||||
visualCheckpoints.push({
|
||||
time,
|
||||
psnr,
|
||||
passed: psnr >= minPsnrForMode,
|
||||
});
|
||||
|
||||
// Progress indicator every 20 checkpoints
|
||||
if ((i + 1) % 20 === 0) {
|
||||
logPretty(` Progress: ${i + 1}/100 checkpoints`, " ");
|
||||
if (isPngSequence) {
|
||||
// png-sequence visual comparison: byte-equal per frame. The renderer's
|
||||
// png output is the raw RGBA Chrome captured, with libpng deflate
|
||||
// applied — byte-identical pixels round-trip to byte-identical files.
|
||||
// Comparing whole-file SHA-256 catches both pixel drift and any
|
||||
// metadata-chunk reorder that would also be a regression.
|
||||
logPretty("Comparing png-sequence frames...", "🔍");
|
||||
const renderedFrames = readdirSync(renderedOutputPath)
|
||||
.filter((name) => name.toLowerCase().endsWith(".png"))
|
||||
.sort();
|
||||
const snapshotFrames = readdirSync(snapshotVideoPath)
|
||||
.filter((name) => name.toLowerCase().endsWith(".png"))
|
||||
.sort();
|
||||
if (renderedFrames.length !== snapshotFrames.length) {
|
||||
logPretty(
|
||||
`Frame count mismatch: rendered=${renderedFrames.length}, snapshot=${snapshotFrames.length}`,
|
||||
"✗",
|
||||
);
|
||||
result.visual = {
|
||||
passed: false,
|
||||
failedFrames: Math.abs(renderedFrames.length - snapshotFrames.length),
|
||||
checkpoints: [],
|
||||
};
|
||||
result.audio = { passed: true, correlation: 1, lagWindows: 0 };
|
||||
result.passed = false;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
failedFrames = 0;
|
||||
const fpsForLog = fpsToNumber(suite.meta.renderConfig.fps);
|
||||
for (let i = 0; i < renderedFrames.length; i++) {
|
||||
const renderedFrameName = renderedFrames[i];
|
||||
const snapshotFrameName = snapshotFrames[i];
|
||||
// Defensive: TypeScript's strict-mode index returns `string | undefined`
|
||||
// even though we just length-checked. Skip with a failure if the
|
||||
// filename ever comes back undefined.
|
||||
if (renderedFrameName === undefined || snapshotFrameName === undefined) {
|
||||
failedFrames++;
|
||||
continue;
|
||||
}
|
||||
const renderedBytes = readFileSync(join(renderedOutputPath, renderedFrameName));
|
||||
const snapshotBytes = readFileSync(join(snapshotVideoPath, snapshotFrameName));
|
||||
const equal =
|
||||
renderedFrameName === snapshotFrameName &&
|
||||
renderedBytes.byteLength === snapshotBytes.byteLength &&
|
||||
renderedBytes.equals(snapshotBytes);
|
||||
visualCheckpoints.push({
|
||||
time: i / fpsForLog,
|
||||
// PSNR is Infinity for byte-identical frames, 0 otherwise. The
|
||||
// existing summary code interprets psnr >= threshold as "passed"
|
||||
// and JSON-serializes Infinity as null; both render correctly.
|
||||
psnr: equal ? Number.POSITIVE_INFINITY : 0,
|
||||
passed: equal,
|
||||
});
|
||||
if (!equal) failedFrames++;
|
||||
if ((i + 1) % 20 === 0) {
|
||||
logPretty(` Progress: ${i + 1}/${renderedFrames.length} frames`, " ");
|
||||
}
|
||||
}
|
||||
visualPassed = failedFrames <= suite.meta.maxFrameFailures;
|
||||
} else {
|
||||
// Visual comparison (100 frames, 1 per 1% of video duration)
|
||||
logPretty("Comparing visual quality (100 checkpoints)...", "🔍");
|
||||
const videoMetadata = await extractMediaMetadata(renderedOutputPath);
|
||||
const snapshotMetadata = await extractMediaMetadata(snapshotVideoPath);
|
||||
// Sample at the common duration. Container duration can drift between
|
||||
// rendered and snapshot when encoder/mux flags change (e.g. -avoid_negative_ts
|
||||
// can shift the first audio sample, extending reported duration without
|
||||
// changing video frame count). Using the rendered duration alone makes the
|
||||
// last checkpoint land on a frame index that may not exist in the snapshot,
|
||||
// which causes ffmpeg's PSNR filter to emit no `average:` line.
|
||||
const videoDuration = Math.min(
|
||||
videoMetadata.durationSeconds,
|
||||
snapshotMetadata.durationSeconds,
|
||||
);
|
||||
|
||||
const failedFrames = visualCheckpoints.filter((c) => !c.passed).length;
|
||||
const visualPassed = failedFrames <= suite.meta.maxFrameFailures;
|
||||
const minPsnrForMode = resolveMinPsnrForMode(options.mode, suite.meta.minPsnr);
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const time = (videoDuration * i) / 100;
|
||||
const psnr = psnrAtCheckpoint(
|
||||
renderedOutputPath,
|
||||
snapshotVideoPath,
|
||||
time,
|
||||
fpsToNumber(suite.meta.renderConfig.fps),
|
||||
);
|
||||
visualCheckpoints.push({
|
||||
time,
|
||||
psnr,
|
||||
passed: psnr >= minPsnrForMode,
|
||||
});
|
||||
|
||||
// Progress indicator every 20 checkpoints
|
||||
if ((i + 1) % 20 === 0) {
|
||||
logPretty(` Progress: ${i + 1}/100 checkpoints`, " ");
|
||||
}
|
||||
}
|
||||
|
||||
failedFrames = visualCheckpoints.filter((c) => !c.passed).length;
|
||||
visualPassed = failedFrames <= suite.meta.maxFrameFailures;
|
||||
}
|
||||
|
||||
result.visual = {
|
||||
passed: visualPassed,
|
||||
@@ -872,26 +1008,30 @@ async function runTestSuite(
|
||||
);
|
||||
}
|
||||
|
||||
// Audio comparison
|
||||
logPretty("Comparing audio quality...", "🔊");
|
||||
const renderedAudio = extractMonoPcm16(renderedOutputPath);
|
||||
const snapshotAudio = extractMonoPcm16(snapshotVideoPath);
|
||||
|
||||
// Audio comparison. png-sequence outputs are frame directories with no
|
||||
// audio channel — there's nothing to compare, so we report pass and
|
||||
// skip the envelope correlation entirely.
|
||||
let audioPassed = true;
|
||||
let audioCorrelation = 1;
|
||||
let audioLagWindows = 0;
|
||||
|
||||
if (renderedAudio.length > 0 && snapshotAudio.length > 0) {
|
||||
const renderedEnvelope = buildRmsEnvelope(renderedAudio);
|
||||
const snapshotEnvelope = buildRmsEnvelope(snapshotAudio);
|
||||
const audio = compareAudioEnvelopes(
|
||||
renderedEnvelope,
|
||||
snapshotEnvelope,
|
||||
suite.meta.maxAudioLagWindows,
|
||||
);
|
||||
audioCorrelation = audio.correlation;
|
||||
audioLagWindows = audio.lagWindows;
|
||||
audioPassed = audio.correlation >= suite.meta.minAudioCorrelation;
|
||||
if (!isPngSequence) {
|
||||
logPretty("Comparing audio quality...", "🔊");
|
||||
const renderedAudio = extractMonoPcm16(renderedOutputPath);
|
||||
const snapshotAudio = extractMonoPcm16(snapshotVideoPath);
|
||||
|
||||
if (renderedAudio.length > 0 && snapshotAudio.length > 0) {
|
||||
const renderedEnvelope = buildRmsEnvelope(renderedAudio);
|
||||
const snapshotEnvelope = buildRmsEnvelope(snapshotAudio);
|
||||
const audio = compareAudioEnvelopes(
|
||||
renderedEnvelope,
|
||||
snapshotEnvelope,
|
||||
suite.meta.maxAudioLagWindows,
|
||||
);
|
||||
audioCorrelation = audio.correlation;
|
||||
audioLagWindows = audio.lagWindows;
|
||||
audioPassed = audio.correlation >= suite.meta.minAudioCorrelation;
|
||||
}
|
||||
}
|
||||
|
||||
result.audio = {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "Distributed: png-sequence",
|
||||
"description": "60-frame composition (2s @ 30fps) with transparent background, text, and a rotating SVG icon. Output is a directory of zero-padded RGBA PNGs; the assemble path merges chunk frame directories rather than concat-copying mp4 files. renderConfig.chunkSize=15 produces N=4 chunks, exercising the per-frame state continuity across chunk seams that distinguishes the assemble path's directory-merge from mp4's concat-copy. maxFrameFailures=0 makes this a strict byte-identity gate; the upstream byte sources are Chrome's CDP screenshot output and libpng's deflate, so a Chromium or zlib bump in Dockerfile.test will produce identical pixels but different bytes — the failure mode on a Chrome version bump is 'regenerate baselines via docker:test:update', not 'investigate regression'.",
|
||||
"tags": ["distributed", "png-sequence", "alpha"],
|
||||
|
||||
"minPsnr": 30,
|
||||
"maxFrameFailures": 0,
|
||||
|
||||
"minAudioCorrelation": 0.9,
|
||||
"maxAudioLagWindows": 120,
|
||||
|
||||
"renderConfig": {
|
||||
"fps": 30,
|
||||
"format": "png-sequence",
|
||||
"chunkSize": 15
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7d866c0c58cb7204be92e2730cf72b6f44bf66f0d5258b33387f4a0fe8535300
|
||||
size 7997
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2da1f3a1a728c9985d23449545c68b9ce8321b711feffa753373c923bbfc524c
|
||||
size 9205
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:94bb7045aeef6d2dd8d12148db9ae4b71aed97ae88621f807736e9d01061a073
|
||||
size 9261
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ca879e3cfa9234a1249c3a83a6caf03eaa0e34cc9161052834a9a4dc008e7b31
|
||||
size 9272
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c8a0d4f9830bf0d4547141a0f37db75d129fcab321908c81854d7eaa4d99cf1b
|
||||
size 9249
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e5ab5ccf721fad514cedde2c0cc11fb532f9b345eb33fd7675fb6ba87fb065b3
|
||||
size 9322
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7e7cc26f468dc5909e3e4e4f74acb8c3432f36f96c1249d708f71586fbb9a854
|
||||
size 9302
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ff479888c0aeca94395550596527ae575867a2d7282cd1e1f5f21103537bc057
|
||||
size 9371
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e18e4b2b120c8ffa0b875d5c678ced2cee553c27868282f09e64ec7c97dc5417
|
||||
size 9385
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:29704f744985f554129efea32be6ce4994d9b10d2aad948140ed2b9deff69856
|
||||
size 9285
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7648ddbbd67e3ce185b3c159cc2baee343df59d933a0b8c73f2b96bcc2b32fcc
|
||||
size 9303
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8a760105081d5b86b62c91312a89278575b0095edd25d48c13b11f4e10eaef31
|
||||
size 9236
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:999f2d14b6f9e450cbfe0cb8936de6b1d9e311f6c92f6ce9f5e04554db5bccba
|
||||
size 9265
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a4a38f94b45ad6c9bd028c7edf8e977e32e2c5582b187fdbce31b6e1c8145f79
|
||||
size 9286
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fe3ee790c19b220dfbfa76f551f2b8139195f79339682306090d9dc3c752a2a6
|
||||
size 9204
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7d866c0c58cb7204be92e2730cf72b6f44bf66f0d5258b33387f4a0fe8535300
|
||||
size 7997
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2da1f3a1a728c9985d23449545c68b9ce8321b711feffa753373c923bbfc524c
|
||||
size 9205
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:94bb7045aeef6d2dd8d12148db9ae4b71aed97ae88621f807736e9d01061a073
|
||||
size 9261
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ca879e3cfa9234a1249c3a83a6caf03eaa0e34cc9161052834a9a4dc008e7b31
|
||||
size 9272
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c8a0d4f9830bf0d4547141a0f37db75d129fcab321908c81854d7eaa4d99cf1b
|
||||
size 9249
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e5ab5ccf721fad514cedde2c0cc11fb532f9b345eb33fd7675fb6ba87fb065b3
|
||||
size 9322
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:70b8e8667affb7e66d0af173f663e073be43e01fc63232e4685948726115869a
|
||||
size 9301
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ff479888c0aeca94395550596527ae575867a2d7282cd1e1f5f21103537bc057
|
||||
size 9371
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4aa7bf1f9baafd9e52233cf9d0d30bfddf4fee8a1201f6cce0e2ab975ff49d93
|
||||
size 9389
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:15783fa540e8604a64ba009e99e9957befef302c6b60ead09f38a5e04a2b6184
|
||||
size 9285
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7648ddbbd67e3ce185b3c159cc2baee343df59d933a0b8c73f2b96bcc2b32fcc
|
||||
size 9303
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8a760105081d5b86b62c91312a89278575b0095edd25d48c13b11f4e10eaef31
|
||||
size 9236
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:999f2d14b6f9e450cbfe0cb8936de6b1d9e311f6c92f6ce9f5e04554db5bccba
|
||||
size 9265
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fc4203840af5af3b3a65ab6143f67b3d28cbc2d06308298cedac8cc3ed6aa0f2
|
||||
size 11311
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:18bd47e4ef75975ff9f5e3da0a56f1d12fbb39b896ac884abf1fa888ba84fd2e
|
||||
size 11391
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3fe1d484ba324f3b0a638ed75557e9fa708a09561b7d85975ce41faf28172da9
|
||||
size 10344
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:15783d4521f12eb7063e7e15725afe5737b504b972eb1580393a50a53d03dce6
|
||||
size 11471
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7150c982c63b59748879a11766c57bbb39c9c4ae2f229e0e1238ade8b1d9506e
|
||||
size 11467
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6122eea9ea5c9cafed77056744210b93f500be45e1b4926f91be0e8cd8cfec22
|
||||
size 10046
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3c779c178c493c24c4943f0cacc176f7fcee9eb5703049f559abbea5f4c87331
|
||||
size 10031
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e9e6304269ee5171232413e80b8a7eea7c368fe98cab5af0d19534314e3bdac6
|
||||
size 10110
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:72929e2123f754e2d22c58b8ab0adadbe405499876eb9a169bee00e148ecd31f
|
||||
size 10093
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4ab23376d28d3bd81de4aa6060ae45d9a624e49907f74170df641706e3ea46e8
|
||||
size 10156
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5aa7c542c21cd072fca9702d0bad1cd802a03d57b56d9631a35bb4bc4bead195
|
||||
size 10164
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8222c07a1d46eee96ce20c1f7c84be8a5609922f8589d5c4f6a38a69f002171f
|
||||
size 10070
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7241a58d580ff19f59562f261fc7c45ae87d17a0b7d61f2717a73fbcafb6cf2c
|
||||
size 10093
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a274b147f50782918ae6ba61e58b7106997c545d2eda73ea9e567b30811a2d35
|
||||
size 10020
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5b9f7ccca8ce3405abd9244d0de21344bc159f24bad5ce118e8e0f8e89acf56d
|
||||
size 10040
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:47d773c1304be5a52e5f3196bab02283032bc68a5f6008209c38471db7ab7bab
|
||||
size 10070
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d3ed0e78fd503af5c75fa3c9a16a9ae54e2e17376825acb4ccc8d7cbdcba3361
|
||||
size 9990
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d8779c23e767dd7e06a75eb2f45e26fe57e00e741f6b25a169fd55107a0a9bbe
|
||||
size 8828
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:58a5de4260703b7c5d4a02980557f83d873544b319e03b474e2e7abaeaec8cfa
|
||||
size 9988
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c75fef553c769936720e04e3b0bb0e43315c480340645abdadfd9983614fa797
|
||||
size 10043
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6122eea9ea5c9cafed77056744210b93f500be45e1b4926f91be0e8cd8cfec22
|
||||
size 10046
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3c779c178c493c24c4943f0cacc176f7fcee9eb5703049f559abbea5f4c87331
|
||||
size 10031
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e9e6304269ee5171232413e80b8a7eea7c368fe98cab5af0d19534314e3bdac6
|
||||
size 10110
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8e3c40aa3d22eeb97e7a490f7e87842fd4a76b7c81d78c9ce1e2c2467187e247
|
||||
size 10097
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4ab23376d28d3bd81de4aa6060ae45d9a624e49907f74170df641706e3ea46e8
|
||||
size 10156
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5aa7c542c21cd072fca9702d0bad1cd802a03d57b56d9631a35bb4bc4bead195
|
||||
size 10164
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8222c07a1d46eee96ce20c1f7c84be8a5609922f8589d5c4f6a38a69f002171f
|
||||
size 10070
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7241a58d580ff19f59562f261fc7c45ae87d17a0b7d61f2717a73fbcafb6cf2c
|
||||
size 10093
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a274b147f50782918ae6ba61e58b7106997c545d2eda73ea9e567b30811a2d35
|
||||
size 10020
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5b9f7ccca8ce3405abd9244d0de21344bc159f24bad5ce118e8e0f8e89acf56d
|
||||
size 10040
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:47d773c1304be5a52e5f3196bab02283032bc68a5f6008209c38471db7ab7bab
|
||||
size 10070
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d3ed0e78fd503af5c75fa3c9a16a9ae54e2e17376825acb4ccc8d7cbdcba3361
|
||||
size 9990
|
||||
@@ -0,0 +1,109 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
||||
<title>png-sequence distributed fixture</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
|
||||
<style>
|
||||
@import url("https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap");
|
||||
|
||||
body,
|
||||
html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 640px;
|
||||
height: 360px;
|
||||
/* Transparent background so the png-sequence's alpha channel actually carries information. */
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
font-family: "Space Mono", monospace;
|
||||
}
|
||||
|
||||
#main-comp {
|
||||
position: relative;
|
||||
width: 640px;
|
||||
height: 360px;
|
||||
}
|
||||
|
||||
.label {
|
||||
position: absolute;
|
||||
top: 22%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 18px;
|
||||
letter-spacing: 4px;
|
||||
color: #94a3b8;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.title {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-family: "Space Mono", monospace;
|
||||
font-size: 56px;
|
||||
font-weight: 700;
|
||||
color: #6366f1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
bottom: 18%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="main-comp"
|
||||
data-composition-id="main-comp"
|
||||
data-width="640"
|
||||
data-height="360"
|
||||
data-start="0"
|
||||
data-duration="2"
|
||||
>
|
||||
<div class="stage" id="stage-a">
|
||||
<div class="label">PHASE</div>
|
||||
<div class="title" id="title-a">ALPHA ONE</div>
|
||||
</div>
|
||||
<div class="stage" id="stage-b" style="opacity: 0">
|
||||
<div class="label">PHASE</div>
|
||||
<div class="title" id="title-b">ALPHA TWO</div>
|
||||
</div>
|
||||
<svg class="icon" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg" id="icon">
|
||||
<circle cx="24" cy="24" r="18" fill="none" stroke="#6366f1" stroke-width="4" />
|
||||
<circle cx="24" cy="24" r="6" fill="#6366f1" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Same chunk-boundary stressors as the mp4-h264-sdr fixture: a
|
||||
// crossfade straddling frame 30 and a continuous rotation across
|
||||
// every chunk seam. A png-sequence regression at a seam would show
|
||||
// up as a sudden brightness step or rotation discontinuity in the
|
||||
// boundary frame.
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["main-comp"] = tl;
|
||||
|
||||
const stageA = document.getElementById("stage-a");
|
||||
const stageB = document.getElementById("stage-b");
|
||||
const icon = document.getElementById("icon");
|
||||
|
||||
tl.to(stageA, { opacity: 0, duration: 0.2, ease: "none" }, 0.9);
|
||||
tl.to(stageB, { opacity: 1, duration: 0.2, ease: "none" }, 0.9);
|
||||
tl.to(icon, { rotation: 360, duration: 2, ease: "none" }, 0);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user