variable-poisson
v0.1.1
Published
A TypeScript library for generating Poisson disk sampled points
Maintainers
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-poissonUsage
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 identifierradius(number): Radius of the item in world units
config(PoissonConfig): Configuration object:width(number): Width of the sampling areaheight(number): Height of the sampling areaseed(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 withxandycoordinatesunplaced(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); // ≈ 400rectRadiusWithPadding(width, height, padding)
Computes radius with multiplicative padding (e.g., 1.1 = +10%).
rectRadiusWithPadding(640, 480, 1.1); // ≈ 440rectRadiusWithGap(width, height, gap)
Computes radius with a fixed pixel gap added.
rectRadiusWithGap(640, 480, 16); // ≈ 416adaptiveRectRadius(width, height, padding, gap)
Combines padding and gap: (rectRadius * padding) + gap.
adaptiveRectRadius(640, 480, 1.1, 16); // ≈ 456How It Works
- Sorting: Items are sorted by radius (largest first) to maximize placement success
- Grid Acceleration: Uses a spatial grid to quickly check for collisions
- Anchor-Based Placement: 70% of attempts use existing placed items as anchors, creating natural clustering
- 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 typecheckLicense
MIT
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
