5

I've read a lot of StackOverflow answers and other pages talking about how to do letter spacing in Canvas. One of the more useful ones was Letter spacing in canvas element

As that other question said, '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.' Note that letter spacing is sometimes, and incorrectly, referred to as kerning.

I notice that the general approach seems to be to output the string on a letter by letter basis, using measureText(letter) to get the letter's width and then adding additional spacing. The problem with this is it doesn't take into account letter kerning pairs and the like. See the above link for an example of this and related comments.

Seems to me that the way to do it, for a line spacing of 'spacing', would be to do something like:

  1. Start at position (X, Y).
  2. Measure wAll, the width of the entire string using measureText()
  3. Remove the first character from the string
  4. Print the first character at position (X, Y) using fillText()
  5. Measure wShorter, the width of the resulting shorter string using measureText().
  6. Subtract the width of the shorter string from the width of the entire string, giving the kerned width of the character, wChar = wAll - wShorter
  7. Increment X by wChar + spacing
  8. wAll = wShorter
  9. Repeat from step 3

Would this not take into account kerning? Am I missing something? Does measureText() add a load of padding that varies depending on the outermost character, or something, and if it does, would not fillText() use the same system to output the character, negating that issue? Someone in the link above mentioned 'pixel-aligned font hinting' but I don't see how that applies here. Can anyone advise either generally or specifically if this will work or if there are problems with it?

EDIT: This is not a duplicate of the other question - which it links to and refers to. The question is NOT about how to do 'letter spacing in canvas', per the proposed duplicate; this is proposing a possible solution (which as far as I know was not suggested by anyone else) to that and other questions, and asking if anyone can see or knows of any issues with that proposed solution - i.e. it's asking about the proposed solution and its points, including details of measureText(), fillText() and 'pixel-aligned font hinting'.

James Carlyle-Clarke
  • 830
  • 1
  • 12
  • 19
  • I have done some preliminary testing and it looks like it works... but I have not done exhaustive testing. – James Carlyle-Clarke Nov 27 '15 at 10:54
  • 1
    **Your question is unclear about what are you trying to achieve?** If you want to exactly center a letter over an [x,y] you can do this: `context.textAlign='center'` to horizontally center over X, `context.textBaseline='middle'` to vertically center over Y, `context.fillText('A',x,y)` to draw A centered at [x,y]. – markE Nov 27 '15 at 16:05
  • Please share some code... – Bob van Luijt Nov 27 '15 at 23:06
  • @markE, I hope that is clearer now. Apologies I accidentally marked your comment as useful when it did not relate to what I was after but your comment about it being unclear at least caused me to clarify it, so thanks. – James Carlyle-Clarke Nov 28 '15 at 02:56
  • @bvl - the reason I gave pseudocode was __because__ I didn't have code, and I was hoping not to write it unless it was going to be useful. Still, while waiting for answers I went ahead and wrote the code, which you can see in the answer below. – James Carlyle-Clarke Nov 28 '15 at 02:59
  • Possible duplicate of [Letter spacing in canvas element](https://stackoverflow.com/questions/8952909/letter-spacing-in-canvas-element) – Krisztián Balla Sep 06 '18 at 12:19

2 Answers2

4

Well, I've written the code, based on the pseudocode above, and done a few comparisons by screenshotting and eyeballing it for differences (zoomed, using straight lines from eg clip boxes to compare X position and width for each character). Looks exactly the same for me, with spacing set at 0.

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.

James Carlyle-Clarke
  • 830
  • 1
  • 12
  • 19
  • It should be noted that while this is not exactly an answer to my question, it __is__ an answer to a __lot__ of questions about how to do this, and it's there for that purpose. If someone answers my question explaining why it's not a good system then that answer will obviously supersede my answer. – James Carlyle-Clarke Nov 28 '15 at 03:04
  • A drawback to this solution is that it has O(n^2) complexity, as measureText loops over the remaining characters in the string, and you're calling it once for each character. It seems it will be easy to convert it to only take in account the current and the next character, that will turn it into an O(n) algorithm. Other than that, very nice solution! – Joaquin Cuenca Abela Jan 19 '16 at 10:33
  • @JoaquinCuencaAbela - I agree that it's probably overkill for the reason you give. I left it like that as I was unsure if there was some kind of 'magic sauce' added depending on the entire string (as opposed to pairs of characters), but I suspect your addition would be the exact same result, and if not, at least three characters in a row or entire words would probably suffice. Tests by anyone welcomed - please post back. – James Carlyle-Clarke Nov 02 '17 at 15:57
  • Two points - 1. This solution is excellent for most of the Latin scripts (English, French, etc), but will give incorrect results for characters from non-latin scripts like Brahmic, Arabic, Hebrew, etc. 2. This solution can be modified a bit to give correct results with emoji. – Nirmit Dalal Oct 21 '20 at 17:50
  • Hi @NirmitDalal, could you detail the modification you suggest for Emojis? – James Carlyle-Clarke Oct 23 '20 at 09:54
0

My answer got deleted. So, I'm using chrome and here is my complete code.

second_image = $('#block_id').first();
canvas = document.getElementById('canvas');
canvas.style.letterSpacing = '2px';
ctx = canvas.getContext('2d');
canvas.crossOrigin = "Anonymous";
canvasDraw = function(text, font_size, font_style, fill_or_stroke){
    canvas.width = second_image.width();
    canvas.height = second_image.height();
    ctx.clearRect(0,0,canvas.width,canvas.height);
    ctx.drawImage(second_image.get(0), 0, 0, canvas.width, canvas.height);
    //refill text
    ctx.font = font_size +'px '+ font_style + ',Symbola';
    $test = ctx.font;
    ctx.textAlign = "center";
    if(fill_or_stroke){
        ctx.fillStyle = "#d2b76d";
        ctx.strokeStyle = "#9d8a5e";
        ctx.strokeText(text,canvas.width*$left,canvas.height*$top);
        ctx.fillText(text,canvas.width*$left,canvas.height*$top);
    }
    else{
        ctx.strokeStyle = "#888888";
        ctx.strokeText(text,canvas.width*$left,canvas.height*$top);
    }
};

And you don't need to use this function this.fillTextWithSpacing. I didn't use and it worked like a charm)