SweptKnot
A star swept round a trefoil knot, turning as it goes (Mesh.sweep along a Curve3D), carriages riding the knot's frames, and a striped ribbon (Mesh.strip) whose rungs a Pace can space by area.
- Run
swift run --package-path Examples Example-3D-Geometry-SweptKnot- Source
- Examples/3D/Geometry/SweptKnot/Sketch.swift
import Ollin
/// **A swept knot**: a star carried round a trefoil knot, carriages riding the knot
/// without ever rolling over, and a ribbon round its foot whose rungs can be laid
/// by an even pace.
///
/// The knot is a `Curve3D`, a path through space whose frames turn with the path and
/// never twist about it. `Mesh.sweep` lays the star across the path at every point and
/// joins the copies; `twist` adds turns on purpose, so the star's points wind round
/// the knot the number of times you ask and no more. The carriages ride the same
/// frames with `frame(at:)`: each one faces the way the knot runs and keeps its roof
/// where the frame puts it, through every bend.
///
/// The ribbon is a `Mesh.strip` between two loops that bulge and pinch. Its rungs are
/// striped in fours so their spacing shows. With **even pace** on (the default), the
/// rungs are placed by a `Pace` of the strip's area, so every stripe covers the same
/// area of ribbon however the loops bend. Turn it off and the rungs fall at even
/// steps of the loops' parameter instead: the stripes crowd where the loops slow
/// down and stretch where they speed up.
///
/// Try it: set **turns** to 0 to see a sweep that does not twist at all, or raise
/// **points** to round the star off toward a tube.
@main
final class SweptKnot_Example: Sketch {
@Param(0 ... 4, icon: "arrow.trianglehead.2.clockwise.rotate.90") var turns = 1
@Param(3 ... 9, icon: "star") var points = 5
@Param(0 ... 16, icon: "cablecar") var riders = 7
@Param(icon: "ruler") var evenPace = true
private let knot = Curve3D((0..<320).map { k -> Vector3 in
let t = Double(k) / 320 * .tau
let r = 1.15 + 0.45 * cos(3 * t)
return Vector3(r * cos(2 * t), 0.55 * sin(3 * t) + 0.35, r * sin(2 * t))
}, closed: true)
private var tube = Mesh(positions: [], indices: [])
private var ribbon = Mesh(positions: [], indices: [])
private var built: (turns: Int, points: Int, evenPace: Bool)?
override func draw() {
if built.map({ $0 != (turns, points, evenPace) }) ?? true { rebuild() }
background(Color(hex: 0x0A0B10))
cameraShowcase(.orbitAndRise(period: 30, rise: 0.3, in: 16),
radius: 8.4, elevation: 0.36, fieldOfView: .pi / 3.6)
directionalLight(.white, direction: Vector3(-0.3, -0.8, -0.5), intensity: 0.9)
headlight(intensity: 0.45)
ambientLight(Color(white: 0.25))
specular(0.4); specularSharpness(48)
translate(0, 0.55, 0) // the knot and ribbon centered
fill(Color(hex: 0xF2B544))
drawMesh(tube)
fill(Color(hex: 0xE4572E))
for k in 0..<riders {
let along = (time * 0.03 + Double(k) / Double(riders)).truncatingRemainder(dividingBy: 1)
let f = knot.frame(at: along)
withState {
translate(f.position)
rotate(f.rotation) // z along the knot, y up the frame
rotateZ(along * Double(turns) * .tau) // turned as the star is turned there
translate(0, 0.27, 0) // riding the star's top point
drawBox(width: 0.16, height: 0.1, depth: 0.32)
}
}
fill(.white)
drawMesh(ribbon)
}
private func rebuild() {
let star = Profile.star(points: points, outerRadius: 0.2, innerRadius: 0.09)
let wind = Double(turns)
tube = .sweep(star, along: knot, twist: { $0 * wind * .tau })
// Two loops round the knot's foot, the ribbon between them tall and far
// out at two sides and short and close in at the other two, so a plain
// step of their parameter sweeps about eight times more ribbon in one
// place than in another.
func reach(_ t: Double) -> Double { 2.6 + 0.9 * cos(2 * t) }
func lower(_ t: Double) -> Vector3 {
Vector3(reach(t) * cos(t), -1.3, reach(t) * sin(t))
}
func upper(_ t: Double) -> Vector3 {
let r = reach(t) * 0.9
return Vector3(r * cos(t), -1.0 + 0.45 * (1 + cos(2 * t)), r * sin(t))
}
let rungs = 240
let pace = Pace(byAreaBetween: upper, and: lower, period: .tau)
let ts = (0..<rungs).map { k -> Double in
let share = Double(k) / Double(rungs)
return evenPace ? pace.parameter(at: share) : share * .tau
}
let colors = (0..<rungs).map { k in
(k / 4) % 2 == 0 ? Color(hex: 0x5BC0BE) : Color(hex: 0xF4F1E8)
}
// Upper line first, so the strip's front (and its normals) faces out.
ribbon = .strip(between: ts.map(upper), and: ts.map(lower), colors: colors, closed: true)
built = (turns, points, evenPace)
}
}