mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
* chore(skills): package Codex plugin upload * chore(skills): harden Codex plugin content * fix(skills): satisfy plugin quality gates * fix(skills): address plugin packaging review * fix(plugin): simplify asset validation * fix(skills): correct embedded-captions catalog count to 35 after nightcity removal The nightcity theme removal left SKILL.md claiming 36 identities in four places, including the frontmatter description the router reads. The catalog now has 35 entries (10 classic + 25 themed). --------- Co-authored-by: Miao Yang <miao.yang@heygen.com>
225 lines
8.3 KiB
HTML
225 lines
8.3 KiB
HTML
<!--
|
|
Code editorial — caption skin (preset-local source).
|
|
|
|
This is the preset's own lower-third karaoke caption look. product-launch-video's
|
|
(and pr-to-video's) Step 2 copies it into the project as caption-skin.html;
|
|
captions.mjs then fills the three reserved holes, wraps the fragment in a
|
|
<template>, and writes compositions/captions.html:
|
|
· the empty GROUPS array → the engine's scene-aware word groups
|
|
· the 0 duration + 0 width/height → real total + canvas size
|
|
· the empty data-brand-tokens style → :root tokens derived from the project's frame.md
|
|
|
|
Token contract — reference ONLY this fixed vocab (captions.mjs injects it from
|
|
frame.md, so the Step-2 brand overlay flows through; each var has a code-editorial literal
|
|
fallback for standalone preview):
|
|
--cap-ink · --cap-canvas · --cap-accent · --cap-accent-2 · --font-display ·
|
|
--font-body · --cap-band-top · --cap-band-height
|
|
Hooks (do not rename): .caption-group / .caption-word + states .is-active / .is-spoken,
|
|
root data-composition-id="captions", timeline at window.__timelines["captions"].
|
|
State changes use gsap.set({className}) (applied on the engine's frame-by-frame seek);
|
|
never tl.call() callbacks (seek does not fire them → wrong state on render).
|
|
|
|
Visual: a warm cream card lifted off the page on a 1px hairline ink border + one soft
|
|
warm shadow — code-editorial's only elevation language, no tilt, editorial and precise.
|
|
EB Garamond sentence case, warm-ink glyphs. Upcoming words sit in faint ink; the current
|
|
word reads in full ink under a 3px coral "voltage" underline (the brand's inline-link
|
|
gesture, border-bottom = underline without layout shift); spoken words settle to solid
|
|
ink, underline cleared.
|
|
-->
|
|
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
|
|
|
<!-- captions.mjs injects the project's :root tokens here -->
|
|
<style data-brand-tokens></style>
|
|
|
|
<style>
|
|
#captions-root {
|
|
position: absolute;
|
|
inset: 0;
|
|
pointer-events: none;
|
|
}
|
|
.caption-layer {
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: 20;
|
|
pointer-events: none;
|
|
}
|
|
/* the keep-out band, from captionBand(H) — frame content stays above it */
|
|
.caption-stage {
|
|
position: absolute;
|
|
left: 0;
|
|
right: 0;
|
|
top: var(--cap-band-top, 900px);
|
|
height: var(--cap-band-height, 180px);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.caption-group {
|
|
/* every group is absolutely stacked over the SAME band — exactly one shown at a
|
|
time via opacity. Must be absolute: in-flow groups would lay out side-by-side. */
|
|
position: absolute;
|
|
inset: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
opacity: 0;
|
|
}
|
|
|
|
/* THE lifted cream card — warm cream fill, 1px hairline ink border, one soft warm
|
|
shadow (code-editorial's only elevation language), 12px radius, no tilt */
|
|
.caption-pill {
|
|
max-width: 78%;
|
|
padding: 22px 44px 26px;
|
|
background: var(--cap-canvas, #faf9f5);
|
|
border: 1px solid color-mix(in srgb, var(--cap-ink, #141413) 12%, transparent);
|
|
border-radius: 12px;
|
|
box-shadow:
|
|
0 1px 3px color-mix(in srgb, var(--cap-ink, #141413) 8%, transparent),
|
|
0 4px 16px color-mix(in srgb, var(--cap-ink, #141413) 4%, transparent);
|
|
}
|
|
.caption-line {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
gap: 0.12em 0.3em;
|
|
font-family: var(--font-display, "EB Garamond"), Georgia, serif;
|
|
font-weight: 500;
|
|
font-size: clamp(40px, 4.4vw, 56px);
|
|
line-height: 1.24;
|
|
letter-spacing: -0.02em;
|
|
}
|
|
|
|
/* upcoming — faint warm ink, no underline */
|
|
.caption-word {
|
|
display: inline-block;
|
|
padding: 0 0.02em 0.06em;
|
|
color: color-mix(in srgb, var(--cap-ink, #141413) 40%, var(--cap-canvas, #faf9f5));
|
|
border-bottom: 3px solid transparent;
|
|
}
|
|
/* current word — full ink under the coral voltage underline (the brand's signature
|
|
inline-link gesture; border-bottom = underline without layout shift) */
|
|
.caption-word.is-active {
|
|
color: var(--cap-ink, #141413);
|
|
border-bottom: 3px solid var(--cap-accent, #cc785c);
|
|
}
|
|
/* already spoken — solid warm ink, underline cleared */
|
|
.caption-word.is-spoken {
|
|
color: var(--cap-ink, #141413);
|
|
border-bottom: 3px solid transparent;
|
|
}
|
|
|
|
/* optional per-word accents (dormant until a word-classer tags them) */
|
|
/* brand / key term — the expressive EB Garamond italic */
|
|
.caption-word.cap-brand {
|
|
font-style: italic;
|
|
color: var(--cap-ink, #141413);
|
|
}
|
|
.caption-word.cap-brand.is-active {
|
|
border-bottom: 3px solid var(--cap-accent, #cc785c);
|
|
}
|
|
/* numerals / data — slightly recessed warm ink */
|
|
.caption-word.cap-num {
|
|
color: color-mix(in srgb, var(--cap-ink, #141413) 72%, var(--cap-canvas, #faf9f5));
|
|
}
|
|
/* emphasis — a persistent coral voltage underline */
|
|
.caption-word.cap-cta {
|
|
border-bottom: 3px solid var(--cap-accent-2, #b5654a);
|
|
color: var(--cap-ink, #141413);
|
|
}
|
|
|
|
@media (max-aspect-ratio: 9/16) {
|
|
.caption-pill {
|
|
max-width: 92%;
|
|
}
|
|
.caption-line {
|
|
font-size: clamp(34px, 5.4vw, 46px);
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<div
|
|
id="captions-root"
|
|
data-composition-id="captions"
|
|
data-timeline-locked
|
|
data-start="0"
|
|
data-duration="0"
|
|
data-fps="30"
|
|
data-width="0"
|
|
data-height="0"
|
|
>
|
|
<div class="caption-layer" aria-hidden="true">
|
|
<div id="caption-stage" class="caption-stage"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// captions.mjs injects the engine's scene-aware groups + real duration here.
|
|
var GROUPS = [];
|
|
var DURATION = 0;
|
|
|
|
(function () {
|
|
var stage = document.getElementById("caption-stage");
|
|
|
|
// build DOM: one .caption-group per group, .caption-word per word
|
|
GROUPS.forEach(function (group, g) {
|
|
var groupEl = document.createElement("div");
|
|
groupEl.className = "caption-group";
|
|
groupEl.id = "caption-group-" + g;
|
|
var pill = document.createElement("div");
|
|
pill.className = "caption-pill";
|
|
var line = document.createElement("div");
|
|
line.className = "caption-line";
|
|
(group.words || []).forEach(function (w, i) {
|
|
var span = document.createElement("span");
|
|
span.className = "caption-word";
|
|
span.id = "caption-word-" + g + "-" + i;
|
|
span.textContent = String(w.text);
|
|
line.appendChild(span);
|
|
});
|
|
pill.appendChild(line);
|
|
groupEl.appendChild(pill);
|
|
stage.appendChild(groupEl);
|
|
});
|
|
|
|
window.__timelines = window.__timelines || {};
|
|
var tl = gsap.timeline({ paused: true });
|
|
|
|
GROUPS.forEach(function (group, g) {
|
|
var groupEl = document.getElementById("caption-group-" + g);
|
|
var words = group.words || [];
|
|
var next = GROUPS[g + 1];
|
|
var isLast = g === GROUPS.length - 1;
|
|
var start = Math.max(0, Number(group.start));
|
|
var end = isLast ? DURATION : Math.min(Number(next.start), Number(group.end) + 0.3);
|
|
if (end <= start) end = start + 0.01;
|
|
|
|
// group on/off — exactly one group visible at a time
|
|
tl.set(groupEl, { opacity: 1 }, start);
|
|
tl.set(groupEl, { opacity: 0 }, end);
|
|
|
|
// 3-state karaoke via className SETS — seek-safe (gsap.set applies on the
|
|
// engine's frame-by-frame seek; .call callbacks would NOT fire).
|
|
words.forEach(function (w, i) {
|
|
var el = document.getElementById("caption-word-" + g + "-" + i);
|
|
var at = Math.max(start, Number(w.start));
|
|
tl.set(el, { className: "caption-word" }, start); // upcoming (also resets on reverse-seek)
|
|
tl.set(el, { className: "caption-word is-active" }, at); // current
|
|
tl.fromTo(el, { scale: 0.98 }, { scale: 1, duration: 0.16, ease: "power1.out" }, at);
|
|
if (i + 1 < words.length) {
|
|
var nextAt = Math.max(start, Number(words[i + 1].start));
|
|
tl.set(el, { className: "caption-word is-spoken" }, nextAt); // demote when next activates
|
|
}
|
|
});
|
|
if (words.length) {
|
|
var lastEl = document.getElementById("caption-word-" + g + "-" + (words.length - 1));
|
|
var lastSpoken = Math.min(end, Number(words[words.length - 1].end) + 0.1);
|
|
tl.set(lastEl, { className: "caption-word is-spoken" }, lastSpoken);
|
|
}
|
|
});
|
|
|
|
// full-span anchor so the sub-comp timeline spans the whole video
|
|
tl.to({}, { duration: DURATION }, 0);
|
|
window.__timelines["captions"] = tl;
|
|
})();
|
|
</script>
|