Channel Breakdown
Every pixel in the packed texture encodes spatial data instead of color. A shader reads this data to reconstruct each circle's position, identity, and shape at runtime.
Red — U Coordinate
Stores each pixel's horizontal position within its circle, from 0 (left edge) to 1 (right edge). This gives each circle its own local X axis, so a shader can map a texture across it horizontally.
Green — V Coordinate
Stores each pixel's vertical position within its circle, from 0 (top edge) to 1 (bottom edge). Combined with U, this gives every circle a full UV coordinate system for texture mapping.
Blue — Unique ID
A random value (0–1) unique to each circle. Shaders use this to give each circle its own color, animation timing, texture offset, or any other per-instance variation — all from a single number.
Alpha — Radial Gradient
A gradient from 1.0 at the circle center to 0.0 at the edge. This defines each circle's shape and size. It's used for masking, glow falloff, pulsing animations, and soft edges via smoothstep.
How Circle Packing Works
The algorithm uses a greedy placement strategy with growth:
- Placement: For each new circle, the algorithm picks a random position and checks it against all existing circles (including tiled copies at the edges). If there's no overlap, the spot is valid. Otherwise it retries up to the "Placement Density" number of times.
- Growth: Once placed, the circle starts at the minimum radius and grows one pixel at a time until it either collides with a neighbor or reaches its target size (influenced by Size Bias). A 1px gap is enforced between circles so they never quite touch.
- Size Bias: Controls the size distribution. Values above 0.5 favor placing large circles first (creating a natural mix of big and small), while values below 0.5 favor small circles.
Seamless Tiling
Every circle is checked against 9 copies of the canvas (the center tile plus 8 neighbors in every direction). Circles that cross an edge wrap around and appear on the opposite side. This means the texture repeats perfectly in all directions with no visible seams — ideal for infinite surfaces, scrolling backgrounds, or large-scale particle fields.
Using This Texture in a Game Engine
Download the packed RGBA texture and sample it in any shader. Here's the general idea in HLSL/ShaderLab (Unity) or GLSL:
vec4 data = texture(packed_texture, world_uv);
vec2 circle_uv = data.rg;
float id = data.b;
float gradient = data.a;
vec3 color = hsv2rgb(vec3(id, 0.8, 1.0));
float pulse = sin(time + id * 6.28) * 0.5 + 0.5;
float mask = smoothstep(0.0, 0.05, gradient - (1.0 - pulse));
vec4 sprite = texture(sprite_texture, circle_uv);
Because each circle carries its own UV space, you can map any image onto every circle individually. The live preview above demonstrates this with animated colored pulses, but in a real engine you could render eyeballs, bubbles, raindrops, sparks, or any sprite — thousands of them — from a single texture lookup with no instancing or draw call overhead.
Controls Guide
- Total Circles: How many circles the algorithm attempts to place. More circles = denser packing, but diminishing returns as space fills up.
- Min / Max Radius: The size range for circles in pixels. A wide gap between these produces varied, organic-looking layouts.
- Placement Density: How many random positions to try for each circle before giving up. Higher values pack circles more tightly but generation takes longer.
- Size Bias: Controls whether the algorithm favors large circles (>0.5) or small circles (<0.5). At 0.8 (default), big circles are placed first and small ones fill the gaps.
- Seed: A number that makes the random layout reproducible. Same seed = same layout every time. Set to 0 for a new random seed each generation.
- Texture Size: Resolution of the output. 1024x1024 is a good balance. 2048x2048 gives more detail for large displays but generates slower.