Rewrite the text run and glyph store classes for simplicity

This commit is contained in:
Patrick Walton 2017-09-27 13:02:32 -07:00
parent 17b34685a1
commit 1675944dfb
5 changed files with 398 additions and 364 deletions

View File

@ -17,8 +17,8 @@ import {PerspectiveCamera} from "./camera";
import {mat4, vec2} from "gl-matrix"; import {mat4, vec2} from "gl-matrix";
import {PathfinderMeshData} from "./meshes"; import {PathfinderMeshData} from "./meshes";
import {ShaderMap, ShaderProgramSource} from "./shader-loader"; import {ShaderMap, ShaderProgramSource} from "./shader-loader";
import {BUILTIN_FONT_URI, ExpandedMeshData, TextFrameGlyphStorage, PathfinderGlyph} from "./text"; import {BUILTIN_FONT_URI, ExpandedMeshData} from "./text";
import {Hint, SimpleTextLayout, TextFrame, TextRun} from "./text"; import { Hint, TextFrame, TextRun, GlyphStore, PathfinderFont } from "./text";
import {PathfinderError, assert, panic, unwrapNull} from "./utils"; import {PathfinderError, assert, panic, unwrapNull} from "./utils";
import {PathfinderDemoView, Timings} from "./view"; import {PathfinderDemoView, Timings} from "./view";
import SSAAStrategy from "./ssaa-strategy"; import SSAAStrategy from "./ssaa-strategy";
@ -128,24 +128,25 @@ class ThreeDController extends DemoAppController<ThreeDView> {
} }
protected fileLoaded(fileData: ArrayBuffer): void { protected fileLoaded(fileData: ArrayBuffer): void {
const font = opentype.parse(fileData); const font = new PathfinderFont(fileData);
assert(font.isSupported(), "The font type is unsupported!");
this.monumentPromise.then(monument => this.layoutMonument(font, fileData, monument)); this.monumentPromise.then(monument => this.layoutMonument(font, fileData, monument));
} }
private layoutMonument(font: opentype.Font, fileData: ArrayBuffer, monument: MonumentSide[]) { private layoutMonument(font: PathfinderFont, fileData: ArrayBuffer, monument: MonumentSide[]) {
const createGlyph = (glyph: opentype.Glyph) => new ThreeDGlyph(glyph); this.textFrames = [];
let textFrames = []; let glyphsNeeded: number[] = [];
for (const monumentSide of monument) { for (const monumentSide of monument) {
let textRuns = []; let textRuns = [];
for (let lineNumber = 0; lineNumber < monumentSide.lines.length; lineNumber++) { for (let lineNumber = 0; lineNumber < monumentSide.lines.length; lineNumber++) {
const line = monumentSide.lines[lineNumber]; const line = monumentSide.lines[lineNumber];
const lineY = -lineNumber * font.lineHeight(); const lineY = -lineNumber * font.opentypeFont.lineHeight();
const lineGlyphs = line.names.map(string => { const lineGlyphs = line.names.map(string => {
const glyphs = font.stringToGlyphs(string).map(createGlyph); const glyphs = font.opentypeFont.stringToGlyphs(string);
return { glyphs: glyphs, width: _.sumBy(glyphs, glyph => glyph.advanceWidth) }; const glyphIDs = glyphs.map(glyph => (glyph as any).index);
const width = _.sumBy(glyphs, glyph => glyph.advanceWidth);
return { glyphs: glyphIDs, width: width };
}); });
const usedSpace = _.sumBy(lineGlyphs, 'width'); const usedSpace = _.sumBy(lineGlyphs, 'width');
@ -155,20 +156,27 @@ class ThreeDController extends DemoAppController<ThreeDView> {
let currentX = 0.0; let currentX = 0.0;
for (const glyphInfo of lineGlyphs) { for (const glyphInfo of lineGlyphs) {
const textRunOrigin = [currentX, lineY]; const textRunOrigin = [currentX, lineY];
textRuns.push(new TextRun(glyphInfo.glyphs, textRunOrigin, font, createGlyph)); const textRun = new TextRun(glyphInfo.glyphs, textRunOrigin, font);
textRun.layout();
textRuns.push(textRun);
currentX += glyphInfo.width + spacing; currentX += glyphInfo.width + spacing;
} }
} }
textFrames.push(new TextFrame(textRuns, font)); const textFrame = new TextFrame(textRuns, font);
this.textFrames.push(textFrame);
glyphsNeeded.push(...textFrame.allGlyphIDs);
} }
this.glyphStorage = new TextFrameGlyphStorage(fileData, textFrames, font); glyphsNeeded.sort((a, b) => a - b);
this.glyphStorage.layoutRuns(); glyphsNeeded = _.sortedUniq(glyphsNeeded);
this.glyphStorage.partition().then(result => { this.glyphStore = new GlyphStore(font, glyphsNeeded);
this.glyphStore.partition().then(result => {
this.baseMeshes = result.meshes; this.baseMeshes = result.meshes;
this.expandedMeshes = this.glyphStorage.expandMeshes(this.baseMeshes); this.expandedMeshes = this.textFrames.map(textFrame => {
return textFrame.expandMeshes(this.baseMeshes, glyphsNeeded);
});
this.view.then(view => { this.view.then(view => {
view.uploadPathColors(this.expandedMeshes.length); view.uploadPathColors(this.expandedMeshes.length);
view.uploadPathTransforms(this.expandedMeshes.length); view.uploadPathTransforms(this.expandedMeshes.length);
@ -191,7 +199,8 @@ class ThreeDController extends DemoAppController<ThreeDView> {
return FONT; return FONT;
} }
glyphStorage: TextFrameGlyphStorage<ThreeDGlyph>; textFrames: TextFrame[];
glyphStore: GlyphStore;
private baseMeshes: PathfinderMeshData; private baseMeshes: PathfinderMeshData;
private expandedMeshes: ExpandedMeshData[]; private expandedMeshes: ExpandedMeshData[];
@ -222,9 +231,8 @@ class ThreeDView extends PathfinderDemoView {
} }
protected pathColorsForObject(textFrameIndex: number): Uint8Array { protected pathColorsForObject(textFrameIndex: number): Uint8Array {
const textFrame = this.appController.glyphStorage.textFrames[textFrameIndex]; const textFrame = this.appController.textFrames[textFrameIndex];
const textGlyphs = textFrame.allGlyphs; const pathCount = textFrame.totalGlyphCount;
const pathCount = textGlyphs.length;
const pathColors = new Uint8Array(4 * (pathCount + 1)); const pathColors = new Uint8Array(4 * (pathCount + 1));
for (let pathIndex = 0; pathIndex < pathCount; pathIndex++) for (let pathIndex = 0; pathIndex < pathCount; pathIndex++)
@ -234,18 +242,24 @@ class ThreeDView extends PathfinderDemoView {
} }
protected pathTransformsForObject(textFrameIndex: number): Float32Array { protected pathTransformsForObject(textFrameIndex: number): Float32Array {
const textFrame = this.appController.glyphStorage.textFrames[textFrameIndex]; const textFrame = this.appController.textFrames[textFrameIndex];
const textGlyphs = textFrame.allGlyphs; const pathCount = textFrame.totalGlyphCount;
const pathCount = textGlyphs.length;
const hint = new Hint(this.appController.glyphStorage.font, PIXELS_PER_UNIT, false); const hint = new Hint(this.appController.glyphStore.font, PIXELS_PER_UNIT, false);
const pathTransforms = new Float32Array(4 * (pathCount + 1)); const pathTransforms = new Float32Array(4 * (pathCount + 1));
for (let pathIndex = 0; pathIndex < pathCount; pathIndex++) { let globalPathIndex = 0;
const textGlyph = textGlyphs[pathIndex]; for (const run of textFrame.runs) {
const glyphOrigin = textGlyph.calculatePixelOrigin(hint, PIXELS_PER_UNIT); for (let pathIndex = 0;
pathTransforms.set([1, 1, glyphOrigin[0], glyphOrigin[1]], (pathIndex + 1) * 4); pathIndex < run.glyphIDs.length;
pathIndex++, globalPathIndex++) {
const glyphOrigin = run.calculatePixelOriginForGlyphAt(pathIndex,
PIXELS_PER_UNIT,
hint);
pathTransforms.set([1, 1, glyphOrigin[0], glyphOrigin[1]],
(globalPathIndex + 1) * 4);
}
} }
return pathTransforms; return pathTransforms;
@ -380,12 +394,6 @@ class ThreeDView extends PathfinderDemoView {
camera: PerspectiveCamera; camera: PerspectiveCamera;
} }
class ThreeDGlyph extends PathfinderGlyph {
constructor(glyph: opentype.Glyph) {
super(glyph);
}
}
function main() { function main() {
const controller = new ThreeDController; const controller = new ThreeDController;
window.addEventListener('load', () => controller.start(), false); window.addEventListener('load', () => controller.start(), false);

View File

@ -13,7 +13,7 @@ import * as opentype from "opentype.js";
import { AppController, DemoAppController } from "./app-controller"; import { AppController, DemoAppController } from "./app-controller";
import {PathfinderMeshData} from "./meshes"; import {PathfinderMeshData} from "./meshes";
import { BUILTIN_FONT_URI, TextFrameGlyphStorage, PathfinderGlyph, TextFrame, TextRun, ExpandedMeshData } from "./text"; import { BUILTIN_FONT_URI, TextFrame, TextRun, ExpandedMeshData, GlyphStore, PathfinderFont } from "./text";
import { assert, unwrapNull, PathfinderError } from "./utils"; import { assert, unwrapNull, PathfinderError } from "./utils";
import { PathfinderDemoView, Timings, MonochromePathfinderView } from "./view"; import { PathfinderDemoView, Timings, MonochromePathfinderView } from "./view";
import { ShaderMap, ShaderProgramSource } from "./shader-loader"; import { ShaderMap, ShaderProgramSource } from "./shader-loader";
@ -75,26 +75,28 @@ class BenchmarkAppController extends DemoAppController<BenchmarkTestView> {
} }
protected fileLoaded(fileData: ArrayBuffer): void { protected fileLoaded(fileData: ArrayBuffer): void {
const font = opentype.parse(fileData); const font = new PathfinderFont(fileData);
this.font = font; this.font = font;
assert(this.font.isSupported(), "The font type is unsupported!");
const createGlyph = (glyph: opentype.Glyph) => new BenchmarkGlyph(glyph); const textRun = new TextRun(STRING, [0, 0], font);
const textRun = new TextRun<BenchmarkGlyph>(STRING, [0, 0], font, createGlyph); textRun.layout();
this.textRun = textRun; this.textRun = textRun;
const textFrame = new TextFrame([textRun], font); const textFrame = new TextFrame([textRun], font);
this.glyphStorage = new TextFrameGlyphStorage(fileData, [textFrame], font);
this.glyphStorage.partition().then(result => { const glyphIDs = textFrame.allGlyphIDs;
glyphIDs.sort((a, b) => a - b);
this.glyphStore = new GlyphStore(font, glyphIDs);
this.glyphStore.partition().then(result => {
this.baseMeshes = result.meshes; this.baseMeshes = result.meshes;
const partitionTime = result.time / this.glyphStorage.uniqueGlyphs.length * 1e6; const partitionTime = result.time / this.glyphStore.glyphIDs.length * 1e6;
const timeLabel = this.resultsPartitioningTimeLabel; const timeLabel = this.resultsPartitioningTimeLabel;
while (timeLabel.firstChild != null) while (timeLabel.firstChild != null)
timeLabel.removeChild(timeLabel.firstChild); timeLabel.removeChild(timeLabel.firstChild);
timeLabel.appendChild(document.createTextNode("" + partitionTime)); timeLabel.appendChild(document.createTextNode("" + partitionTime));
const expandedMeshes = this.glyphStorage.expandMeshes(this.baseMeshes)[0]; const expandedMeshes = textFrame.expandMeshes(this.baseMeshes, glyphIDs);
this.expandedMeshes = expandedMeshes; this.expandedMeshes = expandedMeshes;
this.view.then(view => { this.view.then(view => {
@ -162,7 +164,7 @@ class BenchmarkAppController extends DemoAppController<BenchmarkTestView> {
private resultsTableBody: HTMLTableSectionElement; private resultsTableBody: HTMLTableSectionElement;
private resultsPartitioningTimeLabel: HTMLSpanElement; private resultsPartitioningTimeLabel: HTMLSpanElement;
private glyphStorage: TextFrameGlyphStorage<BenchmarkGlyph>; private glyphStore: GlyphStore;
private baseMeshes: PathfinderMeshData; private baseMeshes: PathfinderMeshData;
private expandedMeshes: ExpandedMeshData; private expandedMeshes: ExpandedMeshData;
@ -170,8 +172,8 @@ class BenchmarkAppController extends DemoAppController<BenchmarkTestView> {
private elapsedTimes: ElapsedTime[]; private elapsedTimes: ElapsedTime[];
private partitionTime: number; private partitionTime: number;
font: opentype.Font | null; font: PathfinderFont | null;
textRun: TextRun<BenchmarkGlyph> | null; textRun: TextRun | null;
} }
class BenchmarkTestView extends MonochromePathfinderView { class BenchmarkTestView extends MonochromePathfinderView {
@ -212,13 +214,16 @@ class BenchmarkTestView extends MonochromePathfinderView {
let currentX = 0, currentY = 0; let currentX = 0, currentY = 0;
const availableWidth = this.canvas.width / this.pixelsPerUnit; const availableWidth = this.canvas.width / this.pixelsPerUnit;
const lineHeight = unwrapNull(this.appController.font).lineHeight(); const lineHeight = unwrapNull(this.appController.font).opentypeFont.lineHeight();
for (let glyphIndex = 0; glyphIndex < STRING.length; glyphIndex++) { for (let glyphIndex = 0; glyphIndex < STRING.length; glyphIndex++) {
const glyph = unwrapNull(this.appController.textRun).glyphs[glyphIndex]; const glyphID = unwrapNull(this.appController.textRun).glyphIDs[glyphIndex];
pathTransforms.set([1, 1, currentX, currentY], (glyphIndex + 1) * 4); pathTransforms.set([1, 1, currentX, currentY], (glyphIndex + 1) * 4);
currentX += glyph.advanceWidth; currentX += unwrapNull(this.appController.font).opentypeFont
.glyphs
.get(glyphID)
.advanceWidth;
if (currentX > availableWidth) { if (currentX > availableWidth) {
currentX = 0; currentX = 0;
currentY += lineHeight; currentY += lineHeight;
@ -230,14 +235,14 @@ class BenchmarkTestView extends MonochromePathfinderView {
protected renderingFinished(): void { protected renderingFinished(): void {
if (this.renderingPromiseCallback != null) { if (this.renderingPromiseCallback != null) {
const glyphCount = unwrapNull(this.appController.textRun).glyphs.length; const glyphCount = unwrapNull(this.appController.textRun).glyphIDs.length;
const usPerGlyph = this.lastTimings.rendering * 1000.0 / glyphCount; const usPerGlyph = this.lastTimings.rendering * 1000.0 / glyphCount;
this.renderingPromiseCallback(usPerGlyph); this.renderingPromiseCallback(usPerGlyph);
} }
} }
uploadHints(): void { uploadHints(): void {
const glyphCount = unwrapNull(this.appController.textRun).glyphs.length; const glyphCount = unwrapNull(this.appController.textRun).glyphIDs.length;
const pathHints = new Float32Array((glyphCount + 1) * 4); const pathHints = new Float32Array((glyphCount + 1) * 4);
const pathHintsBufferTexture = new PathfinderBufferTexture(this.gl, 'uPathHints'); const pathHintsBufferTexture = new PathfinderBufferTexture(this.gl, 'uPathHints');
@ -276,7 +281,7 @@ class BenchmarkTestView extends MonochromePathfinderView {
} }
private get pixelsPerUnit(): number { private get pixelsPerUnit(): number {
return this._pixelsPerEm / unwrapNull(this.appController.font).unitsPerEm; return this._pixelsPerEm / unwrapNull(this.appController.font).opentypeFont.unitsPerEm;
} }
get pixelsPerEm(): number { get pixelsPerEm(): number {
@ -304,8 +309,6 @@ class BenchmarkTestView extends MonochromePathfinderView {
protected camera: OrthographicCamera; protected camera: OrthographicCamera;
} }
class BenchmarkGlyph extends PathfinderGlyph {}
function main() { function main() {
const controller = new BenchmarkAppController; const controller = new BenchmarkAppController;
window.addEventListener('load', () => controller.start(), false); window.addEventListener('load', () => controller.start(), false);

View File

@ -20,8 +20,8 @@ import {B_QUAD_UPPER_CONTROL_POINT_VERTEX_OFFSET, B_QUAD_LOWER_LEFT_VERTEX_OFFSE
import {B_QUAD_LOWER_RIGHT_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 {B_QUAD_LOWER_CONTROL_POINT_VERTEX_OFFSET, PathfinderMeshData} from "./meshes";
import {SVGLoader, BUILTIN_SVG_URI} from './svg-loader'; import {SVGLoader, BUILTIN_SVG_URI} from './svg-loader';
import {BUILTIN_FONT_URI, TextFrameGlyphStorage, PathfinderGlyph, TextRun} from "./text"; import {BUILTIN_FONT_URI, TextRun} from "./text";
import {GlyphStorage, TextFrame} from "./text"; import { GlyphStore, TextFrame, PathfinderFont } from "./text";
import {unwrapNull, UINT32_SIZE, UINT32_MAX, assert} from "./utils"; import {unwrapNull, UINT32_SIZE, UINT32_MAX, assert} from "./utils";
import {PathfinderView} from "./view"; import {PathfinderView} from "./view";
import {Font} from 'opentype.js'; import {Font} from 'opentype.js';
@ -111,22 +111,21 @@ class MeshDebuggerAppController extends AppController {
} }
private fontLoaded(fileData: ArrayBuffer): void { private fontLoaded(fileData: ArrayBuffer): void {
this.file = opentype.parse(fileData); this.file = new PathfinderFont(fileData);
assert(this.file.isSupported(), "The font type is unsupported!");
this.fileData = fileData; this.fileData = fileData;
const glyphCount = this.file.numGlyphs; const glyphCount = this.file.opentypeFont.numGlyphs;
for (let glyphIndex = 1; glyphIndex < glyphCount; glyphIndex++) { for (let glyphIndex = 1; glyphIndex < glyphCount; glyphIndex++) {
const newOption = document.createElement('option'); const newOption = document.createElement('option');
newOption.value = "" + glyphIndex; newOption.value = "" + glyphIndex;
const glyphName = this.file.glyphIndexToName(glyphIndex); const glyphName = this.file.opentypeFont.glyphIndexToName(glyphIndex);
newOption.appendChild(document.createTextNode(glyphName)); newOption.appendChild(document.createTextNode(glyphName));
this.fontPathSelect.appendChild(newOption); this.fontPathSelect.appendChild(newOption);
} }
// Automatically load a path if this is the initial pageload. // Automatically load a path if this is the initial pageload.
if (this.meshes == null) if (this.meshes == null)
this.loadPath(this.file.charToGlyph(CHARACTER)); this.loadPath(this.file.opentypeFont.charToGlyph(CHARACTER));
} }
private svgLoaded(fileData: ArrayBuffer): void { private svgLoaded(fileData: ArrayBuffer): void {
@ -148,14 +147,13 @@ class MeshDebuggerAppController extends AppController {
let promise: Promise<PathfinderMeshData>; let promise: Promise<PathfinderMeshData>;
if (this.file instanceof opentype.Font && this.fileData != null) { if (this.file instanceof PathfinderFont && this.fileData != null) {
if (opentypeGlyph == null) { if (opentypeGlyph == null) {
const glyphIndex = parseInt(this.fontPathSelect.selectedOptions[0].value); const glyphIndex = parseInt(this.fontPathSelect.selectedOptions[0].value);
opentypeGlyph = this.file.glyphs.get(glyphIndex); opentypeGlyph = this.file.opentypeFont.glyphs.get(glyphIndex);
} }
const glyph = new MeshDebuggerGlyph(opentypeGlyph); const glyphStorage = new GlyphStore(this.file, [(opentypeGlyph as any).index]);
const glyphStorage = new GlyphStorage(this.fileData, [glyph], this.file);
promise = glyphStorage.partition().then(result => result.meshes); promise = glyphStorage.partition().then(result => result.meshes);
} else if (this.file instanceof SVGLoader) { } else if (this.file instanceof SVGLoader) {
promise = this.file.partition(this.fontPathSelect.selectedIndex); promise = this.file.partition(this.fontPathSelect.selectedIndex);
@ -171,7 +169,7 @@ class MeshDebuggerAppController extends AppController {
protected readonly defaultFile: string = FONT; protected readonly defaultFile: string = FONT;
private file: Font | SVGLoader | null; private file: PathfinderFont | SVGLoader | null;
private fileType: FileType; private fileType: FileType;
private fileData: ArrayBuffer | null; private fileData: ArrayBuffer | null;
@ -318,8 +316,6 @@ class MeshDebuggerView extends PathfinderView {
camera: OrthographicCamera; camera: OrthographicCamera;
} }
class MeshDebuggerGlyph extends PathfinderGlyph {}
function getPosition(positions: Float32Array, vertexIndex: number): Float32Array | null { function getPosition(positions: Float32Array, vertexIndex: number): Float32Array | null {
if (vertexIndex == UINT32_MAX) if (vertexIndex == UINT32_MAX)
return null; return null;

View File

@ -8,7 +8,6 @@
// 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';
@ -23,12 +22,14 @@ 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 {BUILTIN_FONT_URI, Hint, PathfinderGlyph, SimpleTextLayout} from "./text"; import {BUILTIN_FONT_URI, Hint, SimpleTextLayout, GlyphStore, calculatePixelXMin} from "./text";
import {calculatePixelDescent, calculatePixelRectForGlyph, PathfinderFont} from "./text";
import {PathfinderError, UINT32_SIZE, assert, expectNotNull, scaleRect, panic} from './utils'; import {PathfinderError, UINT32_SIZE, assert, expectNotNull, scaleRect, panic} from './utils';
import {unwrapNull} from './utils'; import {unwrapNull} from './utils';
import { MonochromePathfinderView, Timings, TIMINGS } from './view'; import { MonochromePathfinderView, Timings, TIMINGS } from './view';
import PathfinderBufferTexture from './buffer-texture'; import PathfinderBufferTexture from './buffer-texture';
import SSAAStrategy from './ssaa-strategy'; import SSAAStrategy from './ssaa-strategy';
import { Metrics } from 'opentype.js';
const DEFAULT_TEXT: string = const DEFAULT_TEXT: string =
`Twas brillig, and the slithy toves `Twas brillig, and the slithy toves
@ -159,7 +160,6 @@ class TextDemoController extends DemoAppController<TextDemoView> {
private updateText(): void { private updateText(): void {
this.text = this.editTextArea.value; this.text = this.editTextArea.value;
//this.recreateLayout();
window.jQuery(this.editTextModal).modal('hide'); window.jQuery(this.editTextModal).modal('hide');
} }
@ -171,16 +171,23 @@ class TextDemoController extends DemoAppController<TextDemoView> {
} }
protected fileLoaded(fileData: ArrayBuffer) { protected fileLoaded(fileData: ArrayBuffer) {
this.recreateLayout(fileData); const font = new PathfinderFont(fileData);
this.recreateLayout(font);
} }
private recreateLayout(fileData: ArrayBuffer) { private recreateLayout(font: PathfinderFont) {
const newLayout = new SimpleTextLayout(fileData, const newLayout = new SimpleTextLayout(font, this.text);
this.text,
glyph => new GlyphInstance(glyph)); let uniqueGlyphIDs = newLayout.textFrame.allGlyphIDs;
newLayout.glyphStorage.partition().then(result => { uniqueGlyphIDs.sort((a, b) => a - b);
uniqueGlyphIDs = _.sortedUniq(uniqueGlyphIDs);
const glyphStore = new GlyphStore(font, uniqueGlyphIDs);
glyphStore.partition().then(result => {
this.view.then(view => { this.view.then(view => {
this.font = font;
this.layout = newLayout; this.layout = newLayout;
this.glyphStore = glyphStore;
this.meshes = result.meshes; this.meshes = result.meshes;
view.attachText(); view.attachText();
@ -206,7 +213,7 @@ class TextDemoController extends DemoAppController<TextDemoView> {
} }
get pixelsPerUnit(): number { get pixelsPerUnit(): number {
return this._fontSize / this.layout.glyphStorage.font.unitsPerEm; return this._fontSize / this.font.opentypeFont.unitsPerEm;
} }
get useHinting(): boolean { get useHinting(): boolean {
@ -214,7 +221,7 @@ class TextDemoController extends DemoAppController<TextDemoView> {
} }
createHint(): Hint { createHint(): Hint {
return new Hint(this.layout.glyphStorage.font, this.pixelsPerUnit, this.useHinting); return new Hint(this.font, this.pixelsPerUnit, this.useHinting);
} }
protected get builtinFileURI(): string { protected get builtinFileURI(): string {
@ -225,6 +232,8 @@ class TextDemoController extends DemoAppController<TextDemoView> {
return DEFAULT_FONT; return DEFAULT_FONT;
} }
font: PathfinderFont;
private hintingSelect: HTMLSelectElement; private hintingSelect: HTMLSelectElement;
private editTextModal: HTMLElement; private editTextModal: HTMLElement;
@ -239,7 +248,8 @@ class TextDemoController extends DemoAppController<TextDemoView> {
private text: string; private text: string;
layout: SimpleTextLayout<GlyphInstance>; layout: SimpleTextLayout;
glyphStore: GlyphStore;
} }
class TextDemoView extends MonochromePathfinderView { class TextDemoView extends MonochromePathfinderView {
@ -285,26 +295,32 @@ class TextDemoView extends MonochromePathfinderView {
let textBounds = layout.textFrame.bounds; let textBounds = layout.textFrame.bounds;
this.camera.bounds = textBounds; this.camera.bounds = textBounds;
const textGlyphs = layout.glyphStorage.allGlyphs; const totalGlyphCount = layout.textFrame.totalGlyphCount;
const glyphPositions = new Float32Array(textGlyphs.length * 8); const glyphPositions = new Float32Array(totalGlyphCount * 8);
const glyphIndices = new Uint32Array(textGlyphs.length * 6); const glyphIndices = new Uint32Array(totalGlyphCount * 6);
for (let glyphIndex = 0; glyphIndex < textGlyphs.length; glyphIndex++) { const hint = this.appController.createHint();
const textGlyph = textGlyphs[glyphIndex]; const pixelsPerUnit = this.appController.pixelsPerUnit;
const rect = textGlyph.pixelRect(this.appController.createHint(),
this.appController.pixelsPerUnit);
glyphPositions.set([
rect[0], rect[3],
rect[2], rect[3],
rect[0], rect[1],
rect[2], rect[1],
], glyphIndex * 8);
for (let glyphIndexIndex = 0; let globalGlyphIndex = 0;
glyphIndexIndex < QUAD_ELEMENTS.length; for (const run of layout.textFrame.runs) {
glyphIndexIndex++) { for (let glyphIndex = 0;
glyphIndices[glyphIndexIndex + glyphIndex * 6] = glyphIndex < run.glyphIDs.length;
QUAD_ELEMENTS[glyphIndexIndex] + 4 * glyphIndex; glyphIndex++, globalGlyphIndex++) {
const rect = run.pixelRectForGlyphAt(glyphIndex, pixelsPerUnit, hint);
glyphPositions.set([
rect[0], rect[3],
rect[2], rect[3],
rect[0], rect[1],
rect[2], rect[1],
], globalGlyphIndex * 8);
for (let glyphIndexIndex = 0;
glyphIndexIndex < QUAD_ELEMENTS.length;
glyphIndexIndex++) {
glyphIndices[glyphIndexIndex + globalGlyphIndex * 6] =
QUAD_ELEMENTS[glyphIndexIndex] + 4 * globalGlyphIndex;
}
} }
} }
@ -317,9 +333,11 @@ class TextDemoView extends MonochromePathfinderView {
} }
private buildAtlasGlyphs() { private buildAtlasGlyphs() {
const textGlyphs = this.appController.layout.glyphStorage.allGlyphs; const font = this.appController.font;
const glyphStore = this.appController.glyphStore;
const pixelsPerUnit = this.appController.pixelsPerUnit; const pixelsPerUnit = this.appController.pixelsPerUnit;
const textFrame = this.appController.layout.textFrame;
const hint = this.appController.createHint(); const hint = this.appController.createHint();
// Only build glyphs in view. // Only build glyphs in view.
@ -329,34 +347,39 @@ class TextDemoView extends MonochromePathfinderView {
-translation[0] + this.canvas.width, -translation[0] + this.canvas.width,
-translation[1] + this.canvas.height); -translation[1] + this.canvas.height);
let atlasGlyphs = let atlasGlyphs = [];
textGlyphs.filter(glyph => { for (const run of textFrame.runs) {
return rectsIntersect(glyph.pixelRect(hint, pixelsPerUnit), canvasRect); for (let glyphIndex = 0; glyphIndex < run.glyphIDs.length; glyphIndex++) {
}).map(textGlyph => new AtlasGlyph(textGlyph.opentypeGlyph)); const pixelRect = run.pixelRectForGlyphAt(glyphIndex, pixelsPerUnit, hint);
atlasGlyphs.sort((a, b) => a.index - b.index); if (!rectsIntersect(pixelRect, canvasRect))
atlasGlyphs = _.sortedUniqBy(atlasGlyphs, glyph => glyph.index); continue;
const glyphID = run.glyphIDs[glyphIndex];
const glyphStoreIndex = glyphStore.indexOfGlyphWithID(glyphID);
if (glyphStoreIndex == null)
continue;
atlasGlyphs.push(new AtlasGlyph(glyphStoreIndex, glyphID));
}
}
atlasGlyphs.sort((a, b) => a.glyphID - b.glyphID);
atlasGlyphs = _.sortedUniqBy(atlasGlyphs, glyph => glyph.glyphID);
if (atlasGlyphs.length === 0)
return;
this.appController.atlasGlyphs = atlasGlyphs; this.appController.atlasGlyphs = atlasGlyphs;
this.appController.atlas.layoutGlyphs(atlasGlyphs, font, pixelsPerUnit, hint);
this.appController.atlas.layoutGlyphs(atlasGlyphs, hint, pixelsPerUnit);
const uniqueGlyphs = this.appController.layout.glyphStorage.uniqueGlyphs;
const uniqueGlyphIndices = uniqueGlyphs.map(glyph => glyph.index);
uniqueGlyphIndices.sort((a, b) => a - b);
this.uploadPathTransforms(1); this.uploadPathTransforms(1);
// 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 pathHints = new Float32Array((uniqueGlyphs.length + 1) * 4); const glyphCount = this.appController.glyphStore.glyphIDs.length;
const pathHints = new Float32Array((glyphCount + 1) * 4);
for (let glyphIndex = 0; glyphIndex < atlasGlyphs.length; glyphIndex++) { for (let glyphID = 0; glyphID < glyphCount; glyphID++) {
const glyph = atlasGlyphs[glyphIndex]; pathHints[glyphID * 4 + 0] = hint.xHeight;
pathHints[glyphID * 4 + 1] = hint.hintedXHeight;
let pathID = _.sortedIndexOf(uniqueGlyphIndices, glyph.index);
assert(pathID >= 0, "No path ID!");
pathID++;
pathHints[pathID * 4 + 0] = hint.xHeight;
pathHints[pathID * 4 + 1] = hint.hintedXHeight;
} }
const pathHintsBufferTexture = new PathfinderBufferTexture(this.gl, 'uPathHints'); const pathHintsBufferTexture = new PathfinderBufferTexture(this.gl, 'uPathHints');
@ -367,24 +390,18 @@ class TextDemoView extends MonochromePathfinderView {
} }
protected pathTransformsForObject(objectIndex: number): Float32Array { protected pathTransformsForObject(objectIndex: number): Float32Array {
const glyphCount = this.appController.glyphStore.glyphIDs.length;
const atlasGlyphs = this.appController.atlasGlyphs; const atlasGlyphs = this.appController.atlasGlyphs;
const pixelsPerUnit = this.appController.pixelsPerUnit; const pixelsPerUnit = this.appController.pixelsPerUnit;
const uniqueGlyphs = this.appController.layout.glyphStorage.uniqueGlyphs; const transforms = new Float32Array((glyphCount + 1) * 4);
const uniqueGlyphIndices = uniqueGlyphs.map(glyph => glyph.index);
uniqueGlyphIndices.sort((a, b) => a - b);
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];
let pathID = _.sortedIndexOf(uniqueGlyphIndices, glyph.index); const pathID = glyph.glyphStoreIndex + 1;
assert(pathID >= 0, "No path ID!"); const atlasOrigin = glyph.calculatePixelOrigin(pixelsPerUnit);
pathID++;
const atlasOrigin = glyph.pixelOrigin(pixelsPerUnit);
const metrics = glyph.metrics;
transforms[pathID * 4 + 0] = pixelsPerUnit; transforms[pathID * 4 + 0] = pixelsPerUnit;
transforms[pathID * 4 + 1] = pixelsPerUnit; transforms[pathID * 4 + 1] = pixelsPerUnit;
transforms[pathID * 4 + 2] = atlasOrigin[0]; transforms[pathID * 4 + 2] = atlasOrigin[0];
@ -398,9 +415,9 @@ class TextDemoView extends MonochromePathfinderView {
const atlasColorTexture = this.appController.atlas.ensureTexture(this.gl); const atlasColorTexture = this.appController.atlas.ensureTexture(this.gl);
this.atlasDepthTexture = createFramebufferDepthTexture(this.gl, ATLAS_SIZE); this.atlasDepthTexture = createFramebufferDepthTexture(this.gl, ATLAS_SIZE);
this.atlasFramebuffer = createFramebuffer(this.gl, this.atlasFramebuffer = createFramebuffer(this.gl,
this.drawBuffersExt, this.drawBuffersExt,
[atlasColorTexture], [atlasColorTexture],
this.atlasDepthTexture); this.atlasDepthTexture);
// Allow the antialiasing strategy to set up framebuffers as necessary. // Allow the antialiasing strategy to set up framebuffers as necessary.
if (this.antialiasingStrategy != null) if (this.antialiasingStrategy != null)
@ -408,39 +425,51 @@ class TextDemoView extends MonochromePathfinderView {
} }
private setGlyphTexCoords() { private setGlyphTexCoords() {
const textGlyphs = this.appController.layout.glyphStorage.allGlyphs; const textFrame = this.appController.layout.textFrame;
const font = this.appController.font;
const atlasGlyphs = this.appController.atlasGlyphs; const atlasGlyphs = this.appController.atlasGlyphs;
const hint = this.appController.createHint(); const hint = this.appController.createHint();
const pixelsPerUnit = this.appController.pixelsPerUnit;
const atlasGlyphIndices = atlasGlyphs.map(atlasGlyph => atlasGlyph.index); const atlasGlyphIDs = atlasGlyphs.map(atlasGlyph => atlasGlyph.glyphID);
const glyphTexCoords = new Float32Array(textGlyphs.length * 8); const glyphTexCoords = new Float32Array(textFrame.totalGlyphCount * 8);
const currentPosition = glmatrix.vec2.create(); let globalGlyphIndex = 0;
for (const run of textFrame.runs) {
for (let glyphIndex = 0;
glyphIndex < run.glyphIDs.length;
glyphIndex++, globalGlyphIndex++) {
const textGlyphID = run.glyphIDs[glyphIndex];
for (let textGlyphIndex = 0; textGlyphIndex < textGlyphs.length; textGlyphIndex++) { let atlasGlyphIndex = _.sortedIndexOf(atlasGlyphIDs, textGlyphID);
const textGlyph = textGlyphs[textGlyphIndex]; if (atlasGlyphIndex < 0)
const textGlyphMetrics = textGlyph.metrics; continue;
let atlasGlyphIndex = _.sortedIndexOf(atlasGlyphIndices, textGlyph.index); // Set texture coordinates.
if (atlasGlyphIndex < 0) const atlasGlyph = atlasGlyphs[atlasGlyphIndex];
continue; const atlasGlyphMetrics = font.metricsForGlyph(atlasGlyph.glyphID);
if (atlasGlyphMetrics == null)
continue;
// Set texture coordinates. const atlasGlyphPixelOrigin = atlasGlyph.calculatePixelOrigin(pixelsPerUnit);
const atlasGlyph = atlasGlyphs[atlasGlyphIndex]; const atlasGlyphRect = calculatePixelRectForGlyph(atlasGlyphMetrics,
const atlasGlyphRect = atlasGlyph.pixelRect(hint, this.appController.pixelsPerUnit); atlasGlyphPixelOrigin,
const atlasGlyphBL = atlasGlyphRect.slice(0, 2) as glmatrix.vec2; pixelsPerUnit,
const atlasGlyphTR = atlasGlyphRect.slice(2, 4) as glmatrix.vec2; hint);
glmatrix.vec2.div(atlasGlyphBL, atlasGlyphBL, ATLAS_SIZE); const atlasGlyphBL = atlasGlyphRect.slice(0, 2) as glmatrix.vec2;
glmatrix.vec2.div(atlasGlyphTR, atlasGlyphTR, ATLAS_SIZE); const atlasGlyphTR = atlasGlyphRect.slice(2, 4) as glmatrix.vec2;
glmatrix.vec2.div(atlasGlyphBL, atlasGlyphBL, ATLAS_SIZE);
glmatrix.vec2.div(atlasGlyphTR, atlasGlyphTR, ATLAS_SIZE);
glyphTexCoords.set([ glyphTexCoords.set([
atlasGlyphBL[0], atlasGlyphTR[1], atlasGlyphBL[0], atlasGlyphTR[1],
atlasGlyphTR[0], atlasGlyphTR[1], atlasGlyphTR[0], atlasGlyphTR[1],
atlasGlyphBL[0], atlasGlyphBL[1], atlasGlyphBL[0], atlasGlyphBL[1],
atlasGlyphTR[0], atlasGlyphBL[1], atlasGlyphTR[0], atlasGlyphBL[1],
], textGlyphIndex * 8); ], globalGlyphIndex * 8);
}
} }
this.glyphTexCoordsBuffer = unwrapNull(this.gl.createBuffer()); this.glyphTexCoordsBuffer = unwrapNull(this.gl.createBuffer());
@ -457,7 +486,7 @@ class TextDemoView extends MonochromePathfinderView {
this.layoutText(); this.layoutText();
this.camera.zoomToFit(); this.camera.zoomToFit();
this.appController.fontSize = this.camera.scale * this.appController.fontSize = this.camera.scale *
this.appController.layout.glyphStorage.font.unitsPerEm; this.appController.font.opentypeFont.unitsPerEm;
this.buildAtlasGlyphs(); this.buildAtlasGlyphs();
this.setDirty(); this.setDirty();
@ -477,7 +506,7 @@ class TextDemoView extends MonochromePathfinderView {
protected onZoom() { protected onZoom() {
this.appController.fontSize = this.camera.scale * this.appController.fontSize = this.camera.scale *
this.appController.layout.glyphStorage.font.unitsPerEm; this.appController.font.opentypeFont.unitsPerEm;
this.buildAtlasGlyphs(); this.buildAtlasGlyphs();
this.setDirty(); this.setDirty();
} }
@ -543,7 +572,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.layout.glyphStorage.allGlyphs.length * 6, this.appController.layout.textFrame.totalGlyphCount * 6,
this.gl.UNSIGNED_INT, this.gl.UNSIGNED_INT,
0); 0);
} }
@ -632,24 +661,40 @@ class Atlas {
this._usedSize = glmatrix.vec2.create(); this._usedSize = glmatrix.vec2.create();
} }
layoutGlyphs(glyphs: AtlasGlyph[], hint: Hint, pixelsPerUnit: number) { layoutGlyphs(glyphs: AtlasGlyph[], font: PathfinderFont, pixelsPerUnit: number, hint: Hint) {
let nextOrigin = glmatrix.vec2.fromValues(1.0, 1.0); let nextOrigin = glmatrix.vec2.fromValues(1.0, 1.0);
let shelfBottom = 2.0; let shelfBottom = 2.0;
for (const glyph of glyphs) { for (const glyph of glyphs) {
// Place the glyph, and advance the origin. // Place the glyph, and advance the origin.
glyph.setPixelLowerLeft(nextOrigin, pixelsPerUnit); const metrics = font.metricsForGlyph(glyph.glyphID);
nextOrigin[0] = glyph.pixelRect(hint, pixelsPerUnit)[2] + 1.0; if (metrics == null)
continue;
glyph.setPixelLowerLeft(nextOrigin, metrics, pixelsPerUnit);
let pixelOrigin = glyph.calculatePixelOrigin(pixelsPerUnit);
nextOrigin[0] = calculatePixelRectForGlyph(metrics,
pixelOrigin,
pixelsPerUnit,
hint)[2] + 1.0;
// If the glyph overflowed the shelf, make a new one and reposition the glyph. // If the glyph overflowed the shelf, make a new one and reposition the glyph.
if (nextOrigin[0] > ATLAS_SIZE[0]) { if (nextOrigin[0] > ATLAS_SIZE[0]) {
nextOrigin = glmatrix.vec2.fromValues(1.0, shelfBottom + 1.0); nextOrigin = glmatrix.vec2.clone([1.0, shelfBottom + 1.0]);
glyph.setPixelLowerLeft(nextOrigin, pixelsPerUnit); glyph.setPixelLowerLeft(nextOrigin, metrics, pixelsPerUnit);
nextOrigin[0] = glyph.pixelRect(hint, pixelsPerUnit)[2] + 1.0; pixelOrigin = glyph.calculatePixelOrigin(pixelsPerUnit);
nextOrigin[0] = calculatePixelRectForGlyph(metrics,
pixelOrigin,
pixelsPerUnit,
hint)[2] + 1.0;
} }
// Grow the shelf as necessary. // Grow the shelf as necessary.
shelfBottom = Math.max(shelfBottom, glyph.pixelRect(hint, pixelsPerUnit)[3] + 1.0); const glyphBottom = calculatePixelRectForGlyph(metrics,
pixelOrigin,
pixelsPerUnit,
hint)[3];
shelfBottom = Math.max(shelfBottom, glyphBottom + 1.0);
} }
// 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.
@ -685,16 +730,36 @@ class Atlas {
private _usedSize: Size2D; private _usedSize: Size2D;
} }
class AtlasGlyph extends PathfinderGlyph { class AtlasGlyph {
constructor(glyph: opentype.Glyph) { constructor(glyphStoreIndex: number, glyphID: number) {
super(glyph); this.glyphStoreIndex = glyphStoreIndex;
this.glyphID = glyphID;
this.origin = glmatrix.vec2.create();
} }
}
class GlyphInstance extends PathfinderGlyph { calculatePixelOrigin(pixelsPerUnit: number): glmatrix.vec2 {
constructor(glyph: opentype.Glyph) { const pixelOrigin = glmatrix.vec2.create();
super(glyph); glmatrix.vec2.scale(pixelOrigin, this.origin, pixelsPerUnit);
glmatrix.vec2.round(pixelOrigin, pixelOrigin);
return pixelOrigin;
} }
setPixelLowerLeft(pixelLowerLeft: glmatrix.vec2, metrics: Metrics, pixelsPerUnit: number):
void {
const pixelXMin = calculatePixelXMin(metrics, pixelsPerUnit);
const pixelDescent = calculatePixelDescent(metrics, pixelsPerUnit);
const pixelOrigin = glmatrix.vec2.clone([pixelLowerLeft[0] - pixelXMin,
pixelLowerLeft[1] + pixelDescent]);
this.setPixelOrigin(pixelOrigin, pixelsPerUnit);
}
private setPixelOrigin(pixelOrigin: glmatrix.vec2, pixelsPerUnit: number): void {
glmatrix.vec2.scale(this.origin, pixelOrigin, 1.0 / pixelsPerUnit);
}
readonly glyphStoreIndex: number;
readonly glyphID: number;
readonly origin: glmatrix.vec2;
} }
const ANTIALIASING_STRATEGIES: AntialiasingStrategyTable = { const ANTIALIASING_STRATEGIES: AntialiasingStrategyTable = {

View File

@ -8,14 +8,14 @@
// 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, Metrics} from 'opentype.js'; import {Metrics} from 'opentype.js';
import * as base64js from 'base64-js'; import * as base64js from 'base64-js';
import * as glmatrix from 'gl-matrix'; import * as glmatrix from 'gl-matrix';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as opentype from "opentype.js"; import * as opentype from "opentype.js";
import {B_QUAD_SIZE, PathfinderMeshData} from "./meshes"; import {B_QUAD_SIZE, PathfinderMeshData} from "./meshes";
import {UINT32_SIZE, UINT32_MAX, assert, panic} from "./utils"; import { UINT32_SIZE, UINT32_MAX, assert, panic, unwrapNull } from "./utils";
export const BUILTIN_FONT_URI: string = "/otf/demo"; export const BUILTIN_FONT_URI: string = "/otf/demo";
@ -30,8 +30,6 @@ export interface PartitionResult {
time: number, time: number,
} }
type CreateGlyphFn<Glyph> = (glyph: opentype.Glyph) => Glyph;
export interface PixelMetrics { export interface PixelMetrics {
left: number; left: number;
right: number; right: number;
@ -48,48 +46,103 @@ opentype.Font.prototype.lineHeight = function() {
return os2Table.sTypoAscender - os2Table.sTypoDescender + os2Table.sTypoLineGap; return os2Table.sTypoAscender - os2Table.sTypoDescender + os2Table.sTypoLineGap;
}; };
export class TextRun<Glyph extends PathfinderGlyph> { export class PathfinderFont {
constructor(text: string | Glyph[], constructor(data: ArrayBuffer) {
origin: number[], this.data = data;
font: Font,
createGlyph: CreateGlyphFn<Glyph>) {
if (typeof(text) === 'string')
text = font.stringToGlyphs(text).map(createGlyph);
this.glyphs = text; this.opentypeFont = opentype.parse(data);
this.origin = origin; if (!this.opentypeFont.isSupported())
panic("Unsupported font!");
this.metricsCache = [];
} }
layout() { metricsForGlyph(glyphID: number): Metrics | null {
let currentX = this.origin[0]; if (this.metricsCache[glyphID] == null)
for (const glyph of this.glyphs) { this.metricsCache[glyphID] = this.opentypeFont.glyphs.get(glyphID).getMetrics();
glyph.origin = glmatrix.vec2.fromValues(currentX, this.origin[1]); return this.metricsCache[glyphID];
currentX += glyph.advanceWidth;
}
} }
get measure(): number { readonly opentypeFont: opentype.Font;
const lastGlyph = _.last(this.glyphs); readonly data: ArrayBuffer;
return lastGlyph == null ? 0.0 : lastGlyph.origin[0] + lastGlyph.advanceWidth;
}
readonly glyphs: Glyph[]; private metricsCache: Metrics[];
readonly origin: number[];
} }
export class TextFrame<Glyph extends PathfinderGlyph> { export class TextRun {
constructor(runs: TextRun<Glyph>[], font: Font) { constructor(text: number[] | string, origin: number[], font: PathfinderFont) {
this.runs = runs; if (typeof(text) === 'string') {
this.glyphIDs = font.opentypeFont
.stringToGlyphs(text)
.map(glyph => (glyph as any).index);
} else {
this.glyphIDs = text;
}
this.origin = origin;
this.advances = [];
this.font = font; this.font = font;
} }
expandMeshes(uniqueGlyphs: Glyph[], meshes: PathfinderMeshData): ExpandedMeshData { layout() {
this.advances = [];
let currentX = 0;
for (const glyphID of this.glyphIDs) {
this.advances.push(currentX);
currentX += this.font.opentypeFont.glyphs.get(glyphID).advanceWidth;
}
}
private pixelMetricsForGlyphAt(index: number, pixelsPerUnit: number, hint: Hint):
PixelMetrics {
const metrics = unwrapNull(this.font.metricsForGlyph(index));
return calculatePixelMetricsForGlyph(metrics, pixelsPerUnit, hint);
}
calculatePixelOriginForGlyphAt(index: number, pixelsPerUnit: number, hint: Hint):
glmatrix.vec2 {
const textGlyphOrigin = glmatrix.vec2.clone(this.origin);
textGlyphOrigin[0] += this.advances[index];
glmatrix.vec2.scale(textGlyphOrigin, textGlyphOrigin, pixelsPerUnit);
return textGlyphOrigin;
}
pixelRectForGlyphAt(index: number, pixelsPerUnit: number, hint: Hint): glmatrix.vec4 {
const metrics = unwrapNull(this.font.metricsForGlyph(this.glyphIDs[index]));
const textGlyphOrigin = this.calculatePixelOriginForGlyphAt(index, pixelsPerUnit, hint);
glmatrix.vec2.round(textGlyphOrigin, textGlyphOrigin);
return calculatePixelRectForGlyph(metrics, textGlyphOrigin, pixelsPerUnit, hint);
}
get measure(): number {
const lastGlyphID = _.last(this.glyphIDs), lastAdvance = _.last(this.advances);
if (lastGlyphID == null || lastAdvance == null)
return 0.0;
return lastAdvance + this.font.opentypeFont.glyphs.get(lastGlyphID).advanceWidth;
}
readonly glyphIDs: number[];
advances: number[];
readonly origin: number[];
private readonly font: PathfinderFont;
}
export class TextFrame {
constructor(runs: TextRun[], font: PathfinderFont) {
this.runs = runs;
this.origin = glmatrix.vec3.create();
this.font = font;
}
expandMeshes(meshes: PathfinderMeshData, glyphIDs: number[]): ExpandedMeshData {
const pathIDs = []; const pathIDs = [];
for (const textRun of this.runs) { for (const textRun of this.runs) {
for (const textGlyph of textRun.glyphs) { for (let glyphIndex = 0; glyphIndex < textRun.glyphIDs.length; glyphIndex++) {
const uniqueGlyphIndex = _.sortedIndexBy(uniqueGlyphs, textGlyph, 'index'); const glyphID = textRun.glyphIDs[glyphIndex];
if (uniqueGlyphIndex >= 0) if (glyphID === 0)
pathIDs.push(uniqueGlyphIndex + 1); continue;
const pathID = _.sortedIndexOf(glyphIDs, glyphID);
pathIDs.push(pathID + 1);
} }
} }
@ -108,7 +161,7 @@ export class TextFrame<Glyph extends PathfinderGlyph> {
const lowerLeft = glmatrix.vec2.clone([upperLeft[0], lowerRight[1]]); const lowerLeft = glmatrix.vec2.clone([upperLeft[0], lowerRight[1]]);
const upperRight = glmatrix.vec2.clone([lowerRight[0], upperLeft[1]]); const upperRight = glmatrix.vec2.clone([lowerRight[0], upperLeft[1]]);
const lineHeight = this.font.lineHeight(); const lineHeight = this.font.opentypeFont.lineHeight();
lowerLeft[1] -= lineHeight; lowerLeft[1] -= lineHeight;
upperRight[1] += lineHeight * 2.0; upperRight[1] += lineHeight * 2.0;
@ -117,30 +170,28 @@ export class TextFrame<Glyph extends PathfinderGlyph> {
return glmatrix.vec4.clone([lowerLeft[0], lowerLeft[1], upperRight[0], upperRight[1]]); return glmatrix.vec4.clone([lowerLeft[0], lowerLeft[1], upperRight[0], upperRight[1]]);
} }
get allGlyphs(): Glyph[] { get totalGlyphCount(): number {
return _.flatMap(this.runs, run => run.glyphs); return _.sumBy(this.runs, run => run.glyphIDs.length);
} }
readonly runs: TextRun<Glyph>[]; get allGlyphIDs(): number[] {
const glyphIDs = [];
for (const run of this.runs)
glyphIDs.push(...run.glyphIDs);
return glyphIDs;
}
readonly runs: TextRun[];
readonly origin: glmatrix.vec3; readonly origin: glmatrix.vec3;
private readonly font: Font; private readonly font: PathfinderFont;
} }
export class GlyphStorage<Glyph extends PathfinderGlyph> { /// Stores one copy of each glyph.
constructor(fontData: ArrayBuffer, glyphs: Glyph[], font?: Font) { export class GlyphStore {
if (font == null) { constructor(font: PathfinderFont, glyphIDs: number[]) {
font = opentype.parse(fontData);
assert(font.isSupported(), "The font type is unsupported!");
}
this.fontData = fontData;
this.font = font; this.font = font;
this.glyphIDs = glyphIDs;
// Determine all glyphs potentially needed.
this.uniqueGlyphs = glyphs;
this.uniqueGlyphs.sort((a, b) => a.index - b.index);
this.uniqueGlyphs = _.sortedUniqBy(this.uniqueGlyphs, glyph => glyph.index);
} }
partition(): Promise<PartitionResult> { partition(): Promise<PartitionResult> {
@ -149,17 +200,11 @@ export class GlyphStorage<Glyph extends PathfinderGlyph> {
// FIXME(pcwalton): If this is a builtin font, don't resend it to the server! // FIXME(pcwalton): If this is a builtin font, don't resend it to the server!
const request = { const request = {
face: { face: {
Custom: base64js.fromByteArray(new Uint8Array(this.fontData)), Custom: base64js.fromByteArray(new Uint8Array(this.font.data)),
}, },
fontIndex: 0, fontIndex: 0,
glyphs: this.uniqueGlyphs.map(glyph => { glyphs: this.glyphIDs.map(id => ({ id: id, transform: [1, 0, 0, 1, 0, 0] })),
const metrics = glyph.metrics; pointSize: this.font.opentypeFont.unitsPerEm,
return {
id: glyph.index,
transform: [1, 0, 0, 1, 0, 0],
};
}),
pointSize: this.font.unitsPerEm,
}; };
// Make the request. // Make the request.
@ -178,150 +223,36 @@ export class GlyphStorage<Glyph extends PathfinderGlyph> {
}); });
} }
readonly fontData: ArrayBuffer; indexOfGlyphWithID(glyphID: number): number | null {
readonly font: Font; const index = _.sortedIndexOf(this.glyphIDs, glyphID);
readonly uniqueGlyphs: Glyph[]; return index >= 0 ? index : null;
}
readonly font: PathfinderFont;
readonly glyphIDs: number[];
} }
export class TextFrameGlyphStorage<Glyph extends PathfinderGlyph> extends GlyphStorage<Glyph> { export class SimpleTextLayout {
constructor(fontData: ArrayBuffer, textFrames: TextFrame<Glyph>[], font?: Font) { constructor(font: PathfinderFont, text: string) {
const allGlyphs = _.flatMap(textFrames, textRun => textRun.allGlyphs); const lineHeight = font.opentypeFont.lineHeight();
super(fontData, allGlyphs, font); const textRuns: TextRun[] = text.split("\n").map((line, lineNumber) => {
return new TextRun(line, [0.0, -lineHeight * lineNumber], font);
this.textFrames = textFrames;
}
expandMeshes(meshes: PathfinderMeshData): ExpandedMeshData[] {
return this.textFrames.map(textFrame => textFrame.expandMeshes(this.uniqueGlyphs, meshes));
}
layoutRuns() {
for (const textFrame of this.textFrames)
textFrame.runs.forEach(textRun => textRun.layout());
}
get allGlyphs(): Glyph[] {
return _.flatMap(this.textFrames, textRun => textRun.allGlyphs);
}
readonly textFrames: TextFrame<Glyph>[];
}
export class SimpleTextLayout<Glyph extends PathfinderGlyph> {
constructor(fontData: ArrayBuffer, text: string, createGlyph: CreateGlyphFn<Glyph>) {
const font = opentype.parse(fontData);
assert(font.isSupported(), "The font type is unsupported!");
const lineHeight = font.lineHeight();
const textRuns: TextRun<Glyph>[] = text.split("\n").map((line, lineNumber) => {
return new TextRun<Glyph>(line, [0.0, -lineHeight * lineNumber], font, createGlyph);
}); });
this.textFrame = new TextFrame(textRuns, font); this.textFrame = new TextFrame(textRuns, font);
this.glyphStorage = new TextFrameGlyphStorage(fontData, [this.textFrame], font);
} }
layoutRuns() { layoutRuns() {
this.textFrame.runs.forEach(textRun => textRun.layout()); this.textFrame.runs.forEach(textRun => textRun.layout());
} }
get allGlyphs(): Glyph[] { readonly textFrame: TextFrame;
return this.textFrame.allGlyphs;
}
readonly textFrame: TextFrame<Glyph>;
readonly glyphStorage: TextFrameGlyphStorage<Glyph>;
}
export abstract class PathfinderGlyph {
constructor(glyph: opentype.Glyph) {
this.opentypeGlyph = glyph;
this._metrics = null;
this.origin = 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;
}
pixelOrigin(pixelsPerUnit: number): glmatrix.vec2 {
const origin = glmatrix.vec2.create();
glmatrix.vec2.scale(origin, this.origin, pixelsPerUnit);
return origin;
}
setPixelOrigin(pixelOrigin: glmatrix.vec2, pixelsPerUnit: number): void {
glmatrix.vec2.scale(this.origin, pixelOrigin, 1.0 / pixelsPerUnit);
}
private calculatePixelXMin(pixelsPerUnit: number): number {
return Math.floor(this.metrics.xMin * pixelsPerUnit);
}
private calculatePixelDescent(pixelsPerUnit: number): number {
return Math.ceil(-this.metrics.yMin * pixelsPerUnit);
}
setPixelLowerLeft(pixelLowerLeft: glmatrix.vec2, pixelsPerUnit: number): void {
const pixelXMin = this.calculatePixelXMin(pixelsPerUnit);
const pixelDescent = this.calculatePixelDescent(pixelsPerUnit);
const pixelOrigin = glmatrix.vec2.fromValues(pixelLowerLeft[0] - pixelXMin,
pixelLowerLeft[1] + pixelDescent);
this.setPixelOrigin(pixelOrigin, pixelsPerUnit);
}
protected pixelMetrics(hint: Hint, pixelsPerUnit: number): PixelMetrics {
const metrics = this.metrics;
const top = hint.hintPosition(glmatrix.vec2.fromValues(0, metrics.yMax))[1];
return {
left: this.calculatePixelXMin(pixelsPerUnit),
right: Math.ceil(metrics.xMax * pixelsPerUnit),
ascent: Math.ceil(top * pixelsPerUnit),
descent: this.calculatePixelDescent(pixelsPerUnit),
};
}
calculatePixelOrigin(hint: Hint, pixelsPerUnit: number): glmatrix.vec2 {
const textGlyphOrigin = glmatrix.vec2.clone(this.origin);
glmatrix.vec2.scale(textGlyphOrigin, textGlyphOrigin, pixelsPerUnit);
return textGlyphOrigin;
}
pixelRect(hint: Hint, pixelsPerUnit: number): glmatrix.vec4 {
const pixelMetrics = this.pixelMetrics(hint, pixelsPerUnit);
const textGlyphOrigin = this.calculatePixelOrigin(hint, pixelsPerUnit);
glmatrix.vec2.round(textGlyphOrigin, textGlyphOrigin);
return glmatrix.vec4.fromValues(textGlyphOrigin[0] + pixelMetrics.left,
textGlyphOrigin[1] - pixelMetrics.descent,
textGlyphOrigin[0] + pixelMetrics.right,
textGlyphOrigin[1] + pixelMetrics.ascent);
}
readonly opentypeGlyph: opentype.Glyph;
private _metrics: Metrics | null;
/// In font units, relative to (0, 0).
origin: glmatrix.vec2;
} }
export class Hint { export class Hint {
constructor(font: Font, pixelsPerUnit: number, useHinting: boolean) { constructor(font: PathfinderFont, pixelsPerUnit: number, useHinting: boolean) {
this.useHinting = useHinting; this.useHinting = useHinting;
const os2Table = font.tables.os2; const os2Table = font.opentypeFont.tables.os2;
this.xHeight = os2Table.sxHeight != null ? os2Table.sxHeight : 0; this.xHeight = os2Table.sxHeight != null ? os2Table.sxHeight : 0;
if (!useHinting) { if (!useHinting) {
@ -352,3 +283,34 @@ export class Hint {
readonly hintedXHeight: number; readonly hintedXHeight: number;
private useHinting: boolean; private useHinting: boolean;
} }
export function calculatePixelXMin(metrics: Metrics, pixelsPerUnit: number): number {
return Math.floor(metrics.xMin * pixelsPerUnit);
}
export function calculatePixelDescent(metrics: Metrics, pixelsPerUnit: number): number {
return Math.ceil(-metrics.yMin * pixelsPerUnit);
}
function calculatePixelMetricsForGlyph(metrics: Metrics, pixelsPerUnit: number, hint: Hint):
PixelMetrics {
const top = hint.hintPosition(glmatrix.vec2.fromValues(0, metrics.yMax))[1];
return {
left: calculatePixelXMin(metrics, pixelsPerUnit),
right: Math.ceil(metrics.xMax * pixelsPerUnit),
ascent: Math.ceil(top * pixelsPerUnit),
descent: calculatePixelDescent(metrics, pixelsPerUnit),
};
}
export function calculatePixelRectForGlyph(metrics: Metrics,
pixelOrigin: glmatrix.vec2,
pixelsPerUnit: number,
hint: Hint):
glmatrix.vec4 {
const pixelMetrics = calculatePixelMetricsForGlyph(metrics, pixelsPerUnit, hint);
return glmatrix.vec4.clone([pixelOrigin[0] + pixelMetrics.left,
pixelOrigin[1] - pixelMetrics.descent,
pixelOrigin[0] + pixelMetrics.right,
pixelOrigin[1] + pixelMetrics.ascent]);
}