【问题标题】:Letter spacing in canvas element画布元素中的字母间距
【发布时间】:2012-02-15 16:44:04
【问题描述】:

这个问题说明了一切。我一直在寻找并开始担心这是不可能的。

我有这个画布元素,我正在向其中绘制文本。我想设置类似于 CSS letter-spacing 属性的字母间距。我的意思是在绘制字符串时增加字母之间的像素数量。

我绘制文本的代码是这样的,ctx 是画布上下文变量。

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

我尝试在绘图前添加ctx.letterSpacing = "2px";,但无济于事。有没有办法通过简单的设置来做到这一点,还是我必须制作一个函数来单独绘制每个字符并考虑间距?

【问题讨论】:

标签: javascript css html canvas letter-spacing


【解决方案1】:

这可能是一个老问题,但它仍然相关。我接受了 Patrick Matte 对 James Carlyle-Clarke 响应的扩展,并得到了一些我认为效果很好的东西,而且仍然是普通的旧 JavaScript。这个想法是测量两个连续字符之间的空间并“添加”到它。是的,负数有效。

这就是我得到的(大量评论的版本):

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
}

这是一个演示:

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>

【讨论】:

    【解决方案2】:

    这是基于 James Carlyle-Clarke 先前回答的另一种方法。它还可以让您将文本左、中和右对齐。

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

    【讨论】:

      【解决方案3】:

      我不了解其他人,但我通过增加我正在编写的文本的 y 值来调整行间距。我实际上是用空格分割一个字符串,并在循环内将每个单词踢下一行。我使用的数字基于默认字体。如果您使用不同的字体,这些数字可能需要调整。

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

      【讨论】:

        【解决方案4】:

        支持画布中的字母间距,我使用了这个

        canvas = document.getElementById('canvas');
        canvas.style.letterSpacing = '2px';
        

        【讨论】:

        • 对我来说这似乎没有任何作用。见这里:jsfiddle.net/o1n5014u/55
        • @JennyO'Reilly 它有效。只需输入一个不同的值,多​​于或少于 '5px'。
        • 你说得对,它在 Chrome 中有效,但在任何其他浏览器中无效。
        • 目前仅适用于 Chrome(可能还有其他基于 Blink 的浏览器)。
        • 我正在使用 chrome,是的,这实际上完美地解决了我的问题,很抱歉你的降旗,我赞成你,因为即使这看起来如此远离解决方案它是完美的,除非你想要不同的字母画布环境中不同文本的空间
        【解决方案5】:

        其实字母间距概念画布是不支持的。

        所以我使用 javascript 来做到这一点。

        var value = $('#sourceText1').val().split("").join(" ");

        var sample_text = "Praveen Chelumalla";
        var text = sample_text.split("").join(" ");

        【讨论】:

          【解决方案6】:

          我不确定它是否应该工作(根据规格),但在某些浏览器 (Chrome) 中,您可以在 &lt;canvas&gt; 元素本身上设置 letter-spacing CSS 属性,然后将应用于在上下文中绘制的所有文本。 (适用于 Chrome v56,不适用于 Firefox v51 或 IE v11。)

          请注意,在 Chrome v56 中,您必须在每次更改为 letter-spacing 样式后重新获取画布 2d 上下文(并重新设置您关心的任何值);间距似乎已融入您获得的 2d 上下文中。

          示例: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>

          原始的跨浏览器答案:

          这是不可能的; HTML5 Canvas 不具备 HTML 中 CSS 的所有文本转换功能。我建议您应该为每种用途结合适当的技术。使用与 Canvas 甚至 SVG 分层的 HTML,每个都尽其所能。

          另请注意,鉴于存在字母字距调整对和像素对齐字体提示,“滚动你自己的”(使用自定义偏移量绘制每个字符)会对大多数字体产生不良结果。

          【讨论】:

          • 这并不是说您可以自己滚动并完全完成,手动输入字距调整对的图表。我发现执行Math.sqrt(width_of_character + width_of_previous_char, 3/4) 会产生对于大多数用途而言相当准确的间距。当然,字符的宽度可以通过context.measureText(myString[26]).width找到。 26 在这种情况下是字符串的第 26 个字母,所以应该循环遍历
          • Funkodebat // 哎呀... thx... html5 中没有调整画布文本空间的选项太糟糕了。
          • @Funkodebat 您将不得不做的不仅仅是 3/4,您还必须手动调整很多对,例如 WAII
          • gist.github.com/danschumann/cb832f6341aa697e58cb 有一个我正在做的例子..(只有第一种方法)。是的,字母需要手动调整...可能有更好的方法,例如打印字母表中2个字母的每个组合并测量宽度等。
          • 很好的答案。作为侧指针(技巧),对画布的引用已经存在于上下文对象之前,即。 ctx.canvas 所以ctx.canvas.style.letterSpacing 可以工作。
          【解决方案7】:

          我用:

          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
          

          【讨论】:

            【解决方案8】:

            不正确。您可以在 css 中为 canvas 元素添加 letter-spacing 属性,它可以完美运行。无需复杂的解决方法。我现在才在我的画布项目中弄清楚了。 IE。: 帆布 { 宽度:480px; 高度:350px; 边距:30px 自动 0; 填充:15px 0 0 0; 背景:粉红色; 显示:块; 边框:2px 虚线棕色; 字母间距:0.1em; }

            【讨论】:

              【解决方案9】:

              为了允许“字母字距调整对”等,我编写了以下内容。它应该考虑到这一点,粗略的测试表明确实如此。如果你有任何关于它的 cmets,那么我会向你指出我关于这个主题的问题 (Adding Letter Spacing in HTML Canvas)

              基本上它使用 measureText() 来获取整个字符串的宽度,然后删除字符串的第一个字符并测量剩余字符串的宽度,并使用差异来计算正确的定位 - 从而考虑到字距对等。有关更多伪代码,请参见给定链接。

              这是 HTML:

              <canvas id="Test1" width="800px" height="200px"><p>Your browser does not support canvas.</p></canvas>
              

              代码如下:

              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 != "");
              }
              

              演示/眼球测试代码:

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

              理想情况下,我会向它抛出多个随机字符串,然后逐个像素地进行比较。我也不确定 Verdana 的默认字距调整有多好,尽管我知道它比 Arial 更好 - 其他字体的建议值得尝试接受。

              所以...到目前为止看起来不错。事实上,它看起来很完美。 仍然希望有人指出过程中的任何缺陷。

              与此同时,我会将其放在这里以供其他人查看他们是否正在寻找解决方案。

              【讨论】:

                【解决方案10】:

                这里有一些咖啡脚本,可让您像这样根据上下文设置字距调整

                tctx = tcanv.getContext('2d')
                tctx.kerning = 10
                tctx.fillStyle = 'black'
                tctx.fillText 'Hello World!', 10, 10
                

                支持代码为:

                _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
                

                JavaScript 将是

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

                【讨论】:

                • 非常好的解决方案。只是说它不处理理由,即'中心'或'右'。
                • @JonSmith 虽然它有点脆弱(有些字母有点不同),但如果您需要比这更好的解决方案,gist.github.com/danschumann/cb832f6341aa697e58cb 有一些逐个字母间距的东西。该要点实际上编写了旋转的文本,但在这个字距调整示例中也存在同样的问题。一些 measureText(..).width 的东西不是很准确,需要纠正。
                • 感谢您的链接。我现在已经实现了一个跟踪解决方案,正如您所指出的,它对于旋转字符等也很有用。我不确定您所说的 measureText(..).width stuff isn't super accurate 是什么意思。我在实现tracking 时发现 measureText 没问题。注意:您的代码所做的并不是真正的字距调整,大约是两个字母字距对,而是“跟踪”,或者至少是 adobe 所说的。
                【解决方案11】:

                您无法设置字母间距属性,但您可以通过在字符串中的每个字母之间插入各种空白之一来在画布中实现更宽的字母间距。比如

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

                这将在每个字母之间插入一个hair space

                使用 8201(而不是 8202)将插入稍宽的 thin space

                有关更多空白选项,请参阅this list of Unicode Spaces

                与手动定位每个字母相比,此方法将帮助您更轻松地保留字体的字距调整,但是您无法通过这种方式收紧字母间距。

                【讨论】:

                • 我喜欢这个解决方法。不幸的是,Chrome 似乎平等地呈现所有 Unicode 空间。
                • 天才解决方案,我做了非常复杂的事情,多次重绘文本并取字符宽度的平均值来计算字距调整大小,然后我阅读了这篇文章,它也能正常工作并且对性能的影响为零。谢谢!
                【解决方案12】:

                不能将letter-spacing设置为Canvas上下文的属性。只能通过手动间距来达到效果,抱歉。 (例如,手动绘制每个字母,将每个字母上的 x 增加一些像素量)

                作为记录,您可以使用ctx.font 设置一些文本属性,但字母间距不是其中之一。您可以设置的有:“font-style font-variant font-weight font-size/line-height font-family”

                例如,从技术上讲,您可以编写 ctx.font = "bold normal normal 12px/normal Verdana"(或其中任何一个省略),它会正确解析。

                【讨论】:

                  猜你喜欢
                  • 2012-03-20
                  • 2011-10-20
                  • 2013-04-07
                  • 2012-08-02
                  • 2021-10-16
                  • 2013-10-19
                  • 2012-10-25
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多