Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: text renderer support letter spacing #1375

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
25 changes: 21 additions & 4 deletions packages/core/src/2d/text/TextRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ export class TextRenderer extends Renderer implements ICustomClone {
@assignmentClone
private _lineSpacing: number = 0;
@assignmentClone
private _letterSpacing: number = 0;
@assignmentClone
private _horizontalAlignment: TextHorizontalAlignment = TextHorizontalAlignment.Center;
@assignmentClone
private _verticalAlignment: TextVerticalAlignment = TextVerticalAlignment.Center;
Expand Down Expand Up @@ -168,7 +170,7 @@ export class TextRenderer extends Renderer implements ICustomClone {
}

/**
* The space between two lines (in pixels).
* The space between two lines.
*/
get lineSpacing(): number {
return this._lineSpacing;
Expand All @@ -181,6 +183,20 @@ export class TextRenderer extends Renderer implements ICustomClone {
}
}

/**
* The space between two letters.
*/
get letterSpacing(): number {
return this._letterSpacing;
}

set letterSpacing(value: number) {
if (this._letterSpacing !== value) {
this._letterSpacing = value;
this._setDirtyFlagTrue(DirtyFlag.Position);
}
}

/**
* The horizontal alignment.
*/
Expand Down Expand Up @@ -492,6 +508,7 @@ export class TextRenderer extends Renderer implements ICustomClone {
const rendererWidth = this.width * _pixelsPerUnit;
const halfRendererWidth = rendererWidth * 0.5;
const rendererHeight = this.height * _pixelsPerUnit;
const letterSpacing = this.letterSpacing * _pixelsPerUnit;

const textMetrics = this.enableWrapping
? TextUtils.measureTextWithWrap(this)
Expand Down Expand Up @@ -532,10 +549,10 @@ export class TextRenderer extends Renderer implements ICustomClone {
startX = -halfRendererWidth;
break;
case TextHorizontalAlignment.Center:
startX = -lineWidth * 0.5;
startX = -lineWidth * 0.5 + letterSpacing * 0.5;
break;
case TextHorizontalAlignment.Right:
startX = halfRendererWidth - lineWidth;
startX = halfRendererWidth - lineWidth + letterSpacing;
break;
}

Expand Down Expand Up @@ -566,7 +583,7 @@ export class TextRenderer extends Renderer implements ICustomClone {
j === 0 && (minX = Math.min(minX, left));
j === m && (maxX = Math.max(maxX, right));
}
startX += charInfo.xAdvance;
startX += charInfo.xAdvance + letterSpacing;
}

startY -= lineHeight;
Expand Down
186 changes: 154 additions & 32 deletions packages/core/src/2d/text/TextUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export class TextUtils {
const lineMaxSizes = new Array<FontSizeInfo>();
const { _pixelsPerUnit } = Engine;
const lineHeight = fontSizeInfo.size + renderer.lineSpacing * _pixelsPerUnit;
const letterSpacing = renderer.letterSpacing * _pixelsPerUnit;
const wrapWidth = renderer.width * _pixelsPerUnit;
let width = 0;

Expand All @@ -110,52 +111,154 @@ export class TextUtils {
let charsWidth = 0;
let maxAscent = -1;
let maxDescent = -1;
let wordChars = "";
let wordCharsWidth = 0;
let wordMaxAscent = -1;
let wordMaxDescent = -1;
let curInEnglishWord = false;
let isNotFirstLine = false;

for (let j = 0, m = subText.length; j < m; ++j) {
const char = subText[j];
const charInfo = TextUtils._getCharInfo(char, fontString, subFont);
const charCode = char.charCodeAt(0);
// 32 is space.
const isSpace = charCode === 32;
if (isSpace && isNotFirstLine && chars.length === 0 && wordChars.length === 0) {
continue;
}

const isNotEnglish = isSpace || charCode > 255;
const { w, offsetY } = charInfo;
const halfH = charInfo.h * 0.5;
const ascent = halfH + offsetY;
const descent = halfH - offsetY;
if (charsWidth + w > wrapWidth) {
if (charsWidth === 0) {
lines.push(char);
lineWidths.push(w);
lineMaxSizes.push({
ascent,
descent,
size: ascent + descent
});

if (isNotEnglish) {
// If it is an English word before, need to handle the previous English word and chars.
if (curInEnglishWord) {
if (charsWidth + wordCharsWidth > wrapWidth) {
this._pushCharsToLines(lines, lineWidths, lineMaxSizes, chars, charsWidth, maxAscent, maxDescent);
isNotFirstLine = true;
chars = wordChars;
charsWidth = wordCharsWidth;
maxAscent = wordMaxAscent;
maxDescent = wordMaxDescent;
} else {
chars += wordChars;
charsWidth += wordCharsWidth;
maxAscent < wordMaxAscent && (maxAscent = wordMaxAscent);
maxDescent < wordMaxDescent && (maxDescent = wordMaxDescent);
}

curInEnglishWord = false;
wordChars = "";
wordCharsWidth = 0;
wordMaxAscent = -1;
wordMaxDescent = -1;
}

// Handle cur char.
if (charsWidth + w > wrapWidth) {
if (charsWidth === 0) {
this._pushCharsToLines(lines, lineWidths, lineMaxSizes, char, w, ascent, descent);
isNotFirstLine = true;
} else {
this._pushCharsToLines(lines, lineWidths, lineMaxSizes, chars, charsWidth, maxAscent, maxDescent);
isNotFirstLine = true;
if (isSpace) {
chars = "";
charsWidth = 0;
maxAscent = -1;
maxDescent = -1;
} else {
chars = char;
charsWidth = charInfo.xAdvance + letterSpacing;
maxAscent = ascent;
maxDescent = descent;
}
}
} else {
lines.push(chars);
lineWidths.push(charsWidth);
lineMaxSizes.push({
ascent: maxAscent,
descent: maxDescent,
size: maxAscent + maxDescent
});
chars = char;
charsWidth = charInfo.xAdvance;
maxAscent = ascent;
maxDescent = descent;
chars += char;
charsWidth += charInfo.xAdvance + letterSpacing;
maxAscent < ascent && (maxAscent = ascent);
maxDescent < descent && (maxDescent = descent);
}
} else {
chars += char;
charsWidth += charInfo.xAdvance;
maxAscent < ascent && (maxAscent = ascent);
maxDescent < descent && (maxDescent = descent);
curInEnglishWord = true;
// Total width from chars and wordChars and char exceed wrap width.
if (charsWidth + wordCharsWidth + charInfo.w > wrapWidth) {
// Handle chars if it not empty.
if (charsWidth > 0) {
this._pushCharsToLines(lines, lineWidths, lineMaxSizes, chars, charsWidth, maxAscent, maxDescent);
isNotFirstLine = true;
chars = "";
charsWidth = 0;
maxAscent = -1;
maxDescent = -1;
}

// Total width from wordChars and char exceed wrap width.
if (wordCharsWidth + charInfo.w > wrapWidth) {
// Push wordchars to a single line, char becomes the start of a new line.
this._pushCharsToLines(
lines,
lineWidths,
lineMaxSizes,
wordChars,
wordCharsWidth,
wordMaxAscent,
wordMaxDescent
);
isNotFirstLine = true;
wordChars = char;
wordCharsWidth = charInfo.xAdvance + letterSpacing;
wordMaxAscent = ascent;
wordMaxDescent = descent;
} else {
wordChars += char;
wordCharsWidth += charInfo.xAdvance + letterSpacing;
wordMaxAscent < ascent && (wordMaxAscent = maxAscent = ascent);
wordMaxDescent < descent && (wordMaxDescent = maxDescent = descent);
}
} else {
wordChars += char;
wordCharsWidth += charInfo.xAdvance + letterSpacing;
wordMaxAscent < ascent && (wordMaxAscent = maxAscent = ascent);
wordMaxDescent < descent && (wordMaxDescent = maxDescent = descent);
}
}
}

if (wordCharsWidth > 0) {
// If the total width from chars and wordChars exceed wrap width.
if (charsWidth + wordCharsWidth > wrapWidth) {
// Push chars to a single line.
this._pushCharsToLines(lines, lineWidths, lineMaxSizes, chars, charsWidth, maxAscent, maxDescent);
charsWidth = 0;
// Push wordChars to a single line.
this._pushCharsToLines(
lines,
lineWidths,
lineMaxSizes,
wordChars,
wordCharsWidth,
wordMaxAscent,
wordMaxDescent
);
isNotFirstLine = true;
} else {
// Merge to chars.
chars += wordChars;
charsWidth += wordCharsWidth;
maxAscent < wordMaxAscent && (maxAscent = wordMaxAscent);
maxDescent < wordMaxDescent && (maxDescent = wordMaxDescent);
}
}

if (charsWidth > 0) {
lines.push(chars);
lineWidths.push(charsWidth);
lineMaxSizes.push({
ascent: maxAscent,
descent: maxDescent,
size: maxAscent + maxDescent
});
this._pushCharsToLines(lines, lineWidths, lineMaxSizes, chars, charsWidth, maxAscent, maxDescent);
isNotFirstLine = true;
}
}

Expand Down Expand Up @@ -185,6 +288,7 @@ export class TextUtils {
const lineMaxSizes = new Array<FontSizeInfo>();
const { _pixelsPerUnit } = Engine;
const lineHeight = fontSizeInfo.size + renderer.lineSpacing * _pixelsPerUnit;
const letterSpacing = renderer.letterSpacing * _pixelsPerUnit;
let width = 0;
let height = renderer.height * _pixelsPerUnit;
if (renderer.overflowMode === OverflowMode.Overflow) {
Expand All @@ -199,7 +303,7 @@ export class TextUtils {

for (let j = 0, m = line.length; j < m; ++j) {
const charInfo = TextUtils._getCharInfo(line[j], fontString, subFont);
curWidth += charInfo.xAdvance;
curWidth += charInfo.xAdvance + letterSpacing;
const { offsetY } = charInfo;
const halfH = charInfo.h * 0.5;
const ascent = halfH + offsetY;
Expand Down Expand Up @@ -348,6 +452,24 @@ export class TextUtils {

return charInfo;
}

private static _pushCharsToLines(
lines: Array<string>,
lineWidths: Array<number>,
lineMaxSizes: Array<FontSizeInfo>,
chars: string,
charsWidth: number,
ascent: number,
descent: number
): void {
lines.push(chars);
lineWidths.push(charsWidth);
lineMaxSizes.push({
ascent,
descent,
size: ascent + descent
});
}
}

/**
Expand Down