Dimidium

TL;DR:

Using Color Appearance Model (CAM), we can deal with human color perception scientifically.
With CAM, I made a standard-looking terminal color scheme with uniform visibility across all colors.
(Preview, Downloads)

한국어

Problem of the 16-colors

Modern terminals can use 24-bit colors, but many applications still use the ANSI 16-color scheme: K, R, G, Y, B, M, C, W, and their bright variants.

Traditional 16-color scheme Traditional 16-color scheme

These default terminal color settings usually have blue that’s too dark. Green is too vibrant and hurts my eyes. 🥶

And there are tons of custom color schemes being shared, each made to their taste and senses.

But none of them fulfill my eyes. Common problems are:

  • Name and color not matching (Red is magenta, blue is purple… etc)
  • Some colors are buried in the background and hard to see
  • ‘Bright’ colors are not defined, or messed up

New Standard Terminal color scheme 🌈

If it doesn’t exist, let’s create it.

Let’s reduce excessive brightness, chroma difference of the traditional terminal color scheme.

So we can get a new color scheme that is standard-looking and well-visible.

First step: Brighten up without changing hue

Let’s start by increasing the lightness of the blue color.

0,0,255 Blue
Even with the maximum B value, ■(0,0,255) it’s still too dark.

100,100,255 Blue
If we increase R, G to raise the lightness, ■(100,100,255) does get brighter, but a reddish tint starts to appear.

We’re in trouble from the start. 🤦

Color Appearance Model (CAM)

As you saw above, the human vision system does not respond linearly to the RGB value. Hue varies even if the same amount of R, G value changed.

This is where we need a CAM. If you’re interested in the latest CSS standards, you might have heard of Oklab and Oklch, which are also CAMs.

Color planes Left: Without CAM / Right: With Oklch

The lightness of the color plane is uneven with HSV (left). But with CAM, it became uniform (right).

Let’s look at the blue again using the Oklch color picker.
Color planes Left: Without CAM / Right: With Oklch

Compared to the top ■ #0000ff, while the left palette shows a reddish tint, the right palette using Oklch shows more appropriate blue color.

Again, First Step: Brighten up without changing hue

By increasing the lightness using Oklch, ■(72,127,255), the reddish tint is gone. It finally looks like a bright blue.

Using a CAM allows us to handle colors in a way that human perception works.

So from now on, I will tweak the terminal color scheme using a CAM. The goal is to maintain the hue while reducing extreme differences in lightness.

CAM16-UCS

The CAM I’ll use is CAM16-UCS (Color Appearance Model 2016 - Uniform Color Space).

CAM16-UCS color gamut Image source: ColorAide Documentation

It represents color using 3 values: J (lightness), a (red-green), b (yellow-blue).

(Conventionally, Lightness is denoted as L* or J to distinguish it from Luminance L)

By converting the J, a, b Cartesian coordinates (x, y, z) to cylindrical coordinates (r, Φ, z), we get the 3 components of the color we want to use:

HSL color cylinder Image source: HSL and HSV - Wikipedia

J: Lightness
C: Chroma
h: Hue

(The diagrams below are for understanding purposes and may differ from the actual CAM and colors.)

Color Adjustment 🖌️

Base is xterm’s default color setting.

I’ll use the Python colour-science package for color conversion. (sRGB → CAM16-UCS)

import colour  # colour-science

# Convert to CAM16-UCS-JCh
xyz = colour.sRGB_to_XYZ(color_rgb/255)
jab = colour.XYZ_to_CAM16UCS(xyz)
color_jch = colour.models.Jab_to_JCh(jab)

Then we separate J, C, h.

j = color_jch[..., 0]
c = color_jch[..., 1]
h = color_jch[..., 2]

Lightness

Let’s start by reducing the gap between the too-dark blue and too-bright green. I’ll Thanos it by half.

(Note: ‘Dimidium’ is Latin for ‘half’.)

j_mean = np.mean(j[2:8])
j[2:8] = (j[2:8] + j_mean) / 2  # colors

j_mean = np.mean(j[10:16])
j[10:16] = (j[10:16] + j_mean) / 2  # bright colors

Before lightness adjust

After lightness adjust

The lightness difference is not entirely removed. Doing so would reduce the color distinction and worsen the clipping issue I’ll explain later, making the result look strange.

Hue

When we plot the colors on a plane, the angular spacing (=hue difference) is uneven. Yellow, in particular, is skewed towards green.

Let’s spread them out equally at 60° intervals to maximize the difference between colors.

# Set hue(mean delta to original is about 3)
h[2:8] = (30, 150, 90, 270, 330, 210)
h[2:8] += 3

h[10:16] = (30, 150, 90, 270, 330, 210)
h[10:16] += 3
Before lightness adjust After lightness adjust
Before: Uneven hue (angle) spacing After: Even hue (angle) spacing

Chroma

The adjusted colors include out-of-gamut values like RGB(-32,266,128) that cannot be displayed on SDR displays. It’s something like over/under-exposure clipping in photography.

To fix this, first, let’s halve the chroma difference.

# Normalize chroma
c_min = np.min(c[2:8])
c[2:8] = (c[2:8] + c_min) / 2

c_min = np.min(c[10:16])
c[10:16] = (c[10:16] + c_min) / 2

Then we desaturate the colors proportionally to bring them back into the (0~255) range and tidy up the tones.

# clip chroma into sRGB gamut
for desaturate in np.arange(1, 0.1, -0.001):
    # Convert back to RGB
    color_jch_adj = np.stack([j, c*desaturate, h], axis=-1)
    jab = colour.models.JCh_to_Jab(color_jch_adj)
    xyz = colour.CAM16UCS_to_XYZ(jab)
    color_rgb = colour.XYZ_to_sRGB(xyz)

    if np.all(0 <= color_rgb) and np.all(color_rgb <= 1):
        color_jch = color_jch_adj
        break
Before lightness adjust After lightness adjust
Before: Out of sRGB gamut After: Within sRGB gamut

Result

Here is the result:

Result color scheme

Preview of the result

It’s close to the standard while ensuring all colors are well visible.

The reduced lightness difference makes red and blue more visible, and the oversaturation of cyan and green is gone.

Additional Adjustments

Some subjective adjustments to improve readability and color distinction:

Background Color

A pure (0,0,0) black background is known to be bad for readability.

I changed the background to a near-black gray (20,20,20).

Enhancing Color Distinction

I added a slight hue separation between the ‘normal’ and ‘bright’ colors to make them more distinguishable.

# Set hue(mean delta to original is about 3)
h[2:8] = (30, 150, 90, 270, 330, 210)
h[2:8] -= 10  # Originally was +3

h[10:16] = (30, 150, 90, 270, 330, 210)
h[10:16] += 3
Before lightness adjust After lightness adjust
Before: Little difference between normal/bright After: Normal/bright more distinct

Final Result ✨

Dimidium color scheme Dimidium color scheme

Dimidium in terminal Dimidium in the terminal

compare image Comparison with the traditional color scheme

🔍 More preview

Download Settings 🛠️⬇️

You can download the color scheme settings for terminals here.

https://github.com/dofuuz/dimidium

Further readings

Color appearance model - Wikipedia

Roseus colormap - perceptually uniform colormap made by me

Code

The Python code used for color generation and visualization:

Colab (May be outdated. Use code at Github to develop with.)

Github

Try Color Appearance Models

You don’t need to know Python to use CAMs. You can experiment with CAMs at these sites.

OKLCH Color Picker & Converter

Interactive color picker comparison

CSS HD Gradients

Color picker for any color space - Color.js