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:
2026-08-01 13:18:21 -04:00
parent ae9019465e
commit 2e09776342
16 changed files with 1001 additions and 72 deletions
+20 -23
View File
@@ -25,6 +25,7 @@
#include "core/instrument/ui/param_slider.h" // KnobGeometry / KnobArc (drawKnobFace)
#include "core/ui/component_geometry.h" // KitBox / waveformColumnCount
#include "core/ui/theme.h" // Role / InteractionState / KitColor / spectralColor
#include "shell/instrument/editor_stroke.h" // strokeArcAA / strokePolylineAA
#include "shell/panel/draw_kit.h" // the L1 draw kit: fillSurface/text/drawWaveform/toLice
#endif
@@ -111,11 +112,11 @@ inline void drawTitleBand(LICE_IBitmap* bmp, const instrument::ui::Rect& title,
kitText(bmp, titleText, readout.c_str(), Font::Title, ui::Role::TextPrimary);
}
// Stroke widths for the radial faces. The value arc is drawn as adjacent 1px AA arcs rather
// than one thick primitive — LICE has no thick-arc call, and stacking radii is what keeps every
// ring antialiased.
inline constexpr int kKnobValueArcPx = 3;
inline constexpr int kInnerDialArcPx = 2;
// 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;
inline constexpr float kKnobValueArcPx = 3.0f;
inline constexpr float kInnerDialArcPx = 2.0f;
inline constexpr int kKnobNeedlePx = 2;
// Draws one radial knob face: param_slider owns the value<->angle map; this turns it into
@@ -143,27 +144,26 @@ inline void drawKnobFace(LICE_IBitmap* bmp, const instrument::ui::Rect& knobRect
const float a0 = static_cast<float>((arc.startDeg - 360.0) * kDegToRad);
const float a1 = static_cast<float>(
(arc.startDeg + instrument::ui::knobSweepDeg(arc) - 360.0) * kDegToRad);
LICE_Arc(bmp, cx, cy, rOuter, a0, a1, toLice(ui::roleColor(ui::Role::LineHairline)), 1.0f, 0,
true);
strokeArcAA(bmp, cx, cy, rOuter - kKnobTrackArcPx * 0.5f, a0, a1, kKnobTrackArcPx,
toLice(ui::roleColor(ui::Role::LineHairline)));
const double v = value01 < 0.0 ? 0.0 : (value01 > 1.0 ? 1.0 : value01);
if (v > 0.0) {
const float av = static_cast<float>(
(arc.startDeg + v * instrument::ui::knobSweepDeg(arc) - 360.0) * kDegToRad);
const ui::Role valueRole = disabled ? ui::Role::TextDim
: (hot ? ui::Role::AccentHot : ui::Role::AccentPrimary);
const LICE_pixel valueCol = toLice(ui::roleColor(valueRole));
for (int i = 0; i < kKnobValueArcPx; ++i) {
LICE_Arc(bmp, cx, cy, rOuter - static_cast<float>(i), a0, av, valueCol, 1.0f, 0, true);
}
strokeArcAA(bmp, cx, cy, rOuter - kKnobValueArcPx * 0.5f, a0, av, kKnobValueArcPx,
toLice(ui::roleColor(valueRole)));
}
// Needle: from ~35% radius out to the rim at the value's angle. ThickFLine keeps the float
// endpoints AND is always antialiased, so the needle is smooth at every angle.
// Needle: from ~35% radius out to the rim at the value's angle, stroked through the same
// analytic path as the arcs so it holds its weight at every knob position.
const KnobPoint tip = instrument::ui::knobNeedlePoint(kg, arc, v);
const double ix = kg.centerX + (tip.x - kg.centerX) * 0.35;
const double iy = kg.centerY + (tip.y - kg.centerY) * 0.35;
const ui::Role needleRole = disabled ? ui::Role::TextDim : ui::Role::TextPrimary;
LICE_ThickFLine(bmp, ix, iy, tip.x, tip.y, toLice(ui::roleColor(needleRole)), 1.0f, 0,
kKnobNeedlePx);
strokeLineAA(bmp, static_cast<float>(ix), static_cast<float>(iy),
static_cast<float>(tip.x), static_cast<float>(tip.y),
static_cast<float>(kKnobNeedlePx), toLice(ui::roleColor(needleRole)));
}
// The concentric INNER dial: a second value on the same cell, drawn in the categorical
@@ -194,15 +194,12 @@ inline void drawInnerDial(LICE_IBitmap* bmp, const instrument::ui::Rect& innerRe
(arc.startDeg + v * instrument::ui::knobSweepDeg(arc) - 360.0) * kDegToRad);
const ui::Role arcRole = disabled ? ui::Role::TextDim
: (hot ? ui::Role::AccentHot : ui::Role::AccentTertiary);
const LICE_pixel arcCol = toLice(ui::roleColor(arcRole));
for (int i = 0; i < kInnerDialArcPx; ++i) {
LICE_Arc(bmp, cx, cy, r - static_cast<float>(i), a0, av, arcCol, 1.0f, 0, true);
}
strokeArcAA(bmp, cx, cy, r - kInnerDialArcPx * 0.5f, a0, av, kInnerDialArcPx,
toLice(ui::roleColor(arcRole)));
const KnobPoint tip = instrument::ui::knobNeedlePoint(kg, arc, v);
LICE_FLine(bmp, static_cast<float>(kg.centerX), static_cast<float>(kg.centerY),
static_cast<float>(tip.x), static_cast<float>(tip.y),
toLice(ui::roleColor(disabled ? ui::Role::TextDim : ui::Role::AccentTertiary)),
1.0f, 0, true);
strokeLineAA(bmp, static_cast<float>(kg.centerX), static_cast<float>(kg.centerY),
static_cast<float>(tip.x), static_cast<float>(tip.y), 1.0f,
toLice(ui::roleColor(disabled ? ui::Role::TextDim : ui::Role::AccentTertiary)));
}
#endif // _WIN32