fix: enlarge every sub-2px stroker width to 2px, per Daniel's ruling
Knob track arc, inner-dial needle, and mini velocity-trace all sat below the analytic stroker's opaque-core floor. Raised to 2px; updated visual-design-language.md and core/ui/CLAUDE.md to match; kept the 1px-behavior test, corrected its stale comment.
This commit is contained in:
@@ -103,7 +103,7 @@ L7 sub-pass, 2026-07-27):
|
||||
- `card_meta` — pure card-metadata formatters: bars.beats.subdivisions and seconds.milliseconds; blank when the sample is unstamped.
|
||||
- `stroke_aa` — analytic antialiased thick-stroke COVERAGE (the shell blends it): `StrokeCanvas`, a reusable mask holding distance-to-polyline coverage MAX-accumulated across segments, plus `strokePolyline` / `strokeBounds` / `appendArc` / `rasterRowOffset` (the row-major offset
|
||||
math for a possibly bottom-up raster, pulled out of the shell's LICE blend so its flipped
|
||||
branch is pinned by a host-free test). An arc is just a flattened polyline, so ONE path serves the knob arcs, the inner dial, the envelope polyline and both spline traces. Coverage is `clamp(halfWidth + 0.5 - distance, 0, 1)`, which makes perpendicular weight exactly `2·halfWidth` at every angle. **The guaranteed-opaque-core threshold is width ≥ 2 px, not any width above 1 px**: opacity needs `distance <= halfWidth - 0.5`, and the worst-case distance from a pixel centre to the centreline is 0.5, so a 1 px stroke (`halfWidth = 0.5`) has zero slack — its peak alpha modulates with the stroke's exact alignment to the pixel grid instead of pinning to 255 (the knob track arc and the mini curve-thumbnail trace are both 1 px and both live with this). Long segments are subdivided before rasterizing — EXACT, not an approximation (min-distance to a partition of a segment is min-distance to the whole), purely to keep each piece's bounding box tight, since one long diagonal's box has area O(len²).
|
||||
branch is pinned by a host-free test). An arc is just a flattened polyline, so ONE path serves the knob arcs, the inner dial, the envelope polyline and both spline traces. Coverage is `clamp(halfWidth + 0.5 - distance, 0, 1)`, which makes perpendicular weight exactly `2·halfWidth` at every angle. **The guaranteed-opaque-core threshold is width ≥ 2 px, not any width above 1 px**: opacity needs `distance <= halfWidth - 0.5`, and the worst-case distance from a pixel centre to the centreline is 0.5, so a 1 px stroke (`halfWidth = 0.5`) has zero slack — its peak alpha modulates with the stroke's exact alignment to the pixel grid instead of pinning to 255 (Daniel's ruling, 2026-08-01: every stroker-drawn width on the editor is now >= 2 px for this reason — `testSubOpaqueCoreAtOnePixelWidth` in `tests/test_stroke_aa.cpp` still pins the 1 px case as a property of the stroker, independent of whether any surface ships at that width). Long segments are subdivided before rasterizing — EXACT, not an approximation (min-distance to a partition of a segment is min-distance to the whole), purely to keep each piece's bounding box tight, since one long diagonal's box has area O(len²).
|
||||
|
||||
## Gotchas
|
||||
|
||||
|
||||
@@ -113,11 +113,14 @@ inline void drawTitleBand(LICE_IBitmap* bmp, const instrument::ui::Rect& title,
|
||||
}
|
||||
|
||||
// Stroke widths for the radial faces. Every arc is ONE analytic stroke (editor_stroke.h) whose
|
||||
// outer edge sits on the knob's radius, so the centerline is inset by half the width.
|
||||
inline constexpr float kKnobTrackArcPx = 1.0f;
|
||||
// outer edge sits on the knob's radius, so the centerline is inset by half the width. Every
|
||||
// width here is >= 2 px, the stroker's guaranteed-opaque-core floor (core/ui/CLAUDE.md) —
|
||||
// anything narrower modulates peak alpha with pixel-grid alignment instead of pinning solid.
|
||||
inline constexpr float kKnobTrackArcPx = 2.0f;
|
||||
inline constexpr float kKnobValueArcPx = 3.0f;
|
||||
inline constexpr float kInnerDialArcPx = 2.0f;
|
||||
inline constexpr float kKnobNeedlePx = 2.0f;
|
||||
inline constexpr float kInnerDialNeedlePx = 2.0f;
|
||||
|
||||
// Draws one radial knob face: param_slider owns the value<->angle map; this turns it into
|
||||
// LICE calls. LICE takes radians, and drawing the 7->5 o'clock sweep through the top needs
|
||||
@@ -198,7 +201,7 @@ inline void drawInnerDial(LICE_IBitmap* bmp, const instrument::ui::Rect& innerRe
|
||||
toLice(ui::roleColor(arcRole)));
|
||||
const KnobPoint tip = instrument::ui::knobNeedlePoint(kg, arc, v);
|
||||
strokeLineAA(bmp, static_cast<float>(kg.centerX), static_cast<float>(kg.centerY),
|
||||
static_cast<float>(tip.x), static_cast<float>(tip.y), 1.0f,
|
||||
static_cast<float>(tip.x), static_cast<float>(tip.y), kInnerDialNeedlePx,
|
||||
toLice(ui::roleColor(disabled ? ui::Role::TextDim : ui::Role::AccentTertiary)));
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,9 @@ using namespace reasampler::instrument::ui; // popup geometry
|
||||
|
||||
namespace {
|
||||
|
||||
// The deck thumbnail traces a hairline; the full editor matches the envelope traces' weight.
|
||||
constexpr float kMiniTracePx = 1.0f;
|
||||
// Both at the stroker's opaque-core floor (core/ui/CLAUDE.md) — below 2 px, peak alpha
|
||||
// modulates with pixel-grid alignment instead of pinning solid.
|
||||
constexpr float kMiniTracePx = 2.0f;
|
||||
constexpr float kCurveTracePx = 2.0f;
|
||||
|
||||
const char* curveTitle(CurveTarget target) {
|
||||
|
||||
Reference in New Issue
Block a user