mirror of
https://github.com/hanzoai/frames.git
synced 2026-09-24 00:08:03 +00:00
Every SKILL.md already declared the new name — name: frames, frames-cli, frames-core, and so on — but the directories were never moved, so the tree carried frames-* in content and hyperframes-* on disk. Everything that resolves a skill by path missed: the core-skill list, the plugin marketplace pin, and the docs the CLI reads at runtime. Eight tests failed on main because the files they open do not exist. The directories now match what the files say they are, and the guide the claude-design contract reads takes the name that contract already used.
239 lines
8.7 KiB
HTML
239 lines
8.7 KiB
HTML
<!--
|
|
Cobalt Grid — caption skin (preset-local source).
|
|
|
|
This is the preset's own lower-third karaoke caption look. product-launch-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 cobalt 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 cream-paper card carrying the permanent graph-paper grid behind the line,
|
|
framed by top + bottom 1.5px cobalt hairlines (cobalt's signature frame furniture) —
|
|
square corners, NO drop shadow (cobalt's depth is structural and flat, never cast).
|
|
Newsreader serif 400, sentence case, cobalt glyphs. Upcoming words sit in faint
|
|
cobalt; the current word is a hard COBALT FLAT BLOCK — a risograph knockout: a solid
|
|
cobalt fill (box-shadow spread = a square block, no blur, no layout shift) with the
|
|
cream paper reading on top. Spoken words settle to solid cobalt ink, the block cleared.
|
|
Strictly two-color throughout — cream + cobalt, never a second hue.
|
|
-->
|
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/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 cream card — graph-paper grid behind the line + top/bottom 1.5px cobalt
|
|
hairlines (cobalt's frame furniture), square corners, NO drop shadow */
|
|
.caption-pill {
|
|
position: relative;
|
|
max-width: 78%;
|
|
padding: 20px 44px 24px;
|
|
background: var(--cap-canvas, #f0ebde);
|
|
background-image:
|
|
linear-gradient(
|
|
to right,
|
|
color-mix(in srgb, var(--cap-ink, #1f2be0) 10%, transparent) 1px,
|
|
transparent 1px
|
|
),
|
|
linear-gradient(
|
|
to bottom,
|
|
color-mix(in srgb, var(--cap-ink, #1f2be0) 10%, transparent) 1px,
|
|
transparent 1px
|
|
);
|
|
background-size: 16px 16px;
|
|
border-top: 1.5px solid var(--cap-ink, #1f2be0);
|
|
border-bottom: 1.5px solid var(--cap-ink, #1f2be0);
|
|
border-radius: 0;
|
|
}
|
|
.caption-line {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
gap: 0.1em 0.34em;
|
|
font-family: var(--font-display, "Newsreader"), Georgia, serif;
|
|
font-weight: 400;
|
|
font-size: clamp(40px, 4.4vw, 58px);
|
|
line-height: 1.16;
|
|
letter-spacing: -0.01em;
|
|
}
|
|
|
|
/* upcoming — faint cobalt, no block */
|
|
.caption-word {
|
|
display: inline-block;
|
|
padding: 0 0.06em;
|
|
color: color-mix(in srgb, var(--cap-ink, #1f2be0) 42%, var(--cap-canvas, #f0ebde));
|
|
}
|
|
/* current word — THE cobalt flat block (risograph knockout): a solid cobalt fill
|
|
via box-shadow spread = a hard square block w/o layout shift, no blur; the cream
|
|
paper reads on top */
|
|
.caption-word.is-active {
|
|
color: var(--cap-canvas, #f0ebde);
|
|
background: var(--cap-ink, #1f2be0);
|
|
box-shadow: 0 0 0 0.06em var(--cap-ink, #1f2be0);
|
|
}
|
|
/* already spoken — solid cobalt ink, block cleared */
|
|
.caption-word.is-spoken {
|
|
color: var(--cap-ink, #1f2be0);
|
|
background: transparent;
|
|
box-shadow: none;
|
|
}
|
|
|
|
/* optional per-word accents (dormant until a word-classer tags them) */
|
|
/* brand / key term — Newsreader italic, cobalt's only display inflection */
|
|
.caption-word.cap-brand {
|
|
font-style: italic;
|
|
}
|
|
/* numerals / data — the body sans voice, slightly recessed */
|
|
.caption-word.cap-num {
|
|
font-family: var(--font-body, "Hanken Grotesk"), ui-monospace, monospace;
|
|
color: color-mix(in srgb, var(--cap-ink, #1f2be0) 72%, var(--cap-canvas, #f0ebde));
|
|
}
|
|
/* emphasis — a cobalt hairline underline (no second hue; emphasis is the ink) */
|
|
.caption-word.cap-cta {
|
|
text-decoration: underline;
|
|
text-decoration-thickness: 2px;
|
|
text-decoration-color: var(--cap-accent, #5560e5);
|
|
text-underline-offset: 0.1em;
|
|
}
|
|
|
|
@media (max-aspect-ratio: 9/16) {
|
|
.caption-pill {
|
|
max-width: 92%;
|
|
}
|
|
.caption-line {
|
|
font-size: clamp(34px, 5.4vw, 48px);
|
|
}
|
|
}
|
|
</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.985 }, { scale: 1, duration: 0.1, ease: "power3.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>
|