Embroidery
the stitches before the machine sews them: a leaf composed as contours, planned with Embroidery.stitches(_:in:), and drawn as its plan, with a dot at every penetration, the thread between them, the jumps, and a needle walking the plan at machine speed (--export-embroidery)
- Run
swift run --package-path Examples Example-Export-Embroidery- Source
- Examples/Export/Embroidery/Sketch.swift
import Ollin
/// Embroidery: see the stitches before the machine sews them. The sketch
/// composes a leaf as plain contours (an outline, its veins, and a fill laid as
/// rows), plans them with `Embroidery.stitches(_:in:)`, and draws the plan
/// itself: every penetration as a dot, the thread between them, and the jumps
/// where the thread is carried over. A needle walks the plan at machine speed.
///
/// The same frame writes a `.dst` file a machine reads directly, with each color
/// as its own thread:
///
/// ```sh
/// swift run Example-Export-Embroidery --export-embroidery /tmp/leaf.dst
/// swift run Example-Export-Embroidery --export-embroidery /tmp/leaf.dst --embroidery-width 80
/// ```
@main
final class Embroidery_Example: Sketch {
@Param("Stitch", 1 ... 6, icon: "ruler", group: "Plan") var stitchLength = 2.5
@Param("Rows", 0.3 ... 3, icon: "line.3.horizontal", group: "Plan") var fillSpacing = 1.2
@Param(icon: "figure.walk", group: "View") var needle = true
private let leaf = Color(hex: 0x2E7D4F)
private let vein = Color(hex: 0xF3E6B3)
private let stitchInk = Color(hex: 0x1B1040)
private var outline: [Vector2] = []
private var veins: [[Vector2]] = []
private var rows: [[Vector2]] = []
private var plan: Stitching?
private var lastKey = ""
override func setup() {
// A leaf: a pointed oval, a midrib, and side veins that curve toward the tip.
// Laid out in the left half, which is the canvas the plan is made for.
let center = Vector2(width * 0.25, height * 0.5)
let long = width * 0.2, wide = height * 0.17
outline = (0 ..< 96).map { k in
let t = Double(k) / 96 * .tau
let taper = pow(abs(sin(t)), 0.75)
return center + Vector2(cos(t) * long * (0.9 + 0.1 * taper), sin(t) * wide * taper)
}
let tip = center + Vector2(long, 0), base = center - Vector2(long, 0)
veins = [[base, tip]]
for k in 1 ... 5 {
let along = Double(k) / 6
let root = base.lerp(to: tip, along)
for side in [-1.0, 1.0] {
let reach = wide * 0.8 * sin(along * .pi)
veins.append([root, root + Vector2(long * 0.16, side * reach * 0.6),
root + Vector2(long * 0.3, side * reach)])
}
}
}
override func draw() {
background(Color(white: 0.96))
let key = "\(stitchLength)/\(fillSpacing)"
if key != lastKey { replan(); lastKey = key }
guard let plan else { return }
// The design as drawn: what the exported file sews. While a vector export
// records the frame, only the design is drawn, centered, so the plan's own
// dots and hops never become stitches.
func design() {
noStroke()
fill(leaf)
drawPolygon(outline)
stroke(vein)
strokeWeight(3)
strokeCap(.round)
for v in veins { drawPolyline(v) }
}
if isVectorExporting {
withState {
translate(width / 4, 0)
design()
}
return
}
let left = Rectangle(x: 0, y: 0, width: width / 2, height: height)
withClip(left) { design() }
// The plan beside it: dots where the needle goes down, the thread between
// them, the jumps as thin gray hops, and the needle walking the plan.
withState {
translate(width / 2, 0)
noFill()
var previous: Vector2?
for stitch in plan.stitches {
defer { previous = stitch.position }
guard let p = previous else { continue }
switch stitch.kind {
case .stitch:
stroke(stitchInk.withAlpha(0.55))
strokeWeight(1.2)
drawLine(p, stitch.position)
case .jump:
stroke(Color(white: 0.6))
strokeWeight(0.6)
drawLine(p, stitch.position)
case .colorChange:
break
}
}
noStroke()
fill(stitchInk)
for stitch in plan.stitches where stitch.kind == .stitch {
drawCircle(center: stitch.position, radius: 1.6)
}
if needle {
// The thread sewn so far, over the plan, and the needle at its end.
let at = needleIndex(in: plan)
stroke(Color(hex: 0xC94B3F))
strokeWeight(2.2)
var last: Vector2?
for stitch in plan.stitches.prefix(at + 1) {
defer { last = stitch.position }
guard let p = last, stitch.kind == .stitch else { continue }
drawLine(p, stitch.position)
}
noStroke()
fill(Color(hex: 0xC94B3F))
drawCircle(center: plan.stitches[at].position, radius: 6)
}
}
stroke(Color(white: 0.8))
strokeWeight(2)
drawLine(width / 2, 0, width / 2, height)
noStroke()
fill(Color(white: 0.25))
textSize(20)
textAlign(.left, .bottom)
let size = plan.size
drawText("\(plan.stitchCount) stitches, \(Int(plan.threadLength.rounded())) mm of thread, "
+ "\(Int(size.width.rounded())) × \(Int(size.height.rounded())) mm", 24, height - 24)
}
/// The design as contours in one thread: the outline, the veins, and the
/// fill as rows, planned at the current pitch and spacing.
private func replan() {
let settings = Embroidery(width: 100, stitchLength: stitchLength, fillSpacing: fillSpacing)
let canvas = Rectangle(x: 0, y: 0, width: width / 2, height: height)
let scale = 100 / canvas.width
rows = Hatching(spacing: fillSpacing / scale, angle: 0.35).lines(filling: Shape(outline))
var contours = rows.map { Contour($0, closed: false) }
contours.append(Contour(outline, closed: true))
contours.append(contentsOf: veins.map { Contour($0, closed: false) })
plan = settings.stitches(contours, in: canvas)
}
/// Where the needle is, walking the plan at about forty stitches a second.
private func needleIndex(in plan: Stitching) -> Int {
let n = plan.stitches.count
guard n > 0 else { return 0 }
return Int(time * 40) % n
}
}