16

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

    }
});
gman
  • 100,619
  • 31
  • 269
  • 393
Arian Faurtosh
  • 17,987
  • 21
  • 77
  • 115
  • So you want the whole text to always fit on one row? Or do you want the text to fill as much as possible? – daker Dec 12 '13 at 18:45
  • The whole text should only fit on one row or line. – Arian Faurtosh Dec 12 '13 at 18:46
  • 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? – Mark Dec 12 '13 at 18:47
  • 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: http://jsfiddle.net/dgAEY/1/ – Arian Faurtosh Dec 12 '13 at 18:48
  • No loops are needed, just do sample and scale appropriately, please see my answer. – Veetaha Apr 26 '20 at 13:50

7 Answers7

33

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>
EzioMercer
  • 1,502
  • 2
  • 7
  • 23
markE
  • 102,905
  • 11
  • 164
  • 176
  • @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. – daker Dec 12 '13 at 19:16
  • 1
    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. – markE Dec 12 '13 at 20:45
  • 5
    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. – ccnokes Apr 23 '14 at 20:09
  • 2
    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). – Anonymous Jan 07 '16 at 12:44
11

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.

Veetaha
  • 833
  • 1
  • 8
  • 19
  • Tested and works, and no loop required like other answers. Returns a decimal which is more accurate. – CrazyTim Jul 20 '20 at 04:50
  • 1
    Could be improved slightly to pass a reference to an existing canvas context instead of creating a new canvas each time its called. – CrazyTim Jul 20 '20 at 05:01
6

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

    }
});
daker
  • 3,430
  • 3
  • 41
  • 55
  • 2
    Doing so will just squeeze the text together after reaching max width, this doesn't actually shrink text size: http://jsfiddle.net/dgAEY/2/ – Arian Faurtosh Dec 12 '13 at 18:58
  • Then you should look up `measureText()` https://developer.mozilla.org/en-US/docs/Drawing_text_using_a_canvas#measureText() – daker Dec 12 '13 at 19:11
6

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;
}
Vibber
  • 127
  • 1
  • 7
  • 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. – Vibber Apr 01 '16 at 10:11
4

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>
Linh
  • 57,942
  • 23
  • 262
  • 279
3

this will only apply to a small set of circumstances but if provided a perfect answer to the problem for me. The last [optional] parameter to fillText is maxWidth, so

ctx.fillText('long text', x, y, maxWidth);

Will render 'long text' at x,y squashing the result to maxWidth.

For really long text this produces absolutely awful results but for the odd string that was just exceeding your maxWidth it can be a very simple god-send.

rob
  • 8,134
  • 8
  • 58
  • 68
0

If you wanna do it the other way around (size the canvas to the font, not the font to the canvas), you can do something like the following (not fully tested):

var canvas    = document.getElementById('canvas00')
var ctx       = canvas.getContext('2d')
var str       = 'Jesus is God Almighty'
ctx.font      = 'bold 3em'

var metrics   = ctx.measureText(str)
canvas.width  = metrics.width
canvas.height = metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent  // Use *fontBoundingBox* or *actualBoundingBox*, depending on your needs
ctx.font      = 'bold 3em'  // For some reason you have to call this again?
ctx.fillStyle = '#3240ff'

ctx.fillText(str, 0, canvas.height - metrics.fontBoundingBoxDescent)
étale-cohomology
  • 2,098
  • 2
  • 28
  • 33