PrintSeparation
a sunrise poster split into three spot-ink printing masters, with a halftoned overprint preview and a view parameter that switches between them
- Run
swift run --package-path Examples Example-Color-PrintSeparation- Source
- Examples/Color/PrintSeparation/Sketch.swift
import Ollin
/// One poster, three drums: splitting a sketch into spot-ink printing masters.
///
/// A risograph or screen-print press has no full-color mode. It prints one ink
/// at a time, each pass a translucent layer that darkens the paper, and the
/// colors mix where the layers overlap: blue over yellow reads green, pink
/// over yellow reads orange. `separated(into:paper:)` works that press in
/// reverse: for every pixel it finds the ink coverages whose overprint comes
/// closest, and hands back one grayscale master per ink, black where the drum
/// should print.
///
/// The canvas is the poster itself, which is the whole point: draw normally,
/// declare the `printInks`, and the separation is an export away. The `view` parameter
/// in the inspector flips to the three masters, or to the overprint preview
/// screened through `halftoned()` so the dots the press would lay down are
/// visible, each ink's grid at its own angle. Write the actual print files
/// (masters plus a registration-marked preview) with:
///
/// --export-separations poster.png [--screen halftone]
enum SeparationView: String, CaseIterable, ParamOption { case artwork, masters, preview }
@main
final class PrintSeparationSketch: Sketch {
override var printInks: [Ink]? { [.fluorescentPink, .blue, .yellow] }
@Param(style: .segmented, icon: "square.3.layers.3d") var view: SeparationView = .artwork
private let labelFont = OutlineFont.system
private var artwork = Image(width: 1, height: 1)
private var separation: PrintSeparation?
private var screenedPreview = Image(width: 1, height: 1)
override func setup() {
noStroke()
artwork = paint()
// The whole pipeline is CPU work, done once: separate, then screen a
// copy for the dot preview (the masters keep their continuous tones).
let sep = artwork.separated(into: printInks ?? [])
separation = sep
screenedPreview = sep.halftoned(pitch: 8).preview()
}
override func draw() {
switch view {
case .artwork:
drawImage(artwork, in: Rectangle(x: 0, y: 0, width: width, height: height))
case .preview:
drawImage(screenedPreview, in: Rectangle(x: 0, y: 0, width: width, height: height))
case .masters:
drawMasters()
}
}
/// The three drums side by side: what each one prints, black = full ink.
private func drawMasters() {
background(Color(hex: 0x101014))
guard let separation else { return }
let tile = 480.0, gutter = 40.0
let origin = (width - tile * 2 - gutter) / 2
textFont(labelFont)
textSize(16)
textAlign(.left, .middle)
for (i, layer) in separation.layers.enumerated() {
let x = origin + Double(i % 2) * (tile + gutter)
let y = origin + Double(i / 2) * (tile + gutter + 40)
drawImage(layer.master, in: Rectangle(x: x, y: y, width: tile, height: tile))
fill(layer.ink.color)
drawRect(x, y + tile + 12, 18, 18)
fill(.white)
let coverage = Int((layer.averageInk * 100).rounded())
drawText("\(layer.ink.name.lowercased()) \(coverage)% ink", x + 28, y + tile + 21)
}
drawCaption("one grayscale master per drum, black where the ink lands")
}
/// A little sunrise poster painted the way the press will print it: each
/// ink's coverage darkens the paper (base times the ink, weighted by how
/// much of it lands), so overlaps mix into the overprint colors for free.
private func paint() -> Image {
let w = 1080, h = 1080
let image = Image(width: w, height: h)
let pink = Ink.fluorescentPink.color
let blue = Ink.blue.color
let yellow = Ink.yellow.color
func inked(_ base: Color, _ ink: Color, _ coverage: Double) -> Color {
let a = clamp(coverage, 0, 1)
return Color(red: base.red * (1 - a + a * ink.red),
green: base.green * (1 - a + a * ink.green),
blue: base.blue * (1 - a + a * ink.blue))
}
for y in 0..<h {
for x in 0..<w {
let u = Double(x) / Double(w - 1)
let v = Double(y) / Double(h - 1)
// A yellow sun with a glow that fades with distance, a blue
// mountain range behind, a pink one in front. Each ridge is a
// wavy line; coverage turns on just below it.
let sun = dist(u, v, 0.63, 0.30)
let glow = clamp(1 - sun * 2.2, 0, 1) * 0.5
let disk = 1 - smoothstep(0.155, 0.165, sun)
let backRidge = 0.56 + 0.06 * sin(u * 7 + 1.2)
let back = smoothstep(backRidge - 0.005, backRidge + 0.005, v)
let frontRidge = 0.74 + 0.08 * sin(u * 4.2 + 4.0)
let front = smoothstep(frontRidge - 0.005, frontRidge + 0.005, v)
var c = Color.white
c = inked(c, yellow, glow + disk)
// The pink hill knocks the blue out behind it (all but a thin
// violet shadow), the way a printer traps a foreground shape.
c = inked(c, blue, back * 0.85 * (1 - front * 0.85))
c = inked(c, pink, front * 0.9 + disk * 0.3)
image[x, y] = c
}
}
return image
}
}