CodeChroma All articles
Colour Inspiration

Blue from Woad, Red from Madder: Mixing Britain’s Dye Garden in GLSL

CodeChroma

Woad blue is not one blue. Madder red is not a fire-engine red. Weld yellow is not a highlighter. That is the first useful thing about historic British dye palettes: they refuse the tidy saturation of a default colour picker.

For makers working in p5.js or WebGL, the appeal is obvious. These are colours with soil and weather in them. They also come with a warning label. A dye on wool under cloudy light is not the same thing as a pixel emitted by a phone screen. If you ignore that, you end up with a muddy brown soup and a vague sense that history has let you down.

The dye garden, translated

Start with approximations, not commandments. Woad gives a deep, slightly greenish blue; madder gives brick, rust and dusty rose depending on mordant and temperature; weld gives a soft, green-tinged yellow. The following hex values are practical starting points for screen work, not historical reconstructions.

Dye Screen starting point Character
Woad #2f5d7c Deep, slightly green blue
Madder #9e3b2f Brick red with a dusty edge
Weld #d6c35a Soft yellow with a green undertone

You can push woad darker with #1f3a4d, warm madder with #b34a36, and dull weld with #b8a94c. The point is not to find the one true woad. It is to build a family that behaves like a dye pot: related, slightly uneven, and capable of shifting when you change the mordant.

Pigment on cloth, pixels on glass

Dye is subtractive. It absorbs some wavelengths and reflects others. A screen is additive: it emits red, green and blue light and asks your eye to do the mixing. When you blend two dye colours in sRGB, you are not simulating a vat. You are averaging light values, which often produces a flat, greyish middle.

For better results, mix in linear light or a perceptual space such as OKLab. In GLSL, a rough linear-light conversion is enough for a sketch:

vec3 toLinear(vec3 c) {
  return pow(c, vec3(2.2));
}

vec3 toSRGB(vec3 c) {
  return pow(c, vec3(1.0 / 2.2));
}

This is not physically accurate for real dyes. It is, however, a useful lie: it stops madder and woad from collapsing into a dull purple-grey too early, and it makes the final output feel more like pigment sitting in cloth.

A p5.js sketch that blends the dye pot

Create a simple WebGL sketch with three dye weights and a slow mordant wobble. The fragment shader below normalises the weights, mixes in linear light and adds a little grain so the surface does not look like a flat gradient.

precision mediump float;

uniform vec2 u_resolution;
uniform float u_time;
uniform float u_woad;
uniform float u_madder;
uniform float u_weld;

vec3 toLinear(vec3 c) {
  return pow(c, vec3(2.2));
}

vec3 toSRGB(vec3 c) {
  return pow(c, vec3(1.0 / 2.2));
}

void main() {
  vec2 uv = gl_FragCoord.xy / u_resolution.xy;

  vec3 woad = toLinear(vec3(0.184, 0.365, 0.486));
  vec3 madder = toLinear(vec3(0.620, 0.231, 0.184));
  vec3 weld = toLinear(vec3(0.839, 0.765, 0.353));

  float total = max(u_woad + u_madder + u_weld, 0.001);
  vec3 dye = (woad * u_woad + madder * u_madder + weld * u_weld) / total;

  float grain = fract(sin(dot(uv, vec2(12.9898, 78.233))) * 43758.5453);
  float mordant = 0.5 + 0.5 * sin(u_time * 0.4);

  dye = mix(dye, dye * 0.72 + vec3(0.08), grain * 0.28 * mordant);

  gl_FragColor = vec4(toSRGB(dye), 1.0);
}

The p5.js side is deliberately small. You can expose the three weights as sliders later.

let dyeShader;

function preload() {
  dyeShader = loadShader('dye.vert', 'dye.frag');
}

function setup() {
  createCanvas(720, 480, WEBGL);
  noStroke();
}

function draw() {
  shader(dyeShader);
  dyeShader.setUniform('u_resolution', [width, height]);
  dyeShader.setUniform('u_time', millis() / 1000);
  dyeShader.setUniform('u_woad', 0.6);
  dyeShader.setUniform('u_madder', 0.3);
  dyeShader.setUniform('u_weld', 0.1);
  rect(-width / 2, -height / 2, width, height);
}

Recipes worth trying

A 70/30 woad-madder blend gives a deep indigo-violet that is much more interesting than either colour alone. Madder and weld together produce an ochre that feels like old paper and dyed linen. All three in roughly equal measure give a mossy, desaturated olive — useful for backgrounds, less useful for body text.

If you are pulling these colours back into a website, stop and check contrast. Historic palettes are full of mid-tones that look lovely in a swatch and fail badly as interface colours. Use the dye palette for atmosphere and illustration; use a separate, audited token set for text and controls.

That is the real lesson of the dye garden. Woad, madder and weld are not there to make your shader look old. They are there to make you choose more carefully — to notice that a colour has a temperature, a history and a limit.

All Articles

Related Articles

Laying Bricks in Code: Procedural British Materials for p5.js Sketches

Laying Bricks in Code: Procedural British Materials for p5.js Sketches

Salt, Slate and Sea Glass: Ten UK Coastal Palettes for Generative Makers

Salt, Slate and Sea Glass: Ten UK Coastal Palettes for Generative Makers

When the Sky Clocks Off Early: A Seasonal Palette for UK Websites

When the Sky Clocks Off Early: A Seasonal Palette for UK Websites