Support multiple lines of text
This commit is contained in:
parent
1c21771414
commit
defa2d63c3
|
@ -7,8 +7,41 @@ import * as base64js from 'base64-js';
|
||||||
import * as glmatrix from 'gl-matrix';
|
import * as glmatrix from 'gl-matrix';
|
||||||
import * as opentype from 'opentype.js';
|
import * as opentype from 'opentype.js';
|
||||||
|
|
||||||
//const TEXT: string = "Lorem ipsum dolor sit amet";
|
const TEXT: string =
|
||||||
const TEXT: string = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
`’Twas brillig, and the slithy toves
|
||||||
|
Did gyre and gimble in the wabe;
|
||||||
|
All mimsy were the borogoves,
|
||||||
|
And the mome raths outgrabe.
|
||||||
|
|
||||||
|
“Beware the Jabberwock, my son!
|
||||||
|
The jaws that bite, the claws that catch!
|
||||||
|
Beware the Jubjub bird, and shun
|
||||||
|
The frumious Bandersnatch!”
|
||||||
|
|
||||||
|
He took his vorpal sword in hand:
|
||||||
|
Long time the manxome foe he sought—
|
||||||
|
So rested he by the Tumtum tree,
|
||||||
|
And stood awhile in thought.
|
||||||
|
|
||||||
|
And as in uffish thought he stood,
|
||||||
|
The Jabberwock, with eyes of flame,
|
||||||
|
Came whiffling through the tulgey wood,
|
||||||
|
And burbled as it came!
|
||||||
|
|
||||||
|
One, two! One, two! And through and through
|
||||||
|
The vorpal blade went snicker-snack!
|
||||||
|
He left it dead, and with its head
|
||||||
|
He went galumphing back.
|
||||||
|
|
||||||
|
“And hast thou slain the Jabberwock?
|
||||||
|
Come to my arms, my beamish boy!
|
||||||
|
O frabjous day! Callooh! Callay!”
|
||||||
|
He chortled in his joy.
|
||||||
|
|
||||||
|
’Twas brillig, and the slithy toves
|
||||||
|
Did gyre and gimble in the wabe;
|
||||||
|
All mimsy were the borogoves,
|
||||||
|
And the mome raths outgrabe.`;
|
||||||
|
|
||||||
const INITIAL_FONT_SIZE: number = 72.0;
|
const INITIAL_FONT_SIZE: number = 72.0;
|
||||||
|
|
||||||
|
@ -192,10 +225,6 @@ opentype.Font.prototype.isSupported = function() {
|
||||||
return (this as any).supported;
|
return (this as any).supported;
|
||||||
}
|
}
|
||||||
|
|
||||||
opentype.Glyph.prototype.getIndex = function() {
|
|
||||||
return (this as any).index;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Various utility functions
|
// Various utility functions
|
||||||
|
|
||||||
function assert(value: boolean, message: string) {
|
function assert(value: boolean, message: string) {
|
||||||
|
@ -457,21 +486,24 @@ class AppController {
|
||||||
throw new PathfinderError("The font type is unsupported.");
|
throw new PathfinderError("The font type is unsupported.");
|
||||||
|
|
||||||
// Lay out the text.
|
// Lay out the text.
|
||||||
this.textGlyphs = this.font.stringToGlyphs(TEXT).map(glyph => new TextGlyph(glyph));
|
this.lineGlyphs = TEXT.split("\n").map(line => {
|
||||||
|
return this.font.stringToGlyphs(line).map(glyph => new TextGlyph(glyph));
|
||||||
|
});
|
||||||
|
this.textGlyphs = _.flatten(this.lineGlyphs);
|
||||||
|
|
||||||
// Determine all glyphs potentially needed.
|
// Determine all glyphs potentially needed.
|
||||||
this.uniqueGlyphs = this.textGlyphs.map(textGlyph => textGlyph.glyph);
|
this.uniqueGlyphs = this.textGlyphs.map(textGlyph => textGlyph);
|
||||||
this.uniqueGlyphs.sort((a, b) => a.getIndex() - b.getIndex());
|
this.uniqueGlyphs.sort((a, b) => a.index - b.index);
|
||||||
this.uniqueGlyphs = _.sortedUniqBy(this.uniqueGlyphs, glyph => glyph.getIndex());
|
this.uniqueGlyphs = _.sortedUniqBy(this.uniqueGlyphs, glyph => glyph.index);
|
||||||
|
|
||||||
// Build the partitioning request to the server.
|
// Build the partitioning request to the server.
|
||||||
const request = {
|
const request = {
|
||||||
otf: base64js.fromByteArray(new Uint8Array(this.fontData)),
|
otf: base64js.fromByteArray(new Uint8Array(this.fontData)),
|
||||||
fontIndex: 0,
|
fontIndex: 0,
|
||||||
glyphs: this.uniqueGlyphs.map(glyph => {
|
glyphs: this.uniqueGlyphs.map(glyph => {
|
||||||
const metrics = glyph.getMetrics();
|
const metrics = glyph.metrics;
|
||||||
return {
|
return {
|
||||||
id: glyph.getIndex(),
|
id: glyph.index,
|
||||||
transform: [1, 0, 0, 1, 0, 0],
|
transform: [1, 0, 0, 1, 0, 0],
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
@ -521,8 +553,9 @@ class AppController {
|
||||||
|
|
||||||
private fontData: ArrayBuffer;
|
private fontData: ArrayBuffer;
|
||||||
font: opentype.Font;
|
font: opentype.Font;
|
||||||
|
lineGlyphs: TextGlyph[][];
|
||||||
textGlyphs: TextGlyph[];
|
textGlyphs: TextGlyph[];
|
||||||
uniqueGlyphs: opentype.Glyph[];
|
uniqueGlyphs: PathfinderGlyph[];
|
||||||
|
|
||||||
private _atlas: Atlas;
|
private _atlas: Atlas;
|
||||||
atlasGlyphs: AtlasGlyph[];
|
atlasGlyphs: AtlasGlyph[];
|
||||||
|
@ -693,52 +726,64 @@ class PathfinderView {
|
||||||
|
|
||||||
/// Lays out glyphs on the canvas.
|
/// Lays out glyphs on the canvas.
|
||||||
private layoutGlyphs() {
|
private layoutGlyphs() {
|
||||||
|
const lineGlyphs = this.appController.lineGlyphs;
|
||||||
const textGlyphs = this.appController.textGlyphs;
|
const textGlyphs = this.appController.textGlyphs;
|
||||||
|
|
||||||
this.pixelsPerUnit = this.appController.fontSize / this.appController.font.unitsPerEm;
|
const font = this.appController.font;
|
||||||
|
this.pixelsPerUnit = this.appController.fontSize / font.unitsPerEm;
|
||||||
this.textGlyphCount = textGlyphs.length;
|
|
||||||
|
|
||||||
const glyphPositions = new Float32Array(textGlyphs.length * 8);
|
const glyphPositions = new Float32Array(textGlyphs.length * 8);
|
||||||
const glyphIndices = new Uint32Array(textGlyphs.length * 6);
|
const glyphIndices = new Uint32Array(textGlyphs.length * 6);
|
||||||
|
|
||||||
|
const os2Table = font.tables.os2;
|
||||||
|
const lineHeight = (os2Table.sTypoAscender - os2Table.sTypoDescender +
|
||||||
|
os2Table.sTypoLineGap) * this.pixelsPerUnit;
|
||||||
|
|
||||||
const currentPosition = glmatrix.vec2.create();
|
const currentPosition = glmatrix.vec2.create();
|
||||||
|
|
||||||
for (let glyphIndex = 0; glyphIndex < textGlyphs.length; glyphIndex++) {
|
let glyphIndex = 0;
|
||||||
const textGlyph = textGlyphs[glyphIndex];
|
for (const line of lineGlyphs) {
|
||||||
const glyphMetrics = textGlyph.glyph.getMetrics();
|
for (let lineCharIndex = 0; lineCharIndex < line.length; lineCharIndex++) {
|
||||||
|
const textGlyph = textGlyphs[glyphIndex];
|
||||||
|
const glyphMetrics = textGlyph.metrics;
|
||||||
|
|
||||||
// Determine the atlas size.
|
// Determine the atlas size.
|
||||||
const atlasSize = glmatrix.vec2.fromValues(glyphMetrics.xMax - glyphMetrics.xMin,
|
const atlasSize = glmatrix.vec2.fromValues(glyphMetrics.xMax - glyphMetrics.xMin,
|
||||||
glyphMetrics.yMax - glyphMetrics.yMin);
|
glyphMetrics.yMax - glyphMetrics.yMin);
|
||||||
glmatrix.vec2.scale(atlasSize, atlasSize, this.pixelsPerUnit);
|
glmatrix.vec2.scale(atlasSize, atlasSize, this.pixelsPerUnit);
|
||||||
glmatrix.vec2.ceil(atlasSize, atlasSize);
|
glmatrix.vec2.ceil(atlasSize, atlasSize);
|
||||||
|
|
||||||
// Set positions.
|
// Set positions.
|
||||||
const textGlyphBL = glmatrix.vec2.create(), textGlyphTR = glmatrix.vec2.create();
|
const textGlyphBL = glmatrix.vec2.create(), textGlyphTR = glmatrix.vec2.create();
|
||||||
const offset = glmatrix.vec2.fromValues(glyphMetrics.leftSideBearing,
|
const offset = glmatrix.vec2.fromValues(glyphMetrics.leftSideBearing,
|
||||||
glyphMetrics.yMin);
|
glyphMetrics.yMin);
|
||||||
glmatrix.vec2.scale(offset, offset, this.pixelsPerUnit);
|
glmatrix.vec2.scale(offset, offset, this.pixelsPerUnit);
|
||||||
glmatrix.vec2.add(textGlyphBL, currentPosition, offset);
|
glmatrix.vec2.add(textGlyphBL, currentPosition, offset);
|
||||||
glmatrix.vec2.round(textGlyphBL, textGlyphBL);
|
glmatrix.vec2.round(textGlyphBL, textGlyphBL);
|
||||||
glmatrix.vec2.add(textGlyphTR, textGlyphBL, atlasSize);
|
glmatrix.vec2.add(textGlyphTR, textGlyphBL, atlasSize);
|
||||||
|
|
||||||
glyphPositions.set([
|
glyphPositions.set([
|
||||||
textGlyphBL[0], textGlyphTR[1],
|
textGlyphBL[0], textGlyphTR[1],
|
||||||
textGlyphTR[0], textGlyphTR[1],
|
textGlyphTR[0], textGlyphTR[1],
|
||||||
textGlyphBL[0], textGlyphBL[1],
|
textGlyphBL[0], textGlyphBL[1],
|
||||||
textGlyphTR[0], textGlyphBL[1],
|
textGlyphTR[0], textGlyphBL[1],
|
||||||
], glyphIndex * 8);
|
], glyphIndex * 8);
|
||||||
|
|
||||||
textGlyph.canvasRect = glmatrix.vec4.fromValues(textGlyphBL[0], textGlyphBL[1],
|
textGlyph.canvasRect = glmatrix.vec4.fromValues(textGlyphBL[0], textGlyphBL[1],
|
||||||
textGlyphTR[0], textGlyphTR[1]);
|
textGlyphTR[0], textGlyphTR[1]);
|
||||||
|
|
||||||
// Set indices.
|
// Set indices.
|
||||||
glyphIndices.set(QUAD_ELEMENTS.map(elementIndex => elementIndex + 4 * glyphIndex),
|
glyphIndices.set(Array.from(QUAD_ELEMENTS).map(index => index + 4 * glyphIndex),
|
||||||
glyphIndex * 6);
|
glyphIndex * 6);
|
||||||
|
|
||||||
// Advance.
|
// Advance.
|
||||||
currentPosition[0] += textGlyph.glyph.advanceWidth * this.pixelsPerUnit;
|
currentPosition[0] += textGlyph.advanceWidth * this.pixelsPerUnit;
|
||||||
|
|
||||||
|
glyphIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPosition[0] = 0;
|
||||||
|
currentPosition[1] -= lineHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.glyphPositionsBuffer = unwrapNull(this.gl.createBuffer());
|
this.glyphPositionsBuffer = unwrapNull(this.gl.createBuffer());
|
||||||
|
@ -760,7 +805,7 @@ class PathfinderView {
|
||||||
|
|
||||||
let atlasGlyphs =
|
let atlasGlyphs =
|
||||||
textGlyphs.filter(textGlyph => rectsIntersect(textGlyph.canvasRect, canvasRect))
|
textGlyphs.filter(textGlyph => rectsIntersect(textGlyph.canvasRect, canvasRect))
|
||||||
.map(textGlyph => new AtlasGlyph(textGlyph.glyph));
|
.map(textGlyph => new AtlasGlyph(textGlyph));
|
||||||
atlasGlyphs.sort((a, b) => a.index - b.index);
|
atlasGlyphs.sort((a, b) => a.index - b.index);
|
||||||
atlasGlyphs = _.sortedUniqBy(atlasGlyphs, glyph => glyph.index);
|
atlasGlyphs = _.sortedUniqBy(atlasGlyphs, glyph => glyph.index);
|
||||||
this.appController.atlasGlyphs = atlasGlyphs;
|
this.appController.atlasGlyphs = atlasGlyphs;
|
||||||
|
@ -770,7 +815,7 @@ class PathfinderView {
|
||||||
|
|
||||||
this.appController.atlas.layoutGlyphs(atlasGlyphs, fontSize, unitsPerEm);
|
this.appController.atlas.layoutGlyphs(atlasGlyphs, fontSize, unitsPerEm);
|
||||||
|
|
||||||
const uniqueGlyphIndices = this.appController.uniqueGlyphs.map(glyph => glyph.getIndex());
|
const uniqueGlyphIndices = this.appController.uniqueGlyphs.map(glyph => glyph.index);
|
||||||
uniqueGlyphIndices.sort((a, b) => a - b);
|
uniqueGlyphIndices.sort((a, b) => a - b);
|
||||||
|
|
||||||
// TODO(pcwalton): Regenerate the IBOs to include only the glyphs we care about.
|
// TODO(pcwalton): Regenerate the IBOs to include only the glyphs we care about.
|
||||||
|
@ -820,9 +865,9 @@ class PathfinderView {
|
||||||
|
|
||||||
for (let textGlyphIndex = 0; textGlyphIndex < textGlyphs.length; textGlyphIndex++) {
|
for (let textGlyphIndex = 0; textGlyphIndex < textGlyphs.length; textGlyphIndex++) {
|
||||||
const textGlyph = textGlyphs[textGlyphIndex];
|
const textGlyph = textGlyphs[textGlyphIndex];
|
||||||
const textGlyphMetrics = textGlyph.glyph.getMetrics();
|
const textGlyphMetrics = textGlyph.metrics;
|
||||||
|
|
||||||
let atlasGlyphIndex = _.sortedIndexOf(atlasGlyphIndices, textGlyph.glyph.getIndex());
|
let atlasGlyphIndex = _.sortedIndexOf(atlasGlyphIndices, textGlyph.index);
|
||||||
if (atlasGlyphIndex < 0)
|
if (atlasGlyphIndex < 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
@ -1157,7 +1202,10 @@ class PathfinderView {
|
||||||
this.gl.bindTexture(this.gl.TEXTURE_2D, this.appController.atlas.ensureTexture(this.gl));
|
this.gl.bindTexture(this.gl.TEXTURE_2D, this.appController.atlas.ensureTexture(this.gl));
|
||||||
this.gl.uniform1i(blitProgram.uniforms.uSource, 0);
|
this.gl.uniform1i(blitProgram.uniforms.uSource, 0);
|
||||||
this.setIdentityTexScaleUniform(blitProgram.uniforms);
|
this.setIdentityTexScaleUniform(blitProgram.uniforms);
|
||||||
this.gl.drawElements(this.gl.TRIANGLES, this.textGlyphCount * 6, this.gl.UNSIGNED_INT, 0);
|
this.gl.drawElements(this.gl.TRIANGLES,
|
||||||
|
this.appController.textGlyphs.length * 6,
|
||||||
|
this.gl.UNSIGNED_INT,
|
||||||
|
0);
|
||||||
}
|
}
|
||||||
|
|
||||||
get bgColor(): glmatrix.vec4 {
|
get bgColor(): glmatrix.vec4 {
|
||||||
|
@ -1202,7 +1250,6 @@ class PathfinderView {
|
||||||
atlasDepthTexture: WebGLTexture;
|
atlasDepthTexture: WebGLTexture;
|
||||||
|
|
||||||
private pixelsPerUnit: number;
|
private pixelsPerUnit: number;
|
||||||
private textGlyphCount: number;
|
|
||||||
|
|
||||||
glyphPositionsBuffer: WebGLBuffer;
|
glyphPositionsBuffer: WebGLBuffer;
|
||||||
glyphTexCoordsBuffer: WebGLBuffer;
|
glyphTexCoordsBuffer: WebGLBuffer;
|
||||||
|
@ -2055,9 +2102,34 @@ interface AntialiasingStrategyTable {
|
||||||
ecaa: typeof ECAAStrategy;
|
ecaa: typeof ECAAStrategy;
|
||||||
}
|
}
|
||||||
|
|
||||||
class TextGlyph {
|
class PathfinderGlyph {
|
||||||
constructor(glyph: opentype.Glyph) {
|
constructor(glyph: opentype.Glyph | PathfinderGlyph) {
|
||||||
this.glyph = glyph;
|
this.glyph = glyph instanceof PathfinderGlyph ? glyph.glyph : glyph;
|
||||||
|
this._metrics = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get index(): number {
|
||||||
|
return (this.glyph as any).index;
|
||||||
|
}
|
||||||
|
|
||||||
|
get metrics(): opentype.Metrics {
|
||||||
|
if (this._metrics == null)
|
||||||
|
this._metrics = this.glyph.getMetrics();
|
||||||
|
return this._metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
get advanceWidth(): number {
|
||||||
|
return this.glyph.advanceWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
private glyph: opentype.Glyph;
|
||||||
|
private _metrics: opentype.Metrics | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TextGlyph extends PathfinderGlyph {
|
||||||
|
constructor(glyph: opentype.Glyph | PathfinderGlyph) {
|
||||||
|
super(glyph);
|
||||||
|
this._canvasRect = glmatrix.vec4.create();
|
||||||
}
|
}
|
||||||
|
|
||||||
get canvasRect() {
|
get canvasRect() {
|
||||||
|
@ -2068,13 +2140,13 @@ class TextGlyph {
|
||||||
this._canvasRect = rect;
|
this._canvasRect = rect;
|
||||||
}
|
}
|
||||||
|
|
||||||
glyph: opentype.Glyph;
|
|
||||||
private _canvasRect: Rect;
|
private _canvasRect: Rect;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AtlasGlyph {
|
class AtlasGlyph extends PathfinderGlyph {
|
||||||
constructor(glyph: opentype.Glyph) {
|
constructor(glyph: opentype.Glyph | PathfinderGlyph) {
|
||||||
this.glyph = glyph;
|
super(glyph);
|
||||||
|
this._atlasRect = glmatrix.vec4.create();
|
||||||
}
|
}
|
||||||
|
|
||||||
get atlasRect() {
|
get atlasRect() {
|
||||||
|
@ -2093,15 +2165,6 @@ class AtlasGlyph {
|
||||||
return atlasSize;
|
return atlasSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
get index(): number {
|
|
||||||
return this.glyph.getIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
get metrics(): opentype.Metrics {
|
|
||||||
return this.glyph.getMetrics();
|
|
||||||
}
|
|
||||||
|
|
||||||
private glyph: opentype.Glyph;
|
|
||||||
private _atlasRect: Rect;
|
private _atlasRect: Rect;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue