blendFunc(src, dst) picks the two factors and blendEquation() how the weighted colors combine: FUNC_ADD (the default), FUNC_SUBTRACT, FUNC_REVERSE_SUBTRACT, or MIN and MAX, which ignore the factors (core in WebGL 2, EXT_blend_minmax in WebGL 1). The factors are ZERO, ONE, the source's or destination's color or alpha (SRC_COLOR, DST_ALPHA and so on, each with a ONE_MINUS_ form), CONSTANT_COLOR and CONSTANT_ALPHA from blendColor(), and SRC_ALPHA_SATURATE, which is min(source alpha, 1 - destination alpha), not the destination alpha as some tables say. WebGL 1 accepts it only as a source factor (WebGL 2 as either).
| Setup | Equation, source and destination factors | Orange (0.8, 0.4, 0.2, 0.5) over blue |
|---|---|---|
| Alpha | FUNC_ADD, SRC_ALPHA, ONE_MINUS_SRC_ALPHA | 127, 102, 102, 191 |
| Additive | FUNC_ADD, ONE, ONE | 255, 204, 204, 255 |
| Multiply | FUNC_ADD, DST_COLOR, ZERO | 41, 41, 31, 127 |
| Subtract | FUNC_REVERSE_SUBTRACT, ONE, ONE | 0, 0, 102, 128 |
| Lighten | MAX (factors ignored) | 204, 102, 153, 255 |
Additive blending (glows, particles, Post-Processing and Particles) saturates at 255; multiply darkens, like tinted glass. Watch the first row's alpha: plain alpha blending left 191, not 255, and the page composites the canvas with that alpha (Premultiplied Alpha). blendFuncSeparate() and blendEquationSeparate() treat alpha apart from color.
<!doctype html>
<style>
body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
canvas { display: block; width: 100%; max-width: 600px; }
.names { display: flex; max-width: 600px; text-align: center; }
.names div { flex: 1; padding: 4px 2px; }
.names b { display: block; font-size: 12px; }
.names code { font-size: 10px; color: #666; display: block; }
</style>
<canvas id="c" width="600" height="180"></canvas>
<div class="names" id="names"></div>
<script>
const gl = document.getElementById('c').getContext('webgl2');
const program = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, `#version 300 es
uniform vec4 uRect; // x, y, w, h in clip space
uniform bool uRound;
out vec2 vLocal;
void main() {
vLocal = vec2[6](vec2(0), vec2(1, 0), vec2(1), vec2(0), vec2(1), vec2(0, 1))[gl_VertexID];
gl_Position = vec4(uRect.xy + vLocal * uRect.zw, 0.0, 1.0);
}`], [gl.FRAGMENT_SHADER, `#version 300 es
precision mediump float; uniform vec4 uColor; uniform bool uRound; in vec2 vLocal; out vec4 c;
void main() {
if (uRound && distance(vLocal, vec2(0.5)) > 0.5) discard;
c = uColor;
}`]]) {
const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
gl.attachShader(program, s);
}
gl.linkProgram(program); gl.useProgram(program);
gl.bindVertexArray(gl.createVertexArray());
const u = (n) => gl.getUniformLocation(program, n);
function rect(x, y, w, h, color, round = false) {
gl.uniform4f(u('uRect'), x, y, w, h); gl.uniform4fv(u('uColor'), color); gl.uniform1i(u('uRound'), round);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
const setups = [
['Alpha', 'ADD, SRC_ALPHA, 1-SRC_ALPHA', () => { gl.blendEquation(gl.FUNC_ADD); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); }],
['Additive', 'ADD, ONE, ONE', () => { gl.blendEquation(gl.FUNC_ADD); gl.blendFunc(gl.ONE, gl.ONE); }],
['Multiply', 'ADD, DST_COLOR, ZERO', () => { gl.blendEquation(gl.FUNC_ADD); gl.blendFunc(gl.DST_COLOR, gl.ZERO); }],
['Subtract', 'REVERSE_SUBTRACT, ONE, ONE', () => { gl.blendEquation(gl.FUNC_REVERSE_SUBTRACT); gl.blendFunc(gl.ONE, gl.ONE); }],
['Lighten', 'MAX (factors ignored)', () => { gl.blendEquation(gl.MAX); }],
];
gl.clearColor(0.97, 0.96, 0.93, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
let html = '';
setups.forEach(([name, label, apply], i) => {
const x = -1 + i * 0.4;
gl.disable(gl.BLEND);
rect(x + 0.02, -0.9, 0.36, 1.2, [0.2, 0.4, 0.6, 1]); // the destination: blue
gl.enable(gl.BLEND);
apply();
rect(x + 0.06, -0.3, 0.28, 1.1, [0.8, 0.4, 0.2, 0.5], true); // the source: half-transparent orange
// Read back one pixel where both overlap (pixel coordinates from the bottom left).
const px = new Uint8Array(4);
gl.readPixels(Math.round((x + 0.2 + 1) / 2 * 600), Math.round((0.05 + 1) / 2 * 180), 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, px);
html += `<div><b>${name}</b><code>${label}</code>${px.join(', ')}</div>`;
});
document.getElementById('names').innerHTML = html;
// Restore opaque alpha so the page does not composite the canvas as see-through.
gl.disable(gl.BLEND);
gl.colorMask(false, false, false, true);
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
</script>