在PIXI.js中绘制按宽度截断的字符串

在紧凑的游戏画面中文字的绘制空间经常是有限的。对于较长的字符串通常会采用截断(truncate)的方式进行处理,即只显示头部的若干个字符,并在最后加上省略号表示还有更长的内容无法显示。该效果可以很简单的通过CSS样式text-overflow: ellipsis在网页中实现,但在基于Canvas和WebGL的绘制中,没有提供对应的默认处理方法,PIXI.js的Text类也没有提供类似的功能。

本文提供了一种在PIXI.js中字符串截断处理的绘制方法,该方法使用TextMetrics类动态计算文字尺寸,并根据文本宽度得到截断后的字符串。

最终效果如图所示:

效果图

1. 拆分字符并计算字符宽度

因为TextMetrics类默认并不支持计算单行文本中每个*字符*的宽度,因此需要将文本中的每个*字符*作为单独的一行进行计算。

对于英语或其他以*单词*为组织单位的语言,可以将每个*单词*作为一行进行计算

在判断文本是否超长时需要考虑省略号的宽度,我们也计算了单个省略号的尺寸。

代码如下:

const chars = text.split('');
const metrics = TextMetrics.measureText(`${ELLIPSIS}\n${chars.join('\n')}`, style);
const [ellipsisWidth, ...charWidths] = (metrics as any).lineWidths as number[];

TextMetrics.measureText()函数返回一个TextMetrics对象,该对象的lineWidths属性保存了传入文本中每一行的宽度。本例中传入字符串的第一行是省略号,之后每一行对应原文本中的每个字符,因此我们将省略号长度赋值给ellipsisWidth变量,将其余行宽(每个字符的宽度)保存到数组charWidths中。

TextMetrics类本身并没有暴露lineWidths属性(应该是bug),所以在用TypeScript时为了通过类型检查,必须将metrics对象先转为any再访问lineWidths属性。

2. 遍历字符长度数组并构建新字符串

在这一步中,我们要通过Array.prototype.reduce()函数来实现对字符串的递进处理。大致思路如下:

  1. 构建一个对象,保存文本总宽度width、结果字符串str和文本宽度是否溢出的标记overflow
  2. 遍历charWidths数组中的每个字符长度,检查已构建的文本宽度加上当前字符宽度和省略号后,是否超过最大宽度
  3. 若没有超出最大宽度,则将当前字符添加到结果字符串中,并更新文本总宽度,并继续执行步骤2
  4. 若超出最大宽度,则不再更新结果字符串,并将溢出标记overflow标记为true

实现代码如下:

const { str: truncated, overflow } = charWidths.reduce((data, w, i) => {
  if (data.width + w + ellipsisWidth >= maxWidth) {
    return { ...data, width: maxWidth, overflow: true };
  }
  return {
    str: data.str + chars[i],
    width: data.width + w,
    overflow: false,
  };
}, { str: '', width: 0, overflow: false });

3. 输出结果字符串

这一步很简单,即将截断后的字符串truncated返回给调用者。需要注意的是省略号仅应在文本宽度溢出时才添加。

return truncated + (overflow ? ELLIPSIS : '');

完整代码片段

import {TextMetrics, TextStyle} from 'pixi.js';

const ELLIPSIS = '…';

export function truncWithEllipsis(text: string, style: TextStyle, maxWidth: number) {
    const chars = text.split('');
    const metrics = TextMetrics.measureText(`${ELLIPSIS}\n${chars.join('\n')}`, style);
    const [ellipsisWidth, ...charWidths] = (metrics as any).lineWidths as number[];
    const { str: truncated, overflow } = charWidths.reduce((data, w, i) => {
        if (data.width + w + ellipsisWidth >= maxWidth) {
            return { ...data, width: maxWidth, overflow: true };
        }
        return {
            str: data.str + chars[i],
            width: data.width + w,
            overflow: false,
        };
    }, { str: '', width: 0, overflow: false });
    return truncated + (overflow ? ELLIPSIS : '');
}