Ember: Redshift-veilige kleurpaletten

Introductie

Ember is ontwikkeld om het probleem op te lossen waarbij traditionele kleurpaletten kleuren bevatten die ononderscheidbaar worden zodra redshift wordt ingeschakeld. Het primaire doel van Ember is het bieden van onderscheidende kleuren onder verschillende kleurtemperatuurfilters. Pas wanneer dit is gerealiseerd, worden de door redshift gedegradeerde kleurkanalen verder aangepast om de visuele scheiding overdag (dus zonder redshift) te verbeteren.

Ember bevat vier kleurpaletten:

  • 3400K Dark
  • 3400K Light
  • 2000K Dark
  • 1200K Dark

De 3400K-paletten zijn geschikt voor algemeen gebruik en zijn ongeveer geoptimaliseerd voor het maximale Nightshift-effect van een Macbook. De 2000K- en 1200K-paletten zijn meer gespecialiseerd voor liefhebbers van "diepe redshift" (en astronomen).

Een interessant gevolg is dat de extreme redshift-paletten (2000K en 1200K) minder verschillende kleuren hebben dan de 3400K-paletten. Dit komt doordat dergelijke extreme redshifts de omvang van de perceptuele kleurruimte waaruit kleuren kunnen worden gekozen, drastisch verminderen.

Elk palet is gecreëerd in opgegeven (commanded) sRGB, geverifieerd onder de gemodelleerde per-kanaal RGB-winsten van de corresponderende kleurtemperatuur, en geëxporteerd als terminalthema's (Alacritty, iTerm2, Windows Terminal), UI-oppervlakkerollen, categorische grafiekkleuren, een sequentiële kaart met 256 monsters, en Matplotlib/CSS/JSON/Python-artefacten.

Let op: Ember past het filter niet zelf toe; blijf de software gebruiken die u al gebruikt.

De vier paletten

  • 3400K Dark
  • 3400K Light
  • 2000K Dark
  • 1200K Dark

Dit zijn de exacte opgegeven sRGB-waarden in elke export. Hoe dieper het doelfilter, hoe minder kleuridentiteiten een familie bevat; aliased terminal-slots worden gelabeld als aliassen in plaats van als extra kleuren.

Met en zonder redshift

  • In een terminal
  • In grafieken en heatmaps

Waarom identiteiten verdwijnen

Een warm filter is geen getinte overlay: het vermenigvuldigt rood, groen en blauw met verschillende winstfactoren (gains). Bij het 1200K-model van Ember is de blauwe winst nul, waardoor kleuren die alleen in blauw verschillen, een identiek resultaat geven. De diepere paletten bevatten daarom minder identiteiten, in plaats van te doen alsof aliased kleuren onderscheidend blijven.

Gefilterde rijen zijn deterministische signaalsimulaties — opgegeven sRGB vermenigvuldigd met de gepubliceerde winstfactoren van elk profiel. Het zijn geen foto's, gekalibreerde fysieke kleurtemperaturen of voorspellingen van elke display-pipeline. Schakel elk actief warm filter uit voordat u ze beoordeelt, anders past uw scherm de transformatie een tweede keer toe.

Kies een profiel

PaletGebruik het wanneer...Onderscheidende categorieën
3400k-darku een bijna-zwart algemeen warm thema wilt6
3400k-lightu een matige warme verschuiving op een licht oppervlak gebruikt6
2000k-darku Redshift rond de 2000 K draait4
1200k-darku een extreem 1200 K stresstprofiel wilt3

Begin met 3400k-dark, tenzij u bewust een dieper filter gebruikt. De 2000K- en 1200K-profielen zijn alleen beschikbaar voor donkere modi, omdat een gefilterd licht canvas verandert in een groot oranje-rood veld.

Ember implementeren

1. De bestanden verkrijgen

Voor terminalthema's, CSS en het JSON-manifest:

git clone https://github.com/carpdiem/ember.git
cd ember

Python-gebruikers kunnen de clone overslaan en direct vanaf GitHub installeren:

python -m pip install "ember-palettes @ git+https://github.com/carpdiem/ember.git"

2. Een terminalthema importeren

Alacritty: kopieer één bestand uit themes/terminal/alacritty/ naar uw config-map, en importeer het vervolgens in alacritty.toml:

mkdir -p ~/.config/alacritty/themes
cp themes/terminal/alacritty/2000k-dark.toml ~/.config/alacritty/themes/

[general]
import = ["~/.config/alacritty/themes/2000k-dark.toml"]

iTerm2: open SettingsProfilesColorsColor Presets…Import… en kies een bestand uit themes/terminal/iterm2/.

Windows Terminal: open SettingsOpen JSON file, kopieer één object uit themes/terminal/windows-terminal/ naar de root schemes array, en stel vervolgens de colorScheme van uw profiel in op de exacte naam.

De terminal-gids bevat de volledige importstappen en legt uit hoe gereduceerde ANSI-banken zich gedragen onder de diepe profielen.

3. De UI-oppervlakkerollen gebruiken

Elke familie biedt dezelfde geordende rollen aan in JSON, CSS en de Python surfaces() API:

RolBeoogd gebruik
bg_0basis canvas van de applicatie
bg_1aangrenzende regio met lage nadruk of zijbalk
bg_2gewoon paneel of kaart
bg_3genest paneel, actieve besturing of hover-status
bg_4zwevend paneel, menu of popover
bg_5geselecteerde rij, bereik of gefocuste regio
fg_0primaire tekst en essentiële labels
fg_1grotere ondersteunende tekst of graphics; geen normale hoofdtekst
fg_2gedempte, niet-essentiële metadata of decoratie

De zes achtergronden vormen een monotone ladder. bg0 is altijd het canvas en bg5 is de sterkste achtergrondstatus: donkere families worden lichter richting bg5, terwijl de lichte familie donkerder wordt richting bg5.

Matplotlib

import matplotlib.pyplot as plt
from ember import categorical, categorical_norm, encode_categories, sequential, surfaces

palette = "2000k-dark"
ui = surfaces(palette)
labels = ["control", "alpha", "beta", "gamma"]
order = ["control", "alpha", "beta", "gamma"]
category_ids = encode_categories(labels, order, slug=palette)

fig, (points, image) = plt.subplots(1, 2)
fig.patch.set_facecolor(ui["bg_0"])
points.set_facecolor(ui["bg_2"])
image.set_facecolor(ui["bg_3"])

points.scatter(
    [1, 2, 3, 4],
    [1.2, 2.4, 1.8, 3.1],
    c=category_ids,
    cmap=categorical(palette),
    norm=categorical_norm(palette),
)
image.imshow([[0.0, 0.4], [0.7, 1.0]], cmap=sequential(palette))
plt.show()

Geef de palette slug door aan categoricalnorm() en encodecategories() zodat hun capaciteitscontroles overeenkomen met de geselecteerde familie. Sequentiële kaarten bieden altijd 256 canonieke float-monsters, onafhankelijk van het aantal categorische kleuren.

CSS

Laad ember.css en selecteer vervolgens een familie:

<link rel="stylesheet" href="/path/to/ember.css">
<section data-ember-palette="3400k-dark">
  <div class="panel">...</div>
</section>
[data-ember-palette] {
  color: var(--ember-fg-0);
  background: var(--ember-bg-0);
}
.panel {
  background: var(--ember-bg-2);
}
.panel:hover {
  background: var(--ember-bg-3);
}
.popover {
  background: var(--ember-bg-4);
}
.selected {
  background: var(--ember-bg-5);
}
.series-a {
  color: var(--ember-category-one);
}
.heatmap-key {
  background: var(--ember-sequential);
}

CSS biedt elf representatieve 8-bit gradiëntstops. Het JSON-manifest en het Python-pakket behouden alle 256 canonieke float-monsters, samen met oppervlakken, categorische kleuren, ANSI-slots, winstprofielen en gemeten resultaten.

Wat is de wetenschap hierachter?

1. Het signaal modelleren dat het display bereikt

Ember benadert een warme display-transformatie als een onafhankelijke winst op elk sRGB-kanaal: display RGB ≈ commanded RGB × [red gain, green gain, blue gain]

ProfielRGB winstfactorenBasis
3400k[1.00, 0.74, 0.53]warm-wit engineering surrogaat
2000k[1.0000, 0.5436, 0.0868]pinned Redshift 2000 K signaal LUT
1200k[1.0000, 0.3094, 0.0000]pinned Redshift 1200 K signaal LUT

Bij 1200 K draagt blauw niets bij aan de gemodelleerde output. Bij 2000 K overleeft slechts 9%. Gewone sRGB-afstand is daarom een slechte proxy voor onderscheid in de nacht: twee kleuren kunnen overdag ver uit elkaar liggen en convergeren na filtering.

Dit zijn software-signaalmodellen, geen gekalibreerde fysieke kleurtemperaturen. Een echt resultaat hangt ook af van het display, besturingssysteem, kalibratie, helderheid en omgevingslicht.

2. Eerst de beperkte staat oplossen

Ember behandelt dag en nacht als twee weergaven van dezelfde opgegeven kleur. Het middelt hun kwaliteit niet tot één score, omdat uitstekende dagelijkse spreiding een nachtelijke botsing niet kan compenseren.

  • Stel harde getransformeerde doelen in voor contrast, lichtheid/chroma-geometrie en minimale perceptuele scheiding in Oklab.
  • Kies uit de opgegeven kleuren die deze doelen reproduceren een set met een matige chroma voor overdag met sterke ongefilterde scheiding.

Dit draait de gebruikelijke workflow om. Bij 1200 K kan het wijzigen van alleen blauw de getransformeerde kleur niet verstoren, waardoor Ember dat anders verloren kanaal kan gebruiken om de identiteit overdag te verbeteren.

3. Frequente pixels neutraal houden en kleur reserveren voor betekenis

Het menselijk gezichtsvermogen transporteert fijne ruimtelijke details sterker via luminantiekanalen dan via chromatische kanalen. Verzadigde glyphs en tegenovergestelde tinten zijn daarom ongeschikte plekken om een beperkt nachtelijk kleurgamma te gebruiken.

Ember plaatst de meeste pixels in warm-neutrale oppervlakken, gebruikt crème in plaats van puur wit voor hoofdtekst, en reserveert hogere chroma voor semantische accenten.

4. Identiteit beschermen met zowel kleur als structuur

Ember ondersteunt zes categorische identiteiten bij 3400 K, vier bij 2000 K en drie bij 1200 K. Diepe terminalthema's herhalen deze capaciteiten over de zestien ANSI-slots; niet-ondersteunde namen worden bewust als alias behandeld.

Kleur is nog steeds niet genoeg voor kritieke identiteit. Grafieken moeten dit combineren met directe labels, positie, stippellijnpatronen, markervormen of texturen.

5. Continue kaarten spreiden in de getransformeerde weergave

Elke sequentiële kaart begint met een door mensen gekozen pad in aardetinten. De generator maakt dit pad vloeiend in Oklab, meet de cumulatieve afstand na de doeltitransformatie en bemonstert het opnieuw met gelijke getransformeerde-afstand-intervallen.

Het resultaat is een kaart met 256 monsters met strikt monotone getransformeerde lichtheid en bijna gelijke gemodelleerde getransformeerde Oklab-stappen.

Gemeten eigenschappen en exacte release-gates

Gemeten eigenschappen

$\Delta E_{OK}$ hieronder is de Euclidische Oklab-afstand vermenigvuldigd met 100.

FamilieCategorieënDag min $\Delta E_{OK}$Getransf. min $\Delta E_{OK}$Gem. / max raw chromaGetransf. L bereikMin ANSI contrast
3400K Dark615.0011.450.0971 / 0.10450.22065.29:1
3400K Light616.7312.410.0988 / 0.10370.30684.65:1
2000K Dark417.0012.910.0956 / 0.10820.15384.52:1
1200K Dark320.7210.250.1047 / 0.11070.12684.55:1

Categorie- en achtergrondcontrast:

FamilieDag min. hue gapTargetGetransf. cat / bg_0Target
3400K Dark20.23°$\ge 20°$3.01:1$\ge 3:1$
3400K Light31.75°$\ge 30°$3.03:1$\ge 3:1$
2000K Dark27.76°$\ge 20°$3.05:1$\ge 3:1$
1200K Dark65.03°$\ge 45°$3.12:1$\ge 3:1$

Terminal-bank separatie (accenten versus foreground):

FamilieDag accent minDag → fg_0Dag → fg_1Dag → fg_2Getransf. accent minGetransf. → fg_0Getransf. → fg_1Getransf. → fg_2
3400K Dark10.768.678.1311.727.316.585.1610.18
3400K Light15.649.259.479.2211.076.476.707.13
2000K Dark12.6213.7310.868.257.757.645.034.75
1200K Dark12.359.698.9614.684.134.494.1311.29

Foreground coherentie:

Familiefg0 / fg1 / fg_2Dag adj. stappenGetransf. adj. stappenDag / getransf. gap ratioDag / getransf. hue spanMax dag chroma
3400K Dark#DDD0B2 / #BDAE93 / #90847210.41 / 13.798.70 / 11.880.7568 / 0.73729.32° / 2.63°0.0426
3400K Light#342F2C / #4D4540 / #665C548.75 / 8.527.55 / 7.320.9734 / 0.97070.00° / 1.12°0.0181
2000K Dark#EED5AE / #D3BB99 / #AA9D8B8.04 / 10.446.20 / 9.040.7886 / 0.70082.74° / 2.51°0.0584
1200K Dark#FFE5BD / #CBAF89 / #A18C7316.43 / 11.7911.07 / 9.160.7099 / 0.81776.73° / 0.71°0.0607

Donkere oppervlakte metingen (WCAG sRGB relatieve luminantie):

Donkere familiebg_0Opgegeven luminantie, bg0 → bg5Getransf. fg_0 contrast bereik
3400K Dark#0908070.00247 → 0.020196.83–8.52:1
2000K Dark#0705040.00162 → 0.018525.86–6.98:1
1200K Dark#0603020.00108 → 0.015715.32–6.08:1

De build reproduceren

uv sync --extra dev
uv run python tools/build_all.py --check
uv run pytest -q
uv run ruff check src tests tools examples
uv build

De release-gates handhaven onder andere:

  • Exact vier paletfamilies met categorische capaciteiten 6, 6, 4, 3.
  • Categorische opgegeven gemiddelde Oklab chroma tussen 0.09 en 0.105, met geen kleur boven 0.111.
  • Minimale afstandsvloeren in zowel ongeverschuifde als getransformeerde staten.
  • Terminal dag/nacht capaciteiten 6/6, 6/6, 4/4, 3/3.
  • Maximaal 0.15 $\Delta E_{OK}$ tussen elk geautoriseerd getransformeerd accentdoel en de getransformeerde geserialiseerde kleur.
  • Minimaal 4.5:1 getransformeerd contrast voor foreground-capabele ANSI-slots tegen de terminal basisachtergrond (bg_0).
  • Getransformeerde contrastvloeren van 4.5:1, 3.5:1 en 2.4:1 voor fg0, fg1 en fg_2 op elk oppervlak.
  • Donkere modus opgegeven relatieve luminantie-caps van 0.003, 0.005, 0.009, 0.013, 0.020 en 0.021 over de zes-staps ladder.
  • 256 unieke float-monsters per sequentiële kaart, met monotone lichtheid in beide display-staten.

Referenties

  • Gruvbox: origineel project en palet: warme neutralen, voldoende contrast en terughoudende accenten voor langdurig gebruik.
  • MIT 6.813, Color: chromatische aberratie, de lagere ruimtelijke resolutie van chromatische kanalen, schaarse kleur en terughoudende verzadiging.
  • Fan et al. (2024), The Effect of Ambient Illumination and Text Color on Visual Fatigue under Negative Polarity: gecontroleerd bewijs dat tekstkleur en omgevingsverlichting visuele vermoeidheid beïnvloeden bij donkere polariteit.
  • EU Data Visualisation Guide, Colour for categories: beperk het aantal categorieën en vermijd oververzadigde of heldere kleuren.
  • Datawrapper, What to consider when choosing colors: gebruik neutrale structuren, bescherm contrast van kleine tekst en voeg redundante signalen toe.
  • Redshift: pinned kleur-ramp implementatie en temperatuur tabel.
  • Björn Ottosson: Een perceptuele kleurruimte voor beeldbewerking.
  • Matplotlib: Choosing Colormaps.
  • W3C WAI: Understanding contrast minimum.

Licentie

MIT © 2026 Michael Woods.