Implement some rudimentary text layout for the 3D demo
This commit is contained in:
parent
2cce20db10
commit
7de664e4a9
|
@ -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!");
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,8 +132,9 @@ 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];
|
||||
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;
|
||||
|
@ -157,6 +184,9 @@ export class GlyphStorage<Glyph extends PathfinderGlyph> {
|
|||
expandedBQuads.push(srcIndex + indexDelta);
|
||||
}
|
||||
}
|
||||
|
||||
textGlyphIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -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++;
|
||||
this.glyphStorage = new GlyphStorage(fontData, this.textRuns, createGlyph, font);
|
||||
}
|
||||
|
||||
currentPosition[0] = 0;
|
||||
currentPosition[1] -= lineHeight;
|
||||
}
|
||||
layoutRuns() {
|
||||
this.textRuns.forEach(textRun => textRun.layout());
|
||||
}
|
||||
|
||||
readonly lineGlyphs: Glyph[][];
|
||||
get allGlyphs(): Glyph[] {
|
||||
return _.flatMap(this.textRuns, textRun => textRun.glyphs);
|
||||
}
|
||||
|
||||
readonly textRuns: TextRun<Glyph>[];
|
||||
readonly glyphStorage: GlyphStorage<Glyph>;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue