QuasiImage
one detail of the same system, enlarged, after the Quasi-images of 1977 on. The seed deals the system as Displacements does, so one seed is one system in both sketches. Then random numbers pick a window, one and a quarter, nearly two, or two and a half cells wide, around the middle of the grid where the parts overlap, and the canvas is that window. Each part's edges and diagonals are bands a tenth of a cell wide, the warm plane over the cold one. Over a cycle of twenty steps the bands slide through the window and come back. look picks the system's own colors on white, the broken colors of a relative quasi-image, or the white fields cut away and the bands hung on the wall over their shadow. A press deals the next. An original Ollin interpretation written from the paintings and her statement of the system; there is no code to port.
- Run
swift run --package-path Examples Example-Recreations-DoraMaurer-QuasiImage- Source
- Examples/Recreations/DoraMaurer/QuasiImage/Sketch.swift
// Recreation after Dóra Maurer - the Quasi-images of 1977 on, read from Quasi
// Image No. 1 (1977, acrylic on pressboard) in the Ludwig Museum, Budapest,
// and Relative Quasi-Image (1996, acrylic on canvas and wood, 100 x 100 cm),
// shown at Tate Modern in 2019. A homage, not a reproduction, and not
// affiliated with or endorsed by the artist or her estate.
// https://www.ludwigmuseum.hu/en/node/11730
// https://www.ludwigmuseum.hu/en/node/847
//
// An original Ollin interpretation, written from the paintings and from the
// system of her Displacements as she stated it, quoted in László Beke's
// essay "Objective Tenderness" (1994), with the selection she described: a
// detail of one step, chosen by random numbers, enlarged. Nothing was ported.
// The paths the parts take between the corners, the sizes of the windows,
// the looks, and the motion are this sketch's own.
import Foundation
import Ollin
/// Quasi-image (Dóra Maurer, from 1977). Her Displacements are a system: a
/// grid of 10 by 10 units on a surface of 4:5, and on it eight parts, four
/// warm and four cold, each displaced one unit a step, the cold ones
/// diagonally and the warm ones vertically and horizontally. From 1976 she
/// stopped painting the whole system and painted details of it instead,
/// chosen first by random numbers and later by eye, enlarged until a cell or
/// two filled the canvas and every thin ruled line was a band of flat color.
/// She called them quasi-images, because they fail to comply with the
/// requirements of an image: they show nothing but a frozen moment of the
/// system. In 1986 she began cutting the white fields out, so the bands hung
/// on the wall as a lattice, and in the 1990s she painted the same details
/// again in broken and glowing colors as relative quasi-images, a homage to
/// Monet's series of one motif under changing light.
///
/// This sketch runs the system of the Displacements sketch beside it: the
/// parts are quarters of the grid, a warm and a cold one starting together in
/// each corner as a contrasting pair, every part ruled with one diagonal a
/// cell and kept in a lane of its own, a warm part moving one unit a step
/// along the border and a cold one along the diagonal, so twenty steps bring
/// them all home. The seed deals the system the same way, then picks a window
/// by random numbers, one and a quarter, nearly two, or two and a half cells
/// wide, its middle on a quarter cell within the four units around the grid's
/// own middle, where the overlaps are, and the canvas is that window. Each
/// part's edges and diagonals are bands a tenth of a cell wide, the warm
/// plane over the cold one, and over a cycle the system steps through all
/// twenty steps, so the bands slide through the window and come back. `look`
/// picks the system's own colors on white, the broken colors of a relative
/// quasi-image, or the white cut away and the bands hung on the wall. A press
/// deals the next.
///
/// `--export-svg` gives every part back as a rectangle and its diagonals as
/// lines, so the window and the step can be read from the file: every part
/// is 5 by 5 cells of one size, apart from its lane, where the rule puts it,
/// and one unit from where it was a step before.
@main
final class QuasiImage: Sketch {
/// The paints, after three kinds of quasi-image.
enum Look: String, CaseIterable, ParamOption {
/// From 1977: the system's own industrial colors on white.
case quasi
/// The 1990s: the relative quasi-image, broken and glowing colors on gray.
case relative
/// From 1986: the white fields cut away and the bands hung on the wall.
case cut
}
@Param(icon: "paintpalette") var look = Look.quasi
@Param(0.3 ... 4, icon: "clock") var seconds = 1.2
override var canvasSize: CanvasSize { .square(1080) }
override var loopDuration: Double? { Double(System.period) * seconds }
/// The scene and the rule, as the Displacements sketch states them.
struct System {
static let units = 10
static let side = 5
static let period = 20
static let corners = [Vector2(0, 0), Vector2(5, 0), Vector2(5, 5), Vector2(0, 5)]
struct Part {
var corner: Int
var isWarm: Bool
var pair: Int
var lane: Double
}
var parts: [Part] = []
var turn = 1
/// The top left corner of a part after `step` steps, in units.
func origin(of part: Part, at step: Double) -> Vector2 {
let corners = Self.corners
if part.isWarm {
let s = Self.wrapped(step, Double(Self.period))
let leg = min(3, Int(s / Double(Self.side)))
let from = corners[(part.corner + turn * leg + 8) % 4]
let to = corners[(part.corner + turn * (leg + 1) + 8) % 4]
return from + (to - from) * ((s - Double(leg * Self.side)) / Double(Self.side))
}
let s = Self.wrapped(step, Double(2 * Self.side))
let across = s <= Double(Self.side) ? s : Double(2 * Self.side) - s
let from = corners[part.corner], to = corners[(part.corner + 2) % 4]
return from + (to - from) * (across / Double(Self.side))
}
static func diagonals(from origin: Vector2) -> [(Vector2, Vector2)] {
let n = Double(side)
return (1 ..< 2 * side).map { m in
let m = Double(m)
return m <= n
? (origin + Vector2(0, m), origin + Vector2(m, 0))
: (origin + Vector2(m - n, n), origin + Vector2(n, m - n))
}
}
static func wrapped(_ value: Double, _ period: Double) -> Double {
let r = value.truncatingRemainder(dividingBy: period)
return r < 0 ? r + period : r
}
}
/// The contrasting pairs, warm first: the system's colors, and the broken
/// ones of a relative quasi-image.
static let systemPairs: [(warm: Color, cold: Color)] = [
(Color(hex: 0xD9281C), Color(hex: 0x1D8A48)),
(Color(hex: 0xEE7B1E), Color(hex: 0x1E5FD8)),
(Color(hex: 0xF0BE14), Color(hex: 0x5B3A9A)),
(Color(hex: 0x6E4B26), Color(hex: 0x169BA8)),
]
static let relativePairs: [(warm: Color, cold: Color)] = [
(Color(hex: 0xBC2B12), Color(hex: 0x3A3880)),
(Color(hex: 0xD6611E), Color(hex: 0x294BC3)),
(Color(hex: 0xC99A34), Color(hex: 0x4F8C98)),
(Color(hex: 0x6B2E58), Color(hex: 0x5E8A62)),
]
private struct Dealt: Equatable { var seed: Int }
private var dealtFor: Dealt?
private var system = System()
/// The window, in units: its top left corner and its width (its height is
/// four fifths of that, so the cells keep their 4:5 on a square canvas).
private var windowOrigin = Vector2(0, 0)
private var windowWidth = 2.5
private var pressed = false
override func mousePressed() {
pressed = true
}
override func draw() {
if pressed {
pressed = false
seed(variation + 1)
}
let wanted = Dealt(seed: variation)
if dealtFor != wanted {
dealtFor = wanted
deal()
}
let raw = time / max(0.1, seconds)
let whole = (raw + 1e-9).rounded(.down)
let rest = 0.4
let glide = max(0, min(1, (raw - whole - rest) / (1 - rest)))
let step = whole + glide * glide * (3 - 2 * glide)
switch look {
case .quasi:
background(Color(hex: 0xF4F2EA))
paintBands(at: step, pairs: Self.systemPairs)
case .relative:
background(Color(hex: 0xDCDBD6))
paintBands(at: step, pairs: Self.relativePairs)
case .cut:
background(Color(hex: 0xE9E7E2))
let lattice = makeRenderTarget()
withTarget(lattice) {
paintBands(at: step, pairs: Self.systemPairs)
}
let shadow = Filter.dropShadow(offset: Vector2(width * 0.009, width * 0.014),
radius: width * 0.008, color: Color(white: 0, alpha: 0.32))
drawImage(lattice.filtered(shadow).image, 0, 0)
}
}
/// Every part's edges and diagonals as bands of its color, the lower plane
/// first, each band in the part's lane.
private func paintBands(at step: Double, pairs: [(warm: Color, cold: Color)]) {
let cell = Vector2(width / windowWidth, width / windowWidth * 1.25)
let band = cell.x / 10
let diagonal = band * (cell.x + cell.y) / (cell.x * cell.x + cell.y * cell.y).squareRoot()
func pixel(_ unit: Vector2) -> Vector2 {
Vector2((unit.x - windowOrigin.x) * cell.x, (unit.y - windowOrigin.y) * cell.y)
}
noFill()
strokeCap(.butt)
strokeJoin(.miter)
let order = system.parts.filter { !$0.isWarm }.sorted { $0.lane < $1.lane }
+ system.parts.filter { $0.isWarm }.sorted { $0.lane < $1.lane }
for part in order {
let origin = system.origin(of: part, at: step)
let offset = Vector2(part.lane * band, part.lane * band)
let topLeft = pixel(origin) + offset
let reach = Rectangle(x: topLeft.x - band / 2, y: topLeft.y - band / 2,
width: 5 * cell.x + band, height: 5 * cell.y + band)
stroke(part.isWarm ? pairs[part.pair].warm : pairs[part.pair].cold)
withClip(reach) {
strokeWeight(diagonal)
for (a, b) in System.diagonals(from: origin) {
let p = pixel(a) + offset, q = pixel(b) + offset
let along = (q - p).normalized * band
drawLine(p - along, q + along)
}
}
// A polygon, so the corners are mitered square as painted.
strokeWeight(band)
drawPolygon([topLeft, topLeft + Vector2(5 * cell.x, 0),
topLeft + Vector2(5 * cell.x, 5 * cell.y), topLeft + Vector2(0, 5 * cell.y)])
}
}
/// Deals the system as the Displacements sketch does, then the window.
private func deal() {
randomSeed(variation)
let corners = shuffled([0, 1, 2, 3])
system.parts = []
for p in 0 ..< 4 {
system.parts.append(.init(corner: corners[p], isWarm: true, pair: p, lane: Double(2 * p) - 3.5))
system.parts.append(.init(corner: corners[p], isWarm: false, pair: p, lane: Double(2 * p + 1) - 3.5))
}
system.turn = random() < 0.5 ? 1 : -1
// The window, by random numbers: a width, then a middle on the quarter
// cell within the four units around the grid's own middle, where the
// parts always overlap (a warm part's inner edges never leave it, and
// the cold ones cross it every five steps).
windowWidth = randomChoice([1.25, 1.875, 2.5])
let windowHeight = windowWidth * 0.8
let middle = Vector2(Double(randomChoice(Array(12 ... 28))) / 4,
Double(randomChoice(Array(12 ... 28))) / 4)
windowOrigin = middle - Vector2(windowWidth, windowHeight) / 2
}
}