Ollin

GlyphMosaic

a bundled portrait under a passing band of light, rebuilt as a grid of geometric marks, where each cell's character is chosen by how much ink that glyph has in the bundled bitmap font (drawGlyphMosaic, GlyphSet, a custom character set)

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

/// A portrait rebuilt as a grid of glyphs, under a light that passes over it.
///
/// `drawGlyphMosaic` samples an image cell by cell and places, in each cell,
/// the character whose measured ink matches the brightness underneath: bright
/// cores earn solid blocks, mid tones the checkered and crossed marks, faint
/// edges a lone dot, and true shadow stays empty. The ramp is not hand-ordered;
/// every glyph in the set is measured in the active font, so any string works
/// as a palette of marks. The bundled bitmap font covers the whole default
/// `GlyphSet.technical` set, and its chunky pixel forms are the classic look.
///
/// The picture is one of the bundled sample photographs, a young woman in a
/// lace headdress, read at 176 pixels: a mosaic reads one value per cell. A
/// slow band of light sweeps across it once per loop, so the mosaic re-reads
/// the face live: characters promote and demote as the light passes through
/// their cells.
@main
final class GlyphMosaic: Sketch {
    override var loopDuration: Double? { 10 }

    private var picture = Image(width: 1, height: 1)
    private let source = Image(width: 176, height: 176, color: .black)

    override func setup() {
        picture = SamplePhoto.portrait.load().resized(width: source.width, height: source.height)
    }

    override func draw() {
        light(phase: loopProgress(over: 10) * .tau)
        background(.black)
        textFont(BitmapFont.builtIn)
        noStroke()
        fill(.white)

        // The default set ends in full-cell blocks and shades, which tile
        // into solid regions at the bright end. This set tops out at discrete
        // filled marks instead, so even the brightest cells stay separate
        // glyphs with black gutters, and any string works the same way.
        let marks = "·⠂∙•⠒1x∷+=⠶✕▪∴≡⁘┼⠿※╬◌═◇○▖▘▝▗⊘⊞◐⊗✚✜▚▞⣤▤◈⊠□▣⣶◆●◉▧▨▦░▒⊡◘▩◙⣿■"
        drawGlyphMosaic(source, columns: 64, characters: marks,
                        in: canvasRectangle.inset(by: 64))
        drawCaption("every mark chosen by its measured ink; empty cells are true shadow")
    }

    /// The photograph under a broad band of light that makes one turn per
    /// loop, so the mosaic's perfect-loop export closes exactly. The band
    /// never takes a cell to black, so the face is always there to read.
    private func light(phase: Double) {
        let n = source.width
        for y in 0 ..< n {
            for x in 0 ..< n {
                let u = (Double(x) + 0.5) / Double(n) * 2 - 1
                let v = (Double(y) + 0.5) / Double(n) * 2 - 1
                let band = unipolar(sin(2.6 * (u * cos(phase) + v * sin(phase)) - phase))
                let lift = 0.45 + 0.55 * band
                let c = picture[x, y]
                source[x, y] = Color(red: clamp(c.red * lift, 0, 0.9),
                                     green: clamp(c.green * lift, 0, 0.9),
                                     blue: clamp(c.blue * lift, 0, 0.9))
            }
        }
    }
}

↑↓ move ↩ open