From 4ef6f90c1bdb7897afb4fab50b421dffdf4229e8 Mon Sep 17 00:00:00 2001 From: Jason Jobe Date: Tue, 15 Sep 2026 12:09:06 -0400 Subject: [PATCH] Add pen-width taper (forward/circle widthTo:) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ramp the pen width across a move: 🐢.penWidth = 1 🐢.forward(200, widthTo: 12) 🐢.circle(radius: 70, extent: 270, widthTo: 10) Implemented as sugar over the existing command stream — the move is subdivided into short constant-width segments emitting ordinary .penWidth + .forward/.arc pairs. No new TortoiseCommand case, no wire-format change, and no renderer change: a variable-width Stroke would have forced both renderers to fill outline polygons instead of stroking lines, which is the expensive half of the feature. Three properties keep the cost off programs that don't use it: - The automatic step count keys off the width delta, not the distance (taperWidthQuantum, maxTaperSteps), so a gentle ramp over a long move stays cheap. - A taper whose end width equals its start width emits exactly one command, so it stays eligible for the same-width stroke batching in CanvasRenderer.drawElements. - Widths are sampled at each sub-segment's midpoint, which centers the error; a trailing .penWidth(endWidth) then lands the pen exactly on the requested width. Adds a taperedStrokes drawing scenario (both goldens recorded) and a TaperedPetals gallery example with its regenerated SVG. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 2 + README.md | 2 + Sources/Examples/Gallery/Gallery.swift | 1 + Sources/Examples/Gallery/TaperedPetals.swift | 62 ++ .../Documentation.docc/TortoiseCore.md | 17 + Sources/TortoiseCore/Tortoise.swift | 101 +++ .../TortoiseCoreTests/TortoiseCoreTests.swift | 131 ++++ .../scenario.taperedStrokes.svg | 133 ++++ .../DrawingScenarios.swift | 36 + .../scenario.taperedStrokes.png | Bin 0 -> 31605 bytes docs/examples/tapered-petals.svg | 629 ++++++++++++++++++ 11 files changed, 1114 insertions(+) create mode 100644 Sources/Examples/Gallery/TaperedPetals.swift create mode 100644 Tests/TortoiseSVGTests/__Snapshots__/DrawingScenarioSVGTests/scenario.taperedStrokes.svg create mode 100644 Tests/TortoiseUITests/__Snapshots__/DrawingScenarioCanvasTests/scenario.taperedStrokes.png create mode 100644 docs/examples/tapered-petals.svg diff --git a/CLAUDE.md b/CLAUDE.md index d023721..6b2bb56 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -67,6 +67,8 @@ Tortoise API → [TortoiseCommand] → CommandPlayer.play() → [PlaybackFrame] **`Color.defaultBackground` is the single source of truth for the initial background (#44).** `Tortoise` starts at it, `CommandPlayer.play`'s `initialBackgroundColor` defaults to it, and `CanvasModel` / `SVGBuilder` use it as their empty-stream fallback. It is white. Renderers must never substitute a fallback of their own: they previously started from `.clear` while `Tortoise.backgroundColor` reported white, so a program that issued no `.backgroundColor` command painted nothing — the tortoise and its own drawing disagreed about the color of the paper. Note that `Tortoise.backgroundColor` is the *current* value, not the initial one, so it must not be threaded in as `initialBackgroundColor`; that would back-date a later background change to frame 0. Transparency is still available, but must be asked for: `tortoise.backgroundColor = .clear`. Both renderers still skip the fill / omit the `` when `alpha == 0`, which is what makes SwiftUI's `.background()` modifier work. +**Pen-width taper is sugar, not a primitive (`Tortoise.taper`).** `forward(_:widthTo:steps:)` and `circle(radius:extent:widthTo:steps:)` subdivide the move and emit ordinary `.penWidth` + `.forward`/`.arc` pairs. Nothing downstream changed: no new `TortoiseCommand` case, no wire-format change, no renderer change — a variable-width `Stroke` would have forced both renderers to fill outline polygons instead of stroking lines, which is the expensive half of the feature. Three properties are load-bearing. **The automatic step count keys off the width delta, not the distance** (`taperWidthQuantum`, `maxTaperSteps`), so a gentle ramp over a long move stays cheap. **A taper whose end width equals its start width emits exactly one command** — a caller that passes its current width pays nothing, and the stroke stays eligible for the same-width batching in `CanvasRenderer.drawElements`, which every *real* taper defeats by construction. **Widths are sampled at each sub-segment's midpoint**, which centers the error (half the steps for the same fidelity) but never emits the endpoint — hence the trailing `.penWidth(endWidth)` so `penWidth` after the call is what the caller asked for. Don't "simplify" that trailing command away. + **`TortoiseSprite` is a TortoiseUI-only concept.** The sprite (built-in triangle or a user `Image`) is chosen through the `\.tortoiseSprite` environment value, like `\.tortoiseViewport` — it is *not* a `TortoiseCommand`, so it never enters the serialized stream and `TortoiseSVG` is unaffected (SVG output has never drawn the tortoise). Both canvas layers read the environment value even though only `AnimationLayer` draws the sprite: `ViewportMode.autoFit`'s edge inset is `TortoiseSprite.halfExtent * tortoiseScaleMax`, and the two layers must derive the identical transform. `halfExtent` is the sprite's half-*diagonal* so the inset holds at every heading. Image sprites are aspect-fitted into `size` (`ctx.resolve` gives the intrinsic size; a `ResolvedImage` is bound to its context, so this cannot be hoisted out of the per-frame draw). ## Coordinate System diff --git a/README.md b/README.md index 5fd18af..8969d1c 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,8 @@ itself. `swift run ExamplesRunner` regenerates the images below. | [Square Spiral](Sources/Examples/Gallery/SquareSpiral.swift) | [Fractal Tree](Sources/Examples/Gallery/FractalTree.swift) | [Koch Snowflake](Sources/Examples/Gallery/KochSnowflake.swift) | | Circle Rosette | Filled Star | Waves | | [Circle Rosette](Sources/Examples/Gallery/CircleRosette.swift) | [Filled Star](Sources/Examples/Gallery/FilledStar.swift) | [Waves](Sources/Examples/Gallery/Waves.swift) | +| Tapered Petals | | | +| [Tapered Petals](Sources/Examples/Gallery/TaperedPetals.swift) | | | ## Showcase diff --git a/Sources/Examples/Gallery/Gallery.swift b/Sources/Examples/Gallery/Gallery.swift index 08c5114..de6d623 100644 --- a/Sources/Examples/Gallery/Gallery.swift +++ b/Sources/Examples/Gallery/Gallery.swift @@ -14,5 +14,6 @@ public enum Gallery { ("circle-rosette", CircleRosette.draw), ("filled-star", FilledStar.draw), ("waves", Waves.draw), + ("tapered-petals", TaperedPetals.draw), ] } diff --git a/Sources/Examples/Gallery/TaperedPetals.swift b/Sources/Examples/Gallery/TaperedPetals.swift new file mode 100644 index 0000000..285fcbe --- /dev/null +++ b/Sources/Examples/Gallery/TaperedPetals.swift @@ -0,0 +1,62 @@ +import SwiftUI +import TortoiseUI + +/// A pinwheel of brush strokes, each one thick at the hub and vanishing at the +/// tip — drawn with the pen-width taper (see `forward(_:widthTo:)` and +/// `circle(radius:extent:widthTo:)`). +/// +/// A taper is not a stroke primitive: the move is subdivided into short +/// constant-width segments, so this is an ordinary command stream. The step +/// count comes from the *width* change rather than the distance, which is why +/// the long curved petals here cost about as much as the short straight rays. +enum TaperedPetals { + @MainActor + static func draw(_ 🐢: Tortoise) { + 🐢.backgroundColor = .white + 🐢.speed = 10 + + let petals = 12 + for i in 0.. TortoiseCore.Color { + func channel(_ phase: Double) -> Double { + (sin((t + phase) * 2 * .pi) + 1) / 2 + } + return TortoiseCore.Color( + red: channel(0), green: channel(1.0 / 3), blue: channel(2.0 / 3)) + } +} + +#Preview("Tapered Petals") { + TortoiseCanvas(TaperedPetals.draw) + .padding() +} diff --git a/Sources/TortoiseCore/Documentation.docc/TortoiseCore.md b/Sources/TortoiseCore/Documentation.docc/TortoiseCore.md index eecfa14..5351de9 100644 --- a/Sources/TortoiseCore/Documentation.docc/TortoiseCore.md +++ b/Sources/TortoiseCore/Documentation.docc/TortoiseCore.md @@ -26,6 +26,23 @@ This means animation, SVG export, and unit tests all share a single source of tr a snapshot of tortoise state after each command — which renderers step through to produce output. +### Pen-width taper + +``Tortoise/forward(_:widthTo:steps:)`` and +``Tortoise/circle(radius:extent:widthTo:steps:)`` ramp the pen width across a +move: + +```swift +🐢.penWidth = 1 +🐢.forward(200, widthTo: 12) // a stroke that thickens as it is drawn +``` + +There is no variable-width stroke primitive — the move is subdivided into +sub-segments of constant width, so the result is an ordinary command stream that +every renderer already understands and that serializes unchanged. The step count +is chosen from the *width* change, not the distance, and a taper whose end width +equals its start width records a single plain move. + ### Coordinate system - **Origin** — center of the logical canvas. diff --git a/Sources/TortoiseCore/Tortoise.swift b/Sources/TortoiseCore/Tortoise.swift index 3b49e80..503b8e7 100644 --- a/Sources/TortoiseCore/Tortoise.swift +++ b/Sources/TortoiseCore/Tortoise.swift @@ -115,6 +115,32 @@ public final class Tortoise { forward(-distance) } + /// Move forward by `distance` pixels while ramping the pen width from its + /// current value to `endWidth`. + /// + /// The taper is approximated by subdividing the move into sub-segments, + /// each drawn at a constant width — there is no variable-width stroke + /// primitive, so this is sugar over ``forward(_:)`` and + /// ``penWidth``. After the call ``penWidth`` is exactly `endWidth`. + /// + /// - Parameters: + /// - distance: Distance to travel (negative = backward). + /// - endWidth: Pen width at the end of the move. Clamped to `>= 0`. + /// - steps: Number of sub-segments. Defaults to `nil`, which picks the + /// fewest steps that keep the width change per sub-segment at or below + /// ``taperWidthQuantum`` (capped at ``maxTaperSteps``). When the width + /// does not change at all, a single plain ``forward(_:)`` is recorded. + public func forward(_ distance: Double, widthTo endWidth: Double, steps: Int? = nil) { + taper(to: endWidth, steps: steps) { self.record(.forward(distance * $0)) } + } + + /// Move backward by `distance` pixels while ramping the pen width to `endWidth`. + /// + /// See ``forward(_:widthTo:steps:)`` for how the taper is approximated. + public func backward(_ distance: Double, widthTo endWidth: Double, steps: Int? = nil) { + forward(-distance, widthTo: endWidth, steps: steps) + } + /// Rotate clockwise by `degrees`. public func right(_ degrees: Double) { record(.rotate(degrees)) @@ -198,6 +224,81 @@ public final class Tortoise { record(.arc(radius: radius, extent: extent)) } + /// Draw a circular arc while ramping the pen width from its current value + /// to `endWidth`. + /// + /// The arc is subdivided into sub-arcs of equal extent, each drawn at a + /// constant width; sub-arcs compose exactly, so the tortoise lands where an + /// undivided ``circle(radius:extent:)`` would. After the call ``penWidth`` + /// is exactly `endWidth`. + /// + /// See ``forward(_:widthTo:steps:)`` for how `steps` is chosen. + public func circle( + radius: Double, extent: Double = 360, widthTo endWidth: Double, steps: Int? = nil + ) { + taper(to: endWidth, steps: steps) { self.record(.arc(radius: radius, extent: extent * $0)) } + } + + // MARK: - Pen-width taper + + /// Largest pen-width change (in logical units) allowed between consecutive + /// sub-segments when a taper picks its own step count. + /// + /// A quarter of a unit is below the visible threshold at normal scales, and + /// tying the step count to the *width* delta rather than to the distance is + /// what keeps a gentle taper cheap: `forward(500, widthTo: penWidth + 1)` + /// costs four sub-segments, not five hundred. + public static let taperWidthQuantum = 0.25 + + /// Upper bound on the sub-segments a taper will pick for itself. + /// + /// Each sub-segment is a distinct `Stroke` width, which defeats the + /// same-width stroke batching in `TortoiseUI` and emits its own `` in + /// SVG, so an extreme width change buys fidelity with draw calls rather + /// than unbounded ones. Pass `steps:` explicitly to go past this. + public static let maxTaperSteps = 64 + + /// Records `segment` `n` times — each call drawing a `1/n` fraction of the + /// whole move — with the pen width stepped along the way. + /// + /// `segment` receives the fraction of the move to draw, so the same helper + /// serves straight moves and arcs. + private func taper(to rawEndWidth: Double, steps requested: Int?, segment: (Double) -> Void) { + let endWidth = max(0, rawEndWidth) + let startWidth = state.penWidth + let n = Self.taperSteps(from: startWidth, to: endWidth, requested: requested) + + // No width change: a taper is then exactly a plain move, so a caller + // that happens to pass its current width pays nothing for the call — + // one command, one full-width stroke, still eligible for batching. + guard startWidth != endWidth else { + segment(1) + return + } + + let fraction = 1.0 / Double(n) + for i in 0.. Int + { + if let requested { return max(1, requested) } + let delta = abs(endWidth - startWidth) + guard delta > 0 else { return 1 } + return min(maxTaperSteps, max(1, Int((delta / taperWidthQuantum).rounded(.up)))) + } + // MARK: - Pen public func penDown() { diff --git a/Tests/TortoiseCoreTests/TortoiseCoreTests.swift b/Tests/TortoiseCoreTests/TortoiseCoreTests.swift index ab9d052..2170bb2 100644 --- a/Tests/TortoiseCoreTests/TortoiseCoreTests.swift +++ b/Tests/TortoiseCoreTests/TortoiseCoreTests.swift @@ -290,6 +290,137 @@ struct TortoiseAPITests { } } +// MARK: - Pen-width taper + +@Suite("Pen-width taper") +@MainActor +struct PenWidthTaperTests { + /// The widths of the strokes a command stream produces, in order. + private func strokeWidths(_ t: Tortoise) -> [Double] { + CommandPlayer.play(commands: t.commands).compactMap { $0.newStroke?.width } + } + + @Test("no width change records a single plain forward") + func noWidthChangeIsFree() { + let t = Tortoise() + t.penWidth = 3 + t.forward(100, widthTo: 3) + #expect(t.commands == [.penWidth(3), .forward(100)]) + } + + @Test("taper ends at exactly the requested width") + func endsAtRequestedWidth() { + let t = Tortoise() + t.forward(100, widthTo: 5) + #expect(t.penWidth == 5) + #expect(t.commands.last == .penWidth(5)) + } + + @Test("taper travels the full distance") + func travelsFullDistance() { + let t = Tortoise() + t.forward(100, widthTo: 9) + #expect(isClose(t.position, Point(x: 0, y: 100))) + } + + @Test("step count scales with the width delta, not the distance") + func stepCountScalesWithWidthDelta() { + let gentle = Tortoise() + gentle.forward(500, widthTo: 2) // delta 1 -> ceil(1 / 0.25) = 4 + #expect(gentle.commands.filter { $0 == .forward(125) }.count == 4) + + let steep = Tortoise() + steep.forward(10, widthTo: 5) // delta 4 -> ceil(4 / 0.25) = 16 + #expect(strokeWidths(steep).count == 16) + } + + @Test("automatic step count is capped") + func stepCountIsCapped() { + let t = Tortoise() + t.forward(100, widthTo: 1000) + #expect(strokeWidths(t).count == Tortoise.maxTaperSteps) + } + + @Test("explicit steps override the automatic count") + func explicitSteps() { + let t = Tortoise() + t.forward(100, widthTo: 5, steps: 4) + let widths = strokeWidths(t) + #expect(widths.count == 4) + // Midpoint sampling across 1 -> 5: 1.5, 2.5, 3.5, 4.5. + #expect(zip(widths, [1.5, 2.5, 3.5, 4.5]).allSatisfy(isClose)) + } + + @Test("widths increase monotonically and stay inside the requested range") + func widthsAreMonotonicAndBounded() { + let t = Tortoise() + t.penWidth = 2 + t.forward(100, widthTo: 8) + let widths = strokeWidths(t) + #expect(widths == widths.sorted()) + #expect(widths.allSatisfy { $0 > 2 && $0 < 8 }) + } + + @Test("a shrinking taper is monotonically decreasing") + func shrinkingTaper() { + let t = Tortoise() + t.penWidth = 8 + t.forward(100, widthTo: 1) + let widths = strokeWidths(t) + #expect(widths == widths.sorted(by: >)) + #expect(t.penWidth == 1) + } + + @Test("negative end width is clamped to zero") + func negativeEndWidthClamped() { + let t = Tortoise() + t.forward(100, widthTo: -4) + #expect(t.penWidth == 0) + #expect(strokeWidths(t).allSatisfy { $0 >= 0 }) + } + + @Test("backward taper mirrors forward") + func backwardTaper() { + let t = Tortoise() + t.backward(100, widthTo: 4) + #expect(isClose(t.position, Point(x: 0, y: -100))) + #expect(t.penWidth == 4) + } + + @Test("tapered arc lands where an undivided arc would") + func taperedArcEndsInTheSamePlace() { + let plain = Tortoise() + plain.circle(radius: 50, extent: 135) + + let tapered = Tortoise() + tapered.circle(radius: 50, extent: 135, widthTo: 6) + + #expect(isClose(tapered.position, plain.position)) + #expect(isClose(tapered.heading, plain.heading)) + #expect(tapered.penWidth == 6) + } + + @Test("tapered arc emits one arc stroke per step") + func taperedArcStepCount() { + let t = Tortoise() + t.circle(radius: 50, extent: 180, widthTo: 3, steps: 6) + let frames = CommandPlayer.play(commands: t.commands) + let arcs = frames.compactMap(\.newArcStroke) + #expect(arcs.count == 6) + #expect(arcs.allSatisfy { isClose($0.sweep, 30) }) + } + + @Test("taper with the pen up draws nothing but still moves and sets width") + func taperWithPenUp() { + let t = Tortoise() + t.penUp() + t.forward(100, widthTo: 6) + #expect(strokeWidths(t).isEmpty) + #expect(isClose(t.position, Point(x: 0, y: 100))) + #expect(t.penWidth == 6) + } +} + // MARK: - CommandPlayer @Suite("CommandPlayer") diff --git a/Tests/TortoiseSVGTests/__Snapshots__/DrawingScenarioSVGTests/scenario.taperedStrokes.svg b/Tests/TortoiseSVGTests/__Snapshots__/DrawingScenarioSVGTests/scenario.taperedStrokes.svg new file mode 100644 index 0000000..020f2c8 --- /dev/null +++ b/Tests/TortoiseSVGTests/__Snapshots__/DrawingScenarioSVGTests/scenario.taperedStrokes.svg @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Tests/TortoiseTestSupport/DrawingScenarios.swift b/Tests/TortoiseTestSupport/DrawingScenarios.swift index 5292052..2ba13d3 100644 --- a/Tests/TortoiseTestSupport/DrawingScenarios.swift +++ b/Tests/TortoiseTestSupport/DrawingScenarios.swift @@ -5,6 +5,7 @@ extension DrawingScenario { public static let all: [DrawingScenario] = [ linesAndTurns, penStyles, + taperedStrokes, arcs, negativeRadiusArcs, filledShapes, @@ -59,6 +60,41 @@ extension DrawingScenario { } } + /// Covers the pen-width taper sugar: growing and shrinking straight tapers, + /// a tapered arc, and an explicit low `steps:` count (visibly stepped by + /// design — it pins that the caller's override is honored). + public static let taperedStrokes = DrawingScenario("taperedStrokes") { t in + t.penUp() + t.setPosition(x: -150, y: 120) + t.penDown() + + t.penColor = .blue + t.penWidth = 1 + t.heading = 90 + t.forward(280, widthTo: 12) + + t.penUp() + t.setPosition(x: -150, y: 40) + t.penDown() + t.penColor = .red + t.forward(280, widthTo: 1) + + t.penUp() + t.setPosition(x: -150, y: -30) + t.penDown() + t.penColor = .green + t.penWidth = 1 + t.forward(280, widthTo: 12, steps: 5) + + t.penUp() + t.setPosition(x: 0, y: -140) + t.penDown() + t.penColor = .purple + t.penWidth = 1 + t.heading = 90 + t.circle(radius: 70, extent: 270, widthTo: 10) + } + /// Covers `arc`: full circle, half circle (CCW), and negative extent (CW). public static let arcs = DrawingScenario("arcs") { t in t.circle(radius: 60) diff --git a/Tests/TortoiseUITests/__Snapshots__/DrawingScenarioCanvasTests/scenario.taperedStrokes.png b/Tests/TortoiseUITests/__Snapshots__/DrawingScenarioCanvasTests/scenario.taperedStrokes.png new file mode 100644 index 0000000000000000000000000000000000000000..8898d4bf28487b2b53d9ca66ccd784963a3cc480 GIT binary patch literal 31605 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file