Droste
a ring of lit windows that repeats inside itself without end: the copies twist into one spiral, and holding the mouse unwinds them to plain rings (.droste(inner:twist:zoom:))
- Run
swift run --package-path Examples Example-Effects-Droste- Source
- Examples/Effects/Droste/Sketch.swift
import Ollin
/// A ring of lit windows falling into itself, without end.
///
/// The droste filter maps the ring between `inner` and the layer's edge onto a
/// straight strip, repeats the strip along its own length, and maps it back. That
/// is what puts a smaller copy of the picture inside the picture, with a smaller
/// copy inside that one, and so on. `zoom` slides the strip: at 1 the picture is
/// exactly back where it started, so the fall loops with nothing to hide.
///
/// `twist` is the Escher half. At 0 the copies are plain concentric rings; at 1 one
/// turn around the middle also steps one copy down in size, and the rings wind into
/// a single spiral. Hold the mouse to see the rings without the twist.
///
/// The scene is built so the join never shows: the windows sit well inside the ring,
/// and the ground is the same flat color where each copy ends and the next begins.
@main
final class Droste_Example: Sketch {
override var loopDuration: Double? { 16 }
private let palette = CosinePalette.rainbow
override func draw() {
background(.black)
let scene = makeRenderTarget()
withTarget(scene) { paint() }
let twist = mouseIsPressed ? 0.0 : 1.0
drawImage(scene.filtered(.droste(inner: 0.42, twist: twist,
zoom: loopProgress(over: 16) * 2,
angle: 0.12)).image,
in: canvasRectangle)
drawCaption(mouseIsPressed
? "twist 0: the copies are plain concentric rings"
: "one turn steps one copy down, so the rings wind into one spiral; hold the mouse to unwind it")
}
/// The picture the filter reads: a flat ground, a ring of lit windows, and a
/// scatter of small lights, all held between 0.5 and 0.88 of the half-height so
/// neither the inner edge of the ring nor the outer one falls on anything.
private func paint() {
seed(4)
background(Color(hex: 0x0A0E1A))
let middle = center
let unit = height / 2
// The windows, each turned to face the middle.
let count = 14
for i in 0 ..< count {
let angle = Double(i) / Double(count) * .tau
let place = middle + Vector2(angle: angle) * unit * 0.69
withState {
translate(place)
rotate(angle)
noStroke()
fill(palette.color(at: Double(i) / Double(count)))
drawRect(center: .zero, width: unit * 0.2, height: unit * 0.13,
cornerRadius: unit * 0.02)
fill(Color(white: 1, alpha: 0.85))
drawRect(center: Vector2(unit * 0.035, 0), width: unit * 0.05,
height: unit * 0.05, cornerRadius: unit * 0.008)
}
}
// A ring of small lights between the windows, and a few loose ones.
noStroke()
for i in 0 ..< count {
let angle = (Double(i) + 0.5) / Double(count) * .tau
fill(Color(hex: 0xFFE08A, alpha: 0.9))
drawCircle(center: middle + Vector2(angle: angle) * unit * 0.54,
radius: unit * 0.012)
}
for _ in 0 ..< 60 {
let angle = random(.tau)
let r = random(0.52, 0.86)
fill(Color(white: 1, alpha: random(0.15, 0.6)))
drawCircle(center: middle + Vector2(angle: angle) * unit * r,
radius: unit * random(0.002, 0.005))
}
}
}