Letter spacing in canvas element

50.1k views Asked by At

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?

13

There are 13 answers

7
Phrogz On BEST ANSWER

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.

0
Praveen Chelumalla On

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(" ");
0
Simon Sarris On

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.

2
Yanofsky On

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.

0
Patrick Matte On

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;
    }
}
0
James Carlyle-Clarke On

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.

0
Johanna Walter On

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>

5
Artyom Hachikov On

Letter spacing in canvas IS SUPPORTED, I used this

canvas = document.getElementById('canvas');
canvas.style.letterSpacing = '2px';
0
Maria Campbell On

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

0
o17t H1H' S'k On

in chrome, edge and firefox:

ctx.letterSpacing = "2px"

safari does not support

https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/letterSpacing

0
LaFeteParFete On

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
3
dansch On

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;
  });
};
0
modnarrandom On

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