Packing
256 discs of mixed sizes drift with no gravity and bounce off the walls and each other. Each disc is drawn as a slowly spinning crosshair token, and the spin reacts to the disc's own speed. This is the main example for World.particlesCollide, a pairwise disk-collision pass. Gravity is off and drag = 0, so the motion never slows down (World, Particle, collisions). The scene composition follows @eaviles's sketch 2025.037. The physics is Ollin's own.
- Run
swift run --package-path Examples Example-Physics-Packing- Imports
OllinPhysics- Source
- Examples/Physics/Packing/Sketch.swift
// Scene composition after @eaviles's sketch 2025.037 (a packed field of
// spinning crosshair tokens). The physics here is Ollin's own — an
// `OllinPhysics` Verlet world with disk collisions — not a port of that
// sketch's collision math.
import Foundation
import Ollin
import OllinPhysics
/// A field of discs drifting weightlessly, bouncing off the walls and off each
/// other, each drawn as a slowly spinning token: a translucent disk, a crosshair,
/// a crisp rim, and a center dot. The packing and the bouncing are the physics;
/// the spin reacts to each token's own speed, so a collision that speeds a token
/// up spins it faster.
///
/// This is the headline for `OllinPhysics.World` collisions: 256 disks resolved
/// pairwise every frame with `world.particlesCollide = true`, no gravity, walls at the
/// canvas edge.
@main
final class Packing: Sketch {
let world = World()
let count = 256
/// Per-token spin state, hung off each particle via `userData`.
final class Token {
var rotation: Double
let spin: Double // sign + base rate
init(rotation: Double, spin: Double) {
self.rotation = rotation
self.spin = spin
}
}
override func setup() {
world.gravity = .zero // weightless drift
world.drag = 0 // no damping — the field drifts forever
world.restitution = 1.0 // walls lose no speed
world.particlesCollide = true
world.iterations = 3 // a packing needs few passes to settle
world.bounds = bounds
noStroke()
for _ in 0 ..< count {
let radius = random(8, 32) * scale
// Find a spot that isn't already taken (reject overlaps).
guard let position = freeSpot(radius: radius) else { continue }
// Heavier discs (more area) shove lighter ones around.
let particle = world.addParticle(at: position, radius: radius,
mass: (radius / scale) * (radius / scale))
// Launch it in a random direction; `push` is a per-step displacement.
particle.push(Vector2(angle: random(.tau), length: random(0.8, 2.8) * scale))
particle.userData = Token(rotation: random(.tau),
spin: (random() < 0.5 ? -1 : 1) * random(0.5, 1.5))
}
}
/// A position whose disk doesn't overlap any placed one, or `nil` after a
/// budget of tries (so a crowded field just places fewer).
func freeSpot(radius: Double) -> Vector2? {
for _ in 0 ..< 200 {
let candidate = Vector2(random(radius, width - radius),
random(radius, height - radius))
let clear = world.particles.allSatisfy {
candidate.distance(to: $0.position) > radius + $0.radius
}
if clear { return candidate }
}
return nil
}
override func draw() {
background(.black)
world.advance(by: deltaTime)
let weight = 2 * scale
for particle in world.particles {
guard let token = particle.userData as? Token else { continue }
// Spin faster the faster the token is moving.
token.rotation += token.spin * (0.01 + particle.velocity.length * 0.012)
drawToken(particle, token: token, weight: weight)
}
}
func drawToken(_ particle: Particle, token: Token, weight: Double) {
let r = particle.radius
withState {
translate(particle.position)
// Translucent body.
noStroke()
fill(Color(white: 1, alpha: 0.33))
drawCircle(center: .zero, radius: r)
// Rotating crosshair.
rotate(token.rotation)
stroke(Color(white: 1, alpha: 0.67))
strokeWeight(weight)
drawLine(-r, 0, r, 0)
drawLine(0, -r, 0, r)
// Crisp rim and center dot.
noFill()
stroke(.white)
drawCircle(center: .zero, radius: r - 1)
noStroke()
fill(.white)
drawCircle(center: .zero, radius: 4 * scale)
}
}
}