LineTextures
material textures by name, the line kind: wood grain traced as streamlines around its knots, hoarfrost needles along a frame and a disc, the crackle of batik wax crowding along two folds, and ikat stripes with their stepped edges, each drawn stroke by stroke the way a plotter would, so it plots straight to SVG (WoodGrain, Frost, Crackle, Ikat)
- Run
swift run --package-path Examples Example-Patterns-LineTextures- Source
- Examples/Patterns/LineTextures/Sketch.swift
import Ollin
/// Material textures by name, the line kind: wood grain, hoarfrost, the
/// crackle of batik wax, and the stepped edge of an ikat stripe, one picker
/// across them. Each is geometry the sketch holds (`WoodGrain`, `Frost`,
/// `Crackle`, `Ikat`), polylines and shapes a pen can draw, so the picture
/// plots straight to SVG with `--export-svg`. The pen draws it over the loop:
/// the strokes appear in the order a plotter would lay them, then the finished
/// sheet holds for the last stretch before the next lap starts it again.
@main
final class LineTextures: Sketch {
enum Texture: String, CaseIterable, ParamOption { case woodGrain, frost, crackle, ikat }
@Param(style: .segmented, icon: "scribble") var texture = Texture.woodGrain
@Param("Seed", 1 ... 99, icon: "dice") var seed = 7
@Param("Detail", 0.5 ... 2, icon: "slider.horizontal.3") var detail = 1.0
@Param("Pen weight", 0.6 ... 3, icon: "pencil.tip") var penWeight = 1.2
@Param("Pen draws it", icon: "pencil.and.outline") var penDraws = true
override var loopDuration: Double? { 12 }
/// One stroke the pen lays: its points, whether it closes, and a tone the
/// wood grain sets by its ring (1 everywhere else).
private struct Line {
var points: [Vector2]
var isClosed = false
var tone = 1.0
}
private var lines: [Line] = []
private var fills: [(Shape, Color)] = []
private var paper = Color.white
private var ink = Color.black
private var builtFor: (Texture, Int, Double)?
private func rebuildIfNeeded() {
if let builtFor, builtFor == (texture, seed, detail) { return }
lines = []
fills = []
let canvas = canvasRectangle
switch texture {
case .woodGrain:
paper = Color(hex: 0xEADFC8)
ink = Color(hex: 0x4A2A14)
let plank = WoodGrain.trace(in: canvas, direction: 0.12, spacing: 5.5 * scale / detail, knots: 2,
ringSpacing: 44 * scale, ringContrast: 0.65, wobble: 0.4, seed: seed)
lines = plank.strokes.map { Line(points: $0.points, isClosed: $0.isClosed, tone: $0.tone) }
case .frost:
paper = Color(hex: 0x0B1B33)
ink = Color(hex: 0xEAF4FF)
// The pane's frame, frosting inward, and a round pane frosting out.
let frame = Rectangle(x: 26 * scale, y: 26 * scale, width: width - 52 * scale, height: height - 52 * scale)
let pane = Frost.grow(along: frame.contour, spacing: 8 * scale / detail, length: 110 * scale,
clump: 0.6, barbSpacing: 7 * scale / detail, side: .inside, seed: seed)
let disc = Circle(center: Vector2(width / 2, height / 2), radius: width * 0.17).contour
let rim = Frost.grow(along: disc, spacing: 7 * scale / detail, length: 80 * scale,
clump: 0.5, barbSpacing: 6 * scale / detail, side: .outside, seed: seed + 1)
lines = (pane.strokes + rim.strokes).map { Line(points: $0) }
case .crackle:
// Batik: a waxed panel over indigo; the dye got in where the wax
// cracked, most along the two folds.
paper = Color(hex: 0x1F2B5B)
ink = Color(hex: 0x1F2B5B)
let panel = Rectangle(x: 70 * scale, y: 70 * scale, width: width - 140 * scale, height: height - 140 * scale)
let folds: [[Vector2]] = [
[Vector2(panel.x + panel.width * 0.42, panel.y), Vector2(panel.x + panel.width * 0.46, panel.y + panel.height)],
[Vector2(panel.x, panel.y + panel.height * 0.58), Vector2(panel.x + panel.width, panel.y + panel.height * 0.52)],
]
let wax = Crackle.grow(in: panel, spacing: 34 * scale / detail, creases: folds, forkChance: 0.55,
wobble: 0.3, seed: seed)
fills = [(Shape(contours: [panel.contour]), Color(hex: 0xF1E6C8))]
lines = wax.lines.map { Line(points: $0) }
case .ikat:
// Five stripes across a vertical warp, two dyes by turns.
paper = Color(hex: 0xF1E6C8)
ink = Color(hex: 0x15203F)
let cloth = Rectangle(x: 40 * scale, y: 40 * scale, width: width - 80 * scale, height: height - 80 * scale)
let stripes = Ikat.stripes(in: cloth, count: 5, bundle: 13 * scale / detail, bleed: 24 * scale,
spikes: 0.14, intrusions: 0.14, skew: 1.2, seed: seed)
let dyes = [Color(hex: 0x1F2B5B), Color(hex: 0x9E3A2F)]
for (i, stripe) in stripes.stripes.enumerated() {
fills.append((stripe.shape, dyes[i % dyes.count]))
lines.append(contentsOf: stripe.edges.map { Line(points: $0) })
}
}
builtFor = (texture, seed, detail)
}
override func draw() {
rebuildIfNeeded()
background(paper)
noStroke()
for (shape, color) in fills {
fill(color)
drawShape(shape)
}
// The pen's progress: every stroke by four fifths of the loop, the
// one it is on drawn point by point.
let progress = penDraws ? smoothstep(0, 0.8, loopProgress(over: 12)) : 1
let reach = progress * Double(lines.count)
let whole = Int(reach)
noFill()
strokeCap(.round)
strokeJoin(.round)
for (i, line) in lines.enumerated() where i <= whole {
let alpha = texture == .woodGrain ? 0.3 + 0.7 * line.tone : 1
stroke(ink.withAlpha(alpha))
strokeWeight(penWeight * scale * (texture == .woodGrain ? 0.55 + 0.65 * line.tone : 1))
if i < whole {
if line.isClosed { drawPolygon(line.points) } else { drawPolyline(line.points) }
} else {
let part = reach - Double(whole)
let count = max(2, Int(part * Double(line.points.count)))
if count < line.points.count { drawPolyline(Array(line.points.prefix(count))) }
else if line.isClosed { drawPolygon(line.points) } else { drawPolyline(line.points) }
}
}
}
}