Colour system
Gradients
Two natural-law gradients, atmospheric extinction and the Moffat profile, built only from opacity ladder steps.
Atmospheric extinction
Light from a star loses brightness as it crosses more atmosphere. The loss in magnitudes is proportional to the relative airmass minus one, with an extinction coefficient :
The airmass for a zenith angle in degrees comes from the Kasten and Young approximation:
Stops sit at z = 0, 60, 70, 78, 84, 88 and 90 degrees. Each stop's position along the gradient is , so the zenith is at 0% and the horizon at 100%.
gradient-extinction [--grad-color:var(--color-deep-space-300)]With the default direction the top edge is the zenith and the bottom edge is the horizon.
| z (deg) | Airmass X | Position | Raw alpha | Snapped step | Snapped alpha | CSS stop | Match |
|---|---|---|---|---|---|---|---|
| 0 | 1.000 | 0% | 1.0000 | --mag-0 | 100.00% | --mag-0 @ 0% | ✓ |
| 60 | 1.994 | 66.7% | 0.8326 | --mag-0 | 100.00% | --mag-0 @ 66.7% | ✓ |
| 70 | 2.903 | 77.8% | 0.7043 | --mag-1 | 63.10% | --mag-1 @ 77.8% | ✓ |
| 78 | 4.709 | 86.7% | 0.5050 | --mag-1 | 63.10% | --mag-1 @ 86.7% | ✓ |
| 84 | 8.841 | 93.3% | 0.2359 | --mag-3 | 25.12% | --mag-3 @ 93.3% | ✓ |
| 88 | 19.433 | 97.8% | 0.0335 | --mag-7 | 3.98% | --mag-7 @ 97.8% | ✓ |
| 90 | 37.920 | 100% | 0.0011 | transparent | 0.00% | transparent @ 100% | ✓ |
Moffat profile
A point source blurred by the atmosphere spreads into a glow described by the Moffat function of radius and width :
The gradient reaches full transparency at a radius of four widths:
so a stop at position sits at , where the position is measured as .
gradient-moffat [--grad-color:var(--color-deep-space-300)]The centre is the source; intensity falls off to transparent at 60% of the radius.
| Position p | r / a | I(r) | Snapped step | Snapped alpha | CSS stop | Match |
|---|---|---|---|---|---|---|
| 0% | 0.00 | 1.0000 | --mag-0 | 100.00% | --mag-0 @ 0% | ✓ |
| 10% | 0.40 | 0.6407 | --mag-1 | 63.10% | --mag-1 @ 10% | ✓ |
| 20% | 0.80 | 0.2267 | --mag-3 | 25.12% | --mag-3 @ 20% | ✓ |
| 30% | 1.20 | 0.0688 | --mag-6 | 6.31% | --mag-6 @ 30% | ✓ |
| 40% | 1.60 | 0.0222 | --mag-8 | 2.51% | --mag-8 @ 40% | ✓ |
| 50% | 2.00 | 0.0080 | --mag-10 | 1.00% | --mag-10 @ 50% | ✓ |
| 60% | 2.40 | 0.0032 | transparent | 0.00% | transparent @ 60% | ✓ |
| 100% | 4.00 | 0.0002 | transparent | 0.00% | transparent @ 100% | ✓ |
Usage
<div class="gradient-extinction [--grad-color:var(--color-deep-space-300)]"></div><div class="gradient-moffat [--grad-color:var(--color-space-grey-200)]"></div>@utility gradient-extinction { background-image: linear-gradient( in srgb var(--grad-dir, to bottom), color-mix(in srgb, var(--grad-color) var(--mag-0), transparent) 0%, color-mix(in srgb, var(--grad-color) var(--mag-0), transparent) 66.7%, color-mix(in srgb, var(--grad-color) var(--mag-1), transparent) 77.8%, color-mix(in srgb, var(--grad-color) var(--mag-1), transparent) 86.7%, color-mix(in srgb, var(--grad-color) var(--mag-3), transparent) 93.3%, color-mix(in srgb, var(--grad-color) var(--mag-7), transparent) 97.8%, transparent 100% );}@utility gradient-moffat { background-image: radial-gradient( in srgb circle closest-side, color-mix(in srgb, var(--grad-color) var(--mag-0), transparent) 0%, color-mix(in srgb, var(--grad-color) var(--mag-1), transparent) 10%, color-mix(in srgb, var(--grad-color) var(--mag-3), transparent) 20%, color-mix(in srgb, var(--grad-color) var(--mag-6), transparent) 30%, color-mix(in srgb, var(--grad-color) var(--mag-8), transparent) 40%, color-mix(in srgb, var(--grad-color) var(--mag-10), transparent) 50%, transparent 60%, transparent 100% );}import 'dart:math' as math;import 'package:flutter/painting.dart';import 'astr_opacity.dart';/// Astr gradients: natural-law alpha profiles snapped to the [Mag] ladder.////// Both gradients take any colour stop as [base]. Fading to the same colour at/// alpha 0 (not `Colors.transparent`, which is transparent black) keeps the/// tail from greying, because Flutter interpolates gradient stops without/// premultiplying alpha.abstract final class AstrGradients { // --- Atmospheric extinction (zenith to horizon) --------------------------- /// Extinction coefficient, magnitudes per airmass. static const double extinctionK = 0.2; /// Zenith angles in degrees; stop position is `z / 90`. static const List<double> extinctionZs = <double>[0, 60, 70, 78, 84, 88, 90]; /// Kasten-Young relative airmass, `z` in degrees. static double airmass(double zDeg) { final double rad = zDeg * math.pi / 180; return 1 / (math.cos(rad) + 0.50572 * math.pow(96.07995 - zDeg, -1.6364)); } /// alpha = 10^(-0.4 * k * (X - 1)) static double extinctionAlpha(double zDeg) => math .min(1.0, math.pow(10, -0.4 * extinctionK * (airmass(zDeg) - 1))) .toDouble(); // Positions and ladder steps precomputed from the model above and verified // against `gradient-extinction` in globals.css. A null step is transparent. static const List<double> _extinctionStops = <double>[ 0, 0.667, 0.778, 0.867, 0.933, 0.978, 1, ]; static const List<int?> _extinctionSteps = <int?>[0, 0, 1, 1, 3, 7, null]; /// Zenith-to-horizon fade. Defaults to top (zenith) down to bottom (horizon). static LinearGradient extinction( Color base, { AlignmentGeometry begin = Alignment.topCenter, AlignmentGeometry end = Alignment.bottomCenter, }) { return LinearGradient( begin: begin, end: end, colors: _colors(base, _extinctionSteps), stops: _extinctionStops, ); } // --- Moffat profile (radial glow) ----------------------------------------- static const double moffatBeta = 3; /// Gradient radius in units of the profile's scale length `a`. static const double moffatRadiusInA = 4; /// I(r) = (1 + (r / a)^2)^-beta static double moffatIntensity(double rOverA) => math.pow(1 + rOverA * rOverA, -moffatBeta).toDouble(); static const List<double> _moffatStops = <double>[ 0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 1, ]; static const List<int?> _moffatSteps = <int?>[0, 1, 3, 6, 8, 10, null, null]; /// Bright core with a long soft tail. `radius: 0.5` is the closest side, /// matching CSS `radial-gradient(circle closest-side, ...)`. static RadialGradient moffat( Color base, { AlignmentGeometry center = Alignment.center, }) { return RadialGradient( center: center, radius: 0.5, colors: _colors(base, _moffatSteps), stops: _moffatStops, ); } static List<Color> _colors(Color base, List<int?> steps) => <Color>[ for (final int? s in steps) base.withValues(alpha: s == null ? 0 : Mag.steps[s]), ];}