LifeQuilt
the classic life rules stepped by hand on the CPU and drawn as a quilt of triangular wedges, colored by a radial cosine palette. It is the by-hand counterpart to the GPU rules in Automata (drawTriangle, SDF)
- Run
swift run --package-path Examples Example-Simulation-LifeQuilt- Source
- Examples/Simulation/LifeQuilt/Sketch.swift
// Ported from @eaviles's sketch 2024.026. Reworked for Ollin's API.
import Ollin
/// A quilt of triangular wedges driven by Conway's Game of Life.
///
/// Four independent life grids share the canvas. Each live cell is filled with a
/// right-isosceles wedge whose apex sits at the cell center, rotated a quarter
/// turn per layer — so the four layers tile every cell into quadrants. A wedge
/// is colored by its angle from the canvas center through a cosine palette, and
/// fades in and out with the cell's life. The grids step every 20 frames (a cell
/// is born on 3 live neighbors, survives on 2 or 3); a cell that lives past 60
/// generations dies and scatters fresh seeds, so the quilt keeps churning. Every
/// wedge is one analytic SDF triangle, so the thousands on screen stay crisp and
/// cheap.
@main
final class LifeQuilt: Sketch {
static let gridSize = 48
static let layerCount = 4
struct Cell { var alive = false; var alpha = 0.0; var gen = 0 }
/// `cells[layer][i][j]` — four overlaid life grids.
var cells: [[[Cell]]] = []
override func setup() {
seed(2_024_026)
let n = Self.gridSize
let column = Array(repeating: Cell(), count: n)
let plane = Array(repeating: column, count: n)
cells = Array(repeating: plane, count: Self.layerCount)
for layer in 0..<Self.layerCount {
for _ in 0..<(n * 2) {
let (i, j) = seedCell()
cells[layer][i][j].alive = true
cells[layer][i][j].alpha = 1
}
}
}
/// A grid index clustered near the center (a small disk around it).
private func seedCell() -> (Int, Int) {
let p = randomVector(innerRadius: 0, outerRadius: 0.25)
let i = Int(map(p.x, -1, 1, 0, Double(Self.gridSize - 1)).rounded())
let j = Int(map(p.y, -1, 1, 0, Double(Self.gridSize - 1)).rounded())
return (clampIndex(i), clampIndex(j))
}
private func clampIndex(_ v: Int) -> Int {
Swift.max(0, Swift.min(Self.gridSize - 1, v))
}
override func draw() {
background(.black)
noStroke()
let n = Self.gridSize
// The grid gives each cell's center and size; the nested loops stay,
// since they walk the four Game-of-Life state grids, not the layout.
let g = grid(columns: n, rows: n)
let side = g.cellWidth
let half = side / 2
let canvasCenter = g.bounds.center
let t = time * 0.02 // slow color drift
for layer in 0..<Self.layerCount {
let off = map(Double(layer), 0, 3, 0.8, 1.2)
for i in 0..<n {
for j in 0..<n {
let step = cells[layer][i][j].alive ? 0.01 : -0.01
let alpha = Swift.max(0, Swift.min(1, cells[layer][i][j].alpha + step))
cells[layer][i][j].alpha = alpha
guard alpha > 0 else { continue }
let c = g.cell(column: i, row: j).center
let diff = c - canvasCenter
let ci = map(atan2(diff.y, diff.x), -.pi, .pi, 0, 1)
let hue = CosinePalette.rainbow.color(at: ci * off + t)
// Fade toward black with the cell's life.
fill(Color(red: hue.red * alpha, green: hue.green * alpha, blue: hue.blue * alpha))
withState {
translate(c)
rotate(Double(layer) * .pi / 2)
drawTriangle(0, 0, side, half) // wedge: apex at the cell center
}
}
}
}
if frameCount % 20 == 0 { stepLife() }
}
/// One Conway step per layer; cells that live too long die and reseed.
private func stepLife() {
let n = Self.gridSize
for layer in 0..<Self.layerCount {
for i in 0..<n {
for j in 0..<n {
var neighbors = 0
for x in (i - 1)...(i + 1) {
for y in (j - 1)...(j + 1) {
if x == i && y == j { continue }
if x >= 0, x < n, y >= 0, y < n, cells[layer][x][y].alive { neighbors += 1 }
}
}
if neighbors < 2 || neighbors > 3 { cells[layer][i][j].alive = false }
if neighbors == 3 { cells[layer][i][j].alive = true }
if cells[layer][i][j].alive {
cells[layer][i][j].gen += 1
}
if cells[layer][i][j].gen > 60 {
cells[layer][i][j].alive = false
cells[layer][i][j].gen = 0
for _ in 0..<4 {
let (sx, sy) = seedCell()
cells[layer][sx][sy].alive = true
}
}
}
}
}
}
}