Support multiple lines of text

This commit is contained in:
Patrick Walton 2017-08-25 17:02:11 -07:00
parent 1c21771414
commit defa2d63c3
1 changed files with 130 additions and 67 deletions

View File

@ -7,8 +7,41 @@ import * as base64js from 'base64-js';
import * as glmatrix from 'gl-matrix';
import * as opentype from 'opentype.js';
//const TEXT: string = "Lorem ipsum dolor sit amet";
const TEXT: string = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const TEXT: string =
`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;
@ -192,10 +225,6 @@ opentype.Font.prototype.isSupported = function() {
return (this as any).supported;
}
opentype.Glyph.prototype.getIndex = function() {
return (this as any).index;
}
// Various utility functions
function assert(value: boolean, message: string) {
@ -457,21 +486,24 @@ class AppController {
throw new PathfinderError("The font type is unsupported.");
// 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.
this.uniqueGlyphs = this.textGlyphs.map(textGlyph => textGlyph.glyph);
this.uniqueGlyphs.sort((a, b) => a.getIndex() - b.getIndex());
this.uniqueGlyphs = _.sortedUniqBy(this.uniqueGlyphs, glyph => glyph.getIndex());
this.uniqueGlyphs = this.textGlyphs.map(textGlyph => textGlyph);
this.uniqueGlyphs.sort((a, b) => a.index - b.index);
this.uniqueGlyphs = _.sortedUniqBy(this.uniqueGlyphs, glyph => glyph.index);
// Build the partitioning request to the server.
const request = {
otf: base64js.fromByteArray(new Uint8Array(this.fontData)),
fontIndex: 0,
glyphs: this.uniqueGlyphs.map(glyph => {
const metrics = glyph.getMetrics();
const metrics = glyph.metrics;
return {
id: glyph.getIndex(),
id: glyph.index,
transform: [1, 0, 0, 1, 0, 0],
};
}),
@ -521,8 +553,9 @@ class AppController {
private fontData: ArrayBuffer;
font: opentype.Font;
lineGlyphs: TextGlyph[][];
textGlyphs: TextGlyph[];
uniqueGlyphs: opentype.Glyph[];
uniqueGlyphs: PathfinderGlyph[];
private _atlas: Atlas;
atlasGlyphs: AtlasGlyph[];
@ -693,52 +726,64 @@ class PathfinderView {
/// Lays out glyphs on the canvas.
private layoutGlyphs() {
const lineGlyphs = this.appController.lineGlyphs;
const textGlyphs = this.appController.textGlyphs;
this.pixelsPerUnit = this.appController.fontSize / this.appController.font.unitsPerEm;
this.textGlyphCount = textGlyphs.length;
const font = this.appController.font;
this.pixelsPerUnit = this.appController.fontSize / font.unitsPerEm;
const glyphPositions = new Float32Array(textGlyphs.length * 8);
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();
for (let glyphIndex = 0; glyphIndex < textGlyphs.length; glyphIndex++) {
const textGlyph = textGlyphs[glyphIndex];
const glyphMetrics = textGlyph.glyph.getMetrics();
let glyphIndex = 0;
for (const line of lineGlyphs) {
for (let lineCharIndex = 0; lineCharIndex < line.length; lineCharIndex++) {
const textGlyph = textGlyphs[glyphIndex];
const glyphMetrics = textGlyph.metrics;
// Determine the atlas size.
const atlasSize = glmatrix.vec2.fromValues(glyphMetrics.xMax - glyphMetrics.xMin,
glyphMetrics.yMax - glyphMetrics.yMin);
glmatrix.vec2.scale(atlasSize, atlasSize, this.pixelsPerUnit);
glmatrix.vec2.ceil(atlasSize, atlasSize);
// Determine the atlas size.
const atlasSize = glmatrix.vec2.fromValues(glyphMetrics.xMax - glyphMetrics.xMin,
glyphMetrics.yMax - glyphMetrics.yMin);
glmatrix.vec2.scale(atlasSize, atlasSize, this.pixelsPerUnit);
glmatrix.vec2.ceil(atlasSize, atlasSize);
// Set positions.
const textGlyphBL = glmatrix.vec2.create(), textGlyphTR = glmatrix.vec2.create();
const offset = glmatrix.vec2.fromValues(glyphMetrics.leftSideBearing,
glyphMetrics.yMin);
glmatrix.vec2.scale(offset, offset, this.pixelsPerUnit);
glmatrix.vec2.add(textGlyphBL, currentPosition, offset);
glmatrix.vec2.round(textGlyphBL, textGlyphBL);
glmatrix.vec2.add(textGlyphTR, textGlyphBL, atlasSize);
// Set positions.
const textGlyphBL = glmatrix.vec2.create(), textGlyphTR = glmatrix.vec2.create();
const offset = glmatrix.vec2.fromValues(glyphMetrics.leftSideBearing,
glyphMetrics.yMin);
glmatrix.vec2.scale(offset, offset, this.pixelsPerUnit);
glmatrix.vec2.add(textGlyphBL, currentPosition, offset);
glmatrix.vec2.round(textGlyphBL, textGlyphBL);
glmatrix.vec2.add(textGlyphTR, textGlyphBL, atlasSize);
glyphPositions.set([
textGlyphBL[0], textGlyphTR[1],
textGlyphTR[0], textGlyphTR[1],
textGlyphBL[0], textGlyphBL[1],
textGlyphTR[0], textGlyphBL[1],
], glyphIndex * 8);
glyphPositions.set([
textGlyphBL[0], textGlyphTR[1],
textGlyphTR[0], textGlyphTR[1],
textGlyphBL[0], textGlyphBL[1],
textGlyphTR[0], textGlyphBL[1],
], glyphIndex * 8);
textGlyph.canvasRect = glmatrix.vec4.fromValues(textGlyphBL[0], textGlyphBL[1],
textGlyphTR[0], textGlyphTR[1]);
textGlyph.canvasRect = glmatrix.vec4.fromValues(textGlyphBL[0], textGlyphBL[1],
textGlyphTR[0], textGlyphTR[1]);
// Set indices.
glyphIndices.set(QUAD_ELEMENTS.map(elementIndex => elementIndex + 4 * glyphIndex),
glyphIndex * 6);
// Set indices.
glyphIndices.set(Array.from(QUAD_ELEMENTS).map(index => index + 4 * glyphIndex),
glyphIndex * 6);
// Advance.
currentPosition[0] += textGlyph.glyph.advanceWidth * this.pixelsPerUnit;
// Advance.
currentPosition[0] += textGlyph.advanceWidth * this.pixelsPerUnit;
glyphIndex++;
}
currentPosition[0] = 0;
currentPosition[1] -= lineHeight;
}
this.glyphPositionsBuffer = unwrapNull(this.gl.createBuffer());
@ -760,7 +805,7 @@ class PathfinderView {
let atlasGlyphs =
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 = _.sortedUniqBy(atlasGlyphs, glyph => glyph.index);
this.appController.atlasGlyphs = atlasGlyphs;
@ -770,7 +815,7 @@ class PathfinderView {
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);
// 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++) {
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)
continue;
@ -1157,7 +1202,10 @@ class PathfinderView {
this.gl.bindTexture(this.gl.TEXTURE_2D, this.appController.atlas.ensureTexture(this.gl));
this.gl.uniform1i(blitProgram.uniforms.uSource, 0);
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 {
@ -1202,7 +1250,6 @@ class PathfinderView {
atlasDepthTexture: WebGLTexture;
private pixelsPerUnit: number;
private textGlyphCount: number;
glyphPositionsBuffer: WebGLBuffer;
glyphTexCoordsBuffer: WebGLBuffer;
@ -2055,9 +2102,34 @@ interface AntialiasingStrategyTable {
ecaa: typeof ECAAStrategy;
}
class TextGlyph {
constructor(glyph: opentype.Glyph) {
this.glyph = glyph;
class PathfinderGlyph {
constructor(glyph: opentype.Glyph | PathfinderGlyph) {
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() {
@ -2068,13 +2140,13 @@ class TextGlyph {
this._canvasRect = rect;
}
glyph: opentype.Glyph;
private _canvasRect: Rect;
}
class AtlasGlyph {
constructor(glyph: opentype.Glyph) {
this.glyph = glyph;
class AtlasGlyph extends PathfinderGlyph {
constructor(glyph: opentype.Glyph | PathfinderGlyph) {
super(glyph);
this._atlasRect = glmatrix.vec4.create();
}
get atlasRect() {
@ -2093,15 +2165,6 @@ class AtlasGlyph {
return atlasSize;
}
get index(): number {
return this.glyph.getIndex();
}
get metrics(): opentype.Metrics {
return this.glyph.getMetrics();
}
private glyph: opentype.Glyph;
private _atlasRect: Rect;
}