1

The width of character 1 is 8.8984375.

So I think 10 characters width is 88.984375, but it is actually 78.296875.

let canvas = document.querySelector('#canvas');
let ctx = canvas.getContext('2d');
ctx.font = '16px/16px arial';
let per = ctx.measureText('1').width;
let num = 10;
let total = ctx.measureText('1111111111').width;
console.log(per, total, per * 10);
<canvas id="canvas"></canvas>
army8735
  • 13
  • 3
  • I believe this post explains it, and has a workaround for it https://stackoverflow.com/questions/18713716/html5-canvas-why-does-measuring-text-with-measuretext-and-offsetwidth-give-di – Cypho Mar 09 '21 at 14:41

1 Answers1

0

Text metrics is complicated especially when fonts are not mono spaced. Some characters are padded when measured on their own. 1 happens to be one of them when using "arial"

Calculating the padding.

Assumptions

  • A character next to its self (either side) will take up the same width for each

  • A string of the same characters at each end will have the same padding.

Calculation

For the character "1" we can workout the approximate padding by measuring 2 cases "111" and "11111". We can then create an expression to define the character width w, and the padding p.

Thus for the strings...

  • "111" the width would be w * 3 + p = w3, and

  • "11111" the width would be w * 5 + p = w5.

We now have two equations with 2 unknowns as we can get w3 and w5 using measureText.

Solving for p (padding) p = (15 * (w3 / 3) - (3 * w5)) / 2 Then w (width of character) is w = (w3 - p) / 3

Example

The example calculates the width of 1 between 1 and 1 to ~100th of a pixel width.

const width = str => ctx.measureText(str).width;
const test = (str, calc) => 
    console.log("'" + str + "' width = " + width(str).toFixed(2) + 
                "px, calculated = " + calc.toFixed(2) + 
                "px, dif = " + (calc - width(str)).toFixed(2) + "px");


const ctx = canvas.getContext("2d");
ctx.font = '16px arial';
const c = "1";

// calculate padding and width of 1
const w3 = width(c.padStart(3, c));
const w5 = width(c.padStart(5, c));    
const padding = (15 * (w3 / 3) -  3 * w5) / 2;
const w = (w3 - padding) / 3;

// test result
test(c, w + padding);
test(c.padStart(10, c), w * 10 + padding);
test(c.padStart(20, c), w * 20 + padding);

console.log("Width of '"+c+"' is ~" + w.toFixed(2) + "px");
console.log("Padding is ~" + padding.toFixed(2) + "px");
<canvas id="canvas"></canvas>

Other Characters

Not all characters have the padding the example below calculates the padding for numbers and lowercase letters.

const width = str => ctx.measureText(str).width;
const test = (str, calc) => 
    console.log("'" + str + "' width = " + width(str).toFixed(2) + 
                "px, calculated = " + calc.toFixed(2) + 
                "px, dif = " + (calc - width(str)).toFixed(2) + "px");


const ctx = canvas.getContext("2d");
ctx.font = '16px arial';
console.log("Font: " + ctx.font);
[..."1234567890abcdefghijklmnopqrstuvwxyz"].forEach(calcWidthFor);

function calcWidthFor(c) {
    const w3 = width(c.padStart(3, c));
    const padding = (15 * (w3 / 3) - (3 * width(c.padStart(5, c)))) / 2;
    const w = (w3 - padding) / 3;
    console.log("Width of '"+c+"' is ~ " + w.toFixed(2) + "px Padding is ~ " + padding.toFixed(2) + "px");
}
<canvas id="canvas"></canvas>
Blindman67
  • 51,134
  • 11
  • 73
  • 136