Epicycles
Fourier epicycles: a chain of spinning circles redraws an SVG whale, the term count is a parameter, and one lap takes one loopDuration
- Run
swift run --package-path Examples Example-Motion-Epicycles- Beside it
- whale.svg
- Source
- Examples/Motion/Epicycles/Sketch.swift
import Ollin
/// Fourier epicycles: an imported outline rebuilt by a chain of spinning
/// circles. `Epicycles` runs the discrete Fourier transform over a whale
/// silhouette read by the SVG importer; each term is a circle spinning a
/// whole number of turns per lap, and chained tip to tail the last tip
/// re-draws the whale. The Terms parameter truncates the chain to its largest
/// circles (a handful gives a soft phantom, more sharpens the outline), and
/// the chain itself is drawn by `drawEpicycles`. One trace is one lap, so the
/// sketch declares `loopDuration` and `--export-loop epicycles.gif` renders a
/// seamless cycle.
@main
final class EpicycleTrace: Sketch {
@Param("Terms", 4...160) var termCount = 64
@Param("Show chain") var showChain = true
let period = 12.0
override var loopDuration: Double? { period }
var epicycles = Epicycles(points: [])
override func setup() {
guard let art = try? SVG(resource: "whale", in: .module) else { return }
let fitted = art.fitted(in: bounds.inset(by: .all(140 * scale)))
guard let outline = fitted.contours.first else { return }
epicycles = Epicycles(outline, samples: 320)
}
override func draw() {
background(Color(hex: 0x0B1220))
let phase = loopProgress(over: period)
// The trail: a fixed length just under one lap, fading toward its
// tail. Its shape is the same every frame (the pen just re-inks it),
// so the exported loop has no pop at the wrap. `point(at:)` is
// periodic, so reaching behind phase 0 wraps by itself.
let trailLaps = 0.86
let samples = 560
let trail = (0 ... samples).map { i in
epicycles.point(at: phase - trailLaps * (1 - Double(i) / Double(samples)),
terms: termCount)
}
noFill()
strokeWeight(3 * scale)
let chunk = 20
for start in stride(from: 0, to: samples, by: chunk) {
let age = 1 - Double(start) / Double(samples) // 1 at the tail, 0 at the pen
stroke(Color(hex: 0x9FD6E8, alpha: 0.18 + 0.82 * (1 - age) * (1 - age)))
drawPolyline(Array(trail[start ... min(start + chunk, samples)]))
}
// The machine that draws it: the chain of spinning circles.
if showChain {
strokeWeight(1 * scale)
stroke(Color(hex: 0x51648A, alpha: 0.55))
drawEpicycles(epicycles, at: phase, terms: termCount)
}
// The pen.
noStroke()
fill(Color(hex: 0xFFE9A8))
drawCircle(center: epicycles.point(at: phase, terms: termCount), radius: 5 * scale)
}
}