Files
hyperframes/skills/hyperframes-animation/rules/context-sensitive-cursor.md
WaterrrForever 853256403b feat(skills): c2v mining pass — 7 new blueprints, 10 new rules, compacted recipe corpus (#2680)
* feat(skills): c2v mining pass over animation blueprints and rules

Compacts ~45 existing animation rules/blueprints into tighter recipe form
(net -3.4k lines) and adds 17 mined from the c2v corpus:

- 7 blueprints: agent-progress-theater, camera-journey, fixed-anchor-cycle,
  panel-edit-live-sync, prompt-type-submit-generate,
  transcript-scroll-artifact-reveal, zoom-out-workspace-reveal
- 10 rules: 3d-camera-flight, anchored-layout-expand, chart-scrub-readout,
  chromatic-glitch, control-target-sync, cursor-drag, gradient-text-sweep,
  multi-cursor-choreography, particle-burst, theme-crossfade-morph






Both indexes updated.

* feat(skills): sync product-launch script bank with mined blueprint roles

The role->blueprint script bank in product-launch-video/story-design.md is
kept 1:1 with blueprints-index role declarations, which the c2v mining pass
expanded. Adds the 25 missing entries (script-shape descriptor + example
lines + pattern): 13 for the 7 new blueprints, 12 for role widenings on 7
existing ones (cursor-ui-demo, dataviz-countup, titlecard-reveal, et al.),
and states the 1:1 sync contract in the bank's intro.

* docs(skills): cover constellation-hub scatter-drift variant in the script bank

Review follow-up on #2680: the SOCIAL_PROOF constellation-hub entry
patterned only the orbit shape; the c2v pass added a scatter-drift
end-card variant with the opposite geometry (no hub, no ring). Adds an
example line and extends the pattern so a scatter-drift beat's VO isn't
steered toward the orbit shape.
2026-07-21 17:28:55 +08:00

6.8 KiB
Raw Permalink Blame History

name, description, metadata
name description metadata
context-sensitive-cursor Cursor color and styling that adapt to the current text segment being typed — accent color on highlights, dim on placeholders, etc.
tags
cursor, color, context, typewriter, styling, segment

Context-Sensitive Cursor

In a typewriter sequence, the cursor's color (and optionally height / blink behavior) matches the active text segment — brand accent while typing the brand name, dim on placeholders, success color on the completion mark. The eye lands on the keyword being typed because the cursor shifts with it; a fixed single-color cursor is visual noise by comparison. Layers on top of discrete-text-sequence's SEQUENCE pattern.

How It Works

The text is authored as a SEQUENCE of { t, text, segment, color } entries; a linear driver's onUpdate reverse-searches for the current entry and writes both the visible text and the cursor's background (the cursor is a colored block, so background, NOT color). A second linear tween sweeps a phase p through × BLINK_CYCLES_PER_SCENE and gates cursor opacity on sin(p) > 0 — a deterministic square-wave blink on the timeline.

Recipe

<!-- inside a standard scene clip (hyperframes-core) -->
<div class="terminal">
  <div class="prompt">$</div>
  <div class="text-wrap">
    <span class="text" id="text"></span><span class="cursor" id="cursor">_</span>
  </div>
</div>
.terminal {
  font-family: {monoFont}; /* proportional fonts drift the cursor mid-segment */
  display: flex;
  align-items: baseline;
  white-space: pre; /* preserve trailing spaces — cursor sits at segment end */
}
.text {
  white-space: pre;
}
.cursor {
  display: inline-block; /* inline ignores width/height */
  width: {cursorWidth}px;
  height: {cursorHeight}px;
  background: {textColor}; /* default — overridden per segment in onUpdate */
  vertical-align: {cursorBaselineFix}px; /* small negative — anchor to baseline, not line-height */
}
// Adjacent entries usually share a text prefix but may differ in `segment` —
// that's what shifts the cursor color mid-line.
const SEQUENCE = [
  { t: 0, text: "", segment: "main", color: "{mainColor}" },
  { t: T_LEADIN_END, text: "{leadInChunk}", segment: "main", color: "{mainColor}" },
  { t: T_BRAND_IN, text: "{leadInBrandPrefix}", segment: "brand", color: "{brandColor}" },
  { t: T_BRAND_OUT, text: "{leadInBrandFull}", segment: "main", color: "{mainColor}" },
  { t: T_CMD_IN, text: "{leadInCmdPrefix}", segment: "cmd", color: "{cmdColor}" },
  { t: T_SUCCESS, text: "{leadInDone}", segment: "success", color: "{successColor}" },
];

function entryAt(time) {
  for (let i = SEQUENCE.length - 1; i >= 0; i--) {
    if (time >= SEQUENCE[i].t) return SEQUENCE[i];
  }
  return SEQUENCE[0];
}

const textEl = document.getElementById("text");
const cursorEl = document.getElementById("cursor");

const driver = { t: 0 };
tl.to(
  driver,
  {
    t: DURATION,
    duration: DURATION,
    ease: "none",
    onUpdate: () => {
      const entry = entryAt(driver.t);
      textEl.textContent = entry.text;
      cursorEl.style.background = entry.color;
    },
  },
  0,
);

// Deterministic square-wave blink
const blink = { p: 0 };
tl.to(
  blink,
  {
    p: Math.PI * 2 * BLINK_CYCLES_PER_SCENE,
    duration: DURATION,
    ease: "none",
    onUpdate: () => {
      cursorEl.style.opacity = Math.sin(blink.p) > 0 ? "1" : "0";
    },
  },
  0,
);

Variations

  • Non-blinking during active typing — suppress blink while letters are appearing (solid cursor), resume on idle. This MUST be a pure function of the driver's time: tracking a mutable lastChangeTime in onUpdate is not reverse-seek-safe (scrubbing backwards leaves the stale forward-pass value behind and the cursor blinks — or holds solid — at the wrong frames). Bake the change times from the SEQUENCE instead — every entry whose text differs from its predecessor is a typing event:
// Baked once at build time — no runtime state.
const CHANGE_TIMES = SEQUENCE.filter((e, i) => i > 0 && e.text !== SEQUENCE[i - 1].text).map(
  (e) => e.t,
);
// In onUpdate — identical result at any seek, either direction:
const isTyping = CHANGE_TIMES.some((t) => t <= driver.t && driver.t - t < TYPING_GRACE);
cursorEl.style.opacity = isTyping ? "1" : Math.sin(blink.p) > 0 ? "1" : "0";
  • Cursor HEIGHT shifts on segment — larger cursor on the brand segment: cursorEl.style.height = entry.segment === "brand" ? cursorHeightEmphasis : cursorHeight (1.11.25×; more reads as glitch).
  • Contrast reversal — a dark-text-on-light segment needs a dark cursor too; keep entry.color as the single source of truth and read from it.

Values

token range notes
DURATION 48s per typed line ≥ SEQUENCE[last].t + closing dwell
entry t spacing 0.20.5s micro-additions ascending, non-uniform — slow down on highlights
segment palette 34 colors max more reads as random; brand vs success should differ in saturation/luminance
cursorWidth / Height 824px / 0.851.0× fontSize too thin vanishes in render compression; too tall outranks the text
cursorBaselineFix small negative px drop the block to the text baseline
BLINK_CYCLES_PER_SCENE period ≈ 0.61.2s whole number — otherwise the sin sweep ends mid-cycle and the cursor pops on the last frame
TYPING_GRACE 0.150.3s < shortest dwell between adjacent entries — otherwise the cursor never blinks

Critical Constraints

  • Cursor color goes on background — it's a colored block, not a glyph.
  • Blink is timeline-driven sin, pure of any mutable tracker — the typing-grace variation shows the seek-safe form.
  • white-space: pre on text and container — collapsed trailing spaces park the cursor in the wrong column.
  • Monospace font + display: inline-block cursor — proportional faces drift the cursor mid-segment; inline ignores the block geometry.
  • BLINK_CYCLES_PER_SCENE is a whole number for the fixed DURATION.

See also

discrete-text-sequence (the underlying SEQUENCE pattern) · camera-cursor-tracking (camera follows the cursor) · press-release-spring (post-typing confirm press).