Ollin

Feedback

a layer that keeps its previous frame and draws over it: a spiralling feedback tunnel (withFeedback)

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

/// Previous-frame feedback: a layer that **remembers itself** across frames. Each
/// frame is built from the *last* frame (zoomed, spun, and faded) with a fresh
/// mark drawn on top. That read-transform-write loop is the whole effect: it's how
/// you get trails, tunnels, and the video-feedback look of a camera pointed at its
/// own screen. It's distinct from the accumulation surface (`noClear`), which can
/// only pile new draws onto an unchanging canvas; feedback hands you the previous
/// frame as an *image you can transform* before drawing it back.
///
/// The pieces: `makeFeedback()` makes a persistent layer (made once in `setup()` and
/// held, its identity carrying state from frame to frame); `withFeedback`
/// redirects drawing into it and hands in last frame as `prev`; `feedback.image`
/// composites the result onto the canvas.
///
/// Try it: change `scale(0.98)` (above 1 zooms outward into a different tunnel),
/// the `rotate` amount (0 = straight trails), or the decay alpha (lower = shorter
/// trails).
@main
final class Feedback_Example: Sketch {
    var trail: Feedback!

    override func setup() {
        trail = makeFeedback()
    }

    override func draw() {
        background(Color(white: 0.02))   // clear the canvas first (it resets the frame)

        withFeedback(trail) { prev in
            // Draw last frame back, transformed about the canvas center so it
            // spirals inward into a tunnel, and faded a little so trails decay.
            withState {
                translate(center)
                rotate(0.06)
                scale(0.98)
                translate(-width / 2, -height / 2)
                tint(Color(white: 1, alpha: 0.94))
                drawImage(prev, 0, 0)
            }
            // A fresh dot tracing a Lissajous path, time-driven so it's the same
            // every run (and deterministic for the snapshot test).
            noStroke()
            let x = width * 0.5 + sin(time * 1.1) * width * 0.32
            let y = height * 0.5 + sin(time * 1.7 + 1) * height * 0.32
            fill(Color(hue: (time * 0.05).truncatingRemainder(dividingBy: 1),
                       saturation: 0.7, brightness: 1))
            drawCircle(x, y, 26)
        }

        drawImage(trail.image, 0, 0)
        drawCaption("previous-frame feedback: last frame, zoomed + spun + faded, plus a new dot")
    }
}

↑↓ move ↩ open