Arian Faurtosh
Arian Faurtosh

Reputation: 18491

Size to fit font on a canvas

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

    }
});

Upvotes: 18

Views: 28738

Answers (7)

markE
markE

Reputation: 105015

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>

Upvotes: 35

&#233;tale-cohomology
&#233;tale-cohomology

Reputation: 1861

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)

Upvotes: 0

Linh
Linh

Reputation: 60913

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>

Upvotes: 4

Veetaha
Veetaha

Reputation: 862

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.

Upvotes: 13

Vibber
Vibber

Reputation: 127

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

Upvotes: 6

rob
rob

Reputation: 8400

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.

Upvotes: 3

daker
daker

Reputation: 3540

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

    }
});

Upvotes: 6

Related Questions