@awayjs/scene
Version:
AwayJS scene classes
1,182 lines (962 loc) • 35 kB
text/typescript
import { AssetBase, Point, Rectangle, ColorTransform, ColorUtils } from '@awayjs/core';
import { AttributesBuffer, Float2Attributes, AttributesView, BitmapImage2D, Stage } from '@awayjs/stage';
import { Style, TriangleElements } from '@awayjs/renderer';
import { GraphicsPath, Shape, GraphicsFactoryFills, GraphicsFactoryHelper, MaterialManager, GraphicsPathCommand, SolidFillStyle, TextureAtlas } from '@awayjs/graphics';
import { TesselatedFontChar } from './TesselatedFontChar';
import { IFontTable } from './IFontTable';
import { TextFormat } from './TextFormat';
import { TextShape } from './TextShape';
import { TextField } from '../display/TextField';
import { Settings } from '../Settings';
import { FNTGeneratorCanvas } from './FNTGeneratorCanvas';
const ONCE_EMIT_TABLE: StringMap<boolean> = Object.create(null);
function once(obj: any, key = '') {
const has = ONCE_EMIT_TABLE[obj._id + key];
ONCE_EMIT_TABLE[obj._id + key] = true;
return !has;
}
interface ICharEntry {
x: number;
y: number;
char: TesselatedFontChar;
selected?: boolean;
shape?: TextShape;
}
/**
* GraphicBase wraps a TriangleElements as a scene graph instantiation. A GraphicBase is owned by a Sprite object.
*
*
* @see away.base.TriangleElements
* @see away.entities.Sprite
*
* @class away.base.GraphicBase
*/
export class TesselatedFontTable extends AssetBase implements IFontTable {
public static assetType: string = '[asset TesselatedFontTable]';
public static DEFAULT_SPACE = 14;
public font: any;
/*internal*/ _font_chars_dic: StringMap<TesselatedFontChar> = {};
/*internal*/ _current_size: number = 0;
/*internal*/ _size_multiply: number = 0;
private _font_chars: TesselatedFontChar[] = [];
// default size
private _font_em_size: number = 32;
private _whitespace_width: number;
private _ascent: number = 0;
private _descent: number = 0;
private _usesCurves: boolean = false;
private _opentype_font: any;
private _glyphIdxToChar: Record<number, TesselatedFontChar> = {};
private _fntSizeLimit: number = -1;
private _fnt_channels: BitmapImage2D[] = [];
/**
* Creates a new TesselatedFont object
* If a opentype_font object is passed, the chars will get tessellated whenever requested.
* If no opentype font object is passed, it is expected that tesselated chars
*/
constructor(opentype_font: any = null) {
super();
if (opentype_font) {
this._opentype_font = opentype_font;
/*
console.log("head.yMax ",opentype_font);
console.log("head.yMax "+opentype_font.tables.head.yMax);
console.log("head.yMin "+opentype_font.tables.head.yMin);
console.log("font.numGlyphs "+opentype_font.numGlyphs);
console.log('Ascender', opentype_font.tables.hhea.ascender);
console.log('Descender', opentype_font.tables.hhea.descender);
console.log('Typo Ascender', opentype_font.tables.os2.sTypoAscender);
console.log('Typo Descender', opentype_font.tables.os2.sTypoDescender);
*/
this._font_em_size = 72;//2048;
this._ascent = opentype_font.tables.hhea.ascender / (this._opentype_font.unitsPerEm / 72);
this._descent = -2 + opentype_font.tables.hhea.descender / (this._opentype_font.unitsPerEm / 72);
this._current_size = 0;
this._size_multiply = 0;
const thisGlyph = this._opentype_font.charToGlyph(String.fromCharCode(32));
if (thisGlyph) {
//this._whitespace_width = thisGlyph.advanceWidth / (this._opentype_font.unitsPerEm / 72);
}
return;
}
}
public addFNTChannel(mat: BitmapImage2D) {
this._fnt_channels.push(mat);
}
public getGlyphCount(): number {
return this._font_chars.length;
}
public generateFNTData (stage: Stage): void {
// already exist
if (this._fntSizeLimit > 0) {
return;
}
const generator = new FNTGeneratorCanvas(stage, false);
const bitmaps = generator.generate(this.font, this._font_em_size, this._current_size, 4);
if (bitmaps && bitmaps.length > 0) {
/**
* Random value, anyway we render real font dimension ;)
*/
this._fntSizeLimit = this._current_size * 4;
}
}
public generateFNTTextures(padding: number, fontSize: number, texSize: number): Shape[] {
console.log('generateFNTTextures');
if (fontSize) {
this._fntSizeLimit = fontSize * 0.5; //why half?
this.initFontSize(fontSize);
}
if (this._opentype_font) {
// if this was loaded from opentype,
// we make sure all glyphs are tesselated first
for (let g: number = 0; g < this._opentype_font.glyphs.length;g++) {
const thisGlyph = this._opentype_font.charToGlyph(
String.fromCharCode(this._opentype_font.glyphs.glyphs[g].unicode));
if (thisGlyph) {
const thisPath = thisGlyph.getPath();
const awayPath: GraphicsPath = new GraphicsPath();
let i = 0;
const len = thisPath.commands.length;
let startx: number = 0;
let starty: number = 0;
const y_offset = this._ascent;
for (i = 0;i < len;i++) {
const cmd = thisPath.commands[i];
//console.log("cmd", cmd.type, cmd.x, cmd.y, cmd.x1, cmd.y1, cmd.x2, cmd.y2);
if (cmd.type === 'M') {
awayPath.moveTo(cmd.x, cmd.y + y_offset);
startx = cmd.x;
starty = cmd.y + y_offset;
} else if (cmd.type === 'L') {
awayPath.lineTo(cmd.x, cmd.y + y_offset);
} else if (cmd.type === 'Q') {
awayPath.curveTo(cmd.x1, cmd.y1 + y_offset, cmd.x, cmd.y + y_offset);
} else if (cmd.type === 'C') {
awayPath.cubicCurveTo(cmd.x1, cmd.y1 + y_offset, cmd.x2,
cmd.y2 + y_offset, cmd.x, cmd.y + y_offset);
} else if (cmd.type === 'Z') { awayPath.lineTo(startx, starty);}
}
const t_font_char: TesselatedFontChar = new TesselatedFontChar(null, null, awayPath);
t_font_char.char_width = (thisGlyph.advanceWidth * (1 / thisGlyph.path.unitsPerEm * 72));
t_font_char.fill_data = GraphicsFactoryFills.pathToAttributesBuffer(awayPath, true);
if (!t_font_char.fill_data) {
console.log('error tesselating opentype glyph', this._opentype_font.glyphs.glyphs[g]);
//return null;
} else {
this._font_chars.push(t_font_char);
this._font_chars_dic[this._opentype_font.glyphs.glyphs[g].unicode] = t_font_char;
}
//console.log("unicode:",this._opentype_font.glyphs.glyphs[g].unicode);
//console.log("name:",this._opentype_font.glyphs.glyphs[g].name);
} else {
console.log('no char found for', this._opentype_font.glyphs.glyphs[g]);
}
}
}
let char_vertices: AttributesBuffer;
let x: number = 0;
let y: number = 0;
let buffer: Float32Array;
let v: number;
const glyph_verts: number[][] = [[]];
let channel_idx: number = 0;
let maxSize = 0;
const allChars: any[] = [];
let maxy: number = Number.MIN_VALUE;
let maxx: number = Number.MIN_VALUE;
let miny: number = Number.MAX_VALUE;
let minx: number = Number.MAX_VALUE;
let tmpx: number = 0;
let tmpy: number = 0;
// step1: loop over all chars, get their bounds and sort them by there height
// done without applying any font-scale to the glyphdata
let allAreas: number = 0;
for (let i: number = 0; i < this._font_chars.length;i++) {
char_vertices = this._font_chars[i].fill_data;
//console.log("got the glyph from opentype", this._opentype_font.glyphs.glyphs[g]);
buffer = new Float32Array(char_vertices.buffer);
maxy = Number.MIN_VALUE;
maxx = Number.MIN_VALUE;
miny = Number.MAX_VALUE;
minx = Number.MAX_VALUE;
tmpx = 0;
tmpy = 0;
for (v = 0; v < char_vertices.count; v++) {
tmpx = (buffer[v * 2]);
tmpy = (buffer[v * 2 + 1]);
if (tmpx < minx) minx = tmpx;
if (tmpx > maxx) maxx = tmpx;
if (tmpy < miny) miny = tmpy;
if (tmpy > maxy) maxy = tmpy;
}
this._font_chars[i].fnt_rect = new Rectangle(
(minx) / (this._font_chars[i].char_width),
(miny) / (this._font_em_size),
(maxx - minx) / (this._font_chars[i].char_width),
(maxy - miny) / (this._font_em_size));
allAreas += (maxx - minx + padding + padding) * (maxy - miny + padding + padding);
allChars[allChars.length] = {
idx:i,
minx:minx,
miny:miny,
width:maxx - minx,
height:maxy - miny
};
}
allChars.sort(function(a,b) {
return a.height > b.height ? -1 : 1;
});
// now allChars contains all glyphs sorted by height
let curHeight: number = 0;
maxSize = texSize;
let size_multiply: number = 1;
if (fontSize) {
this.initFontSize(fontSize);
size_multiply = this._size_multiply;
} else {
// figure out the optiomal fontSize so that the glyphs fill the texture
// imagine the spaces needed by all glyphs would be a perfect quat
// compare the width of that quat with the available width to have a first guess at fontSize
// make the size slightly bigger as the estimate and check if all glyphs will fit
// if they do not fit, slightly reduce the scale.
// repeat until all glyphs fit
size_multiply = maxSize / (Math.sqrt(allAreas)) * 1.2;
let invalid: boolean = true;
let curCharHeight: number = 0;
while (invalid) {
invalid = false;
x = 0;
y = padding;
for (let i: number = 0; i < allChars.length;i++) {
curCharHeight = allChars[i].height * size_multiply;
tmpx = 0;
tmpy = 0;
x += padding;
if (x + allChars[i].width * size_multiply + padding >= maxSize) {
x = padding;
y += curHeight + padding * 2;
curHeight = 0;
}
if (curCharHeight > curHeight) {
curHeight = curCharHeight;
}
if (y + curCharHeight + padding * 2 >= maxSize) {
invalid = true;
size_multiply = size_multiply * 0.995;
break;
}
x += (allChars[i].width) * size_multiply + padding;
}
}
}
this._fntSizeLimit = size_multiply * this._font_em_size * 0.5; //why half?
// collect the glyph-data:
let font_char: TesselatedFontChar;
x = 0;
y = padding;
for (let i: number = 0; i < allChars.length;i++) {
font_char = this._font_chars[allChars[i].idx];
char_vertices = font_char.fill_data;
buffer = new Float32Array(char_vertices.buffer);
minx = allChars[i].minx;
maxx = Number.MIN_VALUE;
miny = allChars[i].miny;
maxy = Number.MIN_VALUE;
tmpx = 0;
tmpy = 0;
x += padding;
if (x + allChars[i].width * size_multiply + padding >= maxSize) {
x = padding;
y += curHeight + padding * 2;
curHeight = 0;
}
allChars[i].height *= size_multiply;
if (allChars[i].height > curHeight) {
curHeight = allChars[i].height;
}
if (y + curHeight + padding * 2 >= maxSize) {
x = padding;
y = padding;
curHeight = 0;
channel_idx++;
glyph_verts[channel_idx] = [];
}
font_char.fnt_channel = channel_idx;
for (v = 0; v < char_vertices.count; v++) {
tmpx = ((buffer[v * 2] - minx) * size_multiply) + x;
tmpy = ((buffer[v * 2 + 1] - miny) * size_multiply) + y;
glyph_verts[channel_idx][glyph_verts[channel_idx].length] = tmpx;
glyph_verts[channel_idx][glyph_verts[channel_idx].length] = tmpy;
if (tmpx > maxx) {
maxx = tmpx;
}
if (tmpy > maxy) {
maxy = tmpy;
}
}
font_char.fnt_uv = new Rectangle(
x / maxSize, 1 - (y / maxSize),
(maxx - x) / maxSize, ((maxy - y) / maxSize));
//this._drawDebugRect(glyph_verts[channel_idx], x, maxx, y, maxy);
x += (allChars[i].width) * size_multiply + padding;
}
const shapes: Shape[] = [];
for (let i: number = 0; i < glyph_verts.length;i++) {
const attr_length: number = 2;//(tess_fontTable.usesCurves)?3:2;
const attributesView: AttributesView = new AttributesView(Float32Array, attr_length);
attributesView.set(glyph_verts[i]);
const vertexBuffer: AttributesBuffer = attributesView.attributesBuffer.cloneBufferView();
attributesView.dispose();
const elements = new TriangleElements(vertexBuffer);
elements.setPositions(new Float2Attributes(vertexBuffer));
const shape = Shape.getShape(elements);
const solid = new SolidFillStyle(0xFFFFFF, 1);
const material = MaterialManager.getMaterialForColor(solid);
shape.material = material;
if (material.getNumTextures()) {
shape.style = new Style();
shape.style.image = TextureAtlas.getTextureForColor(solid);
shape.style.uvMatrix = solid.getUVMatrix();
}
shapes[i] = shape;
}
return shapes;
}
public getRatio(size: number): number {
return this._size_multiply;
}
public hasChar(char_code: string): boolean {
let has = !!this._font_chars_dic[char_code];
if (!has && this._opentype_font) {
has = !!this._opentype_font.charToGlyph(String.fromCharCode(parseInt(char_code)));
}
return has;
}
public changeOpenTypeFont(newOpenTypeFont: any, tesselateAllOld: boolean = true) {
if ((tesselateAllOld) && (this._opentype_font)) {
//todo: make sure all available chars are tesselated
}
// todo: when updating a font we must take care that they are compatible in terms of em_size
this._opentype_font = newOpenTypeFont;
this._ascent = newOpenTypeFont.ascender;// * (1024 / newOpenTypeFont.unitsPerEm);
this._descent = newOpenTypeFont.descender ;//* (1024 / newOpenTypeFont.unitsPerEm);
this._font_em_size = newOpenTypeFont.unitsPerEm;
const space = this.getChar('32');
if (space) {
this._whitespace_width = space.char_width;
}
//console.log('changeOpenTypeFont', this._ascent, this._descent, this._font_em_size);
}
public initFontSize(font_size: number) {
if (this._current_size === font_size) {
return;
}
this._current_size = font_size;
this._size_multiply = font_size / this._font_em_size;
if (!this._size_multiply) {
debugger;
}
}
public getCharVertCnt(char_code: string): number {
const t_font_char: TesselatedFontChar = this._font_chars_dic[char_code];
if (t_font_char) {
return t_font_char.fill_data.length;
}
return 0;
}
public getCharWidth(char_code: string): number {
const space = this._whitespace_width * this._size_multiply || TesselatedFontTable.DEFAULT_SPACE;
// SPACE
if (char_code == '32') {
return (space * 20 | 0) / 20;
}
// TAB
if (char_code == '9') {
// todo: temporary change this to 2.
// need to implement textFormat tabstop
return (space * 2 * 20) / 20;
}
let t_font_char = this.getChar(char_code, false);
if (t_font_char) {
return ((t_font_char.char_width * this._size_multiply * 20) | 0) / 20;
} else if (char_code == '9679') {
t_font_char = this.createPointGlyph_9679();
return ((t_font_char.char_width * this._size_multiply * 20) | 0) / 20;
}
return 0;
}
public get usesCurves(): boolean {
return this._usesCurves;
}
public getLineHeight(): number {
// @todo: root evil for wrong linespace :(
// i am still not sure if we use the correct version of this 3:
return this._size_multiply * (this._ascent - this.descent); // used for sf
//return this._size_multiply * this._font_em_size; // used a long time for poki etc
//return (this._ascent+this._descent)*this._size_multiply; // used long ago for icycle
}
public getUnderLineHeight(): number {
return this._size_multiply * (this._ascent - this.descent / 2);
}
public get assetType(): string {
return TesselatedFontTable.assetType;
}
public dispose(): void {
for (let i: number = 0; i < this._font_chars.length; i++) {
this._font_chars[i].dispose();
}
this._font_chars.length = 0;
this._font_chars_dic = null;
}
get fntSizeLimit(): number {
return this._fntSizeLimit;
}
set fntSizeLimit(value: number) {
this._fntSizeLimit = value;
}
get ascent(): number {
return this._ascent;
}
set ascent(value: number) {
this._ascent = value;
}
get descent(): number {
return this._descent;
}
set descent(value: number) {
this._descent = value;
}
public get_font_chars(): Array<TesselatedFontChar> {
return this._font_chars;
}
public get_font_em_size(): number {
return this._font_em_size;
}
public set_whitespace_width(value: number): void {
this._whitespace_width = value;
}
public get_whitespace_width(): number {
return this._whitespace_width;
}
public set_font_em_size(font_em_size: number): void {
this._font_em_size = font_em_size;
}
public buildTextLineFromIndices(
field: TextField,
format: TextFormat,
x: number, y: number,
indices: number[],
advance: number[]): Point {
const textShape = field.getTextShapeForIdentifierAndFormat(format.color.toString(), format);
const origin_x = x;
const indicesCount = indices.length;
const charEntries: ICharEntry[] = [];
let textBuffSize: number = 0;
y -= this._ascent * this._size_multiply;//this.getLineHeight();
// loop over all the words and create the text data for it
// each word provides its own start-x and start-y values, so we can just ignore whitespace-here
for (let i = 0; i < indicesCount; i++) {
const idx = indices[i];
const charGlyph = this._glyphIdxToChar[idx];
if (!charGlyph) {
x += advance[i];
//console.log("no glyph found at idx", idx, "todo: support fallback fonts");
continue;
}
if (charGlyph.fill_data === null) {
if (charGlyph.fill_data_path.commands[0] == GraphicsPathCommand.MOVE_TO
&& charGlyph.fill_data_path.data[0] == 0
&& charGlyph.fill_data_path.data[1] == 0) {
charGlyph.fill_data_path.data.shift();
charGlyph.fill_data_path.data.shift();
charGlyph.fill_data_path.commands.shift();
charGlyph.fill_data_path.commands[0] = GraphicsPathCommand.LINE_TO;
}
charGlyph.fill_data = GraphicsFactoryFills.pathToAttributesBuffer(charGlyph.fill_data_path, true);
}
const charVertices = charGlyph.fill_data;
if (charVertices) {
charEntries.push({
char: charGlyph, x, y, selected: false
});
textBuffSize += charVertices.buffer.byteLength / 4;
}
x += advance[i];// * size_multiply;
}
const buff = new Float32Array(textBuffSize);
this._fillBuffer(buff, charEntries, false);
textShape.addChunk(buff);
return new Point(x - origin_x, this.getLineHeight());
}
public fillTextRun (tf: TextField, format: TextFormat, startWord: number, wordCnt: number) {
const useFNT: boolean = this._fntSizeLimit >= 0 && this._fntSizeLimit >= format.size;
if (useFNT) {
return this.fillTextRunFNT(tf, format, startWord, wordCnt);
}
const textShape = this._queryShape(tf, format);
let textShapeSelected: TextShape;
let newFormat: TextFormat;
const wordsCount = startWord + wordCnt;
const size_multiply = this._size_multiply;
let select_start = tf.selectionBeginIndex;
let select_end = tf.selectionEndIndex;
let start_x: number = 0;
if (tf.selectable) {
newFormat = format.clone();
newFormat.color = 0xffffff;
textShapeSelected = this._queryShape(tf, newFormat);
if (newFormat.underline && (startWord + 1) < tf.words.length) {
start_x = tf.words.get(startWord).x;
}
if (tf.selectionEndIndex < tf.selectionBeginIndex) {
select_start = tf.selectionEndIndex;
select_end = tf.selectionBeginIndex;
}
}
let selectedBuffSize = 0;
let textBuffSize = 0;
const charEntries: ICharEntry[] = [];
const selectedCharEntries: ICharEntry[] = [];
const underlines: Float32Array[] = [];
// loop over all the words and create the text data for it
// each word provides its own start-x and start-y values, so we can just ignore whitespace-here
for (let w = startWord; w < wordsCount; w += 1) {
const word = tf.words.get(w);
let x = word.x;
const y = word.y;
const startIdx = word.start;
const charsCount = startIdx + word.len;
for (let c = startIdx; c < charsCount; c++) {
const curTShape = (tf.isInFocus && c >= select_start && c < select_end) ?
textShapeSelected : textShape;
const code = tf.chars_codes[c];
if (code === 32 || code === 9) {
continue;
}
const charGlyph = this.getChar(code, true);
if (!charGlyph) {
if (once(this, 'miss' + tf.chars_codes[c])) {
console.debug('[TesselatedFontTable] Error: char not found in fontTable',
tf.chars_codes[c], String.fromCharCode(tf.chars_codes[c]));
}
continue;
}
if (curTShape === textShapeSelected) {
selectedCharEntries.push({
char: charGlyph, x, y, selected: true
});
selectedBuffSize += charGlyph.fill_data.buffer.byteLength / 4;
} else {
charEntries.push({
char: charGlyph, x, y, selected: false
});
textBuffSize += charGlyph.fill_data.buffer.byteLength / 4;
}
x += charGlyph.char_width * size_multiply;
}
const half_thickness: number = 0.25 * tf.internalScale.y;
const topY: number = y + this.getUnderLineHeight() + half_thickness;
const bottomY: number = y + this.getUnderLineHeight() - half_thickness;
if (tf.selectable && newFormat.underline && (startWord + 1) < tf.words.length) {
const underBuff = new Float32Array(12);
underBuff[0] = start_x;
underBuff[1] = bottomY;
underBuff[2] = start_x;
underBuff[3] = topY;
underBuff[4] = x;
underBuff[5] = topY;
underBuff[6] = x;
underBuff[7] = topY;
underBuff[8] = x;
underBuff[9] = bottomY;
underBuff[10] = start_x;
underBuff[11] = bottomY;
underlines.push(underBuff);
}
} // for word
// reallock buffers and fill
if (textBuffSize > 0) {
textBuffSize += underlines.length * 12;
const buff = new Float32Array(textBuffSize);
let offset = this._fillBuffer(buff, charEntries, false);
for (const under of underlines) {
buff.set(under, offset);
offset += under.length;
}
textShape.addChunk(buff);
}
if (selectedBuffSize > 0) {
const buff = new Float32Array(selectedBuffSize);
this._fillBuffer(buff, selectedCharEntries, true);
textShapeSelected.addChunk(buff);
}
}
private _cacheKey (color: number = 0xffffff, channel: number = -1) {
const fnt = channel >= 0;
return `${color}${fnt}${fnt ? channel : 0}`;
}
/**
* Query shape from TextField cache
* @param tf
* @param format
* @param channel - FNT channel, -1 for regular font
* @private
*/
private _queryShape (tf: TextField, format: TextFormat, channel = -1): TextShape {
const textShape = tf.getTextShapeForIdentifierAndFormat(this._cacheKey(format.color, channel), format);
if (channel >= 0 && !textShape.fntBitmap) {
const argb = ColorUtils.float32ColorToARGB(format.color);
textShape.fntBitmap = this._fnt_channels[channel];
textShape.fntColorTransform = new ColorTransform(argb[1] / 255, argb[2] / 255, argb[3] / 255);
}
return textShape;
}
/**
* Fill text run but use FNT Texture.
*/
public fillTextRunFNT (tf: TextField, format: TextFormat, startWord: number, wordCnt: number) {
const textShape: TextShape = this._queryShape(tf, format, 0);
const charShapesCache: Record<string, TextShape> = {};
const charShapesSelectedCache: Record<string, TextShape> = {};
const wordsCount = startWord + wordCnt;
const size_multiply = this._size_multiply;
let textShapeSelected: TextShape;
let select_start = tf.selectionBeginIndex;
let select_end = tf.selectionEndIndex;
let start_x: number = 0;
if (tf.selectable) {
const newFormat = format.clone();
newFormat.color = 0xffffff;
textShapeSelected = this._queryShape(tf, newFormat, 0);
charShapesSelectedCache[textShapeSelected.cacheId] = textShapeSelected;
if (newFormat.underline && (startWord + 1) < tf.words.length) {
start_x = tf.words.get(startWord).x;
}
if (tf.selectionEndIndex < tf.selectionBeginIndex) {
select_start = tf.selectionEndIndex;
select_end = tf.selectionBeginIndex;
}
}
charShapesCache[textShape.cacheId] = textShape;
const shapeArray = new Map<TextShape, {
uv: number [],
pos: number []
}>();
const underlines: Float32Array[] = [];
// loop over all the words and create the text data for it
// each word provides its own start-x and start-y values, so we can just ignore whitespace-here
for (let w = startWord; w < wordsCount; w += 1) {
const word = tf.words.get(w);
let x = word.x;
const y = word.y;
const startIdx = word.start;
const charsCount = startIdx + word.len;
let preFilledShape: TextShape = null;
let target: any = null;
let color: number = 0;
let fntID: number = -1;
for (let c = startIdx; c < charsCount; c++) {
// space codes
if (tf.chars_codes[c] === 32 || tf.chars_codes[c] === 9) {
continue;
}
const charGlyph = this.getChar(tf.chars_codes[c], false);
if (!charGlyph) {
if (once(this, 'miss' + tf.chars_codes[c])) {
console.debug('[TesselatedFontTable] Error: char not found in fontTable',
tf.chars_codes[c], String.fromCharCode(tf.chars_codes[c]));
}
continue;
}
const selected = (tf.isInFocus && c >= select_start && c < select_end);
let baseShape = textShape;
let cacheStore = charShapesCache;
if (selected) {
baseShape = textShapeSelected;
cacheStore = charShapesSelectedCache;
}
const curFormat = baseShape.format;
let cacheId = preFilledShape ? preFilledShape.cacheId : '';
if (!cacheId || color !== curFormat.color || charGlyph.fnt_channel !== fntID) {
cacheId = this._cacheKey(curFormat.color, charGlyph.fnt_channel);
color = curFormat.color;
fntID = charGlyph.fnt_channel;
}
let fntFilledShape = preFilledShape && preFilledShape.cacheId !== cacheId
? cacheStore[cacheId]
: preFilledShape;
if (!fntFilledShape) {
fntFilledShape = this._queryShape(tf, curFormat, charGlyph.fnt_channel);
cacheStore[fntFilledShape.cacheId] = fntFilledShape;
}
if (preFilledShape !== fntFilledShape || !target) {
// FNT is very little, has 6 vertices per shape (instead of 300 ++)
// fill it directly
target = shapeArray.get(fntFilledShape) || {
uv: [],
pos: []
};
shapeArray.set(fntFilledShape, target);
preFilledShape = fntFilledShape;
}
this._emitCharFNT (target, charGlyph, x, y);
x += charGlyph.char_width * size_multiply;
}
/*
todo Generate underline for regular shape
const half_thickness: number = 0.25 * tf.internalScale.y;
const topY: number = y + this.getUnderLineHeight() + half_thickness;
const bottomY: number = y + this.getUnderLineHeight() - half_thickness;
if (tf.selectable && newFormat.underline && (startWord + 1) < tf.words.length) {
const underBuff = new Float32Array(12);
underBuff[0] = start_x;
underBuff[1] = bottomY;
underBuff[2] = start_x;
underBuff[3] = topY;
underBuff[4] = x;
underBuff[5] = topY;
underBuff[6] = x;
underBuff[7] = topY;
underBuff[8] = x;
underBuff[9] = bottomY;
underBuff[10] = start_x;
underBuff[11] = bottomY;
underlines.push(underBuff);
}
*/
} // for word
// oh, es5... iterating over map won't working
const shapes = Array.from(shapeArray.keys());
for (const shape of shapes) {
const values = shapeArray.get(shape);
shape.addChunk(
new Float32Array(values.pos),
new Float32Array(values.uv)
);
}
}
private _emitCharFNT (
target: {uv: number[], pos: number[]},
charGlyph: TesselatedFontChar,
x: number,
y: number
) {
const scale = this._size_multiply;
const size = this._font_em_size * scale;
const rect = charGlyph.fnt_rect;
const uv = charGlyph.fnt_uv;
const width = charGlyph.char_width * scale;
if (!rect) {
// empty chars not have data
return;
}
const x1 = x + width * rect.x * 20;
const x2 = x1 + width * rect.width * 20;
const y1 = y + size * rect.y * 20;
const y2 = y1 + size * rect.height * 20;
target.pos.push(
x1, y1, x1, y2, x2, y1,
x2, y1, x1, y2, x2, y2
);
target.uv.push(
uv.x, uv.y, uv.x, uv.bottom, uv.right, uv.y,
uv.right, uv.y, uv.x, uv.bottom, uv.right, uv.bottom
);
}
public createPointGlyph_9679(): TesselatedFontChar {
const verts: number[] = [];
GraphicsFactoryHelper.drawElipse(
this._font_em_size / 2, this._font_em_size / 2,
this._font_em_size / 8, this._font_em_size / 8,
verts, 0, 360, 5, false);
const attributesView: AttributesView = new AttributesView(Float32Array, 2);
attributesView.set(verts);
const attributesBuffer: AttributesBuffer = attributesView.attributesBuffer.cloneBufferView();
attributesView.dispose();
const t_font_char: TesselatedFontChar = new TesselatedFontChar(attributesBuffer, null, null);
t_font_char.char_width = this._font_em_size;
this._font_chars.push(t_font_char);
this._font_chars_dic['9679'] = t_font_char;
return t_font_char;
}
private getCharOpenType (name: string): TesselatedFontChar {
if (!this._opentype_font) {
return null;
}
const thisGlyph = this._opentype_font.charToGlyph(String.fromCharCode(parseInt(name)));
if (!thisGlyph) {
return null;
}
//console.log("got the glyph from opentype");
const thisPath = thisGlyph.getPath();
const awayPath: GraphicsPath = new GraphicsPath();
let i = 0;
const len = thisPath.commands.length;
//awayPath.lineTo(0, 0);
//awayPath.moveTo(0,0);//-100);
//awayPath.curveTo(100, 250, 200,0);
//awayPath.lineTo(150, 100);
//awayPath.moveTo(0,20);
//awayPath.curveTo(100, 270, 200,20);
//awayPath.moveTo(0,-20);
//awayPath.moveTo(0,-10);
//awayPath.curveTo(100, -110, 200,-10);
let startx: number = 0;
let starty: number = 0;
const y_offset = this._ascent;
//40 = 66
const scale = (this._opentype_font.unitsPerEm / 72);
for (i = 0; i < len; i++) {
const cmd = thisPath.commands[i];
//console.log("cmd", cmd.type, cmd.x, cmd.y, cmd.x1, cmd.y1, cmd.x2, cmd.y2);
if (cmd.type === 'M') {
awayPath.moveTo(scale * cmd.x, scale * cmd.y + y_offset);
startx = scale * cmd.x;
starty = scale * cmd.y + y_offset;
} else if (cmd.type === 'L') {
awayPath.lineTo(scale * cmd.x, scale * cmd.y + y_offset);
} else if (cmd.type === 'Q') {
awayPath.curveTo(scale * cmd.x1, scale * cmd.y1 + y_offset,
scale * cmd.x, scale * cmd.y + y_offset);
} else if (cmd.type === 'C') {
const mergedX = cmd.x1 + (cmd.x2 - cmd.x1) / 2;
const mergedY = cmd.y1 + (cmd.y2 - cmd.y1) / 2;
awayPath.curveTo(scale * cmd.x1, scale * cmd.y1 + y_offset,
scale * mergedX, scale * mergedY + y_offset);
awayPath.curveTo(scale * cmd.x2, scale * cmd.y2 + y_offset,
scale * cmd.x, scale * cmd.y + y_offset);
} else if (cmd.type === 'Z') { awayPath.lineTo(startx, starty);}
}
const t_font_char = new TesselatedFontChar(null, null, awayPath);
t_font_char.char_width = thisGlyph.advanceWidth;//(1 / thisGlyph.path.unitsPerEm * 72);
t_font_char.fill_data = GraphicsFactoryFills.pathToAttributesBuffer(
awayPath,
true,
null,
scale * Settings.FONT_TESSELATION_QUALITY
);
t_font_char.lastTesselatedScale = scale;
if (!t_font_char.fill_data) {
if (once(this, 'tess' + name)) {
console.debug('[TesselatedFontTable] Error:tesselating opentype glyph:',
name.charCodeAt(0));
}
return null;
}
this._font_chars.push(t_font_char);
this._font_chars_dic[name] = t_font_char;
}
public getChar(name: string | number, tesselation = true): TesselatedFontChar {
name = '' + name;
const fontChar: TesselatedFontChar = this._font_chars_dic[name];
if (!tesselation && fontChar) {
return fontChar;
}
if (!fontChar) {
if (this._opentype_font) {
return this.getCharOpenType(name);
}
if (name == '9679') {
return this.createPointGlyph_9679();
}
return null;
}
// tesselation pass
const scale = this._size_multiply;
const qualityStepScale = Math.max(0.01, Math.round(scale * 100) / 100);
if (fontChar.fill_data_path === null) {
return null;
}
// we already has buffer with tesselation scale ratio
if (!(fontChar.fill_data == null && fontChar.stroke_data == null)
&& fontChar.lastTesselatedScale >= qualityStepScale
) {
return fontChar;
}
const path = fontChar.fill_data_path;
// hack for messed up "X": remove the first command if it is moveTo that points to 0,0
// change the new first command to moveTo
if (path.commands[0] == GraphicsPathCommand.MOVE_TO && path.data[0] == 0 && path.data[1] == 0) {
path.data.shift();
path.data.shift();
path.commands.shift();
path.commands[0] = GraphicsPathCommand.LINE_TO;
}
if (fontChar.fill_data) {
// it not have dispose, but clear will drop abstraction if it exist
fontChar.fill_data.clear();
}
fontChar.fill_data = GraphicsFactoryFills.pathToAttributesBuffer(
fontChar.fill_data_path,
true,
null,
qualityStepScale * Settings.FONT_TESSELATION_QUALITY
);
if (fontChar.lastTesselatedScale > 0) {
console.debug(
'[TesselatedFontTable] Retesselate char from scale:',
fontChar.lastTesselatedScale, qualityStepScale, String.fromCharCode(+name)
);
}
fontChar.lastTesselatedScale = qualityStepScale;
if (!fontChar.fill_data) {
if (once(this, 'tess' + name)) {
console.debug('[TesselatedFontTable] Error:tesselating glyph:', name.charCodeAt(0));
}
return null;
}
return fontChar;
}
public setChar(
name: string, char_width: number, fills_data: AttributesBuffer = null,
stroke_data: AttributesBuffer = null, uses_curves: boolean = false,
glyph_idx: number = 0, fill_data_path: GraphicsPath = null): void {
char_width = Math.floor(char_width * 20) / 20;
//console.log("adding char", name, String.fromCharCode(parseInt(name)));
if ((fills_data == null) && (stroke_data == null) && (fill_data_path == null))
throw (`TesselatedFontTable: trying to create a TesselatedFontChar with no data
(fills_data, stroke_data and fill_data_path is null)`);
if (this._font_chars.length > 0) {
if (uses_curves != this._usesCurves) {
throw (`TesselatedFontTable: Can not set different types of graphic-glyphs
(curves vs non-cuves) on the same FontTable!`);
}
} else {
this._usesCurves = uses_curves;
}
const t_font_char: TesselatedFontChar = new TesselatedFontChar(fills_data, stroke_data, fill_data_path);
t_font_char.char_width = char_width;
t_font_char.glyph_idx = glyph_idx;
t_font_char.name = name;
this._glyphIdxToChar[glyph_idx] = t_font_char;
this._font_chars.push(t_font_char);
this._font_chars_dic[name] = t_font_char;
}
private roundTo(val: number) {
const p = Settings.TEXT_SHAPE_ROUND_PRECISION;
const invP = 1 / (p || 1);
return Math.floor(val * invP) * p;
}
private _fillBuffer(
buffer: Float32Array,
chars: ICharEntry[],
selection: boolean = false
): number {
const scale = this._size_multiply;
let offset = 0;
for (const entry of chars) {
if (entry.selected !== selection) {
continue;
}
const { x, y, char } = entry;
const view = new Float32Array(char.fill_data.buffer);
const count = view.length;
for (let v = 0; v < count; v += 2) {
buffer[offset + v + 0] = this.roundTo(view[v + 0] * scale + x);
buffer[offset + v + 1] = this.roundTo(view[v + 1] * scale + y);
}
offset += view.length;
}
return offset;
}
}