Skip to main content
The dye system lets you colorize a card’s frame overlay at render time. Instead of pre-authoring a separate asset for every color variant, you supply a single color value and Hagaki blends it into the frame’s color layer using the Oklab perceptual color space.

The dye field

The dye field is a 24-bit RGB integer — the same value as a CSS hex color, interpreted as a decimal number.
dye = (R << 16) | (G << 8) | B

Common values

ColorHexDecimal (dye)
Red#FF000016711680
Green#00FF0065280
Blue#0000FF255
White#FFFFFF16777215
Black#0000000
Orange#FF800016744448
Purple#8000FF8388863

Converting a hex color to a dye integer

// From a CSS hex string (with or without '#')
function hexToDye(hex) {
  return parseInt(hex.replace('#', ''), 16);
}

console.log(hexToDye('#FF8000')); // 16744448
console.log(hexToDye('8000FF'));  // 8388863

Oklab blending

Hagaki does not perform a simple RGB tint. It converts both the frame pixel and the dye color to the Oklab color space before blending. Oklab is a perceptually uniform color space designed to match how human vision perceives color differences. Unlike RGB, equal numerical steps in Oklab correspond to equal perceived color differences. This means:
  • Hue shifts look natural across the full brightness range.
  • Saturated colors do not blow out to clipped white or muddy grey.
  • The frame’s lighting and shading are preserved rather than overwritten.

Blend parameters

The blending operation applies the following constants:
ParameterValueEffect
Chroma blend strength0.90The output chroma (color saturation) is shifted 90% toward the dye color’s chroma.
Lightness blend strength0.50The output lightness blends 50% between the original pixel and the dye color.
Inverse blend0.10The remaining 10% of chroma retains the original pixel’s color influence.
Chroma boost factor0.50Darker pixels receive a stronger chroma boost, deepening color in shadowed areas.
Max chroma (squared)1.0Chroma is clamped to prevent oversaturation.
In plain terms: the dye color strongly shifts the hue and saturation of the frame (90% toward the target) while preserving roughly half the original luminance. Darker areas of the frame receive more vivid coloring to compensate for their lower base brightness.

Which frames support dye

Only frames with a color layer (color_model: true) are affected by the dye field.
FrameDye supportBehavior
MoonweaverYesColor layer is blended with the dye value.
EssentiaYesColor layer is blended with the dye value.
SnowglowNodye field is ignored; no color layer exists.
Setting a dye value on a Snowglow card has no effect. The render completes normally, but the dye is silently discarded. If you need a colorized frame, use Moonweaver or Essentia instead.
There is no “no dye” value. The dye field always affects the color layer of frames that support it. Setting dye: 0 applies black as the dye color — this blends 50% of the lightness toward black and reduces chroma to 10% of the original, resulting in a dark, desaturated frame. If you want a frame rendered with its authored colors unmodified, use a frame type with color_model: false (Snowglow), which ignores the dye field entirely.

Full example

Node.js
const payload = {
  id: 7,
  variant: 0,
  dye: 0x9B59B6, // Amethyst purple → 10179254
  kindled: false,
  frame_type: 0,  // Moonweaver supports dye
};

const hash = Buffer.from(JSON.stringify(payload)).toString('base64').replace(/=+$/, '');
const response = await fetch(`http://localhost:8899/render/card/${hash}`);

Build docs developers (and LLMs) love