Show HN: Ember - Redshift safe color palettes
Terminal, chart, heatmap, and UI palettes that remain usable before and after aggressive warm-screen filtering. Ember was created to solve the problem of traditional color palettes having colors that appear indistinguishable once you turn on redshift. With Ember, the primary goal is distinctive colors under various color temperature filters. Only once that's satisfied do we further tweak the redshift-degenerate color channels to improve visual separation during daytime (aka - with no redshift applied). Ember comes with 4 color palettes: The 3400K palettes are good for general use, and approximately optimized for a Macbook's maximum Nightshift effect. The 2000K and 1200K palettes are more specialized for true "deep redshift" fans (and astronomers). One interesting consequence is that the 2000K and 1200K extreme redshift palettes have fewer distinct colors than the 3400K palettes. This is because such extreme redshifts dramatically reduce the size of the perceptual color space available to choose colors from! More on this below... Each palette is authored in commanded sRGB, verified under its corresponding color temperature's modeled per-channel RGB gains, and exported as terminal themes (Alacritty, iTerm2, Windows Terminal), UI surface roles, categorical chart colors, a 256-sample sequential map, and Matplotlib/CSS/JSON/Python artifacts. Ember does not apply the filter; keep using the one you already have. These are the exact commanded sRGB values shipped in every export. The deeper the target filter, the fewer color identities a family authors; aliased terminal slots are labeled as aliases instead of posing as additional colors. A warm filter is not a tinted overlay: it multiplies red, green, and blue by different gains. At Ember's 1200 K model the blue gain is zero, so colors that differ only in blue produce identical output. The deeper palettes therefore author fewer identities instead of pretending aliased colors remain distinct. Filtered rows are deterministic signal simulations - commanded sRGB multiplied by each profile's published gains. They are not photographs, calibrated physical color temperatures, or predictions of every display pipeline. Turn off any active warm filter before judging them, or your screen applies the transform a second time. | Palette | Use it when | Distinct categories | |---|---|---| 3400k-dark | you want a near-black general-purpose warm theme | 6 | 3400k-light | you use a moderate warm shift on a light surface | 6 | 2000k-dark | you run Redshift near 2000 K | 4 | 1200k-dark | you want an extreme 1200 K stress profile | 3 | Start with 3400k-dark unless you deliberately run a deeper filter. The 2000 K and 1200 K profiles are dark-only because a filtered light canvas becomes a large orange-red field. For terminal themes, CSS, and the JSON manifest: git clone https://github.com/carpdiem/ember.git cd ember Python users can skip the clone and install directly from GitHub: python -m pip install "ember-palettes @ git+https://github.com/carpdiem/ember.git" - Alacritty: copy one file from themes/terminal/alacritty/ into your config directory, then import it fromalacritty.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 Settings β Profiles β Colors β Color Presetsβ¦ β Importβ¦ and choose a file from themes/terminal/iterm2/ . - Windows Terminal: open Settings β Open JSON file, copy one object from themes/terminal/windows-terminal/ into the rootschemes array, then set your profile'scolorScheme to its exactname . The terminal guide has the complete import steps and explains how reduced ANSI banks behave under the deep profiles. Every family exposes the same ordered roles in JSON, CSS, and the Python surfaces() API: | Role | Intended use | |---|---| bg_0 | base application canvas | bg_1 | low-emphasis adjacent region or sidebar | bg_2 | ordinary panel or card | bg_3 | nested panel, active control, or hover state | bg_4 | floating panel, menu, or popover | bg_5 | selected row, range, or focused region | fg_0 | primary text and essential labels | fg_1 | larger supporting text or graphics; not normal-size body text | fg_2 | muted, nonessential metadata or decoration | The six backgrounds form a monotonic ladder. bg_0 is always the canvas and bg_5 is the strongest background state: dark families become lighter toward bg_5 , while the light family becomes darker toward bg_5 . 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() Pass the palette slug to categorical_norm() and encode_categories() so their capacity checks match the selected family. Sequential maps always expose 256 canonical float samples, independent of the number of categorical colors. Load ember.css , then select a family: ... [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 exposes eleven representative 8-bit gradient stops. The JSON manifest and Python package preserve all 256 canonical float samples, along with surfaces, categorical colors, ANSI slots, gain profiles, and measured results. Ember approximates a warm display transform as an independent gain on each sRGB channel: display RGB β commanded RGB Γ [red gain, green gain, blue gain] | Profile | RGB gains | Basis | |---|---|---| 3400k | [1.00, 0.74, 0.53] | warm-white engineering surrogate | 2000k | [1.0000, 0.5436, 0.0868] | pinned Redshift 2000 K signal LUT | 1200k | [1.0000, 0.3094, 0.0000] | pinned Redshift 1200 K signal LUT | At 1200 K, blue contributes nothing to the modeled output. At 2000 K, only 9% survives. Ordinary sRGB distance is therefore a bad proxy for nighttime distinction: two colors can be far apart by day and converge after filtering. These are software signal models, not calibrated physical color temperatures. A real result also depends on the display, operating system, calibration, brightness, and ambient light. The JSON manifest also reports sensitivity diagnostics at four Β±5% green/blue gain corners for categorical colors, terminal groups, foreground/surface contrast, and sequential spacing. These sampled corners expose nearby model sensitivity; they are not extrema over every point inside a continuous gain box and are not display calibration measurements. Ember treats day and night as two views of the same commanded color. It does not average their quality into one score, because excellent daytime spacing cannot compensate for a nighttime collision. - Set hard transformed targets for contrast, lightness/chroma geometry, and minimum perceptual separation in Oklab. - Among the commanded colors that reproduce those targets, choose a moderate-chroma daytime set with strong unfiltered separation. This reverses the usual workflow. At 1200 K, changing only blue cannot disturb the transformed color, so Ember can use that otherwise lost channel to improve daytime identity. At 2000 K, the same freedom is smaller because a weak blue residual remains. Generated release checks keep every serialized accent within 0.15 ΞEOK of its authored transformed target. Categorical colors must also clear the complete fg_0 / fg_1 / fg_2 ladder in both states, not merely remain distinct from one another. The 2000 K and 1200 K banks repeat their category-spacing, foreground-clearance, and background-contrast checks at all four sampled gain corners. Human vision carries fine spatial detail more strongly through luminance than chromatic channels. Dense saturated glyphs and opposing hues are therefore poor places to spend a limited nighttime color gamut. The comparison below shows the practical consequence: pure white becomes a brighter transformed orange than Ember's cream body text, while a daytime dark gray becomes a much larger rust-colored signal than Ember's near-black canvas. Ember puts most pixels in warm-neutral surfaces, uses cream rather than pure white for body text, and reserves higher chroma for semantic accents. Every foreground-capable terminal accent still clears 4.5:1 contrast against the terminal base background (bg_0 ) after its target transform. fg_1 and fg_2 remain available for larger supporting text and nonessential metadata. Under the current release gates, Ember supports six categorical identities at 3400 K, four at 2000 K, and three at 1200 K. Deep terminal themes repeat those supported capacities across the sixteen ANSI slots; unsupported names alias deliberately instead of pretending to add another color identity. An accent may change apparent hue between states; it must remain distinguishable in both. Every terminal bank is evaluated with all three foreground roles so an accent cannot pass by colliding with ordinary, supporting, or muted text. Every foreground trio must remain one ordered warm-neutral ladder rather than three unrelated colors. Color is still not enough for critical identity. Charts should combine it with direct labels, position, dash pattern, marker shape, or texture: Each sequential map begins with a human-chosen earth-tone path. The generator smooths that path in Oklab, m
Comments
No comments yet. Start the discussion.