Files
hyperframes/skills/hyperframes-animation/rules/svg-icon-enrichment.md
T
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

7.4 KiB
Raw Blame History

name, description, metadata
name description metadata
svg-icon-enrichment Animate internal SVG elements (rotating hands, opening blades, pulsing dots, dash flows) to make icons feel alive without replacing them.
tags
svg, icon, animation, internal, micro-animation, pulse, rotation

SVG Icon Enrichment

Treats an SVG icon as a composition of animated PARTS, not an opaque image. Each meaningful internal element (a clock hand, scissor blade, recording dot, data line) gets its own micro-animation, targeted by id. Distinct from svg-path-draw (which animates the OUTLINE drawing) — enrichment animates INTERNAL PARTS, ideally after the outline has drawn.

Four signature patterns:

Pattern Use For Math Tip
Rotation Clock, gear, loader, dial rotate(deg cx cy) attribute, linear see the transform-center gotcha
Oscillation Scissors, wings, toggle rotate(±sin·amp) on opposing groups opposite signs on the two parts
Pulse Recording dot, heart, notification scale(1 + sin·amp) + opacity ring lags dot by π/2 for ripple
Dash flow Cutting line, data stream strokeDashoffset linear via time negative for L→R, positive for R→L

The transform-center gotcha

For rotation around an explicit point inside an SVG, use the SVG transform ATTRIBUTE, not CSS transform: el.setAttribute("transform", rotate(${deg} ${cx} ${cy})). The CSS combination transform: rotate(...) + transform-origin: 60px 60px + transform-box: fill-box interprets the origin in the element's OWN bbox-local coordinates, NOT viewBox coordinates. For a thin <line> (whose bbox is the line's narrow envelope), 60 60 bbox-local is a point OUTSIDE the line — the hand flies along an off-center arc instead of rotating in place. Same trap for small inner shapes (a dot circle whose bbox is the small circle, not the full viewBox).

Scaling around a center point: same attribute route — el.setAttribute("transform", translate(${cx} ${cy}) scale(${s}) translate(-${cx} -${cy})).

Recipe

<!-- inside a standard scene clip — named children are the animation targets -->
<svg class="icon-svg" viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg">
  <circle cx="60" cy="60" r="50" fill="none" stroke="{accentColor}" stroke-width="6" />
  <line
    id="hand-min"
    x1="60"
    y1="60"
    x2="60"
    y2="22"
    stroke="{textColor}"
    stroke-width="6"
    stroke-linecap="round"
  />
  <line
    id="hand-sec"
    x1="60"
    y1="60"
    x2="60"
    y2="30"
    stroke="{recordColor}"
    stroke-width="3"
    stroke-linecap="round"
  />
  <circle cx="60" cy="60" r="6" fill="{textColor}" />
</svg>
<!-- pulse icon: #rec-ring + #rec-dot circles; dash-flow: a <line> with stroke-dasharray="14 12" -->
// Pattern 1 — Rotation. Proxy tween → SVG transform attribute (explicit center, see gotcha).
const hand = document.getElementById("hand-min");
const minState = { deg: 0 };
tl.to(
  minState,
  {
    deg: 360 * MIN_REVOLUTIONS,
    duration: TOTAL_DURATION,
    ease: "none", // linear motion is the point
    onUpdate: () => hand.setAttribute("transform", `rotate(${minState.deg} 60 60)`),
  },
  0,
);
// second hand: same shape with SEC_REVOLUTIONS (visibly faster).

// Pattern 3 — Pulse. One phase proxy drives dot + ring, ring offset by π/2.
const dot = document.getElementById("rec-dot");
const ring = document.getElementById("rec-ring");
const pulse = { p: 0 };
tl.to(
  pulse,
  {
    p: Math.PI * 2 * PULSE_CYCLES,
    duration: TOTAL_DURATION,
    ease: "none", // sine handles the curve
    onUpdate: () => {
      const sD = 1 + Math.sin(pulse.p) * PULSE_DOT_AMP;
      const sR = 1 + Math.sin(pulse.p + Math.PI / 2) * PULSE_RING_AMP;
      dot.setAttribute("transform", `translate(60 60) scale(${sD}) translate(-60 -60)`);
      ring.setAttribute("transform", `translate(60 60) scale(${sR}) translate(-60 -60)`);
      ring.style.opacity = String(
        PULSE_RING_OPACITY_BASE + Math.sin(pulse.p) * PULSE_RING_OPACITY_AMP,
      );
    },
  },
  0,
);

// Pattern 4 — Dash flow. Linear offset tween on a dashed stroke.
const flowState = { offset: 0 };
tl.to(
  flowState,
  {
    offset: DASH_FLOW_TOTAL_OFFSET, // negative = L→R
    duration: TOTAL_DURATION,
    ease: "none",
    onUpdate: () => {
      document.getElementById("data-flow").style.strokeDashoffset = String(flowState.offset);
    },
  },
  0,
);

Variations

  • Stroke draw → enrichment chain — draw the outline first via svg-path-draw (phase 1, 0 → OUTLINE_DUR), then start enrichment at OUTLINE_DUR: the icon "wakes up" after assembly.
  • Per-icon entry stagger — for a row of icons, each icon's enrichment starts as it fades in, not synchronized.

Values

token range notes
MIN_REVOLUTIONS 0.52.0 avoid integer revolutions if the end frame is visible (lands back at start)
SEC_REVOLUTIONS 410 > MIN × 3 or the speed difference doesn't read
PULSE_CYCLES 24 over a 35s comp ≥5 reads as anxious flicker; ≤1 reads as forgotten
PULSE_DOT_AMP 0.050.20 0.05 = breathing; 0.20 = throbbing
PULSE_RING_AMP 0.040.12 must be < PULSE_DOT_AMP or the ring overshadows the dot
PULSE_RING_OPACITY_BASE / _AMP 0.40.6 / 0.30.5 BASE AMP ≥ 0 and BASE + AMP ≤ 1
DASH_FLOW_TOTAL_OFFSET ±100400 must be an integer multiple of the dash period (dash + gap) or the end frame shows a phase jump

Critical Constraints

  • The transform-center gotcha above — SVG transform attribute for any rotation/scale around an explicit interior point; never CSS transform-origin + transform-box: fill-box on thin lines or small inner shapes.
  • No requestAnimationFrame — like CSS animation, it desyncs from HF's frame-by-frame seek; continuous motion lives inside the timeline as linear proxy tweens.
  • Amplitudes subtle — icons are decorative, not headlines; calibrate rotation speed against composition length, not absolute time.
  • Phase-offset the parts — minute vs second hand at different speeds, ring lagging dot by π/2. Pure sync looks mechanical.
  • stroke-linecap: round on flowing/dashed lines for clean dash edges.
  • Climax dwell ≥1s — if the enrichment is the headline beat, the composition continues ≥1s after the most dramatic moment.

See also

svg-path-draw (outline draws first, enrichment second) · orbit-3d-entry (orbiting items are enriched icons) · sine-wave-loop (the whole icon floats while internal parts animate).