Ollin

TapeAndLens

eight tape and lens looks, one picker across them: a street on a worn videotape, woven blankets saved as a JPEG at a quality that sweeps down to tiles and back, a city panned through a datamosh until its blocks drag, rooftops through a tilt-shift lens, a shoreline blurring in from the middle down, a portrait fogged by a light leak, a sheet of tiles peeling off the stone under it, and a headland in sepia under the hairline scratches of a worn print

Run
swift run --package-path Examples Example-Effects-TapeAndLens
Imports
OllinSamplePhotos
Source
Examples/Effects/TapeAndLens/Sketch.swift
import Ollin
import OllinSamplePhotos

/// Eight tape and lens looks, one picker across them: a street on a worn
/// videotape, woven blankets saved as a JPEG at a quality that sweeps down to
/// tiles and back, a city panned through a datamosh until its blocks drag, the
/// same rooftops through a tilt-shift lens that turns them into a model, a
/// shoreline blurring in from the bottom, a portrait fogged by a light leak,
/// a page of tiles peeling off the stone under it, and a headland in sepia
/// under the hairline scratches of a worn print. Each is one filter, one
/// generator, or one simulation, and each moves.
@main
final class TapeAndLens: Sketch {
    enum Look: String, CaseIterable, ParamOption {
        case vhs, jpeg, datamosh, tiltShift, progressiveBlur, lightLeak, pagePeel, scratches
    }

    @Param(style: .segmented, icon: "film") var look = Look.vhs
    @Param("Amount", 0 ... 2, icon: "slider.horizontal.3") var amount = 1.0
    @Param("Seed", 0 ... 99, icon: "dice") var seed = 3

    override var loopDuration: Double? { 12 }

    private var street = Image(width: 1, height: 1)
    private var textiles = Image(width: 1, height: 1)
    private var city = Image(width: 1, height: 1)
    private var boats = Image(width: 1, height: 1)
    private var portrait = Image(width: 1, height: 1)
    private var talavera = Image(width: 1, height: 1)
    private var stone = Image(width: 1, height: 1)
    private var headland = Image(width: 1, height: 1)
    private var mosh: SimField?
    private var moshSeed = -1

    override func setup() {
        street = SamplePhoto.street.load()
        textiles = SamplePhoto.textiles.load()
        city = SamplePhoto.city.load()
        boats = SamplePhoto.boats.load()
        portrait = SamplePhoto.portrait.load()
        talavera = SamplePhoto.talavera.load()
        stone = SamplePhoto.stone.load()
        headland = SamplePhoto.headland.load()
    }

    override func draw() {
        let t = loopProgress(over: 12)
        switch look {
        case .vhs: drawTape(t)
        case .jpeg: drawCompression(t)
        case .datamosh: drawDatamosh(t)
        case .tiltShift: drawTiltShift(t)
        case .progressiveBlur: drawProgressive(t)
        case .lightLeak: drawLeak(t)
        case .pagePeel: drawPeel(t)
        case .scratches: drawScratches(t)
        }
    }

    /// `photo` filling the canvas, drifting by `pan` (a fraction of the
    /// canvas) and magnified by `zoom`, so a still picture has motion in it.
    private func drawPanned(_ photo: Image, pan: Vector2, zoom: Double) {
        let w = width * zoom, h = height * zoom
        let x = (width - w) / 2 + pan.x * width, y = (height - h) / 2 + pan.y * height
        drawImage(photo, in: Rectangle(x: x, y: y, width: w, height: h), fit: .cover)
    }

    // MARK: Tape

    /// A street on tape, the camera drifting along it.
    private func drawTape(_ t: Double) {
        let scene = makeRenderTarget()
        withTarget(scene) {
            drawPanned(street, pan: Vector2(0.05 * sin(t * .tau), 0), zoom: 1.12)
        }
        drawImage(scene.filtered(.vhs(amount: amount, phase: time)).image, 0, 0)
    }

    // MARK: Compression

    /// Woven blankets saved as a JPEG, the quality sweeping from 100 down to
    /// 4 and back over the loop, the number in the corner.
    private func drawCompression(_ t: Double) {
        let sweep = 0.5 + 0.5 * cos(t * .tau)
        let quality = max(1, Int((4 + 96 * pow(sweep, 3)) / max(amount, 0.05)))
        let scene = makeRenderTarget()
        withTarget(scene) { drawPanned(textiles, pan: .zero, zoom: 1) }
        drawImage(scene.filtered(.jpeg(quality: min(quality, 100))).image, 0, 0)
        drawLabel("quality \(min(quality, 100))")
    }

    // MARK: Datamosh

    /// A city panned and zoomed, through a datamosh: held blocks drag the old
    /// picture along the pan until they refresh.
    private func drawDatamosh(_ t: Double) {
        if mosh == nil || moshSeed != seed {
            mosh = makeSimField(.datamosh(amount: min(0.85, 0.55 * amount), seed: seed))
            moshSeed = seed
        }
        guard let mosh else { return }
        let a = t * .tau
        withField(mosh) {
            drawPanned(city, pan: Vector2(0.08 * sin(a), 0.04 * sin(2 * a)), zoom: 1.25 + 0.1 * cos(a))
        }
        drawImage(mosh.image, 0, 0)
    }

    // MARK: Lenses

    /// The rooftops through a tilt-shift lens, the focus line drifting up
    /// and down the town.
    private func drawTiltShift(_ t: Double) {
        let scene = makeRenderTarget()
        withTarget(scene) { drawPanned(city, pan: .zero, zoom: 1) }
        let focus = 0.55 + 0.12 * sin(t * .tau)
        let lens = Filter.tiltShift(focus: focus, angle: 0.08 * sin(t * .tau * 2),
                                    radius: 14 * amount)
        drawImage(scene.filtered(lens).filtered(.vibrance(amount: 0.4)).image, 0, 0)
    }

    /// The boats with a blur fading in from the middle down, its start
    /// sliding with the loop.
    private func drawProgressive(_ t: Double) {
        let scene = makeRenderTarget()
        withTarget(scene) { drawPanned(boats, pan: .zero, zoom: 1) }
        let start = 0.3 + 0.25 * (0.5 + 0.5 * sin(t * .tau))
        drawImage(scene.filtered(.progressiveBlur(start: start, end: 1, radius: 24 * amount)).image, 0, 0)
    }

    // MARK: Light leak

    /// A portrait with light leaking in from the right edge, breathing.
    private func drawLeak(_ t: Double) {
        let scene = makeRenderTarget()
        withTarget(scene) { drawPanned(portrait, pan: .zero, zoom: 1) }
        let anchor = Vector2(0.97, 0.35 + 0.1 * sin(t * .tau))
        drawImage(scene.filtered(.lightLeak(anchor: anchor, intensity: 0.4 * amount,
                                            phase: time, seed: seed)).image, 0, 0)
    }

    // MARK: Page peel

    /// A sheet of tiles peeling off the stone under it and laying back down.
    private func drawPeel(_ t: Double) {
        drawPanned(stone, pan: .zero, zoom: 1)
        let page = makeRenderTarget()
        withTarget(page) {
            background(.clear)
            drawPanned(talavera, pan: .zero, zoom: 1)
        }
        let progress = 0.5 - 0.5 * cos(t * .tau)
        drawImage(page.filtered(.pagePeel(amount: 0.4 * progress * min(amount, 2))).image, 0, 0)
    }

    // MARK: Scratches

    /// A headland in sepia under the scratches and grain of a worn print.
    private func drawScratches(_ t: Double) {
        let scene = makeRenderTarget()
        withTarget(scene) { drawPanned(headland, pan: Vector2(0, 0.004 * sin(time * 9)), zoom: 1.02) }
        drawImage(scene.filtered(.sepia(amount: 0.85)).filtered(.filmGrain(amount: 0.06)).image, 0, 0)
        let lines = generate(.scratches(count: 16, flicker: 0.6, phase: time, seed: seed))
        withState {
            blendMode(.screen)
            withState { tint(Color(white: 1, alpha: min(amount, 1))); drawImage(lines.image, 0, 0) }
        }
        let dirt = generate(.scratches(count: 6, thickness: 2, length: 0.25, wobble: 1,
                                       color: Color(white: 0, alpha: 0.6), phase: time, seed: seed + 50))
        drawImage(dirt.image, 0, 0)
    }

    // MARK: Label

    private func drawLabel(_ text: String) {
        withState {
            noStroke()
            fill(Color(white: 0, alpha: 0.55))
            drawRect(24, height - 76, 260, 52)
            fill(.white)
            textSize(26)
            textAlign(.left, .center)
            drawText(text, 40, height - 50)
        }
    }
}

↑↓ move ↩ open