Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions e2e/case/text-character-spacing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* @title CharacterSpacing
* @category Text
*/

import { Camera, TextHorizontalAlignment, TextRenderer, TextVerticalAlignment, Vector3, WebGLEngine } from "@galacean/engine";
import { initScreenshot, updateForE2E } from "./.mockForE2E";

WebGLEngine.create({ canvas: "canvas" }).then((engine) => {
engine.canvas.resizeByClientSize();
const scene = engine.sceneManager.activeScene;
const rootEntity = scene.createRootEntity();

// camera
const cameraEntity = rootEntity.createChild("camera_node");
cameraEntity.transform.position = new Vector3(0, 0, 10);
const camera = cameraEntity.addComponent(Camera);

const text = "Character Spacing";

// Default spacing (0)
const entity0 = rootEntity.createChild("text0");
entity0.transform.position = new Vector3(0, 2, 0);
const tr0 = entity0.addComponent(TextRenderer);
tr0.fontSize = 36;
tr0.text = text;
tr0.horizontalAlignment = TextHorizontalAlignment.Center;
tr0.verticalAlignment = TextVerticalAlignment.Center;

// Positive spacing (0.1 em)
const entity1 = rootEntity.createChild("text1");
entity1.transform.position = new Vector3(0, 1, 0);
const tr1 = entity1.addComponent(TextRenderer);
tr1.fontSize = 36;
tr1.text = text;
tr1.characterSpacing = 0.1;
tr1.horizontalAlignment = TextHorizontalAlignment.Center;
tr1.verticalAlignment = TextVerticalAlignment.Center;

// Larger positive spacing (0.3 em)
const entity2 = rootEntity.createChild("text2");
entity2.transform.position = new Vector3(0, 0, 0);
const tr2 = entity2.addComponent(TextRenderer);
tr2.fontSize = 36;
tr2.text = text;
tr2.characterSpacing = 0.3;
tr2.horizontalAlignment = TextHorizontalAlignment.Center;
tr2.verticalAlignment = TextVerticalAlignment.Center;

// Full em spacing (1 em)
const entity3 = rootEntity.createChild("text3");
entity3.transform.position = new Vector3(0, -0.5, 0);
const tr3 = entity3.addComponent(TextRenderer);
tr3.fontSize = 36;
tr3.text = text;
tr3.characterSpacing = 1;
tr3.horizontalAlignment = TextHorizontalAlignment.Center;
tr3.verticalAlignment = TextVerticalAlignment.Center;

// Negative spacing (-0.05 em)
const entity4 = rootEntity.createChild("text4");
entity4.transform.position = new Vector3(0, -1.5, 0);
const tr4 = entity4.addComponent(TextRenderer);
tr4.fontSize = 36;
tr4.text = text;
tr4.characterSpacing = -0.05;
tr4.horizontalAlignment = TextHorizontalAlignment.Center;
tr4.verticalAlignment = TextVerticalAlignment.Center;

updateForE2E(engine);
initScreenshot(engine, camera);
});
6 changes: 6 additions & 0 deletions e2e/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,12 @@ export const E2E_CONFIG = {
caseFileName: "text-typed",
threshold: 0.016,
diffPercentage: 0.00136
},
CharacterSpacing: {
category: "Text",
caseFileName: "text-character-spacing",
threshold: 0.0,
diffPercentage: 0.0
}
},
Trail: {
Expand Down
61 changes: 42 additions & 19 deletions packages/core/src/2d/text/TextRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,33 +40,35 @@ export class TextRenderer extends Renderer implements ITextRenderer {
_subFont: SubFont = null;
/** @internal */
@ignoreClone
_dirtyFlag: number = DirtyFlag.Font;
_dirtyFlag = DirtyFlag.Font;
@deepClone
private _color: Color = new Color(1, 1, 1, 1);
private _color = new Color(1, 1, 1, 1);
@assignmentClone
private _text: string = "";
private _text = "";
@assignmentClone
private _width: number = 0;
private _width = 0;
@assignmentClone
private _height: number = 0;
private _height = 0;
@ignoreClone
private _localBounds: BoundingBox = new BoundingBox();
private _localBounds = new BoundingBox();
@assignmentClone
private _font: Font = null;
@assignmentClone
private _fontSize: number = 24;
private _fontSize = 24;
@assignmentClone
private _fontStyle: FontStyle = FontStyle.None;
private _fontStyle = FontStyle.None;
@assignmentClone
private _lineSpacing: number = 0;
private _lineSpacing = 0;
@assignmentClone
private _horizontalAlignment: TextHorizontalAlignment = TextHorizontalAlignment.Center;
private _characterSpacing = 0;
@assignmentClone
private _verticalAlignment: TextVerticalAlignment = TextVerticalAlignment.Center;
private _horizontalAlignment = TextHorizontalAlignment.Center;
@assignmentClone
private _enableWrapping: boolean = false;
private _verticalAlignment = TextVerticalAlignment.Center;
@assignmentClone
private _overflowMode: OverflowMode = OverflowMode.Overflow;
private _enableWrapping = false;
@assignmentClone
private _overflowMode = OverflowMode.Overflow;

/**
* Rendering color for the Text.
Expand Down Expand Up @@ -170,7 +172,7 @@ export class TextRenderer extends Renderer implements ITextRenderer {
}

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

/**
* The space between two characters, in em (ratio of fontSize).
*/
get characterSpacing(): number {
return this._characterSpacing;
}

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

/**
* The horizontal alignment.
*/
Expand Down Expand Up @@ -510,14 +526,21 @@ export class TextRenderer extends Renderer implements ITextRenderer {
const { min, max } = this._localBounds;
const charRenderInfos = TextRenderer._charRenderInfos;
const charFont = this._getSubFont();
const characterSpacing = this._characterSpacing * this._fontSize;
const textMetrics = this.enableWrapping
? TextUtils.measureTextWithWrap(
this,
this.width * _pixelsPerUnit,
this.height * _pixelsPerUnit,
this._lineSpacing * _pixelsPerUnit
this._lineSpacing * this._fontSize,
characterSpacing
)
: TextUtils.measureTextWithoutWrap(this, this.height * _pixelsPerUnit, this._lineSpacing * _pixelsPerUnit);
: TextUtils.measureTextWithoutWrap(
this,
this.height * _pixelsPerUnit,
this._lineSpacing * this._fontSize,
characterSpacing
);
const { height, lines, lineWidths, lineHeight, lineMaxSizes } = textMetrics;
const charRenderInfoPool = this.engine._charRenderInfoPool;
const linesLen = lines.length;
Expand All @@ -526,9 +549,9 @@ export class TextRenderer extends Renderer implements ITextRenderer {
if (linesLen > 0) {
const { horizontalAlignment } = this;
const pixelsPerUnitReciprocal = 1.0 / _pixelsPerUnit;
const rendererWidth = this.width * _pixelsPerUnit;
const rendererWidth = this._width * _pixelsPerUnit;
const halfRendererWidth = rendererWidth * 0.5;
const rendererHeight = this.height * _pixelsPerUnit;
const rendererHeight = this._height * _pixelsPerUnit;
const halfLineHeight = lineHeight * 0.5;

let startY = 0;
Expand Down Expand Up @@ -591,7 +614,7 @@ export class TextRenderer extends Renderer implements ITextRenderer {
j === firstRow && (minX = Math.min(minX, left));
maxX = Math.max(maxX, right);
}
startX += charInfo.xAdvance + charInfo.offsetX;
startX += charInfo.xAdvance + characterSpacing;
}
}
startY -= lineHeight;
Expand Down
29 changes: 21 additions & 8 deletions packages/core/src/2d/text/TextUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ export class TextUtils {
renderer: ITextRenderer,
rendererWidth: number,
rendererHeight: number,
lineSpacing: number
lineSpacing: number,
characterSpacing: number
): TextMetrics {
const subFont = renderer._getSubFont();
const fontString = subFont.nativeFontString;
Expand Down Expand Up @@ -157,6 +158,7 @@ export class TextUtils {
if (lineWidth + wordWidth > rendererWidth) {
// Push if before line is not empty
if (lineWidth > 0) {
lineWidth -= characterSpacing;
this._pushLine(lines, lineWidths, lineMaxSizes, line, lineWidth, lineMaxAscent, lineMaxDescent);
}

Expand All @@ -180,6 +182,7 @@ export class TextUtils {
// Handle char
// At least one char in a line
if (lineWidth + w > rendererWidth && lineWidth > 0) {
lineWidth -= characterSpacing;
this._pushLine(lines, lineWidths, lineMaxSizes, line, lineWidth, lineMaxAscent, lineMaxDescent);
textWidth = Math.max(textWidth, lineWidth);
notFirstLine = true;
Expand All @@ -188,19 +191,20 @@ export class TextUtils {
lineWidth = lineMaxAscent = lineMaxDescent = 0;
} else {
line = char;
lineWidth = charInfo.xAdvance;
lineWidth = charInfo.xAdvance + characterSpacing;
lineMaxAscent = ascent;
lineMaxDescent = descent;
}
} else {
line += char;
lineWidth += charInfo.xAdvance;
lineWidth += charInfo.xAdvance + characterSpacing;
lineMaxAscent = Math.max(lineMaxAscent, ascent);
lineMaxDescent = Math.max(lineMaxDescent, descent);
}
} else {
if (wordWidth + charInfo.w > rendererWidth) {
if (lineWidth > 0) {
lineWidth -= characterSpacing;
this._pushLine(lines, lineWidths, lineMaxSizes, line, lineWidth, lineMaxAscent, lineMaxDescent);
textWidth = Math.max(textWidth, lineWidth);
line = "";
Expand All @@ -215,12 +219,12 @@ export class TextUtils {
textWidth = Math.max(textWidth, wordWidth);
notFirstLine = true;
word = char;
wordWidth = charInfo.xAdvance;
wordWidth = charInfo.xAdvance + characterSpacing;
wordMaxAscent = ascent;
wordMaxDescent = descent;
} else {
word += char;
wordWidth += charInfo.xAdvance;
wordWidth += charInfo.xAdvance + characterSpacing;
wordMaxAscent = Math.max(wordMaxAscent, ascent);
wordMaxDescent = Math.max(wordMaxDescent, descent);
}
Expand All @@ -232,13 +236,15 @@ export class TextUtils {
if (lineWidth + wordWidth > rendererWidth) {
// Push chars to a single line
if (lineWidth > 0) {
lineWidth -= characterSpacing;
this._pushLine(lines, lineWidths, lineMaxSizes, line, lineWidth, lineMaxAscent, lineMaxDescent);
}
textWidth = Math.max(textWidth, lineWidth);

lineWidth = 0;
// Push word to a single line
if (wordWidth > 0) {
wordWidth -= characterSpacing;
this._pushLine(lines, lineWidths, lineMaxSizes, word, wordWidth, wordMaxAscent, wordMaxDescent);
}
textWidth = Math.max(textWidth, wordWidth);
Expand All @@ -252,6 +258,7 @@ export class TextUtils {
}

if (lineWidth > 0) {
lineWidth -= characterSpacing;
this._pushLine(lines, lineWidths, lineMaxSizes, line, lineWidth, lineMaxAscent, lineMaxDescent);
textWidth = Math.max(textWidth, lineWidth);
}
Expand All @@ -272,7 +279,12 @@ export class TextUtils {
};
}

static measureTextWithoutWrap(renderer: ITextRenderer, rendererHeight: number, lineSpacing: number): TextMetrics {
static measureTextWithoutWrap(
renderer: ITextRenderer,
rendererHeight: number,
lineSpacing: number,
characterSpacing: number
): TextMetrics {
const subFont = renderer._getSubFont();
const fontString = subFont.nativeFontString;
const fontSizeInfo = TextUtils.measureFont(fontString);
Expand All @@ -287,11 +299,12 @@ export class TextUtils {
subFont.nativeFontString = fontString;
for (let i = 0; i < textCount; ++i) {
const line = subTexts[i];
let curWidth = 0;
const lineLength = line.length;
let curWidth = lineLength > 1 ? characterSpacing * (lineLength - 1) : 0;
let maxAscent = 0;
let maxDescent = 0;

for (let j = 0, m = line.length; j < m; ++j) {
for (let j = 0; j < lineLength; ++j) {
const charInfo = TextUtils._getCharInfo(line[j], fontString, subFont);
curWidth += charInfo.xAdvance;
const { offsetY } = charInfo;
Expand Down
Loading