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-inthere 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 throughset_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.