// stroke_aa.h — analytic antialiased thick-stroke coverage: distance-to-polyline, MAX-accumulated // into a scratch mask that a shell blends ONCE. Pure geometry; no LICE, no host types. // // The single blend is the load-bearing part. Compositing a stroke segment-by-segment (or as a // stack of 1px arcs) re-lays ink over the previous segment's antialiased fringe, which is what // makes a stroke read as a soft glow that never reaches an opaque core. #pragma once #include #include #include "core/ui/rect.h" namespace reasampler::ui { struct StrokePoint { float x = 0.0f; float y = 0.0f; }; // Chord-flatness bound for arc flattening: an order of magnitude under the AA fringe the arc is // drawn with, so the polyline is indistinguishable from the true arc at any radius the deck uses. inline constexpr float kArcFlatnessPx = 0.05f; // Hard cap on flattening output, so a pathological radius/flatness pair terminates with a coarse // arc rather than allocating without bound. inline constexpr int kMaxArcSegments = 512; // Long segments are split before rasterizing. This is EXACT, not an approximation: the minimum // distance to a partition of a segment is the minimum distance to the whole segment. It exists // because a piece is rasterized over its bounding box, and one long diagonal's box has area // O(len^2) — subdivision is what keeps a stroke's cost linear in its length. inline constexpr float kMaxPieceLen = 4.0f; // A reusable coverage mask. Allocation is amortized across calls: `reset` grows the buffer but // never clears it, because per-row valid extents make a clear unnecessary. class StrokeCanvas { public: // Grows the buffer to fit `bounds` and marks every row empty. O(height), not O(area). void reset(const Rect& bounds); // MAX-accumulates one segment. Endpoints are round-capped, so a zero-length segment is a dot // of radius `halfWidth` and a polyline's joints are round by construction. void addSegment(float ax, float ay, float bx, float by, float halfWidth); const Rect& bounds() const { return bounds_; } // Only [rowLo, rowHi) of a row holds meaningful coverage; outside that span the buffer is // deliberately uninitialized, which is what keeps cost proportional to ink, not to the box. int rowLo(int y) const { return rowLo_[static_cast(y - bounds_.y)]; } int rowHi(int y) const { return rowHi_[static_cast(y - bounds_.y)]; } // Row base pointer; index it by (x - bounds().x) within [rowLo, rowHi). const float* rowData(int y) const { return coverage_.data() + static_cast(y - bounds_.y) * static_cast(bounds_.width); } // Bounds-checked single read — 0 outside the valid span. For tests and cold callers; the // blend loop walks rows directly. float coverageAt(int x, int y) const; private: void addPiece(float ax, float ay, float bx, float by, float halfWidth); void extendRow(int y, int x0, int x1); Rect bounds_{}; std::vector coverage_; std::vector rowLo_; std::vector rowHi_; }; // The pixel box a polyline of this half-width can touch, intersected with `clip`. Rect strokeBounds(const StrokePoint* pts, std::size_t count, float halfWidth, const Rect& clip); // A whole stroke in one pass: bounds, reset, every segment MAX-accumulated. Blending the finished // canvas exactly once is the caller's half of the contract. void strokePolyline(StrokeCanvas& canvas, const StrokePoint* pts, std::size_t count, float halfWidth, const Rect& clip); // Appends a flattened arc (n+1 points for n chords) to `out`. Angles are radians in LICE's // convention: 0 is 12 o'clock, increasing clockwise — x = cx + r*sin(a), y = cy - r*cos(a). void appendArc(std::vector& out, float cx, float cy, float radius, float startRad, float endRad, float flatnessPx = kArcFlatnessPx); // The row-major element offset of row `y` within a `rowSpan`-elements-per-row pixel buffer, // accounting for a possibly bottom-up (`flipped`) layout. Pulled out of the shell's LICE blend // so its flipped branch — dead for every bitmap type the shell actually constructs, and // otherwise unverifiable without a live LICE surface — is pinned by a host-free test. Matches // LICE's own row math (`lice.cpp`'s free-function `LICE_GetPixel`: `(h-1-y)*rowspan + x`). inline std::size_t rasterRowOffset(int y, int height, int rowSpan, bool flipped) { const int row = flipped ? height - 1 - y : y; return static_cast(row) * static_cast(rowSpan); } } // namespace reasampler::ui