GlyphContours
a word's outline flattened to points and drawn three ways: a smooth curve, a polygon, and dots that shimmer like heat haze, each pushed sideways by a sine of its height, while the sample density rises and falls (textToShapes, drawCurve/drawPolygon/drawPoints)
- Run
swift run --package-path Examples Example-Text-GlyphContours- Source
- Examples/Text/GlyphContours/Sketch.swift
// Ported from the p5.js sketches week-3/type-4 and week-3/type-5 (MIT) in
// https://github.com/eaviles/rtp-sfpc-f21-p5, themselves p5 ports of Zach
// Lieberman's openFrameworks samples for the RTP class at SFPC, Fall 2021
// (https://github.com/ofZach/RTP_SFPC_F21). Reworked for Ollin's API.
import Foundation
import Ollin
/// A word's outline as evenly spaced points, rendered three ways. `textToShapes`
/// gives the glyph contours; resampling each at an even spacing turns the type
/// into clean, regularly spaced points. The
/// same points become a smooth Catmull-Rom **curve** (`drawCurve`), a straight
/// **polygon** outline (`drawPolygon`), and a field of **dots** (`drawPoints`),
/// stacked so you can compare the three modes on identical points. The spacing
/// breathes coarse↔fine with time (the original sketch drove this the same way);
/// coarse is where the rounded curve and the faceted polygon visibly diverge.
/// The dot row alone adds a gentle sideways push, each point nudged by a sine
/// of its own height, so it wobbles like type seen through heat haze while the
/// rows above hold still. Uses the bold system font.
@main
final class GlyphContours: Sketch {
// A face whose counters are *separate* contours, so each strokes cleanly. SF
// draws e/a/s as one self-touching outline, whose internal aperture edges show
// up as a glitch when you stroke or dot it (filling hides them).
let font = OutlineFont(name: "Helvetica Neue Bold") ?? .systemBold
let word = "hello"
override func setup() {
textFont(font)
strokeJoin(.round)
strokeCap(.round)
}
override func draw() {
background(Color(white: 0.24))
textFont(font)
textAlign(.center, .middle)
textSize(150 * scale)
// Each contour resampled at an even spacing that breathes coarse↔fine,
// centered on the origin so the rows just translate into place. Drawn
// per-contour (each closed on itself), so a letter's bowl and counter are
// separate clean loops — no threads between them and no internal edges.
let spacing = map(sin(time * 0.6), -1, 1, 22, 10) * scale
let contours = textToShapes(word, 0, 0)
.flatMap(\.contours)
.map { $0.resampled(spacing: spacing).points }
.filter { $0.count >= 2 }
// 1) Each contour as a smooth closed curve — rounded letterforms.
withState {
translate(width / 2, height * 0.24)
noFill()
stroke(.white)
strokeWeight(6 * scale)
for contour in contours { drawCurve(contour, closed: true) }
}
// 2) The same points as straight segments — faceted letterforms.
withState {
translate(width / 2, height * 0.5)
noFill()
stroke(.white)
strokeWeight(6 * scale)
for contour in contours where contour.count >= 3 { drawPolygon(contour) }
}
// 3) The bare points (markers take `fill`, so paint the dots with it),
// shimmering: each point pushed sideways by a sine of its own height,
// the wave traveling with time and its amplitude breathing. Kept
// small so the small inner contours (the e's aperture, the o's ring)
// stay intact instead of scattering.
let amp = map(sin(time * 0.7), -1, 1, 2, 7) * scale
withState {
translate(width / 2, height * 0.76)
noStroke()
fill(.white)
for contour in contours {
let shimmered = contour.map { p in
Vector2(p.x + amp * sin(p.y * 0.04 + time * 2), p.y)
}
drawPoints(shimmered, size: 8 * scale)
}
}
}
}