OkLab A/B plane¶

This chart holds OkLab lightness at L = 0.7 while the horizontal A and vertical B components vary. The axes show the opponent directions: green to red for A, and blue to yellow for B.

In [1]:
from pathlib import Path

import brushcue
from IPython.display import Image, display

SIZE = 720
OUTPUT_FILE = Path("/home/dito/dev/monorepo/writing/graphics/chapters/color-formats/assets/oklab-a-b-plane.png")

ctx = brushcue.Context()
[wgpu] using backend Vulkan — adapter 'NVIDIA GeForce RTX 5070' (DiscreteGpu), driver 'NVIDIA'
In [2]:
FREEFORM_BODY = """
let point = (canvas_position - vec2<f32>(360.0, 360.0)) / 300.0;
let radius = length(point);

if radius > 1.0 {
    return vec4<f32>(1.0, 1.0, 1.0, 0.0);
}

let oklab = vec3<f32>(0.70, point.x * 0.22, -point.y * 0.22);
var rgb = linear_to_srgb(clamp(oklab_to_linear_srgb(oklab), vec3<f32>(0.0), vec3<f32>(1.0)));
let axis = min(abs(point.x), abs(point.y)) < 0.006;
let border = radius > 0.985;

if axis || border {
    rgb = vec3<f32>(0.08);
}

return vec4<f32>(rgb, 1.0);
"""

WGSL_HELPERS = """
fn linear_to_srgb(rgb: vec3<f32>) -> vec3<f32> {
    let lo = 12.92 * rgb;
    let hi = 1.055 * pow(max(rgb, vec3<f32>(0.0)), vec3<f32>(1.0 / 2.4)) - 0.055;
    return select(hi, lo, rgb <= vec3<f32>(0.0031308));
}

fn oklab_to_linear_srgb(oklab: vec3<f32>) -> vec3<f32> {
    let l = pow(oklab.x + 0.3963377774 * oklab.y + 0.2158037573 * oklab.z, 3.0);
    let m = pow(oklab.x - 0.1055613458 * oklab.y - 0.0638541728 * oklab.z, 3.0);
    let s = pow(oklab.x - 0.0894841775 * oklab.y - 1.2914855480 * oklab.z, 3.0);
    return vec3<f32>(
        4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
        -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
        -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s,
    );
}
"""
In [3]:
chart = brushcue.Composition.freeform_shader(
    FREEFORM_BODY,
    WGSL_HELPERS,
    brushcue.Bounds2f.from_x_y_width_height(0, 0, SIZE, SIZE),
    brushcue.ColorRepresentation.srgb(),
    brushcue.Dictionary.create(),
)

png = chart.execute(ctx).to_image_bytes(ctx)
OUTPUT_FILE.write_bytes(png)
display(Image(data=png))
No description has been provided for this image

The center is neutral gray because both A and B are zero. Moving right adds red, moving left adds green, moving up adds yellow, and moving down adds blue. The circular boundary keeps the figure focused on chroma direction; some edge colors reach the limits of sRGB and are clipped to the displayable gamut.