Correct brightness
The answer by ggorlen is incorrect
- it assumes the background color of white
- it uses the incorrect calculation to get the brightness
Unknown background color
It will be difficult to match the canvas background color at each pixel so the best solution is avoid transparent pixels. This can be done by filling the canvas with a non transparent background color before adding the text.
sRGB
To calculate the brightness (lum) is complicated.
- First convert the sRGB values to a perceptual corrected linear value.
- Then sum the calculated value for all pixels.
- Get the mean linear value by dividing by number of pixels.
- Convert the resulting value back to sRGB
Details can be found on the wiki page
Example
Example show correct brightness calculation for a set of characters.
const BG_COLOR = "white";
const CHAR_COLOR = "black";
const char = "#";
const width = 30;
const height = 32;
const tag = (tag, props = {}) => Object.assign(document.createElement(tag), props);
const append = (el, ...sibs) => sibs.reduce((el, sib)=>(el.appendChild(sib), el), el);
const sC2Lin = (val) => {
val /= 255;
return val <= 0.04045 ? val / 12.92 : ((val + 0.055) / 1.055) ** 2.4;
}
const RGB2Gama = (r, g, b) => {
return sC2Lin(r) * 0.212655 +
sC2Lin(g) * 0.715158 +
sC2Lin(b) * 0.072187;
}
const brightToHex = b => {
b = Math.round(b);
const hex = b.toString(16).padStart(2,"0");
return "#" + hex + hex + hex;
}
function charBrightness(ctx, char, color = CHAR_COLOR, bgColor = BG_COLOR) {
ctx.font = "38px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, width, height);
ctx.fillStyle = color;
ctx.fillText(char, width / 2, height * (1.9 / 3));
const d = ctx.getImageData(0, 0, width, height).data;
var bSum = 0, i = 0;
while (i < d.length) {
bSum += RGB2Gama(d[i++], d[i++], d[i++]);
i++;
}
const meanGam = bSum / (d.length / 4);
return (meanGam <= 0.0031308 ?
meanGam * 12.92 :
1.055 * meanGam ** (1 / 2.4) - 0.055) * 255;
}
function testChar(char) {
const can1 = tag("canvas", {width, height});
const can2 = tag("canvas", {width, height});
const ctx1 = can1.getContext("2d");
const ctx2 = can2.getContext("2d");
const bright = Math.round(charBrightness(ctx1, char));
console.log("Brightness: '" + char + "' " + Math.round(bright));
ctx2.fillStyle = brightToHex(bright);
ctx2.fillRect(0, 0, width, height);
append(document.body, can1, can2)
}
testChar("#")
testChar("$")
testChar("H")
testChar("J")
testChar("L")
testChar("I")
testChar("i")
testChar("-")
testChar(".")
canvas {
border: 1px solid #000000;
margin-right: 2px;
}
More
There is also an additional problem that will be very hard to solve.
The perceptual brightness of a character is not just the mean of the pixels, there is a geometric component. Our perception reduces the brightness of light lines between dark lines and small light areas in dark areas
Note example the characters pairs #,$ and L,J have the same calculated brightness