Size to fit font on a canvas

23,201

Solution 1

You can use context.measureText to get the pixel width of any given text in the current font.

Then if that width is too big, reduce the font size until it fits.

context.font="14px verdana";

var width = context.measureText("Hello...Do I fit on the canvas?").width

if(width>myDesiredWidth)  // then reduce the font size and re-measure

Demo:

var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");

fitTextOnCanvas("Hello, World!", "verdana", 125);


function fitTextOnCanvas(text, fontface, yPosition) {

  // start with a large font size
  var fontsize = 300;

  // lower the font size until the text fits the canvas
  do {
    fontsize--;
    context.font = fontsize + "px " + fontface;
  } while (context.measureText(text).width > canvas.width)

  // draw the text
  context.fillText(text, 0, yPosition);

  alert("A fontsize of " + fontsize + "px fits this text on the canvas");

}
body {
  background-color: ivory;
}

#canvas {
  border: 1px solid red;
}
<canvas id="canvas" width=300 height=300></canvas>

Solution 2

Simple and efficient solution for DOM environment, no loops, just do one sample measurement and scale the result appropriately.

function getFontSizeToFit(text: string, fontFace: string, maxWidth: number) {
    const ctx = document.createElement('canvas').getContext('2d');
    ctx.font = `1px ${fontFace}`;
    return maxWidth / ctx.measureText(text).width;
}

Beware that if you use npm 'canvas' module for NodeJs environment, the result won't be that accurate as they use some custom C++ implementation that returns only integer sample width.

Solution 3

I have created an improved version of markE's code. It becomes apparent that his code is slower if you have multiple texts. The browsers are good at caching but on the first run you can definitely get a noticeable lag even with just a handful of lines that need scaling.

Try these two versions:

Original method (markE):

http://jsfiddle.net/be6ppdre/29/

Faster method:

http://jsfiddle.net/ho9thkvo/2/

The main code is here:

function fitTextOnCanvas(text, fontface){    
    var size = measureTextBinaryMethod(text, fontface, 0, 600, canvas.width);
    return size;
}

function measureTextBinaryMethod(text, fontface, min, max, desiredWidth) {
    if (max-min < 1) {
        return min;     
    }
    var test = min+((max-min)/2); //Find half interval
    context.font=test+"px "+fontface;
    measureTest = context.measureText(text).width;
    if ( measureTest > desiredWidth) {
        var found = measureTextBinaryMethod(text, fontface, min, test, desiredWidth)
    } else {
        var found = measureTextBinaryMethod(text, fontface, test, max, desiredWidth)
    }
    return found;
}

Solution 4

Add the maxWidth Parameter to your context.textfill

$(document).ready(function () {
    var canvas = $('#myCanvas')[0];
    var context = canvas.getContext('2d');

    var imageObj = new Image();
    imageObj.onload = function() {
        context.drawImage(imageObj, 0, 0);
    };
    imageObj.src = "http://dummyimage.com/282x177/FFF/FFF"; 

    $('#nametag').bind('change keyup input', updateCanvas);
    $('#line2').bind('click', updateCanvas);
    $('#line3').bind('click', updateCanvas);
    $('#line4').bind('click', updateCanvas);

    function updateCanvas() {
        var maxWith = canvas.width;

        context.clearRect(0, 0, canvas.width, canvas.height);
        context.drawImage(imageObj, 0, 0);
        context.textAlign = "center";

        context.font = "bold 18pt Arial";
        context.fillText($('#line1').val(), canvas.width * 0.5, 70, maxWith);

        context.font = "12pt Arial";
        context.fillText($('#line2').val(), canvas.width * 0.5, 90, maxWith);
        context.fillText($('#line3').val(), canvas.width * 0.5, 120, maxWith);
        context.fillText($('#line4').val(), canvas.width * 0.5, 140, maxWith);

    }
});

Solution 5

Base on @Veetaha answer, here is my function to fit multiple lines of center text

function getFontSizeToFit(ctx, text, fontFace, width, height) {
    ctx.font = `1px ${fontFace}`;
    
    let fitFontWidth = Number.MAX_VALUE
    const lines = text.match(/[^\r\n]+/g);
    lines.forEach(line => {
        fitFontWidth = Math.min(fitFontWidth, width / ctx.measureText(line).width)
    })
    let fitFontHeight = height / (lines.length * 1.2); // if you want more spacing between line, you can increase this value
    return Math.min(fitFontHeight, fitFontWidth)
}

Demo

var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");

let text = "Hello World \n Hello World 2222 \n AAAAA \n BBBB"

fitTextCenter()
function fitTextCenter() {
    let fontSize = getFontSizeToFit(ctx, text, "Arial", c.width, c.height)
    ctx.font = fontSize + "px Arial"

    fillTextCenter(ctx, text, 0, 0, c.width, c.height)
}

function fillTextCenter(ctx, text, x, y, width, height) {
    ctx.textBaseline = 'middle';
    ctx.textAlign = "center";

    const lines = text.match(/[^\r\n]+/g);
    for (let i = 0; i < lines.length; i++) {
        let xL = (width - x) / 2
        let yL = y + (height / (lines.length + 1)) * (i + 1)

        ctx.fillText(lines[i], xL, yL)
    }
}

function getFontSizeToFit(ctx, text, fontFace, width, height) {
    ctx.font = `1px ${fontFace}`;

    let fitFontWidth = Number.MAX_VALUE
    const lines = text.match(/[^\r\n]+/g);
    lines.forEach(line => {
        fitFontWidth = Math.min(fitFontWidth, width / ctx.measureText(line).width)
    })
    let fitFontHeight = height / (lines.length * 1.2); // if you want more spacing between line, you can increase this value
    return Math.min(fitFontHeight, fitFontWidth)
}

function testScaleUpX() {
    c.width += 1
    fitTextCenter()
}

function testScaleUpY() {
    c.height += 1
    fitTextCenter()
}

function testScaleDownX() {
    c.width -= 1
    fitTextCenter()
}

function testScaleDownY() {
    c.height -= 1
    fitTextCenter()
}
<canvas id="myCanvas" width="200" height="80" style="border:1px solid #000;"></canvas>
<button onclick="testScaleUpX()">+ X</button>
<button onclick="testScaleUpY()">+ Y</button>
<button onclick="testScaleDownX()">- X</button>
<button onclick="testScaleDownY()">- Y</button>

Share:
23,201
Arian Faurtosh
Author by

Arian Faurtosh

Blog: https://arian.io Github: https://github.com/arianf Linkedin: http://linkedin.com/in/arianf

Updated on February 10, 2022

Comments

  • Arian Faurtosh
    Arian Faurtosh about 2 years

    I currently have this http://jsfiddle.net/dgAEY/ which works perfectly, I just need to figure out a way to size the font when it gets too long. I've looked into Auto-size dynamic text to fill fixed size container and I've tried to apply the Jquery function they posted but I couldn't get it to work.

    HTML

    <form action="" method="POST" id="nametag" class="nametag">
        Line1: 
        <input type="text" id="line1" name="line1" style="width:250px;" /><br>
        Line2:
        <input type="text" id="line2" name="line2" style="width:250px;" /><br>
        Line3:
        <input type="text" id="line3" name="line3" style="width:250px;" /><br>
        Line4:
        <input type="text" id="line4" name="line4" style="width:250px;" /><br>
    
        <br><br><b>Name Tag</b><br>
        <canvas width="282px" height="177px" id="myCanvas" style="border: black thin solid;"></canvas>
    </form>
    

    JavaScript

    $(document).ready(function () {
        var canvas = $('#myCanvas')[0];
        var context = canvas.getContext('2d');
    
        var imageObj = new Image();
        imageObj.onload = function() {
            context.drawImage(imageObj, 0, 0);
        };
        imageObj.src = "http://dummyimage.com/282x177/FFF/FFF"; 
    
        $('#nametag').bind('change keyup input', updateCanvas);
        $('#line2').bind('click', updateCanvas);
        $('#line3').bind('click', updateCanvas);
        $('#line4').bind('click', updateCanvas);
    
        function updateCanvas() {
    
            context.clearRect(0, 0, canvas.width, canvas.height);
            context.drawImage(imageObj, 0, 0);
            context.textAlign = "center";
    
            context.font = "bold 18pt Arial";
            context.fillText($('#line1').val(), canvas.width * 0.5, 70);
    
            context.font = "12pt Arial";
            context.fillText($('#line2').val(), canvas.width * 0.5, 90);
            context.fillText($('#line3').val(), canvas.width * 0.5, 120);
            context.fillText($('#line4').val(), canvas.width * 0.5, 140);
    
        }
    });
    
    • daker
      daker over 10 years
      So you want the whole text to always fit on one row? Or do you want the text to fill as much as possible?
    • Arian Faurtosh
      Arian Faurtosh over 10 years
      The whole text should only fit on one row or line.
    • Mark
      Mark over 10 years
      I see in the linked post they extend jQuery to add the function textfill but I don't see you using the function? Did you extend jQuery with that function and use it?
    • Arian Faurtosh
      Arian Faurtosh over 10 years
      I did extend it, and tried using the function with context.textfill but it broke the javascript, and didn't do anything... Here is that example: jsfiddle.net/dgAEY/1
    • Veetaha
      Veetaha about 4 years
      No loops are needed, just do sample and scale appropriately, please see my answer.
  • Arian Faurtosh
    Arian Faurtosh over 10 years
    Doing so will just squeeze the text together after reaching max width, this doesn't actually shrink text size: jsfiddle.net/dgAEY/2
  • daker
    daker over 10 years
    Then you should look up measureText() developer.mozilla.org/en-US/docs/…
  • daker
    daker over 10 years
    @Arian Yes, but you can do it in a loop as well and loop until width < desired width and make a step in text size each turn.
  • markE
    markE over 10 years
    As @daker says, let the computer do the work for you by putting the resizing in a loop. I updated my answer with code that finds the appropriate font size and draws the text to fit the canvas.
  • ccnokes
    ccnokes about 10 years
    It should be: context.font=fontsize+"px "+fontface; (you forgot the "px"). Without that, it doesn't work. Otherwise, it works. Thanks for the code.
  • Anonymous
    Anonymous over 8 years
    I'd suggest binary search loop instead of linear search, so for max font size = 1024px only up to 10 iterations are needed in order to get fit size with 1px precision (instead of ~1000 for tiny test area).
  • Vibber
    Vibber about 8 years
    The 0 and 600 in the call to measureTextBinaryMethod is the range of sizes that the text can have. So in other words you need to set the max size to a font size that is big enough for the shortest of your texts.
  • CrazyTim
    CrazyTim almost 4 years
    Tested and works, and no loop required like other answers. Returns a decimal which is more accurate.
  • CrazyTim
    CrazyTim almost 4 years
    Could be improved slightly to pass a reference to an existing canvas context instead of creating a new canvas each time its called.
  • Will
    Will over 2 years
    thanks! multiple-line is important for us.