Files
zooqueen 62bbc7766c skills: give each skill the directory its name declares
Every SKILL.md already declared the new name — name: frames, frames-cli,
frames-core, and so on — but the directories were never moved, so the tree
carried frames-* in content and hyperframes-* on disk. Everything that
resolves a skill by path missed: the core-skill list, the plugin marketplace
pin, and the docs the CLI reads at runtime. Eight tests failed on main because
the files they open do not exist.

The directories now match what the files say they are, and the guide the
claude-design contract reads takes the name that contract already used.
2026-08-13 18:17:24 -07:00

9.7 KiB
Raw Permalink Blame History

name, description, metadata
name description metadata
particle-burst Deterministic particle / confetti events — a confetti pop that bursts up and drifts down (optionally instant-shrinking away), a dot burst from behind text, or a glyph dissolving to particles. Every particle's state is a pure ballistic function of timeline time from index-seeded values, so a scrub to any t shows the correct mid-flight frame.
tags
particles, confetti, burst, dissolve, celebration, ballistic, deterministic, punctuation

Particle Burst

Discrete flying particles as a one-shot event: a confetti pop that erupts upward and drifts back down on gravity, a dot burst radiating from behind a landing word, or a glyph dissolve where text breaks into particles that scatter and die. Particles are ephemeral garnish — born from a beat, fly, gone; they never become layout.

Boundaries: css-marker-patterns.md's burst mode is radiating drawn lines — a static accent, no flight. press-release-spring.md's release burst is one blurred radial layer faking an explosion — enough when a single glow pop will do. center-outward-expansion.md moves real layout elements to final resting slots; particles have no destination, only physics and a death.

How It Works

The whole event is one driver tween and one formula:

  1. Seeded setup — a fixed pool of PARTICLE_COUNT small divs is created once at composition setup (a deterministic loop — setup-time generation is fine; per-frame DOM creation is not). Each particle i derives everything from a pure hash:

    // angle, speed, size, spin, color (palette[i % palette.length]) — all from prand(i * k)
    const prand = (n) => {
      const x = Math.sin(n * 127.1 + 311.7) * 43758.5453;
      return x - Math.floor(x); // 0..1, pure function of n
    };
    
  2. Ballistic formula — a proxy tween advances T: 0 → 1 over FLIGHT_DUR with ease: "none"; onUpdate positions every particle as a pure function of T:

    x(T) = vx · T·FLIGHT_DUR
    y(T) = vy · T·FLIGHT_DUR + ½ · G · (T·FLIGHT_DUR)²
    rot(T) = spin · T·FLIGHT_DUR
    

    Gravity G supplies the rise-decelerate-fall arc for free. Because position is computed from T (never accumulated per frame), a seek to any moment renders the exact mid-flight state — this is what makes DOM particles seek-safe. The driver's ease: "none" is load-bearing: the physics lives in the formula; an eased driver warps gravity and the arc stops reading as thrown objects.

  3. Death — an opacity tail inside the same formula (fade over the last FADE_FRAC of flight), or the confetti signature: a separate instant-shrink tween scaling the pool to 0 in a blink at flight end. Either way the particles end invisible and stay invisible.

Recipe

<!-- inside a standard scene clip (frames-core) -->
<div class="burst-stage">
  <div class="particle-field" id="particle-field"></div>
  <div class="burst-hero" id="burst-hero">{heroWord}</div>
</div>
/* .burst-stage: position: relative; display: grid; place-items: center.
   .burst-hero: z-index: 2 — particles fly BEHIND the word. */
.particle-field {
  position: absolute;
  z-index: 1;
  left: 50%;
  top: 50%; /* the launch origin — offset to taste (e.g. the word's baseline) */
  width: 0;
  height: 0;
}
.particle {
  position: absolute;
  left: 0;
  top: 0;
  border-radius: 2px; /* confetti chip; 50% for dots */
  opacity: 0; /* invisible until the event fires */
  will-change: transform, opacity;
}
// Setup: deterministic pool, generated ONCE.
const field = document.getElementById("particle-field");
const palette = ["{accentA}", "{accentB}", "{accentC}"]; // 3-5 brand tokens
const parts = [];
for (let i = 0; i < PARTICLE_COUNT; i++) {
  const el = document.createElement("div");
  el.className = "particle";
  const size = SIZE_MIN + prand(i * 3 + 1) * (SIZE_MAX - SIZE_MIN);
  el.style.width = `${size}px`;
  el.style.height = `${size * 0.7}px`; // slightly oblong = confetti chip
  el.style.background = palette[i % palette.length];
  field.appendChild(el);
  // Index-seeded launch parameters — the particle's whole life, fixed here.
  const angle = -Math.PI / 2 + (prand(i * 5 + 2) * 2 - 1) * CONE; // upward cone
  const speed = SPEED_MIN + prand(i * 7 + 3) * (SPEED_MAX - SPEED_MIN);
  parts.push({
    el,
    vx: Math.cos(angle) * speed,
    vy: Math.sin(angle) * speed, // negative = up
    spin: (prand(i * 11 + 4) * 2 - 1) * SPIN_MAX,
  });
}

// Confetti pop — one driver, pure ballistic formula.
const drive = { T: 0 };
tl.fromTo(
  drive,
  { T: 0 },
  {
    T: 1,
    duration: FLIGHT_DUR,
    ease: "none", // physics lives in the formula, not the ease
    onUpdate: () => {
      const t = drive.T * FLIGHT_DUR; // seconds of flight — pure function of T
      const fade = Math.min(1, (1 - drive.T) / FADE_FRAC); // opacity tail
      parts.forEach((p) => {
        const x = p.vx * t;
        const y = p.vy * t + 0.5 * G * t * t; // rise, stall, drift down
        p.el.style.transform = `translate(${x}px, ${y}px) rotate(${p.spin * t}deg)`;
        p.el.style.opacity = String(drive.T === 0 ? 0 : fade); // T===0 guard covers seeks before the event
      });
    },
  },
  BURST_AT,
);

Variations

  • Confetti pop, then instant-shrink — the playful signature: full burst, gravity drift, then every chip scales to 0 in a blink: FADE_FRAC near 0, plus tl.to(".particle", { scale: 0, duration: SHRINK_DUR, ease: "power2.in" }, BURST_AT + FLIGHT_DUR - SHRINK_DUR) with SHRINK_DUR 0.15–0.25s. Keep the whole event tiny relative to the subject — a garnish measured in a few dozen pixels, not a screen-filling cannon.
  • Dot burst behind a landing word — radial instead of a cone: angle = prand(i) * Math.PI * 2, G near 0, short flight (0.4–0.7s), round dots (border-radius: 50%), pool z-indexed behind the word. Fire at the word's settle frame.
  • Glyph dissolve — seed each particle's origin across the glyph block's box (ox = (prand(i*13) - 0.5) * BLOCK_W, same for oy, added inside the transform), gentle outward drift with low G; text fades out over the first ~30% of flight while particles fade in from its silhouette. Color every particle {textColor} so the swarm reads as the text's own material. (True per-pixel dissolves are Canvas-2D territory — techniques.md; this DOM version sells it up to ~40 particles.)
  • Two-stage burst (pop + stragglers) — split the pool: 70% on the main driver, 30% on a second driver ~0.12s later with lower speeds; the split is index-derived (i % 10 < 3). Same formula, two windows.

Values

token range notes
PARTICLE_COUNT 10–18 pop/dots; 24–40 dissolve cap ~40 — per-frame style writes; past that, seek perf and register degrade
G 900–1600 px/s² confetti; 0–200 dots/dissolve natural fall vs drift
SPEED_MIN / SPEED_MAX 250–700 px/s per-particle via prand, never uniform
CONE 0.35–0.8 rad (~20–45°) wider = splash, narrower = fountain
FLIGHT_DUR 0.7–1.4s arc should peak ~35–45% of flight: check ` vy / G ≈ 0.4 × FLIGHT_DUR`
SIZE_MIN / SIZE_MAX 5–14px chips; 4–8px dots on a 1080p frame
SPIN_MAX 180–720 deg/s confetti; 0 dots tumble
FADE_FRAC 0.2–0.35 near 0 when using instant-shrink
BURST_AT on a cause the word's settle, a click, a lockup completing — an uncaused burst is noise

Critical Constraints

  • Position is a pure function of time, driver ease "none" — x(T), y(T), rot(T) computed from the driver value every frame, never accumulated (+=) per tick (accumulation breaks the moment the renderer seeks); gravity is the ease — an eased driver bends the parabola.
  • Fixed pool, no per-frame DOM — all particles exist after setup with opacity: 0; the event only writes transform / opacity. PARTICLE_COUNT ≤ ~40 — per-frame style writes scale linearly; keep the event cheap.
  • Particles start AND end at opacity: 0 — the drive.T === 0 guard covers seeks to before the event; the tail/shrink covers after. A chip frozen mid-air at driver end is a bug every subsequent frame.
  • Particles are punctuation — one event per beat, fired on a cause, small relative to the subject, dead before the next beat; z-ordered behind or around the word it celebrates, never over it. A persistent particle system is a background, and that's not this rule.

See also

spring-pop-entrance (confetti fires on the hero's settle frame) · kinetic-beat-slam (one beat earns the confetti payoff) · press-release-spring (single-layer glow alternative, or compose both) · css-marker-patterns (drawn-line burst when the accent should feel hand-annotated) · scale-swap-transition (glyph dissolve covers the exit).