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

stipple-gl

v0.2.0

Published

GPU-rendered WebGL2 particle field that morphs into any SVG. Zero dependencies, framework agnostic, with a first-class React binding.

Readme

stipple-gl

A WebGL2 particle field that morphs into any SVG.

A particle field dispersing, gathering into a hexagon, morphing into a spirograph, and dispersing again

Playground · Documentation · Examples

Zero dependencies. Framework agnostic. 12.4 KB gzipped for a particle background, 17.9 KB with SVG morphing, raster image sources and every behaviour.

Optionally runs the whole simulation on a Web Worker, so it costs the main thread nothing.

npm i stipple-gl
import { Stipple, shapeFromURL } from 'stipple-gl';

const stipple = new Stipple('#hero');

await stipple.morphTo(await shapeFromURL('/shield.svg'));

That's the entire API surface for the common case. Everything else is opt-in.


Why this exists

Most particle libraries give you drifting dots. This one gives you a field with two states — a dispersed 3D sphere and a target shape — and a single scalar that moves between them. Drive that scalar from a route change, a scroll position, a hover, a promise resolving, or a slider, and the particles reassemble into whatever SVG you hand it.

It renders in a single draw call with no scene graph, no three.js, and no runtime dependencies.

| | stipple-gl | tsparticles | particles.js | | -------------------- | -------------------------------- | ----------------------- | ------------ | | gzipped | 12.4 KB lite · 17.9 KB full | 22 KB core · 52 KB full | 8.8 KB | | dependencies | 0 | 14 direct (52 packages) | 0 | | renderer | WebGL2 | canvas 2D | canvas 2D | | off-main-thread | yes, OffscreenCanvas | no | no | | SVG morphing | built in | no | no | | raster image sources | PNG, JPEG, WebP, AVIF, video | no | no | | written in | TypeScript | TypeScript | JavaScript | | status | active | active | unmaintained |

Measured with gzip on the published bundles: [email protected] (tsparticles.bundle.min.js and @tsparticles/engine), and [email protected] (ships unminified). Reproduce with node scripts/measure.mjs.

To be straight about it: particles.js is smaller. It is also canvas 2D, unmaintained since 2016, and cannot morph. stipple-gl is not trying to be the smallest particle library — it is trying to be the smallest one that renders on the GPU and morphs into arbitrary SVG geometry. If all you need is drifting dots and every kilobyte counts, the older libraries are a reasonable answer.


The three layers

Every instance runs up to three particle populations, each independently configurable:

  • major — the morph pool. Laid out on a Fibonacci sphere when dispersed, mapped onto sampled SVG points when shaped.
  • minor — an ambient drift layer that never morphs. Value-noise flow field with wrap-around. This is the layer you keep if you just want a nice background.
  • emission — short-lived sparks emitted from major particles, with spiral forces and life curves.

Set count: 0 and you have a pure ambient particle background. Set minorCount: 0 and you have a clean morph target. Both, and you get the full effect.


Quick starts

Vanilla

import { Stipple, shapeFromString } from 'stipple-gl';

const stipple = new Stipple(document.body, {
  count: 4000,
  color: '#5ec8f2',
  mode: 'background',
});

const shield = shapeFromString(`<svg viewBox="0 0 100 100">
  <path d="M50 6 L86 20 V48 C86 70 70 86 50 94 C30 86 14 70 14 48 V20 Z" />
</svg>`);

stipple.setShape(shield);
stipple.setMorph(1);

React

import { Particles } from 'stipple-gl/react';

export function Hero() {
  return <Particles mode="background" count={4000} shape="/shield.svg" morph={1} />;
}

The component owns the canvas and the lifecycle. Change shape or morph and it transitions.

Inside a container

<Particles
  mode="container"
  className="h-96 w-full rounded-xl"
  count={1800}
  shape="/logo.svg"
  morph={1}
/>

mode: 'container' scopes the canvas and its pointer listeners to the host element, so you can run several instances on one page without them fighting.

Just particles, no morphing

import { Particles } from 'stipple-gl/react';
import { starfield } from 'stipple-gl/presets';

<Particles {...starfield} color="#8ab4f8" />;

Off the main thread

import { createWorkerStipple } from 'stipple-gl/worker';

const stipple = createWorkerStipple('#hero', { count: 8000 });

The canvas is transferred to a Web Worker with transferControlToOffscreen(), so simulation and rendering never touch the main thread. Blocking the main thread for 900 ms, a transition kept advancing at 60 fps in worker mode and rendered nothing at all on the main thread. Same API either way — see docs/worker.md.

Scroll-driven morphing

<section data-stipple-shape="brain">…</section>
<section data-stipple-shape="gear">…</section>
<section data-stipple-shape="none">…</section>
import { createScrollMorph } from 'stipple-gl/scroll';

createScrollMorph(stipple, {
  sections: '[data-stipple-shape]',
  shapes: {
    brain: '/shapes/brain.svg',
    gear: '/shapes/gear.svg',
    none: null,
  },
});

Particles disperse as a section leaves the viewport and reassemble into the next section's shape. It reads scroll position rather than hijacking it, so it composes with native CSS scroll-snap and does not break keyboard or accessibility behaviour. See docs/scroll.md.


Three ways to import

| entry | gzipped | what you get | | ------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | stipple-gl/lite | 12.4 KB | The engine and the ambient layer. No SVG parser, no morph sampling, no emission, pointer or shockwave behaviours. For a particle background. | | stipple-gl | 17.9 KB | Everything: SVG parsing and sampling, raster image sources, angular assignment, all behaviours. | | stipple-gl/worker | +2 KB | The main-thread proxy. The engine itself ships in the worker chunk. |

Those are what a real import costs, measured on a production bundle — stipple-gl is import { Stipple, shapeFromURL }. Take only Stipple and it is 16.0 KB. Importing the whole barrel is 19.6 KB, which nobody does.

stipple-gl/react, stipple-gl/scroll and stipple-gl/presets are separate entries too, so you only pay for what you import.

Any image, not just SVG

Raster decoding goes through the browser, so whatever it can decode, this can sample — PNG, JPEG, WebP, AVIF, GIF, a canvas, even a video frame.

import { shapeFromFile, shapeFromImageURL } from 'stipple-gl';

await stipple.morphTo(await shapeFromImageURL('/logo.png'));
await stipple.morphTo(await shapeFromFile(droppedFile));

shapeFromFile picks the strategy. SVG is traced as vector paths, which sample cleanly at any size — unless the markup leans on gradients, filters or stylesheets, which Path2D cannot reproduce, in which case it is rasterised so the artwork survives. On the Firefox logo that is the difference between zero colours and 1,269.

A photograph has no alpha to mask with, so use luminance:

await shapeFromFile(photo, { mask: 'dark', threshold: 0.45 });

Pair any of it with color: { type: 'shape' } and the field takes the source artwork's own palette. See docs/images.md.


Drop it in a page

No bundler, no install — the script-tag build puts everything on window.stipple.

<div id="hero" style="height:100vh"></div>
<script src="https://unpkg.com/stipple-gl"></script>
<script>
  const s = stipple.createStipple('#hero');
  s.morphTo(stipple.shapeFromString(document.querySelector('#logo').outerHTML));
</script>

Transitions

A move between states is a choreography, and there are three places one can run:

new Stipple('#hero', {
  transition: {
    enter: 'condense', // spread → shape
    exit: 'mirror', // shape → spread (reuses enter, gentler)
    swap: 'burst', // shape → shape
  },
});

condense (the default), uniform, sweep and burst are shorthand for full objects you can also write out:

transition: {
  enter: { speed: 0.05, easing: "outExpo", stagger: 0.68, order: "radial", turbulence: 14 },
}

stagger and order are the wipe; flash is an optional glow on the wavefront, off by default. uniform turns the wipe off entirely. See docs/options.md.


Presets

import { presets, nebula, starfield } from 'stipple-gl/presets';

morph · snap · starfield · constellation · nebula · dust

Each is a plain config object — spread it and override whatever you want.


API

new Stipple(target, config?)

target is an element, a CSS selector, or an existing <canvas>. If you pass a canvas, stipple-gl renders into it and leaves its styling alone.

| method | description | | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | | morphTo(shape, options?) | Set a shape and morph into it. Returns a promise that resolves on arrival, or immediately if a later call supersedes it. | | release() | Return to the spread, animated. Returns the same kind of promise. | | setMorph(0…1) | Move toward dispersed (0) or shaped (1). Returns a promise for arrival. | | getMorph() | Current interpolated value. | | setShape(shape | null, choreography?) | Swap the target without touching the morph value. Returns false if the field has no major particles. | | on(event, handler) | Subscribe to morphstart, morphprogress, morphend or shapechange. Returns an unsubscribe function. | | off(event, handler) | Unsubscribe. | | setOptions(config) | Deep-merge new options at runtime. | | resetOptions(config?) | Drop every runtime tweak and rebuild from the defaults. | | setCount(count, minorCount?) | Resize the particle pools. | | pulse(x, y, strength?) | Fire a shockwave ring from a point. | | tick(dt?) | Advance one frame manually, for driving your own loop. | | start() / stop() | Control the internal loop. | | resize() | Force a re-measure. | | destroy() | Full teardown — listeners, GL objects, canvas. |

Read-only: canvas, options, running, fps.

Shape helpers

import { shapeFromURL, shapeFromString, shapeFromSVG, fitShapeToElement } from 'stipple-gl';

The parser handles path, circle, ellipse, rect (including rounded), line, polyline, polygon, nested <g> transforms, fill-rule="evenodd", and stroke-only paths. fitShapeToElement positions and scales a shape to line up with a DOM element, so the particles can assemble exactly where your layout has a gap.

Full option reference: docs/options.md.


Performance

  • Zero allocation per frame. All state lives in preallocated Float32Arrays; the vertex buffer is written in place and uploaded with bufferSubData.
  • 16-byte vertex stride — position, size, and an RGBA byte-packed colour.
  • Integer-hash value noise, no trigonometry in the hash.
  • Angular target assignment, so particles take short paths into the shape instead of scrambling across the canvas.
  • SVG sampling rasterises to a bounded bitmap and samples systematically, independent of canvas size or DPR.
  • Volume-distributed rotating sphere, so the dispersed field has depth parallax and no visible boundary.
  • Auto-pauses when scrolled offscreen or when the tab is hidden.
  • Adaptive quality reduces render resolution under frame-budget pressure.
  • Honours prefers-reduced-motion by rendering a single static frame.
  • Recovers from webglcontextlost.

Details and tuning advice: docs/performance.md.


Pluggable simulation

The simulation sits behind a SimulationBackend interface. The bundled CpuBackend runs a pipeline of small, tree-shakeable behaviours (morph, jelly, pointer, shockwave, integrate, drift, emission, breathe), each a tight loop over typed arrays.

import { createMorphBehavior, createIntegrateBehavior } from 'stipple-gl';

new Stipple('#hero', {
  behaviors: [createMorphBehavior(), createIntegrateBehavior(), myCustomBehavior],
});

A GPU backend using transform feedback is planned behind the same interface — see docs/architecture.md.


Browser support

Requires WebGL2 — Chrome/Edge 56+, Firefox 51+, Safari 15+. The constructor throws if WebGL2 is unavailable; pass onError to render your own fallback.


Documentation

These also read as a site with search at stipple-gl.vercel.app, or locally with pnpm docs:dev.

Examples

Four standalone pages, one finished scene each and no control panel. They load the script-tag build by relative path, so there is nothing to compile. Browse them at stipple-gl.vercel.app/examples, or run them locally with pnpm build then pnpm examples.

| Example | Shows | | --------------------------------------------- | ---------------------------------------------------------------- | | container.html | A field bounded by a card rather than the viewport | | positioned.html | fitShapeToElement aiming the shape at a layout slot | | scroll-snap.html | createScrollMorph, a shape declared per section | | mobile.html | A layout that reflows on its container, with a responsive budget |

Licence

MIT