From 7de664e4a900e973ad4ebfeafaa1f5532cb7c91b Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Wed, 6 Sep 2017 22:11:32 -0700 Subject: [PATCH] Implement some rudimentary text layout for the 3D demo --- demo/client/src/3d-demo.ts | 58 ++++++++-- demo/client/src/mesh-debugger.ts | 14 ++- demo/client/src/text-demo.ts | 11 +- demo/client/src/text.ts | 185 ++++++++++++++++++------------- 4 files changed, 168 insertions(+), 100 deletions(-) diff --git a/demo/client/src/3d-demo.ts b/demo/client/src/3d-demo.ts index 074086fc..91f3d7bd 100644 --- a/demo/client/src/3d-demo.ts +++ b/demo/client/src/3d-demo.ts @@ -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 { } 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 { return FONT; } - layout: TextLayout; + glyphStorage: GlyphStorage; 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!"); } diff --git a/demo/client/src/mesh-debugger.ts b/demo/client/src/mesh-debugger.ts index a50146a9..fc333a19 100644 --- a/demo/client/src/mesh-debugger.ts +++ b/demo/client/src/mesh-debugger.ts @@ -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(CHARACTER, [0, 0], font, createGlyph); + this.glyphStorage = new GlyphStorage(this.fileData, [textRun], createGlyph, font); this.glyphStorage.partition().then(meshes => { this.meshes = meshes; diff --git a/demo/client/src/text-demo.ts b/demo/client/src/text-demo.ts index ef661073..77d3e02f 100644 --- a/demo/client/src/text-demo.ts +++ b/demo/client/src/text-demo.ts @@ -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); } diff --git a/demo/client/src/text.ts b/demo/client/src/text.ts index d831ea50..41159f95 100644 --- a/demo/client/src/text.ts +++ b/demo/client/src/text.ts @@ -21,6 +21,30 @@ export const BUILTIN_FONT_URI: string = "/otf/demo"; const PARTITION_FONT_ENDPOINT_URI: string = "/partition-font"; +export class TextRun { + constructor(text: string | Glyph[], + origin: number[], + font: Font, + createGlyph: CreateGlyphFn) { + 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 { constructor(fontData: ArrayBuffer, - textGlyphs: Glyph[] | string, + textRuns: TextRun[], createGlyph: CreateGlyphFn, font?: Font) { if (font == null) { @@ -48,15 +77,12 @@ export class GlyphStorage { 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 { 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 { } } + 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[]; readonly uniqueGlyphs: Glyph[]; } @@ -189,34 +227,25 @@ export class TextLayout { 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(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[]; readonly glyphStorage: GlyphStorage; }