mirror of
https://github.com/cocos/cocos-engine.git
synced 2026-09-10 17:48:09 +08:00
1159 lines
49 KiB
TypeScript
1159 lines
49 KiB
TypeScript
/*
|
|
Copyright (c) 2023 Xiamen Yaji Software Co., Ltd.
|
|
|
|
https://www.cocos.com/
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights to
|
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
of the Software, and to permit persons to whom the Software is furnished to do so,
|
|
subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in
|
|
all copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
THE SOFTWARE.
|
|
*/
|
|
import { ANDROID, JSB } from 'internal:constants';
|
|
import { cclegacy } from '@base/global';
|
|
import { log, logID, warn } from '@base/debug';
|
|
import { Color, Rect, Vec2 } from '@base/math';
|
|
import { Texture2D } from '../../../asset/assets';
|
|
import { WrapMode } from '../../../asset/assets/asset-enum';
|
|
import { SpriteFrame } from '../../assets';
|
|
import { FontLetterDefinition } from '../../assets/bitmap-font';
|
|
import { HorizontalTextAlignment, Overflow, VerticalTextAlignment } from '../../components/label';
|
|
import { BASELINE_RATIO, fragmentText, getBaselineOffset, isUnicodeCJK, isUnicodeSpace, safeMeasureText } from '../../utils/text-utils';
|
|
import { CanvasPool, ISharedLabelData, shareLabelInfo } from './font-utils';
|
|
import { TextOutputLayoutData, TextOutputRenderData } from './text-output-data';
|
|
import { TextStyle } from './text-style';
|
|
import { TextLayout } from './text-layout';
|
|
|
|
const Alignment = [
|
|
'left', // macro.TextAlignment.LEFT
|
|
'center', // macro.TextAlignment.CENTER
|
|
'right', // macro.TextAlignment.RIGHT
|
|
];
|
|
const MAX_SIZE = 2048;
|
|
const _BASELINE_OFFSET = getBaselineOffset();
|
|
const _invisibleAlpha = (1 / 255).toFixed(3);
|
|
const MAX_CALCULATION_NUM = 3;
|
|
|
|
export interface IRenderData {
|
|
x: number;
|
|
y: number;
|
|
z: number;
|
|
u: number;
|
|
v: number;
|
|
color: Color;
|
|
}
|
|
|
|
class LetterInfo {
|
|
public char = '';
|
|
public valid = true;
|
|
public x = 0;
|
|
public y = 0;
|
|
public line = 0;
|
|
public hash = '';
|
|
}
|
|
|
|
export class TextProcessing {
|
|
// -------------------- Common Part --------------------------
|
|
public static instance: TextProcessing;
|
|
constructor () {
|
|
this._canvasData = CanvasPool.getInstance().get();
|
|
this._canvas = this._canvasData.canvas;
|
|
this._context = this._canvasData.context;
|
|
}
|
|
|
|
public destroy (): void {
|
|
CanvasPool.getInstance().put(this._canvasData!);
|
|
this._lettersInfo.length = 0;
|
|
}
|
|
|
|
public processingString (isBmFont: boolean, style: TextStyle, layout: TextLayout,
|
|
outputLayoutData: TextOutputLayoutData, inputString: string, out?: string[]): void {
|
|
if (!isBmFont) {
|
|
let loopTime = 0;
|
|
this._fontScale = this._getStyleFontScale(style.fontSize, style.fontScale);
|
|
this._updatePaddingRect(style, outputLayoutData);
|
|
this._calculateLabelFont(style, layout, outputLayoutData, inputString);
|
|
// check & limit canvas size
|
|
while ((outputLayoutData.canvasSize.width > MAX_SIZE || outputLayoutData.canvasSize.height > MAX_SIZE)
|
|
&& (loopTime <= MAX_CALCULATION_NUM)) {
|
|
loopTime++;
|
|
if (loopTime > MAX_CALCULATION_NUM) {
|
|
this._fontScale = 1;
|
|
} else {
|
|
const maxValue = Math.max(outputLayoutData.canvasSize.width, outputLayoutData.canvasSize.height); // Current Canvas Size max dimension
|
|
const canvasScaleToMaxSizeRatio = MAX_SIZE / maxValue;
|
|
this._fontScale *= canvasScaleToMaxSizeRatio;
|
|
this._fontScale = Math.max(1, this._fontScale);
|
|
}
|
|
|
|
this._updatePaddingRect(style, outputLayoutData);
|
|
this._calculateLabelFont(style, layout, outputLayoutData, inputString);
|
|
}
|
|
} else {
|
|
if (!style.fntConfig) { // for char
|
|
this._fontScale = this._getStyleFontScale(style.originFontSize, style.fontScale);
|
|
} else {
|
|
this._fontScale = 1;
|
|
}
|
|
shareLabelInfo.fontScale = this._fontScale;
|
|
this._setupBMFontOverflowMetrics(layout, outputLayoutData);
|
|
this._updateFontScale(style);
|
|
this._computeHorizontalKerningForText(style, layout, inputString);
|
|
this._alignText(style, layout, outputLayoutData, inputString);
|
|
}
|
|
if (out) {
|
|
out = outputLayoutData.parsedString;
|
|
}
|
|
}
|
|
|
|
public generateRenderInfo (isBmFont: boolean, style: TextStyle, layout: TextLayout, outputLayoutData: TextOutputLayoutData,
|
|
outputRenderData: TextOutputRenderData, inputString: string, callback: AnyFunction): void {
|
|
if (!isBmFont) {
|
|
this._updateLabelDimensions(style, layout, outputLayoutData);
|
|
this._updateTexture(style, layout, outputLayoutData, outputRenderData);
|
|
this.generateVertexData(isBmFont, style, layout, outputLayoutData, outputRenderData, inputString, callback);
|
|
} else {
|
|
this._computeAlignmentOffset(style, layout, outputLayoutData);
|
|
this.generateVertexData(isBmFont, style, layout, outputLayoutData, outputRenderData, inputString, callback);
|
|
}
|
|
}
|
|
|
|
public setCanvasUsed (canvas: HTMLCanvasElement, content: CanvasRenderingContext2D | null): void {
|
|
this._canvas = canvas;
|
|
this._context = content;
|
|
}
|
|
// -------------------- Common Part --------------------------
|
|
|
|
// -------------------- Canvas Mode Part --------------------------
|
|
|
|
// -------------------- String Processing Part --------------------------
|
|
private _context: CanvasRenderingContext2D | null = null;
|
|
private _canvas: HTMLCanvasElement | null = null;
|
|
private _canvasData: ISharedLabelData | null = null;
|
|
|
|
private _lettersInfo: LetterInfo[] = [];
|
|
private _tmpRect = new Rect();
|
|
|
|
private _maxFontSize = 100;
|
|
private _fontScale = 1;
|
|
|
|
private _getStyleFontScale (fontSize: number, fontScale: number): number {
|
|
let scale = fontScale;
|
|
if (scale * fontSize > this._maxFontSize && fontSize < this._maxFontSize) { // Font size limit
|
|
scale = this._maxFontSize / fontSize;
|
|
}
|
|
if (scale < 1) { scale = 1; }
|
|
return scale;
|
|
}
|
|
|
|
private _calculateLabelFont (style: TextStyle, layout: TextLayout,
|
|
outputLayoutData: TextOutputLayoutData, inputString: string): void {
|
|
if (!this._context) {
|
|
return;
|
|
}
|
|
|
|
style.actualFontSize = style.fontSize * this._fontScale;
|
|
if (ANDROID) {
|
|
// Android restriction only accepts integer font sizes
|
|
style.actualFontSize = Math.floor(style.actualFontSize);
|
|
this._fontScale = style.actualFontSize / style.fontSize;
|
|
}
|
|
const paragraphedStrings = inputString.split('\n');
|
|
|
|
const _splitStrings = outputLayoutData.parsedString = paragraphedStrings;
|
|
const _fontDesc = this._getFontDesc(style.actualFontSize, style.fontFamily, style.isBold, style.isItalic);
|
|
this._context.font = style.fontDesc = _fontDesc;
|
|
|
|
switch (layout.overFlow) {
|
|
case Overflow.NONE: {
|
|
let canvasSizeX = 0;
|
|
let canvasSizeY = 0;
|
|
for (let i = 0; i < paragraphedStrings.length; ++i) {
|
|
const paraLength = safeMeasureText(this._context, paragraphedStrings[i], _fontDesc);
|
|
canvasSizeX = canvasSizeX > paraLength ? canvasSizeX : paraLength;
|
|
}
|
|
canvasSizeY = (_splitStrings.length + BASELINE_RATIO)
|
|
* this._getLineHeight(layout.lineHeight, style.actualFontSize, style.fontSize);
|
|
const rawWidth = canvasSizeX;
|
|
const rawHeight = canvasSizeY;
|
|
|
|
outputLayoutData.canvasSize.width = rawWidth + outputLayoutData.canvasPadding.width * this._fontScale;
|
|
outputLayoutData.canvasSize.height = rawHeight + outputLayoutData.canvasPadding.height * this._fontScale;
|
|
outputLayoutData.nodeContentSize.width = (rawWidth + outputLayoutData.contentSizeExtend.width * this._fontScale) / this._fontScale;
|
|
outputLayoutData.nodeContentSize.height = (rawHeight + outputLayoutData.contentSizeExtend.height * this._fontScale) / this._fontScale;
|
|
break;
|
|
}
|
|
case Overflow.SHRINK: {
|
|
this._calculateShrinkFont(paragraphedStrings, style, layout, outputLayoutData);
|
|
this._calculateWrapText(paragraphedStrings, style, layout, outputLayoutData);
|
|
outputLayoutData.canvasSize.width = outputLayoutData.nodeContentSize.width * this._fontScale;
|
|
outputLayoutData.canvasSize.height = outputLayoutData.nodeContentSize.height * this._fontScale;
|
|
break;
|
|
}
|
|
case Overflow.CLAMP: {
|
|
this._calculateWrapText(paragraphedStrings, style, layout, outputLayoutData);
|
|
outputLayoutData.canvasSize.width = outputLayoutData.nodeContentSize.width * this._fontScale;
|
|
outputLayoutData.canvasSize.height = outputLayoutData.nodeContentSize.height * this._fontScale;
|
|
break;
|
|
}
|
|
case Overflow.RESIZE_HEIGHT: {
|
|
this._calculateWrapText(paragraphedStrings, style, layout, outputLayoutData);
|
|
const rawHeight = (outputLayoutData.parsedString.length + BASELINE_RATIO)
|
|
* this._getLineHeight(layout.lineHeight, style.actualFontSize, style.fontSize);
|
|
|
|
outputLayoutData.canvasSize.width = outputLayoutData.nodeContentSize.width * this._fontScale;
|
|
outputLayoutData.canvasSize.height = (rawHeight + outputLayoutData.canvasPadding.height * this._fontScale);
|
|
// set node height
|
|
outputLayoutData.nodeContentSize.height = (rawHeight + outputLayoutData.contentSizeExtend.height * this._fontScale) / this._fontScale;
|
|
break;
|
|
}
|
|
default: {
|
|
// nop
|
|
}
|
|
}
|
|
}
|
|
|
|
// can cache
|
|
private _getFontDesc (fontSize: number, fontFamily: string, isBold: boolean, isItalic: boolean): string {
|
|
let fontDesc = `${fontSize.toString()}px `;
|
|
fontDesc += fontFamily;
|
|
if (isBold) {
|
|
fontDesc = `bold ${fontDesc}`;
|
|
}
|
|
|
|
if (isItalic) {
|
|
fontDesc = `italic ${fontDesc}`;
|
|
}
|
|
|
|
return fontDesc;
|
|
}
|
|
|
|
// can cache
|
|
private _getLineHeight (lineHeight: number, fontSize: number, drawFontsize: number): number {
|
|
let nodeSpacingY = lineHeight;
|
|
if (nodeSpacingY === 0) {
|
|
nodeSpacingY = fontSize;
|
|
} else {
|
|
nodeSpacingY = nodeSpacingY * fontSize / drawFontsize;
|
|
}
|
|
|
|
return nodeSpacingY;
|
|
}
|
|
|
|
private _calculateShrinkFont (paragraphedStrings: string[], style: TextStyle, layout: TextLayout, outputLayoutData: TextOutputLayoutData): void {
|
|
if (!this._context) return;
|
|
let _fontDesc = this._getFontDesc(style.actualFontSize, style.fontFamily, style.isBold, style.isItalic);
|
|
this._context.font = _fontDesc;
|
|
const paragraphLength = this._calculateParagraphLength(paragraphedStrings, this._context, _fontDesc);
|
|
|
|
let i = 0;
|
|
let totalHeight = 0;
|
|
let maxLength = 0;
|
|
let _fontSize = style.actualFontSize;
|
|
|
|
if (layout.wrapping) {
|
|
const canvasWidthNoMargin = outputLayoutData.nodeContentSize.width * this._fontScale;
|
|
const canvasHeightNoMargin = outputLayoutData.nodeContentSize.height * this._fontScale;
|
|
if (canvasWidthNoMargin < 0 || canvasHeightNoMargin < 0) {
|
|
return;
|
|
}
|
|
totalHeight = canvasHeightNoMargin + 1;
|
|
const actualFontSize = style.actualFontSize + 1;
|
|
let textFragment: string[] = [];
|
|
let left = 0;
|
|
let right = actualFontSize | 0;
|
|
let mid = 0;
|
|
|
|
while (left < right) {
|
|
mid = (left + right + 1) >> 1;
|
|
|
|
if (mid <= 0) {
|
|
logID(4003);
|
|
break;
|
|
}
|
|
|
|
_fontSize = mid;
|
|
_fontDesc = this._getFontDesc(_fontSize, style.fontFamily, style.isBold, style.isItalic);
|
|
this._context.font = _fontDesc;
|
|
const lineHeight = this._getLineHeight(layout.lineHeight, _fontSize, style.fontSize);
|
|
|
|
totalHeight = 0;
|
|
for (i = 0; i < paragraphedStrings.length; ++i) {
|
|
const allWidth = safeMeasureText(this._context, paragraphedStrings[i], _fontDesc);
|
|
textFragment = fragmentText(paragraphedStrings[i],
|
|
allWidth,
|
|
canvasWidthNoMargin,
|
|
this._measureText(this._context, _fontDesc));
|
|
totalHeight += textFragment.length * lineHeight;
|
|
}
|
|
|
|
if (totalHeight > canvasHeightNoMargin) {
|
|
right = mid - 1;
|
|
} else {
|
|
left = mid;
|
|
}
|
|
}
|
|
|
|
if (left === 0) {
|
|
logID(4003);
|
|
} else {
|
|
_fontSize = left;
|
|
_fontDesc = this._getFontDesc(_fontSize, style.fontFamily, style.isBold, style.isItalic);
|
|
this._context.font = _fontDesc;
|
|
}
|
|
} else {
|
|
totalHeight = paragraphedStrings.length * this._getLineHeight(layout.lineHeight, _fontSize, style.fontSize);
|
|
|
|
for (i = 0; i < paragraphedStrings.length; ++i) {
|
|
if (maxLength < paragraphLength[i]) {
|
|
maxLength = paragraphLength[i];
|
|
}
|
|
}
|
|
const scaleX = (outputLayoutData.canvasSize.width - outputLayoutData.canvasPadding.width) * this._fontScale / maxLength;
|
|
const scaleY = (outputLayoutData.canvasSize.height * this._fontScale) / totalHeight;
|
|
|
|
_fontSize = (style.actualFontSize * Math.min(1, scaleX, scaleY)) | 0;
|
|
_fontDesc = this._getFontDesc(_fontSize, style.fontFamily, style.isBold, style.isItalic);
|
|
this._context.font = _fontDesc;
|
|
}
|
|
|
|
style.actualFontSize = _fontSize;
|
|
style.fontDesc = _fontDesc;
|
|
}
|
|
|
|
private _calculateWrapText (paragraphedStrings: string[], style: TextStyle, layout: TextLayout, outputLayoutData: TextOutputLayoutData): void {
|
|
if (!layout.wrapping || !this._context) return;
|
|
|
|
let _splitStrings: string[] = [];
|
|
const canvasWidthNoMargin = outputLayoutData.nodeContentSize.width * this._fontScale;
|
|
const _fontDesc = this._getFontDesc(style.actualFontSize, style.fontFamily, style.isBold, style.isItalic);
|
|
this._context.font = _fontDesc;
|
|
for (let i = 0; i < paragraphedStrings.length; ++i) {
|
|
const allWidth = safeMeasureText(this._context, paragraphedStrings[i], _fontDesc);
|
|
const textFragment = fragmentText(paragraphedStrings[i],
|
|
allWidth,
|
|
canvasWidthNoMargin,
|
|
this._measureText(this._context, _fontDesc));
|
|
_splitStrings = _splitStrings.concat(textFragment);
|
|
}
|
|
outputLayoutData.parsedString = _splitStrings;
|
|
style.fontDesc = _fontDesc;
|
|
}
|
|
|
|
private _measureText (ctx: CanvasRenderingContext2D, fontDesc): (str: string) => number {
|
|
return (str: string): number => safeMeasureText(ctx, str, fontDesc);
|
|
}
|
|
|
|
private _calculateParagraphLength (paragraphedStrings: string[], ctx: CanvasRenderingContext2D, fontDesc: string): number[] {
|
|
const paragraphLength: number[] = [];
|
|
|
|
for (const para of paragraphedStrings) {
|
|
const width: number = safeMeasureText(ctx, para, fontDesc);
|
|
paragraphLength.push(width);
|
|
}
|
|
|
|
return paragraphLength;
|
|
}
|
|
|
|
private _updatePaddingRect (style: TextStyle, outputLayoutData: TextOutputLayoutData): void {
|
|
let top = 0; let bottom = 0; let left = 0; let right = 0;
|
|
let outlineWidth = 0;
|
|
outputLayoutData.contentSizeExtend.width = outputLayoutData.contentSizeExtend.height = 0;
|
|
if (style.isOutlined) {
|
|
outlineWidth = style.outlineWidth;
|
|
top = bottom = left = right = outlineWidth;
|
|
outputLayoutData.contentSizeExtend.width = outputLayoutData.contentSizeExtend.height = outlineWidth * 2;
|
|
}
|
|
if (style.hasShadow) {
|
|
const shadowWidth = style.shadowBlur + outlineWidth;
|
|
const offsetX = style.shadowOffsetX;
|
|
const offsetY = style.shadowOffsetY;
|
|
left = Math.max(left, -offsetX + shadowWidth);
|
|
right = Math.max(right, offsetX + shadowWidth);
|
|
top = Math.max(top, offsetY + shadowWidth);
|
|
bottom = Math.max(bottom, -offsetY + shadowWidth);
|
|
}
|
|
if (style.isItalic) {
|
|
// 0.0174532925 = 3.141592653 / 180
|
|
const offset = style.fontSize * Math.tan(12 * 0.0174532925);
|
|
right += offset;
|
|
outputLayoutData.contentSizeExtend.width += offset;
|
|
}
|
|
outputLayoutData.canvasPadding.x = left;
|
|
outputLayoutData.canvasPadding.y = top;
|
|
outputLayoutData.canvasPadding.width = left + right;
|
|
outputLayoutData.canvasPadding.height = top + bottom;
|
|
}
|
|
|
|
// -------------------- String Processing Part --------------------------
|
|
|
|
// -------------------- Render Processing Part --------------------------
|
|
|
|
private _updateLabelDimensions (style: TextStyle, layout: TextLayout, outputLayoutData: TextOutputLayoutData): void {
|
|
outputLayoutData.canvasSize.width = Math.min(outputLayoutData.canvasSize.width, MAX_SIZE);
|
|
outputLayoutData.canvasSize.height = Math.min(outputLayoutData.canvasSize.height, MAX_SIZE);
|
|
|
|
this._canvas!.width = outputLayoutData.canvasSize.width;
|
|
this._canvas!.height = outputLayoutData.canvasSize.height;
|
|
|
|
this._context!.font = style.fontDesc;
|
|
// align
|
|
this._context!.textAlign = Alignment[layout.horizontalAlign] as any;
|
|
this._context!.textBaseline = 'alphabetic';
|
|
}
|
|
|
|
private _calculateFillTextStartPosition (style: TextStyle, layout: TextLayout, outputLayoutData: TextOutputLayoutData): void {
|
|
let labelX = 0;
|
|
if (layout.horizontalAlign === HorizontalTextAlignment.RIGHT) {
|
|
labelX = outputLayoutData.canvasSize.width - outputLayoutData.canvasPadding.width;
|
|
} else if (layout.horizontalAlign === HorizontalTextAlignment.CENTER) {
|
|
labelX = (outputLayoutData.canvasSize.width - outputLayoutData.canvasPadding.width) / 2;
|
|
}
|
|
|
|
const lineHeight = this._getLineHeight(layout.lineHeight, style.actualFontSize, style.fontSize);
|
|
const drawStartY = lineHeight * (outputLayoutData.parsedString.length - 1);
|
|
// TOP
|
|
let firstLinelabelY = style.actualFontSize * (1 - BASELINE_RATIO / 2);
|
|
if (layout.verticalAlign !== VerticalTextAlignment.TOP) {
|
|
// free space in vertical direction
|
|
let blank = drawStartY + outputLayoutData.canvasPadding.height + style.actualFontSize - outputLayoutData.canvasSize.height;
|
|
if (layout.verticalAlign === VerticalTextAlignment.BOTTOM) {
|
|
// Unlike BMFont, needs to reserve space below.
|
|
blank += BASELINE_RATIO / 2 * style.actualFontSize;
|
|
// BOTTOM
|
|
firstLinelabelY -= blank;
|
|
} else {
|
|
// CENTER
|
|
firstLinelabelY -= blank / 2;
|
|
}
|
|
}
|
|
|
|
firstLinelabelY += _BASELINE_OFFSET * style.actualFontSize;
|
|
|
|
outputLayoutData.startPosition.set(labelX + outputLayoutData.canvasPadding.x, firstLinelabelY + outputLayoutData.canvasPadding.y);
|
|
}
|
|
|
|
private _updateTexture (style: TextStyle, layout: TextLayout, outputLayoutData: TextOutputLayoutData, outputRenderData: TextOutputRenderData): void {
|
|
if (!this._context || !this._canvas) {
|
|
return;
|
|
}
|
|
|
|
this._context.clearRect(0, 0, this._canvas.width, this._canvas.height);
|
|
this._context.font = style.fontDesc;
|
|
|
|
this._calculateFillTextStartPosition(style, layout, outputLayoutData);
|
|
const lineHeight = this._getLineHeight(layout.lineHeight, style.actualFontSize, style.fontSize);
|
|
// use round for line join to avoid sharp intersect point
|
|
this._context.lineJoin = 'round';
|
|
|
|
if (style.isOutlined) {
|
|
this._context.fillStyle = `rgba(${style.outlineColor.r}, ${style.outlineColor.g}, ${style.outlineColor.b}, ${_invisibleAlpha})`;
|
|
// Notice: fillRect twice will not effect
|
|
this._context.fillRect(0, 0, this._canvas.width, this._canvas.height);
|
|
} else {
|
|
this._context.fillStyle = `rgba(${style.color.r}, ${style.color.g}, ${style.color.b}, ${_invisibleAlpha})`;
|
|
this._context.fillRect(0, 0, this._canvas.width, this._canvas.height);
|
|
}
|
|
this._context.fillStyle = `rgb(${style.color.r}, ${style.color.g}, ${style.color.b})`;
|
|
// Use the value that has been amplified by fontScale
|
|
const tempPos = new Vec2(outputLayoutData.startPosition.x, outputLayoutData.startPosition.y);
|
|
const drawTextPosX = tempPos.x;
|
|
let drawTextPosY = 0;
|
|
// draw shadow and underline
|
|
this._drawTextEffect(tempPos, lineHeight, style, layout, outputLayoutData);
|
|
// draw text and outline
|
|
for (let i = 0; i < outputLayoutData.parsedString.length; ++i) {
|
|
drawTextPosY = tempPos.y + i * lineHeight;
|
|
if (style.isOutlined) {
|
|
this._context.strokeText(outputLayoutData.parsedString[i], drawTextPosX, drawTextPosY);
|
|
}
|
|
this._context.fillText(outputLayoutData.parsedString[i], drawTextPosX, drawTextPosY);
|
|
}
|
|
|
|
if (style.hasShadow) {
|
|
this._context.shadowColor = 'transparent';
|
|
}
|
|
|
|
this._uploadTexture(outputRenderData);
|
|
}
|
|
|
|
private _uploadTexture (outputRenderData: TextOutputRenderData): void {
|
|
if (outputRenderData.texture && this._canvas) {
|
|
let tex: Texture2D;
|
|
if (outputRenderData.texture instanceof SpriteFrame) {
|
|
tex = (outputRenderData.texture.texture as Texture2D);
|
|
} else {
|
|
tex = outputRenderData.texture;
|
|
}
|
|
|
|
const uploadAgain = this._canvas.width !== 0 && this._canvas.height !== 0;
|
|
|
|
if (uploadAgain) {
|
|
tex.reset({
|
|
width: this._canvas.width,
|
|
height: this._canvas.height,
|
|
mipmapLevel: 1,
|
|
});
|
|
tex.uploadData(this._canvas);
|
|
tex.setWrapMode(WrapMode.CLAMP_TO_EDGE, WrapMode.CLAMP_TO_EDGE);
|
|
if (outputRenderData.texture instanceof SpriteFrame) {
|
|
outputRenderData.texture.rect = new Rect(0, 0, this._canvas.width, this._canvas.height);
|
|
outputRenderData.texture._calculateUV();
|
|
}
|
|
if (cclegacy.director.root && cclegacy.director.root.batcher2D) {
|
|
if (JSB) {
|
|
cclegacy.director.root.batcher2D._releaseDescriptorSetCache(tex.getGFXTexture(), tex.getGFXSampler());
|
|
} else {
|
|
cclegacy.director.root.batcher2D._releaseDescriptorSetCache(tex.getHash());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private _drawTextEffect (startPosition: Vec2, lineHeight: number, style: TextStyle, layout: TextLayout, outputLayoutData: TextOutputLayoutData): void {
|
|
if (!style.hasShadow && !style.isOutlined && !style.isUnderline) return;
|
|
|
|
const isMultiple = outputLayoutData.parsedString.length > 1 && style.hasShadow;
|
|
const measureText = this._measureText(this._context!, style.fontDesc);
|
|
let drawTextPosX = 0;
|
|
let drawTextPosY = 0;
|
|
|
|
// only one set shadow and outline
|
|
if (style.hasShadow) {
|
|
this._setupShadow(style);
|
|
}
|
|
|
|
if (style.isOutlined) {
|
|
this._setupOutline(style);
|
|
}
|
|
|
|
// draw shadow and (outline or text)
|
|
for (let i = 0; i < outputLayoutData.parsedString.length; ++i) {
|
|
drawTextPosX = startPosition.x;
|
|
drawTextPosY = startPosition.y + i * lineHeight;
|
|
// multiple lines need to be drawn outline and fill text
|
|
if (isMultiple) {
|
|
if (style.isOutlined) {
|
|
this._context!.strokeText(outputLayoutData.parsedString[i], drawTextPosX, drawTextPosY);
|
|
}
|
|
this._context!.fillText(outputLayoutData.parsedString[i], drawTextPosX, drawTextPosY);
|
|
}
|
|
|
|
// draw underline
|
|
if (style.isUnderline) {
|
|
const _drawUnderlineWidth = measureText(outputLayoutData.parsedString[i]);
|
|
const _drawUnderlinePos = new Vec2();
|
|
if (layout.horizontalAlign === HorizontalTextAlignment.RIGHT) {
|
|
_drawUnderlinePos.x = startPosition.x - _drawUnderlineWidth;
|
|
} else if (layout.horizontalAlign === HorizontalTextAlignment.CENTER) {
|
|
_drawUnderlinePos.x = startPosition.x - (_drawUnderlineWidth / 2);
|
|
} else {
|
|
_drawUnderlinePos.x = startPosition.x;
|
|
}
|
|
_drawUnderlinePos.y = drawTextPosY + style.actualFontSize / 8;
|
|
this._context!.fillRect(_drawUnderlinePos.x, _drawUnderlinePos.y, _drawUnderlineWidth, style.underlineHeight * this._fontScale);
|
|
}
|
|
}
|
|
|
|
if (isMultiple) {
|
|
this._context!.shadowColor = 'transparent';
|
|
}
|
|
}
|
|
|
|
private _setupOutline (style: TextStyle): void {
|
|
this._context!.strokeStyle = `rgba(${style.outlineColor.r}, ${style.outlineColor.g}, ${style.outlineColor.b}, ${style.outlineColor.a / 255})`;
|
|
this._context!.lineWidth = style.outlineWidth * 2 * this._fontScale;
|
|
}
|
|
|
|
private _setupShadow (style: TextStyle): void {
|
|
const fontScale = this._fontScale;
|
|
this._context!.shadowColor = `rgba(${style.shadowColor.r}, ${style.shadowColor.g}, ${style.shadowColor.b}, ${style.shadowColor.a / 255})`;
|
|
this._context!.shadowBlur = style.shadowBlur * fontScale;
|
|
this._context!.shadowOffsetX = style.shadowOffsetX * fontScale;
|
|
this._context!.shadowOffsetY = -style.shadowOffsetY * fontScale;
|
|
}
|
|
|
|
// -------------------- Render Processing Part --------------------------
|
|
|
|
private generateVertexData (isBmFont: boolean, style: TextStyle, layout: TextLayout, outputLayoutData: TextOutputLayoutData,
|
|
outputRenderData: TextOutputRenderData, inputString: string, callback: AnyFunction): void {
|
|
if (!isBmFont) {
|
|
this.updateQuatCount(outputRenderData); // update vbBuffer count
|
|
callback(style, outputLayoutData, outputRenderData);
|
|
} else {
|
|
this._updateQuads(style, layout, outputLayoutData, outputRenderData, inputString, callback);
|
|
}
|
|
}
|
|
|
|
private updateQuatCount (outputRenderData: TextOutputRenderData): void {
|
|
const data: IRenderData[] = outputRenderData.vertexBuffer;
|
|
const count = outputRenderData.quadCount;
|
|
if (data.length !== count) {
|
|
for (let i = data.length; i < count; i++) {
|
|
data.push({
|
|
x: 0,
|
|
y: 0,
|
|
z: 0,
|
|
u: 0,
|
|
v: 0,
|
|
color: Color.WHITE.clone(),
|
|
});
|
|
}
|
|
|
|
data.length = count;
|
|
}
|
|
}
|
|
|
|
// -------------------- Canvas Mode Part ---------------------------
|
|
// -------------------- Multiple Quad Mode Part --------------------
|
|
|
|
private _setupBMFontOverflowMetrics (layout: TextLayout, outputLayoutData: TextOutputLayoutData): void {
|
|
let newWidth = outputLayoutData.nodeContentSize.width;
|
|
let newHeight = outputLayoutData.nodeContentSize.height;
|
|
|
|
if (layout.overFlow === Overflow.RESIZE_HEIGHT) {
|
|
newHeight = 0;
|
|
}
|
|
|
|
if (layout.overFlow === Overflow.NONE) {
|
|
newWidth = 0;
|
|
newHeight = 0;
|
|
}
|
|
|
|
layout.textWidthTemp = newWidth;
|
|
layout.textHeightTemp = newHeight;
|
|
layout.textDimensions.width = newWidth;
|
|
layout.textDimensions.height = newHeight;
|
|
layout.maxLineWidth = newWidth;
|
|
}
|
|
|
|
private _updateFontScale (style: TextStyle): void {
|
|
style.bmfontScale = style.actualFontSize / (style.originFontSize * this._fontScale);
|
|
}
|
|
|
|
private _computeHorizontalKerningForText (style: TextStyle, layout: TextLayout, inputString: string): void {
|
|
const string = inputString;
|
|
const stringLen = string.length;
|
|
if (!style.fntConfig) return; // for char
|
|
|
|
const kerningDict = style.fntConfig.kerningDict;
|
|
const horizontalKerning = layout.horizontalKerning;
|
|
|
|
if (!kerningDict || kerningDict.length === 0) {
|
|
return;
|
|
}
|
|
|
|
let prev = -1;
|
|
for (let i = 0; i < stringLen; ++i) {
|
|
const key = string.charCodeAt(i);
|
|
const kerningAmount = kerningDict[(prev << 16) | (key & 0xffff)] || 0;
|
|
if (i < stringLen - 1) {
|
|
horizontalKerning[i] = kerningAmount;
|
|
} else {
|
|
horizontalKerning[i] = 0;
|
|
}
|
|
prev = key;
|
|
}
|
|
}
|
|
|
|
private _alignText (style: TextStyle, layout: TextLayout, outputLayoutData: TextOutputLayoutData, inputString: string): void {
|
|
this._multilineTextWrap(style, layout, outputLayoutData, inputString, this._getFirstWordLen);
|
|
|
|
// shrink
|
|
if (layout.overFlow === Overflow.SHRINK) {
|
|
if (style.fontSize > 0 && this._isVerticalClamp(style, layout, outputLayoutData, inputString, this)) {
|
|
this._shrinkLabelToContentSize(style, layout, outputLayoutData, inputString, this._isVerticalClamp);
|
|
}
|
|
if (style.fontSize > 0 && this._isHorizontalNeedShrink(layout, outputLayoutData)) {
|
|
this._shrinkLabelToContentSize(style, layout, outputLayoutData, inputString, this._isHorizontalClamp);
|
|
}
|
|
}
|
|
this._parsedString(outputLayoutData, inputString);
|
|
}
|
|
|
|
private _parsedString (outputLayoutData: TextOutputLayoutData, inputString: string): void {
|
|
let _splitStrings: string[] = [];
|
|
let textFragment = '';
|
|
|
|
for (let i = 0, line = 0, l = inputString.length; i < l; ++i) {
|
|
const letterInfo = this._lettersInfo[i];
|
|
if (!letterInfo.valid) { continue; }
|
|
if (line === letterInfo.line) {
|
|
textFragment += letterInfo.char;
|
|
} else {
|
|
_splitStrings = _splitStrings.concat(textFragment);
|
|
line = letterInfo.line;
|
|
textFragment = '';
|
|
}
|
|
}
|
|
_splitStrings = _splitStrings.concat(textFragment);
|
|
outputLayoutData.parsedString = _splitStrings;
|
|
}
|
|
|
|
private _multilineTextWrap (style: TextStyle, layout: TextLayout, outputLayoutData: TextOutputLayoutData,
|
|
inputString: string, nextTokenFunc: (arg0: TextStyle, arg1: TextLayout, arg2: string, arg3: number, arg4: number) => number): boolean {
|
|
layout.linesWidth.length = 0;
|
|
|
|
const _string = inputString;
|
|
const textLen = _string.length;
|
|
|
|
let lineIndex = 0;
|
|
let nextTokenX = 0;
|
|
let nextTokenY = 0;
|
|
let longestLine = 0;
|
|
let letterRight = 0;
|
|
|
|
let highestY = 0;
|
|
let lowestY = 0;
|
|
let letterDef: FontLetterDefinition | null = null;
|
|
|
|
const _lineSpacing = 0; // use less?
|
|
|
|
for (let index = 0; index < textLen;) {
|
|
let character = _string.charAt(index);
|
|
if (character === '\n') {
|
|
layout.linesWidth.push(letterRight);
|
|
letterRight = 0;
|
|
lineIndex++;
|
|
nextTokenX = 0;
|
|
nextTokenY -= layout.lineHeight * this._getFontScale(style, layout) + _lineSpacing;
|
|
this._recordPlaceholderInfo(index, character);
|
|
index++;
|
|
continue;
|
|
}
|
|
|
|
const tokenLen = nextTokenFunc(style, layout, _string, index, textLen);
|
|
let tokenHighestY = highestY;
|
|
let tokenLowestY = lowestY;
|
|
let tokenRight = letterRight;
|
|
let nextLetterX = nextTokenX;
|
|
let newLine = false;
|
|
const letterPosition = new Vec2();
|
|
|
|
for (let tmp = 0; tmp < tokenLen; ++tmp) {
|
|
const letterIndex = index + tmp;
|
|
character = _string.charAt(letterIndex);
|
|
if (character === '\r') {
|
|
this._recordPlaceholderInfo(letterIndex, character);
|
|
continue;
|
|
}
|
|
letterDef = shareLabelInfo.fontAtlas!.getLetterDefinitionForChar(character, shareLabelInfo);
|
|
if (!letterDef) {
|
|
this._recordPlaceholderInfo(letterIndex, character);
|
|
log(`Can't find letter definition in texture atlas ${
|
|
style.fntConfig!.atlasName} for letter:${character}`);
|
|
continue;
|
|
}
|
|
|
|
const letterX = nextLetterX + letterDef.offsetX * style.bmfontScale - shareLabelInfo.margin;
|
|
|
|
if (layout.wrapping
|
|
&& layout.maxLineWidth > 0
|
|
&& nextTokenX > 0
|
|
&& letterX + letterDef.w * style.bmfontScale > layout.maxLineWidth
|
|
&& !isUnicodeSpace(character)) {
|
|
layout.linesWidth.push(letterRight);
|
|
letterRight = 0;
|
|
lineIndex++;
|
|
nextTokenX = 0;
|
|
nextTokenY -= (layout.lineHeight * this._getFontScale(style, layout) + _lineSpacing);
|
|
newLine = true;
|
|
break;
|
|
} else {
|
|
letterPosition.x = letterX;
|
|
}
|
|
|
|
letterPosition.y = nextTokenY - letterDef.offsetY * style.bmfontScale;
|
|
this._recordLetterInfo(letterPosition, character, letterIndex, lineIndex);
|
|
|
|
if (letterIndex + 1 < layout.horizontalKerning.length && letterIndex < textLen - 1) {
|
|
nextLetterX += layout.horizontalKerning[letterIndex + 1] * style.bmfontScale;
|
|
}
|
|
|
|
nextLetterX += letterDef.xAdvance * style.bmfontScale + layout.spacingX;
|
|
|
|
tokenRight = letterPosition.x + letterDef.w * style.bmfontScale;
|
|
|
|
if (tokenHighestY < letterPosition.y) {
|
|
tokenHighestY = letterPosition.y;
|
|
}
|
|
|
|
if (tokenLowestY > letterPosition.y - letterDef.h * style.bmfontScale) {
|
|
tokenLowestY = letterPosition.y - letterDef.h * style.bmfontScale;
|
|
}
|
|
} // end of for loop
|
|
|
|
if (newLine) { continue; }
|
|
|
|
nextTokenX = nextLetterX;
|
|
letterRight = tokenRight;
|
|
|
|
if (highestY < tokenHighestY) {
|
|
highestY = tokenHighestY;
|
|
}
|
|
if (lowestY > tokenLowestY) {
|
|
lowestY = tokenLowestY;
|
|
}
|
|
if (longestLine < letterRight) {
|
|
longestLine = letterRight;
|
|
}
|
|
|
|
index += tokenLen;
|
|
} // end of for loop
|
|
|
|
layout.linesWidth.push(letterRight);
|
|
|
|
layout.numberOfLines = lineIndex + 1;
|
|
layout.textDesiredHeight = layout.numberOfLines * layout.lineHeight * this._getFontScale(style, layout);
|
|
if (layout.numberOfLines > 1) {
|
|
layout.textDesiredHeight += (layout.numberOfLines - 1) * _lineSpacing;
|
|
}
|
|
|
|
outputLayoutData.nodeContentSize.width = layout.textWidthTemp;
|
|
outputLayoutData.nodeContentSize.height = layout.textHeightTemp;
|
|
if (layout.textWidthTemp <= 0) {
|
|
outputLayoutData.nodeContentSize.width = parseFloat(longestLine.toFixed(2)) + shareLabelInfo.margin * 2;
|
|
}
|
|
if (layout.textHeightTemp <= 0) {
|
|
outputLayoutData.nodeContentSize.height = parseFloat(layout.textDesiredHeight.toFixed(2)) + shareLabelInfo.margin * 2;
|
|
}
|
|
|
|
layout.tailoredTopY = outputLayoutData.nodeContentSize.height;
|
|
layout.tailoredBottomY = 0;
|
|
if (highestY > 0) {
|
|
layout.tailoredTopY = outputLayoutData.nodeContentSize.height + highestY;
|
|
}
|
|
if (lowestY < -layout.textDesiredHeight) {
|
|
layout.tailoredBottomY = layout.textDesiredHeight + lowestY;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private _recordPlaceholderInfo (letterIndex: number, char: string): void {
|
|
if (letterIndex >= this._lettersInfo.length) {
|
|
const tmpInfo = new LetterInfo();
|
|
this._lettersInfo.push(tmpInfo);
|
|
}
|
|
|
|
this._lettersInfo[letterIndex].char = char;
|
|
this._lettersInfo[letterIndex].hash = `${char.charCodeAt(0)}${shareLabelInfo.hash}`;
|
|
this._lettersInfo[letterIndex].valid = false;
|
|
}
|
|
|
|
private _recordLetterInfo (letterPosition: Vec2, character: string, letterIndex: number, lineIndex: number): void {
|
|
if (letterIndex >= this._lettersInfo.length) {
|
|
const tmpInfo = new LetterInfo();
|
|
this._lettersInfo.push(tmpInfo);
|
|
}
|
|
|
|
const char = character.charCodeAt(0);
|
|
const key = `${char}${shareLabelInfo.hash}`;
|
|
|
|
this._lettersInfo[letterIndex].line = lineIndex;
|
|
this._lettersInfo[letterIndex].char = character;
|
|
this._lettersInfo[letterIndex].hash = key;
|
|
this._lettersInfo[letterIndex].valid = shareLabelInfo.fontAtlas!.getLetter(key).valid;
|
|
this._lettersInfo[letterIndex].x = letterPosition.x;
|
|
this._lettersInfo[letterIndex].y = letterPosition.y;
|
|
}
|
|
|
|
private _getFirstWordLen (style: TextStyle, layout: TextLayout, text: string, startIndex: number, textLen: number): number {
|
|
let character = text.charAt(startIndex);
|
|
if (isUnicodeCJK(character)
|
|
|| character === '\n'
|
|
|| isUnicodeSpace(character)) {
|
|
return 1;
|
|
}
|
|
|
|
let len = 1;
|
|
let letterDef = shareLabelInfo.fontAtlas!.getLetterDefinitionForChar(character, shareLabelInfo);
|
|
if (!letterDef) {
|
|
return len;
|
|
}
|
|
let nextLetterX = letterDef.xAdvance * style.bmfontScale + layout.spacingX;
|
|
let letterX = 0;
|
|
for (let index = startIndex + 1; index < textLen; ++index) {
|
|
character = text.charAt(index);
|
|
|
|
letterDef = shareLabelInfo.fontAtlas!.getLetterDefinitionForChar(character, shareLabelInfo);
|
|
if (!letterDef) {
|
|
break;
|
|
}
|
|
letterX = nextLetterX + letterDef.offsetX * style.bmfontScale;
|
|
|
|
if (letterX + letterDef.w * style.bmfontScale > layout.maxLineWidth
|
|
&& !isUnicodeSpace(character)
|
|
&& layout.maxLineWidth > 0) {
|
|
return len;
|
|
}
|
|
nextLetterX += letterDef.xAdvance * style.bmfontScale + layout.spacingX;
|
|
if (character === '\n'
|
|
|| isUnicodeSpace(character)
|
|
|| isUnicodeCJK(character)) {
|
|
break;
|
|
}
|
|
len++;
|
|
}
|
|
|
|
return len;
|
|
}
|
|
|
|
private _computeAlignmentOffset (style: TextStyle, layout: TextLayout, outputLayoutData: TextOutputLayoutData): void {
|
|
layout.linesOffsetX.length = 0;
|
|
layout.letterOffsetY = 0;
|
|
|
|
switch (layout.horizontalAlign) {
|
|
case HorizontalTextAlignment.LEFT:
|
|
for (let i = 0; i < layout.numberOfLines; ++i) {
|
|
layout.linesOffsetX.push(0);
|
|
}
|
|
break;
|
|
case HorizontalTextAlignment.CENTER:
|
|
for (let i = 0, l = layout.linesWidth.length; i < l; i++) {
|
|
layout.linesOffsetX.push((outputLayoutData.nodeContentSize.width - layout.linesWidth[i]) / 2);
|
|
}
|
|
break;
|
|
case HorizontalTextAlignment.RIGHT:
|
|
for (let i = 0, l = layout.linesWidth.length; i < l; i++) {
|
|
layout.linesOffsetX.push(outputLayoutData.nodeContentSize.width - layout.linesWidth[i]);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// TOP
|
|
layout.letterOffsetY = outputLayoutData.nodeContentSize.height;
|
|
if (layout.verticalAlign !== VerticalTextAlignment.TOP) {
|
|
const blank = outputLayoutData.nodeContentSize.height - layout.textDesiredHeight
|
|
+ layout.lineHeight * this._getFontScale(style, layout) - style.originFontSize * this._fontScale * style.bmfontScale;
|
|
if (layout.verticalAlign === VerticalTextAlignment.BOTTOM) {
|
|
// BOTTOM
|
|
layout.letterOffsetY -= blank;
|
|
} else {
|
|
// CENTER:
|
|
layout.letterOffsetY -= blank / 2;
|
|
}
|
|
}
|
|
}
|
|
|
|
private _getFontScale (style: TextStyle, layout: TextLayout): number {
|
|
return layout.overFlow === Overflow.SHRINK ? style.bmfontScale : 1;
|
|
}
|
|
|
|
private _isVerticalClamp (style: TextStyle, layout: TextLayout, outputLayoutData: TextOutputLayoutData,
|
|
inputString: string, process: TextProcessing): boolean {
|
|
if (layout.textDesiredHeight > outputLayoutData.nodeContentSize.height) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private _isHorizontalClamp (style: TextStyle, layout: TextLayout, outputLayoutData: TextOutputLayoutData,
|
|
inputString: string, process: TextProcessing): boolean {
|
|
let letterClamp = false;
|
|
const _string = inputString;
|
|
for (let ctr = 0, l = _string.length; ctr < l; ++ctr) {
|
|
const letterInfo = process._lettersInfo[ctr];
|
|
if (letterInfo.valid) {
|
|
const letterDef = shareLabelInfo.fontAtlas!.getLetterDefinitionForChar(letterInfo.char, shareLabelInfo);
|
|
if (!letterDef) {
|
|
continue;
|
|
}
|
|
|
|
const px = letterInfo.x + letterDef.w * style.bmfontScale;
|
|
const lineIndex = letterInfo.line;
|
|
if (layout.textWidthTemp > 0) {
|
|
if (!layout.wrapping) {
|
|
if (px > outputLayoutData.nodeContentSize.width) {
|
|
letterClamp = true;
|
|
break;
|
|
}
|
|
} else {
|
|
const wordWidth = layout.linesWidth[lineIndex];
|
|
if (wordWidth > outputLayoutData.nodeContentSize.width && (px > outputLayoutData.nodeContentSize.width || px < 0)) {
|
|
letterClamp = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return letterClamp;
|
|
}
|
|
|
|
private _isHorizontalNeedShrink (layout: TextLayout, outputLayoutData: TextOutputLayoutData): boolean {
|
|
let wordWidth = 0;
|
|
for (let ctr = 0, l = layout.linesWidth.length; ctr < l; ++ctr) {
|
|
wordWidth = layout.linesWidth[ctr];
|
|
if (wordWidth > outputLayoutData.nodeContentSize.width) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private _shrinkLabelToContentSize (style: TextStyle, layout: TextLayout, outputLayoutData: TextOutputLayoutData, inputString: string,
|
|
lambda: (style: TextStyle, layout: TextLayout, outputLayoutData: TextOutputLayoutData,
|
|
inputString: string, process: TextProcessing) => boolean): void {
|
|
const fontSize = style.actualFontSize;
|
|
|
|
let left = 0;
|
|
let right = fontSize | 0;
|
|
let mid = 0;
|
|
while (left < right) {
|
|
mid = (left + right + 1) >> 1;
|
|
|
|
const newFontSize = mid;
|
|
if (newFontSize <= 0) {
|
|
break;
|
|
}
|
|
|
|
style.bmfontScale = newFontSize / (style.originFontSize * this._fontScale);
|
|
|
|
this._multilineTextWrap(style, layout, outputLayoutData, inputString, this._getFirstWordLen);
|
|
|
|
this._computeAlignmentOffset(style, layout, outputLayoutData);
|
|
|
|
if (lambda(style, layout, outputLayoutData, inputString, this)) {
|
|
right = mid - 1;
|
|
} else {
|
|
left = mid;
|
|
}
|
|
}
|
|
|
|
if (left >= 0) {
|
|
this._scaleFontSizeDown(style, layout, outputLayoutData, inputString, left);
|
|
}
|
|
}
|
|
|
|
private _scaleFontSizeDown (style: TextStyle, layout: TextLayout, outputLayoutData: TextOutputLayoutData, inputString: string, fontSize: number): void {
|
|
let shouldUpdateContent = true;
|
|
if (!fontSize) {
|
|
fontSize = 0.1;
|
|
shouldUpdateContent = false;
|
|
}
|
|
style.actualFontSize = fontSize;
|
|
|
|
if (shouldUpdateContent) {
|
|
this._updateFontScale(style);
|
|
this._multilineTextWrap(style, layout, outputLayoutData, inputString, this._getFirstWordLen);
|
|
}
|
|
}
|
|
|
|
private _updateQuads (style: TextStyle, layout: TextLayout, outputLayoutData: TextOutputLayoutData,
|
|
outputRenderData: TextOutputRenderData, inputString: string, callback): boolean {
|
|
const texture = style.spriteFrame ? style.spriteFrame.texture : shareLabelInfo.fontAtlas!.getTexture();
|
|
|
|
const appX = outputRenderData.uiTransAnchorX * outputLayoutData.nodeContentSize.width;
|
|
const appY = outputRenderData.uiTransAnchorY * outputLayoutData.nodeContentSize.height;
|
|
|
|
const ret = true;
|
|
for (let ctr = 0, l = inputString.length; ctr < l; ++ctr) {
|
|
const letterInfo = this._lettersInfo[ctr];
|
|
if (!letterInfo.valid) { continue; }
|
|
const letterDef = shareLabelInfo.fontAtlas!.getLetter(letterInfo.hash);
|
|
if (!letterDef) {
|
|
warn('Can\'t find letter in this bitmap-font');
|
|
continue;
|
|
}
|
|
|
|
this._tmpRect.height = letterDef.h;
|
|
this._tmpRect.width = letterDef.w;
|
|
this._tmpRect.x = letterDef.u;
|
|
this._tmpRect.y = letterDef.v;
|
|
|
|
let py = letterInfo.y + layout.letterOffsetY;
|
|
|
|
if (layout.textHeightTemp > 0) {
|
|
if (py > layout.tailoredTopY) {
|
|
const clipTop = py - layout.tailoredTopY;
|
|
this._tmpRect.y += clipTop;
|
|
this._tmpRect.height -= clipTop;
|
|
py -= clipTop;
|
|
}
|
|
|
|
if ((py - this._tmpRect.height * style.bmfontScale < layout.tailoredBottomY) && layout.overFlow === Overflow.CLAMP) {
|
|
this._tmpRect.height = (py < layout.tailoredBottomY) ? 0 : (py - layout.tailoredBottomY) / style.bmfontScale;
|
|
}
|
|
}
|
|
|
|
const lineIndex = letterInfo.line;
|
|
const px = letterInfo.x + letterDef.w / 2 * style.bmfontScale + layout.linesOffsetX[lineIndex];
|
|
|
|
if (layout.textWidthTemp > 0) {
|
|
if (this._isHorizontalClamped(layout, outputLayoutData, px, lineIndex)) {
|
|
if (layout.overFlow === Overflow.CLAMP) {
|
|
this._tmpRect.width = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this._tmpRect.height > 0 && this._tmpRect.width > 0) {
|
|
const isRotated = this._determineRect(style);
|
|
const letterPositionX = letterInfo.x + layout.linesOffsetX[letterInfo.line];
|
|
const offset = outputRenderData.quadCount;
|
|
outputRenderData.quadCount += 4; // Hard Code
|
|
this.updateQuatCount(outputRenderData);
|
|
callback(style, outputLayoutData, outputRenderData, offset, texture, this._tmpRect, isRotated, letterPositionX - appX, py - appY);
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
private _isHorizontalClamped (layout: TextLayout, outputLayoutData: TextOutputLayoutData, px: number, lineIndex: number): boolean {
|
|
const wordWidth = layout.linesWidth[lineIndex];
|
|
const letterOverClamp = (px > outputLayoutData.nodeContentSize.width || px < 0);
|
|
|
|
if (!layout.wrapping) {
|
|
return letterOverClamp;
|
|
} else {
|
|
return (wordWidth > outputLayoutData.nodeContentSize.width && letterOverClamp);
|
|
}
|
|
}
|
|
|
|
private _determineRect (style: TextStyle): boolean {
|
|
const _spriteFrame = style.spriteFrame;
|
|
if (!_spriteFrame) return false; // for char mode
|
|
const isRotated = _spriteFrame.isRotated();
|
|
|
|
const originalSize = _spriteFrame.getOriginalSize();
|
|
const rect = _spriteFrame.getRect();
|
|
const offset = _spriteFrame.getOffset();
|
|
const trimmedLeft = offset.x + (originalSize.width - rect.width) / 2;
|
|
const trimmedTop = offset.y - (originalSize.height - rect.height) / 2;
|
|
|
|
if (!isRotated) {
|
|
this._tmpRect.x += (rect.x - trimmedLeft);
|
|
this._tmpRect.y += (rect.y + trimmedTop);
|
|
} else {
|
|
const originalX = this._tmpRect.x;
|
|
this._tmpRect.x = rect.x + rect.height - this._tmpRect.y - this._tmpRect.height - trimmedTop;
|
|
this._tmpRect.y = originalX + rect.y - trimmedLeft;
|
|
if (this._tmpRect.y < 0) {
|
|
this._tmpRect.height += trimmedTop;
|
|
}
|
|
}
|
|
|
|
return isRotated;
|
|
}
|
|
|
|
// -------------------- Multiple Quad Mode Part --------------------
|
|
}
|
|
|
|
TextProcessing.instance = new TextProcessing();
|