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 🙏

© 2025 – Pkg Stats / Ryan Hefner

variable-poisson

v0.1.1

Published

A TypeScript library for generating Poisson disk sampled points

Readme

Poisson Disk Sampler

A TypeScript library for generating Poisson disk sampled points with variable radii. Unlike traditional Poisson disk samplers that use a fixed minimum distance, this library allows each item to have its own radius, making it perfect for placing objects of different sizes (like images, sprites, or UI elements) without overlap.

This was built for an interactive exhibition, where images needed to be 'scattered' across an infinite canvas with roughly equal distance but an organic feel. Poisson disk sampling is a good solution for this use case, but no libraries exist that allow for dynamic radii. Alternatively something like d3-force will work as a physics-based approach, this works better for static, deterministic layouts.

Features

  • Variable Radii: Each item can have its own radius
  • Type-safe: Written in TypeScript with full type definitions
  • Zero dependencies: Lightweight and fast
  • Well tested: Comprehensive test suite
  • Configurable: Customizable sampling parameters
  • Rectangle Helpers: Utilities for computing radii from rectangular dimensions

Installation

yarn add variable-poisson

Usage

Basic Example

import { sampleVariablePoisson } from 'variable-poisson';

const items = [
  { id: 'item1', radius: 20 },
  { id: 'item2', radius: 15 },
  { id: 'item3', radius: 25 },
  { id: 'item4', radius: 10 },
];

const result = sampleVariablePoisson(items, {
  width: 800,
  height: 600,
  seed: 'my-seed', // optional
  maxAttemptsPerItem: 120, // optional, default: 120
  border: 0, // optional, default: 0
});

console.log(result.placed);
// [
//   { id: 'item3', radius: 25, x: 400, y: 300 },
//   { id: 'item1', radius: 20, x: 450, y: 350 },
//   ...
// ]

console.log(result.unplaced);
// Items that couldn't be placed (if any)

Using Rectangle Helpers

When working with rectangular objects (like images), use the radius helpers:

import {
  sampleVariablePoisson,
  rectRadius,
  rectRadiusWithPadding,
  rectRadiusWithGap,
  adaptiveRectRadius,
} from 'variable-poisson';

// Example: Place images of different sizes
const images = [
  { id: 'img1', width: 640, height: 480 },
  { id: 'img2', width: 320, height: 240 },
  { id: 'img3', width: 1280, height: 720 },
];

// Convert to items with radii
const items = images.map((img) => ({
  id: img.id,
  radius: rectRadius(img.width, img.height), // Uses half-diagonal
}));

// Or with padding for breathing room
const itemsWithPadding = images.map((img) => ({
  id: img.id,
  radius: rectRadiusWithPadding(img.width, img.height, 1.1), // +10% padding
}));

// Or with a fixed gap
const itemsWithGap = images.map((img) => ({
  id: img.id,
  radius: rectRadiusWithGap(img.width, img.height, 16), // 16px gap
}));

// Or combine both
const itemsAdaptive = images.map((img) => ({
  id: img.id,
  radius: adaptiveRectRadius(img.width, img.height, 1.1, 16),
}));

const result = sampleVariablePoisson(items, {
  width: 1920,
  height: 1080,
});

API

sampleVariablePoisson(items, config)

Generates non-overlapping positions for items with variable radii.

Parameters

  • items (PoissonItem[]): Array of items to place, each with:
    • id (string): Unique identifier
    • radius (number): Radius of the item in world units
  • config (PoissonConfig): Configuration object:
    • width (number): Width of the sampling area
    • height (number): Height of the sampling area
    • seed (string, optional): Seed for reproducible results (default: 'default-seed')
    • maxAttemptsPerItem (number, optional): Maximum placement attempts per item (default: 120)
    • border (number, optional): Border padding around the edges (default: 0)

Returns

PoissonResult object containing:

  • placed (PoissonPlacedItem[]): Successfully placed items with x and y coordinates
  • unplaced (PoissonItem[]): Items that couldn't be placed

Radius Helper Functions

rectRadius(width, height)

Returns the radius of the smallest circle that fully contains an axis-aligned rectangle (half-diagonal).

rectRadius(640, 480); // ≈ 400

rectRadiusWithPadding(width, height, padding)

Computes radius with multiplicative padding (e.g., 1.1 = +10%).

rectRadiusWithPadding(640, 480, 1.1); // ≈ 440

rectRadiusWithGap(width, height, gap)

Computes radius with a fixed pixel gap added.

rectRadiusWithGap(640, 480, 16); // ≈ 416

adaptiveRectRadius(width, height, padding, gap)

Combines padding and gap: (rectRadius * padding) + gap.

adaptiveRectRadius(640, 480, 1.1, 16); // ≈ 456

How It Works

  1. Sorting: Items are sorted by radius (largest first) to maximize placement success
  2. Grid Acceleration: Uses a spatial grid to quickly check for collisions
  3. Anchor-Based Placement: 70% of attempts use existing placed items as anchors, creating natural clustering
  4. Collision Detection: Ensures no two items overlap by checking distance >= radius1 + radius2

Examples

Placing UI Elements

const uiElements = [
  { id: 'button-large', radius: 30 },
  { id: 'button-medium', radius: 20 },
  { id: 'button-small', radius: 10 },
];

const result = sampleVariablePoisson(uiElements, {
  width: 400,
  height: 300,
  border: 10, // 10px border
});

Reproducible Layouts

const result1 = sampleVariablePoisson(items, {
  width: 800,
  height: 600,
  seed: 'layout-v1',
});

const result2 = sampleVariablePoisson(items, {
  width: 800,
  height: 600,
  seed: 'layout-v1', // Same seed = same layout
});

// result1.placed === result2.placed (same positions)

Handling Unplaced Items

const result = sampleVariablePoisson(items, {
  width: 100,
  height: 100,
  maxAttemptsPerItem: 200, // More attempts = better chance
});

if (result.unplaced.length > 0) {
  console.warn(`Could not place ${result.unplaced.length} items`);
  // Handle unplaced items (e.g., show error, resize, etc.)
}

Development

# Install dependencies
yarn install

# Build the library
yarn build

# Run tests
yarn test

# Run tests in watch mode (for development)
yarn test:watch

# Run tests with coverage
yarn test:coverage

# Lint code
yarn lint

# Format code
yarn format

# Type check
yarn typecheck

License

MIT

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.