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

@solitti/field

v0.3.0

Published

Particle field on canvas. Modes decide position authority, drivers are summed forces.

Readme

@solitti/field

Particle field on canvas: physics-based motion with a composable driver model. Zero framework dependencies.

Installation

Packages are published to GitHub Packages (private). Add a .npmrc to your project:

@solitti:registry=https://npm.pkg.github.com

Then install:

npm install @solitti/field

Quick start

React

Use @solitti/field-react for a one-liner hook API.

Vanilla JS: drop into a div

import { mountField } from "@solitti/field";

const { dispose } = mountField(document.querySelector("#bg"), {
  scene: "drift",
  seed: 7,
});

// tear down later
dispose();

mountField creates a <canvas> inside the target element, starts the field, and returns { field, canvas, dispose }. Pass an existing <canvas> as the target and mountField adopts it instead (dispose leaves it in the DOM).

Direct canvas control

import { Field } from "@solitti/field";

const canvas = document.querySelector("canvas");
const field = new Field(canvas, { scene: "drift", seed: 7 });

// later
field.destroy();

DRIFT presets

Ready-made configurations that reproduce maintenance.html behavior:

import { DRIFT, Field } from "@solitti/field";

// Pre-built (default params)
const field = new Field(canvas, DRIFT.SATELLITES);   // 98 orange squares
const field = new Field(canvas, DRIFT.DUST);          // 150 fine circles
const field = new Field(canvas, DRIFT.EMBER);         // 48 sparse squares

// Factories (custom count + seed)
const field = new Field(canvas, DRIFT.satellites(120, 42));
const field = new Field(canvas, DRIFT.dust(200, 13));
const field = new Field(canvas, DRIFT.ember(60, 99));

Scenes

Pass scene to pick a named preset driver stack:

| Scene | Behaviour | |-------|-----------| | "drift" | Seeded organic drift, the maintenance.html preset | | "lattice" | Spring-attracted grid | | "form" | Spring-attracted square outline | | "restore" | Grid + pointer repel | | "levy" | Lévy flight random walk | | "brownian" | Brownian motion | | "chemotaxis" | Self-aggregating swarm |

Field API

class Field {
  constructor(canvas: HTMLCanvasElement, opts?: FieldOptions)

  // Live configuration
  configure(o: ConfigInput): this       // replace drivers / particle
  retarget(o: ConfigInput): this        // keep physics state, swap target
  setParticle(p: Partial<Particle>): this
  setCount(n: number): this
  setMode(m: "dynamics" | "timeline"): this
  setProgress(p: number): this          // timeline scrub [0, 1]
  setTimeline(p: number): this          // alias for setProgress
  scatter(): this                       // randomise positions
  disturb(magnitude: number): this      // impulse kick

  // Manual frame (when constructed with { ticker: false })
  tick(dt: number): void

  // Instrumentation (read-only, observable; no driver internals)
  stats(): FieldStats
  probe(i: number, out?: ProbeResult): ProbeResult

  destroy(): void   // remove listeners, disconnect ResizeObserver, stop ticker
}

FieldOptions

All SimulationOptions plus:

| Option | Type | Default | Description | |--------|------|---------|-------------| | ticker | Ticker \| false | shared | BYO ticker or false for manual tick() | | onFps | (n: number) => void | none | FPS callback (fires every ~500 ms) |

SimulationOptions

| Option | Type | Default | Description | |--------|------|---------|-------------| | units | number | 484 | Particle count | | width | number | 800 | Canvas logical width (px) | | height | number | 600 | Canvas logical height (px) | | seed | number | 0 | RNG seed for scatter and drift | | scene | string | none | Named preset (see table above) | | mode | "dynamics" \| "timeline" | "dynamics" | Position authority | | reduced | boolean | none | Freeze motion (reduced-motion) | | particle | Partial<Particle> | none | Shape, color, size overrides | | drivers | Driver[] | none | Explicit driver list (overrides scene) | | scatterMode | "ring" \| "fill" \| "maintenance" | "ring" | Initial scatter distribution |

Backdrop contract

The field renders transparent, clearRect on every frame. Drift particles have alpha as low as 0.18, which is legible only against a dark ground. This matches maintenance.html, which supplies html { background: oklch(0.13 0.008 152) } in CSS.

When used as a transparent overlay, the host element's background serves as the ground (correct). When the field needs to be self-contained, standalone or on an unknown background, supply the ground via the background particle option:

// Self-contained field: carries its own dark ground
new Field(canvas, {
  scene: "drift",
  seed: 7,
  particle: { background: "oklch(0.13 0.008 152)" },
});

background defaults to unset (transparent). Existing overlay usage is unaffected.

Particle options

field.setParticle({
  shape: "circle",        // "square" | "circle" | "ring" | "triangle" | "diamond" | "plus"
  size: 3,                // base size (px)
  color: "#ff4d00",       // settled color
  colorRest: "rgba(255,77,0,0.18)",  // unsettled / roaming color
  spacing: 1.2,           // lattice spacing multiplier
  background: "oklch(0.13 0.008 152)",  // optional opaque ground; unset = transparent
});

Drivers

Compose motion by listing drivers explicitly. Drivers are summed: a particle's position is the result of all active drivers:

new Field(canvas, {
  drivers: [
    { type: "attract", to: "lattice", stiffness: 90, damping: 0.9 },
    { type: "repel",   from: "pointer", strength: 2400, radius: 120 },
  ],
});

| Driver type | Key options | Description | |-------------|-------------|-------------| | attract | to, stiffness, damping | Spring-pull to a geometry | | repel | from: "pointer", strength, radius | Push away from pointer | | wander | process, speed | Random walk (brownian/levy/crw/chemotaxis/flow) | | drift | seed, params | Seeded organic drift (maintenance.html physics) |

Timeline mode

mode: "timeline" makes position a pure, reversible function of progress p ∈ [0, 1]:

const field = new Field(canvas, {
  mode: "timeline",
  keyframes: [
    { at: 0,   to: "point"   },
    { at: 0.5, to: "lattice" },
    { at: 1,   to: "square"  },
  ],
});

// drive from scroll position
document.addEventListener("scroll", () => {
  field.setProgress(window.scrollY / (document.body.scrollHeight - window.innerHeight));
});

Sizing

Field reads the canvas element's bounding box via getBoundingClientRect and tracks changes with ResizeObserver. Make the canvas fill its container with CSS and it will resize automatically, no JS needed:

<div style="position:relative; width:100%; height:400px;">
  <canvas style="position:absolute; inset:0; width:100%; height:100%;"></canvas>
</div>

Reduced motion

Field checks prefers-reduced-motion at construction and freezes drift animations automatically. No handling needed in application code.

Instrumentation

Field and Simulation expose a read-only surface for debugging and live dashboards.

stats()

Returns aggregate metrics over all active particles for the last completed frame:

interface FieldStats {
  count: number;        // total particles
  activeCount: number;  // particles currently active
  speed: Range;         // px/s, wrap-suppressed
  alpha: Range;
  size:  Range;         // px (full side length)
  bounds: { minX: number; maxX: number; minY: number; maxY: number };
  wrappedCount: number; // particles that crossed a seam this frame
}

interface Range { min: number; max: number; mean: number; }

All aggregates are over active particles only. Speed is derived from successive positions with wrap suppression, a particle crossing the canvas seam contributes speed = 0 for that frame.

stats() is O(n); throttle to ≤ 10 Hz for UI use (see mountInsights and useFieldStats).

probe(i)

Per-particle read with wrap-aware velocity:

probe(i: number, out?: ProbeResult): ProbeResult

O(1). Pass an out object to avoid allocation in hot paths. Throws RangeError for out-of-range i.

Pure functions

For use outside a Field instance, for example to instrument a custom canvas loop:

import { computeStats, probeAt } from "@solitti/field";

const stats = computeStats(view, dt);   // same computation as field.stats()
const p     = probeAt(view, i, dt);     // same as field.probe(i)

Insights overlay

mountInsights mounts a default-off diagnostic card that displays live FieldStats:

import { mountField, mountInsights } from "@solitti/field";

const { field, dispose } = mountField(document.querySelector("#bg"), { scene: "drift" });
const card = mountInsights(document.body, () => field.stats());

Press i to toggle. Returns an InsightsHandle:

interface InsightsHandle {
  readonly visible: boolean;
  show(): void;
  hide(): void;
  dispose(): void;   // removes the element + key listener
}

Optional { hz } (default 4) controls the poll rate.

mountField

function mountField(target: HTMLElement, opts?: FieldOptions): FieldHandle;

interface FieldHandle {
  field: Field;
  canvas: HTMLCanvasElement;
  dispose: () => void;
}
  • Target is a <canvas> → adopted. dispose() tears down Field only; canvas stays in DOM.
  • Target is anything else → a canvas is created, appended, and styled display:block; width:100%; height:100%. dispose() also removes the canvas.
  • dispose() is idempotent.
  • Throws "mountField requires a DOM" when called server-side.