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.
231 lines
8.4 KiB
HTML
231 lines
8.4 KiB
HTML
<!--
|
|
Daisy Days — 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 daisy-days 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 cheerful picture-book card — a cream surface inside a 3px charcoal outline
|
|
with a hard 6px charcoal offset shadow (no blur) and a generous 24px radius, the
|
|
sticker-on-paper signature. Fredoka One glyphs, sentence case, charcoal ink. Upcoming
|
|
words sit in muted charcoal; the current word pops into a rounded sunny-pastel candy
|
|
block (coral fill, 3px charcoal outline, hard 3px offset shadow, 12px radius) with a
|
|
bouncy scale pop; spoken words settle to solid charcoal, the candy block cleared.
|
|
-->
|
|
<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 picture-book card — cream surface, 3px charcoal outline, generous 24px radius,
|
|
a hard 6px charcoal offset shadow (no blur): the sticker-on-paper signature */
|
|
.caption-pill {
|
|
max-width: 80%;
|
|
padding: 18px 40px 22px;
|
|
background: var(--cap-canvas, #f5f0e6);
|
|
border: 3px solid var(--cap-ink, #2d2d2d);
|
|
border-radius: 24px;
|
|
box-shadow: 6px 6px 0 var(--cap-ink, #2d2d2d);
|
|
}
|
|
.caption-line {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
gap: 0.18em 0.32em;
|
|
font-family: var(--font-display, "Fredoka One"), "Baloo 2", sans-serif;
|
|
font-weight: 600;
|
|
font-size: clamp(38px, 4.2vw, 56px);
|
|
line-height: 1.22;
|
|
letter-spacing: 0.02em;
|
|
}
|
|
|
|
/* upcoming — muted charcoal, no candy block */
|
|
.caption-word {
|
|
display: inline-block;
|
|
padding: 0.02em 0.16em;
|
|
border-radius: 12px;
|
|
color: color-mix(in srgb, var(--cap-ink, #2d2d2d) 52%, var(--cap-canvas, #f5f0e6));
|
|
}
|
|
/* current word — THE sunny-pastel candy block: a rounded coral chip with a 3px
|
|
charcoal outline + a hard 3px charcoal offset shadow (no blur), charcoal on top */
|
|
.caption-word.is-active {
|
|
color: var(--cap-ink, #2d2d2d);
|
|
background: var(--cap-accent, #f8635f);
|
|
border-radius: 12px;
|
|
box-shadow:
|
|
0 0 0 3px var(--cap-ink, #2d2d2d),
|
|
3px 5px 0 0 var(--cap-ink, #2d2d2d);
|
|
}
|
|
/* already spoken — solid charcoal, candy block cleared */
|
|
.caption-word.is-spoken {
|
|
color: var(--cap-ink, #2d2d2d);
|
|
background: transparent;
|
|
box-shadow: none;
|
|
}
|
|
|
|
/* optional per-word accents (dormant until a word-classer tags them) */
|
|
/* brand / key term — a butter candy chip, the cheerful second pastel */
|
|
.caption-word.cap-brand {
|
|
color: var(--cap-ink, #2d2d2d);
|
|
background: var(--cap-accent-2, #fde68a);
|
|
border-radius: 12px;
|
|
box-shadow: 0 0 0 3px var(--cap-ink, #2d2d2d);
|
|
}
|
|
/* numerals / data — Quicksand, slightly recessed */
|
|
.caption-word.cap-num {
|
|
font-family: var(--font-body, "Quicksand"), system-ui, sans-serif;
|
|
font-weight: 700;
|
|
color: color-mix(in srgb, var(--cap-ink, #2d2d2d) 78%, var(--cap-canvas, #f5f0e6));
|
|
}
|
|
/* emphasis — a butter underline rule (the second sunny pastel) */
|
|
.caption-word.cap-cta {
|
|
text-decoration: underline;
|
|
text-decoration-thickness: 3px;
|
|
text-decoration-color: var(--cap-accent-2, #fde68a);
|
|
text-underline-offset: 0.12em;
|
|
}
|
|
|
|
@media (max-aspect-ratio: 9/16) {
|
|
.caption-pill {
|
|
max-width: 92%;
|
|
}
|
|
.caption-line {
|
|
font-size: clamp(32px, 5.2vw, 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.82 }, { scale: 1, duration: 0.2, ease: "back.out(2.6)" }, 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>
|