19

I'm drawing text in an HTML5 canvas on top of an image (a meme generator). I want to calculate a font size so that the string will span the entire width of the canvas.

So basically, is there a way in JavaScript to determine what font size I should use for some string of a certain font for a given width?

4 Answers 4

35

The fillText() method has an optional fourth argument, which is the max width to render the string.

MDN's documentation says...

maxWidth

Optional; the maximum width to draw. If specified, and the string is computed to be wider than this width, the font is adjusted to use a more horizontally condensed font (if one is available or if a reasonably readable one can be synthesized by scaling the current font horizontally) or a smaller font.

However, at the time of writing, this argument isn't supported well cross browser, which leads to the second solution using measureText() to determine the dimensions of a string without rendering it.

var width = ctx.measureText(text).width;

Here is how I may do it...

var canvas = document.getElementById('canvas'),
    ctx = canvas.getContext('2d');

// Font sizes must be in descending order. You may need to use `sort()`
// if you can't guarantee this, e.g. user input.
var fontSizes = [72, 36, 28, 14, 12, 10, 5, 2],
    text = 'Measure me!';

// Default styles.
ctx.textBaseline = 'top';
ctx.fillStyle = 'blue';

var textDimensions,
    i = 0;

do {
   ctx.font = fontSizes[i++] + 'px Arial';
   textDimensions = ctx.measureText(text);        
} while (textDimensions.width >= canvas.width);

ctx.fillText(text, (canvas.width - textDimensions.width) / 2, 10);​

jsFiddle.

I have a list of font sizes in descending order and I iterate through the list, determining if the rendered text will fit within the canvas dimensions.

If it will, I render the text center aligned. If you must have padding on the left and right of the text (which will look nicer), add the padding value to the textDimensions.width when calculating if the text will fit.

If you have a long list of font sizes to try, you'd be better off using a binary search algorithm. This will increase the complexity of your code, however.

For example, if you have 200 font sizes, the linear O(n) iteration through the array elements could be quite slow.

The binary chop should be O(log n).

Here is the guts of the function.

var textWidth = (function me(fontSizes, min, max) {

    var index = Math.floor((min + max) / 2);

    ctx.font = fontSizes[index] + 'px Arial';

    var textWidth = ctx.measureText(text).width;

    if (min > max) {
        return textWidth;
    }

    if (textWidth > canvas.width) {
        return me(fontSizes, min, index - 1);
    } else {
        return me(fontSizes, index + 1, max);
    }

})(fontSizes, 0, fontSizes.length - 1);

jsFiddle.

Sign up to request clarification or add additional context in comments.

5 Comments

I should have known you don't get 90k+ rep by not following through. :) Very nice.
I'm trying to use this in a function that runs via the onkeyup attribute of a text field, and whenever I type into the field, my browser freezes and must be force quit. Any ideas?
@edc1591 Can you set up a fiddle to demonstrate? It may be likely that the do/while loop's expression isn't ever evaluating to true.
Looks like I just pasted the code into the wrong place in my js file. It's working now. The binary search algorithm also works great, thanks so much!
It looks like the do while in this could possibly loop forever I think. I'm not sure what the browser does when the font is "undefinedpx Arial"
3
  1. Write all the configurations you want for the text. (for example, ctx.font)

  2. Before writing the actual text, use ctx.measureText("myText").width to get the width of the text. Because we have called it after making changes on ctx, it will be different whenever we change properties on ctx (like ctx.font).

  3. Now we will scale it from the middle.

     ctx.translate(midPoint_X_Of_Text, midPoint_Y_Of_Text);
     ctx.scale(desiredWidth/measuredWidth, desiredWidth/measuredWidth);
     ctx.translate(-midPoint_X_Of_Text, -midPoint_Y_Of_Text);
    
  4. Write the text using ctx.fillText() or ctx.strokeText().

  5. Reverse the changes by using ctx.save() and ctx.restore(), or manually with setTransform().

var can = document.getElementById('myCan');
var ctx = can.getContext("2d");

var desiredWidth = can.width;
var myText = "Hello, world!"

function draw(){
  ctx.font = can.height/2 + "px sans-serif";
  ctx.fillStyle = "red";
  ctx.textAlign = "center";
  ctx.textBaseline = "middle";
  let measuredWidth = ctx.measureText(myText).width;
  let ratio = desiredWidth/measuredWidth;
  ctx.translate(can.width/2,can.height/2);
  ctx.scale(ratio,ratio);
  ctx.translate(-can.width/2,-can.height/2);
  ctx.fillText(myText,can.width/2,can.height/2);
  ctx.setTransform();
}

draw();

let myInput = document.getElementById('myInput');
myInput.addEventListener('input',(e)=>{
    ctx.clearRect(0,0,can.width,can.height);
        myText = myInput.value;
        draw();
})
<html>
<body>
  <center>
    <input id="myInput" value="Hello, world!"/><br>
    <canvas id="myCan" height="300" width=300 style="border:black solid 2px;">
    </canvas>
  </center>
</body>
</html>

Comments

0

I have the next code and it works perfectly for me. Maybe there is a little error but I don't need to have a list of font sizes. I just get the width with a font size and if it is too big I calculate a proportional size depending of the canvas width and padding (50)

ctx.font = FONT_SIZE+"pt Verdana";
var textWidth = ctx.measureText(markText).width;
if(textWidth > canvas.width - 50) {
    ctx.font = parseInt(FONT_SIZE*(canvas.width-50)/textWidth)+"pt Verdana";
    textWidth = ctx.measureText(markText).width;
}
ctx.textBaseline = "middle";
ctx.fillStyle = "rgba(255,255,255,0.5)";
ctx.strokeStyle = "rgba(0,0,0,0.5)";
var xT = image.width / 2 - textWidth / 2;
var yT = image.height / 2;
ctx.fillText(markText, xT, yT);
ctx.strokeText(markText, xT, yT);

Comments

0

use second temporary canvas2D

 function determineWidth(text, font) {
     var textCtx = document.createElement("canvas").getContext("2d");
     textCtx.font = font
     let measure = textCtx.measureText(text);
     let fontHei = parseInt(font.slice(0, font.indexOf("px")))
     return [measure.width,fontHei]
 }

use it before create real canvas

   let font = `40px monospace`;
   let [wid,hei]=determineWidth(text,font)
   let textAlign = "center";
   let textBaseline = "middle";
   let fillStyle = "black"; 
   var textCtx = document.createElement("canvas").getContext("2d");
            
   textCtx.canvas.width = wid;
   textCtx.canvas.height = hei;
          
   textCtx.font = font
   textCtx.textAlign = textAlign
   textCtx.textBaseline = textBaseline
   textCtx.fillStyle = fillStyle

   textCtx.clearRect(0, 0, textCtx.canvas.width, textCtx.canvas.height);
   textCtx.fillText(text, textCtx.canvas.width / 2, textCtx.canvas.height / 2);

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.