到目前为止,要进行居中,我正在使用以下两行代码:

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


这几乎可以完成工作,但是某些字符,例如“ g”和“ y”并没有完全居中。如何确保所有这些功能均受支持? Here是一个JSbin,它显示大多数字符(例如“ g”)在中心线下方。

期望:
javascript - 如何在圆形html Canvas 中居中放置字母?-LMLPHP

现实:javascript - 如何在圆形html Canvas 中居中放置字母?-LMLPHP

为了使我的“期望”起作用,我从字母的y值中减去了15px,但这会使诸如“ a”的小字母弄乱,并使它们超出顶部的界限。

最佳答案

测量文字。

一种方法是渲染角色,然后扫描像素以找到范围,顶部,底部,左侧和右侧,以找到角色的真实中心。

这是一个非常昂贵的过程,因此您需要将以前的测量结果存储在地图中,并以相同的字符和字体返回这些结果。

下面的示例创建对象charSizer。设置字体charSizer.font = "28px A font"即可获取有关任何字符的信息。 charSizer.measure(char)返回一个包含有关字符尺寸信息的对象。

您可以测量生产中的字符并将信息提供给页面以减少客户端处理,但是您将需要针对每个浏览器,因为它们都以不同的方式呈现文本。



该示例包含说明。左画布显示使用ctx.textAlign = "center"ctx.textBaseline = "middle"将char渲染到正常中心。还包括彩色拼合线,以显示范围,中心,边界中心和加权中心。中间的画布使用边界中心画圆,而右画布使用加权中心。

这仅是示例,未经测试且未达到生产质量。



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

09-19 09:35