Blending

Blend Equations and Blend Functions

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).

Five blend setups, as read back with readPixels over a (0.2, 0.4, 0.6, 1) destination
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.

Orange (0.8, 0.4, 0.2, 0.5) blended over blue with five equation and factor setups, read back with readPixelsHTMLLive
<!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>