Implement some rudimentary text layout for the 3D demo

This commit is contained in:
Patrick Walton 2017-09-06 22:11:32 -07:00
parent 2cce20db10
commit 7de664e4a9
4 changed files with 168 additions and 100 deletions

View File

@ -9,6 +9,7 @@
// except according to those terms.
import * as glmatrix from 'gl-matrix';
import * as opentype from "opentype.js";
import {AntialiasingStrategy, AntialiasingStrategyName, NoAAStrategy} from "./aa-strategy";
import {DemoAppController} from "./app-controller";
@ -16,12 +17,18 @@ import {PerspectiveCamera} from "./camera";
import {mat4, vec2} from "gl-matrix";
import {PathfinderMeshData} from "./meshes";
import {ShaderMap, ShaderProgramSource} from "./shader-loader";
import {BUILTIN_FONT_URI, TextLayout, PathfinderGlyph} from "./text";
import {PathfinderError, panic, unwrapNull} from "./utils";
import {BUILTIN_FONT_URI, PathfinderGlyph, TextRun, TextLayout, GlyphStorage} from "./text";
import {PathfinderError, assert, panic, unwrapNull} from "./utils";
import {PathfinderDemoView, Timings} from "./view";
import SSAAStrategy from "./ssaa-strategy";
import * as _ from "lodash";
const TEXT: string = "Lorem ipsum dolor sit amet";
const WIDTH: number = 40000;
const TEXT: string[][] = [
[ "Lorem ipsum", "dolor sit amet" ],
[ "consectetur adipiscing elit." ],
];
const FONT: string = 'open-sans';
@ -51,13 +58,39 @@ class ThreeDController extends DemoAppController<ThreeDView> {
}
protected fileLoaded(): void {
this.layout = new TextLayout(this.fileData, TEXT, glyph => new ThreeDGlyph(glyph));
this.layout.layoutText();
this.layout.glyphStorage.partition().then((baseMeshes: PathfinderMeshData) => {
const font = opentype.parse(this.fileData);
assert(font.isSupported(), "The font type is unsupported!");
const createGlyph = (glyph: opentype.Glyph) => new ThreeDGlyph(glyph);
let textRuns = [];
for (let lineNumber = 0; lineNumber < TEXT.length; lineNumber++) {
const line = TEXT[lineNumber];
const lineY = -lineNumber * font.lineHeight();
const lineGlyphs = line.map(string => {
const glyphs = font.stringToGlyphs(string).map(createGlyph);
return { glyphs: glyphs, width: _.sumBy(glyphs, glyph => glyph.advanceWidth) };
});
const usedSpace = _.sumBy(lineGlyphs, 'width');
const emptySpace = Math.max(WIDTH - usedSpace, 0.0);
const spacing = emptySpace / Math.max(lineGlyphs.length - 1, 1);
let currentX = 0.0;
for (const glyphInfo of lineGlyphs) {
textRuns.push(new TextRun(glyphInfo.glyphs, [currentX, lineY], font, createGlyph));
currentX += glyphInfo.width + spacing;
}
}
this.glyphStorage = new GlyphStorage(this.fileData, textRuns, createGlyph, font);
this.glyphStorage.layoutRuns();
this.glyphStorage.partition().then((baseMeshes: PathfinderMeshData) => {
this.baseMeshes = baseMeshes;
this.expandedMeshes = this.layout.glyphStorage.expandMeshes(baseMeshes).meshes;
this.expandedMeshes = this.glyphStorage.expandMeshes(baseMeshes).meshes;
this.view.then(view => {
view.uploadPathMetadata(this.layout.glyphStorage.textGlyphs.length);
view.uploadPathMetadata();
view.attachMeshes(this.expandedMeshes);
});
});
@ -77,7 +110,7 @@ class ThreeDController extends DemoAppController<ThreeDView> {
return FONT;
}
layout: TextLayout<ThreeDGlyph>;
glyphStorage: GlyphStorage<ThreeDGlyph>;
private baseMeshes: PathfinderMeshData;
private expandedMeshes: PathfinderMeshData;
@ -95,8 +128,9 @@ class ThreeDView extends PathfinderDemoView {
this.camera.onChange = () => this.setDirty();
}
uploadPathMetadata(pathCount: number) {
const textGlyphs = this.appController.layout.glyphStorage.textGlyphs;
uploadPathMetadata() {
const textGlyphs = this.appController.glyphStorage.allGlyphs;
const pathCount = textGlyphs.length;
const pathColors = new Uint8Array(4 * (pathCount + 1));
const pathTransforms = new Float32Array(4 * (pathCount + 1));
@ -121,7 +155,7 @@ class ThreeDView extends PathfinderDemoView {
aaLevel: number,
subpixelAA: boolean):
AntialiasingStrategy {
if (aaType != 'ecaa')
if (aaType !== 'ecaa')
return new (ANTIALIASING_STRATEGIES[aaType])(aaLevel, subpixelAA);
throw new PathfinderError("Unsupported antialiasing type!");
}

View File

@ -17,9 +17,10 @@ import {B_QUAD_UPPER_RIGHT_VERTEX_OFFSET} from "./meshes";
import {B_QUAD_UPPER_CONTROL_POINT_VERTEX_OFFSET, B_QUAD_LOWER_LEFT_VERTEX_OFFSET} from "./meshes";
import {B_QUAD_LOWER_RIGHT_VERTEX_OFFSET} from "./meshes";
import {B_QUAD_LOWER_CONTROL_POINT_VERTEX_OFFSET, PathfinderMeshData} from "./meshes";
import {BUILTIN_FONT_URI, GlyphStorage, PathfinderGlyph} from "./text";
import {unwrapNull, UINT32_SIZE, UINT32_MAX} from "./utils";
import { BUILTIN_FONT_URI, GlyphStorage, PathfinderGlyph, TextRun } from "./text";
import { unwrapNull, UINT32_SIZE, UINT32_MAX, assert } from "./utils";
import {PathfinderView} from "./view";
import * as opentype from "opentype.js";
const CHARACTER: string = 'r';
@ -39,9 +40,12 @@ class MeshDebuggerAppController extends AppController {
}
protected fileLoaded(): void {
this.glyphStorage = new GlyphStorage(this.fileData,
CHARACTER,
glyph => new MeshDebuggerGlyph(glyph));
const font = opentype.parse(this.fileData);
assert(font.isSupported(), "The font type is unsupported!");
const createGlyph = (glyph: opentype.Glyph) => new MeshDebuggerGlyph(glyph);
const textRun = new TextRun<MeshDebuggerGlyph>(CHARACTER, [0, 0], font, createGlyph);
this.glyphStorage = new GlyphStorage(this.fileData, [textRun], createGlyph, font);
this.glyphStorage.partition().then(meshes => {
this.meshes = meshes;

View File

@ -103,6 +103,7 @@ type ShaderType = number;
declare module 'opentype.js' {
interface Font {
isSupported(): boolean;
lineHeight(): number;
}
interface Glyph {
getIndex(): number;
@ -252,9 +253,9 @@ class TextDemoView extends MonochromePathfinderView {
/// Lays out glyphs on the canvas.
private layoutGlyphs() {
this.appController.layout.layoutText();
this.appController.layout.layoutRuns();
const textGlyphs = this.appController.layout.glyphStorage.textGlyphs;
const textGlyphs = this.appController.layout.glyphStorage.allGlyphs;
const glyphPositions = new Float32Array(textGlyphs.length * 8);
const glyphIndices = new Uint32Array(textGlyphs.length * 6);
@ -280,7 +281,7 @@ class TextDemoView extends MonochromePathfinderView {
}
private buildAtlasGlyphs() {
const textGlyphs = this.appController.layout.glyphStorage.textGlyphs;
const textGlyphs = this.appController.layout.glyphStorage.allGlyphs;
const pixelsPerUnit = this.appController.pixelsPerUnit;
// Only build glyphs in view.
@ -338,7 +339,7 @@ class TextDemoView extends MonochromePathfinderView {
}
private setGlyphTexCoords() {
const textGlyphs = this.appController.layout.glyphStorage.textGlyphs;
const textGlyphs = this.appController.layout.glyphStorage.allGlyphs;
const atlasGlyphs = this.appController.atlasGlyphs;
const atlasGlyphIndices = atlasGlyphs.map(atlasGlyph => atlasGlyph.index);
@ -452,7 +453,7 @@ class TextDemoView extends MonochromePathfinderView {
this.gl.uniform1i(blitProgram.uniforms.uSource, 0);
this.setIdentityTexScaleUniform(blitProgram.uniforms);
this.gl.drawElements(this.gl.TRIANGLES,
this.appController.layout.glyphStorage.textGlyphs.length * 6,
this.appController.layout.glyphStorage.allGlyphs.length * 6,
this.gl.UNSIGNED_INT,
0);
}

View File

@ -21,6 +21,30 @@ export const BUILTIN_FONT_URI: string = "/otf/demo";
const PARTITION_FONT_ENDPOINT_URI: string = "/partition-font";
export class TextRun<Glyph extends PathfinderGlyph> {
constructor(text: string | Glyph[],
origin: number[],
font: Font,
createGlyph: CreateGlyphFn<Glyph>) {
if (typeof(text) === 'string')
text = font.stringToGlyphs(text).map(createGlyph);
this.glyphs = text;
this.origin = origin;
}
layout() {
let currentX = this.origin[0];
for (const glyph of this.glyphs) {
glyph.origin = glmatrix.vec2.fromValues(currentX, this.origin[1]);
currentX += glyph.advanceWidth;
}
}
readonly glyphs: Glyph[];
readonly origin: number[];
}
export interface ExpandedMeshData {
meshes: PathfinderMeshData;
}
@ -38,9 +62,14 @@ opentype.Font.prototype.isSupported = function() {
return (this as any).supported;
}
opentype.Font.prototype.lineHeight = function() {
const os2Table = this.tables.os2;
return os2Table.sTypoAscender - os2Table.sTypoDescender + os2Table.sTypoLineGap;
};
export class GlyphStorage<Glyph extends PathfinderGlyph> {
constructor(fontData: ArrayBuffer,
textGlyphs: Glyph[] | string,
textRuns: TextRun<Glyph>[],
createGlyph: CreateGlyphFn<Glyph>,
font?: Font) {
if (font == null) {
@ -48,15 +77,12 @@ export class GlyphStorage<Glyph extends PathfinderGlyph> {
assert(font.isSupported(), "The font type is unsupported!");
}
if (typeof(textGlyphs) === 'string')
textGlyphs = font.stringToGlyphs(textGlyphs).map(createGlyph);
this.fontData = fontData;
this.textGlyphs = textGlyphs;
this.textRuns = textRuns;
this.font = font;
// Determine all glyphs potentially needed.
this.uniqueGlyphs = this.textGlyphs.map(textGlyph => textGlyph);
this.uniqueGlyphs = _.flatMap(this.textRuns, textRun => textRun.glyphs);
this.uniqueGlyphs.sort((a, b) => a.index - b.index);
this.uniqueGlyphs = _.sortedUniqBy(this.uniqueGlyphs, glyph => glyph.index);
}
@ -106,56 +132,60 @@ export class GlyphStorage<Glyph extends PathfinderGlyph> {
const expandedCoverInteriorIndices: number[] = [];
const expandedCoverCurveIndices: number[] = [];
for (let textGlyphIndex = 0; textGlyphIndex < this.textGlyphs.length; textGlyphIndex++) {
const textGlyph = this.textGlyphs[textGlyphIndex];
const uniqueGlyphIndex = _.sortedIndexBy(this.uniqueGlyphs, textGlyph, 'index');
if (uniqueGlyphIndex < 0)
continue;
const firstBVertexIndex = _.sortedIndex(bVertexPathIDs, uniqueGlyphIndex + 1);
if (firstBVertexIndex < 0)
continue;
let textGlyphIndex = 0;
for (const textRun of this.textRuns) {
for (const textGlyph of textRun.glyphs) {
const uniqueGlyphIndex = _.sortedIndexBy(this.uniqueGlyphs, textGlyph, 'index');
if (uniqueGlyphIndex < 0)
continue;
const firstBVertexIndex = _.sortedIndex(bVertexPathIDs, uniqueGlyphIndex + 1);
if (firstBVertexIndex < 0)
continue;
// Copy over vertices.
let bVertexIndex = firstBVertexIndex;
const firstExpandedBVertexIndex = expandedBVertexPathIDs.length;
while (bVertexIndex < bVertexPathIDs.length &&
bVertexPathIDs[bVertexIndex] === uniqueGlyphIndex + 1) {
expandedBVertexPositions.push(bVertexPositions[bVertexIndex * 2 + 0],
bVertexPositions[bVertexIndex * 2 + 1]);
expandedBVertexPathIDs.push(textGlyphIndex + 1);
expandedBVertexLoopBlinnData.push(bVertexLoopBlinnData[bVertexIndex]);
bVertexIndex++;
}
// Copy over indices.
copyIndices(expandedCoverInteriorIndices,
new Uint32Array(meshes.coverInteriorIndices),
firstExpandedBVertexIndex,
firstBVertexIndex,
bVertexIndex);
copyIndices(expandedCoverCurveIndices,
new Uint32Array(meshes.coverCurveIndices),
firstExpandedBVertexIndex,
firstBVertexIndex,
bVertexIndex);
// Copy over B-quads.
let firstBQuadIndex =
_.findIndex(bQuads, bQuad => bVertexPathIDs[bQuad[0]] == uniqueGlyphIndex + 1);
if (firstBQuadIndex < 0)
firstBQuadIndex = bQuads.length;
const indexDelta = firstExpandedBVertexIndex - firstBVertexIndex;
for (let bQuadIndex = firstBQuadIndex; bQuadIndex < bQuads.length; bQuadIndex++) {
const bQuad = bQuads[bQuadIndex];
if (bVertexPathIDs[bQuad[0]] !== uniqueGlyphIndex + 1)
break;
for (let indexIndex = 0; indexIndex < B_QUAD_SIZE / UINT32_SIZE; indexIndex++) {
const srcIndex = bQuad[indexIndex];
if (srcIndex === UINT32_MAX)
expandedBQuads.push(srcIndex);
else
expandedBQuads.push(srcIndex + indexDelta);
// Copy over vertices.
let bVertexIndex = firstBVertexIndex;
const firstExpandedBVertexIndex = expandedBVertexPathIDs.length;
while (bVertexIndex < bVertexPathIDs.length &&
bVertexPathIDs[bVertexIndex] === uniqueGlyphIndex + 1) {
expandedBVertexPositions.push(bVertexPositions[bVertexIndex * 2 + 0],
bVertexPositions[bVertexIndex * 2 + 1]);
expandedBVertexPathIDs.push(textGlyphIndex + 1);
expandedBVertexLoopBlinnData.push(bVertexLoopBlinnData[bVertexIndex]);
bVertexIndex++;
}
// Copy over indices.
copyIndices(expandedCoverInteriorIndices,
new Uint32Array(meshes.coverInteriorIndices),
firstExpandedBVertexIndex,
firstBVertexIndex,
bVertexIndex);
copyIndices(expandedCoverCurveIndices,
new Uint32Array(meshes.coverCurveIndices),
firstExpandedBVertexIndex,
firstBVertexIndex,
bVertexIndex);
// Copy over B-quads.
let firstBQuadIndex =
_.findIndex(bQuads, bQuad => bVertexPathIDs[bQuad[0]] == uniqueGlyphIndex + 1);
if (firstBQuadIndex < 0)
firstBQuadIndex = bQuads.length;
const indexDelta = firstExpandedBVertexIndex - firstBVertexIndex;
for (let bQuadIndex = firstBQuadIndex; bQuadIndex < bQuads.length; bQuadIndex++) {
const bQuad = bQuads[bQuadIndex];
if (bVertexPathIDs[bQuad[0]] !== uniqueGlyphIndex + 1)
break;
for (let indexIndex = 0; indexIndex < B_QUAD_SIZE / UINT32_SIZE; indexIndex++) {
const srcIndex = bQuad[indexIndex];
if (srcIndex === UINT32_MAX)
expandedBQuads.push(srcIndex);
else
expandedBQuads.push(srcIndex + indexDelta);
}
}
textGlyphIndex++;
}
}
@ -178,9 +208,17 @@ export class GlyphStorage<Glyph extends PathfinderGlyph> {
}
}
layoutRuns() {
this.textRuns.forEach(textRun => textRun.layout());
}
get allGlyphs(): Glyph[] {
return _.flatMap(this.textRuns, textRun => textRun.glyphs);
}
readonly fontData: ArrayBuffer;
readonly font: Font;
readonly textGlyphs: Glyph[];
readonly textRuns: TextRun<Glyph>[];
readonly uniqueGlyphs: Glyph[];
}
@ -189,34 +227,25 @@ export class TextLayout<Glyph extends PathfinderGlyph> {
const font = opentype.parse(fontData);
assert(font.isSupported(), "The font type is unsupported!");
this.lineGlyphs = text.split("\n").map(line => font.stringToGlyphs(line).map(createGlyph));
const textGlyphs = _.flatten(this.lineGlyphs);
this.glyphStorage = new GlyphStorage(fontData, textGlyphs, createGlyph, font);
}
layoutText() {
const os2Table = this.glyphStorage.font.tables.os2;
const os2Table = font.tables.os2;
const lineHeight = os2Table.sTypoAscender - os2Table.sTypoDescender +
os2Table.sTypoLineGap;
this.textRuns = text.split("\n").map((line, lineNumber) => {
return new TextRun<Glyph>(line, [0.0, -lineHeight * lineNumber], font, createGlyph);
});
const currentPosition = glmatrix.vec2.create();
let glyphIndex = 0;
for (const line of this.lineGlyphs) {
for (let lineCharIndex = 0; lineCharIndex < line.length; lineCharIndex++) {
const textGlyph = this.glyphStorage.textGlyphs[glyphIndex];
textGlyph.origin = glmatrix.vec2.clone(currentPosition);
currentPosition[0] += textGlyph.advanceWidth;
glyphIndex++;
}
currentPosition[0] = 0;
currentPosition[1] -= lineHeight;
}
this.glyphStorage = new GlyphStorage(fontData, this.textRuns, createGlyph, font);
}
readonly lineGlyphs: Glyph[][];
layoutRuns() {
this.textRuns.forEach(textRun => textRun.layout());
}
get allGlyphs(): Glyph[] {
return _.flatMap(this.textRuns, textRun => textRun.glyphs);
}
readonly textRuns: TextRun<Glyph>[];
readonly glyphStorage: GlyphStorage<Glyph>;
}