Letter spacing in canvas element
Asked Answered
S

13

53

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?

Severn answered 21/1, 2012 at 12:19 Comment(2)
I couldn't find a simple way to do this, although it seems there may be some dark magic involved to make it possible: gaboosh.com/dmi/type1/letter_negative_space_word/index.htmlRural
That is some dark magic indeed, but I think I'll go for the use HTML to draw the text route.Severn
S
27

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.

Sech answered 21/1, 2012 at 19:7 Comment(7)
that's not to say you can roll your own and do it fully, manually inputting a chart of the kerning pairs. I've found that doing Math.sqrt(width_of_character + width_of_previous_char, 3/4) produces spacing that is fairly accurate for most purposes. Of course, width of characters can be found by doing context.measureText(myString[26]).width. 26 in this case is the 26th letter of the string, so that should be looped throughFolio
Funkodebat // gee... thx... it is too bad that there is no options to adjust canvas text spaces in html5.Gurevich
@Funkodebat you'll have to do more than just 3/4 though, you'll have to manually kern a lot of pairs like WA and II.Sash
gist.github.com/danschumann/cb832f6341aa697e58cb There's an example there of what I'm doing.. (only the first method). Yes letters need manual adjusting... There might be a better way to do it, such as printing every combination of 2 letters of the alphabet and measuring width, etc.Folio
Great answer. As a side pointer (trick), a reference to the canvas already exists front the context object ie. ctx.canvas so ctx.canvas.style.letterSpacing will work.Valais
Why is the kerning off a little when I loop through letters and add each letter with fillText using letterSpacing?Moue
sadly it not works off-screen (when you use document.createElement('canvas');)Edik
P
57

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.

Packton answered 20/2, 2013 at 22:46 Comment(2)
I like the workaround. Unfortunately Chrome seems to render all Unicode spaces equally.Maurita
Genius solution, I was doing really complex stuff with redrawing the text multuple times and taking averages of character widths to calculate kerning sizes, then I read this and it works just as well and has zero performance impact. Thanks!Antenatal
S
27

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.

Sech answered 21/1, 2012 at 19:7 Comment(7)
that's not to say you can roll your own and do it fully, manually inputting a chart of the kerning pairs. I've found that doing Math.sqrt(width_of_character + width_of_previous_char, 3/4) produces spacing that is fairly accurate for most purposes. Of course, width of characters can be found by doing context.measureText(myString[26]).width. 26 in this case is the 26th letter of the string, so that should be looped throughFolio
Funkodebat // gee... thx... it is too bad that there is no options to adjust canvas text spaces in html5.Gurevich
@Funkodebat you'll have to do more than just 3/4 though, you'll have to manually kern a lot of pairs like WA and II.Sash
gist.github.com/danschumann/cb832f6341aa697e58cb There's an example there of what I'm doing.. (only the first method). Yes letters need manual adjusting... There might be a better way to do it, such as printing every combination of 2 letters of the alphabet and measuring width, etc.Folio
Great answer. As a side pointer (trick), a reference to the canvas already exists front the context object ie. ctx.canvas so ctx.canvas.style.letterSpacing will work.Valais
Why is the kerning off a little when I loop through letters and add each letter with fillText using letterSpacing?Moue
sadly it not works off-screen (when you use document.createElement('canvas');)Edik
I
9

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.

Imperceptive answered 21/1, 2012 at 19:5 Comment(0)
R
8

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.

Rubadub answered 3/12, 2015 at 4:51 Comment(0)
F
3

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;
  });
};
Folio answered 19/3, 2013 at 19:44 Comment(3)
Really nice solution. Just to say it does not handle justify, i.e. 'center' or 'right'.Langur
@JonSmith while it's kind of fragile ( some letters are kind of different ), if you need a better solution than this, gist.github.com/danschumann/cb832f6341aa697e58cb has some case by case letter spacing stuff. That gist actually writes rotated text, but the problem exists the same in this kerning example. some measureText(..).width stuff isn't super accurate and needs to be corrected.Folio
Thanks for the link. I have now implemented a solution for tracking which, as you point out, is also useful for rotate char etc. I'm not sure what you mean by measureText(..).width stuff isn't super accurate. I have found measureText ok when implementing tracking. Note: what your code does isn't really kerning, which is about two letter kern-pairs, but is 'tracking', or at least that is what adobe calls it.Langur
S
3

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

Stairway answered 7/11, 2016 at 16:31 Comment(0)
D
1

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>
Distemper answered 30/12, 2021 at 4:21 Comment(0)
A
0

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(" ");
Azeria answered 7/9, 2017 at 6:51 Comment(0)
C
0

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;
}               
Cierracig answered 2/5, 2019 at 2:2 Comment(0)
B
0

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;
    }
}
Baerl answered 23/3, 2021 at 20:21 Comment(0)
S
0

in chrome, edge and firefox:

ctx.letterSpacing = "2px"

safari does not support

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

Sawyere answered 27/12, 2023 at 13:58 Comment(0)
A
-1

Letter spacing in canvas IS SUPPORTED, I used this

canvas = document.getElementById('canvas');
canvas.style.letterSpacing = '2px';
Attenweiler answered 1/11, 2017 at 9:12 Comment(5)
For me this does not seem to do anything. See here: jsfiddle.net/o1n5014u/55Kenti
@JennyO'Reilly It works. Just put a different value, more or less than '5px'.Attenweiler
You're right, it works in Chrome, but not in any other browser.Kenti
This only works in Chrome (and perhaps other Blink-based browsers) at the moment.Stanfordstang
I am using chrome and yes this actually solved my issue perfectly, sorry about tall your down flags, I upvoted you because even though this seem like it so way off solution it's perfect, unless you want different letter space for different pieces of text in your canvas envSphinx
G
-4

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
Galinagalindo answered 25/5, 2016 at 0:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.