等宽字符的精确宽度
Precise width of a monospace character
我正在开发一个带有语法高亮显示的代码编辑器,我需要知道等宽字符的精确宽度。我使用这个值来计算一个字符在一行中的位置,我需要知道这个位置以便我可以放置各种 GUI 元素,例如文本光标(可以有多个)、选择矩形、警告工具提示等。到现在为止我一直在使用以下功能:
function getCharacterWidth(char, fontFamily, fontSize) {
let span = document.createElement("span");
span.style.fontFamily = fontFamily;
span.style.fontSize = fontSize;
span.style.position = "absolute";
span.style.visibility = "hidden";
span.style.width = "auto";
span.style.whiteSpace = "nowrap";
span.style.padding = "0";
span.style.margin = "0";
span.style.letterSpacing = "0px";
span.style.wordSpacing = "0px";
span.innerText = char;
document.body.appendChild(span);
let width = span.getBoundingClientRect().width;
span.remove();
return width;
}
它一直运行良好,但后来我注意到 Google Chrome 上有问题。当我的文本编辑器渲染包含数千个字符的大行时,由于舍入问题,字符位置无法正确计算。似乎在 Google Chrome 上,getBoundingClientRect()
返回的 width
的精度最多为小数点后 5 位,这对于我的用例来说并不理想。在 Firefox 上,精度似乎高得多,最高可达小数点后 15 位,这就是为什么我在那里从未遇到过这个问题。
经过一番挖掘,我听说了根据包含数千个字符的跨度宽度来计算字符宽度的想法 ()。因此,在我原来的函数中,我用 span.innerText = char.repeat(10000)
替换了 span.innerText = char
并返回了 width / 10000
。它有所帮助,但当我处理大行时,计算仍然明显不对。
所以我来了。如何在其他浏览器中像 Firefox 一样高精度地计算字符的宽度?
这不是一个干净的解决方案,但我怀疑没有真正干净的解决方案。
您可以保留一个“列映射”,基于您已经掌握的语法格式<span>
。说你的荧光笔给你:
<div class="line">
var long_line_of_variables = <span class="number">123</span>;
</div>
你可以测量这个跨度的左偏移+它出现的列(=它之前兄弟姐妹的textContent的长度= 29)并到达
colums = {
'29': 290.456
}
现在您可以插入第 14 列在 290.456*14/29=140.22
像素处。
我们添加的跨度越多,我们就能猜得越好:
colums = {
'29': 290.456,
'2900': 28997.000 // whoops, not what we would have calculated!
}
此方法是启发式的,因此您需要找到一种策略,该策略适用于各种浏览器、zoom/font-scale 设置等,包括
- 向这个“地图”添加越来越多的跨度
- 但也许偶尔“清理”一次它?
- 保留全球地图或每行一个?
- 智能插值:选择最近的地图条目(条目之间的最佳间隔)
- 添加更多“跨度探针”
- 将没有任何格式的长行拆分为 N 个字符的块,将它们包装在跨度中,或者在中间插入空跨度
- 也许只是每行末尾的一个探测跨度?
在研究过类似的问题和启发式方法后,我的建议是:不要。 :)
它涉及大量的调整和测试,并且可能是跨平台的噩梦(例如比较 Win 上 Firefox 与 Linux 与 MacOS 与 iOS 的字体渲染和舍入)。相反,尝试将 anything 附加到本地化范围。我知道这可能更难——很多文本编辑器都在为排长队而苦苦挣扎,尤其是。当涉及到 MB 大小的编译 JS 时...
我提出了一个解决方案,该解决方案在我的用例 中运行良好。这个想法是根据我的编辑器的最大行来计算字符的宽度。我的线条元素看起来像这样:
<div class="line"><span class="keyword">let</span><span class="space"> </span><span class="identifier">foo</span></div>
如果我们想根据那条线计算一个字符的宽度,我们会这样做:
let lineElement = document.querySelector(".line");
let lineWidth = lineElement.getBoundingClientRect().width;
let charWidth = lineWidth / lineElement.textContent.length;
我会跟踪我在编辑器中呈现的最大行。每当我呈现新的最大行时,我都会根据该新行更新 charWidth
。
这使我可以通过简单的乘法 column * charWidth
来计算一行中任何给定列中字符的位置。到目前为止,它一直运行良好,即使对于包含 100000 多个字符的行也是如此。
但是,我还必须做最后一件事来处理更大的行。 Google Chrome 上似乎有一个错误,当您尝试呈现带有巨大文本节点的行时,例如<span>many many characters...</span>
,它不会为您提供元素的正确宽度(参见 Inaccurate width of large element on Chrome)。为了克服这个问题,每当我必须渲染一个巨大的文本时,我将它分成多个跨度,例如<span>many many </span><span>characters...</span>
.
现在我可以毫无问题地渲染 500000 个字符的长行,字体大小为 48。除此之外,事情又开始变得奇怪了,出现计算错误和其他奇怪的浏览器行为。所以我决定设置一行中呈现 500000 个字符的硬性限制。超过 500000n 个字符的所有内容都对用户隐藏。
我正在开发一个带有语法高亮显示的代码编辑器,我需要知道等宽字符的精确宽度。我使用这个值来计算一个字符在一行中的位置,我需要知道这个位置以便我可以放置各种 GUI 元素,例如文本光标(可以有多个)、选择矩形、警告工具提示等。到现在为止我一直在使用以下功能:
function getCharacterWidth(char, fontFamily, fontSize) {
let span = document.createElement("span");
span.style.fontFamily = fontFamily;
span.style.fontSize = fontSize;
span.style.position = "absolute";
span.style.visibility = "hidden";
span.style.width = "auto";
span.style.whiteSpace = "nowrap";
span.style.padding = "0";
span.style.margin = "0";
span.style.letterSpacing = "0px";
span.style.wordSpacing = "0px";
span.innerText = char;
document.body.appendChild(span);
let width = span.getBoundingClientRect().width;
span.remove();
return width;
}
它一直运行良好,但后来我注意到 Google Chrome 上有问题。当我的文本编辑器渲染包含数千个字符的大行时,由于舍入问题,字符位置无法正确计算。似乎在 Google Chrome 上,getBoundingClientRect()
返回的 width
的精度最多为小数点后 5 位,这对于我的用例来说并不理想。在 Firefox 上,精度似乎高得多,最高可达小数点后 15 位,这就是为什么我在那里从未遇到过这个问题。
经过一番挖掘,我听说了根据包含数千个字符的跨度宽度来计算字符宽度的想法 ()。因此,在我原来的函数中,我用 span.innerText = char.repeat(10000)
替换了 span.innerText = char
并返回了 width / 10000
。它有所帮助,但当我处理大行时,计算仍然明显不对。
所以我来了。如何在其他浏览器中像 Firefox 一样高精度地计算字符的宽度?
这不是一个干净的解决方案,但我怀疑没有真正干净的解决方案。
您可以保留一个“列映射”,基于您已经掌握的语法格式<span>
。说你的荧光笔给你:
<div class="line">
var long_line_of_variables = <span class="number">123</span>;
</div>
你可以测量这个跨度的左偏移+它出现的列(=它之前兄弟姐妹的textContent的长度= 29)并到达
colums = {
'29': 290.456
}
现在您可以插入第 14 列在 290.456*14/29=140.22
像素处。
我们添加的跨度越多,我们就能猜得越好:
colums = {
'29': 290.456,
'2900': 28997.000 // whoops, not what we would have calculated!
}
此方法是启发式的,因此您需要找到一种策略,该策略适用于各种浏览器、zoom/font-scale 设置等,包括
- 向这个“地图”添加越来越多的跨度
- 但也许偶尔“清理”一次它?
- 保留全球地图或每行一个?
- 智能插值:选择最近的地图条目(条目之间的最佳间隔)
- 添加更多“跨度探针”
- 将没有任何格式的长行拆分为 N 个字符的块,将它们包装在跨度中,或者在中间插入空跨度
- 也许只是每行末尾的一个探测跨度?
在研究过类似的问题和启发式方法后,我的建议是:不要。 :)
它涉及大量的调整和测试,并且可能是跨平台的噩梦(例如比较 Win 上 Firefox 与 Linux 与 MacOS 与 iOS 的字体渲染和舍入)。相反,尝试将 anything 附加到本地化范围。我知道这可能更难——很多文本编辑器都在为排长队而苦苦挣扎,尤其是。当涉及到 MB 大小的编译 JS 时...
我提出了一个解决方案,该解决方案在我的用例 中运行良好。这个想法是根据我的编辑器的最大行来计算字符的宽度。我的线条元素看起来像这样:
<div class="line"><span class="keyword">let</span><span class="space"> </span><span class="identifier">foo</span></div>
如果我们想根据那条线计算一个字符的宽度,我们会这样做:
let lineElement = document.querySelector(".line");
let lineWidth = lineElement.getBoundingClientRect().width;
let charWidth = lineWidth / lineElement.textContent.length;
我会跟踪我在编辑器中呈现的最大行。每当我呈现新的最大行时,我都会根据该新行更新 charWidth
。
这使我可以通过简单的乘法 column * charWidth
来计算一行中任何给定列中字符的位置。到目前为止,它一直运行良好,即使对于包含 100000 多个字符的行也是如此。
但是,我还必须做最后一件事来处理更大的行。 Google Chrome 上似乎有一个错误,当您尝试呈现带有巨大文本节点的行时,例如<span>many many characters...</span>
,它不会为您提供元素的正确宽度(参见 Inaccurate width of large element on Chrome)。为了克服这个问题,每当我必须渲染一个巨大的文本时,我将它分成多个跨度,例如<span>many many </span><span>characters...</span>
.
现在我可以毫无问题地渲染 500000 个字符的长行,字体大小为 48。除此之外,事情又开始变得奇怪了,出现计算错误和其他奇怪的浏览器行为。所以我决定设置一行中呈现 500000 个字符的硬性限制。超过 500000n 个字符的所有内容都对用户隐藏。