1

So far, to do the centering, I am using the following two lines of code:

ctx.textAlign="center"; 
ctx.textBaseline = "middle";

This almost does the job, but some characters like "g" and "y" are not completely centered. How can I make sure that all of them are supported? Here is a JSbin that shows that the majority of characters like "g" is below the center line.

Expectation: enter image description here

Reality: enter image description here

To make my "expectation" work, I subtract 15px from the y value of the letter, but this messes up small letters like "a" and makes them go outside of the bounds on the top.

shurup
  • 751
  • 10
  • 33
  • looks as expected. When you need the upper and lower boundries of glyphs to be the same, you have to locate them individualy by yourself (not as a whole string). – Steven Jan 12 '18 at 03:35
  • I am trying to add support in my application for different languages as well so doing each character individually is highly impractical for me. @Steven – shurup Jan 12 '18 at 03:47
  • 1
    P.S. Your problem will be more descriptive, when you post a screenshot from what you have and an image (made with Paint/Photoshop/Gimp/etc.) of what you expect. – Steven Jan 12 '18 at 03:57
  • Does this answer your question? [Center (proportional font) text in an HTML5 canvas](https://stackoverflow.com/questions/13771310/center-proportional-font-text-in-an-html5-canvas) – rofrol Jun 09 '22 at 11:02

1 Answers1

1

Measuring text.

One way is to render the character and then scan the pixels to find the extent, top, bottom, left, and right, to find the real center of the character.

This is an expensive process so added to that you would store the results of previous measurements in a map and return those results for the same character and font.

The example below creates the object charSizer. You set a font charSizer.font = "28px A font" then you can get the information regarding any character. charSizer.measure(char) which returns an object containing information regarding the characters dimensions.

You can measure characters in production and serve the information to the page to reduce client side processing but you will need to target each browser as they all render text differently.

Example

The example has instructions. The left canvas show char render to normal center using ctx.textAlign = "center" and ctx.textBaseline = "middle". Also included are color codded lines to show extent, center, bounds center, and weighted center. The middle canvas draw the char in circle using bounds center and the right canvas uses weighted center.

This is an example only, untested and not up to production quality.

const charSizer = (() => {
  const known = new Map();

  var w,h,wc,hc;
  
  const workCan = document.createElement("canvas");
  const ctx = workCan.getContext("2d");
  var currentFont;
  var fontHeight = 0;
  var fontId = "";
  function resizeCanvas(){
      wc = (w = workCan.width = fontHeight * 2.5 | 0) / 2;
      hc = (h = workCan.height = fontHeight * 2.5 | 0) / 2;
      ctx.font = currentFont;
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";
      ctx.fillStyle = "black";
  }
  function measure(char){
      const info = {
         char,
         width : ctx.measureText(char).width,
         top : null,
         left : w,
         right : 0,
         bottom : 0,
         weightCenter : { x : 0, y : 0 },
         center : { x : 0, y : 0 },
         offset : { x : 0, y : 0 },
         wOffset : { x : 0, y : 0 },
         area : 0,
         width : 0,
         height : 0,
            
      }
      ctx.clearRect(0,0,w,h);
      ctx.fillText(char,wc,hc);
      const pixels8 = ctx.getImageData(0,0,w,h).data;
      const pixels = new Uint32Array(pixels8.buffer);
      var x,y,i;
      i = 0;
      for(y = 0; y < h; y ++){
        for(x = 0; x < w; x ++){
           const pix = pixels[i++];
           if(pix){
               const alpha = pixels8[(i<<2)+3];
               info.bottom = y;
               info.right = Math.max(info.right, x);
               info.left = Math.min(info.left, x);
               info.top = info.top === null ? y : info.top;
               info.area += alpha;
               info.weightCenter.x += (x - wc) * (alpha/255);
               info.weightCenter.y += (y - hc) * (alpha/255);
           }
        }
      }
      if(info.area === 0){
         return {empty : true};
      }
      info.area /= 255;      
      info.weightCenter.x /= info.area;
      info.weightCenter.y /= info.area;

      info.height = info.bottom - info.top + 1;
      info.width = info.right - info.left + 1;
      info.center.x = info.left + info.width / 2;
      info.center.y = info.top + info.height / 2;
      info.offset.x = wc - info.center.x;
      info.offset.y = hc - info.center.y;
      info.wOffset.x = -info.weightCenter.x;
      info.wOffset.y = -info.weightCenter.y;
      info.top -= hc;
      info.bottom -= hc;
      info.left -= wc;
      info.right -= wc;
      info.center.x -= wc;
      info.center.y -= hc;
  
      
      return info;
   }


  


  const API = {
    set font(font){
        currentFont = font;
        fontHeight = Number(font.split("px")[0]);
        resizeCanvas();
        fontId = font;
    },
    measure(char){
        var info = known.get(char + fontId);
        if(info) { return {...info} } // copy so it is save from change
        info = measure(char);
        known.set(char + fontId,info);
        return info;
    }
  }
  return API;
})()





  







  //==============================================================================
//==============================================================================
// Demo code from here down not part of answer code.

const size = 160;
const sizeh = 80;
const fontSize = 120;
function line(x,y,w,h){
  ctx.fillRect(x,y,w,h);
}
function hLine(y){ line(0,y,size,1) }
function vLine(x){ line(x,0,1,size) }
function circle(ctx,col = "red",x= sizeh,y = sizeh,r = sizeh*0.8,lineWidth = 2) {
   ctx.lineWidth = lineWidth;
   ctx.strokeStyle = col;
   ctx.beginPath();
   ctx.arc(x,y,r,0,Math.PI * 2);
   ctx.stroke();
}



const ctx = canvas.getContext("2d");
const ctx1 = canvas1.getContext("2d");
const ctx2 = canvas2.getContext("2d");
canvas.width = size;
canvas.height = size;
canvas1.width = size;
canvas1.height = size;
canvas2.width = size;
canvas2.height = size;
canvas.addEventListener("click",nextChar);
canvas1.addEventListener("click",nextFont);
ctx.font = "20px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("Click this canvas", sizeh,sizeh-30);
ctx.fillText("cycle", sizeh,sizeh);
ctx.fillText("characters", sizeh,sizeh + 30);

ctx1.font = "20px Arial";
ctx1.textAlign = "center";
ctx1.textBaseline = "middle";
ctx1.fillText("Click this canvas", sizeh,sizeh - 30);
ctx1.fillText("cycle", sizeh,sizeh);
ctx1.fillText("fonts", sizeh,sizeh + 30);


charSizer.font = "128px Arial";
ctx1.textAlign = "center";
ctx1.textBaseline = "middle";
ctx2.textAlign = "center";
ctx2.textBaseline = "middle";
const chars = "\"ABCDQWZ{@pqjgw|/*";
const fonts = [            
            fontSize+"px Arial",
            fontSize+"px Arial Black",
            fontSize+"px Georgia",
            fontSize+"px Impact, Brush Script MT",
            fontSize+"px Rockwell Extra Bold",
            fontSize+"px Franklin Gothic Medium",
            fontSize+"px Brush Script MT",
            fontSize+"px Comic Sans MS",
            fontSize+"px Impact",
            fontSize+"px Lucida Sans Unicode",
            fontSize+"px Tahoma",
            fontSize+"px Trebuchet MS",
            fontSize+"px Verdana",
            fontSize+"px Courier New",
            fontSize+"px Lucida Console",
            fontSize+"px Georgia",
            fontSize+"px Times New Roman",
            fontSize+"px Webdings",
            fontSize+"px Symbol",];
var currentChar = 0;
var currentFont = 0;
var firstClick = true;
function nextChar(){
   if(firstClick){
      setCurrentFont();
      firstClick = false;
   }
   ctx.clearRect(0,0,size,size);
   ctx1.clearRect(0,0,size,size);   
   ctx2.clearRect(0,0,size,size);    
   var c = chars[(currentChar++) % chars.length];
   var info = charSizer.measure(c);

   if(!info.empty){
      ctx.fillStyle = "red";
      hLine(sizeh + info.top);
      hLine(sizeh + info.bottom);
      vLine(sizeh + info.left);
      vLine(sizeh + info.right);
      ctx.fillStyle = "black";
      hLine(sizeh);
      vLine(sizeh);
      ctx.fillStyle = "red";
      hLine(sizeh + info.center.y);
      vLine(sizeh + info.center.x);
      ctx.fillStyle = "blue";
      hLine(sizeh + info.weightCenter.y);
      vLine(sizeh + info.weightCenter.x);
    
    
      ctx.fillStyle = "black";
      circle(ctx,"black");
      ctx.fillText(c,sizeh,sizeh);
      ctx1.fillStyle = "black";
      circle(ctx1);
      ctx1.fillText(c,sizeh + info.offset.x,sizeh+ info.offset.y);
      ctx2.fillStyle = "black";
      circle(ctx2,"blue");
      ctx2.fillText(c,sizeh + info.wOffset.x, sizeh + info.wOffset.y);
   }


}
function setCurrentFont(){
fontUsed.textContent = fonts[currentFont % fonts.length];
   charSizer.font = fonts[currentFont % fonts.length];
   ctx.font = fonts[currentFont % fonts.length];
   ctx2.font = fonts[currentFont % fonts.length];
   ctx1.font = fonts[(currentFont ++) % fonts.length];
}

function nextFont(){
   setCurrentFont();
   currentChar = 0;
   nextChar();
}
canvas { border : 2px solid black; }
.red {color :red;}
.blue {color :blue;}
<canvas id="canvas"></canvas><canvas id="canvas1"></canvas><canvas id="canvas2"></canvas><br>
Font <span id="fontUsed">not set</span> [center,middle] <span class=red>[Spacial center]</span> <span class=blue> [Weighted center]</span><br>
Click left canvas cycles char, click center to cycle font. Not not all browsers support all fonts
Community
  • 1
  • 1
Blindman67
  • 51,134
  • 11
  • 73
  • 136