Journal

Color · Image processing ·

One painting, two themes

The jaguar paintings have black backgrounds baked in. A little color math seats each one on the exact page color in dark mode and derives a light version for light mode, so the art emerges from the page instead of sitting in a box.

The problem

The artwork is raster — a painted image with its background baked in. The site has two page colors: a deep navy in dark mode (hsl 228 30% 4.5%, rgb 8, 9, 15) and a warm cream in light mode (hsl 40 38% 94%, rgb 246, 242, 234). The paintings matched neither: their black measured rgb 0, 1, 4 and 0, 0, 0. In both themes the art sat inside a visible rectangle.

Dark: seat the black point on the page

A painting's black point is the most common value in each channel, because the background dominates the frame. In linear light, that value is mapped onto the page color and everything brighter is screened on top:

lifted = max(0, (linear(v) - blackPoint) / (1 - blackPoint))
out    = page + (1 - page) * lifted

Black lands exactly on the navy and the highlights keep their full range. Since it's a per-channel mapping of 256 input values, it runs as a lookup table.

Light: invert lightness, keep the color

For the light version, each pixel is converted to OKLab, a perceptual color space where lightness and color are separate axes. Lightness is inverted onto the cream — the background becomes the page, gold highlights become dark bronze ink — while hue and chroma are kept, so violet wings stay violet. Two details keep it clean:

  • A floor at the painting's black point. Near-black haze maps exactly to cream instead of a faint gray.
  • Color only where there is color. The dark background carries a slight navy tint; chroma is ramped in with a smoothstep on lightness, so that tint doesn't become a lavender wash.

The animation retunes with the painting: on the dark version light is added (lighter, screen); on the cream version the same motion is multiplied in, like ink.

Measured, not eyeballed

The acceptance test reads strips of pixels in the running page, from bare page into the art's background, in both themes. Dark reads 8, 9, 15 on the page and 8–9, 9–10, 15–16 inside the art; light reads 246, 242, 234 against 245, 242, 234. That is within one unit — below what the eye can separate.

Reproducible

The pipeline is one script, scripts/art-plates.mjs, which rebuilds all four plates from the original paintings. If either page color changes, update two constants, run it again, and the art follows.