Crossings
six shapes held as values drift about (LineSegment, Circle, Arc, Ellipse, Rectangle), every pair asked each frame where it meets the other (crossings(with:)) with a dot on every answer, and a probe dropped to the nearest place on each (nearestPoint(to:)); the ellipse's exact perimeter is written beside them
- Run
swift run --package-path Examples Example-Shapes-Crossings- Source
- Examples/Shapes/Crossings/Sketch.swift
import Ollin
/// Shapes as values that know where they cross. A line, two circles, an arc,
/// an ellipse, and a rectangle drift across the canvas, each held as the
/// typed value behind its draw call (`LineSegment`, `Circle`, `Arc`,
/// `Ellipse`, `Rectangle`). Every frame each pair is asked where it meets
/// the other (`crossings(with:)`), and a dot marks every answer: a segment
/// against a circle or an ellipse is a quadratic, two circles are the
/// two-circle construction, and the ellipse against anything round is found
/// on the outlines and polished onto the true curves. The shapes are drawn
/// through the same values (`drawLine(_:)`, `drawArc(_:)`, `drawEllipse(_:)`).
///
/// A probe follows the mouse and drops a line to the nearest place on every
/// shape (`nearestPoint(to:)`), with the ellipse's exact perimeter written
/// beside it.
///
/// ```sh
/// swift run Example-Shapes-Crossings --export-loop /tmp/crossings.gif
/// ```
@main
final class Crossings: Sketch {
@Param(2 ... 14, icon: "circle.fill") var dotSize = 7.0
@Param(icon: "scope") var showsNearest = true
let period = 14.0
override var loopDuration: Double? { period }
override func draw() {
background(Color(hex: 0x14161C))
let t = time / period * 2 * .pi
let w = width, h = height
// The cast, each placed by a slow sine so the picture loops.
let road = LineSegment(Vector2(0.06 * w, h * (0.52 + 0.22 * sin(t))),
Vector2(0.94 * w, h * (0.48 - 0.22 * sin(t + 0.7))))
let left = Circle(center: Vector2(w * (0.32 + 0.08 * cos(t)), h * (0.42 + 0.06 * sin(2 * t))),
radius: 0.16 * w)
let right = Circle(center: Vector2(w * (0.64 + 0.07 * sin(t)), h * (0.58 + 0.05 * cos(t))),
radius: 0.13 * w)
let dish = Ellipse(center: Vector2(w * 0.5, h * (0.5 + 0.14 * sin(t * 1.5))),
radiusX: 0.3 * w, radiusY: 0.11 * w, rotation: t * 0.5)
let bow = Arc(center: Vector2(w * 0.5, h * 0.55), radius: 0.36 * w,
start: .pi * 1.05 + 0.3 * sin(t), stop: .pi * 1.95 + 0.3 * sin(t))
let frame = Rectangle(center: Vector2(w * (0.5 + 0.05 * cos(t * 0.5)), h * 0.5),
width: 0.56 * w, height: 0.5 * h)
let cast: [any PlaneCurve] = [road, left, right, dish, bow, frame]
noFill()
strokeWeight(2.5)
stroke(Color(hex: 0x7F8CA8))
drawLine(road)
stroke(Color(hex: 0xE5B15C))
drawCircle(left)
drawCircle(right)
stroke(Color(hex: 0x9FD6E8))
drawEllipse(dish)
stroke(Color(hex: 0xF4EAD6))
drawArc(bow)
stroke(Color(hex: 0x6BD69B))
drawRect(frame)
// Where the probe lands on each curve.
if showsNearest {
let probe = mouseIsPressed ? Vector2(mouseX, mouseY)
: Vector2(w * (0.5 + 0.38 * cos(t * 0.7)), h * (0.5 + 0.38 * sin(t * 0.9)))
strokeWeight(1)
stroke(Color(white: 1, alpha: 0.35))
for place in [road.nearestPoint(to: probe), left.nearestPoint(to: probe),
right.nearestPoint(to: probe), dish.nearestPoint(to: probe),
bow.nearestPoint(to: probe), frame.contour.nearestPoint(to: probe)] {
drawLine(probe, place)
}
noStroke()
fill(.white)
drawCircle(center: probe, radius: 5)
}
// Every pair asked once; a dot per answer.
noStroke()
fill(Color(hex: 0xFF6B6B))
var count = 0
for i in cast.indices {
for j in cast.indices where j > i {
for hit in cast[i].crossings(with: cast[j]) {
drawCircle(center: hit, radius: dotSize)
count += 1
}
}
}
fill(Color(white: 0.8))
textSize(22)
textAlign(.left, .bottom)
drawText("\(count) crossings, ellipse perimeter \(Int(dish.perimeter.rounded()))",
0.04 * w, 0.96 * h)
}
}