如何将文本调整为html画布上的精确宽度?

问题描述 投票:0回答:2

如何在HTML5画布上将单行文本调整为精确的宽度?到目前为止,我一直尝试以初始字体大小写文本,用measureText(my_text).width测量文本的宽度,然后根据所需的文本宽度和实际的文本宽度之间的比率来计算新的字体大小。它给出的结果大致正确,但是根据文本的不同,边缘会有一些空白。

这里是一些示例代码:

// Draw "guard rails" with 200px space in between
c.fillStyle = "lightgrey";
c.fillRect(90, 0, 10, 200);
c.fillRect(300, 0, 10, 200);

// Measure how wide the text would be with 100px font
var my_text = "AA";
var initial_font_size = 100;
c.font = initial_font_size + "px Arial";
var initial_text_width = c.measureText(my_text).width;

// Calculate the font size to exactly fit the desired width of 200px
var desired_text_width = 200; 
new_font_size = initial_font_size * desired_text_width / initial_text_width;

// Draw the text with the new font size
c.font = new_font_size + "px Arial";
c.fillStyle = "black";
c.textBaseline = "top";
c.fillText(my_text, 100, 0, 500);

此结果对于某些字符串是完美的,例如"AA"

enter image description here

但是对于其他字符串,例如"BB",在边缘处有一个间隙,您会看到文本没有到达“ guardrails”:

enter image description here

我该怎么做,使文本始终到达边缘的右边?

html text html5-canvas measurement
2个回答
0
投票

测量文字宽度

测量文本在许多级别上都是有问题的。

完整且实验性的textMetric已经定义了很多年,但仅在一种主流浏览器(Safari)上可用,隐藏在标志后面(Chrome),由于错误(Firefox)被掩盖,状态未知(Edge,IE )。

仅使用width

充其量您可以使用width返回的对象的ctx.measureText属性来估计宽度。该宽度大于或等于实际像素宽度(最左到最右)。请注意,网络字体必须完全加载,否则宽度可能等于占位符字体的宽度。

蛮力

不幸的是,接缝可靠工作的唯一方法是蛮力技术,该技术将字体呈现为临时/或工作画布,并通过查询像素来计算范围。

这将在支持画布的所有浏览器中都起作用。

不适用于实时动画和应用程序。

以下功能

  • 将返回具有以下属性的对象

    • [width文本的画布像素中的宽度
    • left距画布像素中第一个像素左侧的距离
    • [right从左到画布像素中最后检测到的像素的距离
    • [rightOffset在画布像素中距测量的文本宽度和检测到的右边缘的距离
    • [measuredWidthctx.measureText返回的测量宽度
    • [baseSize字体大小以像素为单位
    • font用于测量文本的字体
  • 如果宽度为零或字符串不包含可见文本,它将返回undefined

然后,您可以使用固定大小的字体和2D变换来缩放文本以适合所需的宽度。这将适用于非常小的字体,从而可以在较小的尺寸下呈现更高质量的字体。

精度取决于要测量的字体的大小。该函数使用120px的固定字体大小,您可以通过传递属性

来设置基本大小。

该功能可以使用部分文本(快捷方式)以减少RAM和处理开销。 rightOffset属性是从ctx.measureText右边缘到具有内容的第一个像素的距离(以像素为单位)。

因此,您可以测量文本"CB"并使用该测量来准确对齐以"C"开头并以"B"结尾的任何文本

如果使用快捷文本,则为示例

    const txtSize = measureText({font: "arial", text: "BB"});
    ctx.font = txtSize.font;
    const width = ctx.measureText("BabcdefghB").width;
    const actualWidth = width - txtSize.left - txtSize.rightOffset;
    const scale = canvas.width / actualWidth;
    ctx.setTransform(scale, 0, 0, scale,  -txtSize.left * scale, 0);
    ctx.fillText("BabcdefghB",0,0);

measureText功能

const measureText = (() => {
    var data, w, size =  120; // for higher accuracy increase this size in pixels.
    const isColumnEmpty = x => {
       var idx = x, h = size * 2;
       while (h--) {
           if (data[idx]) { return false }
           idx += can.width;
       }
       return true;
    }
    const can = document.createElement("canvas");
    const ctx = can.getContext("2d");
    return ({text, font, baseSize = size}) => {   
        size = baseSize;
        can.height = size * 2;
        font = size + "px "+ font;          
        if (text.trim() === "") { return }
        ctx.font = font;
        can.width = (w = ctx.measureText(text).width) + 8;
        ctx.font = font;
        ctx.textBaseline = "middle";
        ctx.textAlign = "left";
        ctx.fillText(text, 0, size);
        data = new Uint32Array(ctx.getImageData(0, 0, can.width, can.height).data.buffer);
        var left, right;
        var lIdx = 0, rIdx = can.width - 1;
        while(lIdx < rIdx) {
            if (left === undefined && !isColumnEmpty(lIdx)) { left = lIdx }
            if (right === undefined && !isColumnEmpty(rIdx)) { right = rIdx }
            if (right !== undefined && left !== undefined) { break }
            lIdx += 1;
            rIdx -= 1;
        }
        data = undefined; // release RAM held
        can.width = 1; // release RAM held
        return right - left >= 1 ? {
            left, right, rightOffset: w - right,  width: right - left, 
            measuredWidth: w, font, baseSize} : undefined;
    }   
})();

用法示例

该示例使用上面的功能,并且通过仅提供第一个和最后一个非空白字符来简化测量。

在文本输入中输入文本。

  • 如果文本太大而无法容纳画布,则控制台将显示警告。
  • 如果文本比例大于1(表示显示的字体大于所测量的字体),控制台将显示警告,因为可能会降低对齐精度。
inText.addEventListener("input", updateCanvasText);
const ctx = canvas.getContext("2d");
canvas.height = canvas.width = 500;

function updateCanvasText() {
    const text = inText.value.trim(); 
    const shortText = text[0] + text[text.length - 1];
    const txtSize = measureText({font: "arial", text: text.length > 1 ? shortText: text});
    if(txtSize) {
        ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height)
        ctx.font = txtSize.font;
        const width = ctx.measureText(text).width;
        const actualWidth = width - txtSize.left - txtSize.rightOffset;
        const scale =  (canvas.width - 20) / actualWidth;
        console.clear();
        if(txtSize.baseSize * scale > canvas.height) {
            console.log("Font scale too large to fit vertically");
        } else if(scale > 1) {
            console.log("Scaled > 1, can result in loss of precision ");
        }
        ctx.textBaseline = "top";
        ctx.fillStyle = "#000";
        ctx.textAlign = "left";
        ctx.setTransform(scale, 0, 0, scale, 10 - txtSize.left * scale, 0);
        ctx.fillText(text,0,0);
        ctx.setTransform(1, 0, 0, 1, 0, 0);
        ctx.fillStyle = "#CCC8";
        ctx.fillRect(0, 0, 10, canvas.height);
        ctx.fillRect(canvas.width - 10, 0, 10, canvas.height);
    } else {
        console.clear();
        console.log("Empty string ignored");
    }
}
const measureText = (() => {
    var data, w, size =  120;
    const isColumnEmpty = x => {
       var idx = x, h = size * 2;
       while (h--) {
           if (data[idx]) { return false }
           idx += can.width;
       }
       return true;
    }
    const can = document.createElement("canvas");
    const ctx = can.getContext("2d");
    return ({text, font, baseSize = size}) => {   
        size = baseSize;
        can.height = size * 2;
        font = size + "px "+ font;          
        if (text.trim() === "") { return }
        ctx.font = font;
        can.width = (w = ctx.measureText(text).width) + 8;
        ctx.font = font;
        ctx.textBaseline = "middle";
        ctx.textAlign = "left";
        ctx.fillText(text, 0, size);
        data = new Uint32Array(ctx.getImageData(0, 0, can.width, can.height).data.buffer);
        var left, right;
        var lIdx = 0, rIdx = can.width - 1;
        while(lIdx < rIdx) {
            if (left === undefined && !isColumnEmpty(lIdx)) { left = lIdx }
            if (right === undefined && !isColumnEmpty(rIdx)) { right = rIdx }
            if (right !== undefined && left !== undefined) { break }
            lIdx += 1;
            rIdx -= 1;
        }
        data = undefined; // release RAM held
        can.width = 1; // release RAM held
        return right - left >= 1 ? {left, right, rightOffset: w - right, width: right - left, measuredWidth: w, font, baseSize} : undefined;
    }   
})();
body {
  font-family: arial;
}
canvas {
   border: 1px solid black;
   width: 500px;
   height: 500px;   
}
<label for="inText">Enter text </label><input type="text" id="inText" placeholder="Enter text..."/>
<canvas id="canvas"></canvas>

[Note

装饰字体可能不起作用,您可能需要在功能measureText]中扩展画布的高度。

您面临的问题是TextMetrics.width代表文本的“ 前进宽度

”。This answer很好地解释了它的含义,并链接到good resources

前进宽度是字形的初始笔位置与下一个字形的初始笔位置之间的距离。

您想要的是bounding-box

宽度,要获得此宽度,您需要计算TextMetric.actualBoundingBoxLeft + TextMetric.actualBoundingBoxLeft的总和。还请注意,在渲染文本时,必须考虑边界框的TextMetric.actualBoundingBoxRight偏移量才能使其正确适合。

不幸的是,所有浏览器都不支持扩展的TextMetrics对象,对于这些浏览器,我们很不幸。

TextMetric.actualBoundingBoxRight
actualBoundingBoxLeft

0
投票

您面临的问题是TextMetrics.width代表文本的“ 前进宽度

© www.soinside.com 2019 - 2024. All rights reserved.