Factor out text layout into a separate class so it can be used by the text and 3D demos

This commit is contained in:
Patrick Walton 2017-08-31 16:11:09 -07:00
parent e448ba7b30
commit b75c327017
4 changed files with 298 additions and 216 deletions

View File

@ -7,3 +7,61 @@
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed // option. This file may not be copied, modified, or distributed
// except according to those terms. // except according to those terms.
import {AntialiasingStrategy, AntialiasingStrategyName} from "./aa-strategy";
import {mat4, vec2} from "gl-matrix";
import {ShaderMap, ShaderProgramSource} from "./shader-loader";
import {PathfinderView, Timings} from "./view";
import AppController from "./app-controller";
class ThreeDController extends AppController<ThreeDView> {
protected fileLoaded(): void {
throw new Error("Method not implemented.");
}
protected createView(canvas: HTMLCanvasElement,
commonShaderSource: string,
shaderSources: ShaderMap<ShaderProgramSource>):
ThreeDView {
throw new Error("Method not implemented.");
}
protected builtinFileURI: string;
}
class ThreeDView extends PathfinderView {
protected resized(initialSize: boolean): void {
throw new Error("Method not implemented.");
}
protected createAAStrategy(aaType: AntialiasingStrategyName, aaLevel: number):
AntialiasingStrategy {
throw new Error("Method not implemented.");
}
protected compositeIfNecessary(): void {
throw new Error("Method not implemented.");
}
protected updateTimings(timings: Timings): void {
throw new Error("Method not implemented.");
}
protected panned(): void {
throw new Error("Method not implemented.");
}
destFramebuffer: WebGLFramebuffer | null;
destAllocatedSize: vec2;
destUsedSize: vec2;
protected usedSizeFactor: vec2;
protected scale: number;
protected worldTransform: mat4;
}
function main() {
const controller = new ThreeDController;
window.addEventListener('load', () => controller.start(), false);
}
main();

View File

@ -10,7 +10,7 @@
import {AntialiasingStrategyName} from "./aa-strategy"; import {AntialiasingStrategyName} from "./aa-strategy";
import {ShaderLoader, ShaderMap, ShaderProgramSource} from './shader-loader'; import {ShaderLoader, ShaderMap, ShaderProgramSource} from './shader-loader';
import { expectNotNull, unwrapUndef, unwrapNull } from './utils'; import {expectNotNull, unwrapUndef, unwrapNull} from './utils';
import {PathfinderView} from "./view"; import {PathfinderView} from "./view";
export default abstract class AppController<View extends PathfinderView> { export default abstract class AppController<View extends PathfinderView> {

View File

@ -8,6 +8,7 @@
// option. This file may not be copied, modified, or distributed // option. This file may not be copied, modified, or distributed
// except according to those terms. // except according to those terms.
import {Font} from "opentype.js";
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as base64js from 'base64-js'; import * as base64js from 'base64-js';
import * as glmatrix from 'gl-matrix'; import * as glmatrix from 'gl-matrix';
@ -20,6 +21,7 @@ import {createFramebufferDepthTexture, QUAD_ELEMENTS, setTextureParameters} from
import {UniformMap} from './gl-utils'; import {UniformMap} from './gl-utils';
import {PathfinderMeshBuffers, PathfinderMeshData} from './meshes'; import {PathfinderMeshBuffers, PathfinderMeshData} from './meshes';
import {PathfinderShaderProgram, ShaderMap, ShaderProgramSource} from './shader-loader'; import {PathfinderShaderProgram, ShaderMap, ShaderProgramSource} from './shader-loader';
import {PathfinderGlyph, TextLayout} from "./text";
import {PathfinderError, assert, expectNotNull, UINT32_SIZE, unwrapNull, panic} from './utils'; import {PathfinderError, assert, expectNotNull, UINT32_SIZE, unwrapNull, panic} from './utils';
import {MonochromePathfinderView, Timings} from './view'; import {MonochromePathfinderView, Timings} from './view';
import AppController from './app-controller'; import AppController from './app-controller';
@ -98,10 +100,6 @@ declare module 'opentype.js' {
} }
} }
opentype.Font.prototype.isSupported = function() {
return (this as any).supported;
}
/// The separating axis theorem. /// The separating axis theorem.
function rectsIntersect(a: glmatrix.vec4, b: glmatrix.vec4): boolean { function rectsIntersect(a: glmatrix.vec4, b: glmatrix.vec4): boolean {
return a[2] > b[0] && a[3] > b[1] && a[0] < b[2] && a[1] < b[3]; return a[2] > b[0] && a[3] > b[1] && a[0] < b[2] && a[1] < b[3];
@ -116,7 +114,7 @@ class TextDemoController extends AppController<TextDemoView> {
start() { start() {
super.start(); super.start();
this.fontSize = INITIAL_FONT_SIZE; this._fontSize = INITIAL_FONT_SIZE;
this.fpsLabel = unwrapNull(document.getElementById('pf-fps-label')); this.fpsLabel = unwrapNull(document.getElementById('pf-fps-label'));
@ -130,50 +128,9 @@ class TextDemoController extends AppController<TextDemoView> {
} }
protected fileLoaded() { protected fileLoaded() {
this.font = opentype.parse(this.fileData); this.layout = new TextLayout(this.fileData, TEXT, glyph => new GlyphInstance(glyph));
if (!this.font.isSupported()) this.layout.partition().then((meshes: PathfinderMeshData) => {
throw new PathfinderError("The font type is unsupported."); this.meshes = meshes;
// Lay out the text.
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);
this.uniqueGlyphs.sort((a, b) => a.index - b.index);
this.uniqueGlyphs = _.sortedUniqBy(this.uniqueGlyphs, glyph => glyph.index);
// Build the partitioning request to the server.
//
// FIXME(pcwalton): If this is a builtin font, don't resend it to the server!
const request = {
face: {
Custom: base64js.fromByteArray(new Uint8Array(this.fileData)),
},
fontIndex: 0,
glyphs: this.uniqueGlyphs.map(glyph => {
const metrics = glyph.metrics;
return {
id: glyph.index,
transform: [1, 0, 0, 1, 0, 0],
};
}),
pointSize: this.font.unitsPerEm,
};
// Make the request.
window.fetch(PARTITION_FONT_ENDPOINT_URL, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(request),
}).then(response => response.text()).then(responseText => {
const response = JSON.parse(responseText);
if (!('Ok' in response))
panic("Failed to partition the font!");
const meshes = response.Ok.pathData;
this.meshes = new PathfinderMeshData(meshes);
this.meshesReceived(); this.meshesReceived();
}); });
} }
@ -181,7 +138,7 @@ class TextDemoController extends AppController<TextDemoView> {
private meshesReceived() { private meshesReceived() {
this.view.then(view => { this.view.then(view => {
view.attachText(); view.attachText();
view.uploadPathData(this.uniqueGlyphs.length); view.uploadPathData(this.layout.uniqueGlyphs.length);
view.attachMeshes(this.meshes); view.attachMeshes(this.meshes);
}) })
} }
@ -203,26 +160,28 @@ class TextDemoController extends AppController<TextDemoView> {
/// The font size in pixels per em. /// The font size in pixels per em.
set fontSize(newFontSize: number) { set fontSize(newFontSize: number) {
this._fontSize = newFontSize; this._fontSize = newFontSize;
this.layout
this.view.then(view => view.attachText()); this.view.then(view => view.attachText());
} }
get pixelsPerUnit(): number {
return this._fontSize / this.layout.font.unitsPerEm;
}
protected get builtinFileURI(): string { protected get builtinFileURI(): string {
return BUILTIN_FONT_URI; return BUILTIN_FONT_URI;
} }
private fpsLabel: HTMLElement; private fpsLabel: HTMLElement;
font: opentype.Font;
lineGlyphs: TextGlyph[][];
textGlyphs: TextGlyph[];
uniqueGlyphs: PathfinderGlyph[];
private _atlas: Atlas; private _atlas: Atlas;
atlasGlyphs: AtlasGlyph[]; atlasGlyphs: AtlasGlyph[];
private meshes: PathfinderMeshData; private meshes: PathfinderMeshData;
private _fontSize: number; private _fontSize: number;
layout: TextLayout<GlyphInstance>;
} }
class TextDemoView extends MonochromePathfinderView { class TextDemoView extends MonochromePathfinderView {
@ -252,64 +211,23 @@ class TextDemoView extends MonochromePathfinderView {
/// Lays out glyphs on the canvas. /// Lays out glyphs on the canvas.
private layoutGlyphs() { private layoutGlyphs() {
const lineGlyphs = this.appController.lineGlyphs; this.appController.layout.layoutText();
const textGlyphs = this.appController.textGlyphs;
const font = this.appController.font;
this.pixelsPerUnit = this.appController.fontSize / font.unitsPerEm;
const textGlyphs = this.appController.layout.textGlyphs;
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; for (let glyphIndex = 0; glyphIndex < textGlyphs.length; glyphIndex++) {
const lineHeight = (os2Table.sTypoAscender - os2Table.sTypoDescender + const textGlyph = textGlyphs[glyphIndex];
os2Table.sTypoLineGap) * this.pixelsPerUnit; const rect = textGlyph.getRect(this.appController.pixelsPerUnit);
glyphPositions.set([
const currentPosition = glmatrix.vec2.create(); rect[0], rect[3],
rect[2], rect[3],
let glyphIndex = 0; rect[0], rect[1],
for (const line of lineGlyphs) { rect[2], rect[1],
for (let lineCharIndex = 0; lineCharIndex < line.length; lineCharIndex++) { ], glyphIndex * 8);
const textGlyph = textGlyphs[glyphIndex]; glyphIndices.set(Array.from(QUAD_ELEMENTS).map(index => index + 4 * glyphIndex),
const glyphMetrics = textGlyph.metrics; glyphIndex * 6);
// 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);
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]);
// Set indices.
glyphIndices.set(Array.from(QUAD_ELEMENTS).map(index => index + 4 * glyphIndex),
glyphIndex * 6);
// Advance.
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());
@ -321,7 +239,8 @@ class TextDemoView extends MonochromePathfinderView {
} }
private buildAtlasGlyphs() { private buildAtlasGlyphs() {
const textGlyphs = this.appController.textGlyphs; const textGlyphs = this.appController.layout.textGlyphs;
const pixelsPerUnit = this.appController.pixelsPerUnit;
// Only build glyphs in view. // Only build glyphs in view.
const canvasRect = glmatrix.vec4.fromValues(-this.translation[0], const canvasRect = glmatrix.vec4.fromValues(-this.translation[0],
@ -330,22 +249,21 @@ class TextDemoView extends MonochromePathfinderView {
-this.translation[1] + this.canvas.height); -this.translation[1] + this.canvas.height);
let atlasGlyphs = let atlasGlyphs =
textGlyphs.filter(textGlyph => rectsIntersect(textGlyph.canvasRect, canvasRect)) textGlyphs.filter(glyph => rectsIntersect(glyph.getRect(pixelsPerUnit), canvasRect))
.map(textGlyph => new AtlasGlyph(textGlyph)); .map(textGlyph => new AtlasGlyph(textGlyph.opentypeGlyph));
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;
const fontSize = this.appController.fontSize; this.appController.atlas.layoutGlyphs(atlasGlyphs, pixelsPerUnit);
const unitsPerEm = this.appController.font.unitsPerEm;
this.appController.atlas.layoutGlyphs(atlasGlyphs, fontSize, unitsPerEm); const uniqueGlyphs = this.appController.layout.uniqueGlyphs;
const uniqueGlyphIndices = uniqueGlyphs.map(glyph => glyph.index);
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.
const transforms = new Float32Array((this.appController.uniqueGlyphs.length + 1) * 4); const transforms = new Float32Array((uniqueGlyphs.length + 1) * 4);
for (let glyphIndex = 0; glyphIndex < atlasGlyphs.length; glyphIndex++) { for (let glyphIndex = 0; glyphIndex < atlasGlyphs.length; glyphIndex++) {
const glyph = atlasGlyphs[glyphIndex]; const glyph = atlasGlyphs[glyphIndex];
@ -353,13 +271,13 @@ class TextDemoView extends MonochromePathfinderView {
assert(pathID >= 0, "No path ID!"); assert(pathID >= 0, "No path ID!");
pathID++; pathID++;
const atlasLocation = glyph.atlasRect; const atlasLocation = glyph.getRect(pixelsPerUnit);
const metrics = glyph.metrics; const metrics = glyph.metrics;
const left = metrics.xMin * this.pixelsPerUnit; const left = metrics.xMin * pixelsPerUnit;
const bottom = metrics.yMin * this.pixelsPerUnit; const bottom = metrics.yMin * pixelsPerUnit;
transforms[pathID * 4 + 0] = this.pixelsPerUnit; transforms[pathID * 4 + 0] = pixelsPerUnit;
transforms[pathID * 4 + 1] = this.pixelsPerUnit; transforms[pathID * 4 + 1] = pixelsPerUnit;
transforms[pathID * 4 + 2] = atlasLocation[0] - left; transforms[pathID * 4 + 2] = atlasLocation[0] - left;
transforms[pathID * 4 + 3] = atlasLocation[1] - bottom; transforms[pathID * 4 + 3] = atlasLocation[1] - bottom;
} }
@ -380,7 +298,7 @@ class TextDemoView extends MonochromePathfinderView {
} }
private setGlyphTexCoords() { private setGlyphTexCoords() {
const textGlyphs = this.appController.textGlyphs; const textGlyphs = this.appController.layout.textGlyphs;
const atlasGlyphs = this.appController.atlasGlyphs; const atlasGlyphs = this.appController.atlasGlyphs;
const atlasGlyphIndices = atlasGlyphs.map(atlasGlyph => atlasGlyph.index); const atlasGlyphIndices = atlasGlyphs.map(atlasGlyph => atlasGlyph.index);
@ -399,7 +317,7 @@ class TextDemoView extends MonochromePathfinderView {
// Set texture coordinates. // Set texture coordinates.
const atlasGlyph = atlasGlyphs[atlasGlyphIndex]; const atlasGlyph = atlasGlyphs[atlasGlyphIndex];
const atlasGlyphRect = atlasGlyph.atlasRect; const atlasGlyphRect = atlasGlyph.getRect(this.appController.pixelsPerUnit);
const atlasGlyphBL = atlasGlyphRect.slice(0, 2) as glmatrix.vec2; const atlasGlyphBL = atlasGlyphRect.slice(0, 2) as glmatrix.vec2;
const atlasGlyphTR = atlasGlyphRect.slice(2, 4) as glmatrix.vec2; const atlasGlyphTR = atlasGlyphRect.slice(2, 4) as glmatrix.vec2;
glmatrix.vec2.div(atlasGlyphBL, atlasGlyphBL, ATLAS_SIZE); glmatrix.vec2.div(atlasGlyphBL, atlasGlyphBL, ATLAS_SIZE);
@ -493,7 +411,7 @@ class TextDemoView extends MonochromePathfinderView {
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.gl.drawElements(this.gl.TRIANGLES,
this.appController.textGlyphs.length * 6, this.appController.layout.textGlyphs.length * 6,
this.gl.UNSIGNED_INT, this.gl.UNSIGNED_INT,
0); 0);
} }
@ -542,8 +460,6 @@ class TextDemoView extends MonochromePathfinderView {
atlasFramebuffer: WebGLFramebuffer; atlasFramebuffer: WebGLFramebuffer;
atlasDepthTexture: WebGLTexture; atlasDepthTexture: WebGLTexture;
private pixelsPerUnit: number;
glyphPositionsBuffer: WebGLBuffer; glyphPositionsBuffer: WebGLBuffer;
glyphTexCoordsBuffer: WebGLBuffer; glyphTexCoordsBuffer: WebGLBuffer;
glyphElementsBuffer: WebGLBuffer; glyphElementsBuffer: WebGLBuffer;
@ -557,106 +473,30 @@ interface AntialiasingStrategyTable {
ecaa: typeof ECAAStrategy; ecaa: typeof ECAAStrategy;
} }
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() {
return this._canvasRect;
}
set canvasRect(rect: Rect) {
this._canvasRect = rect;
}
private _canvasRect: Rect;
}
class AtlasGlyph extends PathfinderGlyph {
constructor(glyph: opentype.Glyph | PathfinderGlyph) {
super(glyph);
this._atlasRect = glmatrix.vec4.create();
}
get atlasRect() {
return this._atlasRect;
}
set atlasRect(rect: Rect) {
this._atlasRect = rect;
}
get atlasSize(): Size2D {
let atlasSize = glmatrix.vec2.create();
glmatrix.vec2.sub(atlasSize,
this._atlasRect.slice(2, 4) as glmatrix.vec2,
this._atlasRect.slice(0, 2) as glmatrix.vec2);
return atlasSize;
}
private _atlasRect: Rect;
}
class Atlas { class Atlas {
constructor() { constructor() {
this._texture = null; this._texture = null;
this._usedSize = glmatrix.vec2.create(); this._usedSize = glmatrix.vec2.create();
} }
layoutGlyphs(glyphs: AtlasGlyph[], fontSize: number, unitsPerEm: number) { layoutGlyphs(glyphs: AtlasGlyph[], pixelsPerUnit: number) {
const pixelsPerUnit = fontSize / unitsPerEm;
let nextOrigin = glmatrix.vec2.create(); let nextOrigin = glmatrix.vec2.create();
let shelfBottom = 0.0; let shelfBottom = 0.0;
for (const glyph of glyphs) { for (const glyph of glyphs) {
const metrics = glyph.metrics; // Place the glyph, and advance the origin.
glyph.setPixelPosition(nextOrigin, pixelsPerUnit);
nextOrigin[0] = glyph.getRect(pixelsPerUnit)[2];
const glyphSize = glmatrix.vec2.fromValues(metrics.xMax - metrics.xMin, // If the glyph overflowed the shelf, make a new one and reposition the glyph.
metrics.yMax - metrics.yMin); if (nextOrigin[0] > ATLAS_SIZE[0]) {
glmatrix.vec2.scale(glyphSize, glyphSize, pixelsPerUnit);
glmatrix.vec2.ceil(glyphSize, glyphSize);
// Make a new shelf if necessary.
const initialGlyphRight = nextOrigin[0] + glyphSize[0] + 2;
if (initialGlyphRight > ATLAS_SIZE[0])
nextOrigin = glmatrix.vec2.fromValues(0.0, shelfBottom); nextOrigin = glmatrix.vec2.fromValues(0.0, shelfBottom);
glyph.setPixelPosition(nextOrigin, pixelsPerUnit);
nextOrigin[0] = glyph.getRect(pixelsPerUnit)[2];
}
const glyphRect = glmatrix.vec4.fromValues(nextOrigin[0] + 1, // Grow the shelf as necessary.
nextOrigin[1] + 1, shelfBottom = Math.max(shelfBottom, glyph.getRect(pixelsPerUnit)[3]);
nextOrigin[0] + glyphSize[0] + 1,
nextOrigin[1] + glyphSize[1] + 1);
glyph.atlasRect = glyphRect;
nextOrigin[0] = glyphRect[2] + 1;
shelfBottom = Math.max(shelfBottom, glyphRect[3]);
} }
// FIXME(pcwalton): Could be more precise if we don't have a full row. // FIXME(pcwalton): Could be more precise if we don't have a full row.
@ -692,6 +532,53 @@ class Atlas {
private _usedSize: Size2D; private _usedSize: Size2D;
} }
class AtlasGlyph extends PathfinderGlyph {
constructor(glyph: opentype.Glyph) {
super(glyph);
}
getRect(pixelsPerUnit: number): glmatrix.vec4 {
const glyphSize = glmatrix.vec2.fromValues(this.metrics.xMax - this.metrics.xMin,
this.metrics.yMax - this.metrics.yMin);
glmatrix.vec2.scale(glyphSize, glyphSize, pixelsPerUnit);
glmatrix.vec2.ceil(glyphSize, glyphSize);
const glyphBL = glmatrix.vec2.create(), glyphTR = glmatrix.vec2.create();
glmatrix.vec2.scale(glyphBL, this.position, pixelsPerUnit);
glmatrix.vec2.add(glyphBL, glyphBL, [1.0, 1.0]);
glmatrix.vec2.add(glyphTR, glyphBL, glyphSize);
glmatrix.vec2.add(glyphTR, glyphTR, [1.0, 1.0]);
return glmatrix.vec4.fromValues(glyphBL[0], glyphBL[1], glyphTR[0], glyphTR[1]);
}
}
class GlyphInstance extends PathfinderGlyph {
constructor(glyph: opentype.Glyph) {
super(glyph);
}
getRect(pixelsPerUnit: number): glmatrix.vec4 {
// Determine the atlas size.
const atlasSize = glmatrix.vec2.fromValues(this.metrics.xMax - this.metrics.xMin,
this.metrics.yMax - this.metrics.yMin);
glmatrix.vec2.scale(atlasSize, atlasSize, pixelsPerUnit);
glmatrix.vec2.ceil(atlasSize, atlasSize);
// Set positions.
const textGlyphBL = glmatrix.vec2.create(), textGlyphTR = glmatrix.vec2.create();
const offset = glmatrix.vec2.fromValues(this.metrics.leftSideBearing,
this.metrics.yMin);
glmatrix.vec2.add(textGlyphBL, this.position, offset);
glmatrix.vec2.scale(textGlyphBL, textGlyphBL, pixelsPerUnit);
glmatrix.vec2.round(textGlyphBL, textGlyphBL);
glmatrix.vec2.add(textGlyphTR, textGlyphBL, atlasSize);
return glmatrix.vec4.fromValues(textGlyphBL[0], textGlyphBL[1],
textGlyphTR[0], textGlyphTR[1]);
}
}
const ANTIALIASING_STRATEGIES: AntialiasingStrategyTable = { const ANTIALIASING_STRATEGIES: AntialiasingStrategyTable = {
none: NoAAStrategy, none: NoAAStrategy,
ssaa: SSAAStrategy, ssaa: SSAAStrategy,

137
demo/client/src/text.ts Normal file
View File

@ -0,0 +1,137 @@
// pathfinder/client/src/text.ts
//
// Copyright © 2017 The Pathfinder Project Developers.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
import {Font, Metrics} from 'opentype.js';
import * as base64js from 'base64-js';
import * as glmatrix from 'gl-matrix';
import * as _ from 'lodash';
import * as opentype from "opentype.js";
import {PathfinderMeshData} from "./meshes";
import {assert, panic} from "./utils";
const PARTITION_FONT_ENDPOINT_URI: string = "/partition-font";
opentype.Font.prototype.isSupported = function() {
return (this as any).supported;
}
export class TextLayout<Glyph extends PathfinderGlyph> {
constructor(fontData: ArrayBuffer,
text: string,
createGlyph: (glyph: opentype.Glyph) => Glyph) {
this.fontData = fontData;
this.font = opentype.parse(fontData);
assert(this.font.isSupported(), "The font type is unsupported!");
// Lay out the text.
this.lineGlyphs = text.split("\n").map(line => {
return this.font.stringToGlyphs(line).map(createGlyph);
});
this.textGlyphs = _.flatten(this.lineGlyphs);
// Determine all glyphs potentially needed.
this.uniqueGlyphs = this.textGlyphs.map(textGlyph => textGlyph);
this.uniqueGlyphs.sort((a, b) => a.index - b.index);
this.uniqueGlyphs = _.sortedUniqBy(this.uniqueGlyphs, glyph => glyph.index);
}
partition(): Promise<PathfinderMeshData> {
// Build the partitioning request to the server.
//
// FIXME(pcwalton): If this is a builtin font, don't resend it to the server!
const request = {
face: {
Custom: base64js.fromByteArray(new Uint8Array(this.fontData)),
},
fontIndex: 0,
glyphs: this.uniqueGlyphs.map(glyph => {
const metrics = glyph.metrics;
return {
id: glyph.index,
transform: [1, 0, 0, 1, 0, 0],
};
}),
pointSize: this.font.unitsPerEm,
};
// Make the request.
return window.fetch(PARTITION_FONT_ENDPOINT_URI, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(request),
}).then(response => response.text()).then(responseText => {
const response = JSON.parse(responseText);
if (!('Ok' in response))
panic("Failed to partition the font!");
return new PathfinderMeshData(response.Ok.pathData);
});
}
layoutText() {
const os2Table = this.font.tables.os2;
const lineHeight = os2Table.sTypoAscender - os2Table.sTypoDescender +
os2Table.sTypoLineGap;
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.textGlyphs[glyphIndex];
textGlyph.position = glmatrix.vec2.clone(currentPosition);
currentPosition[0] += textGlyph.advanceWidth;
glyphIndex++;
}
currentPosition[0] = 0;
currentPosition[1] -= lineHeight;
}
}
readonly fontData: ArrayBuffer;
readonly font: Font;
readonly lineGlyphs: Glyph[][];
readonly textGlyphs: Glyph[];
readonly uniqueGlyphs: Glyph[];
}
export abstract class PathfinderGlyph {
constructor(glyph: opentype.Glyph) {
this.opentypeGlyph = glyph;
this._metrics = null;
this.position = glmatrix.vec2.create();
}
get index(): number {
return (this.opentypeGlyph as any).index;
}
get metrics(): opentype.Metrics {
if (this._metrics == null)
this._metrics = this.opentypeGlyph.getMetrics();
return this._metrics;
}
get advanceWidth(): number {
return this.opentypeGlyph.advanceWidth;
}
setPixelPosition(pixelPosition: glmatrix.vec2, pixelsPerUnit: number): void {
glmatrix.vec2.scale(this.position, pixelPosition, 1.0 / pixelsPerUnit);
}
readonly opentypeGlyph: opentype.Glyph;
private _metrics: Metrics | null;
/// In font units.
position: glmatrix.vec2;
}