# Anamorphosis

a plate that reads as nothing until a mirrored cylinder reflects it: a word is wrapped around the cylinder, and each point of it is traced from the eye, off the mirror, and down to the page; a panel shows what that eye receives, read back off the finished plate (Anamorphosis)

Run it with `swift run --package-path Examples Example-Patterns-Anamorphosis`.

```swift
import Ollin

/// **Anamorphosis**: a drawing that says nothing until a mirror reads it.
///
/// The big shape on the page is the plate. It is a real drawing, drawn once and
/// never corrected, and it is unreadable on purpose. Stand a mirrored cylinder
/// on the circle, put your eye where the map was told you would be, and the
/// smear gathers itself into a word standing upright on the glass.
///
/// The panel at the top is what that eye receives, worked out the long way
/// round: every point of the finished plate is sent back up its own light path
/// to the place on the mirror it bounces off, and those places are drawn as the
/// viewer sees them. So the panel is not the word that went in. It is the word
/// coming back out of the plate.
///
/// Try it: pull `eyeDistance` in. The eye sees less of the curve as it gets
/// closer, so the arc the picture can use narrows and the plate has to swing
/// wider to say the same thing. `eyeHeight` is the other half of the same
/// bargain: a low eye stretches the plate away across the page, and there is a
/// height above which a mark simply never comes down.
@main
final class AnamorphosisSketch: Sketch {
    @Param(280 ... 640, icon: "eye") var eyeDistance = 430.0
    @Param(170 ... 460, icon: "arrow.up.and.down") var eyeHeight = 250.0
    @Param(80 ... 175, icon: "cylinder") var mirrorRadius = 132.0
    @Param(0 ... 90, icon: "arrow.up.to.line") var lift = 40.0

    private let paper = Color(hex: 0x11131A)
    private let chalk = Color(hex: 0xF2ECDD)
    private let warm = Color(hex: 0xE0724A)
    private let cool = Color(hex: 0x5A8FC7)

    private var word: [Shape] = []
    private var wordAspect = 0.25
    private var plate: [Shape] = []
    private var seen: [Shape] = []
    private var cacheKey = ""

    override func setup() {
        textSize(130)
        word = textToShapes("OLLIN", at: Vector2(0, 0))
        let box = bounds(of: word)
        if box.width > 0 { wordAspect = box.height / box.width }
    }

    override func draw() {
        background(paper)
        rebuildIfNeeded()

        let mirror = setupMirror()
        drawPlate(mirror)
        drawPanel(mirror)
        drawCaptions(mirror)
    }

    // MARK: - The setup on the page

    private var mirrorCenter: Vector2 { Vector2(width / 2, height * 0.46) }

    private var panel: Rectangle {
        Rectangle(center: Vector2(width / 2, height * 0.11), width: width * 0.62, height: height * 0.16)
    }

    private func setupMirror() -> Anamorphosis {
        let eye = Vector3(mirrorCenter.x, mirrorCenter.y + eyeDistance, eyeHeight)
        let probe = Anamorphosis(center: mirrorCenter, radius: mirrorRadius, eye: eye,
                                 picture: Rectangle(x: 0, y: 0, width: 1, height: 1))
        // Use most of the arc this eye can actually see, and give the box the
        // word's own shape so the letters show on the glass at full height. The
        // taller the label, the shallower its bounce, and the farther the plate
        // has to run to catch it.
        let pictureWidth = probe.widestPicture * 0.84
        let pictureHeight = pictureWidth * wordAspect
        return Anamorphosis(
            center: mirrorCenter, radius: mirrorRadius, eye: eye,
            picture: Rectangle(center: mirrorCenter, width: pictureWidth, height: pictureHeight),
            lift: lift
        )
    }

    /// The plate is static for a given setup, and the way back is a search, so
    /// both are worked out only when a parameter moves.
    private func rebuildIfNeeded() {
        let key = "\(eyeDistance)|\(eyeHeight)|\(mirrorRadius)|\(lift)|\(width)"
        guard key != cacheKey else { return }
        cacheKey = key

        let mirror = setupMirror()
        let picture = fitted(word, in: mirror.picture)
        plate = picture.map { mirror.plate(of: $0, spacing: 1.5) }
        seen = perceived(plate, of: mirror)
    }

    // MARK: - Drawing

    private func drawPlate(_ mirror: Anamorphosis) {
        // Where the cylinder stands. Its own circle is the one place a mark
        // never moves to, so the plate always clears it.
        noFill()
        stroke(cool.withAlpha(0.5))
        strokeWeight(2)
        drawCircle(mirror.footprint)
        fill(cool.withAlpha(0.08))
        noStroke()
        drawCircle(mirror.footprint)

        noStroke()
        fill(chalk)
        for shape in plate { drawShape(shape) }
    }

    private func drawPanel(_ mirror: Anamorphosis) {
        guard !seen.isEmpty else { return }
        let panel = self.panel
        fill(Color(hex: 0x1B1F2B))
        noStroke()
        drawRect(corner: panel.corner, width: panel.width, height: panel.height)

        withClip(panel) {
            fill(warm)
            noStroke()
            for shape in seen { drawShape(shape) }
        }
        noFill()
        stroke(chalk.withAlpha(0.22))
        strokeWeight(1.5)
        drawRect(corner: panel.corner, width: panel.width, height: panel.height)
    }

    private func drawCaptions(_ mirror: Anamorphosis) {
        textSize(17)
        textAlign(.center)
        fill(chalk.withAlpha(0.55))
        drawText("what the eye receives", width / 2, height * 0.225)
        fill(mirror.fits ? chalk.withAlpha(0.4) : warm)
        drawText(mirror.fits
                 ? "stand the mirror on the circle, eye \(Int(eyeDistance)) away and \(Int(eyeHeight)) up"
                 : "the picture is wider than this eye can see",
                 width / 2, height * 0.955)
    }

    // MARK: - The word, and the way back

    /// The box a set of shapes occupies.
    private func bounds(of shapes: [Shape]) -> Rectangle {
        var low = Vector2(.infinity, .infinity), high = Vector2(-.infinity, -.infinity)
        for shape in shapes {
            for contour in shape.contours {
                for point in contour.points {
                    low = Vector2(min(low.x, point.x), min(low.y, point.y))
                    high = Vector2(max(high.x, point.x), max(high.y, point.y))
                }
            }
        }
        guard high.x > low.x, high.y > low.y else { return Rectangle(x: 0, y: 0, width: 0, height: 0) }
        return Rectangle(corner: low, width: high.x - low.x, height: high.y - low.y)
    }

    /// The word scaled and centered into a box, keeping its shape.
    private func fitted(_ shapes: [Shape], in box: Rectangle) -> [Shape] {
        let size = bounds(of: shapes)
        guard size.width > 0, size.height > 0 else { return shapes }
        let scale = min(box.width / size.width, box.height / size.height)
        let offset = box.center - size.center * scale
        return shapes.map { $0.mapPoints { $0 * scale + offset } }
    }

    /// What the viewer receives, read off the finished plate rather than off
    /// the word that made it: each mark is followed back up to the glass, and
    /// the glass is drawn as it is seen.
    private func perceived(_ plate: [Shape], of mirror: Anamorphosis) -> [Shape] {
        // Aim at the middle of the picture where it sits on the glass, not at
        // the axis behind it. The eye looks down, so a nearer point at the same
        // height sits lower in the view, and aiming through the cylinder drops
        // the whole word out of the frame.
        guard let aim = mirror.mirrorPoint(of: mirror.picture.center) else { return [] }
        let forward = (aim - mirror.eye).normalized
        let right = Vector3(0, 0, 1).cross(forward).normalized
        let up = forward.cross(right).normalized

        func seenBy(_ mark: Vector2) -> Vector2? {
            guard let glass = mirror.mirrorPoint(of: mark, asSeenBy: mirror.eye) else { return nil }
            let offset = glass - mirror.eye
            let depth = offset.dot(forward)
            guard depth > 1e-6 else { return nil }
            return Vector2(offset.dot(right) / depth, -offset.dot(up) / depth)
        }

        let view = plate.map { shape in
            Shape(contours: shape.contours.compactMap { contour in
                let points = contour.resampled(spacing: 5).points.compactMap(seenBy)
                return points.count >= 3 ? Contour(points, closed: true) : nil
            }, winding: shape.winding)
        }
        // The angles are the picture; framing them is only where the panel's
        // edges fall, so the panel is the view scaled to fit inside it.
        return fitted(view, in: Rectangle(center: panel.center,
                                          width: panel.width * 0.9, height: panel.height * 0.78))
    }
}
```
