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

142 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: svg-icon-enrichment
description: Animate internal SVG elements (rotating hands, opening blades, pulsing dots, dash flows) to make icons feel alive without replacing them.
metadata:
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](svg-path-draw.md) (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
```html
<!-- 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" -->
```
```js
// 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](svg-path-draw.md) (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).