Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 4 additions & 9 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -1440,15 +1440,10 @@ underneath them (OKLab dE 6.2 for `new` against the no-coverage red, 7.6 for
sat on. The constant body is dE 25.7 from the nearest coverage colour.

- **Single repeater**: a `#22303A` body in every state and every colour-vision
palette, an 8 px state-coloured bar down the left clipped to the rounded
rect, a 1.5 px state line just outside the body and a 1 px `#0d1114`
hairline outside that. The body is inset 2.5 px (1.5 + 1) so the edge is
added INWARD and the footprint is unchanged. Width follows the id length
(`<= 2` chars: 24, else `10 + len * 7`, plus 8 for the bar), and the label is
centred in the space RIGHT of the bar, not in the whole box. A newly
discovered repeater is taller (28 vs 24), with a larger label and a wider
glow. Corner radius still encodes hop-byte width (4 / 6 / 8), which is this
app's own signal and has no web counterpart.
palette, a 3 px status-coloured border around the whole pill and a 1 px
`#0d1114` hairline outside that. Both borders are drawn inward, so the
footprint remains 28 px tall. Width follows the id length (`26 + len * 8`),
the label is centred, and all statuses use the same capsule geometry.
- **Group marker**: a `#22303A` disc of radius 19 with the count across it, a
2.5 px ring at radius 17.75 in the DOMINANT state's colour, a 1 px hairline
at 19.5, and one uniform 2.6 px dot per state PRESENT, pitch 4.2, centred at
Expand Down
83 changes: 22 additions & 61 deletions lib/utils/repeater_marker_painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,89 +10,52 @@ import 'repeater_marker_style.dart';
/// only puts paint on a canvas. MapLibre plumbing (encoding these to PNG and
/// registering them by name) stays in the map widget.

/// Margin left around every baked chip so the state-coloured glow has room.
/// Uniform across states, so a chip's body stays centred in its bitmap however
/// wide its glow is and the symbol's centre anchor lands on the body's centre.
const double repeaterChipGlowMargin = RepeaterMarkerStyle.chipGlowNew + 2;
/// Transparent margin around every baked chip for antialiased outer corners.
const double repeaterChipCanvasMargin = 2;

/// Paints one repeater chip into [outer] and returns the body rect, so a
/// caller that also draws a label knows where the label may go.
///
/// The body is neutral in every state. The state appears only in the left bar,
/// the line just outside the body and the glow behind it, which is what keeps
/// the marker from competing with the coverage carpet drawn underneath. See
/// [RepeaterMarkerStyle] for the measurements behind that.
/// The body is neutral in every state. The state appears only in the border,
/// which keeps the marker from competing with the coverage carpet drawn
/// underneath. See [RepeaterMarkerStyle] for the measurements behind that.
Rect paintRepeaterChip(
Canvas canvas,
Rect outer,
Color accent,
double bodyRadius, {
double cornerRadius, {
required bool isNew,
}) {
final body = outer.deflate(RepeaterMarkerStyle.bodyInset);
final bodyRRect =
RRect.fromRectAndRadius(body, Radius.circular(bodyRadius));

// Glow in the state colour, behind everything. A new repeater's is wide
// enough to read as emphasis on its own.
// Draw nested fills rather than strokes so both borders stay entirely
// inside [outer] and remain crisp at every device scale.
canvas.drawRRect(
bodyRRect,
Paint()
..color = accent.withValues(alpha: isNew ? 0.85 : 0.55)
..maskFilter = MaskFilter.blur(
BlurStyle.normal,
isNew ? RepeaterMarkerStyle.chipGlowNew : RepeaterMarkerStyle.chipGlow,
),
);

// The neutral body.
canvas.drawRRect(bodyRRect, Paint()..color = RepeaterMarkerStyle.bodyColor);

// State bar down the left edge, clipped to the body so it picks up the
// rounded corners instead of squaring them off.
canvas.save();
canvas.clipRRect(bodyRRect);
canvas.drawRect(
Rect.fromLTWH(
body.left, body.top, RepeaterMarkerStyle.barWidth, body.height),
Paint()..color = accent,
RRect.fromRectAndRadius(outer, Radius.circular(cornerRadius)),
Paint()..color = RepeaterMarkerStyle.hairlineColor,
);
canvas.restore();

// State line laid just outside the body...
const halfLine = RepeaterMarkerStyle.stateLineWidth / 2;
final accentRect = outer.deflate(RepeaterMarkerStyle.hairlineWidth);
canvas.drawRRect(
RRect.fromRectAndRadius(
body.inflate(halfLine),
Radius.circular(bodyRadius + halfLine),
accentRect,
Radius.circular(cornerRadius - RepeaterMarkerStyle.hairlineWidth),
),
Paint()
..color = accent
..style = PaintingStyle.stroke
..strokeWidth = RepeaterMarkerStyle.stateLineWidth,
Paint()..color = accent,
);

// ...and the near-black hairline just outside that, whose outer edge lands
// exactly on [outer]. Two tones because no single border colour survives
// both a dark and a pale basemap.
const hairOffset = RepeaterMarkerStyle.stateLineWidth +
RepeaterMarkerStyle.hairlineWidth / 2;
final body = outer.deflate(RepeaterMarkerStyle.bodyInset);
canvas.drawRRect(
RRect.fromRectAndRadius(
body.inflate(hairOffset),
Radius.circular(bodyRadius + hairOffset),
body,
Radius.circular(cornerRadius - RepeaterMarkerStyle.bodyInset),
),
Paint()
..color = RepeaterMarkerStyle.hairlineColor
..style = PaintingStyle.stroke
..strokeWidth = RepeaterMarkerStyle.hairlineWidth,
Paint()..color = RepeaterMarkerStyle.bodyColor,
);

return body;
}

/// Lays out a chip's hex label. Bold, sized by state, and inked from the body
/// rather than hardcoded white. See [RepeaterMarkerStyle.labelInkFor].
/// Lays out a chip's bold hex label, inked from the body rather than hardcoded
/// white. See [RepeaterMarkerStyle.labelInkFor].
TextPainter repeaterChipLabelPainter(String hex, {required bool isNew}) =>
TextPainter(
text: TextSpan(
Expand All @@ -108,18 +71,16 @@ TextPainter repeaterChipLabelPainter(String hex, {required bool isNew}) =>
textDirection: TextDirection.ltr,
)..layout();

/// Paints a chip's hex label centred in the space right of the state bar.
/// Paints a chip's hex label centred in the neutral body.
void paintRepeaterChipLabel(
Canvas canvas,
Rect body,
TextPainter textPainter,
) {
final labelLeft = body.left + RepeaterMarkerStyle.barWidth;
final labelWidth = body.width - RepeaterMarkerStyle.barWidth;
textPainter.paint(
canvas,
Offset(
labelLeft + (labelWidth - textPainter.width) / 2,
body.left + (body.width - textPainter.width) / 2,
body.top + (body.height - textPainter.height) / 2,
),
);
Expand Down
73 changes: 29 additions & 44 deletions lib/utils/repeater_marker_style.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import 'ping_colors.dart';
/// **The one rule underneath the design: the marker's large area is neutral
/// and the state rides its edge.** The body is a constant [bodyColor] in every
/// state and under every colour-vision palette; the state only ever appears in
/// the left bar, the state line and the group ring. That is not a stylistic
/// the pill border and the group ring. That is not a stylistic
/// preference. The old marker fills were darker siblings of the coverage
/// colours painted underneath them (measured on the web at OKLab dE 6.2 for
/// `new` against the no-coverage red, 7.6 for `stale` against the dead-zone
Expand Down Expand Up @@ -50,37 +50,28 @@ class RepeaterMarkerStyle {

// ── Chip (single repeater) geometry, logical px ───────────────────────────

/// Width of the state-coloured bar down the chip's left edge, clipped to the
/// body's rounded rect.
static const double barWidth = 8;
/// The state-coloured border around the neutral body.
static const double accentBorderWidth = 3;

/// The state line laid just outside the body.
static const double stateLineWidth = 1.5;

/// The near-black hairline laid just outside the state line.
/// The near-black hairline laid just outside the accent border.
static const double hairlineWidth = 1;

/// How far the body is inset from the marker's outer footprint, so the state
/// line and hairline are added *inward* and the footprint is unchanged.
static const double bodyInset = stateLineWidth + hairlineWidth;

/// Chip height, and the taller variant used for a newly discovered repeater.
static const double chipHeight = 24;
static const double chipHeightNew = 28;
/// How far the body is inset from the marker's outer footprint, so both
/// borders are added *inward* and the footprint is unchanged.
static const double bodyInset = accentBorderWidth + hairlineWidth;

/// Uniform body corner radius for every repeater label length.
static const double chipCornerRadius = 8;
/// Every repeater pill has the same height, regardless of status.
static const double chipHeight = 28;
static const double chipHeightNew = chipHeight;

/// Label size, and the larger variant for a newly discovered repeater.
static const double chipFontSize = 12;
static const double chipFontSizeNew = 13;
/// Capsule corner radius, uniform across label lengths and states.
static const double chipCornerRadius = chipHeight / 2;

/// Glow blur behind the chip, in the state colour. A new repeater gets a
/// noticeably wider one: that is the whole emphasis treatment.
static const double chipGlow = 2;
static const double chipGlowNew = 6;
/// Label size. Status changes the border colour, not the pill geometry.
static const double chipFontSize = 13;
static const double chipFontSizeNew = chipFontSize;

/// Padding either side of the label, inside the space right of the bar.
/// Padding either side of the centred label.
static const double chipHorizontalPad = 8;

// ── Badge (cluster) geometry, logical px ──────────────────────────────────
Expand Down Expand Up @@ -137,10 +128,10 @@ class RepeaterMarkerStyle {
/// Pro Max a 6-character chip measured 84 pt wide against the old 56, and
/// the cluster badge 56 pt across against the old 40.
///
/// At 1.0 the badge is 40 pt across, the size it has always been, and a chip
/// is 24 pt tall against the old 28 while growing with the id instead of
/// being padded to a fixed width. Raising this again means re-measuring
/// against those numbers, not guessing.
/// At 1.0 the badge is 40 pt across, the size it has always been, and a pill
/// is 28 pt tall while growing with the id instead of being padded to a fixed
/// width. Raising this again means re-measuring against those numbers, not
/// guessing.
///
/// `addImage` honours the screen scale on both platforms (iOS reads
/// `UIScreen.main.scale`, Android the decoded bitmap's density), so a 2x
Expand All @@ -151,33 +142,27 @@ class RepeaterMarkerStyle {

/// Outer footprint of a chip carrying a [labelLength]-character hex.
///
/// The width rule is the spec's: a one- or two-character id gets a fixed 24,
/// anything longer grows with the label, and the state bar's 8 px is added
/// on top of either. [measuredLabelWidth] only ever widens the result, for a
/// The width grows with the label while preserving the capsule proportions
/// in the reference. [measuredLabelWidth] only ever widens the result, for a
/// font that renders wider than the rule assumed.
///
/// This is the OUTER box. The body is [bodyInset] inside it, so the state
/// line and hairline are added inward and the footprint is unchanged.
/// This is the OUTER box. The body is [bodyInset] inside it, so the accent
/// border and hairline are added inward and the footprint is unchanged.
static Size chipSize(
int labelLength, {
required bool isNew,
double measuredLabelWidth = 0,
}) {
final ruleWidth =
(labelLength <= 2 ? 24.0 : 10.0 + labelLength * 7.0) + barWidth;
final needed =
measuredLabelWidth + chipHorizontalPad * 2 + barWidth + bodyInset * 2;
final ruleWidth = 26.0 + labelLength * 8.0;
final needed = measuredLabelWidth + chipHorizontalPad * 2 + bodyInset * 2;
return Size(
math.max(ruleWidth, needed),
isNew ? chipHeightNew : chipHeight,
);
}

/// Width available for the label: the body, less the state bar. The label is
/// centred in THIS, not in the whole chip, or a short one drifts left and
/// sits on the bar.
static double chipLabelWidth(double outerWidth) =>
outerWidth - bodyInset * 2 - barWidth;
/// Width available for the centred label inside the neutral body.
static double chipLabelWidth(double outerWidth) => outerWidth - bodyInset * 2;

// ── Status registry ───────────────────────────────────────────────────────

Expand Down Expand Up @@ -398,7 +383,7 @@ enum RepeaterMarkerStatus {
/// Online and adverting.
active('active'),

/// Discovered recently. Drawn taller, with a larger label and glow.
/// Discovered recently. Uses the new-repeater accent on the same pill.
fresh('new'),

/// No advert inside the region's window.
Expand Down
Loading
Loading