How Semi-Transparent Layers Combine: Porter-Duff Compositing
When you print walnut dye over madder root on cotton, you’re not replacing one color with another—you’re compositing. The second layer is semi-transparent, and the final color depends on both the new dye and what’s already bound to the fiber. The fabric shows through gaps in the block print, the first dye shows through the second, and the weave texture modulates everything.
Thomas Porter and Tom Duff formalized this in 1984 for Pixar, describing twelve ways that semi-transparent layers can combine. The most common—source over—treats the top layer as partially transparent and blends it with what’s underneath based on alpha values.
Here’s source-over compositing in Python, blending two RGBA pixels:
def composite_over(src, dst):
sr, sg, sb, sa = src
dr, dg, db, da = dst
out_a = sa + da * (1 - sa)
if out_a == 0: return (0, 0, 0, 0)
out_r = (sr * sa + dr * da * (1 - sa)) / out_a
out_g = (sg * sa + dg * da * (1 - sa)) / out_a
out_b = (sb * sa + db * da * (1 - sa)) / out_a
return (out_r, out_g, out_b, out_a)
base = (0.8, 0.3, 0.2, 0.9) # madder root
top = (0.4, 0.3, 0.1, 0.6) # walnut hull
print(composite_over(top, base)) # (0.647, 0.3, 0.161, 0.96)
And in Zig, working with 8-bit channels:
const std = @import("std");
fn compositeOver(src: [4]u8, dst: [4]u8) [4]u8 {
const sa = @as(f32, @floatFromInt(src[3])) / 255.0;
const da = @as(f32, @floatFromInt(dst[3])) / 255.0;
const out_a = sa + da * (1.0 - sa);
if (out_a == 0) return .{0, 0, 0, 0};
var result: [4]u8 = undefined;
for (0..3) |i| {
const s = @as(f32, @floatFromInt(src[i])) / 255.0;
const d = @as(f32, @floatFromInt(dst[i])) / 255.0;
result[i] = @intFromFloat((s * sa + d * da * (1.0 - sa)) / out_a * 255.0);
}
result[3] = @intFromFloat(out_a * 255.0);
return result;
}
The algebra is identical whether you’re blending pixels or modeling how light passes through stacked acetate sheets. Natural dyes don’t have an explicit alpha channel—their transparency comes from concentration, mordant chemistry, and how much pigment the fiber accepts—but the visual result follows the same math. Diane’s block prints layer indigo over madder and get purple for the same reason Photoshop does: the compositing operator defines how overlapping semi-transparent regions resolve into a single perceived color.