我有多目录文本,我想要标记或选择一些字符(例如最后一个字符)
示例字符串:"معنی Hello سلام است یا Salam"
一些字符从左侧扩展文本,一些字符从右侧扩展文本。(您可以在编辑器中复制文本并使用 BackSpace 逐个删除字符以便看得更清楚)
我想看看这个画布中“H”字符的位置在哪里或者用户点击了哪个字符......?
canvas.measureText(txt.slice(0,dest)).width
不工作。因为我不知道我的字符是从右边还是左边长出来的?
我检查画布文本功能,然后用谷歌搜索它,什么也看不到!
不幸的是,Canvas2D API 仍然缺少适当的 API 来正确处理文本。平心而论,多年来人们一直致力于各种事情,但文本非常复杂。
所以目前,没有任何东西可以为您提供每个字形级别的信息,
measureText()
只需要一堆文本,不会暴露每个字形。
如果您可以访问 CSS 渲染器(例如,因为您的脚本在浏览器中运行),您可以做的是使用 CSS 渲染您的文本,然后使用 DOM
Range
API,这足以公开信息以至少获得每个字符的边界框(细节少于画布TextMetrics
,但对于您的情况应该足够了)。
这是一个怪物,它从我以前的一个非画布中心问题的答案中删除了一些代码。请注意,这不是像素完美的,它混合了字符和字形,这是一个问题,尤其是阿拉伯文字。
const text = "معنی Hello سلام است یا Salam";
const x = 10;
const y = 50;
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
ctx.font = "30px sans-serif";
ctx.textBaseLine = "alphabetic";
ctx.strokeStyle = "red";
// See below for the helper functions
// Get the list of all chars and their BBox, sorted rtl-ttb
const chars = getDisplayedChars(text, ctx.font);
// Get the BBox of the whole text (fails in Firefox with this text...)
const wholeMetrics = getCanvasTextBox(ctx, text);
// Compute the relative boxes, in canvas.
const relativeChars = chars.map(({ text, rect }) => {
const left = rect.left + wholeMetrics.left + x;
const top = rect.top + wholeMetrics.top + y;
return new DOMRect(left, top, rect.width, rect.height);
});
let hovered = null;
draw();
canvas.onmousemove = ({ clientX, clientY }) => {
const { left, top } = canvas.getBoundingClientRect();
const x = clientX - left;
const y = clientY - top;
const lastHovered = hovered;
hovered = findCharAt(x, y);
if (lastHovered !== hovered) {
draw();
}
};
function findCharAt(x, y) {
return relativeChars.find(({ top, right, bottom, left }) => {
return left <= x && right >= x &&
top <= y && bottom >= y;
});
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillText(text, x, y);
if (hovered) {
ctx.strokeRect(hovered.left, hovered.top, hovered.width, hovered.height);
}
}
function getDisplayedChars(text, fontStyle) {
// Won't work in StackSnippet, but using an iframe allows to not be polluted by CSS
const frame = document.createElement("iframe");
document.body.append(frame);
const doc = frame.contentDocument || document;
const container = doc.createElement("div");
container.style.font = fontStyle;
container.textContent = text;
doc.body.prepend(container);
const range = doc.createRange(); // to get our nodes positions
// First we get the full text BBox, so we can later have relative positions
range.selectNode(container);
const mainBBox = range.getBoundingClientRect();
const chars = [];
const node = container.firstChild;
node.data.split("").forEach((char, index) => {
range.setStart(node, index); // Move the range to this character
range.setEnd(node, index + 1);
const nodeBBox = range.getBoundingClientRect();
const rect = getRelativeBBox(mainBBox, nodeBBox);
chars.push({ char, rect });
});
container.remove();
frame.remove();
return chars.filter((char) => char.rect.height) // Keep only the displayed ones
.sort((a, b) => { // Sort ttb-ltr
return a.rect.top - b.rect.top ||
a.rect.left - b.rect.left;
})
}
function getRelativeBBox(from, to) {
const x = to.left - from.left;
const y = to.top - from.top;
return new DOMRect(x, y, to.width, to.height);
}
// Returns a BBox based on a TextMetrics
// relative to {0,0}
function getCanvasTextBox(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 new DOMRect(left, top, width, height);
}
<canvas width=500 height=80></canvas><br>
Hover over each character to show its bounding-box.