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

@flogeez/murmuration

v0.1.4

Published

A flock of birds that draws shapes on a canvas: lands on points, circles a zone, shies away from the pointer, follows the page section by section. Plain ES modules, no build, no dependency.

Readme

murmuration

A flock of birds that draws shapes. The birds land on a list of points, settle tight on a shape's outline and sparse inside it, circle a zone above it, shy away from the pointer, and follow the page section by section. One canvas, plain ES modules, no build, no dependency.

It is the engine behind the starlings of florian-etourneau.dev. Birds are only the default: give it a fish, a leaf or a dot and it becomes a school, a gust or a swarm of fireflies. Try the demo: type a word, draw a line, change the glyph, tune the flock and copy the code.

npm i @flogeez/murmuration

A flock in a canvas

import { createMurmuration, sampleShape, fitShape, solid, hollow } from '@flogeez/murmuration';

const ring = (g) => { solid(g); g.beginPath(); g.arc(360, 320, 260, 0, 6.2832); g.fill(); hollow(g); g.beginPath(); g.arc(360, 320, 150, 0, 6.2832); g.fill(); solid(g); };

const m = createMurmuration(canvas, { frame: 'canvas', count: 320 });
m.perch(fitShape(sampleShape(ring, 288), { width: canvas.clientWidth, height: canvas.clientHeight }), {
  rest: 'circle', zone: () => ({ x: canvas.clientWidth / 2, y: 40, rx: canvas.clientWidth * 0.3, ry: 20 }),
});

m.fly() lets the flock fly freely, m.scatter() sends it away, m.setColors(colors) and m.setGlyph(glyph) restyle it, m.setCount(n) adds or removes birds (perch again to share the places out), m.destroy() stops it and removes its listeners.

A glyph is any SVG path facing right. Birds, fish and leaves work, and so does plain geometry: a dot, a square, or a triangle that travels nose first, the classic way of drawing boids.

const TRIANGLE = { path: 'M20 10 L0 2 L4 10 L0 18 Z', anchor: [8, 10], scale: 0.55, flight: 'glyph' };
m.setGlyph(TRIANGLE);

With a framework

The engine owns a canvas and nothing else, so any framework only has to create it after mounting and destroy it on unmount.

// React
function Flock({ shape }) {
  const ref = useRef(null);
  useEffect(() => {
    const canvas = ref.current, m = createMurmuration(canvas, { frame: 'canvas' });
    m.perch(fitShape(sampleShape(shape, 240), { width: canvas.clientWidth, height: canvas.clientHeight }));
    return () => m.destroy();
  }, [shape]);
  return <canvas ref={ref} style={{ width: '100%', aspectRatio: '9 / 8' }} />;
}
// Vue
onMounted(() => { m = createMurmuration(canvas.value, { frame: 'canvas' }); });
onUnmounted(() => m.destroy());

Options

createMurmuration(canvas, options):

| Option | Default | | | --- | --- | --- | | frame | 'page' | 'page': a fixed canvas over the whole page, the birds in document coordinates. 'canvas': the flock lives inside the canvas, in its own coordinates, with soft walls | | count | 170 | How many birds | | colors | two greys | The birds' colours, in turn | | glyph | PLAIN_BIRD | The perched bird: { path, anchor, scale, flight }, an SVG path facing right, the point that sits on its place, its scale, and how it travels: 'wings' (default, flapping wings) or 'glyph' (the glyph itself, turned towards its heading) | | scale | the glyph's | Overrides the glyph's scale | | debug | false | Shows the places, the swarm's zone and the current scene |

m.perch(points, options):

| Option | | | --- | --- | | rest | Birds without a place: 'fly' (default), 'double' (squeeze next to a perched one), 'circle' (in zone), 'leave' | | zone | Function returning an ellipse { x, y, rx, ry } or a path { path: [[x, y], …] } where the swarm circles | | keep | With 'circle': how many birds circle; the others leave | | anchor | Function returning the origin [x, y] of the points: the shape follows a moving element | | shy | Perched birds fly off when the pointer comes close, then land again | | instant | Land at once | | centred | Each bird's middle sits on its point, rather than its feet (the glyph's anchor): for a flock that fills a shape drawn underneath, exactly |

prefers-reduced-motion is honoured: the birds are placed without moving.

Shapes

A shape is a function that draws on a 720 × 640 canvas: fill the matter (solid), hollow out the holes (hollow). sampleShape(draw, count) turns it into about count landing points, cached per shape; fitShape(points, { width, height }, { zoom, dx, dy }) centres them in a box. A shape that changes over time gives its cache key, as draw.key() or the { key } option.

A page in scenes

import { createScenes } from '@flogeez/murmuration';

const scenes = createScenes(m, [
  { name: 'hero', active: () => scrollY < 300, pose: ({ first }) => ({ points: wirePoints(), instant: first, rest: 'circle', zone: heroOrbit }) },
  { name: 'story', active: () => inView(story), variant: () => step.dataset.shape, pose: ({ variant }) => ({ points: shapePoints(variant), anchor: origin }) },
]);
addEventListener('scroll', () => requestAnimationFrame(() => scenes.update()), { passive: true });

The first active scene wins; no active scene, the flock flies freely. variant() re-lands the flock when a scene changes without scrolling; update(true) lands it again after a resize.

animate(update) is a shared animation loop for anything else that moves: update(dt) returns false to stop.

Demo

npx serve ., then open demo/.

License

MIT, see LICENSE.