Technical Deep Dive#

For the color nerds, math geeks, and optimization enthusiasts who want to know HOW the magic actually works

So you’ve used CM-Colors and thought “this is pretty neat, but HOW does it actually work?” Welcome to the rabbit hole! We’re about to get into some serious color science, perceptual mathematics, and optimization theory.

The Big Picture: What We’re Actually Optimizing#

When you give us two colors that don’t meet accessibility standards, we’re solving a constrained optimization problem:

Find the smallest perceptual change to your text color that achieves the required contrast ratio.

Sounds simple? It’s not. Here’s why this is mathematically fascinating:

The Color Space Problem#

Your RGB values (like #FF5733) are great for computers, terrible for human perception. When you change (255, 87, 51) to (250, 87, 51), that looks like a tiny change numerically, but perceptually it might be huge or imperceptible depending on the color.

Solution: We convert everything to OKLCH color space, which is designed to match human vision:

  • L (Lightness): How bright/dark the color appears

  • C (Chroma): How saturated/vivid the color is

  • H (Hue): The actual color (red, blue, etc.)

OKLCH is perceptually uniform, meaning equal numerical changes produce equal visual changes. This is crucial for making “minimal” adjustments that actually look minimal.

The Perceptual Distance Problem#

How do we measure if two colors “look similar”? Enter Delta E 2000 - the current state-of-the-art formula for color difference that considers:

  • How sensitive human eyes are to different hues

  • How lighting conditions affect perception

  • The fact that we’re better at detecting some color changes than others

Delta E < 1.0 = Changes most people can’t detect Delta E < 2.0 = Only noticeable when colors are side-by-side Delta E > 5.0 = Obviously different colors

We try to keep changes under 2.0 Delta E whenever possible.

Note: The function’s we are mentioning are all from optimised.py and is used to create our tune_colors() function

The Algorithm: Multi-Phase Optimization#

Our generate_accessible_color() function uses a sophisticated multi-phase approach:

Phase 1: Binary Search on Lightness#

def binary_search_lightness(text_rgb, bg_rgb, delta_e_threshold=2.0, target_contrast=7.0):

Why lightness first? Most accessibility problems are solved by making text darker or lighter. This gives us the biggest contrast improvements with minimal perceptual change.

The binary search magic:

  • Convert your color to OKLCH

  • Determine if we need to go lighter or darker based on background

  • Binary search the lightness value (L component) in 20 iterations

  • Each iteration cuts the search space in half

  • Achieves ~1 million precision levels in just 20 steps

Mathematical complexity: O(log n) vs O(n) for brute force

Phase 2: Gradient Descent on Lightness + Chroma#

def gradient_descent_oklch(text_rgb, bg_rgb, delta_e_threshold=2.0, target_contrast=7.0):

If adjusting lightness alone isn’t enough, we optimize both lightness AND chroma (saturation) simultaneously using gradient descent.

The cost function we’re minimizing:

cost = contrast_penalty + delta_e_penalty + distance_penalty

# Where:
contrast_penalty = max(0, target_contrast - actual_contrast) * 1000
delta_e_penalty = max(0, delta_e - threshold) * 10000  
distance_penalty = delta_e * 100

Why these specific weights?

  • 10000x penalty for exceeding Delta E threshold (hard constraint)

  • 1000x penalty for missing contrast target (primary goal)

  • 100x penalty for perceptual distance (minimize brand impact)

Numerical gradient computation: We use central difference approximation because the color conversion functions aren’t analytically differentiable:

gradient[i] = (f(x + ε) - f(x - ε)) / (2ε)

Adaptive learning rate: Starts at 0.02, decays by 5% every 10 iterations to ensure convergence.

Phase 3: Progressive Delta E Relaxation#

If neither binary search nor gradient descent finds a solution within our strict Delta E threshold, we progressively relax constraints:

delta_e_sequence = [0.8, 1.0, 1.2, 1.4, 1.6, 1.8, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5, 2.7, 3.0, 3.5, 4.0, 5.0]

We run both optimization phases at each threshold level, keeping the best candidate found so far.

The Color Science Behind the Scenes#

OKLCH Color Space Conversion#

OKLCH is based on the OKLAB color space, which was designed in 2020 to be more perceptually uniform than previous attempts. The conversion from RGB involves:

  1. Linear RGB: Remove gamma correction

  2. XYZ conversion: Transform to CIE XYZ color space

  3. OKLAB transformation: Apply the OK color appearance model

  4. Cylindrical coordinates: Convert to lightness-chroma-hue

Delta E 2000 Calculation#

The Delta E 2000 formula considers:

  • Lightness weighting: How sensitive we are to brightness changes

  • Chroma weighting: How saturation affects perceived difference

  • Hue weighting: Different sensitivity to hue shifts across the color wheel

  • Interaction terms: How lightness, chroma, and hue changes interact

It’s a beast of a formula involving elliptical parameters, rotation terms, and compensation factors. The math is gnarly, but the perceptual accuracy is worth it.

Contrast Ratio Mathematics#

WCAG contrast ratio is defined as:

contrast = (L1 + 0.05) / (L2 + 0.05)

Where L1 and L2 are the relative luminances of the lighter and darker colors respectively.

Target ratios:

  • AAA Large Text: 4.5:1 minimum

  • AA Normal Text: 4.5:1 minimum

  • AAA Normal Text: 7:1 minimum (our default target)

Performance Optimizations#

Why Not Brute Force?#

A naive brute force approach might test every possible RGB combination:

  • 256³ = 16.7 million possible colors

  • Plus Delta E calculation for each = computationally expensive

  • Plus contrast ratio calculation = even slower

Our Optimized Approach#

  1. Binary search: 20 iterations vs ~256 for linear search

  2. OKLCH space: More efficient than RGB for perceptual operations

  3. Gradient descent: Follows the mathematical gradient toward optimal solution

  4. Early termination: Stop as soon as we find a valid solution

  5. Progressive relaxation: Only try harder thresholds if needed

Result: ~100x faster than brute force with identical quality.

Edge Cases and Failure Modes#

When We Can’t Help You#

Some color combinations are just mathematically impossible to fix within reasonable perceptual bounds:

  1. Neon yellow on white: Even perfect black text only gives ~17:1 contrast

  2. Very similar colors: If your text and background are too close, we’d need to change them dramatically

  3. Extreme saturation: Highly saturated colors have limited lightness range in RGB

Fallback Behavior#

When optimization fails:

  • Return the best candidate found (may not meet full contrast requirements)

  • Preserve original color if no improvement possible

  • Never return invalid RGB values

  • Always return something usable

Implementation Details#

Numerical Stability#

Color space conversions can be numerically unstable near the edges of the RGB gamut. We handle this with:

  • Safe conversion functions: Check for valid RGB output at each step

  • Gamut clamping: Keep intermediate values within valid ranges

  • Exception handling: Gracefully fail to fallback methods

Memory Efficiency#

  • No color caching: Each optimization is stateless

  • Minimal object allocation: Reuse data structures where possible

  • Stack-based: No recursive algorithms that could cause stack overflow

Extending the Algorithm#

Want to hack on CM-Colors? Here are some areas for improvement:

Alternative Optimization Methods#

  • Simulated annealing: For even better global optimization

  • Genetic algorithms: Population-based search

  • Constrained optimization: Using scipy.optimize for more sophisticated constraints

Different Color Spaces#

  • LAB: Older but still widely used perceptual space

  • LUV: Alternative perceptual uniform space

  • HSV/HSL: More intuitive for designers but less perceptually uniform

Multi-Objective Optimization#

  • Pareto fronts: Trade-off between multiple objectives

  • Weighted objectives: Let users specify preference for contrast vs. brand preservation

  • Color harmony: Maintain color scheme relationships

Testing and Validation#

Our optimization algorithms are validated against:

  1. Brute force reference: Identical results with better performance

  2. Perceptual studies: Human evaluation of “minimal change”

  3. Edge case coverage: Pathological color combinations

  4. Performance benchmarks: Speed comparisons across color ranges

The Math Stuff (For the Really Curious)#

Lagrangian Formulation#

Our constrained optimization problem can be expressed as:

minimize f(L, C) = ΔE(original, new)
subject to: contrast(new, background) ≥ target
           ΔE(original, new) ≤ threshold
           new ∈ RGB_gamut

Gradient Computation#

For the OKLCH gradient descent, we numerically approximate:

∇f = [∂f/∂L, ∂f/∂C]

Using central differences for stability.

Convergence Criteria#

We stop optimization when:

  • Objective function change < 1e-6 (numerical convergence)

  • Valid solution found (early termination)

  • Maximum iterations reached (50 for gradient descent)

Conclusion#

What looks like “just making colors accessible” is actually a sophisticated optimization problem involving:

  • Perceptual color science

  • Numerical optimization

  • Constrained search algorithms

  • Color space mathematics

The magic happens in the intersection of human perception, mathematical optimization, and practical engineering constraints. Pretty cool for something that “just fixes your colors,” right?


Still want to go deeper? Check out the research papers on OKLCH color space, Delta E 2000 perceptual difference, and WCAG contrast mathematics. Fair warning: there’s a LOT of math involved. 🤓

Found this interesting or spotted an optimization we could make better? Open an issue - we love talking about color science!