fix: stroke arcs and splines analytically — opaque core, angle-independent weight
LICE_Arc never reaches opacity and ThickFLine's width is minor-axis. One distance-to-polyline coverage mask, blended once, replaces both.
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
// 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 <cstddef>
|
||||
#include <vector>
|
||||
|
||||
#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<std::size_t>(y - bounds_.y)]; }
|
||||
int rowHi(int y) const { return rowHi_[static_cast<std::size_t>(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<std::size_t>(y - bounds_.y) * static_cast<std::size_t>(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<float> coverage_;
|
||||
std::vector<int> rowLo_;
|
||||
std::vector<int> 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<StrokePoint>& out, float cx, float cy, float radius, float startRad,
|
||||
float endRad, float flatnessPx = kArcFlatnessPx);
|
||||
|
||||
} // namespace reasampler::ui
|
||||
Reference in New Issue
Block a user