Places
a survey drawn from a JSON document: nested objects reached by name, index pairs looped over as paths, and a station with no color that gets a fallback value without a check
- Run
swift run --package-path Examples Example-Data-Places- Beside it
- places.json
- Source
- Examples/Data/Places/Sketch.swift
import Ollin
/// A drawing built out of a JSON document.
///
/// `loadJSON` reads the file once in `setup()` and hands back a value you reach
/// through by name and by index. Nothing is decoded into a type first, so the
/// shape of the document is the shape of the code: `point["at"]["x"]` walks the
/// nesting, `paths.array` loops the pairs, and `.number` / `.text` / `.color`
/// ask for the kind you want at the end of the walk.
///
/// A key that isn't there answers null rather than stopping, which is what makes
/// the walk safe to write in one line. One station in the file carries no
/// `tint`; it draws in the fallback color instead of needing a check at every
/// step, and nothing else in the sketch has to know it is missing. Cairn is the
/// one: it is the gray station.
///
/// The survey is invented. What is worth copying is the reading.
@main
final class Places: Sketch {
private var document: JSON?
/// A slow drift so the stations breathe rather than sitting still.
private var pulse: Double { (sin(time * 0.6) + 1) / 2 }
override func setup() {
document = try? loadJSON(resource: "places", withExtension: "json", in: .module)
}
override func draw() {
background(Color(hex: 0x101317))
guard let document else {
return drawStatus("places.json could not be read", style: .warning)
}
let points = document["points"].array
guard !points.isEmpty else {
return drawStatus("places.json holds no points", style: .warning)
}
drawPaths(document["paths"].array, between: points)
for point in points { drawPoint(point) }
drawTitle(document["survey"])
drawCaption("a survey read from JSON; the paths are index pairs into the points")
}
// MARK: The drawing
/// Each path is a pair of indices into the points, so a line is drawn
/// between whichever two stations it names.
private func drawPaths(_ paths: [JSON], between points: [JSON]) {
noFill()
stroke(Color(white: 1, alpha: 0.16))
strokeWeight(1.5)
for path in paths {
guard let from = path[0].int, let to = path[1].int,
points.indices.contains(from), points.indices.contains(to) else { continue }
let a = position(of: points[from])
let b = position(of: points[to])
drawLine(a, b)
}
}
private func drawPoint(_ point: JSON) {
let at = position(of: point)
let weight = point["weight"].number ?? 0.5
// The station with no tint in the file lands here, and that is the
// whole handling it needs.
let tint = point["tint"].color ?? Color(white: 0.55)
let radius = 10 + weight * 34
noStroke()
fill(tint.withAlpha(0.14))
drawCircle(center: at, radius: radius + 10 + pulse * weight * 14)
fill(tint)
drawCircle(center: at, radius: radius)
fill(.white)
textFont(OutlineFont.systemMedium)
textSize(15 * scale)
textAlign(.center, .top)
drawText(point["name"].text ?? "", at.x, at.y + radius + 14)
}
private func drawTitle(_ survey: JSON) {
noStroke()
fill(.white)
textFont(OutlineFont.system)
textSize(30 * scale)
textAlign(.left, .top)
drawText(survey.title.text ?? "", 96, 88)
fill(Color(white: 0.45))
textFont(OutlineFont.systemMedium)
textSize(14 * scale)
drawText(survey.subtitle.text ?? "", 96, 134)
}
// MARK: Mapping
/// The file stores positions as fractions of the canvas, so the drawing
/// fits whatever size the sketch runs at.
private func position(of point: JSON) -> Vector2 {
let inset = bounds.inset(by: .all(190))
return inset.point(u: point["at"]["x"].number ?? 0.5,
v: point["at"]["y"].number ?? 0.5)
}
}