The Garage SDKs, in the open
garage_auth, garage_entitlements, garage_iap and garage_ui, moved out of Garage-Services and Metro-Map-Maker into one public repo. MIT, one readme, docs under docs/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013F4NWNvYcdeSgqbWMT1VQ7
This commit is contained in:
@@ -0,0 +1,562 @@
|
||||
import "dart:math" as math;
|
||||
import "dart:ui" show Color, lerpDouble;
|
||||
import "package:flutter/painting.dart" show HSLColor;
|
||||
import "package:flutter/widgets.dart" show Brightness;
|
||||
|
||||
// The apps own colour scheme. Used to be a subclass of shadcns ColorScheme which
|
||||
// meant it kept getting flattened back to a plain base scheme every frame by the
|
||||
// theme lerp - now its just our own first class type, nothing above it.
|
||||
//
|
||||
// Holds the usual semantic slots (background/foreground/primary/...) plus the
|
||||
// app specific ones (chrome, panel borders, the control family) as equals.
|
||||
//
|
||||
// ── On the names ──────────────────────────────────────────────────────────
|
||||
// The control family used to be called input*. That was a lie measured
|
||||
// against its own call sites: of the eighteen reads of `inputBorder` exactly
|
||||
// one was a text field, and the rest were outline buttons, selects, date
|
||||
// inputs, checkboxes, radios, the colour swatch and toast. Anything with a
|
||||
// stroke around it took the "input" token because that was the only stroke
|
||||
// on offer. They're control* now, which is what they always were.
|
||||
//
|
||||
// `surfaceSunken` has the same history from the other end: it was
|
||||
// explorerRowEven, a zebra stripe, and every consumer bar one was using it as
|
||||
// "one step below the ground" for a whole pane. It's a rung on the ladder,
|
||||
// not a row colour, so it gets a rung's name.
|
||||
class ColourScheme {
|
||||
const ColourScheme({
|
||||
required this.brightness,
|
||||
required this.background,
|
||||
required this.foreground,
|
||||
required this.card,
|
||||
required this.popover,
|
||||
required this.popoverBorder,
|
||||
required this.tooltipBackground,
|
||||
required this.tooltipBorder,
|
||||
required this.primary,
|
||||
required this.primaryHovered,
|
||||
required this.primaryForeground,
|
||||
required this.secondary,
|
||||
required this.secondaryHovered,
|
||||
required this.secondaryForeground,
|
||||
required this.muted,
|
||||
required this.mutedForeground,
|
||||
required this.destructive,
|
||||
required this.border,
|
||||
required this.divider,
|
||||
required this.surfaceSunken,
|
||||
required this.controlFill,
|
||||
required this.controlFillHovered,
|
||||
required this.controlFillFocused,
|
||||
required this.controlBorder,
|
||||
required this.switchTrackInactive,
|
||||
required this.ring,
|
||||
required this.chart1,
|
||||
required this.chart2,
|
||||
required this.chart3,
|
||||
required this.chart4,
|
||||
required this.chart5,
|
||||
required this.chrome,
|
||||
required this.panelBorder,
|
||||
required this.panelBorderHighlighted,
|
||||
required this.rowHovered,
|
||||
required this.rowText,
|
||||
required this.popoverItemHovered,
|
||||
required this.propertiesSectionBorder,
|
||||
});
|
||||
|
||||
/// Builds a complete scheme from the handful of colours an app actually
|
||||
/// chooses, deriving the rest.
|
||||
///
|
||||
/// ── How the derivation works ──────────────────────────────────────────
|
||||
///
|
||||
/// This used to be a table of HSL lightness offsets. HSL lightness is not
|
||||
/// perceptually uniform and, worse, an offset that runs off the end of the
|
||||
/// scale was *reflected*: it kept its size and lost only its direction. On
|
||||
/// a ground already near the floor that turned "recess this by twelve" into
|
||||
/// "raise it by twelve", and A&As carbon ended up with a canvas backdrop
|
||||
/// LIGHTER than the paper sat in front of it.
|
||||
///
|
||||
/// Measuring the ten hand authored schemes in CIE L* showed what they
|
||||
/// actually have in common, and its four rules rather than one table:
|
||||
///
|
||||
/// 1. Raised rungs are an absolute L* step off the ground. Zinc and carbon
|
||||
/// agree here to within 1-3 points despite sitting sixteen points apart,
|
||||
/// so this part was never the problem.
|
||||
///
|
||||
/// 2. Recessed rungs are the same idea until the ground runs out of room
|
||||
/// underneath, and then they all squeeze by the same factor. Zinc has
|
||||
/// 19.9 L* of basement and spends 12.6 of it on chrome. Carbon has 3.6
|
||||
/// and spends all of it. Squeezing keeps their ORDER, which is the thing
|
||||
/// that actually matters - reflecting destroyed it.
|
||||
///
|
||||
/// 3. A stroke is measured against the surface it outlines, not against the
|
||||
/// ground. Every scheme puts its popover border about ten points over
|
||||
/// its popover; none of them puts it ten points over the background.
|
||||
/// (This is the fix that landed for propertiesSectionBorder alone in
|
||||
/// 38a206f, generalised - it was never a one slot problem.)
|
||||
///
|
||||
/// 4. Text is a fraction of the background->foreground span, not an
|
||||
/// absolute shift. mutedForeground is 60.7% of the way in zinc and
|
||||
/// 60.7% in carbon, to one decimal, on spans of 71 and 92 points.
|
||||
///
|
||||
/// Anything you want to pin exactly is still an argument - every derived
|
||||
/// slot has an override. Hand authored schemes keep using the const
|
||||
/// constructor and are untouched.
|
||||
factory ColourScheme.derive({
|
||||
required Brightness brightness,
|
||||
required Color background,
|
||||
required Color foreground,
|
||||
required Color primary,
|
||||
Color? primaryForeground,
|
||||
Color? destructive,
|
||||
Color? ring,
|
||||
// --- escape hatches: pin any derived slot ---
|
||||
Color? chrome,
|
||||
Color? card,
|
||||
Color? popover,
|
||||
Color? muted,
|
||||
Color? tooltipBackground,
|
||||
Color? secondary,
|
||||
Color? border,
|
||||
Color? surfaceSunken,
|
||||
Color? controlFill,
|
||||
Color? controlFillHovered,
|
||||
Color? controlFillFocused,
|
||||
Color? controlBorder,
|
||||
Color? switchTrackInactive,
|
||||
Color? mutedForeground,
|
||||
Color? rowText,
|
||||
Color? chart1,
|
||||
Color? chart2,
|
||||
Color? chart3,
|
||||
Color? chart4,
|
||||
Color? chart5,
|
||||
}) {
|
||||
final groundL = _lstar(background);
|
||||
final span = _lstar(foreground) - groundL;
|
||||
|
||||
// How much of what the recessed rungs WANT this ground can actually give
|
||||
// them. chrome is the deepest slot the shared scheme has (the canvas
|
||||
// backdrop goes deeper, but thats an app side slot now), so it sets the
|
||||
// scale: if theres room for it nothing squeezes at all.
|
||||
final basement = brightness == Brightness.dark
|
||||
? groundL
|
||||
: math.max(groundL, _deepestSink);
|
||||
final squeeze = basement >= _deepestSink ? 1.0 : basement / _deepestSink;
|
||||
|
||||
Color raise(double points) => _atLstar(background, groundL + points);
|
||||
Color sink(double points) =>
|
||||
_atLstar(background, groundL - points * squeeze);
|
||||
|
||||
// a stroke sits N points off the surface it outlines. reflects if theres
|
||||
// no headroom that way - a border that cant get lighter than its own fill
|
||||
// gets darker by the same amount, which is what youd have picked anyway.
|
||||
Color edge(Color surface, double points) {
|
||||
final l = _lstar(surface);
|
||||
final wanted = l + points;
|
||||
return _atLstar(
|
||||
surface,
|
||||
(wanted > 100 || wanted < 0) ? l - points : wanted,
|
||||
);
|
||||
}
|
||||
|
||||
Color text(double fraction) =>
|
||||
_atLstar(foreground, groundL + span * fraction);
|
||||
|
||||
final resolvedChrome = chrome ?? sink(12.6);
|
||||
final resolvedCard = card ?? raise(5.9);
|
||||
final resolvedPopover = popover ?? sink(11.6);
|
||||
final resolvedMuted = muted ?? sink(9.1);
|
||||
final resolvedFill = controlFill ?? sink(9.1);
|
||||
final resolvedTooltip = tooltipBackground ?? sink(9.1);
|
||||
|
||||
return ColourScheme(
|
||||
brightness: brightness,
|
||||
background: background,
|
||||
foreground: foreground,
|
||||
|
||||
card: resolvedCard,
|
||||
|
||||
popover: resolvedPopover,
|
||||
popoverBorder: edge(resolvedPopover, 9.9),
|
||||
|
||||
tooltipBackground: resolvedTooltip,
|
||||
tooltipBorder: edge(resolvedTooltip, 9.1),
|
||||
|
||||
primary: primary,
|
||||
// +11 L*, CLAMPED rather than reflected. A stroke that cant get lighter
|
||||
// than its surface has to go the other way or it disappears, but a hover
|
||||
// has no such problem - it just wants to be brighter, and a near white
|
||||
// primary should hover to white, not turn round and dim to grey.
|
||||
primaryHovered: _atLstar(primary, math.min(100, _lstar(primary) + 11.0)),
|
||||
primaryForeground: primaryForeground ?? _contrastColor(primary),
|
||||
|
||||
secondary: secondary ?? raise(16.3),
|
||||
secondaryHovered: raise(23.9),
|
||||
// nine of the ten hand authored schemes put this exactly on foreground.
|
||||
secondaryForeground: foreground,
|
||||
|
||||
muted: resolvedMuted,
|
||||
mutedForeground: mutedForeground ?? text(0.607),
|
||||
|
||||
destructive:
|
||||
destructive ??
|
||||
(brightness == Brightness.dark
|
||||
? const Color(0xffa9575f)
|
||||
: const Color(0xffb3474f)),
|
||||
|
||||
// both reference schemes put the outermost line on the floor with
|
||||
// chrome - its the gutter between panels, not a stroke on a surface.
|
||||
border: border ?? resolvedChrome,
|
||||
// 4.1 points off the ground is a line you have to go looking for. The
|
||||
// shell has drawn its own rules with panelBorder (9.6) since forever
|
||||
// and nobody has ever called those loud, so a divider sits just under
|
||||
// that - visible, still subordinate to the edge of a panel.
|
||||
divider: edge(background, 9.0),
|
||||
|
||||
surfaceSunken: surfaceSunken ?? sink(3.8),
|
||||
|
||||
controlFill: resolvedFill,
|
||||
controlFillHovered: controlFillHovered ?? sink(6.5),
|
||||
controlFillFocused: controlFillFocused ?? sink(11.0),
|
||||
controlBorder: controlBorder ?? edge(resolvedFill, 11.8),
|
||||
|
||||
// the off state track of a Switch. was called `input`, which told you
|
||||
// nothing and collided with muted in every derived scheme.
|
||||
switchTrackInactive: switchTrackInactive ?? resolvedMuted,
|
||||
|
||||
ring: ring ?? _brightenForSelectionRing(primary),
|
||||
|
||||
chart1: chart1 ?? const Color(0xffff3352),
|
||||
chart2: chart2 ?? const Color(0xff8bdc00),
|
||||
chart3: chart3 ?? const Color(0xff2890ff),
|
||||
chart4: chart4 ?? const Color(0xffedba18),
|
||||
chart5: chart5 ?? const Color(0xffed5700),
|
||||
|
||||
chrome: resolvedChrome,
|
||||
|
||||
panelBorder: edge(background, 9.6),
|
||||
panelBorderHighlighted: raise(20.8),
|
||||
|
||||
rowHovered: raise(10.0),
|
||||
rowText: rowText ?? text(0.814),
|
||||
popoverItemHovered: raise(8.0),
|
||||
|
||||
// +6.2 over `card`, which is what a properties section is filled with -
|
||||
// NOT off the ground. Measured off the background it sat under a point
|
||||
// above its own fill and vanished into it.
|
||||
propertiesSectionBorder: edge(resolvedCard, 6.2),
|
||||
);
|
||||
}
|
||||
|
||||
final Brightness brightness;
|
||||
|
||||
final Color background;
|
||||
final Color foreground;
|
||||
|
||||
/// Raised surface. Cards, and the fill behind a properties section.
|
||||
final Color card;
|
||||
|
||||
final Color popover;
|
||||
final Color popoverBorder;
|
||||
|
||||
// tooltips get their own pair rather than riding popover's - they sit on
|
||||
// top of everything and want more contrast than a panel-level surface.
|
||||
final Color tooltipBackground;
|
||||
final Color tooltipBorder;
|
||||
|
||||
final Color primary;
|
||||
final Color primaryHovered;
|
||||
final Color primaryForeground;
|
||||
final Color secondary;
|
||||
// secondary fill, hovered. used to be one hardcoded grey duplicated in
|
||||
// button.dart and select.dart, which meant the crimson/light schemes both
|
||||
// hovered to the same dark grey.
|
||||
final Color secondaryHovered;
|
||||
final Color secondaryForeground;
|
||||
final Color muted;
|
||||
final Color mutedForeground;
|
||||
final Color destructive;
|
||||
final Color border;
|
||||
final Color divider;
|
||||
|
||||
/// One step below the ground. Side panes, list backgrounds, anything
|
||||
/// recessed into the surface its sat on rather than lifted off it.
|
||||
final Color surfaceSunken;
|
||||
|
||||
// The control family: the fill and stroke every control shares - text
|
||||
// fields, selects, date inputs, outline and ghost buttons, checkboxes,
|
||||
// radios, the colour swatch. NOT text-field-only, whatever the old names
|
||||
// claimed.
|
||||
final Color controlFill;
|
||||
final Color controlFillHovered;
|
||||
final Color controlFillFocused;
|
||||
final Color controlBorder;
|
||||
|
||||
/// A Switch's track while its off. Its own slot because nothing else wants
|
||||
/// this colour and it used to squat on `input`.
|
||||
final Color switchTrackInactive;
|
||||
|
||||
final Color ring;
|
||||
final Color chart1;
|
||||
final Color chart2;
|
||||
final Color chart3;
|
||||
final Color chart4;
|
||||
final Color chart5;
|
||||
|
||||
// header/footer chrome, nudged off background so it reads as chrome.
|
||||
final Color chrome;
|
||||
|
||||
// borders of a content panel. The panel FILL is just `background` - all ten
|
||||
// hand authored schemes had them identical, so theres no slot for it.
|
||||
final Color panelBorder;
|
||||
final Color panelBorderHighlighted;
|
||||
|
||||
/// Hovered row, in a list or a menu. One slot: menuItemHovered and
|
||||
/// explorerRowHovered were the same colour in all ten schemes.
|
||||
final Color rowHovered;
|
||||
|
||||
/// Resting label/icon colour for a row. Was explorerRowText, and
|
||||
/// propertiesSectionLabel was the same colour in all ten schemes.
|
||||
final Color rowText;
|
||||
|
||||
// Interactive rows inside popovers. Kept separate from row hover while the
|
||||
// two interaction colours are being evaluated.
|
||||
final Color popoverItemHovered;
|
||||
|
||||
/// Outline of an object properties section. Measured off `card`, which is
|
||||
/// what fills one.
|
||||
final Color propertiesSectionBorder;
|
||||
|
||||
ColourScheme copyWith({
|
||||
Brightness? brightness,
|
||||
Color? background,
|
||||
Color? foreground,
|
||||
Color? card,
|
||||
Color? popover,
|
||||
Color? popoverBorder,
|
||||
Color? tooltipBackground,
|
||||
Color? tooltipBorder,
|
||||
Color? primary,
|
||||
Color? primaryHovered,
|
||||
Color? primaryForeground,
|
||||
Color? secondary,
|
||||
Color? secondaryHovered,
|
||||
Color? secondaryForeground,
|
||||
Color? muted,
|
||||
Color? mutedForeground,
|
||||
Color? destructive,
|
||||
Color? border,
|
||||
Color? divider,
|
||||
Color? surfaceSunken,
|
||||
Color? controlFill,
|
||||
Color? controlFillHovered,
|
||||
Color? controlFillFocused,
|
||||
Color? controlBorder,
|
||||
Color? switchTrackInactive,
|
||||
Color? ring,
|
||||
Color? chart1,
|
||||
Color? chart2,
|
||||
Color? chart3,
|
||||
Color? chart4,
|
||||
Color? chart5,
|
||||
Color? chrome,
|
||||
Color? panelBorder,
|
||||
Color? panelBorderHighlighted,
|
||||
Color? rowHovered,
|
||||
Color? rowText,
|
||||
Color? popoverItemHovered,
|
||||
Color? propertiesSectionBorder,
|
||||
}) {
|
||||
return ColourScheme(
|
||||
brightness: brightness ?? this.brightness,
|
||||
background: background ?? this.background,
|
||||
foreground: foreground ?? this.foreground,
|
||||
card: card ?? this.card,
|
||||
popover: popover ?? this.popover,
|
||||
popoverBorder: popoverBorder ?? this.popoverBorder,
|
||||
tooltipBackground: tooltipBackground ?? this.tooltipBackground,
|
||||
tooltipBorder: tooltipBorder ?? this.tooltipBorder,
|
||||
primary: primary ?? this.primary,
|
||||
primaryHovered: primaryHovered ?? this.primaryHovered,
|
||||
primaryForeground: primaryForeground ?? this.primaryForeground,
|
||||
secondary: secondary ?? this.secondary,
|
||||
secondaryHovered: secondaryHovered ?? this.secondaryHovered,
|
||||
secondaryForeground: secondaryForeground ?? this.secondaryForeground,
|
||||
muted: muted ?? this.muted,
|
||||
mutedForeground: mutedForeground ?? this.mutedForeground,
|
||||
destructive: destructive ?? this.destructive,
|
||||
border: border ?? this.border,
|
||||
divider: divider ?? this.divider,
|
||||
surfaceSunken: surfaceSunken ?? this.surfaceSunken,
|
||||
controlFill: controlFill ?? this.controlFill,
|
||||
controlFillHovered: controlFillHovered ?? this.controlFillHovered,
|
||||
controlFillFocused: controlFillFocused ?? this.controlFillFocused,
|
||||
controlBorder: controlBorder ?? this.controlBorder,
|
||||
switchTrackInactive: switchTrackInactive ?? this.switchTrackInactive,
|
||||
ring: ring ?? this.ring,
|
||||
chart1: chart1 ?? this.chart1,
|
||||
chart2: chart2 ?? this.chart2,
|
||||
chart3: chart3 ?? this.chart3,
|
||||
chart4: chart4 ?? this.chart4,
|
||||
chart5: chart5 ?? this.chart5,
|
||||
chrome: chrome ?? this.chrome,
|
||||
panelBorder: panelBorder ?? this.panelBorder,
|
||||
panelBorderHighlighted:
|
||||
panelBorderHighlighted ?? this.panelBorderHighlighted,
|
||||
rowHovered: rowHovered ?? this.rowHovered,
|
||||
rowText: rowText ?? this.rowText,
|
||||
popoverItemHovered: popoverItemHovered ?? this.popoverItemHovered,
|
||||
propertiesSectionBorder:
|
||||
propertiesSectionBorder ?? this.propertiesSectionBorder,
|
||||
);
|
||||
}
|
||||
|
||||
// Accent override - swaps primary/ring to the given accent colour, and picks a
|
||||
// readable foreground for it. Mirrors what shadcns recolor() did. Pass null
|
||||
// (the "none" accent) to leave the scheme untouched.
|
||||
ColourScheme withAccent(Color? accent) {
|
||||
if (accent == null) return this;
|
||||
return copyWith(
|
||||
primary: accent,
|
||||
primaryHovered: accent,
|
||||
primaryForeground: _contrastColor(accent),
|
||||
ring: accent,
|
||||
);
|
||||
}
|
||||
|
||||
static ColourScheme lerp(ColourScheme a, ColourScheme b, double t) {
|
||||
if (t <= 0) return a;
|
||||
if (t >= 1) return b;
|
||||
Color c(Color x, Color y) => Color.lerp(x, y, t)!;
|
||||
return ColourScheme(
|
||||
brightness: t < 0.5 ? a.brightness : b.brightness,
|
||||
background: c(a.background, b.background),
|
||||
foreground: c(a.foreground, b.foreground),
|
||||
card: c(a.card, b.card),
|
||||
popover: c(a.popover, b.popover),
|
||||
popoverBorder: c(a.popoverBorder, b.popoverBorder),
|
||||
tooltipBackground: c(a.tooltipBackground, b.tooltipBackground),
|
||||
tooltipBorder: c(a.tooltipBorder, b.tooltipBorder),
|
||||
primary: c(a.primary, b.primary),
|
||||
primaryHovered: c(a.primaryHovered, b.primaryHovered),
|
||||
primaryForeground: c(a.primaryForeground, b.primaryForeground),
|
||||
secondary: c(a.secondary, b.secondary),
|
||||
secondaryHovered: c(a.secondaryHovered, b.secondaryHovered),
|
||||
secondaryForeground: c(a.secondaryForeground, b.secondaryForeground),
|
||||
muted: c(a.muted, b.muted),
|
||||
mutedForeground: c(a.mutedForeground, b.mutedForeground),
|
||||
destructive: c(a.destructive, b.destructive),
|
||||
border: c(a.border, b.border),
|
||||
divider: c(a.divider, b.divider),
|
||||
surfaceSunken: c(a.surfaceSunken, b.surfaceSunken),
|
||||
controlFill: c(a.controlFill, b.controlFill),
|
||||
controlFillHovered: c(a.controlFillHovered, b.controlFillHovered),
|
||||
controlFillFocused: c(a.controlFillFocused, b.controlFillFocused),
|
||||
controlBorder: c(a.controlBorder, b.controlBorder),
|
||||
switchTrackInactive: c(a.switchTrackInactive, b.switchTrackInactive),
|
||||
ring: c(a.ring, b.ring),
|
||||
chart1: c(a.chart1, b.chart1),
|
||||
chart2: c(a.chart2, b.chart2),
|
||||
chart3: c(a.chart3, b.chart3),
|
||||
chart4: c(a.chart4, b.chart4),
|
||||
chart5: c(a.chart5, b.chart5),
|
||||
chrome: c(a.chrome, b.chrome),
|
||||
panelBorder: c(a.panelBorder, b.panelBorder),
|
||||
panelBorderHighlighted: c(
|
||||
a.panelBorderHighlighted,
|
||||
b.panelBorderHighlighted,
|
||||
),
|
||||
rowHovered: c(a.rowHovered, b.rowHovered),
|
||||
rowText: c(a.rowText, b.rowText),
|
||||
popoverItemHovered: c(a.popoverItemHovered, b.popoverItemHovered),
|
||||
propertiesSectionBorder: c(
|
||||
a.propertiesSectionBorder,
|
||||
b.propertiesSectionBorder,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// [base]'s lightness in CIE L*, 0 (black) to 100 (white).
|
||||
double lstarOf(Color base) => _lstar(base);
|
||||
|
||||
/// [base] moved [points] in CIE L*, keeping its hue and saturation.
|
||||
///
|
||||
/// Reflects rather than clamps when theres no room that way: a colour that
|
||||
/// cant get [points] lighter gets [points] darker instead, keeping the size of
|
||||
/// the step and losing only its direction. A clamp doesnt shorten a step, it
|
||||
/// deletes it - two slots asking for +6 and +11 against a near-white ground
|
||||
/// both land on white and a distinction that exists in every other scheme is
|
||||
/// gone.
|
||||
///
|
||||
/// This is the same step [ColourScheme.derive] is built out of, exposed so an
|
||||
/// app deriving extra slots of its own (Arcs & Angles' canvas colours) walks
|
||||
/// the identical ladder rather than reinventing a near-miss of it.
|
||||
Color shiftLstar(Color base, double points) {
|
||||
final l = _lstar(base);
|
||||
final wanted = l + points;
|
||||
return _atLstar(base, (wanted > 100 || wanted < 0) ? l - points : wanted);
|
||||
}
|
||||
|
||||
// chrome is the deepest recessed slot the shared scheme has, so its want sets
|
||||
// the squeeze factor for every other one.
|
||||
const double _deepestSink = 12.6;
|
||||
|
||||
// ── CIE L* ───────────────────────────────────────────────────────────────
|
||||
// The offsets above are all in L*, which is perceptually uniform - five
|
||||
// points looks like the same step whether youre near black or near white.
|
||||
// HSL lightness, which this used to use, very much does not.
|
||||
|
||||
double _channel(double v) =>
|
||||
v <= 0.04045 ? v / 12.92 : math.pow((v + 0.055) / 1.055, 2.4).toDouble();
|
||||
|
||||
double _lstar(Color c) {
|
||||
final y =
|
||||
0.2126 * _channel(c.r) + 0.7152 * _channel(c.g) + 0.0722 * _channel(c.b);
|
||||
return y > 0.008856 ? 116 * math.pow(y, 1 / 3).toDouble() - 16 : 903.3 * y;
|
||||
}
|
||||
|
||||
// [base]'s hue and saturation at the given L*.
|
||||
//
|
||||
// Theres no closed form for this that keeps HSL saturation fixed, so it
|
||||
// bisects on HSL lightness instead - which is fine, its monotonic in
|
||||
// luminance and this runs once per scheme at startup, not per frame.
|
||||
Color _atLstar(Color base, double target) {
|
||||
final hsl = HSLColor.fromColor(base);
|
||||
final want = target.clamp(0.0, 100.0);
|
||||
|
||||
var lo = 0.0;
|
||||
var hi = 1.0;
|
||||
for (var i = 0; i < 18; i++) {
|
||||
final mid = (lo + hi) / 2;
|
||||
if (_lstar(hsl.withLightness(mid).toColor()) < want) {
|
||||
lo = mid;
|
||||
} else {
|
||||
hi = mid;
|
||||
}
|
||||
}
|
||||
return hsl.withLightness((lo + hi) / 2).toColor();
|
||||
}
|
||||
|
||||
// flips lightness to get a readable foreground on a given colour. same idea as
|
||||
// shadcns getContrastColor (full luminance contrast).
|
||||
Color _contrastColor(Color on) {
|
||||
final hsl = HSLColor.fromColor(on);
|
||||
final l = hsl.lightness;
|
||||
final target = l >= 0.5 ? 0.0 : 1.0;
|
||||
// nudge toward the target rather than pure black/white so it doesnt look harsh
|
||||
final mixed = lerpDouble(l, target, 1.0)!;
|
||||
return hsl.withLightness(mixed.clamp(0.0, 1.0)).toColor();
|
||||
}
|
||||
|
||||
// a focus ring wants to read punchier than a resting "primary" swatch -
|
||||
// lighter and more saturated, so it pops against whatevers behind it instead
|
||||
// of just matching a button colour.
|
||||
Color _brightenForSelectionRing(Color colour) {
|
||||
final hsl = HSLColor.fromColor(colour);
|
||||
return hsl
|
||||
.withSaturation((hsl.saturation + 0.16).clamp(0.0, 1.0))
|
||||
.withLightness((hsl.lightness + 0.13).clamp(0.0, 0.78))
|
||||
.toColor();
|
||||
}
|
||||
Reference in New Issue
Block a user