Files

7.4 KiB
Raw Permalink 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.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).