StrokeJoinsAndCaps
a zigzag drawn with each join and a segment ended with each cap, with the weight pulsing (strokeJoin/strokeCap)
- Run
swift run --package-path Examples Example-Shapes-StrokeJoinsAndCaps- Source
- Examples/Shapes/StrokeJoinsAndCaps/Sketch.swift
import Ollin
/// `strokeJoin(_:)` styles a stroked path's corners; `strokeCap(_:)` styles its
/// open ends. The top three rows turn the same sharp zigzag with each join —
/// `.miter` (a crisp point), `.bevel` (cut flat), `.round` (arced). The bottom
/// three end a straight segment with each cap — `.butt` (flat at the endpoint),
/// `.round` (a half-disk tip), `.square` (a flat extension half the weight past
/// the end). A thin white centerline and dots mark the true path under each thick
/// stroke, so you can watch the round and square caps reach past their endpoints
/// while butt stops on them, and the join reshape the peaks. The weight pulses
/// with `time`.
@main
final class StrokeJoinsAndCaps: Sketch {
let joins: [StrokeJoin] = [.miter, .bevel, .round]
let caps: [StrokeCap] = [.butt, .round, .square]
let joinNames = ["miter", "bevel", "round"]
let capNames = ["butt", "round", "square"]
override func draw() {
background(Color(white: 0.1))
let pulse = 0.5 - 0.5 * cos(time * 1.6)
let weight = (10 + 34 * pulse) * scale
let rows = joins.count + caps.count
let rowGap = height / Double(rows + 1)
let margin = width * 0.16
// Joins — a sharp zigzag, so the corner style shapes each peak.
for (i, join) in joins.enumerated() {
let cy = rowGap * Double(i + 1)
let amp = rowGap * 0.34
let pts = [
Vector2(margin, cy + amp),
Vector2(width * 0.36, cy - amp),
Vector2(width * 0.5, cy + amp),
Vector2(width * 0.64, cy - amp),
Vector2(width - margin, cy + amp),
]
stroke(rowColor(i, of: rows)); strokeWeight(weight); strokeJoin(join)
drawPolyline(pts)
drawReference(pts)
rowLabel(joinNames[i], cy, margin: margin)
}
// Caps — a straight segment, so the end style is what changes.
for (j, cap) in caps.enumerated() {
let i = joins.count + j
let cy = rowGap * Double(i + 1)
let pts = [Vector2(width * 0.32, cy), Vector2(width * 0.68, cy)]
stroke(rowColor(i, of: rows)); strokeWeight(weight); strokeCap(cap)
drawPolyline(pts)
drawReference(pts)
rowLabel(capNames[j], cy, margin: margin)
}
}
/// A row label in the left gutter, drawn unstroked so the pulsing stroke weight
/// doesn't reach the text. Scoped so it leaks no state into the next row.
func rowLabel(_ text: String, _ cy: Double, margin: Double) {
withState {
noStroke(); fill(.white); textAlign(.right, .middle); textSize(24 * scale)
drawText(text, margin - 28 * scale, cy)
}
}
func rowColor(_ i: Int, of rows: Int) -> Color {
Colormap.turbo.color(at: Double(i) / Double(rows - 1))
}
/// A thin white centerline plus vertex dots, marking the true path under the
/// thick stroke so the cap extension and join shape read clearly. Scoped so it
/// doesn't leak its style into the next row.
func drawReference(_ pts: [Vector2]) {
withState {
stroke(.white); strokeWeight(1.5 * scale); strokeJoin(.miter); strokeCap(.butt)
drawPolyline(pts)
fill(.white)
for p in pts { drawCircle(p.x, p.y, 3 * scale) }
}
}
}