mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 14:50:02 +00:00
* 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.
142 lines
7.4 KiB
Markdown
142 lines
7.4 KiB
Markdown
---
|
||
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.5–2.0 | avoid integer revolutions if the end frame is visible (lands back at start) |
|
||
| SEC_REVOLUTIONS | 4–10 | > MIN × 3 or the speed difference doesn't read |
|
||
| PULSE_CYCLES | 2–4 over a 3–5s comp | ≥5 reads as anxious flicker; ≤1 reads as forgotten |
|
||
| PULSE_DOT_AMP | 0.05–0.20 | 0.05 = breathing; 0.20 = throbbing |
|
||
| PULSE_RING_AMP | 0.04–0.12 | must be < PULSE_DOT_AMP or the ring overshadows the dot |
|
||
| PULSE_RING_OPACITY_BASE / \_AMP | 0.4–0.6 / 0.3–0.5 | BASE − AMP ≥ 0 and BASE + AMP ≤ 1 |
|
||
| DASH_FLOW_TOTAL_OFFSET | ±100–400 | 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).
|