Files
zooqueen 62bbc7766c skills: give each skill the directory its name declares
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.
2026-08-13 18:17:24 -07:00

6.6 KiB
Raw Permalink Blame History

name, description, metadata
name description metadata
asr-keyword-glow Keywords glow + scale up when "spoken" — attack/sustain/release envelope synced to per-word timestamps. Even without real audio, hardcoded timings create a "narrator emphasis" effect.
tags
asr, audio-sync, highlight, glow, keyword, text, speech, emphasis

ASR Keyword Glow

Words in a phrase visually activate (glow blur + scale) when "spoken", following an attack-sustain-release envelope over per-word { start, end } timestamps. In a real ASR pipeline the timings come from a word-level transcript (frames transcribe — same shape); for promo video, hand-author them to control emphasis pacing. The envelope never falls to zero after a word — it decays to a rest level, leaving a breadcrumb of recent emphasis.

How It Works

A single linear driver tween (ease: "none" — any other ease distorts the per-word envelope; do not change) sweeps scene time; its onUpdate loops over ALL words computing each one's envelope: 0 before start, linear attack to 1 over ATTACK_DUR, sustain at 1 until end, decay to REST_LEVEL over RELEASE, then hold at rest. The envelope drives text-shadow blur and scale — one driver for the whole phrase, never one tween per word (60+ words would bloat the timeline).

Recipe

<!-- inside a standard scene clip (frames-core) -->
<div class="phrase">
  <span class="word" data-word="{w1Key}">{w1}</span>
  <span class="word" data-word="{w2Key}">{w2}</span>
  <!-- … the final word may be the brand, with the .brand modifier -->
  <span class="word brand" data-word="{brandKey}">{brandWord}</span>
</div>
.phrase {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  color: {restColor};
}
.word {
  display: inline-block; /* required for transform on <span> */
  transform-origin: 50% 50%;
  text-shadow: 0 0 0 {glowColorTransparent};
}
.word.brand {
  color: {brandAccentColor};
}
// Per-word spoken windows — one entry per span; brand word 1.5-2× a normal word's window.
const TIMINGS = {
  // {w1Key}: { start: …, end: … },  — seconds, local to the scene
};

function envelope(time, start, end) {
  if (time < start) return 0;
  if (time < end) return Math.min((time - start) / ATTACK_DUR, 1);
  const releaseEnd = end + RELEASE;
  if (time < releaseEnd) return 1 - ((time - end) / RELEASE) * (1 - REST_LEVEL);
  return REST_LEVEL;
}

const words = document.querySelectorAll(".word");
const driver = { t: 0 };
tl.to(
  driver,
  {
    t: SCENE_DURATION,
    duration: SCENE_DURATION,
    ease: "none", // linear — t maps 1:1 to scene time
    onUpdate: () => {
      words.forEach((el) => {
        const timing = TIMINGS[el.dataset.word];
        if (!timing) return;
        const env = envelope(driver.t, timing.start, timing.end);
        el.style.textShadow = `0 0 ${MAX_BLUR * env}px ${glowColorRgba(env)}`;
        el.style.transform = `scale(${1 + MAX_SCALE_BOOST * env})`;
      });
    },
  },
  0,
);

glowColorRgba(env) returns the glow color with env-modulated alpha.

Variations

  • Karaoke style (RECOMMENDED for video narration) — the default amplitudes read too subtle in video: inactive words still dominate. Render inactive words DIM and lerp the active word toward bright + larger; at any moment 1–2 words are bright (spoken + lingering rest) and the rest is dim. Use for short phrases (5–10 words) where one word at a time should POP; keep the subtle default for long dense text. Pushes MAX_BLUR, MAX_SCALE_BOOST, and REST↔ACTIVE contrast; everything else identical:
function lerpChannel(a, b, t) {
  return Math.round(a + (b - a) * t);
}
function colorAt(env, isBrand) {
  const target = isBrand ? BRAND_RGB : ACTIVE_RGB;
  return `rgb(${lerpChannel(REST_RGB.r, target.r, env)}, ${lerpChannel(REST_RGB.g, target.g, env)}, ${lerpChannel(REST_RGB.b, target.b, env)})`;
}
// in onUpdate: el.style.color = colorAt(env, el.classList.contains("brand"));
  • Multi-octave glow — multiply the sustain by 1 + sin(driver.t × PULSE_HZ) × PULSE_AMPLITUDE so high-emphasis words breathe at peak.
  • Color shift on the peak — same channel-lerp from restColor → peakColor as env rises (non-karaoke form).
  • 3D pop-out — add translateZ(env × MAX_POP_Z) so the spoken word leans toward camera; requires perspective on the parent.
  • From real ASR transcripts — convert { word, start_ms, end_ms } entries to seconds and feed in identically.

Values

token default style karaoke style notes
ATTACK_DUR 0.1–0.25s same must be < the shortest word's window or it never reaches 1
RELEASE 0.2–0.5s same decay to rest
REST_LEVEL 0.15–0.4 0.05–0.2 > 0 (breadcrumb), < 1
MAX_BLUR 15–25px 30–45px bigger = "shouting"
MAX_SCALE_BOOST 0.03–0.10 0.15–0.25 additive at peak (0.08 ⇒ scale 1.08)
PULSE_HZ / AMP 4–10 rad/s / 0.1–0.3 — multi-octave variation
MAX_POP_Z 20–60px — 3D variation
SCENE_DURATION = data-duration same driver must end in sync with the scene's seek window

Critical Constraints

  • Timings monotonic, non-overlapping — every entry's end < the next entry's start; overlapping windows make the envelope ambiguous.
  • Brand word window 1.5–2× a normal word — the brand is the headline; let it sustain.
  • Driver ease stays "none" — any other ease warps every word's envelope timing.
  • text-shadow, not box-shadow — the glow must hug the GLYPH (speaking emphasis), not the inline-block rectangle.
  • One driver looping all words — never one tween per word.
  • Commit to a style — values between the default and karaoke columns yield awkward "half-loud" emphasis.
  • Climax dwell ≥1s after the final word's emphasis — the last word IS the headline beat.

See also

3d-text-depth-layers (depth on the active word at peak) · sine-wave-loop (idle breathe between emphasis moments) · context-sensitive-cursor (typewriter matching the ASR cadence) · /media-use for frames transcribe and caption rendering.