325 lines
17 KiB
JavaScript
325 lines
17 KiB
JavaScript
import { Color, ObservablePoint, settings, Point, Texture, utils, BLEND_MODES, Program } from "@pixi/core";
|
|
import { Container } from "@pixi/display";
|
|
import { MeshGeometry, MeshMaterial, Mesh } from "@pixi/mesh";
|
|
import { BitmapFont } from "./BitmapFont.mjs";
|
|
import msdfFrag from "./shader/msdf.frag.mjs";
|
|
import msdfVert from "./shader/msdf.vert.mjs";
|
|
import "./utils/index.mjs";
|
|
import { splitTextToCharacters } from "./utils/splitTextToCharacters.mjs";
|
|
import { extractCharCode } from "./utils/extractCharCode.mjs";
|
|
const pageMeshDataDefaultPageMeshData = [], pageMeshDataMSDFPageMeshData = [], charRenderDataPool = [], _BitmapText = class _BitmapText2 extends Container {
|
|
/**
|
|
* @param text - A string that you would like the text to display.
|
|
* @param style - The style parameters.
|
|
* @param {string} style.fontName - The installed BitmapFont name.
|
|
* @param {number} [style.fontSize] - The size of the font in pixels, e.g. 24. If undefined,
|
|
*. this will default to the BitmapFont size.
|
|
* @param {string} [style.align='left'] - Alignment for multiline text ('left', 'center', 'right' or 'justify'),
|
|
* does not affect single line text.
|
|
* @param {PIXI.ColorSource} [style.tint=0xFFFFFF] - The tint color.
|
|
* @param {number} [style.letterSpacing=0] - The amount of spacing between letters.
|
|
* @param {number} [style.maxWidth=0] - The max width of the text before line wrapping.
|
|
*/
|
|
constructor(text, style = {}) {
|
|
super();
|
|
const { align, tint, maxWidth, letterSpacing, fontName, fontSize } = Object.assign(
|
|
{},
|
|
_BitmapText2.styleDefaults,
|
|
style
|
|
);
|
|
if (!BitmapFont.available[fontName])
|
|
throw new Error(`Missing BitmapFont "${fontName}"`);
|
|
this._activePagesMeshData = [], this._textWidth = 0, this._textHeight = 0, this._align = align, this._tintColor = new Color(tint), this._font = void 0, this._fontName = fontName, this._fontSize = fontSize, this.text = text, this._maxWidth = maxWidth, this._maxLineHeight = 0, this._letterSpacing = letterSpacing, this._anchor = new ObservablePoint(() => {
|
|
this.dirty = !0;
|
|
}, this, 0, 0), this._roundPixels = settings.ROUND_PIXELS, this.dirty = !0, this._resolution = settings.RESOLUTION, this._autoResolution = !0, this._textureCache = {};
|
|
}
|
|
/** Renders text and updates it when needed. This should only be called if the BitmapFont is regenerated. */
|
|
updateText() {
|
|
const data = BitmapFont.available[this._fontName], fontSize = this.fontSize, scale = fontSize / data.size, pos = new Point(), chars = [], lineWidths = [], lineSpaces = [], text = this._text.replace(/(?:\r\n|\r)/g, `
|
|
`) || " ", charsInput = splitTextToCharacters(text), maxWidth = this._maxWidth * data.size / fontSize, pageMeshDataPool = data.distanceFieldType === "none" ? pageMeshDataDefaultPageMeshData : pageMeshDataMSDFPageMeshData;
|
|
let prevCharCode = null, lastLineWidth = 0, maxLineWidth = 0, line = 0, lastBreakPos = -1, lastBreakWidth = 0, spacesRemoved = 0, maxLineHeight = 0, spaceCount = 0;
|
|
for (let i = 0; i < charsInput.length; i++) {
|
|
const char = charsInput[i], charCode = extractCharCode(char);
|
|
if (/(?:\s)/.test(char) && (lastBreakPos = i, lastBreakWidth = lastLineWidth, spaceCount++), char === "\r" || char === `
|
|
`) {
|
|
lineWidths.push(lastLineWidth), lineSpaces.push(-1), maxLineWidth = Math.max(maxLineWidth, lastLineWidth), ++line, ++spacesRemoved, pos.x = 0, pos.y += data.lineHeight, prevCharCode = null, spaceCount = 0;
|
|
continue;
|
|
}
|
|
const charData = data.chars[charCode];
|
|
if (!charData)
|
|
continue;
|
|
prevCharCode && charData.kerning[prevCharCode] && (pos.x += charData.kerning[prevCharCode]);
|
|
const charRenderData = charRenderDataPool.pop() || {
|
|
texture: Texture.EMPTY,
|
|
line: 0,
|
|
charCode: 0,
|
|
prevSpaces: 0,
|
|
position: new Point()
|
|
};
|
|
charRenderData.texture = charData.texture, charRenderData.line = line, charRenderData.charCode = charCode, charRenderData.position.x = Math.round(pos.x + charData.xOffset + this._letterSpacing / 2), charRenderData.position.y = Math.round(pos.y + charData.yOffset), charRenderData.prevSpaces = spaceCount, chars.push(charRenderData), lastLineWidth = charRenderData.position.x + Math.max(charData.xAdvance - charData.xOffset, charData.texture.orig.width), pos.x += charData.xAdvance + this._letterSpacing, maxLineHeight = Math.max(maxLineHeight, charData.yOffset + charData.texture.height), prevCharCode = charCode, lastBreakPos !== -1 && maxWidth > 0 && pos.x > maxWidth && (++spacesRemoved, utils.removeItems(chars, 1 + lastBreakPos - spacesRemoved, 1 + i - lastBreakPos), i = lastBreakPos, lastBreakPos = -1, lineWidths.push(lastBreakWidth), lineSpaces.push(chars.length > 0 ? chars[chars.length - 1].prevSpaces : 0), maxLineWidth = Math.max(maxLineWidth, lastBreakWidth), line++, pos.x = 0, pos.y += data.lineHeight, prevCharCode = null, spaceCount = 0);
|
|
}
|
|
const lastChar = charsInput[charsInput.length - 1];
|
|
lastChar !== "\r" && lastChar !== `
|
|
` && (/(?:\s)/.test(lastChar) && (lastLineWidth = lastBreakWidth), lineWidths.push(lastLineWidth), maxLineWidth = Math.max(maxLineWidth, lastLineWidth), lineSpaces.push(-1));
|
|
const lineAlignOffsets = [];
|
|
for (let i = 0; i <= line; i++) {
|
|
let alignOffset = 0;
|
|
this._align === "right" ? alignOffset = maxLineWidth - lineWidths[i] : this._align === "center" ? alignOffset = (maxLineWidth - lineWidths[i]) / 2 : this._align === "justify" && (alignOffset = lineSpaces[i] < 0 ? 0 : (maxLineWidth - lineWidths[i]) / lineSpaces[i]), lineAlignOffsets.push(alignOffset);
|
|
}
|
|
const lenChars = chars.length, pagesMeshData = {}, newPagesMeshData = [], activePagesMeshData = this._activePagesMeshData;
|
|
pageMeshDataPool.push(...activePagesMeshData);
|
|
for (let i = 0; i < lenChars; i++) {
|
|
const texture = chars[i].texture, baseTextureUid = texture.baseTexture.uid;
|
|
if (!pagesMeshData[baseTextureUid]) {
|
|
let pageMeshData = pageMeshDataPool.pop();
|
|
if (!pageMeshData) {
|
|
const geometry = new MeshGeometry();
|
|
let material, meshBlendMode;
|
|
data.distanceFieldType === "none" ? (material = new MeshMaterial(Texture.EMPTY), meshBlendMode = BLEND_MODES.NORMAL) : (material = new MeshMaterial(
|
|
Texture.EMPTY,
|
|
{ program: Program.from(msdfVert, msdfFrag), uniforms: { uFWidth: 0 } }
|
|
), meshBlendMode = BLEND_MODES.NORMAL_NPM);
|
|
const mesh = new Mesh(geometry, material);
|
|
mesh.blendMode = meshBlendMode, pageMeshData = {
|
|
index: 0,
|
|
indexCount: 0,
|
|
vertexCount: 0,
|
|
uvsCount: 0,
|
|
total: 0,
|
|
mesh,
|
|
vertices: null,
|
|
uvs: null,
|
|
indices: null
|
|
};
|
|
}
|
|
pageMeshData.index = 0, pageMeshData.indexCount = 0, pageMeshData.vertexCount = 0, pageMeshData.uvsCount = 0, pageMeshData.total = 0;
|
|
const { _textureCache } = this;
|
|
_textureCache[baseTextureUid] = _textureCache[baseTextureUid] || new Texture(texture.baseTexture), pageMeshData.mesh.texture = _textureCache[baseTextureUid], pageMeshData.mesh.tint = this._tintColor.value, newPagesMeshData.push(pageMeshData), pagesMeshData[baseTextureUid] = pageMeshData;
|
|
}
|
|
pagesMeshData[baseTextureUid].total++;
|
|
}
|
|
for (let i = 0; i < activePagesMeshData.length; i++)
|
|
newPagesMeshData.includes(activePagesMeshData[i]) || this.removeChild(activePagesMeshData[i].mesh);
|
|
for (let i = 0; i < newPagesMeshData.length; i++)
|
|
newPagesMeshData[i].mesh.parent !== this && this.addChild(newPagesMeshData[i].mesh);
|
|
this._activePagesMeshData = newPagesMeshData;
|
|
for (const i in pagesMeshData) {
|
|
const pageMeshData = pagesMeshData[i], total = pageMeshData.total;
|
|
if (!(pageMeshData.indices?.length > 6 * total) || pageMeshData.vertices.length < Mesh.BATCHABLE_SIZE * 2)
|
|
pageMeshData.vertices = new Float32Array(4 * 2 * total), pageMeshData.uvs = new Float32Array(4 * 2 * total), pageMeshData.indices = new Uint16Array(6 * total);
|
|
else {
|
|
const total2 = pageMeshData.total, vertices = pageMeshData.vertices;
|
|
for (let i2 = total2 * 4 * 2; i2 < vertices.length; i2++)
|
|
vertices[i2] = 0;
|
|
}
|
|
pageMeshData.mesh.size = 6 * total;
|
|
}
|
|
for (let i = 0; i < lenChars; i++) {
|
|
const char = chars[i];
|
|
let offset = char.position.x + lineAlignOffsets[char.line] * (this._align === "justify" ? char.prevSpaces : 1);
|
|
this._roundPixels && (offset = Math.round(offset));
|
|
const xPos = offset * scale, yPos = char.position.y * scale, texture = char.texture, pageMesh = pagesMeshData[texture.baseTexture.uid], textureFrame = texture.frame, textureUvs = texture._uvs, index = pageMesh.index++;
|
|
pageMesh.indices[index * 6 + 0] = 0 + index * 4, pageMesh.indices[index * 6 + 1] = 1 + index * 4, pageMesh.indices[index * 6 + 2] = 2 + index * 4, pageMesh.indices[index * 6 + 3] = 0 + index * 4, pageMesh.indices[index * 6 + 4] = 2 + index * 4, pageMesh.indices[index * 6 + 5] = 3 + index * 4, pageMesh.vertices[index * 8 + 0] = xPos, pageMesh.vertices[index * 8 + 1] = yPos, pageMesh.vertices[index * 8 + 2] = xPos + textureFrame.width * scale, pageMesh.vertices[index * 8 + 3] = yPos, pageMesh.vertices[index * 8 + 4] = xPos + textureFrame.width * scale, pageMesh.vertices[index * 8 + 5] = yPos + textureFrame.height * scale, pageMesh.vertices[index * 8 + 6] = xPos, pageMesh.vertices[index * 8 + 7] = yPos + textureFrame.height * scale, pageMesh.uvs[index * 8 + 0] = textureUvs.x0, pageMesh.uvs[index * 8 + 1] = textureUvs.y0, pageMesh.uvs[index * 8 + 2] = textureUvs.x1, pageMesh.uvs[index * 8 + 3] = textureUvs.y1, pageMesh.uvs[index * 8 + 4] = textureUvs.x2, pageMesh.uvs[index * 8 + 5] = textureUvs.y2, pageMesh.uvs[index * 8 + 6] = textureUvs.x3, pageMesh.uvs[index * 8 + 7] = textureUvs.y3;
|
|
}
|
|
this._textWidth = maxLineWidth * scale, this._textHeight = (pos.y + data.lineHeight) * scale;
|
|
for (const i in pagesMeshData) {
|
|
const pageMeshData = pagesMeshData[i];
|
|
if (this.anchor.x !== 0 || this.anchor.y !== 0) {
|
|
let vertexCount = 0;
|
|
const anchorOffsetX = this._textWidth * this.anchor.x, anchorOffsetY = this._textHeight * this.anchor.y;
|
|
for (let i2 = 0; i2 < pageMeshData.total; i2++)
|
|
pageMeshData.vertices[vertexCount++] -= anchorOffsetX, pageMeshData.vertices[vertexCount++] -= anchorOffsetY, pageMeshData.vertices[vertexCount++] -= anchorOffsetX, pageMeshData.vertices[vertexCount++] -= anchorOffsetY, pageMeshData.vertices[vertexCount++] -= anchorOffsetX, pageMeshData.vertices[vertexCount++] -= anchorOffsetY, pageMeshData.vertices[vertexCount++] -= anchorOffsetX, pageMeshData.vertices[vertexCount++] -= anchorOffsetY;
|
|
}
|
|
this._maxLineHeight = maxLineHeight * scale;
|
|
const vertexBuffer = pageMeshData.mesh.geometry.getBuffer("aVertexPosition"), textureBuffer = pageMeshData.mesh.geometry.getBuffer("aTextureCoord"), indexBuffer = pageMeshData.mesh.geometry.getIndex();
|
|
vertexBuffer.data = pageMeshData.vertices, textureBuffer.data = pageMeshData.uvs, indexBuffer.data = pageMeshData.indices, vertexBuffer.update(), textureBuffer.update(), indexBuffer.update();
|
|
}
|
|
for (let i = 0; i < chars.length; i++)
|
|
charRenderDataPool.push(chars[i]);
|
|
this._font = data, this.dirty = !1;
|
|
}
|
|
updateTransform() {
|
|
this.validate(), this.containerUpdateTransform();
|
|
}
|
|
_render(renderer) {
|
|
this._autoResolution && this._resolution !== renderer.resolution && (this._resolution = renderer.resolution, this.dirty = !0);
|
|
const { distanceFieldRange, distanceFieldType, size } = BitmapFont.available[this._fontName];
|
|
if (distanceFieldType !== "none") {
|
|
const { a, b, c, d } = this.worldTransform, dx = Math.sqrt(a * a + b * b), dy = Math.sqrt(c * c + d * d), worldScale = (Math.abs(dx) + Math.abs(dy)) / 2, fontScale = this.fontSize / size, resolution = renderer._view.resolution;
|
|
for (const mesh of this._activePagesMeshData)
|
|
mesh.mesh.shader.uniforms.uFWidth = worldScale * distanceFieldRange * fontScale * resolution;
|
|
}
|
|
super._render(renderer);
|
|
}
|
|
/**
|
|
* Validates text before calling parent's getLocalBounds
|
|
* @returns - The rectangular bounding area
|
|
*/
|
|
getLocalBounds() {
|
|
return this.validate(), super.getLocalBounds();
|
|
}
|
|
/**
|
|
* Updates text when needed
|
|
* @private
|
|
*/
|
|
validate() {
|
|
const font = BitmapFont.available[this._fontName];
|
|
if (!font)
|
|
throw new Error(`Missing BitmapFont "${this._fontName}"`);
|
|
this._font !== font && (this.dirty = !0), this.dirty && this.updateText();
|
|
}
|
|
/**
|
|
* The tint of the BitmapText object.
|
|
* @default 0xffffff
|
|
*/
|
|
get tint() {
|
|
return this._tintColor.value;
|
|
}
|
|
set tint(value) {
|
|
if (this.tint !== value) {
|
|
this._tintColor.setValue(value);
|
|
for (let i = 0; i < this._activePagesMeshData.length; i++)
|
|
this._activePagesMeshData[i].mesh.tint = value;
|
|
}
|
|
}
|
|
/**
|
|
* The alignment of the BitmapText object.
|
|
* @member {string}
|
|
* @default 'left'
|
|
*/
|
|
get align() {
|
|
return this._align;
|
|
}
|
|
set align(value) {
|
|
this._align !== value && (this._align = value, this.dirty = !0);
|
|
}
|
|
/** The name of the BitmapFont. */
|
|
get fontName() {
|
|
return this._fontName;
|
|
}
|
|
set fontName(value) {
|
|
if (!BitmapFont.available[value])
|
|
throw new Error(`Missing BitmapFont "${value}"`);
|
|
this._fontName !== value && (this._fontName = value, this.dirty = !0);
|
|
}
|
|
/** The size of the font to display. */
|
|
get fontSize() {
|
|
return this._fontSize ?? BitmapFont.available[this._fontName].size;
|
|
}
|
|
set fontSize(value) {
|
|
this._fontSize !== value && (this._fontSize = value, this.dirty = !0);
|
|
}
|
|
/**
|
|
* The anchor sets the origin point of the text.
|
|
*
|
|
* The default is `(0,0)`, this means the text's origin is the top left.
|
|
*
|
|
* Setting the anchor to `(0.5,0.5)` means the text's origin is centered.
|
|
*
|
|
* Setting the anchor to `(1,1)` would mean the text's origin point will be the bottom right corner.
|
|
*/
|
|
get anchor() {
|
|
return this._anchor;
|
|
}
|
|
set anchor(value) {
|
|
typeof value == "number" ? this._anchor.set(value) : this._anchor.copyFrom(value);
|
|
}
|
|
/** The text of the BitmapText object. */
|
|
get text() {
|
|
return this._text;
|
|
}
|
|
set text(text) {
|
|
text = String(text ?? ""), this._text !== text && (this._text = text, this.dirty = !0);
|
|
}
|
|
/**
|
|
* The max width of this bitmap text in pixels. If the text provided is longer than the
|
|
* value provided, line breaks will be automatically inserted in the last whitespace.
|
|
* Disable by setting the value to 0.
|
|
*/
|
|
get maxWidth() {
|
|
return this._maxWidth;
|
|
}
|
|
set maxWidth(value) {
|
|
this._maxWidth !== value && (this._maxWidth = value, this.dirty = !0);
|
|
}
|
|
/**
|
|
* The max line height. This is useful when trying to use the total height of the Text,
|
|
* i.e. when trying to vertically align.
|
|
* @readonly
|
|
*/
|
|
get maxLineHeight() {
|
|
return this.validate(), this._maxLineHeight;
|
|
}
|
|
/**
|
|
* The width of the overall text, different from fontSize,
|
|
* which is defined in the style object.
|
|
* @readonly
|
|
*/
|
|
get textWidth() {
|
|
return this.validate(), this._textWidth;
|
|
}
|
|
/** Additional space between characters. */
|
|
get letterSpacing() {
|
|
return this._letterSpacing;
|
|
}
|
|
set letterSpacing(value) {
|
|
this._letterSpacing !== value && (this._letterSpacing = value, this.dirty = !0);
|
|
}
|
|
/**
|
|
* If true PixiJS will Math.floor() x/y values when rendering, stopping pixel interpolation.
|
|
* Advantages can include sharper image quality (like text) and faster rendering on canvas.
|
|
* The main disadvantage is movement of objects may appear less smooth.
|
|
* To set the global default, change {@link PIXI.settings.ROUND_PIXELS}
|
|
* @default PIXI.settings.ROUND_PIXELS
|
|
*/
|
|
get roundPixels() {
|
|
return this._roundPixels;
|
|
}
|
|
set roundPixels(value) {
|
|
value !== this._roundPixels && (this._roundPixels = value, this.dirty = !0);
|
|
}
|
|
/**
|
|
* The height of the overall text, different from fontSize,
|
|
* which is defined in the style object.
|
|
* @readonly
|
|
*/
|
|
get textHeight() {
|
|
return this.validate(), this._textHeight;
|
|
}
|
|
/**
|
|
* The resolution / device pixel ratio of the canvas.
|
|
*
|
|
* This is set to automatically match the renderer resolution by default, but can be overridden by setting manually.
|
|
* @default 1
|
|
*/
|
|
get resolution() {
|
|
return this._resolution;
|
|
}
|
|
set resolution(value) {
|
|
this._autoResolution = !1, this._resolution !== value && (this._resolution = value, this.dirty = !0);
|
|
}
|
|
destroy(options) {
|
|
const { _textureCache } = this, pageMeshDataPool = BitmapFont.available[this._fontName].distanceFieldType === "none" ? pageMeshDataDefaultPageMeshData : pageMeshDataMSDFPageMeshData;
|
|
pageMeshDataPool.push(...this._activePagesMeshData);
|
|
for (const pageMeshData of this._activePagesMeshData)
|
|
this.removeChild(pageMeshData.mesh);
|
|
this._activePagesMeshData = [], pageMeshDataPool.filter((page) => _textureCache[page.mesh.texture.baseTexture.uid]).forEach((page) => {
|
|
page.mesh.texture = Texture.EMPTY;
|
|
});
|
|
for (const id in _textureCache)
|
|
_textureCache[id].destroy(), delete _textureCache[id];
|
|
this._font = null, this._tintColor = null, this._textureCache = null, super.destroy(options);
|
|
}
|
|
};
|
|
_BitmapText.styleDefaults = {
|
|
align: "left",
|
|
tint: 16777215,
|
|
maxWidth: 0,
|
|
letterSpacing: 0
|
|
};
|
|
let BitmapText = _BitmapText;
|
|
export {
|
|
BitmapText
|
|
};
|
|
//# sourceMappingURL=BitmapText.mjs.map
|