chore: import upstream snapshot with attribution
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
---
|
||||
name: caption-template-anatomy
|
||||
description: The shared, reproducible scene engine every caption template is built on — a matted talking-head with a flowing verbatim foreground caption and a single climax word, driven by one paused GSAP timeline. Read it ONCE per session (it is identical for all 54 templates); each per-template file in templates/ only overrides the style tokens + the named climax entrance/exit.
|
||||
metadata:
|
||||
tags: caption, talking-head, matte, occlusion, verbatim, climax, gsap, hyperframes
|
||||
---
|
||||
|
||||
# Caption Template — Anatomy (the shared engine)
|
||||
|
||||
A **caption template** = one complete, reproducible HyperFrames scene:
|
||||
|
||||
> **the person (always in frame)** + a **flowing foreground caption** (verbatim, word-by-word, with appear **and** disappear) + a **climax word** (big, behind the speaker, with a designed entrance **and** exit) + a coherent **font / colour / motion** design.
|
||||
|
||||
Every file in `templates/` is the SAME engine described here with three things swapped: (1) the **style tokens** (font, fills, accent, optional gradient/stroke), (2) the named **climax entrance** + **exit** (see `_motion.md`), (3) the **copy** (flow lines + climax word) and **scene/person**. So read this once; each template file is short.
|
||||
|
||||
HyperFrames-native, so anyone can reproduce it:
|
||||
|
||||
- **One paused GSAP timeline per composition**, registered to `window.__timelines[data-composition-id]`.
|
||||
- All timing in **seconds**; `data-start` / `data-duration` carry the scene window.
|
||||
- **Deterministic + seek-safe** only — no `Math.random()`, no `Date.now()`, no infinite repeats, no un-seekable CSS animations. Every state is reachable by seeking the timeline to a time `t`.
|
||||
- GSAP transform aliases (`x`, `y`, `scale`, `rotation`); animate `opacity`, `filter`, `clipPath`, `textShadow`, `backgroundPosition`, `letterSpacing` — never layout props (width/top/left/margin).
|
||||
|
||||
For the composition contract see `hyperframes-core`; eases + the animated-property allowlist see `hyperframes-gsap`; caption grouping/positioning/exit guarantees see `hyperframes-captions`.
|
||||
|
||||
## 1 · Asset prep (two CLI calls)
|
||||
|
||||
```bash
|
||||
# 1) The person — transparent talking-head cutout over the scene (VP9 + alpha)
|
||||
bash scripts/prepare.sh <project> # matte ∥ transcribe ∥ safe-zones (THIS skill — not remove-background)
|
||||
# (a still works too: remove-background portrait.jpg -o person.png)
|
||||
|
||||
# 2) The verbatim word timings that drive the flowing caption
|
||||
npx hyperframes transcribe subject.mp4 --model small # → transcript.json
|
||||
# shape: [{ "id":"w0","text":"Hello","start":0.0,"end":0.5 }, …]
|
||||
```
|
||||
|
||||
The flow caption consumes that `transcript.json` directly (word `start`/`end` → reveal + active-word emphasis). The climax word is authored by hand (it is the headline beat, not part of the spoken transcript). In production the avatar/matte pipeline yields a pixel-perfect alpha for free — `remove-background` is the fallback for arbitrary footage.
|
||||
|
||||
## 2 · The matte sandwich (HTML)
|
||||
|
||||
Six layers, back-to-front. The person is layered ON TOP of the climax so they physically **occlude** it — this is what sells "behind the speaker" (blur/opacity alone reads as a flat overlay).
|
||||
|
||||
```html
|
||||
<div
|
||||
class="stage {STYLE} {PERSON}"
|
||||
id="cap-{id}"
|
||||
data-composition-id="cap-{id}"
|
||||
data-start="0"
|
||||
data-duration="{SCENE_DUR}"
|
||||
data-track-index="0"
|
||||
>
|
||||
<!-- z0 background plate (the original frame, full) -->
|
||||
<!-- supplied by .{PERSON} as background-image, or a <video> at z-index:0 -->
|
||||
|
||||
<!-- z1 CLIMAX — big, BEHIND the person, occluded -->
|
||||
<div class="climax"><span>{CLIMAX_WORD}</span></div>
|
||||
|
||||
<!-- z4 the person cutout (transparent webm/png), aligned to the plate -->
|
||||
<video class="cut" src="person.webm" muted playsinline></video>
|
||||
<!-- or: <img class="cut" src="person.png"> -->
|
||||
|
||||
<!-- z5 grade / vignette for depth + legibility -->
|
||||
<div class="grade"></div>
|
||||
|
||||
<!-- z6 FLOW — the verbatim caption, IN FRONT, lower third -->
|
||||
<div class="flow"></div>
|
||||
<!-- words injected from transcript.json -->
|
||||
</div>
|
||||
```
|
||||
|
||||
## 3 · Base CSS (layout · z-order · sizing)
|
||||
|
||||
Caption size is in **`cqh`** (% of frame height) via a size container, so it honours broadcast spec at any resolution (~8% cap-height for a 16:9 word). The per-template file only sets the `--ff` / `--cfill` / `--cacc` tokens and any `.climax span` fill (gradient/stroke).
|
||||
|
||||
```css
|
||||
.stage {
|
||||
position: relative;
|
||||
aspect-ratio: 16/9;
|
||||
overflow: hidden;
|
||||
container-type: size;
|
||||
background-size: cover;
|
||||
background-position: center 12%;
|
||||
background-color: #0a0a0e;
|
||||
font-family: var(--ff);
|
||||
}
|
||||
.stage > * {
|
||||
position: absolute;
|
||||
}
|
||||
.cut {
|
||||
z-index: 4;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center 12%;
|
||||
pointer-events: none;
|
||||
}
|
||||
.grade {
|
||||
z-index: 5;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background: radial-gradient(130% 100% at 50% 26%, transparent 40%, rgba(0, 0, 0, 0.6));
|
||||
}
|
||||
|
||||
/* CLIMAX — big, behind person (z1), occluded. line-height ≥1.15 so clip-reveal
|
||||
entrances (inset(0)) never slice glyph tops; pad clips negative for script faces. */
|
||||
.climax {
|
||||
z-index: 1;
|
||||
left: 50%;
|
||||
top: 37%;
|
||||
transform: translate(-50%, -50%);
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
line-height: 1.18;
|
||||
font-family: var(--ff);
|
||||
color: var(--cfill);
|
||||
font-weight: 900;
|
||||
font-size: 44cqh;
|
||||
text-transform: uppercase;
|
||||
text-shadow:
|
||||
0 2px 13px rgba(0, 0, 0, 0.6),
|
||||
0 0 48px rgba(0, 0, 0, 0.42);
|
||||
}
|
||||
.climax span {
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
} /* GSAP reveals it */
|
||||
|
||||
/* FLOW — verbatim caption, in front (z6), lower third */
|
||||
.flow {
|
||||
z-index: 6;
|
||||
left: 50%;
|
||||
bottom: 9%;
|
||||
transform: translateX(-50%);
|
||||
width: 90%;
|
||||
text-align: center;
|
||||
line-height: 1.15;
|
||||
font-family: var(--ff);
|
||||
font-weight: 700;
|
||||
font-size: 7.5cqh;
|
||||
color: var(--cfill);
|
||||
}
|
||||
.flow .w {
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
margin: 0 0.1em;
|
||||
color: var(--cfill);
|
||||
}
|
||||
.flow .w.act {
|
||||
color: var(--cacc);
|
||||
} /* the currently-spoken word */
|
||||
|
||||
/* tokens every template overrides: */
|
||||
.stage {
|
||||
--ff: "Inter";
|
||||
--cfill: #fff;
|
||||
--cacc: #10a37f;
|
||||
}
|
||||
```
|
||||
|
||||
**Legibility on busy/bright scenes:** a behind-the-person climax needs separation from the footage, not just a fill colour. For dark or gradient fills on lit scenes give the climax an outline — `-webkit-text-stroke:1px rgba(0,0,0,.5);paint-order:stroke fill` — a dark drop-shadow alone fails against highlights (e.g. a lamp). Gradient/clip fills must live on `.climax span` (the text node), never on the transformed `.climax` container, or the clip detaches and only the shadow shows.
|
||||
|
||||
## 4 · One paused GSAP timeline (the loop, made seek-safe)
|
||||
|
||||
The gallery used a `setInterval` loop; HyperFrames needs the same beats as **absolute-time tweens on one paused timeline**. The cycle is **FLOW line → (FLOW line) → CLIMAX in → hold → out**. Restraint is the rule: flow stays clean; the one big mood move happens only at the climax.
|
||||
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const stage = document.getElementById("cap-{id}");
|
||||
const flow = stage.querySelector(".flow");
|
||||
const climax = stage.querySelector(".climax span");
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// --- FLOW: render words from transcript.json, reveal each at its start time ---
|
||||
// WORDS = grouped transcript lines: [{ words:[{text,start,end}], end }] in scene-local seconds.
|
||||
function renderLine(line) {
|
||||
flow.innerHTML = line.words
|
||||
.map((w, i) => `<span class="w" data-i="${i}">${w.text}</span>`)
|
||||
.join(" ");
|
||||
return [...flow.querySelectorAll(".w")];
|
||||
}
|
||||
WORDS.forEach((line) => {
|
||||
const spans = renderLine(line); // (one renderLine per active line; see captions skill for multi-line groups)
|
||||
spans.forEach((el, i) => {
|
||||
const w = line.words[i];
|
||||
tl.add(FLOW_IN(el), w.start); // FLOW_IN = the template's flow entrance (see _motion.md)
|
||||
tl.set(spans, { className: "w" }, w.start); // active-word sweep: only the spoken word gets .act
|
||||
tl.set(el, { className: "w act" }, w.start);
|
||||
});
|
||||
tl.add(FLOW_OUT(spans), line.end); // FLOW_OUT = flow exit
|
||||
tl.set(flow, { autoAlpha: 0 }, line.end + FOUT); // hard-hide the group so old text can't linger
|
||||
tl.set(flow, { autoAlpha: 1 }, line.end + FOUT + 0.001);
|
||||
});
|
||||
|
||||
// --- CLIMAX: entrance at the beat → hold ≥1s → exit (named recipes from _motion.md) ---
|
||||
const T = CLIMAX_AT; // beat time (after the flow lines)
|
||||
tl.add(CLIMAX_IN(climax), T); // e.g. SLAM / DEBLUR / INK-LOOM / CHROME-SWEEP …
|
||||
tl.add(CLIMAX_OUT(climax), T + CLIMAX_HOLD); // hold ≥1s, then exit. CLIMAX_OUT ends opacity:0 (hard exit)
|
||||
|
||||
window.__timelines["cap-{id}"] = tl;
|
||||
</script>
|
||||
```
|
||||
|
||||
`FLOW_IN` / `FLOW_OUT` / `CLIMAX_IN` / `CLIMAX_OUT` are the named recipes in **`_motion.md`** — each returns a GSAP tween/timeline so the per-template file just picks four names. A simpler equivalent for the flow active-word _glow_ (rather than discrete reveal) is the single-driver envelope in `hyperframes-animation/rules/asr-keyword-glow.md`.
|
||||
|
||||
## 5 · How to choose values
|
||||
|
||||
- **SCENE_DUR** — must equal `data-duration`. Typical 6–10 s for a looping demo card.
|
||||
- **WORDS grouping** — 2–4 words / line, ~380–520 ms per word (premium pacing is slower than Hormozi). Group from `transcript.json`; keep `end < next.start` (monotonic).
|
||||
- **CLIMAX_AT** — place the climax _after_ the flow lines clear, on the narration's emphasis beat.
|
||||
- **CLIMAX_HOLD** — **≥1 s** of settled dwell _after_ the entrance finishes (the climax is the headline beat). Entrances run 0.6–1.6 s, so e.g. hold = entranceDur + 1.0–1.6.
|
||||
- **FOUT** — flow exit ≈ 0.5 s. **Exit ≈ 75 % of entry** for every element (arrival deliberate, departure swift; see `_motion.md`).
|
||||
- **Climax size** — base 44 cqh; long words bleed off-frame (intended cinematic); 3-char words behind a centred subject need size + an outline so they peek.
|
||||
|
||||
## Critical constraints (HyperFrames)
|
||||
|
||||
- Timeline **paused**; registry key = `data-composition-id`.
|
||||
- **No CSS keyframe animation** on caption elements — all motion is GSAP tweens at absolute times (seek-safe).
|
||||
- No `Math.random` / `Date.now` / infinite repeats.
|
||||
- `display:inline-block` on every `.w` and the climax `span`.
|
||||
- **Hard-hide** each flow group at its end time; **CLIMAX_OUT ends at `opacity:0`** (or fully-clipped) so nothing lingers.
|
||||
- Gradient / `background-clip:text` / stroke fills go on `.climax span`, not the transformed `.climax`.
|
||||
- `.climax` `line-height ≥ 1.15`; pad clip-reveal entrances with negative insets for script/decorative fonts.
|
||||
|
||||
## Pairs with HF skills
|
||||
|
||||
- `media-use` — `remove-background` (the matte) + `transcribe` (word timings).
|
||||
- `hyperframes-captions` — transcript consumption, grouping, positioning, exit guarantees, `fitTextFontSize`.
|
||||
- `hyperframes-animation/rules/asr-keyword-glow.md` — the verbatim active-word envelope.
|
||||
- `hyperframes-gsap` — single paused timeline, transform aliases, ease palette.
|
||||
- `_motion.md` (this folder) — the named flow/climax entrance + exit recipes.
|
||||
@@ -0,0 +1,192 @@
|
||||
---
|
||||
name: caption-template-motion
|
||||
description: The named flow + climax entrance/exit recipes (GSAP, seek-safe) that the per-template files reference, plus the motion numerics (ease palette, exit=75% entry, climax dwell, restraint, mood→motion). Every template picks a FLOW_IN/OUT and a CLIMAX_IN/OUT from here — look up only the 2-3 recipes a template names (grep the `### name`), this is a catalog, not a read-through.
|
||||
metadata:
|
||||
tags: gsap, easing, entrance, exit, timing, restraint, mood-matching, captions
|
||||
---
|
||||
|
||||
# Caption Template — Motion Language
|
||||
|
||||
`_anatomy.md` wires four hooks onto one paused timeline: `FLOW_IN`, `FLOW_OUT`, `CLIMAX_IN`, `CLIMAX_OUT`. This file is the catalog each template picks from. Each recipe is a GSAP tween on the climax `span` (or a flow `.w`); placed at an absolute time it is fully **seek-safe**. Multi-stage looks use GSAP `keyframes:{}` (still seek-safe) — never CSS keyframes.
|
||||
|
||||
## Numerics (shared)
|
||||
|
||||
**Ease palette** (sanctioned — reads clean on video, stays distinct):
|
||||
|
||||
| Curve | GSAP | Use |
|
||||
| --------------- | --------------------------------------- | --------------------------------------------------- |
|
||||
| overshoot entry | `back.out(1.4–1.7)` | confident arrivals (premium/epic/creator) |
|
||||
| heavy/refined | `power3.out` | hero/secondary entrances, slides |
|
||||
| general | `power2.out` | fades, scales |
|
||||
| exit accel | `power2.in` | the _exit_ side of any move (departure accelerates) |
|
||||
| snappy | `expo.out` | warp/fly/sonic confident snaps |
|
||||
| breathing | `sine.inOut` | gentle wellness drift |
|
||||
| impact thud | `back.out(1.5–2)` or a quick `expo.out` | slam/stomp/stamp landings |
|
||||
| linear driver | `none` | the asr active-word envelope driver only |
|
||||
| digital stutter | `steps(n)` | glitch/vhs/blink/possess/cut |
|
||||
|
||||
**Forbidden:** `bounce.out`, `elastic.out` everywhere **except** the Playful cluster (explicit playfulness — KIDS drop-bounce, CANDY jelly). Elsewhere real objects decelerate; they don't bounce. Use low `back.out` for sanctioned overshoot.
|
||||
|
||||
**Durations** — flow word enter 0.30–0.50 s · climax entrance 0.6–1.6 s · **exit ≈ 75 % of entry** (arrival deliberate, departure swift) · stagger 50–150 ms / word.
|
||||
|
||||
**Restraint (the rule that makes it premium):** the FLOW stays clean — a tasteful reveal + the active-word accent, no effects. The big mood move + any scene effect (flash, shockwave, shake) happens **only at the CLIMAX**, then clears. Never sprinkle effects across the flow.
|
||||
|
||||
**Climax dwell ≥ 1 s** after the entrance finishes — the climax is the headline beat.
|
||||
|
||||
**Mood → motion** (pick an entrance whose physics match the theme):
|
||||
|
||||
| Mood | Entrances |
|
||||
| ------------------------------- | ---------------------------------------------------- |
|
||||
| premium | deblur · rise · expose · breathe · flip |
|
||||
| epic | slam · monument · fly · grandrise |
|
||||
| cyber | glitch · type · scan · boot |
|
||||
| horror | loom · possess · glimpse · seep (never bouncy/clean) |
|
||||
| luxury | tracking-expand · hairrise |
|
||||
| retro | vhs · stamp · blink · type |
|
||||
| neon | ignite · buzz |
|
||||
| hype | stomp · punch · slap |
|
||||
| playful | jelly · dropb · scrawl · popr |
|
||||
| creator | boxpop · slideup · flip · shimmer |
|
||||
| ultra (flash in the type) | volt · hyper · liquid · prism · shatter · extrude |
|
||||
| atelier (design) | editwipe · rise · block · vert · ink · weight |
|
||||
| impact (scene effect at climax) | nuke · meteor · sonic · seismic · judge |
|
||||
|
||||
---
|
||||
|
||||
## FLOW entrances / exits (per word `.w`)
|
||||
|
||||
The flow caption reveals word-by-word from the transcript. The active word gets `.act` (→ `--cacc`). Exit hard-hides the group.
|
||||
|
||||
- **fade-up** (premium/atelier default) — `fromTo(w,{opacity:0,y:14},{opacity:1,y:0,duration:.42,ease:'power3.out'})`
|
||||
- **pop** (epic/hype) — `fromTo(w,{opacity:0,scale:.5},{opacity:1,scale:1,duration:.34,ease:'back.out(1.6)'})`
|
||||
- **whip** (kinetic) — `fromTo(w,{opacity:0,x:-30,filter:'blur(10px)'},{opacity:1,x:0,filter:'blur(0)',duration:.32,ease:'expo.out'})`
|
||||
- **glitch** (cyber) — `fromTo(w,{opacity:0,clipPath:'inset(45% 0 45% 0)'},{opacity:1,clipPath:'inset(0)',duration:.4,ease:'steps(6)'})`
|
||||
- **type** (terminal/retro) — `fromTo(w,{opacity:0},{opacity:1,duration:.2,ease:'steps(3)'})` staggered as the cadence
|
||||
- **blur-in** (horror) — `fromTo(w,{opacity:0,filter:'blur(10px)',y:'.1em'},{opacity:1,filter:'blur(0)',y:0,duration:.6,ease:'power2.out'})`
|
||||
- **karaoke** (creator) — words start visible-dim; on `w.start` `set(w,{className:'w act'})` (active → `--cacc`), prior word back to spoken. The signature verbatim mechanic.
|
||||
- **flow exits** — fade-up-out `to{opacity:0,y:-10}` · horror smear `to{opacity:0,x:6,skewX:10,filter:'blur(5px)'}` · all ≈0.4–0.55 s.
|
||||
|
||||
---
|
||||
|
||||
## CLIMAX entrances
|
||||
|
||||
Each is a `gsap.fromTo(span, {from}, {to, duration, ease})`. Where a `keyframes:{}` is shown, pass it as the 2nd arg's `keyframes`.
|
||||
|
||||
### premium
|
||||
|
||||
- **deblur** — `{opacity:0,scale:.96,filter:'blur(8px)'}→{opacity:1,scale:1,filter:'blur(0)',duration:.8,ease:'power3.out'}`
|
||||
- **rise** — `{opacity:0,yPercent:48}→{opacity:1,yPercent:0,duration:.9,ease:'power3.out'}`
|
||||
- **expose** — `{opacity:0,filter:'brightness(3.2) blur(5px)',scale:1.05}→{opacity:1,filter:'brightness(1) blur(0)',scale:1,duration:1,ease:'power2.out'}`
|
||||
- **breathe** — `{opacity:0,scale:1.09,filter:'blur(8px)'}→{opacity:1,scale:1,filter:'blur(0)',duration:1.2,ease:'sine.out'}`
|
||||
- **flip** — `{opacity:0,rotationX:93,transformPerspective:720}→{opacity:1,rotationX:0,duration:.9,ease:'power3.out'}`
|
||||
|
||||
### epic
|
||||
|
||||
- **slam** — `{opacity:0,scale:1.6,filter:'blur(12px)'}→{opacity:1,scale:1,filter:'blur(0)',duration:.7,ease:'back.out(1.6)'}` (lands with a thud; overshoot ~1.03 mid)
|
||||
- **monument** — `{opacity:0,scale:1.42,letterSpacing:'.12em'}→{opacity:1,scale:1,letterSpacing:'0',duration:1.3,ease:'power3.out'}`
|
||||
- **fly** — `{opacity:0,scale:.3,filter:'blur(14px)'}→{opacity:1,scale:1,filter:'blur(0)',duration:.8,ease:'expo.out'}`
|
||||
- **grandrise** — `{opacity:0,yPercent:66,scale:1.05}→{opacity:1,yPercent:0,scale:1,duration:1.15,ease:'power3.out'}`
|
||||
|
||||
### cyber
|
||||
|
||||
- **glitch** — `{opacity:0}→{opacity:1,duration:.6,ease:'steps(8)',keyframes:{clipPath:['inset(40% 0 40% 0)','inset(0)','inset(60% 0 10% 0)','inset(0)'],x:[-8,5,-2,0],textShadow:['4px 0 #ff003c,-4px 0 #00ffd1','none']}}`
|
||||
- **type** — `{opacity:1,clipPath:'inset(0 100% 0 0)'}→{clipPath:'inset(0 0 0 0)',duration:.85,ease:'steps(13)'}` (typewriter L→R)
|
||||
- **scan** — `{opacity:0,scaleY:.02,filter:'brightness(2.4)'}→{opacity:1,scaleY:1,filter:'brightness(1)',duration:.75,ease:'power3.out'}`
|
||||
- **boot** — `{opacity:0,scaleY:.1}→{opacity:1,scaleY:1,duration:1,ease:'steps(1)',keyframes:{opacity:[0,.6,.1,.85,.2,1]}}` (hologram power-up flicker)
|
||||
|
||||
### horror (never bouncy, never clean)
|
||||
|
||||
- **loom** — `{opacity:0,scale:.6,filter:'blur(17px)'}→{opacity:1,scale:1,filter:'blur(0)',duration:1.6,ease:'power2.in',keyframes:{scale:[.6,.93,1.07,1]}}` (slowly approaches out of the dark, then a lurch closer)
|
||||
- **possess** — `{opacity:0}→{opacity:1,duration:1.15,ease:'steps(1)',keyframes:{x:[0,-5,6,-4,5,-2,0],skewX:[0,9,-10,7,-5,3,0],textShadow:['none','4px 0 #c00,-4px 0 #0aa','-6px 0 #c00,6px 0 #0aa','none']}}` (materialise then violent demonic shudder)
|
||||
- **glimpse** — `{opacity:0}→{opacity:1,duration:1.35,ease:'steps(1)',keyframes:{opacity:[0,1,0,0,1,0,0,1,.05,1,.15,1],x:[0,0,0,3,0,-2,0]}}` (subliminal failing-light flicker)
|
||||
- **seep** — `{opacity:0,clipPath:'inset(-15% -8% 100% -8%)',filter:'blur(4px)'}→{opacity:1,clipPath:'inset(-15% -8% -15% -8%)',filter:'blur(0)',duration:1.35,ease:'power1.inOut'}` (blood seeps down into the word; negative insets so script tops aren't clipped)
|
||||
|
||||
### luxury
|
||||
|
||||
- **tracking** — `{opacity:0,letterSpacing:'-.05em'}→{opacity:1,letterSpacing:'.28em',duration:1.4,ease:'power3.out'}` (the most "expensive" move)
|
||||
- **hairrise** — `{opacity:0,yPercent:32,letterSpacing:'.07em'}→{opacity:1,yPercent:0,letterSpacing:'0',duration:1.1,ease:'power3.out'}`
|
||||
|
||||
### retro
|
||||
|
||||
- **vhs** — `{opacity:0}→{opacity:1,duration:.8,ease:'steps(1)',keyframes:{x:[0,-7,6,-3,2,0],textShadow:['none','3px 0 #ff3b5c,-3px 0 #3aa0ff','none']}}` (tracking-lock jitter + chroma)
|
||||
- **stamp** — `{opacity:0,scale:1.7,rotation:-5}→{opacity:1,scale:1,rotation:0,duration:.7,ease:'back.out(2)'}` (printing-stamp thud)
|
||||
- **blink** — `{opacity:0}→{opacity:1,duration:.75,ease:'steps(1)',keyframes:{opacity:[0,0,1,0,1,0,1,1]}}` (8-bit blink-in)
|
||||
|
||||
### neon
|
||||
|
||||
- **ignite** — `{opacity:0}→{opacity:1,duration:1.3,ease:'steps(1)',keyframes:{opacity:[.2,1,.2,1,.3,1,1]}}` (tube strikes + buzzes alight; glow is a style `text-shadow`)
|
||||
- **buzz** — `{opacity:0}→{opacity:1,duration:1.2,ease:'steps(1)',keyframes:{opacity:[0,.7,.1,.85,.2,1,.5,1]}}` (slower warm-up rhythm)
|
||||
|
||||
### hype
|
||||
|
||||
- **stomp** — `{opacity:0,scale:1.5,filter:'blur(8px)'}→{opacity:1,scale:1,filter:'blur(0)',duration:.9,ease:'back.out(1.6)'}`
|
||||
- **punch** — `{opacity:0,xPercent:-55,scale:1.1,filter:'blur(9px)'}→{opacity:1,xPercent:0,scale:1,filter:'blur(0)',duration:.6,ease:'expo.out'}` (whip from the side)
|
||||
- **slap** — `{opacity:0,rotation:12,scale:1.5,filter:'blur(7px)'}→{opacity:1,rotation:0,scale:1,filter:'blur(0)',duration:.7,ease:'back.out(2)'}` (sticker/tag slap)
|
||||
|
||||
### playful (bounce allowed here)
|
||||
|
||||
- **jelly** — `{opacity:0,scale:.5}→{opacity:1,scale:1,duration:.8,ease:'back.out(2.2)',keyframes:{scale:[.5,1.12,.92,1.04,1],skewX:[0,0,6,-4,0]}}`
|
||||
- **dropb** — `{opacity:0,yPercent:-85}→{opacity:1,yPercent:0,duration:.95,ease:'power2.out',keyframes:{yPercent:[-85,0,-22,0,-8,0]}}` (drop + multi-bounce). **❗opacity must reach 1 and stay** — declare it at the end of the keyframes.
|
||||
- **scrawl** — `{opacity:0,clipPath:'inset(-12% 100% -12% 0)',rotation:-3}→{opacity:1,clipPath:'inset(-12% 0 -12% 0)',rotation:0,duration:.8,ease:'power2.out'}` (handwritten write-on, for script faces)
|
||||
- **popr** — `{opacity:0,scale:.4,rotation:8}→{opacity:1,scale:1,rotation:0,duration:.6,ease:'back.out(2)'}`
|
||||
|
||||
### creator
|
||||
|
||||
- **boxpop** — `{opacity:0,scale:.5}→{opacity:1,scale:1,duration:.5,ease:'back.out(1.8)'}` (the active flow word's highlight box is a style)
|
||||
- **slideup** — `{opacity:0,yPercent:44}→{opacity:1,yPercent:0,duration:.55,ease:'power3.out'}`
|
||||
- **flip** — see premium
|
||||
- **shimmer** — `{opacity:0,scale:.7,filter:'brightness(2.2)'}→{opacity:1,scale:1,filter:'brightness(1)',duration:.75,ease:'power2.out'}`
|
||||
|
||||
### ultra (the flash lives in the type)
|
||||
|
||||
- **volt** — `{opacity:0}→{opacity:1,duration:1.1,ease:'steps(1)',keyframes:{opacity:[0,1,.12,1,.35,1],x:[0,0,0,2,-2,0]}}` then settle to glow `text-shadow:0 0 8px #fff,0 0 22px #43f4ff,0 0 48px #1e90ff` (electric strike)
|
||||
- **hyper** — `{opacity:0,scale:.05,filter:'blur(3px) brightness(3)'}→{opacity:1,scale:1,filter:'blur(0) brightness(1)',duration:.9,ease:'expo.out'}` (warp from depth)
|
||||
- **liquid** — `{opacity:0,scale:.9,yPercent:6,filter:'blur(10px)'}→{opacity:1,scale:1,yPercent:0,filter:'blur(0)',duration:1,ease:'power2.out'}` + the `.climax` _container_ carries SVG `filter:url(#liquid)`; in HF drive `feDisplacementMap@scale` from the timeline (don't use SMIL — not seek-safe)
|
||||
- **prism** — `{opacity:0,textShadow:'-38px 0 #ff003c,38px 0 #00ffd1',filter:'blur(5px)',scale:1.08}→{opacity:1,textShadow:'-3px 0 #ff003c,3px 0 #00ffd1',filter:'blur(0)',scale:1,duration:1,ease:'power3.out'}` (chromatic dispersion converge)
|
||||
- **shatter** — `{opacity:0,scale:1.9,rotation:-3,filter:'blur(14px)'}→{opacity:1,scale:1,rotation:0,filter:'blur(0)',duration:1,ease:'back.out(1.4)',keyframes:{scale:[1.9,.9,1.06,.98,1]}}` (+ a 1-frame white `text-shadow` flash on landing)
|
||||
- **extrude** — `{opacity:0,rotationY:42,scale:.9,transformPerspective:700}→{opacity:1,rotationY:-9,scale:1,duration:.9,ease:'power3.out'}` (3D turn; depth via stacked `text-shadow` in the style)
|
||||
|
||||
### atelier (design-forward)
|
||||
|
||||
- **editwipe** — `{opacity:1,clipPath:'inset(-15% 100% -15% 0)'}→{clipPath:'inset(-15% -4% -15% 0)',duration:.7,ease:'power4.inOut'}` (Swiss precise wipe; negative top/bottom so glyph tops aren't clipped)
|
||||
- **rise** — see premium (slower, `duration:.9`)
|
||||
- **block** — `{opacity:0,scaleY:0,transformOrigin:'bottom center'}→{opacity:1,scaleY:1,duration:.7,ease:'power3.out'}` (constructed/Bauhaus)
|
||||
- **vert** — `{opacity:0,clipPath:'inset(0 0 100% 0)'}→{opacity:1,clipPath:'inset(0 0 0 0)',duration:.9,ease:'power2.out'}` (vertical ink-drop; `writing-mode:vertical-rl` is a style, climax sits at `left:81%`)
|
||||
- **ink** — `{opacity:0,clipPath:'inset(-28% 100% -28% -4%)',filter:'blur(3px)'}→{opacity:1,clipPath:'inset(-28% -8% -28% -4%)',filter:'blur(0)',duration:.9,ease:'power2.out'}` (brush write-on; generous negative insets for the script face)
|
||||
- **weight** — `{opacity:0,fontVariationSettings:"'wght' 100",letterSpacing:'.18em'}→{opacity:1,fontVariationSettings:"'wght' 900",letterSpacing:'0',duration:1,ease:'power2.out'}` (variable-font morph; needs a variable face, e.g. Inter `wght@100..900`)
|
||||
|
||||
### impact (the ONE sanctioned scene effect, at the climax only)
|
||||
|
||||
Each pairs the type move with a scene element (`<div class="flash">` / `<div class="ring">`) and/or a stage shake. Keep it to the climax beat.
|
||||
|
||||
- **nuke** — type: `{opacity:0,scale:.3,filter:'brightness(8) blur(10px)'}→{opacity:1,scale:1,filter:'brightness(1) blur(0)',duration:1,ease:'expo.out'}` + white `.flash` `to{opacity:0}` over 0.3 s + brief stage `x` shake
|
||||
- **meteor** — `{opacity:0,yPercent:-45,scale:1.12,filter:'blur(12px) brightness(2.2)'}→{opacity:1,yPercent:0,scale:1,filter:'blur(0)',duration:1,ease:'power4.in'}` (accelerate DOWN) + on-land stage shake
|
||||
- **sonic** — `{opacity:0,scale:1.4,filter:'blur(8px)'}→{opacity:1,scale:1,filter:'blur(0)',duration:.9,ease:'expo.out'}` + a `.ring` scaling `0→3` opacity `1→0`
|
||||
- **seismic** — `{opacity:0,scale:1.1}→{opacity:1,scale:1,duration:.9,ease:'power3.out'}` + stage `x`/`y` quake `keyframes:[3,-3,2,-2,0]` decaying
|
||||
- **judge** — `{opacity:0,yPercent:-45,scale:1.12,filter:'blur(12px) brightness(2.2)'}→{opacity:1,yPercent:0,scale:1,filter:'blur(0)',duration:1,ease:'power3.out'}` (descends from above) + light-shaft `.flash`
|
||||
|
||||
---
|
||||
|
||||
## CLIMAX exits
|
||||
|
||||
Default is **fade** unless a template names another. **Every exit ends at `opacity:0`** (or fully-clipped) so nothing lingers — declare opacity:0 at the end even on transform/clip exits.
|
||||
|
||||
- **fade** — `to{opacity:0,scale:1.03,duration:.6,ease:'power2.in'}` (premium/epic/most defaults)
|
||||
- **rise-off** — `to{opacity:0,yPercent:-42,duration:.6}` · **sink** — `to{opacity:0,yPercent:16,scale:.97}` · **lift** — `to{opacity:0,yPercent:-26,letterSpacing:'.1em'}`
|
||||
- **expose-off** — `to{opacity:0,filter:'brightness(3.6) blur(4px)'}` · **breathe-off** — `to{opacity:0,scale:1.08,filter:'blur(7px)'}`
|
||||
- **flip-off** — `to{opacity:0,rotationX:-90}` · **fly-off** — `to{opacity:0,scale:2.5,filter:'blur(13px)'}`
|
||||
- **untype** — `to{clipPath:'inset(-15% 100% -15% 0)',opacity:0,ease:'steps(11)'}` (backspace) · **scan-collapse** — `to{opacity:0,scaleY:.02}` · **power-off** — `keyframes:{opacity:[1,.3,1,.1,.5,0]}` + scaleY collapse
|
||||
- **drag** (horror) — `to{opacity:0,xPercent:-40,skewX:-20,filter:'blur(11px)',duration:.55,ease:'power2.in'}` (dragged away)
|
||||
- **snap** (horror) — `keyframes:{x:[0,7,-6,0],skewX:[0,-13,0],scale:[1,1.1,1.16,1.32],opacity:[1,1,1,0],textShadow:['none','8px 0 #c00,-8px 0 #0aa','none','none']}` (violent jerk then gone)
|
||||
- **cut** (horror) — `keyframes:{opacity:[1,0,1,0,.8,0]},ease:'steps(1)'` (frame-drop) · **bleed** — `to{opacity:0,yPercent:50,scale:.97,filter:'blur(6px)'}` (drips down)
|
||||
- **knock** — `to{opacity:0,xPercent:55,filter:'blur(9px)'}` · **peel** — `to{opacity:0,rotation:14,xPercent:42,scale:.9}` · **spin-out** — `to{opacity:0,rotation:180,scale:.3}`
|
||||
- **hop** — `keyframes:{yPercent:[0,-26,120],opacity:[1,1,0]},scaleY:.7` · **popout** — `to{opacity:0,scale:.3,rotation:-10}` · **slidedown** — `to{opacity:0,yPercent:42}` · **scrawl-off** — `to{opacity:0,yPercent:-14,rotation:2}`
|
||||
- **vhs-out** — `keyframes:{x:[0,-9,8,0],opacity:[1,1,0]},scaleY:.2` (tracking-lost roll) · **blink-out** — `keyframes:{opacity:[1,0,1,0,1,0,0]},ease:'steps(1)'` · **power-down** — `keyframes:{opacity:[1,.2,.7,.1,0]},ease:'steps(2)'` · **lift2** — `to{opacity:0,scale:1.12,filter:'blur(3px)'}` (stamp lift) · **shim-out** — `to{opacity:0,scale:.8,filter:'brightness(1.9)'}`
|
||||
- **iris-close** — `to{opacity:0,clipPath:'circle(0% at 50% 46%)'}` (Bond) · **un-shine** — `to{opacity:0,clipPath:'inset(0 0 0 100%)',filter:'brightness(1.7)'}` · **sweep-off** — `to{opacity:1,clipPath:'inset(0 100% 0 0)'}`
|
||||
- ultra family exits: **glitch-out**, **neon-out**, **hyper-out** `to{opacity:0,scale:2.6,filter:'blur(13px)'}`, **liquid-out** `to{opacity:0,yPercent:40,scale:.96,filter:'blur(9px)'}`, **prism-out** (channels split apart), **shatter-out** `to{opacity:0,scale:1.45,filter:'blur(9px)',textShadow:'7px 0 #ff003c,-7px 0 #00ffd1'}`
|
||||
|
||||
## Pairs with HF skills
|
||||
|
||||
- `hyperframes-gsap` — single paused timeline, transform aliases, ease list, the animated-property allowlist.
|
||||
- `_anatomy.md` — where these four hooks attach.
|
||||
- `hyperframes-animation/rules/asr-keyword-glow.md` — the verbatim active-word envelope for the flow.
|
||||
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
* build-fonts-css.cjs — bundle the template fonts into ONE self-contained
|
||||
* fonts.css with base64 data-URI @font-face (no sub-resources, no CDN, no
|
||||
* system-font dependency). This is what makes Standard/Cinematic renders
|
||||
* deterministic on ANY machine: hyperframes' renderer only auto-supplies its
|
||||
* ~18 CANONICAL_FONTS; every other family falls back to a generic font unless
|
||||
* the HTML ships a local @font-face. (hyperframes' own font linter says exactly
|
||||
* this: "Use local @font-face declarations with captured .woff2 files.")
|
||||
*
|
||||
* Source woff2 live in ./files/<slug>-latin-<weight>-normal.woff2 (latin subset
|
||||
* from @fontsource via jsdelivr). Re-run after adding/removing a font:
|
||||
* node build-fonts-css.cjs
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const FILES = path.join(__dirname, "files");
|
||||
const OUT = path.join(__dirname, "fonts.css");
|
||||
|
||||
// slug → exact CSS family name (must match what templates declare in font-family).
|
||||
const FAMILY = {
|
||||
anton: "Anton",
|
||||
audiowide: "Audiowide",
|
||||
inter: "Inter",
|
||||
"baloo-2": "Baloo 2",
|
||||
bangers: "Bangers",
|
||||
"bodoni-moda": "Bodoni Moda",
|
||||
caveat: "Caveat",
|
||||
cinzel: "Cinzel",
|
||||
"cormorant-garamond": "Cormorant Garamond",
|
||||
creepster: "Creepster",
|
||||
fredoka: "Fredoka",
|
||||
monoton: "Monoton",
|
||||
orbitron: "Orbitron",
|
||||
"permanent-marker": "Permanent Marker",
|
||||
"press-start-2p": "Press Start 2P",
|
||||
"saira-stencil-one": "Saira Stencil One",
|
||||
"shippori-mincho": "Shippori Mincho",
|
||||
rajdhani: "Rajdhani",
|
||||
"chakra-petch": "Chakra Petch",
|
||||
sora: "Sora",
|
||||
"space-grotesk": "Space Grotesk",
|
||||
"special-elite": "Special Elite",
|
||||
teko: "Teko",
|
||||
vt323: "VT323",
|
||||
};
|
||||
|
||||
const files = fs
|
||||
.readdirSync(FILES)
|
||||
.filter((f) => f.endsWith(".woff2"))
|
||||
.sort();
|
||||
const faces = [];
|
||||
let raw = 0;
|
||||
const seenFamilies = new Set();
|
||||
for (const f of files) {
|
||||
const m = f.match(/^(.*)-latin-(\d+)-(normal|italic)\.woff2$/);
|
||||
if (!m) {
|
||||
console.error(`[fonts] skip unrecognized filename: ${f}`);
|
||||
continue;
|
||||
}
|
||||
const slug = m[1],
|
||||
weight = m[2],
|
||||
style = m[3];
|
||||
const family = FAMILY[slug];
|
||||
if (!family) {
|
||||
console.error(`[fonts] no family mapping for slug "${slug}" (${f}) — add it to FAMILY`);
|
||||
process.exit(1);
|
||||
}
|
||||
const buf = fs.readFileSync(path.join(FILES, f));
|
||||
raw += buf.length;
|
||||
seenFamilies.add(family);
|
||||
faces.push(
|
||||
`@font-face {\n` +
|
||||
` font-family: '${family}';\n` +
|
||||
` font-style: ${style};\n` +
|
||||
` font-weight: ${weight};\n` +
|
||||
` font-display: block;\n` + // block (not swap): render text only once the real face is ready — measure-layout + capture see the true glyphs, never a fallback flash
|
||||
` src: url(data:font/woff2;base64,${buf.toString("base64")}) format('woff2');\n` +
|
||||
`}`,
|
||||
);
|
||||
}
|
||||
|
||||
const header =
|
||||
`/* AUTO-GENERATED by build-fonts-css.cjs — do not edit by hand.\n` +
|
||||
` ${faces.length} faces / ${seenFamilies.size} families, ${Math.round(raw / 1024)}KB raw woff2 (latin subset, base64-inlined).\n` +
|
||||
` Families: ${[...seenFamilies].sort().join(", ")}.\n` +
|
||||
` These are the template fonts NOT in hyperframes' auto-resolved set; inlining\n` +
|
||||
` them makes every render deterministic regardless of installed system fonts. */\n`;
|
||||
|
||||
fs.writeFileSync(OUT, header + "\n" + faces.join("\n\n") + "\n");
|
||||
const outKb = Math.round(fs.statSync(OUT).size / 1024);
|
||||
console.log(`[fonts] wrote ${OUT}`);
|
||||
console.log(
|
||||
`[fonts] ${faces.length} faces, ${seenFamilies.size} families, ${outKb}KB css (${Math.round(raw / 1024)}KB raw)`,
|
||||
);
|
||||
console.log(`[fonts] families: ${[...seenFamilies].sort().join(", ")}`);
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user