Smithery Logo
MCPsSkillsDocsPricing
Login
Smithery Logo

Accelerating the Agent Economy

Resources

DocumentationPrivacy PolicySystem Status

Company

PricingAboutBlog

Connect

© 2026 Smithery. All rights reserved.

    tanepiper

    particles-lifecycle

    tanepiper/particles-lifecycle
    Coding
    22
    2 installs

    About

    SKILL.md

    Install

    Install via Skills CLI

    or add to your agent
    • Claude Code
      Claude Code
    • Codex
      Codex
    • OpenClaw
      OpenClaw
    • Cursor
      Cursor
    • Amp
      Amp
    • GitHub Copilot
      GitHub Copilot
    • Gemini CLI
      Gemini CLI
    • Kilo Code
      Kilo Code
    • Junie
      Junie
    • Replit
      Replit
    • Windsurf
      Windsurf
    • Cline
      Cline
    • Continue
      Continue
    • OpenCode
      OpenCode
    • OpenHands
      OpenHands
    • Roo Code
      Roo Code
    • Augment
      Augment
    • Goose
      Goose
    • Trae
      Trae
    • Zencoder
      Zencoder
    • Antigravity
      Antigravity
    ├─
    ├─
    └─

    About

    Particle lifecycle management—emission/spawning, death conditions, object pooling, trails, fade-in/out, and state transitions...

    SKILL.md

    Particle Lifecycle

    Manage particle birth, life, death, and rebirth for continuous effects.

    Quick Start

    interface Particle {
      position: THREE.Vector3;
      velocity: THREE.Vector3;
      life: number; // Current life (decrements)
      maxLife: number; // Starting life
      alive: boolean;
    }
    
    // Update loop
    for (const p of particles) {
      if (!p.alive) continue;
    
      p.life -= delta;
      if (p.life <= 0) {
        p.alive = false;
        continue;
      }
    
      // Age factor (0 at birth, 1 at death)
      const age = 1 - p.life / p.maxLife;
    
      // Update position, apply fade, etc.
    }
    

    Emission Patterns

    Continuous Emission

    class ContinuousEmitter {
      private accumulator = 0;
    
      emit(
        particles: Particle[],
        rate: number, // Particles per second
        delta: number,
        spawnFn: () => Particle,
      ) {
        this.accumulator += rate * delta;
    
        while (this.accumulator >= 1) {
          this.accumulator -= 1;
    
          // Find dead particle to reuse
          const dead = particles.find((p) => !p.alive);
          if (dead) {
            Object.assign(dead, spawnFn());
            dead.alive = true;
          }
        }
      }
    }
    
    // Usage
    const emitter = new ContinuousEmitter();
    
    useFrame((_, delta) => {
      emitter.emit(particles, 100, delta, () => ({
        position: new THREE.Vector3(0, 0, 0),
        velocity: new THREE.Vector3(
          (Math.random() - 0.5) * 2,
          Math.random() * 5,
          (Math.random() - 0.5) * 2,
        ),
        life: 2 + Math.random(),
        maxLife: 2 + Math.random(),
        alive: true,
      }));
    });
    

    Burst Emission

    function emitBurst(
      particles: Particle[],
      count: number,
      origin: THREE.Vector3,
      speed: number,
      lifeRange: [number, number],
    ) {
      let emitted = 0;
    
      for (const p of particles) {
        if (emitted >= count) break;
        if (p.alive) continue;
    
        // Random direction on sphere
        const theta = Math.random() * Math.PI * 2;
        const phi = Math.acos(2 * Math.random() - 1);
    
        const dir = new THREE.Vector3(
          Math.sin(phi) * Math.cos(theta),
          Math.sin(phi) * Math.sin(theta),
          Math.cos(phi),
        );
    
        p.position.copy(origin);
        p.velocity.copy(dir).multiplyScalar(speed * (0.5 + Math.random()));
        p.maxLife = lifeRange[0] + Math.random() * (lifeRange[1] - lifeRange[0]);
        p.life = p.maxLife;
        p.alive = true;
    
        emitted++;
      }
    
      return emitted;
    }
    

    Shape Emission

    // Emit from sphere surface
    function emitFromSphere(origin: THREE.Vector3, radius: number): THREE.Vector3 {
      const theta = Math.random() * Math.PI * 2;
      const phi = Math.acos(2 * Math.random() - 1);
    
      return new THREE.Vector3(
        origin.x + radius * Math.sin(phi) * Math.cos(theta),
        origin.y + radius * Math.sin(phi) * Math.sin(theta),
        origin.z + radius * Math.cos(phi),
      );
    }
    
    // Emit from box volume
    function emitFromBox(min: THREE.Vector3, max: THREE.Vector3): THREE.Vector3 {
      return new THREE.Vector3(
        min.x + Math.random() * (max.x - min.x),
        min.y + Math.random() * (max.y - min.y),
        min.z + Math.random() * (max.z - min.z),
      );
    }
    
    // Emit from circle edge
    function emitFromCircle(
      center: THREE.Vector3,
      radius: number,
      normal: THREE.Vector3,
    ): THREE.Vector3 {
      const angle = Math.random() * Math.PI * 2;
    
      // Create perpendicular vectors
      const up =
        Math.abs(normal.y) < 0.9
          ? new THREE.Vector3(0, 1, 0)
          : new THREE.Vector3(1, 0, 0);
      const right = new THREE.Vector3().crossVectors(normal, up).normalize();
      const forward = new THREE.Vector3().crossVectors(right, normal).normalize();
    
      return new THREE.Vector3()
        .addScaledVector(right, Math.cos(angle) * radius)
        .addScaledVector(forward, Math.sin(angle) * radius)
        .add(center);
    }
    
    // Emit from cone
    function emitFromCone(
      origin: THREE.Vector3,
      direction: THREE.Vector3,
      angle: number,
      speed: number,
    ): THREE.Vector3 {
      const coneAngle = Math.random() * angle;
      const rotation = Math.random() * Math.PI * 2;
    
      const velocity = direction.clone().normalize();
    
      // Rotate around perpendicular axis
      const perpendicular = new THREE.Vector3(1, 0, 0);
      if (Math.abs(direction.x) > 0.9) perpendicular.set(0, 1, 0);
      perpendicular.cross(direction).normalize();
    
      velocity.applyAxisAngle(perpendicular, coneAngle);
      velocity.applyAxisAngle(direction, rotation);
    
      return velocity.multiplyScalar(speed);
    }
    

    Object Pooling

    Pre-allocate particles to avoid garbage collection:

    class ParticlePool {
      private particles: Particle[] = [];
      private activeCount = 0;
    
      constructor(maxCount: number) {
        for (let i = 0; i < maxCount; i++) {
          this.particles.push({
            position: new THREE.Vector3(),
            velocity: new THREE.Vector3(),
            life: 0,
            maxLife: 0,
            alive: false,
          });
        }
      }
    
      spawn(): Particle | null {
        for (const p of this.particles) {
          if (!p.alive) {
            p.alive = true;
            this.activeCount++;
            return p;
          }
        }
        return null; // Pool exhausted
      }
    
      kill(particle: Particle) {
        particle.alive = false;
        this.activeCount--;
      }
    
      update(delta: number, updateFn: (p: Particle, age: number) => void) {
        for (const p of this.particles) {
          if (!p.alive) continue;
    
          p.life -= delta;
    
          if (p.life <= 0) {
            this.kill(p);
            continue;
          }
    
          const age = 1 - p.life / p.maxLife;
          updateFn(p, age);
        }
      }
    
      forEach(fn: (p: Particle) => void) {
        for (const p of this.particles) {
          if (p.alive) fn(p);
        }
      }
    
      get active() {
        return this.activeCount;
      }
      get capacity() {
        return this.particles.length;
      }
    }
    

    GPU Pool (Buffer-Based)

    class GPUParticlePool {
      positions: Float32Array;
      velocities: Float32Array;
      lives: Float32Array;
      maxLives: Float32Array;
    
      private freeIndices: number[] = [];
    
      constructor(public count: number) {
        this.positions = new Float32Array(count * 3);
        this.velocities = new Float32Array(count * 3);
        this.lives = new Float32Array(count);
        this.maxLives = new Float32Array(count);
    
        // All indices start free
        for (let i = count - 1; i >= 0; i--) {
          this.freeIndices.push(i);
        }
      }
    
      spawn(): number {
        const index = this.freeIndices.pop();
        return index ?? -1;
      }
    
      kill(index: number) {
        this.lives[index] = 0;
        this.freeIndices.push(index);
      }
    
      setParticle(
        index: number,
        pos: THREE.Vector3,
        vel: THREE.Vector3,
        life: number,
      ) {
        this.positions[index * 3] = pos.x;
        this.positions[index * 3 + 1] = pos.y;
        this.positions[index * 3 + 2] = pos.z;
    
        this.velocities[index * 3] = vel.x;
        this.velocities[index * 3 + 1] = vel.y;
        this.velocities[index * 3 + 2] = vel.z;
    
        this.lives[index] = life;
        this.maxLives[index] = life;
      }
    
      update(delta: number) {
        for (let i = 0; i < this.count; i++) {
          if (this.lives[i] <= 0) continue;
    
          this.lives[i] -= delta;
    
          if (this.lives[i] <= 0) {
            this.freeIndices.push(i);
            continue;
          }
    
          // Update position
          this.positions[i * 3] += this.velocities[i * 3] * delta;
          this.positions[i * 3 + 1] += this.velocities[i * 3 + 1] * delta;
          this.positions[i * 3 + 2] += this.velocities[i * 3 + 2] * delta;
        }
      }
    }
    

    Fade Patterns

    Linear Fade

    // age: 0 (birth) to 1 (death)
    const alpha = 1 - age;
    

    Fade In/Out

    function fadeInOut(
      age: number,
      fadeInDuration = 0.1,
      fadeOutStart = 0.7,
    ): number {
      if (age < fadeInDuration) {
        return age / fadeInDuration; // Fade in
      } else if (age > fadeOutStart) {
        return 1 - (age - fadeOutStart) / (1 - fadeOutStart); // Fade out
      }
      return 1; // Full opacity
    }
    

    Eased Fade

    // Smooth fade out (ease-in)
    const alpha = Math.pow(1 - age, 2);
    
    // Quick fade then slow (ease-out)
    const alpha = 1 - Math.pow(age, 2);
    
    // S-curve (smoothstep)
    const alpha = 1 - age * age * (3 - 2 * age);
    

    Blink/Flash

    function blink(age: number, frequency: number): number {
      return (Math.sin(age * frequency * Math.PI * 2) + 1) * 0.5;
    }
    

    Size Over Life

    // Grow then shrink
    function sizeOverLife(age: number, maxSize: number): number {
      // Peak at 20% of life
      const peak = 0.2;
      if (age < peak) {
        return (age / peak) * maxSize;
      } else {
        return (1 - (age - peak) / (1 - peak)) * maxSize;
      }
    }
    
    // Pop in, slow shrink
    function popShrink(age: number, maxSize: number): number {
      const popDuration = 0.05;
      if (age < popDuration) {
        return maxSize; // Instant full size
      }
      return maxSize * (1 - (age - popDuration) / (1 - popDuration));
    }
    

    Color Over Life

    // Gradient from start to end color
    function colorOverLife(
      age: number,
      startColor: THREE.Color,
      endColor: THREE.Color,
    ): THREE.Color {
      return startColor.clone().lerp(endColor, age);
    }
    
    // Multi-stop gradient
    function colorGradient(
      age: number,
      stops: Array<{ pos: number; color: THREE.Color }>,
    ): THREE.Color {
      // Find surrounding stops
      let lower = stops[0];
      let upper = stops[stops.length - 1];
    
      for (let i = 0; i < stops.length - 1; i++) {
        if (age >= stops[i].pos && age <= stops[i + 1].pos) {
          lower = stops[i];
          upper = stops[i + 1];
          break;
        }
      }
    
      const t = (age - lower.pos) / (upper.pos - lower.pos);
      return lower.color.clone().lerp(upper.color, t);
    }
    
    // Usage
    const fireGradient = [
      { pos: 0, color: new THREE.Color("#ffffff") },
      { pos: 0.2, color: new THREE.Color("#ffff00") },
      { pos: 0.5, color: new THREE.Color("#ff6600") },
      { pos: 1, color: new THREE.Color("#330000") },
    ];
    

    Trails

    Position History Trail

    class TrailParticle {
      positions: THREE.Vector3[] = [];
      maxLength: number;
    
      constructor(maxLength: number) {
        this.maxLength = maxLength;
      }
    
      update(newPosition: THREE.Vector3) {
        this.positions.unshift(newPosition.clone());
    
        if (this.positions.length > this.maxLength) {
          this.positions.pop();
        }
      }
    
      getTrailGeometry(): THREE.BufferGeometry {
        const geometry = new THREE.BufferGeometry();
        const positions = new Float32Array(this.positions.length * 3);
        const alphas = new Float32Array(this.positions.length);
    
        for (let i = 0; i < this.positions.length; i++) {
          positions[i * 3] = this.positions[i].x;
          positions[i * 3 + 1] = this.positions[i].y;
          positions[i * 3 + 2] = this.positions[i].z;
    
          alphas[i] = 1 - i / this.positions.length;
        }
    
        geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
        geometry.setAttribute("alpha", new THREE.BufferAttribute(alphas, 1));
    
        return geometry;
      }
    }
    

    GPU Trail (Shader-Based)

    // Vertex shader with trail
    attribute float aTrailIndex;  // 0 = head, 1 = tail
    attribute vec3 aPrevPosition;
    attribute vec3 aNextPosition;
    
    uniform float uTrailLength;
    
    varying float vTrailAlpha;
    
    void main() {
      // Interpolate between positions based on trail index
      vec3 pos = mix(aNextPosition, aPrevPosition, aTrailIndex);
    
      // Alpha fades along trail
      vTrailAlpha = 1.0 - aTrailIndex;
    
      gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
      gl_PointSize = mix(10.0, 2.0, aTrailIndex);  // Size decreases along trail
    }
    

    Line Trail

    function TrailLine({ points, color = "#ffffff" }) {
      const geometry = useMemo(() => {
        const geo = new THREE.BufferGeometry();
        const positions = new Float32Array(points.length * 3);
    
        points.forEach((p, i) => {
          positions[i * 3] = p.x;
          positions[i * 3 + 1] = p.y;
          positions[i * 3 + 2] = p.z;
        });
    
        geo.setAttribute("position", new THREE.BufferAttribute(positions, 3));
        return geo;
      }, [points]);
    
      return (
        <line geometry={geometry}>
          <lineBasicMaterial color={color} transparent opacity={0.5} />
        </line>
      );
    }
    

    State Machines

    enum ParticleState {
      Spawning,
      Active,
      Dying,
      Dead,
    }
    
    interface StatefulParticle extends Particle {
      state: ParticleState;
      stateTime: number;
    }
    
    function updateParticleState(p: StatefulParticle, delta: number) {
      p.stateTime += delta;
    
      switch (p.state) {
        case ParticleState.Spawning:
          // Fade in over 0.2 seconds
          if (p.stateTime >= 0.2) {
            p.state = ParticleState.Active;
            p.stateTime = 0;
          }
          break;
    
        case ParticleState.Active:
          p.life -= delta;
          if (p.life <= 0.5) {
            // Start dying when 0.5s left
            p.state = ParticleState.Dying;
            p.stateTime = 0;
          }
          break;
    
        case ParticleState.Dying:
          p.life -= delta;
          if (p.life <= 0) {
            p.state = ParticleState.Dead;
            p.alive = false;
          }
          break;
      }
    }
    
    function getParticleAlpha(p: StatefulParticle): number {
      switch (p.state) {
        case ParticleState.Spawning:
          return p.stateTime / 0.2;
        case ParticleState.Active:
          return 1;
        case ParticleState.Dying:
          return p.life / 0.5;
        default:
          return 0;
      }
    }
    

    Sub-Emitters

    Spawn particles from dying particles:

    function updateWithSubEmitter(
      particles: Particle[],
      subEmitCount: number,
      subEmitFn: (parent: Particle) => Particle,
    ) {
      const toEmit: Particle[] = [];
    
      for (const p of particles) {
        if (!p.alive) continue;
    
        p.life -= delta;
    
        if (p.life <= 0) {
          p.alive = false;
    
          // Spawn sub-particles
          for (let i = 0; i < subEmitCount; i++) {
            toEmit.push(subEmitFn(p));
          }
        }
      }
    
      // Add sub-particles to pool
      for (const sub of toEmit) {
        const dead = particles.find((p) => !p.alive);
        if (dead) {
          Object.assign(dead, sub);
        }
      }
    }
    

    File Structure

    particles-lifecycle/
    ├── SKILL.md
    ├── references/
    │   ├── emission-patterns.md   # All emission shapes
    │   └── easing-curves.md       # Fade/size curves
    └── scripts/
        ├── emitters/
        │   ├── continuous.ts      # Continuous emission
        │   ├── burst.ts           # Burst emission
        │   └── shapes.ts          # Shape emitters
        ├── pool.ts                # Object pooling
        ├── trails.ts              # Trail implementations
        └── lifecycle.ts           # Fade, size, color curves
    

    Reference

    • references/emission-patterns.md — All emission shape functions
    • references/easing-curves.md — Fade and size curve options
    Recommended Servers
    Svelte
    Svelte
    ThinAir Geo
    ThinAir Geo
    Repository
    tanepiper/teskooano
    Files