Ollin

ScannedByItself

a sketch that reads its own drawing: three discs on closing orbits over drifting bands of hue, slit-scanned on the GPU through a LayerHistory the sketch holds (Filter.slitScan, across by default and radial while the mouse is held), the word it writes read as a glyph mosaic straight off its layer, and the layer's snapshot() drawn back as a small panel a frame late

Run
swift run --package-path Examples Example-Effects-ScannedByItself
Source
Examples/Effects/ScannedByItself/Sketch.swift
import Ollin

/// A sketch that reads its own drawing: a slit scan of the shapes it moves,
/// and a glyph mosaic of the word it writes.
///
/// Until now the picture techniques that read pixels (`SlitScan`, the glyph
/// mosaic, a dither, a stipple) took a photograph, a camera, or an image
/// painted on the CPU, because a layer lives on the GPU. Two routes bring a
/// layer to them. `Filter.slitScan(_:delay:)` keeps the picture on the GPU: a
/// `LayerHistory` made once holds the layer's last frames in a ring, and the
/// filter composes every pixel from the frame its delay names, the left edge
/// now and the right edge the oldest frame held. `layer.snapshot()` brings
/// the layer's pixels to the CPU a frame late, so anything that reads a
/// picture can read the sketch; here the glyph mosaic takes a layer straight
/// (a reduction on the GPU averages its cells, a frame late), and the small
/// panel is the snapshot drawn back as a plain image.
///
/// Hold the mouse to switch the scan's delay from across to radial, where
/// the center is now and the corners are the past.
@main
final class ScannedByItself: Sketch {
    override var loopDuration: Double? { 8 }

    @Param("Reach (frames)", 12...180) var reach = 90
    @Param("Columns", 24...96) var columns = 54

    private var history: LayerHistory?
    private var reachHeld = 0

    override func setup() {
        textFont(OutlineFont.systemBold)
        textAlign(.center, .middle)
    }

    override func draw() {
        if history == nil || reachHeld != reach {
            history = makeLayerHistory(capacity: reach)
            reachHeld = reach
        }
        guard let history else { return }
        background(Color(hex: 0x0B0E14))

        // The scene the scan reads: three discs on orbits that close each lap,
        // over bands of hue drifting up, so slow motion stretches into ribbons
        // and the bands shear into curves.
        let scene = makeRenderTarget(width: Int(width), height: Int(height * 0.62))
        withTarget(scene) {
            paintScene(height: height * 0.62)
        }

        // The scan, filling the upper band of the canvas.
        let delay: SlitScanDelay = mouseIsPressed ? .radial : .across
        drawImage(scene.filtered(.slitScan(history, delay: delay)).image, 0, 0)

        // The live scene, inset at the lower right, from its snapshot: the
        // picture of the frame before, now ordinary CPU pixels.
        let inset = Rectangle(x: width - 300, y: height * 0.62 + 44, width: 256, height: 256 * 0.62)
        if let picture = scene.snapshot() {
            drawImage(picture, in: inset)
        }
        noFill()
        stroke(Color(white: 1, alpha: 0.6))
        strokeWeight(2)
        drawRect(inset)

        // The word, written into its own layer and read back as a mosaic of
        // glyphs: each cell's tone is averaged on the GPU and the character
        // whose measured ink matches it is drawn as text.
        let band = Rectangle(x: 44, y: height * 0.62 + 44, width: width - 388, height: 256 * 0.62)
        let word = makeRenderTarget(width: Int(band.width), height: Int(band.height))
        withTarget(word) {
            paintWord(width: band.width, height: band.height)
        }
        noStroke()
        fill(Color(hex: 0xF2E8D5))
        drawGlyphMosaic(word.image, columns: columns, characters: GlyphSet.classic, in: band)

        drawCaption(mouseIsPressed
            ? "the center is now, the corners the past; the word is read as glyphs a frame late"
            : "left is now, right is a second and a half ago; hold the mouse for radial time")
    }

    /// Three discs on closed orbits over bands of hue drifting upward.
    private func paintScene(height h: Double) {
        let phase = loopProgress(over: 8) * .tau
        noStroke()
        for i in 0..<6 {
            let t = Double(i) / 6
            fill(Color(hue: 0.55 + t * 0.25, saturation: 0.55,
                       brightness: 0.42 + 0.1 * sin(phase + t * .tau)))
            // Each band drawn at its place and a lap above and below it, so
            // the one crossing an edge still covers it.
            let y = fract(t + loopProgress(over: 8)) * h
            for wrap in [-h, 0, h] { drawRect(0, y + wrap - h / 12, width, h / 6) }
        }
        let orbits: [(Double, Double, Double)] = [(1, 0.32, 0.2), (2, 0.18, 0.34), (3, 0.26, 0.12)]
        for (k, (laps, rx, ry)) in orbits.enumerated() {
            let a = phase * laps + Double(k) * 2.1
            let x = width * (0.5 + rx * sin(a))
            let y = h * (0.5 + ry * sin(a * 2 + Double(k)))
            fill(Color(white: 1))
            drawCircle(x, y, 26 + 10 * Double(k))
            fill(Color(hue: Double(k) / 3, saturation: 0.8, brightness: 1))
            drawCircle(x, y, 12 + 5 * Double(k))
        }
    }

    /// The word, swinging across its layer once a lap.
    private func paintWord(width w: Double, height h: Double) {
        let phase = loopProgress(over: 8) * .tau
        background(.clear)
        noStroke()
        fill(.white)
        textSize(h * 0.72)
        withState {
            translate(w / 2 + sin(phase) * w * 0.12, h / 2)
            rotate(sin(phase * 2) * 0.08)
            drawText("OLLIN", 0, 0)
        }
    }
}

↑↓ move ↩ open