2

What I am trying to do is display multiple lines of text in the middle of a canvas element. The text is dynamic as it is inputted by the user using a text box, the canvas is then updated with the text the user has typed (code shown below). I would like the text to appear on the canvas in a similar fashion to the way that the text would appear using vertical-align: middle property in CSS. My question is what would be the easiest way to approach this problem.

A big issue that I am having is that the user can change the font. As different fonts are different heights (even if they are defined at a px height they are not consistently that height). The best idea I have had so far is to calculate the height of the text on the canvas. I read this article on the site How can you find the height of text on an HTML canvas?, see the second answer by Daniel. This should calculate the actual height of the text which can then be used to calculate the correct starting position of the text to center it on the canvas. Based on my code below I believe I would have to essentially run a similar code to predetermine the correct starting position of the font.

This is my approach to properly wrap and display the text on the canvas:

    function wrapText(context, text, x, y, maxWidth, lineHeight) {
    //manage carriage return
    text = text.replace(/(\r\n|\n\r|\r|\n)/g, "\n");
    //manage tabulation
    text = text.replace(/(\t)/g, "    "); // I use 4 spaces for tabulation, but you can use anything you want
    //array of lines
    var sections = text.split("\n"); 

     for (s = 0, len = sections.length; s < len; s++) {
          var words = sections[s].split(' ');
          var line = '';

          for (var n = 0; n < words.length; n++) {
              var testLine = line + words[n] + ' ';
              var metrics = context.measureText(testLine);
              var testWidth = metrics.width;
              if (testWidth > maxWidth) {
                  context.fillText(line, x, y);
                  line = words[n] + ' ';
                  y += lineHeight;
              } else {
                  line = testLine;
              }
          }
          context.fillText(line, x, y);

         //new line for new section of the text
         y += lineHeight;
      }
}

      var canvas = document.getElementById('myCanvas');
      var context = canvas.getContext('2d');
      var maxWidth = 350;
      var lineHeight = 25;
      var x = (canvas.width - maxWidth) / 2;
      var y = 60;
      var text = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. \nLorem Ipsum has been the industry's standard dummy text ever since the 1500s.";

      context.font = '14pt Verdana';
      context.fillStyle = '#000';

      wrapText(context, text, x, y, maxWidth, lineHeight); 

I am wondering if there is another approach that I have not considered that would simplify the problem or whether this approach is the best one to take? Is is a simpler way on a canvas element to vertical-align text similar to CSS?

Community
  • 1
  • 1
grapien
  • 313
  • 3
  • 10

3 Answers3

8

for setting vertical align text in canvas use textBaseline. For example:

ctx.beginPath();
ctx.font = "10px arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle"; // set text in center of place vertically
ctx.fillText("sample text", 100, 100);
ctx.closePath();
Arnab
  • 4,216
  • 2
  • 28
  • 50
mohammad feiz
  • 308
  • 3
  • 4
  • For more info see e.g. [mozilla docs](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D) – djvg Nov 27 '18 at 10:16
3

Cool!

I looked at your reference: How can you find the height of text on an HTML canvas?

I set up a time test for Prestaul's answer that actually uses canvas to pixel-check for lower and upper bounds of text. In my test, I used all uppercase and lowercase letters (a-z and A-Z) instead of his little/big "gM". I also used a canvas created in JS that I didn't add to the DOM.

Results: I was able to repeatedly run the test 900 +/- times per second.

I didn't run Daniel's test that manipulates a DOM element for measuring, but I assume that would be slower than Prestaul's 1+ millisecond(!) result.

My conclusion is that I would use Prestaul's method to test for max-height whenever the user changes font.

On a personal note, hacking the DOM may work, but to me it feels like using a black box that may someday contain a bomb after a browser update.

Thanks for exercising my curiosity--I enjoyed it!

[Edited to include code for stress test]

Here is code for the stress test and a Fiddle: http://jsfiddle.net/m1erickson/ttsjq/

<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>

<style>
    body{ background-color: ivory; padding:50px; }
</style>

<script>
$(function(){

    var canvas=document.createElement("canvas");
    canvas.width=1000;
    canvas.height=50;
    var ctx=canvas.getContext("2d");

    function measureTextHeight(left, top, width, height,text) {

        // Draw the text in the specified area
        ctx.save();
        ctx.clearRect(0,0,canvas.width,canvas.height);
        ctx.fillText(text, 0, 35); // 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;
    }

    ctx.font='32px Arial';
    var start = new Date().getTime();
    var text="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    for(var x=0;x<1000;x++){
      var height = measureTextHeight(0, 0, canvas.width, canvas.height,text);
    }
    console.log(measureTextHeight(0, 0, canvas.width, canvas.height,text));
    console.log((new Date().getTime()-start));



}); // end $(function(){});
</script>

</head>

<body>
    <div>Stress testing Prestaul's measureTextHeight function...</div>
</body>
</html>
Community
  • 1
  • 1
markE
  • 102,905
  • 11
  • 164
  • 176
  • Thanks for the insight MarkE! I will definitely use Prestaul's method when creating vertical align function on the canvas. If nobody posts any insight on how to do it, I am going to proceed with this method and I will post my solution for everyone to see :) – grapien Mar 23 '13 at 14:39
  • I edited my answer to include my stress test code...hope this helps! – markE Mar 23 '13 at 15:33
  • One thing I noticed about your code above, is that I think you need to expand the canvas size beyond 50,50 as this small rectangle will not capture a - Z string that you have provide it. if I were to expand the area to the function to match the canvas it will change the height of the text! Other then that the function works great. – grapien Mar 23 '13 at 16:17
  • Oops...thanks...edited! After rerunning and getting new times, it now takes just less than 2ms to calculate text height--still pretty fast. – markE Mar 23 '13 at 16:22
2
ctx.textAlign = "center";
ctx.textBaseline = "middle"; 
const fix = ctx.measureText("H").actualBoundingBoxDescent / 2; // Notice Here
ctx.fillText("H", convasWidth / 2, canvaseHeight / 2 + fix);

See codepen demo

  • ``` const fix = ctx.measureText("H").fontBoundingBoxDescent; ctx.fillText("H", width / 2, height - fix); ``` worked better for me – Vinujan.S Dec 17 '20 at 06:03