Letter spacing in canvas element - javascript

The question says it all pretty much. I've been searching around and starting to worry that it's impossible.
I've got this canvas element that I'm drawing text to. I want to set the letter spacing similar to the CSS letter-spacing attribute. By that I mean increasing the amount of pixels between letters when a string is drawn.
My code for drawing the text is like so, ctx is the canvas context variable.
ctx.font = "3em sheepsans";
ctx.textBaseline = "middle";
ctx.textAlign = "center";
ctx.fillStyle = "rgb(255, 255, 255)";
ctx.fillText("Blah blah text", 1024 / 2, 768 / 2);
I've tried adding ctx.letterSpacing = "2px"; before the drawing but with no avail. Is there a way to do this just with a simple setting, or will I have to make a function to individually draw each character with the spacing in mind?

You can't set the letter spacing property, but you you can accomplish wider letter spacing in canvas by inserting one of the various white spaces in between every letter in the string. For instance
ctx.font = "3em sheepsans";
ctx.textBaseline = "middle";
ctx.textAlign = "center";
ctx.fillStyle = "rgb(255, 255, 255)";
var ctext = "Blah blah text".split("").join(String.fromCharCode(8202))
ctx.fillText(ctext, 1024 / 2, 768 / 2);
This will insert a hair space between every letter.
Using 8201 (instead of 8202) will insert the slightly wider thin space
For more white space options, see this list of Unicode Spaces
This method will help you to preserve the font's kerning much more easily than manually positioning each letter, however you wont be able to tighten your letter spacing this way.

I'm not sure if it should work (per specs), but in some browsers (Chrome) you can set the letter-spacing CSS property on the <canvas> element itself, and it will be applied to all text drawn on the context. (Works in Chrome v56, does not work in Firefox v51 or IE v11.)
Note that in Chrome v56 you must re-get the canvas 2d context (and re-set any values you care about) after each change to the letter-spacing style; the spacing appears to be baked into the 2d context that you get.
Example: https://jsfiddle.net/hg4pbsne/1/
var inp = document.querySelectorAll('input'),
can = document.querySelector('canvas'),
ctx = can.getContext('2d');
can.width = can.offsetWidth;
[].forEach.call(inp,function(inp){ inp.addEventListener('input', redraw, false) });
redraw();
function redraw(){
ctx.clearRect(0,0,can.width,can.height);
can.style.letterSpacing = inp[0].value + 'px';
ctx = can.getContext('2d');
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = '4em sans-serif';
ctx.fillText('Hello', can.width/2, can.height*1/4);
can.style.letterSpacing = inp[1].value + 'px';
ctx = can.getContext('2d');
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = '4em sans-serif';
ctx.fillText('World', can.width/2, can.height*3/4);
};
canvas { background:white }
canvas, label { display:block; width:400px; margin:0.5em auto }
<canvas></canvas>
<label>hello spacing: <input type="range" min="-20" max="40" value="1" step="0.1"></label>
<label>world spacing: <input type="range" min="-20" max="40" value="1" step="0.1"></label>
Original, cross-browser answer:
This is not possible; the HTML5 Canvas does not have all the text-transformation power of CSS in HTML. I would suggest that you should combine the appropriate technologies for each usage. Use HTML layered with Canvas and perhaps even SVG, each doing what it does best.
Note also that 'rolling your own'—drawing each character with a custom offset—is going to produce bad results for most fonts, given that there are letter kerning pairs and pixel-aligned font hinting.

You can't set letter-spacing as a property of the Canvas context. You can only achieve the effect by doing manual spacing, sorry. (As in, drawing each letter manually increasing the x by some pixel amount on each)
For the record, you can set a few text properties by using ctx.font but letter-spacing is not one of them. The ones you can set are: "font-style font-variant font-weight font-size/line-height font-family"
For instance you can technically write ctx.font = "bold normal normal 12px/normal Verdana" (or any omission of any of those) and it will parse correctly.

To allow for 'letter kerning pairs' and the like, I've written the following. It should take that into account, and rough testing suggests it does. If you have any comments on it then I would point you to my question on the subject (Adding Letter Spacing in HTML Canvas)
Basically it uses measureText() to get the width of the whole string, and then removes the first character of the string and measures the width of the remaining string, and uses the difference to calculate the correct positioning - thus taking into account kerning pairs and the like. See the given link for more pseudocode.
Here's the HTML:
<canvas id="Test1" width="800px" height="200px"><p>Your browser does not support canvas.</p></canvas>
Here's the code:
this.fillTextWithSpacing = function(context, text, x, y, spacing)
{
//Start at position (X, Y).
//Measure wAll, the width of the entire string using measureText()
wAll = context.measureText(text).width;
do
{
//Remove the first character from the string
char = text.substr(0, 1);
text = text.substr(1);
//Print the first character at position (X, Y) using fillText()
context.fillText(char, x, y);
//Measure wShorter, the width of the resulting shorter string using measureText().
if (text == "")
wShorter = 0;
else
wShorter = context.measureText(text).width;
//Subtract the width of the shorter string from the width of the entire string, giving the kerned width of the character, wChar = wAll - wShorter
wChar = wAll - wShorter;
//Increment X by wChar + spacing
x += wChar + spacing;
//wAll = wShorter
wAll = wShorter;
//Repeat from step 3
} while (text != "");
}
Code for demo/eyeball test:
element1 = document.getElementById("Test1");
textContext1 = element1.getContext('2d');
textContext1.font = "72px Verdana, sans-serif";
textContext1.textAlign = "left";
textContext1.textBaseline = "top";
textContext1.fillStyle = "#000000";
text = "Welcome to go WAVE";
this.fillTextWithSpacing(textContext1, text, 0, 0, 0);
textContext1.fillText(text, 0, 100);
Ideally I'd throw multiple random strings at it and do a pixel by pixel comparison. I'm also not sure how good Verdana's default kerning is, though I understand it's better than Arial - suggestions on other fonts to try gratefully accepted.
So... so far it looks good. In fact it looks perfect.
Still hoping that someone will point out any flaws in the process.
In the meantime I will put this here for others to see if they are looking for a solution on this.

here's some coffeescript that allows you to set kerning to your context like so
tctx = tcanv.getContext('2d')
tctx.kerning = 10
tctx.fillStyle = 'black'
tctx.fillText 'Hello World!', 10, 10
the supporting code is:
_fillText = CanvasRenderingContext2D::fillText
CanvasRenderingContext2D::fillText = (str, x, y, args...) ->
# no kerning? default behavior
return _fillText.apply this, arguments unless #kerning?
# we need to remember some stuff as we loop
offset = 0
_.each str, (letter) =>
_fillText.apply this, [
letter
x + offset + #kerning
y
].concat args # in case any additional args get sent to fillText at any time
offset += #measureText(letter).width + #kerning
The javascript would be
var _fillText,
__slice = [].slice;
_fillText = CanvasRenderingContext2D.prototype.fillText;
CanvasRenderingContext2D.prototype.fillText = function() {
var args, offset, str, x, y,
_this = this;
str = arguments[0], x = arguments[1], y = arguments[2], args = 4 <= arguments.length ? __slice.call(arguments, 3) : [];
if (this.kerning == null) {
return _fillText.apply(this, arguments);
}
offset = 0;
return _.each(str, function(letter) {
_fillText.apply(_this, [letter, x + offset + _this.kerning, y].concat(args));
offset += _this.measureText(letter).width + _this.kerning;
});
};

Not true. You can add letter-spacing property to the canvas element in css and it works perfectly. No need for complicated workarounds. I just figured it out right now in my canvas project. i.e.:
canvas {
width: 480px;
height: 350px;
margin: 30px auto 0;
padding: 15px 0 0 0;
background: pink;
display: block;
border: 2px dashed brown;
letter-spacing: 0.1em;
}

This might an old question, but it's still relevant. I took Patrick Matte's expansion of James Carlyle-Clarke's response and got something that, I think, works quite well as is still plain-old-Javascript. The idea is to measure the space between two consecutive characters and "add" to it. Yes, negative numbers work.
Here's what I've got (the heavily commented version):
function fillTextWithSpacing (context, text, x, y, spacing) {
// Total width is needed to adjust the starting X based on text alignment.
const total_width = context.measureText (text).width + spacing * (text.length - 1);
// We need to save the current text alignment because we have to set it to
// left for our calls to fillText() draw in the places we expect. Don't
// worry, we're going to set it back at the end.
const align = context.textAlign;
context.textAlign = "left";
// Nothing to do for left alignment, but adjustments are needed for right
// and left. Justify defeats the purpose of manually adjusting character
// spacing, and it requires a width to be known.
switch (align) {
case "right":
x -= total_width;
break;
case "center":
x -= total_width / 2;
break;
}
// We have some things to keep track of and the C programmer in me likes
// declarations on their own and in groups.
let offset, pair_width, char_width, char_next_width, pair_spacing, char, char_next;
// We're going to step through the text one character at a time, but we
// can't use for(... of ...) because we need to be able to look ahead.
for (offset = 0; offset < text.length; offset = offset + 1) {
// Easy on the eyes later
char = text.charAt (offset);
// Default the spacing between the "pair" of characters to 0. We need
// for the last character.
pair_spacing = 0;
// Check to see if there's at least one more character after this one.
if (offset + 1 < text.length) {
// This is always easier on the eyes
char_next = text.charAt (offset + 1);
// Measure to the total width of both characters, including the
// spacing between them... even if it's negative.
pair_width = context.measureText (char + char_next).width;
// Measure the width of just the current character.
char_width = context.measureText (char).width;
// Measure the width of just the next character.
char_next_width = context.measureText (char_next).width;
// We can determine the kerning by subtracting the width of each
// character from the width of both characters together.
pair_spacing = pair_width - char_width - char_next_width;
}
// Draw the current character
context.fillText (char, x, y);
// Advanced the X position by adding the current character width, the
// spacing between the current and next characters, and the manual
// spacing adjustment (negatives work).
x = x + char_width + pair_spacing + spacing;
}
// Set the text alignment back to the original value.
context.textAlign = align;
// Profit
}
And here's a demo:
let canvas = document.getElementById ("canvas");
canvas.width = 600;
canvas.height = 150;
let context = canvas.getContext ("2d");
function fillTextWithSpacing (context, text, x, y, spacing) {
const total_width = context.measureText (text).width + spacing * (text.length - 1);
const align = context.textAlign;
context.textAlign = "left";
switch (align) {
case "right":
x -= total_width;
break;
case "center":
x -= total_width / 2;
break;
}
let offset, pair_width, char_width, char_next_width, pair_spacing, char, char_next;
for (offset = 0; offset < text.length; offset = offset + 1) {
char = text.charAt (offset);
pair_spacing = 0;
if (offset + 1 < text.length) {
char_next = text.charAt (offset + 1);
pair_width = context.measureText (char + char_next).width;
char_width = context.measureText (char).width;
char_next_width = context.measureText (char_next).width;
pair_spacing = pair_width - char_width - char_next_width;
}
context.fillText (char, x, y);
x = x + char_width + pair_spacing + spacing;
}
context.textAlign = align;
}
function update () {
let
font = document.getElementById ("font").value,
size = parseInt (document.getElementById ("size").value, 10),
weight = parseInt (document.getElementById ("weight").value, 10),
italic = document.getElementById ("italic").checked,
spacing = parseInt (document.getElementById ("spacing").value, 10),
text = document.getElementById ("text").value;
context.textAlign = "center";
context.textBaseline = "alphabetic";
context.fillStyle = "#404040";
context.font = (italic ? "italic " : "") + weight + " " + size + "px " + font;
context.clearRect (0, 0, canvas.width, canvas.height);
fillTextWithSpacing (context, text, canvas.width / 2, (canvas.height + size) / 2, spacing);
}
document.getElementById ("font").addEventListener (
"change",
(event) => {
update ();
}
);
document.getElementById ("size").addEventListener (
"change",
(event) => {
update ();
}
);
document.getElementById ("weight").addEventListener (
"change",
(event) => {
update ();
}
);
document.getElementById ("italic").addEventListener (
"change",
(event) => {
update ();
}
);
document.getElementById ("spacing").addEventListener (
"change",
(event) => {
update ();
}
);
document.getElementById ("text").addEventListener (
"input",
(event) => {
update ();
}
);
update ();
select, input {
display: inline-block;
}
input[type=text] {
display: block;
margin: 0.5rem 0;
}
canvas {
border: 1px solid #b0b0b0;
width: 600px;
height: 150px;
}
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
</head>
<body>
<select id="font">
<option value="serif">Serif</option>
<option value="sans-serif">Sans Serif</option>
<option value="fixed-width">Fixed Width</option>
</select>
<label>Size: <input type="number" id="size" value="60" min="1" max="200" size="3" /></label>
<label>Weight: <input type="number" id="weight" value="100" min="100" max="1000" step="100" size="4" /></label>
<label>Italic: <input type="checkbox" id="italic" checked /></label>
<label>Spacing: <input type="number" id="spacing" value="0" min="-200" max="200" size="4" /></label>
<input type="text" id="text" placeholder="Text" value="hello" size="40"/>
<canvas id="canvas"></canvas>
</body>
</html>

Actually letter spacing concept canvas is not supporting.
So i used javascript to do this.
var value = $('#sourceText1').val().split("").join(" ");
OR
var sample_text = "Praveen Chelumalla";
var text = sample_text.split("").join(" ");

I don't know about other people but I have adjusted line spacing by increasing the y value on the text that I am writing. I'm actually splitting a string by spaces and kicking each word down a line inside a loop. The numbers i use are based on the default font. If you use a different font that these numbers may need to be adjusted.
// object holding x and y coordinates
var vectors = {'x':{1:100, 2:200}, 'y':{1:0, 2:100}
// replace the first letter of a word
var newtext = YOURSTRING.replace(/^\b[a-z]/g, function(oldtext) {
// return it uppercase
return oldtext.toUpperCase();
});
// split string by spaces
newtext = newtext.split(/\s+/);
// line height
var spacing = 10 ;
// initial adjustment to position
var spaceToAdd = 5;
// for each word in the string draw it based on the coordinates + spacing
for (var c = 0; c < newtext.length; c++) {
ctx.fillText(newtext[c], vectors.x[i], vectors.y[i] - spaceToAdd);
// increment the spacing
spaceToAdd += spacing;
}

Here's another method based on James Carlyle-Clarke's previous answer. It also lets you align the text left, center and right.
export function fillTextWithSpacing(context, text, x, y, spacing, textAlign) {
const totalWidth = context.measureText(text).width + spacing * (text.length - 1);
switch (textAlign) {
case "right":
x -= totalWidth;
break;
case "center":
x -= totalWidth / 2;
break;
}
for (let i = 0; i < text.length; i++) {
let char = text.charAt(i);
context.fillText(char, x, y);
x += context.measureText(char).width + spacing;
}
}

Letter spacing in canvas IS SUPPORTED, I used this
canvas = document.getElementById('canvas');
canvas.style.letterSpacing = '2px';

I use:
ctx.font = "32px Tahoma";//set font
ctx.scale(0.75,1);//important! the scale
ctx.fillText("LaFeteParFete test text", 2, 274);//draw
ctx.setTransform(1,0,0,1,0,0);//reset transform

Related

How do i calculate the height of each letter from a string, NOT on a canvas? [duplicate]

The spec has a context.measureText(text) function that will tell you how much width it would require to print that text, but I can't find a way to find out how tall it is. I know it's based on the font, but I don't know to convert a font string to a text height.
Browsers are beginning to support advanced text metrics, which will make this task trivial when it's widely supported:
let metrics = ctx.measureText(text);
let fontHeight = metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent;
let actualHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
fontHeight gets you the bounding box height that is constant regardless of the string being rendered. actualHeight is specific to the string being rendered.
Spec: https://www.w3.org/TR/2012/CR-2dcontext-20121217/#dom-textmetrics-fontboundingboxascent and the sections just below it.
Support status (20-Aug-2017):
Chrome has it behind a flag (https://bugs.chromium.org/p/chromium/issues/detail?id=277215).
Firefox has it in development (https://bugzilla.mozilla.org/show_bug.cgi?id=1102584).
Edge has no support (https://wpdev.uservoice.com/forums/257854-microsoft-edge-developer/suggestions/30922861-advanced-canvas-textmetrics).
node-canvas (node.js module), mostly supported (https://github.com/Automattic/node-canvas/wiki/Compatibility-Status).
UPDATE - for an example of this working, I used this technique in the Carota editor.
Following on from ellisbben's answer, here is an enhanced version to get the ascent and descent from the baseline, i.e. same as tmAscent and tmDescent returned by Win32's GetTextMetric API. This is needed if you want to do a word-wrapped run of text with spans in different fonts/sizes.
The above image was generated on a canvas in Safari, red being the top line where the canvas was told to draw the text, green being the baseline and blue being the bottom (so red to blue is the full height).
Using jQuery for succinctness:
var getTextHeight = function(font) {
var text = $('<span>Hg</span>').css({ fontFamily: font });
var block = $('<div style="display: inline-block; width: 1px; height: 0px;"></div>');
var div = $('<div></div>');
div.append(text, block);
var body = $('body');
body.append(div);
try {
var result = {};
block.css({ verticalAlign: 'baseline' });
result.ascent = block.offset().top - text.offset().top;
block.css({ verticalAlign: 'bottom' });
result.height = block.offset().top - text.offset().top;
result.descent = result.height - result.ascent;
} finally {
div.remove();
}
return result;
};
In addition to a text element, I add a div with display: inline-block so I can set its vertical-align style, and then find out where the browser has put it.
So you get back an object with ascent, descent and height (which is just ascent + descent for convenience). To test it, it's worth having a function that draws a horizontal line:
var testLine = function(ctx, x, y, len, style) {
ctx.strokeStyle = style;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + len, y);
ctx.closePath();
ctx.stroke();
};
Then you can see how the text is positioned on the canvas relative to the top, baseline and bottom:
var font = '36pt Times';
var message = 'Big Text';
ctx.fillStyle = 'black';
ctx.textAlign = 'left';
ctx.textBaseline = 'top'; // important!
ctx.font = font;
ctx.fillText(message, x, y);
// Canvas can tell us the width
var w = ctx.measureText(message).width;
// New function gets the other info we need
var h = getTextHeight(font);
testLine(ctx, x, y, w, 'red');
testLine(ctx, x, y + h.ascent, w, 'green');
testLine(ctx, x, y + h.height, w, 'blue');
You can get a very close approximation of the vertical height by checking the length of a capital M.
ctx.font = 'bold 10px Arial';
lineHeight = ctx.measureText('M').width;
The canvas spec doesn't give us a method for measuring the height of a string. However, you can set the size of your text in pixels and you can usually figure out what the vertical bounds are relatively easily.
If you need something more precise then you could throw text onto the canvas and then get pixel data and figure out how many pixels are used vertically. This would be relatively simple, but not very efficient. You could do something like this (it works, but draws some text onto your canvas that you would want to remove):
function measureTextHeight(ctx, left, top, width, height) {
// Draw the text in the specified area
ctx.save();
ctx.translate(left, top + Math.round(height * 0.8));
ctx.mozDrawText('gM'); // This seems like tall text... Doesn't it?
ctx.restore();
// Get the pixel data from the canvas
var data = ctx.getImageData(left, top, width, height).data,
first = false,
last = false,
r = height,
c = 0;
// Find the last line with a non-white pixel
while(!last && r) {
r--;
for(c = 0; c < width; c++) {
if(data[r * width * 4 + c * 4 + 3]) {
last = r;
break;
}
}
}
// Find the first line with a non-white pixel
while(r) {
r--;
for(c = 0; c < width; c++) {
if(data[r * width * 4 + c * 4 + 3]) {
first = r;
break;
}
}
// If we've got it then return the height
if(first != r) return last - first;
}
// We screwed something up... What do you expect from free code?
return 0;
}
// Set the font
context.mozTextStyle = '32px Arial';
// Specify a context and a rect that is safe to draw in when calling measureTextHeight
var height = measureTextHeight(context, 0, 0, 50, 50);
console.log(height);
For Bespin they do fake a height by measuring the width of a lowercase 'm'... I don't know how this is used, and I would not recommend this method. Here is the relevant Bespin method:
var fixCanvas = function(ctx) {
// upgrade Firefox 3.0.x text rendering to HTML 5 standard
if (!ctx.fillText && ctx.mozDrawText) {
ctx.fillText = function(textToDraw, x, y, maxWidth) {
ctx.translate(x, y);
ctx.mozTextStyle = ctx.font;
ctx.mozDrawText(textToDraw);
ctx.translate(-x, -y);
}
}
if (!ctx.measureText && ctx.mozMeasureText) {
ctx.measureText = function(text) {
ctx.mozTextStyle = ctx.font;
var width = ctx.mozMeasureText(text);
return { width: width };
}
}
if (ctx.measureText && !ctx.html5MeasureText) {
ctx.html5MeasureText = ctx.measureText;
ctx.measureText = function(text) {
var textMetrics = ctx.html5MeasureText(text);
// fake it 'til you make it
textMetrics.ascent = ctx.html5MeasureText("m").width;
return textMetrics;
}
}
// for other browsers
if (!ctx.fillText) {
ctx.fillText = function() {}
}
if (!ctx.measureText) {
ctx.measureText = function() { return 10; }
}
};
EDIT: Are you using canvas transforms? If so, you'll have to track the transformation matrix. The following method should measure the height of text with the initial transform.
EDIT #2: Oddly the code below does not produce correct answers when I run it on this StackOverflow page; it's entirely possible that the presence of some style rules could break this function.
The canvas uses fonts as defined by CSS, so in theory we can just add an appropriately styled chunk of text to the document and measure its height. I think this is significantly easier than rendering text and then checking pixel data and it should also respect ascenders and descenders. Check out the following:
var determineFontHeight = function(fontStyle) {
var body = document.getElementsByTagName("body")[0];
var dummy = document.createElement("div");
var dummyText = document.createTextNode("M");
dummy.appendChild(dummyText);
dummy.setAttribute("style", fontStyle);
body.appendChild(dummy);
var result = dummy.offsetHeight;
body.removeChild(dummy);
return result;
};
//A little test...
var exampleFamilies = ["Helvetica", "Verdana", "Times New Roman", "Courier New"];
var exampleSizes = [8, 10, 12, 16, 24, 36, 48, 96];
for(var i = 0; i < exampleFamilies.length; i++) {
var family = exampleFamilies[i];
for(var j = 0; j < exampleSizes.length; j++) {
var size = exampleSizes[j] + "pt";
var style = "font-family: " + family + "; font-size: " + size + ";";
var pixelHeight = determineFontHeight(style);
console.log(family + " " + size + " ==> " + pixelHeight + " pixels high.");
}
}
You'll have to make sure you get the font style correct on the DOM element that you measure the height of but that's pretty straightforward; really you should use something like
var canvas = /* ... */
var context = canvas.getContext("2d");
var canvasFont = " ... ";
var fontHeight = determineFontHeight("font: " + canvasFont + ";");
context.font = canvasFont;
/*
do your stuff with your font and its height here.
*/
As JJ Stiff suggests, you can add your text to a span and then measure the offsetHeight of the span.
var d = document.createElement("span");
d.font = "20px arial";
d.textContent = "Hello world!";
document.body.appendChild(d);
var emHeight = d.offsetHeight;
document.body.removeChild(d);
As shown on HTML5Rocks
Isn't the height of the text in pixels equal to the font size (in pts) if you define the font using context.font ?
Just to add to Daniel's answer (which is great! and absolutely right!), version without JQuery:
function objOff(obj)
{
var currleft = currtop = 0;
if( obj.offsetParent )
{ do { currleft += obj.offsetLeft; currtop += obj.offsetTop; }
while( obj = obj.offsetParent ); }
else { currleft += obj.offsetLeft; currtop += obj.offsetTop; }
return [currleft,currtop];
}
function FontMetric(fontName,fontSize)
{
var text = document.createElement("span");
text.style.fontFamily = fontName;
text.style.fontSize = fontSize + "px";
text.innerHTML = "ABCjgq|";
// if you will use some weird fonts, like handwriting or symbols, then you need to edit this test string for chars that will have most extreme accend/descend values
var block = document.createElement("div");
block.style.display = "inline-block";
block.style.width = "1px";
block.style.height = "0px";
var div = document.createElement("div");
div.appendChild(text);
div.appendChild(block);
// this test div must be visible otherwise offsetLeft/offsetTop will return 0
// but still let's try to avoid any potential glitches in various browsers
// by making it's height 0px, and overflow hidden
div.style.height = "0px";
div.style.overflow = "hidden";
// I tried without adding it to body - won't work. So we gotta do this one.
document.body.appendChild(div);
block.style.verticalAlign = "baseline";
var bp = objOff(block);
var tp = objOff(text);
var taccent = bp[1] - tp[1];
block.style.verticalAlign = "bottom";
bp = objOff(block);
tp = objOff(text);
var theight = bp[1] - tp[1];
var tdescent = theight - taccent;
// now take it off :-)
document.body.removeChild(div);
// return text accent, descent and total height
return [taccent,theight,tdescent];
}
I've just tested the code above and works great on latest Chrome, FF and Safari on Mac.
EDIT: I have added font size as well and tested with webfont instead of system font - works awesome.
I solved this problem straitforward - using pixel manipulation.
Here is graphical answer:
Here is code:
function textHeight (text, font) {
var fontDraw = document.createElement("canvas");
var height = 100;
var width = 100;
// here we expect that font size will be less canvas geometry
fontDraw.setAttribute("height", height);
fontDraw.setAttribute("width", width);
var ctx = fontDraw.getContext('2d');
// black is default
ctx.fillRect(0, 0, width, height);
ctx.textBaseline = 'top';
ctx.fillStyle = 'white';
ctx.font = font;
ctx.fillText(text/*'Eg'*/, 0, 0);
var pixels = ctx.getImageData(0, 0, width, height).data;
// row numbers where we first find letter end where it ends
var start = -1;
var end = -1;
for (var row = 0; row < height; row++) {
for (var column = 0; column < width; column++) {
var index = (row * width + column) * 4;
// if pixel is not white (background color)
if (pixels[index] == 0) {
// we havent met white (font color) pixel
// on the row and the letters was detected
if (column == width - 1 && start != -1) {
end = row;
row = height;
break;
}
continue;
}
else {
// we find top of letter
if (start == -1) {
start = row;
}
// ..letters body
break;
}
}
}
/*
document.body.appendChild(fontDraw);
fontDraw.style.pixelLeft = 400;
fontDraw.style.pixelTop = 400;
fontDraw.style.position = "absolute";
*/
return end - start;
}
one line answer
var height = parseInt(ctx.font) * 1.2;
CSS "line-height: normal" is between 1 and 1.2
read here for more info
I'm kind of shocked that there are no correct answers here. There is no need to make an estimate or a guess. Also, the font-size is not the actual size of the bounding box of the font. The font height depends on whether you have ascenders and descenders.
To calculate it, use ctx.measureText() and add together the actualBoundingBoxAscent and the actualBoundingBoxDescent. That'll give you the actual size. You can also add together the font* versions to get the size that is used to calculate things like element height, but isn't strictly the height of the actual used space for the font.
const text = 'Hello World';
const canvas = document.querySelector('canvas');
canvas.width = 500;
canvas.height = 200;
const ctx = canvas.getContext('2d');
const fontSize = 100;
ctx.font = `${fontSize}px Arial, Helvetica, sans-serif`;
// top is critical to the fillText() calculation
// you can use other positions, but you need to adjust the calculation
ctx.textBaseline = 'top';
ctx.textAlign = 'center';
const metrics = ctx.measureText(text);
const width = metrics.width;
const actualHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
// fallback to using fontSize if fontBoundingBoxAscent isn't available, like in Firefox. Should be close enough that you aren't more than a pixel off in most cases.
const fontHeight = (metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent) ?? fontSize;
ctx.fillStyle = '#00F'; // blue
ctx.fillRect((canvas.width / 2) - (width / 2), (canvas.height / 2) - (fontHeight / 2), width, fontHeight);
ctx.fillStyle = '#0F0'; // green
ctx.fillRect((canvas.width / 2) - (width / 2), (canvas.height / 2) - (actualHeight / 2), width, actualHeight);
// canvas.height / 2 - actualHeight / 2 gets you to the top of
// the green box. You have to add actualBoundingBoxAscent to shift
// it just right
ctx.fillStyle = '#F00'; // red
ctx.fillText(text, canvas.width / 2, canvas.height / 2 - actualHeight / 2 + metrics.actualBoundingBoxAscent);
<canvas></canvas>
This is what I did based on some of the other answers here:
function measureText(text, font) {
const span = document.createElement('span');
span.appendChild(document.createTextNode(text));
Object.assign(span.style, {
font: font,
margin: '0',
padding: '0',
border: '0',
whiteSpace: 'nowrap'
});
document.body.appendChild(span);
const {width, height} = span.getBoundingClientRect();
span.remove();
return {width, height};
}
var font = "italic 100px Georgia";
var text = "abc this is a test";
console.log(measureText(text, font));
I'm writing a terminal emulator so I needed to draw rectangles around characters.
var size = 10
var lineHeight = 1.2 // CSS "line-height: normal" is between 1 and 1.2
context.font = size+'px/'+lineHeight+'em monospace'
width = context.measureText('m').width
height = size * lineHeight
Obviously if you want the exact amount of space the character takes up, it won't help. But it'll give you a good approximation for certain uses.
I have implemented a nice library for measuring the exact height and width of text using HTML canvas. This should do what you want.
https://github.com/ChrisBellew/text-measurer.js
Here is a simple function. No library needed.
I wrote this function to get the top and bottom bounds relative to baseline. If textBaseline is set to alphabetic. What it does is it creates another canvas, and then draws there, and then finds the top most and bottom most non blank pixel. And that is the top and bottom bounds. It returns it as relative, so if height is 20px, and there is nothing below the baseline, then the top bound is -20.
You must supply characters to it. Otherwise it will give you 0 height and 0 width, obviously.
Usage:
alert(measureHeight('40px serif', 40, 'rg').height)
Here is the function:
function measureHeight(aFont, aSize, aChars, aOptions={}) {
// if you do pass aOptions.ctx, keep in mind that the ctx properties will be changed and not set back. so you should have a devoted canvas for this
// if you dont pass in a width to aOptions, it will return it to you in the return object
// the returned width is Math.ceil'ed
console.error('aChars: "' + aChars + '"');
var defaultOptions = {
width: undefined, // if you specify a width then i wont have to use measureText to get the width
canAndCtx: undefined, // set it to object {can:,ctx:} // if not provided, i will make one
range: 3
};
aOptions.range = aOptions.range || 3; // multiples the aSize by this much
if (aChars === '') {
// no characters, so obviously everything is 0
return {
relativeBot: 0,
relativeTop: 0,
height: 0,
width: 0
};
// otherwise i will get IndexSizeError: Index or size is negative or greater than the allowed amount error somewhere below
}
// validateOptionsObj(aOptions, defaultOptions); // not needed because all defaults are undefined
var can;
var ctx;
if (!aOptions.canAndCtx) {
can = document.createElement('canvas');;
can.mozOpaque = 'true'; // improved performanceo on firefox i guess
ctx = can.getContext('2d');
// can.style.position = 'absolute';
// can.style.zIndex = 10000;
// can.style.left = 0;
// can.style.top = 0;
// document.body.appendChild(can);
} else {
can = aOptions.canAndCtx.can;
ctx = aOptions.canAndCtx.ctx;
}
var w = aOptions.width;
if (!w) {
ctx.textBaseline = 'alphabetic';
ctx.textAlign = 'left';
ctx.font = aFont;
w = ctx.measureText(aChars).width;
}
w = Math.ceil(w); // needed as i use w in the calc for the loop, it needs to be a whole number
// must set width/height, as it wont paint outside of the bounds
can.width = w;
can.height = aSize * aOptions.range;
ctx.font = aFont; // need to set the .font again, because after changing width/height it makes it forget for some reason
ctx.textBaseline = 'alphabetic';
ctx.textAlign = 'left';
ctx.fillStyle = 'white';
console.log('w:', w);
var avgOfRange = (aOptions.range + 1) / 2;
var yBaseline = Math.ceil(aSize * avgOfRange);
console.log('yBaseline:', yBaseline);
ctx.fillText(aChars, 0, yBaseline);
var yEnd = aSize * aOptions.range;
var data = ctx.getImageData(0, 0, w, yEnd).data;
// console.log('data:', data)
var botBound = -1;
var topBound = -1;
// measureHeightY:
for (y=0; y<=yEnd; y++) {
for (var x = 0; x < w; x += 1) {
var n = 4 * (w * y + x);
var r = data[n];
var g = data[n + 1];
var b = data[n + 2];
// var a = data[n + 3];
if (r+g+b > 0) { // non black px found
if (topBound == -1) {
topBound = y;
}
botBound = y; // break measureHeightY; // dont break measureHeightY ever, keep going, we till yEnd. so we get proper height for strings like "`." or ":" or "!"
break;
}
}
}
return {
relativeBot: botBound - yBaseline, // relative to baseline of 0 // bottom most row having non-black
relativeTop: topBound - yBaseline, // relative to baseline of 0 // top most row having non-black
height: (botBound - topBound) + 1,
width: w// EDIT: comma has been added to fix old broken code.
};
}
relativeBot, relativeTop, and height are the useful things in the return object.
Here is example usage:
<!DOCTYPE html>
<html>
<head>
<title>Page Title</title>
<script>
function measureHeight(aFont, aSize, aChars, aOptions={}) {
// if you do pass aOptions.ctx, keep in mind that the ctx properties will be changed and not set back. so you should have a devoted canvas for this
// if you dont pass in a width to aOptions, it will return it to you in the return object
// the returned width is Math.ceil'ed
console.error('aChars: "' + aChars + '"');
var defaultOptions = {
width: undefined, // if you specify a width then i wont have to use measureText to get the width
canAndCtx: undefined, // set it to object {can:,ctx:} // if not provided, i will make one
range: 3
};
aOptions.range = aOptions.range || 3; // multiples the aSize by this much
if (aChars === '') {
// no characters, so obviously everything is 0
return {
relativeBot: 0,
relativeTop: 0,
height: 0,
width: 0
};
// otherwise i will get IndexSizeError: Index or size is negative or greater than the allowed amount error somewhere below
}
// validateOptionsObj(aOptions, defaultOptions); // not needed because all defaults are undefined
var can;
var ctx;
if (!aOptions.canAndCtx) {
can = document.createElement('canvas');;
can.mozOpaque = 'true'; // improved performanceo on firefox i guess
ctx = can.getContext('2d');
// can.style.position = 'absolute';
// can.style.zIndex = 10000;
// can.style.left = 0;
// can.style.top = 0;
// document.body.appendChild(can);
} else {
can = aOptions.canAndCtx.can;
ctx = aOptions.canAndCtx.ctx;
}
var w = aOptions.width;
if (!w) {
ctx.textBaseline = 'alphabetic';
ctx.textAlign = 'left';
ctx.font = aFont;
w = ctx.measureText(aChars).width;
}
w = Math.ceil(w); // needed as i use w in the calc for the loop, it needs to be a whole number
// must set width/height, as it wont paint outside of the bounds
can.width = w;
can.height = aSize * aOptions.range;
ctx.font = aFont; // need to set the .font again, because after changing width/height it makes it forget for some reason
ctx.textBaseline = 'alphabetic';
ctx.textAlign = 'left';
ctx.fillStyle = 'white';
console.log('w:', w);
var avgOfRange = (aOptions.range + 1) / 2;
var yBaseline = Math.ceil(aSize * avgOfRange);
console.log('yBaseline:', yBaseline);
ctx.fillText(aChars, 0, yBaseline);
var yEnd = aSize * aOptions.range;
var data = ctx.getImageData(0, 0, w, yEnd).data;
// console.log('data:', data)
var botBound = -1;
var topBound = -1;
// measureHeightY:
for (y=0; y<=yEnd; y++) {
for (var x = 0; x < w; x += 1) {
var n = 4 * (w * y + x);
var r = data[n];
var g = data[n + 1];
var b = data[n + 2];
// var a = data[n + 3];
if (r+g+b > 0) { // non black px found
if (topBound == -1) {
topBound = y;
}
botBound = y; // break measureHeightY; // dont break measureHeightY ever, keep going, we till yEnd. so we get proper height for strings like "`." or ":" or "!"
break;
}
}
}
return {
relativeBot: botBound - yBaseline, // relative to baseline of 0 // bottom most row having non-black
relativeTop: topBound - yBaseline, // relative to baseline of 0 // top most row having non-black
height: (botBound - topBound) + 1,
width: w
};
}
</script>
</head>
<body style="background-color:steelblue;">
<input type="button" value="reuse can" onClick="alert(measureHeight('40px serif', 40, 'rg', {canAndCtx:{can:document.getElementById('can'), ctx:document.getElementById('can').getContext('2d')}}).height)">
<input type="button" value="dont reuse can" onClick="alert(measureHeight('40px serif', 40, 'rg').height)">
<canvas id="can"></canvas>
<h1>This is a Heading</h1>
<p>This is a paragraph.</p>
</body>
</html>
The relativeBot and relativeTop are what you see in this image here:
https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Drawing_text
Funny that TextMetrics has width only and no height:
http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#textmetrics
Can you use a Span as on this example?
http://mudcu.be/journal/2011/01/html5-typographic-metrics/#alignFix
First of all, you need to set the height of a font size, and then according to the value of the font height to determine the current height of your text is how much, cross-text lines, of course, the same height of the font need to accumulate, if the text does not exceed the largest text box Height, all show, otherwise, only show the text within the box text. High values need your own definition. The larger the preset height, the greater the height of the text that needs to be displayed and intercepted.
After the effect is processed(solve)
Before the effect is processed(
unsolved)
AutoWrappedText.auto_wrap = function(ctx, text, maxWidth, maxHeight) {
var words = text.split("");
var lines = [];
var currentLine = words[0];
var total_height = 0;
for (var i = 1; i < words.length; i++) {
var word = words[i];
var width = ctx.measureText(currentLine + word).width;
if (width < maxWidth) {
currentLine += word;
} else {
lines.push(currentLine);
currentLine = word;
// TODO dynamically get font size
total_height += 25;
if (total_height >= maxHeight) {
break
}
}
}
if (total_height + 25 < maxHeight) {
lines.push(currentLine);
} else {
lines[lines.length - 1] += "…";
}
return lines;};
I found that JUST FOR ARIAL the simplest, fastest and accuratest way to find height of bounding box is to use the width of certain letters. If you plan to use a certain font without letting user to choose one different, you can do a little research to find the right letter that do the job for that font.
<!DOCTYPE html>
<html>
<body>
<canvas id="myCanvas" width="700" height="200" style="border:1px solid #d3d3d3;">
Your browser does not support the HTML5 canvas tag.</canvas>
<script>
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.font = "100px Arial";
var txt = "Hello guys!"
var Hsup=ctx.measureText("H").width;
var Hbox=ctx.measureText("W").width;
var W=ctx.measureText(txt).width;
var W2=ctx.measureText(txt.substr(0, 9)).width;
ctx.fillText(txt, 10, 100);
ctx.rect(10,100, W, -Hsup);
ctx.rect(10,100+Hbox-Hsup, W2, -Hbox);
ctx.stroke();
</script>
<p><strong>Note:</strong> The canvas tag is not supported in Internet
Explorer 8 and earlier versions.</p>
</body>
</html>
setting the font size might not be practical though, since setting
ctx.font = ''
will use the one defined by CSS as well as any embedded font tags. If you use the CSS font you have no idea what the height is from a programmatic way, using the measureText method, which is very short sighted. On another note though, IE8 DOES return the width and height.
This works 1) for multiline text as well 2) and even in IE9!
<div class="measureText" id="measureText">
</div>
.measureText {
margin: 0;
padding: 0;
border: 0;
font-family: Arial;
position: fixed;
visibility: hidden;
height: auto;
width: auto;
white-space: pre-wrap;
line-height: 100%;
}
function getTextFieldMeasure(fontSize, value) {
const div = document.getElementById("measureText");
// returns wrong result for multiline text with last line empty
let arr = value.split('\n');
if (arr[arr.length-1].length == 0) {
value += '.';
}
div.innerText = value;
div.style['font-size']= fontSize + "px";
let rect = div.getBoundingClientRect();
return {width: rect.width, height: rect.height};
};
I know this is an old answered question, but for future reference I'd like to add a short, minimal, JS-only (no jquery) solution I believe people can benefit from:
var measureTextHeight = function(fontFamily, fontSize)
{
var text = document.createElement('span');
text.style.fontFamily = fontFamily;
text.style.fontSize = fontSize + "px";
text.textContent = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 ";
document.body.appendChild(text);
var result = text.getBoundingClientRect().height;
document.body.removeChild(text);
return result;
};
I monkey patched CanvasRenderingContext2D.measureText() in one of my project to include actual height of the text. It's written in vanilla JS and has zero dependencies.
/*
* Monkeypatch CanvasRenderingContext2D.measureText() to include actual height of the text
*/
; (function (global) {
"use strict";
var _measureText = global.CanvasRenderingContext2D.prototype.measureText;
global.CanvasRenderingContext2D.prototype.measureText = function () {
var textMetrics = _measureText.apply(this, arguments);
var _getHeight = function (text) {
var $span = global.document.createElement("span");
var spanTextNode = global.document.createTextNode(text);
$span.appendChild(spanTextNode);
$span.setAttribute("style", `font: ${this.font}`);
var $div = global.document.createElement("div");
$div.setAttribute("style", "display: inline-block; width: 1px; height: 0; vertical-align: super;");
var $parentDiv = global.document.createElement("div");
$parentDiv.appendChild($span);
$parentDiv.appendChild($div);
var $body = global.document.getElementsByTagName("body")[0];
$body.appendChild($parentDiv);
var divRect = $div.getBoundingClientRect();
var spanRect = $span.getBoundingClientRect();
var result = {};
$div.style.verticalAlign = "baseline";
result.ascent = divRect.top - spanRect.top;
$div.style.verticalAlign = "bottom";
result.height = divRect.top - spanRect.top;
result.descent = result.height - result.ascent;
$body.removeChild($parentDiv);
return result.height - result.descent;
}.bind(this);
var height = _getHeight(arguments[0]);
global.Object.defineProperty(textMetrics, "height", { value: height });
return textMetrics;
};
})(window);
You can use it like this
ctx.font = "bold 64px Verdana, sans-serif"; // Automatically considers it as part of height calculation
var textMetrics = ctx.measureText("Foobar");
var textHeight = textMetrics.height;
parseInt(ctx.font, 10)
e.g.
let text_height = parseInt(ctx.font, 10)
e.g. returns 35
In normal situations the following should work:
var can = CanvasElement.getContext('2d'); //get context
var lineHeight = /[0-9]+(?=pt|px)/.exec(can.font); //get height from font variable
This is madding... The height of the text is the font size.. Didn't any of you read the documentation?
context.font = "22px arial";
this will set the height to 22px.
the only reason there is a..
context.measureText(string).width
is because that the width of the string can not be determined unless it knows the string you want the width of but for all the strings drawn with the font.. the height will be 22px.
if you use another measurement than px then the height will still be the same but with that measurement so at most all you would have to do is convert the measurement.
Approximate solution:
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.font = "100px Arial";
var txt = "Hello guys!"
var wt = ctx.measureText(txt).width;
var height = wt / txt.length;
This will be accurate result in monospaced font.

Canvas borders between rectangles

I have made a QR-code generator.
It takes a string of spaces and '#' to generate an image using the canvas. This string is given to the following function as the text parameter:
export const drawQR = (text) => {
// Set up canvas and go to top left
let canvas = document.getElementById("qr-code-canvas");
let ctx = canvas.getContext("2d");
ctx.lineWidth = "1";
// ctx.moveTo(0,0)
// calculate box height and width
let lines = text.split('\n');
let boxes_h = lines.length;
let chars_first = Array.from(lines[0]);
let boxes_w = chars_first.length;
let width = canvas.clientWidth
let height = canvas.clientHeight;
let box_height = height/boxes_h;
let box_width = width/boxes_w;
// Keep track of current position
let x = 0;
let y = 0;
lines.forEach((line) => {
Array.from(line).forEach((char) => {
if (char == " ") {
ctx.beginPath();
ctx.fillStyle = "white";
ctx.rect(x, y, box_width, box_height);
ctx.fill();
} else if (char == "#") {
ctx.beginPath();
ctx.fillStyle = "black";
ctx.rect(x, y, box_width, box_height);
ctx.fill();
} else {
console.error(`error: unkown character in QR code: ${char}`)
}
x += box_width;
});
x = 0;
y += box_height;
});
}
Now I just draw a white rectangle for each space in the string and a black one for each '#' in the string. A new line goes down one box and starts back at the left again.
The problem I have with this is that there are small borders between the boxes:
I have tried making the boxes bigger:
ctx.rext(x, y, box_width + 1, box_height + 1);
This works pretty well for bigger boxes, but when the boxes get smaller, I start to get artifacts (zoom in on the top left box for example):
Drawing a border for each rectangle has a similar result.
Does anyone know how to fix these issues?
Thank you in advance for your answers,
Jonas
I had a very similar problem. I have solved it by having the box_width and the box_height of the rectangles as integers:
let box_height = Math.floor(height/boxes_h);
let box_width = Math.floor(width/boxes_w);
This happens because HTML5 canvas supports sub-pixel rendering, and it will try to use anti-aliasing to smooth the lines. More info here:
https://web.dev/canvas-performance/#avoid-floating-point-coordinates

CanvasRenderingContext2D putImageData oddity

So i'm adding some image manipulation functions to one of our company projects. Part of the feature is an image cropper with the desire to 'auto-detect' the cropped image to some degree. If our guess is bad they can just drag & drop the cropper points, but most images people should be able to be auto-cropped.
My issue is when i'm putting the data back into the canvas indexes that work don't seem make any sense to me based on the documentation. I'm trying to take the rect I find and convert he canvas to a single image size that will now contain my whole rect.
let width = right - left + 1, height = bottom - top + 1;
canvas.width = width;
canvas.height = height;
ctx.putImageData(imageBuffer, -left, -top, left, top, width,height);
This gives me the correct image. I would have expected based on the documentation that the below code would be correct. I verified in mspaint that my indexes for the rect are correct so I know it isn't my algorithm coming up with weird numbers.
let width = right - left + 1, height = bottom - top + 1;
canvas.width = width;
canvas.height = height;
ctx.putImageData(imageBuffer, 0, 0, left, top, width,height);
Why do you have to put a negative indexing for the 2nd & 3rd argument? I've verified it behaves like this in both Chrome & Firefox.
Yes, it might be a bit confusing, but when you putImageData, the destinationWidth and destinationHeight you would have in e.g drawImage, are always equal to the ImageData's width and height.
The 4 last params of putImageData(), dirtyX, dirtyY, dirtyWidth and dirtyHeight values are relative to the ImageData's boundaries.
So with the first two params, you just set the position of the ImageData's boundaries, with the 4 others, you set the position of your pixels in this ImageData's boundary.
var ctx = canvas.getContext('2d');
var imgBound = {
x: 10,
y: 10,
width: 100,
height: 100
},
innerImg = {
x: 20,
y: 20,
width: 200,
height: 200
};
// a new ImageData, the size of our canvas
var img = ctx.createImageData(imgBound.width, imgBound.height);
// fill it with noise
var d = new Uint32Array(img.data.buffer);
for(var i=0;i<d.length; i++)
d[i] = Math.random() * 0xFFFFFFFF;
function draw() {
ctx.putImageData(img,
imgBound.x,
imgBound.y,
innerImg.x,
innerImg.y,
innerImg.width,
innerImg.height
);
// the ImageData's boundaries
ctx.strokeStyle = 'blue';
ctx.strokeRect(imgBound.x, imgBound.y, imgBound.width, imgBound.height);
// our pixels boundaries relative to the ImageData's bbox
ctx.strokeStyle = 'green';
ctx.strokeRect(
// for stroke() we need to add the ImageData's translation
innerImg.x + imgBound.x,
innerImg.y + imgBound.y,
innerImg.width,
innerImg.height
);
}
var inner_direction = -1,
imgBound_direction = -1;
function anim() {
innerImg.width += inner_direction;
innerImg.height += inner_direction;
if(innerImg.width <= -50 || innerImg.width > 200) inner_direction *= -1;
imgBound.x += imgBound_direction;
if(imgBound.x <= 0 || imgBound.x > 200)
imgBound_direction *= -1;
ctx.clearRect(0,0,canvas.width,canvas.height);
draw();
requestAnimationFrame(anim);
}
anim();
canvas{border: 1px solid;}
<canvas id="canvas" width="300" height="300"></canvas>

Determine width of string in HTML5 canvas

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?
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.
Write All Things You Want For Text. ( Ex. ctx.font )
Before Writing Actual Text Use ctx.measureText("myText").width To get Width Of Text, Because We Have Called It After Making Changes On ctx, It Will Be Different Each Time When We Change ctx Properties (ex. font)
Now We Will Scale It From 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);
Write Text ctx.fillText() or ctx.strokeText()
Reverse All Changes By ctx.save() and ctx.restore() Or Manually like setTransform()
var can = document.getElementById('myCan');
var ctx = can.getContext("2d");
var desiredWidth = can.width;
var myText = "Hello How Are You?"
function draw(){
ctx.font = can.height/2 + "px verdana";
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 How Are You?"/><br>
<canvas id="myCan" height="300" width=300 style="border:black solid 2px;">
</canvas>
</center>
</body>
</html>
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);
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);

Creating a Clickable Grid in a Web Browser

I want to draw a grid of 10 x 10 squares on a HTML5 canvas with number 1-100 displayed on the squares. Clicking a square should call a JavaScript function with the square's number passed as a variable to the function.
First, I encourage you to read this answer to another question involving the HTML5 Canvas. You need to understand that there are no squares. In order to detect a click on a 'square', you would have to keep track of a mapping from each canvas coordinate to the square(s) that it logically contains, handle a single click event on the entire canvas, work out which square(s) you want to change, and then redraw the canvas with the changes you want.
Then—since you seem to have no objection to using a more appropriate technology—I encourage you to do this in either HTML (where each 'square' is something like a <div> that is absolutely-positioned and sized and colored using CSS), or SVG (using <rect> if you need the squares to be able to be rotated, or want to introduce other shapes).
HTML and SVG are both 'retained-mode' graphics mode systems, where drawing a shape 'retains' the concept of that shape. You can move the shape, change its colors, size, etc. and the computer will automatically redraw it for you. Moreover, and more importantly for your use case, you can (with both HTML and SVG):
function changeColor(evt){
var clickedOn = evt.target;
// for HTML
clickedOn.style.backgroundColor = '#f00';
// for SVG
clickedOn.setAttribute('fill','red');
}
mySquare.addEventListener('click',changeColor,false);
Edit: I've created a simple implementation in JavaScript and HTML: http://jsfiddle.net/6qkdP/2/
Here's the core code, in case JSFiddle is down:
function clickableGrid( rows, cols, callback ){
var i=0;
var grid = document.createElement('table');
grid.className = 'grid';
for (var r=0;r<rows;++r){
var tr = grid.appendChild(document.createElement('tr'));
for (var c=0;c<cols;++c){
var cell = tr.appendChild(document.createElement('td'));
cell.innerHTML = ++i;
cell.addEventListener('click',(function(el,r,c,i){
return function(){ callback(el,r,c,i); }
})(cell,r,c,i),false);
}
}
return grid;
}
EDIT: Using HTML elements rather than drawing these things on a canvas or using SVG is another option and quite possibly preferable.
Following up on Phrogz's suggestions, see here for an SVG implementation:
jsfiddle example
document.createSvg = function(tagName) {
var svgNS = "http://www.w3.org/2000/svg";
return this.createElementNS(svgNS, tagName);
};
var numberPerSide = 20;
var size = 10;
var pixelsPerSide = 400;
var grid = function(numberPerSide, size, pixelsPerSide, colors) {
var svg = document.createSvg("svg");
svg.setAttribute("width", pixelsPerSide);
svg.setAttribute("height", pixelsPerSide);
svg.setAttribute("viewBox", [0, 0, numberPerSide * size, numberPerSide * size].join(" "));
for(var i = 0; i < numberPerSide; i++) {
for(var j = 0; j < numberPerSide; j++) {
var color1 = colors[(i+j) % colors.length];
var color2 = colors[(i+j+1) % colors.length];
var g = document.createSvg("g");
g.setAttribute("transform", ["translate(", i*size, ",", j*size, ")"].join(""));
var number = numberPerSide * i + j;
var box = document.createSvg("rect");
box.setAttribute("width", size);
box.setAttribute("height", size);
box.setAttribute("fill", color1);
box.setAttribute("id", "b" + number);
g.appendChild(box);
var text = document.createSvg("text");
text.appendChild(document.createTextNode(i * numberPerSide + j));
text.setAttribute("fill", color2);
text.setAttribute("font-size", 6);
text.setAttribute("x", 0);
text.setAttribute("y", size/2);
text.setAttribute("id", "t" + number);
g.appendChild(text);
svg.appendChild(g);
}
}
svg.addEventListener(
"click",
function(e){
var id = e.target.id;
if(id)
alert(id.substring(1));
},
false);
return svg;
};
var container = document.getElementById("container");
container.appendChild(grid(5, 10, 200, ["red", "white"]));
container.appendChild(grid(3, 10, 200, ["white", "black", "yellow"]));
container.appendChild(grid(7, 10, 200, ["blue", "magenta", "cyan", "cornflowerblue"]));
container.appendChild(grid(2, 8, 200, ["turquoise", "gold"]));
As the accepted answer shows, doing this in HTML/CSS is easiest if this is all your design amounts to, but here's an example using canvas as an alternative for folks whose use case might make more sense in canvas (and to juxtapose against HTML/CSS).
The first step of the problem boils down to figuring out where in the canvas the user's mouse is, and that requires knowing the offset of the canvas element. This is the same as finding the mouse position in an element, so there's really nothing unique to canvas here in this respect. I'm using event.offsetX/Y to do this.
Drawing a grid on canvas amounts to a nested loop for rows and columns. Use a tileSize variable to control the step amount. Basic math lets you figure out which tile (coordinates and/or cell number) your mouse is in based on the width and height and row and column values. Use context.fill... methods to write text and draw squares. I've kept everything 0-indexed for sanity, but you can normalize this as a final step before display (don't mix 1-indexing in your logic, though).
Finally, add event listeners to the canvas element to detect mouse actions which will trigger re-computations of the mouse position and selected tile and re-renders of the canvas. I attached most of the logic to mousemove because it's easier to visualize, but the same code applies to click events if you choose.
Keep in mind that the below approach is not particularly performance-conscious; I only re-render when the cursor moves between cells, but partial re-drawing or moving an overlay to indicate the highlighted element would be faster (if available). There are a lot of micro-optimizations I've ignored. Consider this a proof-of-concept.
const drawGrid = (canvas, ctx, tileSize, highlightNum) => {
for (let y = 0; y < canvas.width / tileSize; y++) {
for (let x = 0; x < canvas.height / tileSize; x++) {
const parity = (x + y) % 2;
const tileNum = x + canvas.width / tileSize * y;
const xx = x * tileSize;
const yy = y * tileSize;
if (tileNum === highlightNum) {
ctx.fillStyle = "#f0f";
}
else {
ctx.fillStyle = parity ? "#555" : "#ddd";
}
ctx.fillRect(xx, yy, tileSize, tileSize);
ctx.fillStyle = parity ? "#fff" : "#000";
ctx.fillText(tileNum, xx, yy);
}
}
};
const size = 10;
const canvas = document.createElement("canvas");
canvas.width = canvas.height = 200;
const ctx = canvas.getContext("2d");
ctx.font = "11px courier";
ctx.textBaseline = "top";
const tileSize = canvas.width / size;
const status = document.createElement("pre");
let lastTile = -1;
drawGrid(canvas, ctx, tileSize);
document.body.style.display = "flex";
document.body.style.alignItems = "flex-start";
document.body.appendChild(canvas);
document.body.appendChild(status);
canvas.addEventListener("mousemove", evt => {
event.target.style.cursor = "pointer";
const tileX = ~~(evt.offsetX / tileSize);
const tileY = ~~(evt.offsetY / tileSize);
const tileNum = tileX + canvas.width / tileSize * tileY;
if (tileNum !== lastTile) {
lastTile = tileNum;
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawGrid(canvas, ctx, tileSize, tileNum);
}
status.innerText = ` mouse coords: {${evt.offsetX}, ${evt.offsetX}}
tile coords : {${tileX}, ${tileY}}
tile number : ${tileNum}`;
});
canvas.addEventListener("click", event => {
status.innerText += "\n [clicked]";
});
canvas.addEventListener("mouseout", event => {
drawGrid(canvas, ctx, tileSize);
status.innerText = "";
lastTile = -1;
});

Categories

Resources