Ollin

Fit

one photograph of a city, cropped to 3:2, drawn into three 2:3 boxes, one per ImageFit: .stretch narrows the round dome, .contain leaves part of the box showing, and .cover fills the box and crops the people at the left away (drawImage(_:in:fit:), Image.cropped(toAspect:))

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

/// A picture and a box rarely have the same shape, and `fit:` says what to do
/// about it. The same city goes into the same three tall boxes: `.stretch`
/// squashes it, which the round dome reports at once; `.contain` keeps its
/// proportions and leaves the box showing above and below; `.cover` keeps them
/// too and fills the box, paying for it with the left and right edges, so the
/// people walking up the street go over the side.
///
/// The picture is a bundled photograph, wide because `cropped(toAspect:)` takes
/// a 3:2 slice out of the square it ships as. The panel at the foot is that
/// slice, at the shape it really is.
///
/// See Docs/Drawing/Images.md.
@main
final class Fit: Sketch {
    private var picture = Image(width: 1, height: 1)

    let paper = Color(hex: 0xF2EDE6)
    let ink = Color(hex: 0x24262E)

    /// Each panel: the mode, and the one word for what it gives up.
    static let modes: [(fit: ImageFit, name: String, cost: String)] = [
        (.stretch, ".stretch", "its proportions"),
        (.contain, ".contain", "part of the box"),
        (.cover, ".cover", "its own edges"),
    ]

    override func setup() {
        textFont(OutlineFont.system)
        picture = SamplePhoto.city.load().cropped(toAspect: 3.0 / 2)
    }

    override func draw() {
        background(paper)
        drawTitle()
        for (i, mode) in Fit.modes.enumerated() { panel(mode, at: i) }
        drawSource()
    }

    // MARK: The panels

    static let boxWidth = 280.0, boxHeight = 430.0, gap = 40.0, top = 250.0
    static var boxLeft: Double { (1080 - (boxWidth * 3 + gap * 2)) / 2 }

    func box(at index: Int) -> Rectangle {
        Rectangle(x: Fit.boxLeft + Double(index) * (Fit.boxWidth + Fit.gap),
                  y: Fit.top, width: Fit.boxWidth, height: Fit.boxHeight)
    }

    func panel(_ mode: (fit: ImageFit, name: String, cost: String), at index: Int) {
        let frame = box(at: index)

        // The box itself, drawn first, so a contained picture leaves it visible.
        noStroke()
        fill(Color(hex: 0xE2DACD))
        drawRect(corner: frame.corner, width: frame.width, height: frame.height)

        drawImage(picture, in: frame, fit: mode.fit)

        noFill()
        stroke(ink.withAlpha(0.35))
        strokeWeight(1.5)
        drawRect(corner: frame.corner, width: frame.width, height: frame.height)

        noStroke()
        fill(ink)
        textSize(30)
        textAlign(.center, .top)
        drawText(mode.name, frame.center.x, frame.y + frame.height + 26)
        fill(ink.withAlpha(0.5))
        textSize(21)
        drawText("gives up \(mode.cost)", frame.center.x, frame.y + frame.height + 64)
    }

    func drawTitle() {
        noStroke()
        fill(ink)
        textSize(44)
        textAlign(.center, .top)
        drawText("One picture, three ways into a tall box", 540, 120)
        fill(ink.withAlpha(0.45))
        textSize(23)
        drawText("the picture is 3:2 and every box is 2:3, so something has to give", 540, 180)
    }

    /// The source at its own shape, so the eye has something to compare against.
    func drawSource() {
        let wide = Rectangle(x: 540 - 150, y: 830, width: 300, height: 200)
        drawImage(picture, in: wide)
        noFill()
        stroke(ink.withAlpha(0.35))
        strokeWeight(1.5)
        drawRect(corner: wide.corner, width: wide.width, height: wide.height)
        noStroke()
        fill(ink.withAlpha(0.5))
        textSize(21)
        textAlign(.center, .top)
        drawText("the picture, 3:2", 540, wide.y + wide.height + 14)
    }
}

↑↓ move ↩ open