mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 00:56:23 +00:00
fix(skills): preserve caption skin contrast states (#2486)
* fix(skills): preserve caption skin contrast states * chore(skills): refresh caption contrast manifest * fix(skills): keep caption skin ownership aligned * style(skills): format shared caption builders
This commit is contained in:
@@ -6,8 +6,8 @@
|
|||||||
"files": 144
|
"files": 144
|
||||||
},
|
},
|
||||||
"faceless-explainer": {
|
"faceless-explainer": {
|
||||||
"hash": "e486bec3499bd84d",
|
"hash": "a87a402ac73f3166",
|
||||||
"files": 19
|
"files": 20
|
||||||
},
|
},
|
||||||
"figma": {
|
"figma": {
|
||||||
"hash": "0e6e96f5a76ff824",
|
"hash": "0e6e96f5a76ff824",
|
||||||
@@ -58,12 +58,12 @@
|
|||||||
"files": 132
|
"files": 132
|
||||||
},
|
},
|
||||||
"pr-to-video": {
|
"pr-to-video": {
|
||||||
"hash": "739774abd773a3d3",
|
"hash": "6bc0d09a63097455",
|
||||||
"files": 28
|
"files": 29
|
||||||
},
|
},
|
||||||
"product-launch-video": {
|
"product-launch-video": {
|
||||||
"hash": "86fcf6421ab9aa8f",
|
"hash": "277fe2829b8da022",
|
||||||
"files": 23
|
"files": 24
|
||||||
},
|
},
|
||||||
"remotion-to-hyperframes": {
|
"remotion-to-hyperframes": {
|
||||||
"hash": "c96bb2f0af9e1143",
|
"hash": "c96bb2f0af9e1143",
|
||||||
|
|||||||
@@ -34,9 +34,10 @@
|
|||||||
|
|
||||||
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
import { dirname, join, resolve } from "node:path";
|
import { dirname, join, resolve } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
import { parseStoryboard } from "./lib/storyboard.mjs";
|
import { parseStoryboard } from "./lib/storyboard.mjs";
|
||||||
import { captionBand, parseFormat } from "./lib/dimensions.mjs";
|
import { captionBand, parseFormat } from "./lib/dimensions.mjs";
|
||||||
import { parseColors, parseFonts, semanticColors, lum } from "./lib/tokens.mjs";
|
import { parseColors, parseFonts, semanticColors } from "./lib/tokens.mjs";
|
||||||
|
|
||||||
const flag = (argv, name, def) => {
|
const flag = (argv, name, def) => {
|
||||||
const i = argv.indexOf(`--${name}`);
|
const i = argv.indexOf(`--${name}`);
|
||||||
@@ -276,23 +277,11 @@ function buildFromSkin(skin, groups, total, W, H, tokens, die, faces = "", fonts
|
|||||||
// overflow:hidden — nothing is clipped); zeroing it would need an airy line-height that
|
// overflow:hidden — nothing is clipped); zeroing it would need an airy line-height that
|
||||||
// balloons the pill, which is worse. Override only if a brand font genuinely clips.
|
// balloons the pill, which is worse. Override only if a brand font genuinely clips.
|
||||||
out += "\n<style>\n .caption-line { line-height: 1.1 !important; }\n</style>";
|
out += "\n<style>\n .caption-line { line-height: 1.1 !important; }\n</style>";
|
||||||
// dark-ground caption contrast (auto). The preset caption skins are tuned for a LIGHT
|
|
||||||
// pill (cream); on a dark brand ground the pill goes near-black, so the skin's faint
|
|
||||||
// upcoming-word mix and light highlight block turn unreadable. When the resolved caption
|
|
||||||
// canvas is dark, override the three word states: a brighter muted upcoming color + an
|
|
||||||
// on-brand ACCENT highlight block with light text. Light grounds are left untouched.
|
|
||||||
const capCanvas = (tokens.match(/--cap-canvas:\s*(#[0-9a-fA-F]{6})/) || [])[1];
|
|
||||||
if (capCanvas && (lum(capCanvas) ?? 255) < 90) {
|
|
||||||
out +=
|
|
||||||
"\n<style>\n" +
|
|
||||||
" .caption-word { color: color-mix(in srgb, var(--cap-ink) 64%, var(--cap-canvas)); }\n" +
|
|
||||||
" .caption-word.is-active { color: var(--cap-ink); background: var(--cap-accent); box-shadow: 0 0 0 0.06em var(--cap-accent); }\n" +
|
|
||||||
" .caption-word.is-spoken { color: var(--cap-ink); background: transparent; box-shadow: none; }\n" +
|
|
||||||
"</style>";
|
|
||||||
}
|
|
||||||
return `<template id="captions-template" data-composition-id="captions" data-width="${W}" data-height="${H}">\n${out.trim()}\n</template>\n`;
|
return `<template id="captions-template" data-composition-id="captions" data-width="${W}" data-height="${H}">\n${out.trim()}\n</template>\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { buildFromSkin };
|
||||||
|
|
||||||
// @font-face for the brand display/body fonts, matched from the project's font dirs
|
// @font-face for the brand display/body fonts, matched from the project's font dirs
|
||||||
// (staged assets/fonts first, else capture/assets/fonts) by family-name prefix, with
|
// (staged assets/fonts first, else capture/assets/fonts) by family-name prefix, with
|
||||||
// weight parsed from the filename. Paths are relative to compositions/captions.html.
|
// weight parsed from the filename. Paths are relative to compositions/captions.html.
|
||||||
@@ -498,6 +487,7 @@ function buildCaptionsHtml(groups, total, W, H) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||||
const sub = process.argv[2];
|
const sub = process.argv[2];
|
||||||
if (sub === "build" || sub === undefined) runBuild(process.argv.slice(sub === "build" ? 3 : 2));
|
if (sub === "build" || sub === undefined) runBuild(process.argv.slice(sub === "build" ? 3 : 2));
|
||||||
else {
|
else {
|
||||||
@@ -506,3 +496,4 @@ else {
|
|||||||
);
|
);
|
||||||
process.exit(2);
|
process.exit(2);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { readdirSync, readFileSync } from "node:fs";
|
||||||
|
import test from "node:test";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { buildFromSkin } from "./captions.mjs";
|
||||||
|
|
||||||
|
const presetsDir = fileURLToPath(
|
||||||
|
new URL("../../hyperframes-creative/frame-presets/", import.meta.url),
|
||||||
|
);
|
||||||
|
const skins = readdirSync(presetsDir, { withFileTypes: true })
|
||||||
|
.filter((entry) => entry.isDirectory())
|
||||||
|
.map((entry) => ({
|
||||||
|
name: entry.name,
|
||||||
|
source: readFileSync(
|
||||||
|
new URL(
|
||||||
|
`../../hyperframes-creative/frame-presets/${entry.name}/caption-skin.html`,
|
||||||
|
import.meta.url,
|
||||||
|
),
|
||||||
|
"utf8",
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
.filter(({ source }) => source.includes(".caption-word.is-active"));
|
||||||
|
|
||||||
|
for (const canvas of ["#f7f3e8", "#111827"]) {
|
||||||
|
for (const skin of skins) {
|
||||||
|
test(`${skin.name} preserves word-state rules on ${canvas}`, () => {
|
||||||
|
const active = skin.source.match(/\.caption-word\.is-active\s*\{[^}]*\}/s)?.[0];
|
||||||
|
const spoken = skin.source.match(/\.caption-word\.is-spoken\s*\{[^}]*\}/s)?.[0];
|
||||||
|
assert.ok(active, "skin must define an active-word rule");
|
||||||
|
assert.ok(spoken, "skin must define a spoken-word rule");
|
||||||
|
|
||||||
|
const output = buildFromSkin(
|
||||||
|
skin.source,
|
||||||
|
[],
|
||||||
|
1,
|
||||||
|
1920,
|
||||||
|
1080,
|
||||||
|
`:root { --cap-canvas: ${canvas}; --cap-ink: #111111; --cap-accent: #ffcc00; }`,
|
||||||
|
(message) => {
|
||||||
|
throw new Error(message);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(output.includes(active));
|
||||||
|
assert.ok(output.includes(spoken));
|
||||||
|
assert.equal(output.match(/\.caption-word\.is-active\s*\{/g)?.length, 1);
|
||||||
|
assert.equal(output.match(/\.caption-word\.is-spoken\s*\{/g)?.length, 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,9 +34,10 @@
|
|||||||
|
|
||||||
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
import { dirname, join, resolve } from "node:path";
|
import { dirname, join, resolve } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
import { parseStoryboard } from "./lib/storyboard.mjs";
|
import { parseStoryboard } from "./lib/storyboard.mjs";
|
||||||
import { captionBand, parseFormat } from "./lib/dimensions.mjs";
|
import { captionBand, parseFormat } from "./lib/dimensions.mjs";
|
||||||
import { parseColors, parseFonts, semanticColors, lum } from "./lib/tokens.mjs";
|
import { parseColors, parseFonts, semanticColors } from "./lib/tokens.mjs";
|
||||||
|
|
||||||
const flag = (argv, name, def) => {
|
const flag = (argv, name, def) => {
|
||||||
const i = argv.indexOf(`--${name}`);
|
const i = argv.indexOf(`--${name}`);
|
||||||
@@ -276,23 +277,11 @@ function buildFromSkin(skin, groups, total, W, H, tokens, die, faces = "", fonts
|
|||||||
// overflow:hidden — nothing is clipped); zeroing it would need an airy line-height that
|
// overflow:hidden — nothing is clipped); zeroing it would need an airy line-height that
|
||||||
// balloons the pill, which is worse. Override only if a brand font genuinely clips.
|
// balloons the pill, which is worse. Override only if a brand font genuinely clips.
|
||||||
out += "\n<style>\n .caption-line { line-height: 1.1 !important; }\n</style>";
|
out += "\n<style>\n .caption-line { line-height: 1.1 !important; }\n</style>";
|
||||||
// dark-ground caption contrast (auto). The preset caption skins are tuned for a LIGHT
|
|
||||||
// pill (cream); on a dark brand ground the pill goes near-black, so the skin's faint
|
|
||||||
// upcoming-word mix and light highlight block turn unreadable. When the resolved caption
|
|
||||||
// canvas is dark, override the three word states: a brighter muted upcoming color + an
|
|
||||||
// on-brand ACCENT highlight block with light text. Light grounds are left untouched.
|
|
||||||
const capCanvas = (tokens.match(/--cap-canvas:\s*(#[0-9a-fA-F]{6})/) || [])[1];
|
|
||||||
if (capCanvas && (lum(capCanvas) ?? 255) < 90) {
|
|
||||||
out +=
|
|
||||||
"\n<style>\n" +
|
|
||||||
" .caption-word { color: color-mix(in srgb, var(--cap-ink) 64%, var(--cap-canvas)); }\n" +
|
|
||||||
" .caption-word.is-active { color: var(--cap-ink); background: var(--cap-accent); box-shadow: 0 0 0 0.06em var(--cap-accent); }\n" +
|
|
||||||
" .caption-word.is-spoken { color: var(--cap-ink); background: transparent; box-shadow: none; }\n" +
|
|
||||||
"</style>";
|
|
||||||
}
|
|
||||||
return `<template id="captions-template" data-composition-id="captions" data-width="${W}" data-height="${H}">\n${out.trim()}\n</template>\n`;
|
return `<template id="captions-template" data-composition-id="captions" data-width="${W}" data-height="${H}">\n${out.trim()}\n</template>\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { buildFromSkin };
|
||||||
|
|
||||||
// @font-face for the brand display/body fonts, matched from the project's font dirs
|
// @font-face for the brand display/body fonts, matched from the project's font dirs
|
||||||
// (staged assets/fonts first, else capture/assets/fonts) by family-name prefix, with
|
// (staged assets/fonts first, else capture/assets/fonts) by family-name prefix, with
|
||||||
// weight parsed from the filename. Paths are relative to compositions/captions.html.
|
// weight parsed from the filename. Paths are relative to compositions/captions.html.
|
||||||
@@ -498,6 +487,7 @@ function buildCaptionsHtml(groups, total, W, H) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||||
const sub = process.argv[2];
|
const sub = process.argv[2];
|
||||||
if (sub === "build" || sub === undefined) runBuild(process.argv.slice(sub === "build" ? 3 : 2));
|
if (sub === "build" || sub === undefined) runBuild(process.argv.slice(sub === "build" ? 3 : 2));
|
||||||
else {
|
else {
|
||||||
@@ -506,3 +496,4 @@ else {
|
|||||||
);
|
);
|
||||||
process.exit(2);
|
process.exit(2);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { readdirSync, readFileSync } from "node:fs";
|
||||||
|
import test from "node:test";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { buildFromSkin } from "./captions.mjs";
|
||||||
|
|
||||||
|
const presetsDir = fileURLToPath(
|
||||||
|
new URL("../../hyperframes-creative/frame-presets/", import.meta.url),
|
||||||
|
);
|
||||||
|
const skins = readdirSync(presetsDir, { withFileTypes: true })
|
||||||
|
.filter((entry) => entry.isDirectory())
|
||||||
|
.map((entry) => ({
|
||||||
|
name: entry.name,
|
||||||
|
source: readFileSync(
|
||||||
|
new URL(
|
||||||
|
`../../hyperframes-creative/frame-presets/${entry.name}/caption-skin.html`,
|
||||||
|
import.meta.url,
|
||||||
|
),
|
||||||
|
"utf8",
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
.filter(({ source }) => source.includes(".caption-word.is-active"));
|
||||||
|
|
||||||
|
for (const canvas of ["#f7f3e8", "#111827"]) {
|
||||||
|
for (const skin of skins) {
|
||||||
|
test(`${skin.name} preserves word-state rules on ${canvas}`, () => {
|
||||||
|
const active = skin.source.match(/\.caption-word\.is-active\s*\{[^}]*\}/s)?.[0];
|
||||||
|
const spoken = skin.source.match(/\.caption-word\.is-spoken\s*\{[^}]*\}/s)?.[0];
|
||||||
|
assert.ok(active, "skin must define an active-word rule");
|
||||||
|
assert.ok(spoken, "skin must define a spoken-word rule");
|
||||||
|
|
||||||
|
const output = buildFromSkin(
|
||||||
|
skin.source,
|
||||||
|
[],
|
||||||
|
1,
|
||||||
|
1920,
|
||||||
|
1080,
|
||||||
|
`:root { --cap-canvas: ${canvas}; --cap-ink: #111111; --cap-accent: #ffcc00; }`,
|
||||||
|
(message) => {
|
||||||
|
throw new Error(message);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(output.includes(active));
|
||||||
|
assert.ok(output.includes(spoken));
|
||||||
|
assert.equal(output.match(/\.caption-word\.is-active\s*\{/g)?.length, 1);
|
||||||
|
assert.equal(output.match(/\.caption-word\.is-spoken\s*\{/g)?.length, 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,9 +34,10 @@
|
|||||||
|
|
||||||
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
import { dirname, join, resolve } from "node:path";
|
import { dirname, join, resolve } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
import { parseStoryboard } from "./lib/storyboard.mjs";
|
import { parseStoryboard } from "./lib/storyboard.mjs";
|
||||||
import { captionBand, parseFormat } from "./lib/dimensions.mjs";
|
import { captionBand, parseFormat } from "./lib/dimensions.mjs";
|
||||||
import { parseColors, parseFonts, semanticColors, lum } from "./lib/tokens.mjs";
|
import { parseColors, parseFonts, semanticColors } from "./lib/tokens.mjs";
|
||||||
|
|
||||||
const flag = (argv, name, def) => {
|
const flag = (argv, name, def) => {
|
||||||
const i = argv.indexOf(`--${name}`);
|
const i = argv.indexOf(`--${name}`);
|
||||||
@@ -276,23 +277,11 @@ function buildFromSkin(skin, groups, total, W, H, tokens, die, faces = "", fonts
|
|||||||
// overflow:hidden — nothing is clipped); zeroing it would need an airy line-height that
|
// overflow:hidden — nothing is clipped); zeroing it would need an airy line-height that
|
||||||
// balloons the pill, which is worse. Override only if a brand font genuinely clips.
|
// balloons the pill, which is worse. Override only if a brand font genuinely clips.
|
||||||
out += "\n<style>\n .caption-line { line-height: 1.1 !important; }\n</style>";
|
out += "\n<style>\n .caption-line { line-height: 1.1 !important; }\n</style>";
|
||||||
// dark-ground caption contrast (auto). The preset caption skins are tuned for a LIGHT
|
|
||||||
// pill (cream); on a dark brand ground the pill goes near-black, so the skin's faint
|
|
||||||
// upcoming-word mix and light highlight block turn unreadable. When the resolved caption
|
|
||||||
// canvas is dark, override the three word states: a brighter muted upcoming color + an
|
|
||||||
// on-brand ACCENT highlight block with light text. Light grounds are left untouched.
|
|
||||||
const capCanvas = (tokens.match(/--cap-canvas:\s*(#[0-9a-fA-F]{6})/) || [])[1];
|
|
||||||
if (capCanvas && (lum(capCanvas) ?? 255) < 90) {
|
|
||||||
out +=
|
|
||||||
"\n<style>\n" +
|
|
||||||
" .caption-word { color: color-mix(in srgb, var(--cap-ink) 64%, var(--cap-canvas)); }\n" +
|
|
||||||
" .caption-word.is-active { color: var(--cap-ink); background: var(--cap-accent); box-shadow: 0 0 0 0.06em var(--cap-accent); }\n" +
|
|
||||||
" .caption-word.is-spoken { color: var(--cap-ink); background: transparent; box-shadow: none; }\n" +
|
|
||||||
"</style>";
|
|
||||||
}
|
|
||||||
return `<template id="captions-template" data-composition-id="captions" data-width="${W}" data-height="${H}">\n${out.trim()}\n</template>\n`;
|
return `<template id="captions-template" data-composition-id="captions" data-width="${W}" data-height="${H}">\n${out.trim()}\n</template>\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { buildFromSkin };
|
||||||
|
|
||||||
// @font-face for the brand display/body fonts, matched from the project's font dirs
|
// @font-face for the brand display/body fonts, matched from the project's font dirs
|
||||||
// (staged assets/fonts first, else capture/assets/fonts) by family-name prefix, with
|
// (staged assets/fonts first, else capture/assets/fonts) by family-name prefix, with
|
||||||
// weight parsed from the filename. Paths are relative to compositions/captions.html.
|
// weight parsed from the filename. Paths are relative to compositions/captions.html.
|
||||||
@@ -498,6 +487,7 @@ function buildCaptionsHtml(groups, total, W, H) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||||
const sub = process.argv[2];
|
const sub = process.argv[2];
|
||||||
if (sub === "build" || sub === undefined) runBuild(process.argv.slice(sub === "build" ? 3 : 2));
|
if (sub === "build" || sub === undefined) runBuild(process.argv.slice(sub === "build" ? 3 : 2));
|
||||||
else {
|
else {
|
||||||
@@ -506,3 +496,4 @@ else {
|
|||||||
);
|
);
|
||||||
process.exit(2);
|
process.exit(2);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { readdirSync, readFileSync } from "node:fs";
|
||||||
|
import test from "node:test";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { buildFromSkin } from "./captions.mjs";
|
||||||
|
|
||||||
|
const presetsDir = fileURLToPath(
|
||||||
|
new URL("../../hyperframes-creative/frame-presets/", import.meta.url),
|
||||||
|
);
|
||||||
|
const skins = readdirSync(presetsDir, { withFileTypes: true })
|
||||||
|
.filter((entry) => entry.isDirectory())
|
||||||
|
.map((entry) => ({
|
||||||
|
name: entry.name,
|
||||||
|
source: readFileSync(
|
||||||
|
new URL(
|
||||||
|
`../../hyperframes-creative/frame-presets/${entry.name}/caption-skin.html`,
|
||||||
|
import.meta.url,
|
||||||
|
),
|
||||||
|
"utf8",
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
.filter(({ source }) => source.includes(".caption-word.is-active"));
|
||||||
|
|
||||||
|
for (const canvas of ["#f7f3e8", "#111827"]) {
|
||||||
|
for (const skin of skins) {
|
||||||
|
test(`${skin.name} preserves word-state rules on ${canvas}`, () => {
|
||||||
|
const active = skin.source.match(/\.caption-word\.is-active\s*\{[^}]*\}/s)?.[0];
|
||||||
|
const spoken = skin.source.match(/\.caption-word\.is-spoken\s*\{[^}]*\}/s)?.[0];
|
||||||
|
assert.ok(active, "skin must define an active-word rule");
|
||||||
|
assert.ok(spoken, "skin must define a spoken-word rule");
|
||||||
|
|
||||||
|
const output = buildFromSkin(
|
||||||
|
skin.source,
|
||||||
|
[],
|
||||||
|
1,
|
||||||
|
1920,
|
||||||
|
1080,
|
||||||
|
`:root { --cap-canvas: ${canvas}; --cap-ink: #111111; --cap-accent: #ffcc00; }`,
|
||||||
|
(message) => {
|
||||||
|
throw new Error(message);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(output.includes(active));
|
||||||
|
assert.ok(output.includes(spoken));
|
||||||
|
assert.equal(output.match(/\.caption-word\.is-active\s*\{/g)?.length, 1);
|
||||||
|
assert.equal(output.match(/\.caption-word\.is-spoken\s*\{/g)?.length, 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
- `roles:` — each candidate's role: `cutout` foreground / `background` full-bleed / supporting — plus the real media available (each `public/<basename> — description`; a **`[video]`** tag marks a `.mp4` motion source that cannot be mounted by this sub-composition worker).
|
- `roles:` — each candidate's role: `cutout` foreground / `background` full-bleed / supporting — plus the real media available (each `public/<basename> — description`; a **`[video]`** tag marks a `.mp4` motion source that cannot be mounted by this sub-composition worker).
|
||||||
- `sfx:` — the orchestrator's; you mount no audio.
|
- `sfx:` — the orchestrator's; you mount no audio.
|
||||||
- `frame.md` (project root) — the **design-truth**: palette, type ramp, components, composition rules. The LOOK. Pull every visual token from here.
|
- `frame.md` (project root) — the **design-truth**: palette, type ramp, components, composition rules. The LOOK. Pull every visual token from here.
|
||||||
|
- **Asset paths are project-root relative everywhere.** This includes HTML attributes and CSS `url(...)` values such as `@font-face src`: use `assets/...` (or the supplied `capture/assets/...` path), never `../` or `../../`. Stage shared fonts before dispatch so every parallel frame resolves the same local files; never fall back to a network `@import`.
|
||||||
- `RULES_DIR` — absolute path to this skill's local `../hyperframes-animation/rules/`. The **named motion verbs in the Scene lines** (and the moves the blueprint cites) resolve to rule recipes here: `RULES_DIR/<id>.md` is the mechanics for a motion. (A few rules link an optional runnable demo in the shared `../hyperframes-animation/examples/<id>.html` — open it only when a recipe is unclear.)
|
- `RULES_DIR` — absolute path to this skill's local `../hyperframes-animation/rules/`. The **named motion verbs in the Scene lines** (and the moves the blueprint cites) resolve to rule recipes here: `RULES_DIR/<id>.md` is the mechanics for a motion. (A few rules link an optional runnable demo in the shared `../hyperframes-animation/examples/<id>.html` — open it only when a recipe is unclear.)
|
||||||
- `../references/cut-catalog.md` — the **cut catalog** (zoom-through / inverse / cut-the-curve / waterfall). When a Scene seam is a within-scene swap, a scene-to-scene cut, or a text-to-text line change, build it INSIDE your composition per this catalog (Z-scale + blur + opacity, or per-word x-staggers). You never author the between-frame transition — story's `transition_in` + the injector own that.
|
- `../references/cut-catalog.md` — the **cut catalog** (zoom-through / inverse / cut-the-curve / waterfall). When a Scene seam is a within-scene swap, a scene-to-scene cut, or a text-to-text line change, build it INSIDE your composition per this catalog (Z-scale + blur + opacity, or per-word x-staggers). You never author the between-frame transition — story's `transition_in` + the injector own that.
|
||||||
- Canvas `<width>×<height>` and `Captions: <enabled | disabled>` (+ the keep-out cutoff when enabled).
|
- Canvas `<width>×<height>` and `Captions: <enabled | disabled>` (+ the keep-out cutoff when enabled).
|
||||||
@@ -57,6 +58,7 @@ Generic seek-safety + structure live in `hyperframes-core` (read it; not restate
|
|||||||
1. **Read** — `hyperframes-core`'s composition contract (the structural law), then `frame.md` (the look) and your `## Frame N` block (the shot sequence + `blueprint:` / `focal:` / `roles:` / assets). **Then read the blueprint template** `../hyperframes-animation/blueprints/<id>.md` (skip if `compose`) for the shot's shape and signature move, and **open the rule recipe `RULES_DIR/<id>.md` for every named motion** in the Scene lines (plus the shared `../hyperframes-animation/examples/<id>.html` when the recipe is unclear): you reproduce these mechanics, not improvise them. Internalize the self-check codes below before you write — most lethal is **template transport**: every `<style>` + `<script>` (including the gsap load) must live INSIDE `<template>`, because the runtime only clones template contents and the assembled-project `lint` / `check` gate can miss an unwired blank sub-composition.
|
1. **Read** — `hyperframes-core`'s composition contract (the structural law), then `frame.md` (the look) and your `## Frame N` block (the shot sequence + `blueprint:` / `focal:` / `roles:` / assets). **Then read the blueprint template** `../hyperframes-animation/blueprints/<id>.md` (skip if `compose`) for the shot's shape and signature move, and **open the rule recipe `RULES_DIR/<id>.md` for every named motion** in the Scene lines (plus the shared `../hyperframes-animation/examples/<id>.html` when the recipe is unclear): you reproduce these mechanics, not improvise them. Internalize the self-check codes below before you write — most lethal is **template transport**: every `<style>` + `<script>` (including the gsap load) must live INSIDE `<template>`, because the runtime only clones template contents and the assembled-project `lint` / `check` gate can miss an unwired blank sub-composition.
|
||||||
2. **Design** — turn the time-coded shot sequence into a timeline using `frame.md`'s components and type ramp: each Scene window becomes a phase revealed on its `voiceover` cue, each named motion built from the recipe you just read, the blueprint's signature move kept recognizable. Place the named assets, and find a visual idea that reinforces the beat, not a literal restyle of the words.
|
2. **Design** — turn the time-coded shot sequence into a timeline using `frame.md`'s components and type ramp: each Scene window becomes a phase revealed on its `voiceover` cue, each named motion built from the recipe you just read, the blueprint's signature move kept recognizable. Place the named assets, and find a visual idea that reinforces the beat, not a literal restyle of the words.
|
||||||
3. **Author** — write the full sub-composition to `compositions/frames/<frame_id>.html` (rewrite to iterate; last write wins). `<template>`-wrapped root carrying `data-composition-id="<frame_id>"` and styled via `#root` (not a class on that element — see the self-check below), exactly one `gsap.timeline({ paused: true })` registered at `window.__timelines["<frame_id>"]`, built synchronously — per the core contract.
|
3. **Author** — write the full sub-composition to `compositions/frames/<frame_id>.html` (rewrite to iterate; last write wins). `<template>`-wrapped root carrying `data-composition-id="<frame_id>"` and styled via `#root` (not a class on that element — see the self-check below), exactly one `gsap.timeline({ paused: true })` registered at `window.__timelines["<frame_id>"]`, built synchronously — per the core contract.
|
||||||
|
Prefix authored ids and globally reusable class names with `<frame_id>-` so sibling frames assembled from parallel workers cannot collide. Contract selectors such as `#root` and `.clip` are the only exceptions.
|
||||||
4. **Self-check, then finish** — re-read your file against the checklist below and fix in place. Writing the file is your terminal action; you do **not** run the CLI.
|
4. **Self-check, then finish** — re-read your file against the checklist below and fix in place. Writing the file is your terminal action; you do **not** run the CLI.
|
||||||
|
|
||||||
## Self-check before finishing (you do NOT run the CLI)
|
## Self-check before finishing (you do NOT run the CLI)
|
||||||
|
|||||||
Reference in New Issue
Block a user