VisualCatalog
the Visual chain catalog on one switchable contact sheet: sources, warps, color adjustments, two-chain combines, and per-pixel modulations, chosen by a family parameter, with every tile labeled with the calls it chains
- Run
swift run --package-path Examples Example-Shaders-VisualCatalog- Source
- Examples/Shaders/VisualCatalog/Sketch.swift
import Ollin
/// The fluent `Visual` chain surface on one switchable contact sheet: the
/// **sources** (the coordinate gradient, the soft polygon, a flat color, and a
/// drawn layer read back into a chain), the coordinate **warps** (scroll,
/// repeat, scale, pixelate), the **color** adjustments (brightness, invert,
/// posterize, threshold, luma key, hue shift, color cycle, channel), the
/// two-chain **combines** (mix, difference, mask), and the **modulations**,
/// where one chain's color drives another's coordinates per pixel (rotate,
/// scale, pixelate, kaleidoscope).
///
/// Every tile is one chain compiled into a single GPU pass, generated at its
/// cell's own size and labeled with the calls it makes; several families share
/// an oscillator base so what each step changes reads at a glance, and every
/// number can animate (or hang off an `@Param`) without a recompile.
/// `Shaders/VisualSynth` is the performance-shaped sibling: one deep chain
/// rather than a catalog.
@main
final class VisualCatalog_Example: Sketch {
enum Family: String, CaseIterable, ParamOption {
case sources, warps, color, combines, modulations
}
@Param(style: .segmented, icon: "link") var family = Family.sources
private let labelFont = OutlineFont.system
/// The near-square layout `drawSheet` picks for `count` items, so each tile
/// can be generated at its cell's own size.
private func sheetCell(for count: Int) -> (columns: Int, width: Int, height: Int) {
let cols = Int(Double(count).squareRoot().rounded(.up))
let rows = (count + cols - 1) / cols
let space = width * 0.01
return (cols, Int((width - space * Double(cols + 1)) / Double(cols)),
Int((height - space * Double(rows + 1)) / Double(rows)))
}
override func draw() {
background(Color(white: 0.06))
let t = time
let cell: (columns: Int, width: Int, height: Int)
let chains: [(String, Visual)]
switch family {
case .sources:
cell = sheetCell(for: 4)
let scene = drawnScene(width: cell.width, height: cell.height)
chains = [
(".gradient(speed:)", .gradient(speed: 1)),
(".shape.tinted", .shape(sides: 5, radius: 0.34, smoothing: 0.02)
.tinted(Color(hex: 0xFFB86B))),
(".solid(color)", .solid(Color(hue: (t * 0.04).truncatingRemainder(dividingBy: 1),
saturation: 0.65, brightness: 0.85))),
(".layer(scene)", .layer(scene)),
]
case .warps:
cell = sheetCell(for: 4)
let bands = Visual.oscillator(frequency: 9, speed: 0.8, colorShift: 0.25)
chains = [
(".scrolled(speedX:)", bands.scrolled(speedX: 0.12, speedY: 0.04)),
(".repeated(offsetY:)", Visual.shape(sides: 3, radius: 0.42, smoothing: 0.03)
.tinted(Color(hex: 0x55D6BE))
.repeated(x: 4, y: 4, offsetY: 0.5)),
(".scaled(amount)", bands.scaled(1 + 0.5 * sin(t * 0.7))),
(".pixelated(x:y:)", Visual.voronoi(scale: 3, speed: 0.4, blending: 0.4)
.pixelated(x: 36, y: 12)),
]
case .color:
cell = sheetCell(for: 8)
let field = Visual.oscillator(frequency: 6, speed: 0.7, colorShift: 0.35)
chains = [
(".brightness", field.brightness(0.35 * sin(t))),
(".inverted", field.inverted(unipolar(sin(t * 0.8)))),
(".posterized(levels: 4)", field.posterized(levels: 4, gamma: 0.8)),
(".thresholded(0.5)", field.thresholded(0.5, softness: 0.05)),
(".luma over .solid", Visual.solid(Color(hex: 0x7A1F3D))
.blended(with: field.luma(threshold: 0.45, softness: 0.1))),
(".hueShifted", field.hueShifted(t * 0.08)),
(".colorCycled", field.colorCycled(t * 0.12)),
(".channel(.luminance)", field.channel(.luminance)),
]
case .combines:
cell = sheetCell(for: 3)
let stripes = Visual.oscillator(frequency: 18, speed: 0.5, colorShift: 0.1)
chains = [
(".mixed(with:)", Visual.oscillator(frequency: 7, colorShift: 0.3)
.mixed(with: .voronoi(scale: 5, speed: 0.4)
.tinted(Color(hex: 0x3346FF)),
amount: unipolar(sin(t * 0.5)))),
(".differenced(with:)", stripes.differenced(with: stripes.rotated(t * 0.15))),
(".masked(by: .shape)", Visual.oscillator(frequency: 5, speed: 0.6, colorShift: 0.4)
.masked(by: .shape(sides: 60, radius: 0.26 + 0.06 * sin(t),
smoothing: 0.05))),
]
case .modulations:
cell = sheetCell(for: 4)
let web = Visual.oscillator(frequency: 14, speed: 0.6, colorShift: 0.2)
chains = [
(".rotated(by: .noise)", web.rotated(by: .noise(scale: 2, speed: 0.3),
amount: 1.2)),
(".scaled(by: .oscillator)", web.scaled(by: .oscillator(frequency: 3, speed: 0.4),
amount: 0.5)),
(".pixelated(by: .noise)", Visual.voronoi(scale: 4, speed: 0.3, blending: 0.3)
.pixelated(by: .noise(scale: 2, speed: 0.3), amount: 18, offset: 24)),
(".kaleidoscope(by: .noise)", web.kaleidoscope(by: .noise(scale: 2, speed: 0.25),
segments: 6, amount: 0.15)),
]
}
let sheet: [(String, RenderTarget)] = chains.map {
($0.0, generate($0.1, width: cell.width, height: cell.height))
}
textFont(labelFont)
drawSheet(sheet, columns: cell.columns) { tile, rect in
drawImage(tile.image, in: rect)
}
}
/// The small drawn scene the `.layer` source reads back into a chain: a
/// chain samples it wherever its (possibly warped) coordinate lands, so a
/// drawn layer warps, adjusts, and mixes like any procedural source.
private func drawnScene(width w: Int, height h: Int) -> RenderTarget {
let scene = makeRenderTarget(width: w, height: h)
let cw = Double(w), ch = Double(h)
withTarget(scene) {
background(Color(hex: 0x101826))
noStroke()
fill(Color(hex: 0xFF5252)); drawCircle(cw * 0.32, ch * 0.38, cw * 0.16)
fill(Color(hex: 0x40C4FF)); drawRect(cw * 0.5, ch * 0.52, cw * 0.3, ch * 0.24)
fill(Color(hex: 0xFFD740))
drawTriangle(cw * 0.3, ch * 0.82, cw * 0.16, ch * 0.6, cw * 0.46, ch * 0.6)
stroke(Color(white: 1, alpha: 0.8)); strokeWeight(3); noFill()
drawCircle(cw * 0.5, ch * 0.5, cw * 0.3 + sin(time) * cw * 0.03)
}
return scene
}
}