Skip to content
blend

blend

Mojo module 🡭

blend

Blend and composite modes: what a drawing operation does to the pixels already there, beyond the source-over every primitive uses by default.

Canvas.set_blend_mode sets the mode, save/restore carry it, and every primitive picks it up, since they all reach the buffer through set_pixel/write_pixel.

The arithmetic

Channels and alphas are 0-255 integers throughout, and the canvas stores straight (non-premultiplied) alpha, so each mode is written the way Color.blend_over is: weights accumulate premultiplied, and one division by the output alpha turns the result back into a straight color.

With Cs, Cb a source and backdrop channel and as, ab their alphas, every mode here is the same Porter-Duff form,

ao = as*Fa + ab*Fb
Co = (as*Fa*Cs' + ab*Fb*Cb) / ao

over per-mode fractions Fa, Fb and a source channel Cs' that the mode’s blend function B has already mixed into the backdrop:

Cs' = (1 - ab)*Cs + ab*B(Cb, Cs)

The Porter-Duff operators leave B(Cb, Cs) = Cs, so Cs' = Cs, and differ only in the fractions:

CLEAR             Fa = 0,      Fb = 0
SOURCE            Fa = 1,      Fb = 0
DESTINATION       Fa = 0,      Fb = 1
SOURCE_OVER       Fa = 1,      Fb = 1 - as
DESTINATION_OVER  Fa = 1 - ab, Fb = 1
SOURCE_IN         Fa = ab,     Fb = 0
DESTINATION_IN    Fa = 0,      Fb = as
SOURCE_OUT        Fa = 1 - ab, Fb = 0
DESTINATION_OUT   Fa = 0,      Fb = 1 - as
SOURCE_ATOP       Fa = ab,     Fb = 1 - as
DESTINATION_ATOP  Fa = 1 - ab, Fb = as
XOR               Fa = 1 - ab, Fb = 1 - as
ADD               Fa = 1,      Fb = 1

ADD is the one operator whose weights can sum past 1: ao and each premultiplied channel are clamped to 1, which is the lighter of the HTML5 canvas and Cairo’s ADD.

The separable blend modes composite source-over (Fa = 1, Fb = 1 - as) and differ only in B, applied per channel:

MULTIPLY     B = Cb*Cs
SCREEN       B = Cb + Cs - Cb*Cs
OVERLAY      B = HARD_LIGHT(Cs, Cb)
DARKEN       B = min(Cb, Cs)
LIGHTEN      B = max(Cb, Cs)
DIFFERENCE   B = |Cb - Cs|
EXCLUSION    B = Cb + Cs - 2*Cb*Cs
COLOR_DODGE  B = 0 if Cb = 0; 1 if Cs = 1; else min(1, Cb / (1 - Cs))
COLOR_BURN   B = 1 if Cb = 1; 0 if Cs = 0; else 1 - min(1, (1 - Cb) / Cs)
HARD_LIGHT   B = 2*Cb*Cs                  when Cs <= 0.5
             B = 1 - 2*(1 - Cb)*(1 - Cs)  otherwise
SOFT_LIGHT   B = Cb - (1 - 2*Cs)*Cb*(1 - Cb)   when Cs <= 0.5
             B = Cb + (2*Cs - 1)*(D(Cb) - Cb)  otherwise, with
             D(Cb) = ((16*Cb - 12)*Cb + 4)*Cb  when Cb <= 0.25
             D(Cb) = sqrt(Cb)                  otherwise

The non-separable blend modes take the whole RGB triple, since each moves one of hue, saturation and luminosity from one side to the other:

HUE         B = SetLum(SetSat(Cs, Sat(Cb)), Lum(Cb))
SATURATION  B = SetLum(SetSat(Cb, Sat(Cs)), Lum(Cb))
COLOR       B = SetLum(Cs, Lum(Cb))
LUMINOSITY  B = SetLum(Cb, Lum(Cs))

with Lum(C) = 0.3*R + 0.59*G + 0.11*B, Sat(C) = max(C) - min(C), and SetLum/SetSat the helpers of the same names in the W3C compositing specification. These four and SOFT_LIGHT are computed in floating point and rounded to the nearest channel value; every other mode is integer arithmetic that truncates, the way Color.blend_over does.

These are the formulas of the W3C compositing and blending specification, which is what globalCompositeOperation on the HTML5 canvas and Cairo’s operators both implement. SOURCE_OVER is the identity case of both halves: B(Cb, Cs) = Cs gives Cs' = Cs, and the fractions are the ones Color.blend_over already applies, so the default mode is byte-for-byte the blend it always was.

Scope

Three limits are worth knowing before reaching for the Porter-Duff modes:

  • A mode applies only where a shape actually draws. Cairo and the HTML5 canvas apply an operator over the whole clip region, so destination-in there clears every pixel the source misses; here a pixel no primitive touches is left as it was. DESTINATION_IN is therefore “scale the alpha of what this shape covers”, not “erase everything outside it”, and CLEAR erases the shape, not the canvas.
  • Coverage folds into the source alpha. An anti-aliased edge, and a clip path’s own soft edge, reach the blend as a source whose alpha is scaled – so a SOURCE fill writes a translucent pixel along its edge rather than a partial mix of source and backdrop.
  • draw_canvas (canvas/compose.mojo) composites source-over whatever the canvas mode is: it blends buffer into buffer without going through set_pixel.

SvgCanvas expresses the blend modes as mix-blend-mode, which CSS defines for every one of them. The Porter-Duff operators have no CSS keyword and are raster-only: SvgCanvas draws source-over under any of them.

Structs

  • BlendMode: How a drawn color combines with the pixel underneath.