HilbertLine
a bundled profile photograph's stipple points sorted along a Hilbert curve and drawn as one line that appears and unwinds, a single sort in place of the single line's tour (hilbertSorted); at a low level the curve is drawn faintly under the line to show the order it imposes
- Run
swift run --package-path Examples Example-Images-HilbertLine- Imports
OllinSamplePhotos- Source
- Examples/Images/HilbertLine/Sketch.swift
import Ollin
import OllinSamplePhotos
/// A profile drawn with one line in the order of a space-filling curve.
///
/// The picture is stippled (dots packed by darkness, the bright ground left
/// empty), and the dots are sorted by their place along a Hilbert curve laid
/// over them (`hilbertSorted`), so one line visits every dot with each step
/// near the last. It is a single sort where `singleLine` runs a tour, so it
/// is ready the moment the stipple is, and the line reads as the picture
/// with the curve's own square turns showing through in the shading.
///
/// `level` is how fine the curve is. At 16 it is finer than a pixel and the
/// order is the dots' own. At a low level the curve is drawn faintly under
/// the line to show the order it imposes: the dots in one of its cells keep
/// their given order, so the line scribbles through each cell before moving
/// to the next, and the scribble shrinks as the level rises.
///
/// The drawing plays out as a reveal. `--export-svg out.svg --frame 720`
/// writes the whole line for a plotter.
@main
final class HilbertLine: Sketch {
@Param(1 ... 16, icon: "square.grid.3x3") var level = 16
override var loopDuration: Double? { 24 }
private var dots: [Vector2] = []
private var box = Rectangle(x: 0, y: 0, width: 1, height: 1)
private var line: [Vector2] = []
private var curve: [Vector2] = []
private var sortedAt = -1
override func setup() {
seed(4)
let frame = canvasRectangle.inset(by: 96)
let picture = SamplePhoto.profile.load().resized(width: 340, height: 340)
// Dots by darkness, and none where the plain ground is lighter than
// the cutoff, so the backdrop stays bare.
let cutoff = Color(white: 0.62).luminance
dots = stipple(count: 3600, in: frame, iterations: 45) { p in
let px = min(max(Int((p.x - frame.x) / frame.width * Double(picture.width)), 0), picture.width - 1)
let py = min(max(Int((p.y - frame.y) / frame.height * Double(picture.height)), 0), picture.height - 1)
let tone = picture[px, py].luminance
return tone < cutoff ? 1 - tone : 0
}
box = Shape(dots, closed: false).bounds ?? frame
}
override func draw() {
if level != sortedAt { sort() }
background(Color(hex: 0xF5F2EA))
guard line.count > 2 else { return }
noFill()
strokeJoin(.round)
if !curve.isEmpty {
stroke(Color(hex: 0xC9B9A2).withAlpha(0.6))
strokeWeight(1.2 * scale)
drawPolyline(curve)
}
// Draw in, hold, unwind: an eased out-and-back sweep over the loop.
let sweep = Easing.smoothstep(pingPong(over: 24))
let visible = max(2, Int(Double(line.count) * sweep))
stroke(Color(hex: 0x1A1B26))
strokeWeight(1.7 * scale)
drawPolyline(Array(line.prefix(visible)))
drawCaption(level == 16 ? "3,600 stipples, one line in Hilbert order"
: "3,600 stipples in Hilbert order, level \(level) of 16")
}
/// The dots in curve order, and the curve itself through the cells of
/// the grid over the dots' bounds when it is coarse enough to see.
private func sort() {
sortedAt = level
line = hilbertSorted(dots, level: level)
curve = []
guard level <= 6 else { return }
let n = 1 << level
var centers: [Vector2] = []
for row in 0 ..< n {
for column in 0 ..< n {
centers.append(Vector2(box.x + (Double(column) + 0.5) / Double(n) * box.width,
box.y + (Double(row) + 0.5) / Double(n) * box.height))
}
}
curve = hilbertSorted(centers, level: level)
}
}