Why the text in canvas can't be centered vertically

Viewed 112
    CanvasRenderingContext2D.prototype.roundRect = function (
        x,
        y,
        width,
        height,
        radius
    ) {
        if (width < 2 * radius) radius = width / 2;
        if (height < 2 * radius) radius = height / 2;
        this.beginPath();
        this.moveTo(x + radius, y);
        this.arcTo(x + width, y, x + width, y + height, radius);
        this.arcTo(x + width, y + height, x, y + height, radius);
        this.arcTo(x, y + height, x, y, radius);
        this.arcTo(x, y, x + width, y, radius);
        this.closePath();
        return this;
    };

let generateCanvas = function (config) {
        var tmpCanvas = document.createElement("canvas");
        var tmpContext = tmpCanvas.getContext("2d");
        tmpContext.font = tmpContext.font.replace(/\d+px/, config.fontSize);
        var textWidth = tmpContext.measureText(config.text).width || 10;
        tmpCanvas = null;
        var textCanvas = document.createElement("canvas");
        var textContext = textCanvas.getContext("2d");
        textCanvas.height = config.height;
        textCanvas.width = textWidth;
        textCanvas.width = textWidth + 2 * config.padding;
        textContext.lineWidth = config.lineWidth;
        textContext.strokeStyle = config.lineColor;
        textContext.fillStyle = config.backgroundColor;
        textContext.roundRect(
                0,
                0,
                textCanvas.width,
                textCanvas.height,
                config.rounded
            );
            textContext.stroke();
            textContext.fill();
        textContext.fillStyle = config.color;
        textContext.font = textContext.font.replace(/\d+px/, config.fontSize);
        textContext.textAlign = config.textAlign;
        textContext.textBaseline = config.textBaseline;
        textContext.fillText(
            config.text,
            textCanvas.width / 2,
            config.height / 2
        );
        return textCanvas;
    };

The config I passed to the generateCanvas function is:

{
                            text: "测试名称测试名称测试名称测试名称",
                            height: 40,
                            fontSize: "24px",
                            textAlign: "center",
                            textBaseline: "middle",
                            color: "#F9A403",
                            backgroundColor: "white",
                            lineColor: "transparent",
                            rounded: 25,
                            padding: 10,
                        }

Why is the text in the image generated by the above function not centered vertically? roundRect is a custom function that implements drawing a rectangle with rounded corners using canvas.Please help me if there are any gods to look at this problem.

1 Answers

The "middle" value of textBaseline is based on the em-square. This means it will work well for latin glyphs, but not so well for ideographic glyphs, which would need an ideographic centered baseline value, that the Canvas API doesn't have, yet.

Here is a graphic representation from the specs:

The top of the em square is roughly at the top of the glyphs in a font, the hanging baseline is where some glyphs like आ are anchored, the middle is half-way between the top of the em square and the bottom of the em square, the alphabetic baseline is where characters like Á, ÿ, f, and Ω are anchored, the ideographic-under baseline is where glyphs like 私 and 達 are anchored, and the bottom of the em square is roughly at the bottom of the glyphs in a font. The top and bottom of the bounding box can be far from these baselines, due to glyphs extending far outside the em square.

But all hope is not lost, during the last years, all browsers have made some progress in their implementations of the TextMetrics interface, and we finally have access to enough information from all major browsers to measure the height of the text being measured.
This means that we can implement a truly centered vertical-align, based on the glyphs being rendered:

const text_input = document.querySelector("input[type='text']");
const padding_input = document.querySelector("input[type='range']");
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");;
const font = "60px sans-serif";
let padding_x = 10;
let padding_y = 10;

text_input.oninput = padding_input.oninput =
  (evt) => drawText(text_input.value);

drawText(text_input.value);

// returns an easier to use BBox like object
// from a TextMetrics object
function getTextBBox( ctx, text ) {
  const metrics = ctx.measureText( text );
  const left = metrics.actualBoundingBoxLeft * -1;
  const top = metrics.actualBoundingBoxAscent * -1;
  const right = metrics.actualBoundingBoxRight;
  const bottom = metrics.actualBoundingBoxDescent;
  const width = right - left;
  const height = bottom - top;
  return { left, top, right, bottom, width, height };
}

function drawText( text ) {
  // we set only the font
  // other values like textBaseline and textAlign
  // are left to their default
  ctx.font = font;
  const bbox = getTextBBox(ctx, text);
  const padding = +padding_input.value;
  canvas.width = bbox.width + padding;
  canvas.height = bbox.height + padding;
  ctx.font = font;
  ctx.fillStyle = "#F9A403";

  const middle_x = bbox.left + (bbox.width / 2);
  const middle_y = bbox.top + (bbox.height / 2);
  const left = canvas.width / 2 - middle_x;
  const top = canvas.height / 2 - middle_y;

  ctx.fillText(text, left, top);

  // draw the middle line for reference  
  ctx.fillStyle = "red";
  ctx.fillRect(0, canvas.height/2-1, canvas.width, 2);
}
canvas { border: 1px solid; }
<label>Text content: <input type="text" value="测试名称测试名称测试名称测试名称"></label><br>
<label>Padding: <input type="range" min="0" max="100"></label><br>
<canvas></canvas>

Related