npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zakkster/lite-rain

v1.0.0

Published

Zero-GC, SoA environmental rain engine with Z-depth parallax, streak physics, splash metamorphosis, terminal velocity bounds, and precomputed render buckets for high-performance cinematic rain effects.

Readme

@zakkster/lite-rain

npm version npm bundle size npm downloads npm total downloads TypeScript Dependencies License: MIT

Zero-GC SoA environmental rain engine with Z-depth parallax, streak-to-splash metamorphosis, and bucketed rendering. One dependency. 170 lines.

Live Demo

https://cdpn.io/pen/debug/GgjyqWO

Why lite-rain?

| Feature | lite-rain | weatherJS | tsparticles | p5.js | |---|---|---|---|---| | Zero-GC hot path | Yes | No | No | No | | SoA flat arrays | 12 arrays | No | No | No | | Z-depth parallax | Yes (0.2–1.0) | No | No | Manual | | Splash metamorphosis | Yes | No | No | No | | Bucketed rendering | 3 tiers, 1 stroke() each | N/A | No | No | | Terminal velocity | Depth-scaled | No | No | No | | Responsive density | Area-aware | No | Partial | No | | OKLCH color | Yes | No | No | No | | Bundle size | < 2KB | ~10KB | ~40KB | ~800KB |

Installation

npm install @zakkster/lite-rain

Quick Start

import { RainEngine } from '@zakkster/lite-rain';

const canvas = document.getElementById('stage');
const ctx = canvas.getContext('2d');
const rain = new RainEngine(8000);

let w = canvas.width, h = canvas.height;
let last = performance.now();

function loop(time) {
    const dt = Math.min((time - last) / 1000, 0.1);
    last = time;

    rain.spawn(dt, w, h);           // 1. spawn new drops

    ctx.clearRect(0, 0, w, h);      // 2. clear (caller responsibility)
    // drawScene();                  // 3. draw your scene underneath

    rain.updateAndDraw(ctx, dt, w, h); // 4. rain overlays on top
    requestAnimationFrame(loop);
}

requestAnimationFrame(loop);

Important: updateAndDraw() does not clear the canvas. Rain is an overlay effect — it renders on top of whatever you've drawn. Call ctx.clearRect() and draw your scene before calling updateAndDraw().


The Rain Pipeline

Phase 1: Streak (state = 1)

Raindrops spawn above the viewport with a random Z-depth (0.2–1.0). Every physics parameter scales by Z:

| Property | Formula | Effect | |---|---|---| | Gravity acceleration | gravity × z | Far drops fall slower | | Wind acceleration | wind × z | Far drops drift less | | Streak opacity | z × 0.6 | Far drops are faint | | Streak width | z × 2.0 | Far drops are thin | | Tail length | blurStrength × z | Far drops have shorter streaks | | Terminal velocity | maxSpeed × z | Far drops can't exceed their depth-scaled limit |

All Z-dependent values (gz[], wz[], tailMult[], bucket[]) are precomputed at spawn — the render loop does zero multiplication for these.

Phase 2: Splash (state = 2)

When a streak hits the floor (y >= h), it metamorphoses into a splash particle:

  • Radius computed from impact velocity: z × (1.2 + |vy| / 2000) — faster hits = bigger splashes
  • Bounce velocity: vy × -splashBounce × random — slight upward kick
  • Spread velocity: random horizontal from -splashSpread*z to +splashSpread*z
  • Lifetime: splashLifeMin to splashLifeMax (0.1–0.3s default)
  • Alpha fades by life / splashLifeMax — computed via precomputed invSplashLifeMax (one division per frame, not per particle)

Splashes render as filled circles with depth-modulated alpha.

Bucketed Rendering

Instead of setting globalAlpha and lineWidth per-particle (which forces canvas state changes), drops are sorted into 3 depth tiers at spawn:

| Bucket | Z Range | Opacity | Width | |---|---|---|---| | 0 (far) | 0.2–0.4 | 0.18 | 0.6px | | 1 (mid) | 0.4–0.7 | 0.33 | 1.1px | | 2 (near) | 0.7–1.0 | 0.54 | 1.8px |

Each bucket renders in one batched ctx.stroke() call — 3 draw calls total for all 8,000 streaks, instead of 8,000 individual strokes. This is the single biggest performance win.


Full Config Reference

All config values are live-mutable between frames.

| Option | Type | Default | Description | |---|---|---|---| | gravity | number | 1500 | Downward acceleration (px/s²). Rain is heavy. | | wind | number | 200 | Horizontal wind (px/s). Positive = right, negative = left. | | density | number | 5.0 | Spawn rate multiplier. Scales with canvas area automatically. | | maxSpeed | number | 2500 | Terminal velocity cap (px/s). Depth-scaled per drop. | | blurStrength | number | 0.04 | Velocity streak tail multiplier. Higher = longer streaks. | | splashBounce | number | 0.25 | Splash bounce energy retention (0–1). | | splashSpread | number | 200 | Splash horizontal spread (px). | | splashLifeMin | number | 0.1 | Minimum splash lifetime (seconds). | | splashLifeMax | number | 0.3 | Maximum splash lifetime (seconds). | | angle | number | null | null | Fixed rain angle (radians). null = natural gravity + wind. | | color | OklchColor | string | 'oklch(0.95 0.05 250)' | Rain color. Pre-parsed at construction. | | rng | Function | Math.random | RNG function. Inject seeded RNG for determinism. |


Canvas Setup (No Built-in Resize — By Design)

lite-rain is a pure engine. Here's the recommended canvas setup:

import { RainEngine } from '@zakkster/lite-rain';

const canvas = document.getElementById('stage');
const ctx = canvas.getContext('2d');
const rain = new RainEngine();

let w = 0, h = 0;
const dpr = window.devicePixelRatio || 1;

function updateSize() {
    w = canvas.clientWidth || window.innerWidth;
    h = canvas.clientHeight || window.innerHeight;
    canvas.width = w * dpr;
    canvas.height = h * dpr;
    ctx.setTransform(1, 0, 0, 1, 0, 0);
    ctx.scale(dpr, dpr);
}

let scheduled = false;
new ResizeObserver(() => {
    if (!scheduled) {
        scheduled = true;
        requestAnimationFrame(() => { scheduled = false; updateSize(); });
    }
}).observe(canvas.parentElement || document.body);

updateSize();

let last = performance.now();
function loop(time) {
    const dt = Math.min((time - last) / 1000, 0.1);
    last = time;

    rain.spawn(dt, w, h);
    ctx.fillStyle = '#0a0a14';
    ctx.fillRect(0, 0, w, h);        // dark sky background
    rain.updateAndDraw(ctx, dt, w, h);

    requestAnimationFrame(loop);
}
requestAnimationFrame(loop);

Seeded Random (Deterministic Replays)

import { RainEngine } from '@zakkster/lite-rain';
import { Random } from '@zakkster/lite-random';

const rng = new Random(42);
const rain = new RainEngine(8000, { rng: () => rng.next() });

// Same seed + same frame sequence = identical rain pattern

Recipes

Light, slow rain with minimal wind.

const rain = new RainEngine(4000, {
    gravity: 800,
    wind: 50,
    density: 2.0,
    blurStrength: 0.02,
    splashBounce: 0.1,
});

Heavy rain, strong wind, high density. Pair with lite-fireworks for lightning.

const rain = new RainEngine(12000, {
    gravity: 2000,
    wind: 500,
    density: 10.0,
    blurStrength: 0.06,
    maxSpeed: 3000,
    splashBounce: 0.4,
    splashSpread: 300,
});

Fixed-angle rain for dramatic diagonal streaks.

const rain = new RainEngine(6000, {
    angle: Math.PI * 0.6,  // ~108° — steep diagonal
    density: 6.0,
    blurStrength: 0.05,
    color: { l: 0.85, c: 0.03, h: 220 }, // icy blue-grey
});

Rain as a composited layer over your game scene.

const rain = new RainEngine(8000);

function gameLoop(dt) {
    rain.spawn(dt, w, h);

    ctx.clearRect(0, 0, w, h);
    drawBackground();
    drawCharacters();
    drawUI();

    // Rain overlays everything
    rain.updateAndDraw(ctx, dt, w, h);
}

All config values are live-mutable. Perfect for real-time weather systems.

windSlider.oninput = () => rain.config.wind = +windSlider.value;
densitySlider.oninput = () => rain.config.density = +densitySlider.value;
gravitySlider.oninput = () => rain.config.gravity = +gravitySlider.value;

// Ramp wind over time for storm effect
let windTarget = 0;
setInterval(() => {
    windTarget = (Math.random() - 0.3) * 600;
}, 3000);
// In loop: rain.config.wind += (windTarget - rain.config.wind) * dt * 2;
// Blood rain
const blood = new RainEngine(6000, {
    color: { l: 0.4, c: 0.25, h: 20 },
    gravity: 1200,
});

// Acid rain
const acid = new RainEngine(6000, {
    color: { l: 0.7, c: 0.3, h: 130 },
});

// Neon cyberpunk
const neon = new RainEngine(8000, {
    color: { l: 0.6, c: 0.25, h: 280 },
    wind: 400,
    blurStrength: 0.07,
});

All three engines share the same (ctx, dt, w, h) API pattern.

import { RainEngine } from '@zakkster/lite-rain';
import { FireworksEngine } from '@zakkster/lite-fireworks';
import { SparkEngine } from '@zakkster/lite-sparks';

const rain = new RainEngine(8000);
const fireworks = new FireworksEngine(5000);
const sparks = new SparkEngine(3000);

function loop(time) {
    const dt = /* ... */;

    rain.spawn(dt, w, h);

    // Fireworks draw their own background (bloom mode)
    fireworks.updateAndDraw(ctx, dt, w, h);

    // Rain overlays on top
    rain.updateAndDraw(ctx, dt, w, h);

    // Sparks on click
    sparks.updateAndDraw(ctx, dt, w, h);
}

API

new RainEngine(maxParticles?, config?)

Creates a rain engine with a pre-allocated SoA particle pool.

| Parameter | Type | Default | Description | |---|---|---|---| | maxParticles | number | 8000 | Pool capacity. Shared between streaks and splashes. | | config | RainConfig | see above | All options. Live-mutable after construction. |

Methods

| Method | Description | |---|---| | .spawn(dt, w, h) | Spawn new drops. Call every frame. Count auto-scales with area × density. | | .updateAndDraw(ctx, dt, w, h) | Physics + render. Does not clear canvas. Call after spawn(). | | .clear() | Kill all particles immediately. | | .destroy() | Null all 12 typed arrays. Idempotent. |

Frame Loop Pattern

rain.spawn(dt, w, h);               // 1. spawn new drops
ctx.clearRect(0, 0, w, h);          // 2. clear canvas (caller)
drawYourScene();                     // 3. your scene (caller)
rain.updateAndDraw(ctx, dt, w, h);   // 4. rain on top

License

MIT

Part of the @zakkster ecosystem

Zero-GC, deterministic, tree-shakeable micro-libraries for high-performance web presentation.