Rewrite the text run and glyph store classes for simplicity
This commit is contained in:
parent
17b34685a1
commit
1675944dfb
|
@ -17,8 +17,8 @@ import {PerspectiveCamera} from "./camera";
|
|||
import {mat4, vec2} from "gl-matrix";
|
||||
import {PathfinderMeshData} from "./meshes";
|
||||
import {ShaderMap, ShaderProgramSource} from "./shader-loader";
|
||||
import {BUILTIN_FONT_URI, ExpandedMeshData, TextFrameGlyphStorage, PathfinderGlyph} from "./text";
|
||||
import {Hint, SimpleTextLayout, TextFrame, TextRun} from "./text";
|
||||
import {BUILTIN_FONT_URI, ExpandedMeshData} from "./text";
|
||||
import { Hint, TextFrame, TextRun, GlyphStore, PathfinderFont } from "./text";
|
||||
import {PathfinderError, assert, panic, unwrapNull} from "./utils";
|
||||
import {PathfinderDemoView, Timings} from "./view";
|
||||
import SSAAStrategy from "./ssaa-strategy";
|
||||
|
@ -128,24 +128,25 @@ class ThreeDController extends DemoAppController<ThreeDView> {
|
|||
}
|
||||
|
||||
protected fileLoaded(fileData: ArrayBuffer): void {
|
||||
const font = opentype.parse(fileData);
|
||||
assert(font.isSupported(), "The font type is unsupported!");
|
||||
|
||||
const font = new PathfinderFont(fileData);
|
||||
this.monumentPromise.then(monument => this.layoutMonument(font, fileData, monument));
|
||||
}
|
||||
|
||||
private layoutMonument(font: opentype.Font, fileData: ArrayBuffer, monument: MonumentSide[]) {
|
||||
const createGlyph = (glyph: opentype.Glyph) => new ThreeDGlyph(glyph);
|
||||
let textFrames = [];
|
||||
private layoutMonument(font: PathfinderFont, fileData: ArrayBuffer, monument: MonumentSide[]) {
|
||||
this.textFrames = [];
|
||||
let glyphsNeeded: number[] = [];
|
||||
|
||||
for (const monumentSide of monument) {
|
||||
let textRuns = [];
|
||||
for (let lineNumber = 0; lineNumber < monumentSide.lines.length; lineNumber++) {
|
||||
const line = monumentSide.lines[lineNumber];
|
||||
|
||||
const lineY = -lineNumber * font.lineHeight();
|
||||
const lineY = -lineNumber * font.opentypeFont.lineHeight();
|
||||
const lineGlyphs = line.names.map(string => {
|
||||
const glyphs = font.stringToGlyphs(string).map(createGlyph);
|
||||
return { glyphs: glyphs, width: _.sumBy(glyphs, glyph => glyph.advanceWidth) };
|
||||
const glyphs = font.opentypeFont.stringToGlyphs(string);
|
||||
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');
|
||||
|
@ -155,20 +156,27 @@ class ThreeDController extends DemoAppController<ThreeDView> {
|
|||
let currentX = 0.0;
|
||||
for (const glyphInfo of lineGlyphs) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
this.glyphStorage.layoutRuns();
|
||||
glyphsNeeded.sort((a, b) => a - b);
|
||||
glyphsNeeded = _.sortedUniq(glyphsNeeded);
|
||||
|
||||
this.glyphStorage.partition().then(result => {
|
||||
this.glyphStore = new GlyphStore(font, glyphsNeeded);
|
||||
this.glyphStore.partition().then(result => {
|
||||
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 => {
|
||||
view.uploadPathColors(this.expandedMeshes.length);
|
||||
view.uploadPathTransforms(this.expandedMeshes.length);
|
||||
|
@ -191,7 +199,8 @@ class ThreeDController extends DemoAppController<ThreeDView> {
|
|||
return FONT;
|
||||
}
|
||||
|
||||
glyphStorage: TextFrameGlyphStorage<ThreeDGlyph>;
|
||||
textFrames: TextFrame[];
|
||||
glyphStore: GlyphStore;
|
||||
|
||||
private baseMeshes: PathfinderMeshData;
|
||||
private expandedMeshes: ExpandedMeshData[];
|
||||
|
@ -222,9 +231,8 @@ class ThreeDView extends PathfinderDemoView {
|
|||
}
|
||||
|
||||
protected pathColorsForObject(textFrameIndex: number): Uint8Array {
|
||||
const textFrame = this.appController.glyphStorage.textFrames[textFrameIndex];
|
||||
const textGlyphs = textFrame.allGlyphs;
|
||||
const pathCount = textGlyphs.length;
|
||||
const textFrame = this.appController.textFrames[textFrameIndex];
|
||||
const pathCount = textFrame.totalGlyphCount;
|
||||
|
||||
const pathColors = new Uint8Array(4 * (pathCount + 1));
|
||||
for (let pathIndex = 0; pathIndex < pathCount; pathIndex++)
|
||||
|
@ -234,18 +242,24 @@ class ThreeDView extends PathfinderDemoView {
|
|||
}
|
||||
|
||||
protected pathTransformsForObject(textFrameIndex: number): Float32Array {
|
||||
const textFrame = this.appController.glyphStorage.textFrames[textFrameIndex];
|
||||
const textGlyphs = textFrame.allGlyphs;
|
||||
const pathCount = textGlyphs.length;
|
||||
const textFrame = this.appController.textFrames[textFrameIndex];
|
||||
const pathCount = textFrame.totalGlyphCount;
|
||||
|
||||
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));
|
||||
|
||||
for (let pathIndex = 0; pathIndex < pathCount; pathIndex++) {
|
||||
const textGlyph = textGlyphs[pathIndex];
|
||||
const glyphOrigin = textGlyph.calculatePixelOrigin(hint, PIXELS_PER_UNIT);
|
||||
pathTransforms.set([1, 1, glyphOrigin[0], glyphOrigin[1]], (pathIndex + 1) * 4);
|
||||
let globalPathIndex = 0;
|
||||
for (const run of textFrame.runs) {
|
||||
for (let pathIndex = 0;
|
||||
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;
|
||||
|
@ -380,12 +394,6 @@ class ThreeDView extends PathfinderDemoView {
|
|||
camera: PerspectiveCamera;
|
||||
}
|
||||
|
||||
class ThreeDGlyph extends PathfinderGlyph {
|
||||
constructor(glyph: opentype.Glyph) {
|
||||
super(glyph);
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const controller = new ThreeDController;
|
||||
window.addEventListener('load', () => controller.start(), false);
|
||||
|
|
|
@ -13,7 +13,7 @@ import * as opentype from "opentype.js";
|
|||
|
||||
import { AppController, DemoAppController } from "./app-controller";
|
||||
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 { PathfinderDemoView, Timings, MonochromePathfinderView } from "./view";
|
||||
import { ShaderMap, ShaderProgramSource } from "./shader-loader";
|
||||
|
@ -75,26 +75,28 @@ class BenchmarkAppController extends DemoAppController<BenchmarkTestView> {
|
|||
}
|
||||
|
||||
protected fileLoaded(fileData: ArrayBuffer): void {
|
||||
const font = opentype.parse(fileData);
|
||||
const font = new PathfinderFont(fileData);
|
||||
this.font = font;
|
||||
assert(this.font.isSupported(), "The font type is unsupported!");
|
||||
|
||||
const createGlyph = (glyph: opentype.Glyph) => new BenchmarkGlyph(glyph);
|
||||
const textRun = new TextRun<BenchmarkGlyph>(STRING, [0, 0], font, createGlyph);
|
||||
const textRun = new TextRun(STRING, [0, 0], font);
|
||||
textRun.layout();
|
||||
this.textRun = textRun;
|
||||
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;
|
||||
|
||||
const partitionTime = result.time / this.glyphStorage.uniqueGlyphs.length * 1e6;
|
||||
const partitionTime = result.time / this.glyphStore.glyphIDs.length * 1e6;
|
||||
const timeLabel = this.resultsPartitioningTimeLabel;
|
||||
while (timeLabel.firstChild != null)
|
||||
timeLabel.removeChild(timeLabel.firstChild);
|
||||
timeLabel.appendChild(document.createTextNode("" + partitionTime));
|
||||
|
||||
const expandedMeshes = this.glyphStorage.expandMeshes(this.baseMeshes)[0];
|
||||
const expandedMeshes = textFrame.expandMeshes(this.baseMeshes, glyphIDs);
|
||||
this.expandedMeshes = expandedMeshes;
|
||||
|
||||
this.view.then(view => {
|
||||
|
@ -162,7 +164,7 @@ class BenchmarkAppController extends DemoAppController<BenchmarkTestView> {
|
|||
private resultsTableBody: HTMLTableSectionElement;
|
||||
private resultsPartitioningTimeLabel: HTMLSpanElement;
|
||||
|
||||
private glyphStorage: TextFrameGlyphStorage<BenchmarkGlyph>;
|
||||
private glyphStore: GlyphStore;
|
||||
private baseMeshes: PathfinderMeshData;
|
||||
private expandedMeshes: ExpandedMeshData;
|
||||
|
||||
|
@ -170,8 +172,8 @@ class BenchmarkAppController extends DemoAppController<BenchmarkTestView> {
|
|||
private elapsedTimes: ElapsedTime[];
|
||||
private partitionTime: number;
|
||||
|
||||
font: opentype.Font | null;
|
||||
textRun: TextRun<BenchmarkGlyph> | null;
|
||||
font: PathfinderFont | null;
|
||||
textRun: TextRun | null;
|
||||
}
|
||||
|
||||
class BenchmarkTestView extends MonochromePathfinderView {
|
||||
|
@ -212,13 +214,16 @@ class BenchmarkTestView extends MonochromePathfinderView {
|
|||
|
||||
let currentX = 0, currentY = 0;
|
||||
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++) {
|
||||
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);
|
||||
|
||||
currentX += glyph.advanceWidth;
|
||||
currentX += unwrapNull(this.appController.font).opentypeFont
|
||||
.glyphs
|
||||
.get(glyphID)
|
||||
.advanceWidth;
|
||||
if (currentX > availableWidth) {
|
||||
currentX = 0;
|
||||
currentY += lineHeight;
|
||||
|
@ -230,14 +235,14 @@ class BenchmarkTestView extends MonochromePathfinderView {
|
|||
|
||||
protected renderingFinished(): void {
|
||||
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;
|
||||
this.renderingPromiseCallback(usPerGlyph);
|
||||
}
|
||||
}
|
||||
|
||||
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 pathHintsBufferTexture = new PathfinderBufferTexture(this.gl, 'uPathHints');
|
||||
|
@ -276,7 +281,7 @@ class BenchmarkTestView extends MonochromePathfinderView {
|
|||
}
|
||||
|
||||
private get pixelsPerUnit(): number {
|
||||
return this._pixelsPerEm / unwrapNull(this.appController.font).unitsPerEm;
|
||||
return this._pixelsPerEm / unwrapNull(this.appController.font).opentypeFont.unitsPerEm;
|
||||
}
|
||||
|
||||
get pixelsPerEm(): number {
|
||||
|
@ -304,8 +309,6 @@ class BenchmarkTestView extends MonochromePathfinderView {
|
|||
protected camera: OrthographicCamera;
|
||||
}
|
||||
|
||||
class BenchmarkGlyph extends PathfinderGlyph {}
|
||||
|
||||
function main() {
|
||||
const controller = new BenchmarkAppController;
|
||||
window.addEventListener('load', () => controller.start(), false);
|
||||
|
|
|
@ -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_CONTROL_POINT_VERTEX_OFFSET, PathfinderMeshData} from "./meshes";
|
||||
import {SVGLoader, BUILTIN_SVG_URI} from './svg-loader';
|
||||
import {BUILTIN_FONT_URI, TextFrameGlyphStorage, PathfinderGlyph, TextRun} from "./text";
|
||||
import {GlyphStorage, TextFrame} from "./text";
|
||||
import {BUILTIN_FONT_URI, TextRun} from "./text";
|
||||
import { GlyphStore, TextFrame, PathfinderFont } from "./text";
|
||||
import {unwrapNull, UINT32_SIZE, UINT32_MAX, assert} from "./utils";
|
||||
import {PathfinderView} from "./view";
|
||||
import {Font} from 'opentype.js';
|
||||
|
@ -111,22 +111,21 @@ class MeshDebuggerAppController extends AppController {
|
|||
}
|
||||
|
||||
private fontLoaded(fileData: ArrayBuffer): void {
|
||||
this.file = opentype.parse(fileData);
|
||||
assert(this.file.isSupported(), "The font type is unsupported!");
|
||||
this.file = new PathfinderFont(fileData);
|
||||
this.fileData = fileData;
|
||||
|
||||
const glyphCount = this.file.numGlyphs;
|
||||
const glyphCount = this.file.opentypeFont.numGlyphs;
|
||||
for (let glyphIndex = 1; glyphIndex < glyphCount; glyphIndex++) {
|
||||
const newOption = document.createElement('option');
|
||||
newOption.value = "" + glyphIndex;
|
||||
const glyphName = this.file.glyphIndexToName(glyphIndex);
|
||||
const glyphName = this.file.opentypeFont.glyphIndexToName(glyphIndex);
|
||||
newOption.appendChild(document.createTextNode(glyphName));
|
||||
this.fontPathSelect.appendChild(newOption);
|
||||
}
|
||||
|
||||
// Automatically load a path if this is the initial pageload.
|
||||
if (this.meshes == null)
|
||||
this.loadPath(this.file.charToGlyph(CHARACTER));
|
||||
this.loadPath(this.file.opentypeFont.charToGlyph(CHARACTER));
|
||||
}
|
||||
|
||||
private svgLoaded(fileData: ArrayBuffer): void {
|
||||
|
@ -148,14 +147,13 @@ class MeshDebuggerAppController extends AppController {
|
|||
|
||||
let promise: Promise<PathfinderMeshData>;
|
||||
|
||||
if (this.file instanceof opentype.Font && this.fileData != null) {
|
||||
if (this.file instanceof PathfinderFont && this.fileData != null) {
|
||||
if (opentypeGlyph == null) {
|
||||
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 GlyphStorage(this.fileData, [glyph], this.file);
|
||||
const glyphStorage = new GlyphStore(this.file, [(opentypeGlyph as any).index]);
|
||||
promise = glyphStorage.partition().then(result => result.meshes);
|
||||
} else if (this.file instanceof SVGLoader) {
|
||||
promise = this.file.partition(this.fontPathSelect.selectedIndex);
|
||||
|
@ -171,7 +169,7 @@ class MeshDebuggerAppController extends AppController {
|
|||
|
||||
protected readonly defaultFile: string = FONT;
|
||||
|
||||
private file: Font | SVGLoader | null;
|
||||
private file: PathfinderFont | SVGLoader | null;
|
||||
private fileType: FileType;
|
||||
private fileData: ArrayBuffer | null;
|
||||
|
||||
|
@ -318,8 +316,6 @@ class MeshDebuggerView extends PathfinderView {
|
|||
camera: OrthographicCamera;
|
||||
}
|
||||
|
||||
class MeshDebuggerGlyph extends PathfinderGlyph {}
|
||||
|
||||
function getPosition(positions: Float32Array, vertexIndex: number): Float32Array | null {
|
||||
if (vertexIndex == UINT32_MAX)
|
||||
return null;
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
// option. This file may not be copied, modified, or distributed
|
||||
// except according to those terms.
|
||||
|
||||
import {Font} from 'opentype.js';
|
||||
import * as _ from 'lodash';
|
||||
import * as base64js from 'base64-js';
|
||||
import * as glmatrix from 'gl-matrix';
|
||||
|
@ -23,12 +22,14 @@ import {createFramebufferDepthTexture, QUAD_ELEMENTS, setTextureParameters} from
|
|||
import {UniformMap} from './gl-utils';
|
||||
import {PathfinderMeshBuffers, PathfinderMeshData} from './meshes';
|
||||
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 {unwrapNull} from './utils';
|
||||
import { MonochromePathfinderView, Timings, TIMINGS } from './view';
|
||||
import PathfinderBufferTexture from './buffer-texture';
|
||||
import SSAAStrategy from './ssaa-strategy';
|
||||
import { Metrics } from 'opentype.js';
|
||||
|
||||
const DEFAULT_TEXT: string =
|
||||
`’Twas brillig, and the slithy toves
|
||||
|
@ -159,7 +160,6 @@ class TextDemoController extends DemoAppController<TextDemoView> {
|
|||
|
||||
private updateText(): void {
|
||||
this.text = this.editTextArea.value;
|
||||
//this.recreateLayout();
|
||||
|
||||
window.jQuery(this.editTextModal).modal('hide');
|
||||
}
|
||||
|
@ -171,16 +171,23 @@ class TextDemoController extends DemoAppController<TextDemoView> {
|
|||
}
|
||||
|
||||
protected fileLoaded(fileData: ArrayBuffer) {
|
||||
this.recreateLayout(fileData);
|
||||
const font = new PathfinderFont(fileData);
|
||||
this.recreateLayout(font);
|
||||
}
|
||||
|
||||
private recreateLayout(fileData: ArrayBuffer) {
|
||||
const newLayout = new SimpleTextLayout(fileData,
|
||||
this.text,
|
||||
glyph => new GlyphInstance(glyph));
|
||||
newLayout.glyphStorage.partition().then(result => {
|
||||
private recreateLayout(font: PathfinderFont) {
|
||||
const newLayout = new SimpleTextLayout(font, this.text);
|
||||
|
||||
let uniqueGlyphIDs = newLayout.textFrame.allGlyphIDs;
|
||||
uniqueGlyphIDs.sort((a, b) => a - b);
|
||||
uniqueGlyphIDs = _.sortedUniq(uniqueGlyphIDs);
|
||||
|
||||
const glyphStore = new GlyphStore(font, uniqueGlyphIDs);
|
||||
glyphStore.partition().then(result => {
|
||||
this.view.then(view => {
|
||||
this.font = font;
|
||||
this.layout = newLayout;
|
||||
this.glyphStore = glyphStore;
|
||||
this.meshes = result.meshes;
|
||||
|
||||
view.attachText();
|
||||
|
@ -206,7 +213,7 @@ class TextDemoController extends DemoAppController<TextDemoView> {
|
|||
}
|
||||
|
||||
get pixelsPerUnit(): number {
|
||||
return this._fontSize / this.layout.glyphStorage.font.unitsPerEm;
|
||||
return this._fontSize / this.font.opentypeFont.unitsPerEm;
|
||||
}
|
||||
|
||||
get useHinting(): boolean {
|
||||
|
@ -214,7 +221,7 @@ class TextDemoController extends DemoAppController<TextDemoView> {
|
|||
}
|
||||
|
||||
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 {
|
||||
|
@ -225,6 +232,8 @@ class TextDemoController extends DemoAppController<TextDemoView> {
|
|||
return DEFAULT_FONT;
|
||||
}
|
||||
|
||||
font: PathfinderFont;
|
||||
|
||||
private hintingSelect: HTMLSelectElement;
|
||||
|
||||
private editTextModal: HTMLElement;
|
||||
|
@ -239,7 +248,8 @@ class TextDemoController extends DemoAppController<TextDemoView> {
|
|||
|
||||
private text: string;
|
||||
|
||||
layout: SimpleTextLayout<GlyphInstance>;
|
||||
layout: SimpleTextLayout;
|
||||
glyphStore: GlyphStore;
|
||||
}
|
||||
|
||||
class TextDemoView extends MonochromePathfinderView {
|
||||
|
@ -285,26 +295,32 @@ class TextDemoView extends MonochromePathfinderView {
|
|||
let textBounds = layout.textFrame.bounds;
|
||||
this.camera.bounds = textBounds;
|
||||
|
||||
const textGlyphs = layout.glyphStorage.allGlyphs;
|
||||
const glyphPositions = new Float32Array(textGlyphs.length * 8);
|
||||
const glyphIndices = new Uint32Array(textGlyphs.length * 6);
|
||||
const totalGlyphCount = layout.textFrame.totalGlyphCount;
|
||||
const glyphPositions = new Float32Array(totalGlyphCount * 8);
|
||||
const glyphIndices = new Uint32Array(totalGlyphCount * 6);
|
||||
|
||||
for (let glyphIndex = 0; glyphIndex < textGlyphs.length; glyphIndex++) {
|
||||
const textGlyph = textGlyphs[glyphIndex];
|
||||
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);
|
||||
const hint = this.appController.createHint();
|
||||
const pixelsPerUnit = this.appController.pixelsPerUnit;
|
||||
|
||||
for (let glyphIndexIndex = 0;
|
||||
glyphIndexIndex < QUAD_ELEMENTS.length;
|
||||
glyphIndexIndex++) {
|
||||
glyphIndices[glyphIndexIndex + glyphIndex * 6] =
|
||||
QUAD_ELEMENTS[glyphIndexIndex] + 4 * glyphIndex;
|
||||
let globalGlyphIndex = 0;
|
||||
for (const run of layout.textFrame.runs) {
|
||||
for (let glyphIndex = 0;
|
||||
glyphIndex < run.glyphIDs.length;
|
||||
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() {
|
||||
const textGlyphs = this.appController.layout.glyphStorage.allGlyphs;
|
||||
const font = this.appController.font;
|
||||
const glyphStore = this.appController.glyphStore;
|
||||
const pixelsPerUnit = this.appController.pixelsPerUnit;
|
||||
|
||||
const textFrame = this.appController.layout.textFrame;
|
||||
const hint = this.appController.createHint();
|
||||
|
||||
// Only build glyphs in view.
|
||||
|
@ -329,34 +347,39 @@ class TextDemoView extends MonochromePathfinderView {
|
|||
-translation[0] + this.canvas.width,
|
||||
-translation[1] + this.canvas.height);
|
||||
|
||||
let atlasGlyphs =
|
||||
textGlyphs.filter(glyph => {
|
||||
return rectsIntersect(glyph.pixelRect(hint, pixelsPerUnit), canvasRect);
|
||||
}).map(textGlyph => new AtlasGlyph(textGlyph.opentypeGlyph));
|
||||
atlasGlyphs.sort((a, b) => a.index - b.index);
|
||||
atlasGlyphs = _.sortedUniqBy(atlasGlyphs, glyph => glyph.index);
|
||||
let atlasGlyphs = [];
|
||||
for (const run of textFrame.runs) {
|
||||
for (let glyphIndex = 0; glyphIndex < run.glyphIDs.length; glyphIndex++) {
|
||||
const pixelRect = run.pixelRectForGlyphAt(glyphIndex, pixelsPerUnit, hint);
|
||||
if (!rectsIntersect(pixelRect, canvasRect))
|
||||
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.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.appController.atlas.layoutGlyphs(atlasGlyphs, font, pixelsPerUnit, hint);
|
||||
|
||||
this.uploadPathTransforms(1);
|
||||
|
||||
// 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++) {
|
||||
const glyph = atlasGlyphs[glyphIndex];
|
||||
|
||||
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;
|
||||
for (let glyphID = 0; glyphID < glyphCount; glyphID++) {
|
||||
pathHints[glyphID * 4 + 0] = hint.xHeight;
|
||||
pathHints[glyphID * 4 + 1] = hint.hintedXHeight;
|
||||
}
|
||||
|
||||
const pathHintsBufferTexture = new PathfinderBufferTexture(this.gl, 'uPathHints');
|
||||
|
@ -367,24 +390,18 @@ class TextDemoView extends MonochromePathfinderView {
|
|||
}
|
||||
|
||||
protected pathTransformsForObject(objectIndex: number): Float32Array {
|
||||
const glyphCount = this.appController.glyphStore.glyphIDs.length;
|
||||
const atlasGlyphs = this.appController.atlasGlyphs;
|
||||
const pixelsPerUnit = this.appController.pixelsPerUnit;
|
||||
|
||||
const uniqueGlyphs = this.appController.layout.glyphStorage.uniqueGlyphs;
|
||||
const uniqueGlyphIndices = uniqueGlyphs.map(glyph => glyph.index);
|
||||
uniqueGlyphIndices.sort((a, b) => a - b);
|
||||
|
||||
const transforms = new Float32Array((uniqueGlyphs.length + 1) * 4);
|
||||
const transforms = new Float32Array((glyphCount + 1) * 4);
|
||||
|
||||
for (let glyphIndex = 0; glyphIndex < atlasGlyphs.length; glyphIndex++) {
|
||||
const glyph = atlasGlyphs[glyphIndex];
|
||||
|
||||
let pathID = _.sortedIndexOf(uniqueGlyphIndices, glyph.index);
|
||||
assert(pathID >= 0, "No path ID!");
|
||||
pathID++;
|
||||
const pathID = glyph.glyphStoreIndex + 1;
|
||||
const atlasOrigin = glyph.calculatePixelOrigin(pixelsPerUnit);
|
||||
|
||||
const atlasOrigin = glyph.pixelOrigin(pixelsPerUnit);
|
||||
const metrics = glyph.metrics;
|
||||
transforms[pathID * 4 + 0] = pixelsPerUnit;
|
||||
transforms[pathID * 4 + 1] = pixelsPerUnit;
|
||||
transforms[pathID * 4 + 2] = atlasOrigin[0];
|
||||
|
@ -398,9 +415,9 @@ class TextDemoView extends MonochromePathfinderView {
|
|||
const atlasColorTexture = this.appController.atlas.ensureTexture(this.gl);
|
||||
this.atlasDepthTexture = createFramebufferDepthTexture(this.gl, ATLAS_SIZE);
|
||||
this.atlasFramebuffer = createFramebuffer(this.gl,
|
||||
this.drawBuffersExt,
|
||||
[atlasColorTexture],
|
||||
this.atlasDepthTexture);
|
||||
this.drawBuffersExt,
|
||||
[atlasColorTexture],
|
||||
this.atlasDepthTexture);
|
||||
|
||||
// Allow the antialiasing strategy to set up framebuffers as necessary.
|
||||
if (this.antialiasingStrategy != null)
|
||||
|
@ -408,39 +425,51 @@ class TextDemoView extends MonochromePathfinderView {
|
|||
}
|
||||
|
||||
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 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++) {
|
||||
const textGlyph = textGlyphs[textGlyphIndex];
|
||||
const textGlyphMetrics = textGlyph.metrics;
|
||||
let atlasGlyphIndex = _.sortedIndexOf(atlasGlyphIDs, textGlyphID);
|
||||
if (atlasGlyphIndex < 0)
|
||||
continue;
|
||||
|
||||
let atlasGlyphIndex = _.sortedIndexOf(atlasGlyphIndices, textGlyph.index);
|
||||
if (atlasGlyphIndex < 0)
|
||||
continue;
|
||||
// Set texture coordinates.
|
||||
const atlasGlyph = atlasGlyphs[atlasGlyphIndex];
|
||||
const atlasGlyphMetrics = font.metricsForGlyph(atlasGlyph.glyphID);
|
||||
if (atlasGlyphMetrics == null)
|
||||
continue;
|
||||
|
||||
// Set texture coordinates.
|
||||
const atlasGlyph = atlasGlyphs[atlasGlyphIndex];
|
||||
const atlasGlyphRect = atlasGlyph.pixelRect(hint, this.appController.pixelsPerUnit);
|
||||
const atlasGlyphBL = atlasGlyphRect.slice(0, 2) as glmatrix.vec2;
|
||||
const atlasGlyphTR = atlasGlyphRect.slice(2, 4) as glmatrix.vec2;
|
||||
glmatrix.vec2.div(atlasGlyphBL, atlasGlyphBL, ATLAS_SIZE);
|
||||
glmatrix.vec2.div(atlasGlyphTR, atlasGlyphTR, ATLAS_SIZE);
|
||||
const atlasGlyphPixelOrigin = atlasGlyph.calculatePixelOrigin(pixelsPerUnit);
|
||||
const atlasGlyphRect = calculatePixelRectForGlyph(atlasGlyphMetrics,
|
||||
atlasGlyphPixelOrigin,
|
||||
pixelsPerUnit,
|
||||
hint);
|
||||
const atlasGlyphBL = atlasGlyphRect.slice(0, 2) as glmatrix.vec2;
|
||||
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([
|
||||
atlasGlyphBL[0], atlasGlyphTR[1],
|
||||
atlasGlyphTR[0], atlasGlyphTR[1],
|
||||
atlasGlyphBL[0], atlasGlyphBL[1],
|
||||
atlasGlyphTR[0], atlasGlyphBL[1],
|
||||
], textGlyphIndex * 8);
|
||||
glyphTexCoords.set([
|
||||
atlasGlyphBL[0], atlasGlyphTR[1],
|
||||
atlasGlyphTR[0], atlasGlyphTR[1],
|
||||
atlasGlyphBL[0], atlasGlyphBL[1],
|
||||
atlasGlyphTR[0], atlasGlyphBL[1],
|
||||
], globalGlyphIndex * 8);
|
||||
}
|
||||
}
|
||||
|
||||
this.glyphTexCoordsBuffer = unwrapNull(this.gl.createBuffer());
|
||||
|
@ -457,7 +486,7 @@ class TextDemoView extends MonochromePathfinderView {
|
|||
this.layoutText();
|
||||
this.camera.zoomToFit();
|
||||
this.appController.fontSize = this.camera.scale *
|
||||
this.appController.layout.glyphStorage.font.unitsPerEm;
|
||||
this.appController.font.opentypeFont.unitsPerEm;
|
||||
this.buildAtlasGlyphs();
|
||||
this.setDirty();
|
||||
|
||||
|
@ -477,7 +506,7 @@ class TextDemoView extends MonochromePathfinderView {
|
|||
|
||||
protected onZoom() {
|
||||
this.appController.fontSize = this.camera.scale *
|
||||
this.appController.layout.glyphStorage.font.unitsPerEm;
|
||||
this.appController.font.opentypeFont.unitsPerEm;
|
||||
this.buildAtlasGlyphs();
|
||||
this.setDirty();
|
||||
}
|
||||
|
@ -543,7 +572,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.allGlyphs.length * 6,
|
||||
this.appController.layout.textFrame.totalGlyphCount * 6,
|
||||
this.gl.UNSIGNED_INT,
|
||||
0);
|
||||
}
|
||||
|
@ -632,24 +661,40 @@ class Atlas {
|
|||
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 shelfBottom = 2.0;
|
||||
|
||||
for (const glyph of glyphs) {
|
||||
// Place the glyph, and advance the origin.
|
||||
glyph.setPixelLowerLeft(nextOrigin, pixelsPerUnit);
|
||||
nextOrigin[0] = glyph.pixelRect(hint, pixelsPerUnit)[2] + 1.0;
|
||||
const metrics = font.metricsForGlyph(glyph.glyphID);
|
||||
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 (nextOrigin[0] > ATLAS_SIZE[0]) {
|
||||
nextOrigin = glmatrix.vec2.fromValues(1.0, shelfBottom + 1.0);
|
||||
glyph.setPixelLowerLeft(nextOrigin, pixelsPerUnit);
|
||||
nextOrigin[0] = glyph.pixelRect(hint, pixelsPerUnit)[2] + 1.0;
|
||||
nextOrigin = glmatrix.vec2.clone([1.0, shelfBottom + 1.0]);
|
||||
glyph.setPixelLowerLeft(nextOrigin, metrics, pixelsPerUnit);
|
||||
pixelOrigin = glyph.calculatePixelOrigin(pixelsPerUnit);
|
||||
nextOrigin[0] = calculatePixelRectForGlyph(metrics,
|
||||
pixelOrigin,
|
||||
pixelsPerUnit,
|
||||
hint)[2] + 1.0;
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
@ -685,16 +730,36 @@ class Atlas {
|
|||
private _usedSize: Size2D;
|
||||
}
|
||||
|
||||
class AtlasGlyph extends PathfinderGlyph {
|
||||
constructor(glyph: opentype.Glyph) {
|
||||
super(glyph);
|
||||
class AtlasGlyph {
|
||||
constructor(glyphStoreIndex: number, glyphID: number) {
|
||||
this.glyphStoreIndex = glyphStoreIndex;
|
||||
this.glyphID = glyphID;
|
||||
this.origin = glmatrix.vec2.create();
|
||||
}
|
||||
}
|
||||
|
||||
class GlyphInstance extends PathfinderGlyph {
|
||||
constructor(glyph: opentype.Glyph) {
|
||||
super(glyph);
|
||||
calculatePixelOrigin(pixelsPerUnit: number): glmatrix.vec2 {
|
||||
const pixelOrigin = glmatrix.vec2.create();
|
||||
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 = {
|
||||
|
|
|
@ -8,14 +8,14 @@
|
|||
// option. This file may not be copied, modified, or distributed
|
||||
// except according to those terms.
|
||||
|
||||
import {Font, Metrics} from 'opentype.js';
|
||||
import {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 {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";
|
||||
|
||||
|
@ -30,8 +30,6 @@ export interface PartitionResult {
|
|||
time: number,
|
||||
}
|
||||
|
||||
type CreateGlyphFn<Glyph> = (glyph: opentype.Glyph) => Glyph;
|
||||
|
||||
export interface PixelMetrics {
|
||||
left: number;
|
||||
right: number;
|
||||
|
@ -48,48 +46,103 @@ opentype.Font.prototype.lineHeight = function() {
|
|||
return os2Table.sTypoAscender - os2Table.sTypoDescender + os2Table.sTypoLineGap;
|
||||
};
|
||||
|
||||
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);
|
||||
export class PathfinderFont {
|
||||
constructor(data: ArrayBuffer) {
|
||||
this.data = data;
|
||||
|
||||
this.glyphs = text;
|
||||
this.origin = origin;
|
||||
this.opentypeFont = opentype.parse(data);
|
||||
if (!this.opentypeFont.isSupported())
|
||||
panic("Unsupported font!");
|
||||
|
||||
this.metricsCache = [];
|
||||
}
|
||||
|
||||
layout() {
|
||||
let currentX = this.origin[0];
|
||||
for (const glyph of this.glyphs) {
|
||||
glyph.origin = glmatrix.vec2.fromValues(currentX, this.origin[1]);
|
||||
currentX += glyph.advanceWidth;
|
||||
}
|
||||
metricsForGlyph(glyphID: number): Metrics | null {
|
||||
if (this.metricsCache[glyphID] == null)
|
||||
this.metricsCache[glyphID] = this.opentypeFont.glyphs.get(glyphID).getMetrics();
|
||||
return this.metricsCache[glyphID];
|
||||
}
|
||||
|
||||
get measure(): number {
|
||||
const lastGlyph = _.last(this.glyphs);
|
||||
return lastGlyph == null ? 0.0 : lastGlyph.origin[0] + lastGlyph.advanceWidth;
|
||||
}
|
||||
readonly opentypeFont: opentype.Font;
|
||||
readonly data: ArrayBuffer;
|
||||
|
||||
readonly glyphs: Glyph[];
|
||||
readonly origin: number[];
|
||||
private metricsCache: Metrics[];
|
||||
}
|
||||
|
||||
export class TextFrame<Glyph extends PathfinderGlyph> {
|
||||
constructor(runs: TextRun<Glyph>[], font: Font) {
|
||||
this.runs = runs;
|
||||
export class TextRun {
|
||||
constructor(text: number[] | string, origin: number[], font: PathfinderFont) {
|
||||
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;
|
||||
}
|
||||
|
||||
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 = [];
|
||||
for (const textRun of this.runs) {
|
||||
for (const textGlyph of textRun.glyphs) {
|
||||
const uniqueGlyphIndex = _.sortedIndexBy(uniqueGlyphs, textGlyph, 'index');
|
||||
if (uniqueGlyphIndex >= 0)
|
||||
pathIDs.push(uniqueGlyphIndex + 1);
|
||||
for (let glyphIndex = 0; glyphIndex < textRun.glyphIDs.length; glyphIndex++) {
|
||||
const glyphID = textRun.glyphIDs[glyphIndex];
|
||||
if (glyphID === 0)
|
||||
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 upperRight = glmatrix.vec2.clone([lowerRight[0], upperLeft[1]]);
|
||||
|
||||
const lineHeight = this.font.lineHeight();
|
||||
const lineHeight = this.font.opentypeFont.lineHeight();
|
||||
lowerLeft[1] -= lineHeight;
|
||||
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]]);
|
||||
}
|
||||
|
||||
get allGlyphs(): Glyph[] {
|
||||
return _.flatMap(this.runs, run => run.glyphs);
|
||||
get totalGlyphCount(): number {
|
||||
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;
|
||||
|
||||
private readonly font: Font;
|
||||
private readonly font: PathfinderFont;
|
||||
}
|
||||
|
||||
export class GlyphStorage<Glyph extends PathfinderGlyph> {
|
||||
constructor(fontData: ArrayBuffer, glyphs: Glyph[], font?: Font) {
|
||||
if (font == null) {
|
||||
font = opentype.parse(fontData);
|
||||
assert(font.isSupported(), "The font type is unsupported!");
|
||||
}
|
||||
|
||||
this.fontData = fontData;
|
||||
/// Stores one copy of each glyph.
|
||||
export class GlyphStore {
|
||||
constructor(font: PathfinderFont, glyphIDs: number[]) {
|
||||
this.font = font;
|
||||
|
||||
// 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);
|
||||
this.glyphIDs = glyphIDs;
|
||||
}
|
||||
|
||||
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!
|
||||
const request = {
|
||||
face: {
|
||||
Custom: base64js.fromByteArray(new Uint8Array(this.fontData)),
|
||||
Custom: base64js.fromByteArray(new Uint8Array(this.font.data)),
|
||||
},
|
||||
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,
|
||||
glyphs: this.glyphIDs.map(id => ({ id: id, transform: [1, 0, 0, 1, 0, 0] })),
|
||||
pointSize: this.font.opentypeFont.unitsPerEm,
|
||||
};
|
||||
|
||||
// Make the request.
|
||||
|
@ -178,150 +223,36 @@ export class GlyphStorage<Glyph extends PathfinderGlyph> {
|
|||
});
|
||||
}
|
||||
|
||||
readonly fontData: ArrayBuffer;
|
||||
readonly font: Font;
|
||||
readonly uniqueGlyphs: Glyph[];
|
||||
indexOfGlyphWithID(glyphID: number): number | null {
|
||||
const index = _.sortedIndexOf(this.glyphIDs, glyphID);
|
||||
return index >= 0 ? index : null;
|
||||
}
|
||||
|
||||
readonly font: PathfinderFont;
|
||||
readonly glyphIDs: number[];
|
||||
}
|
||||
|
||||
export class TextFrameGlyphStorage<Glyph extends PathfinderGlyph> extends GlyphStorage<Glyph> {
|
||||
constructor(fontData: ArrayBuffer, textFrames: TextFrame<Glyph>[], font?: Font) {
|
||||
const allGlyphs = _.flatMap(textFrames, textRun => textRun.allGlyphs);
|
||||
super(fontData, allGlyphs, 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);
|
||||
export class SimpleTextLayout {
|
||||
constructor(font: PathfinderFont, text: string) {
|
||||
const lineHeight = font.opentypeFont.lineHeight();
|
||||
const textRuns: TextRun[] = text.split("\n").map((line, lineNumber) => {
|
||||
return new TextRun(line, [0.0, -lineHeight * lineNumber], font);
|
||||
});
|
||||
this.textFrame = new TextFrame(textRuns, font);
|
||||
|
||||
this.glyphStorage = new TextFrameGlyphStorage(fontData, [this.textFrame], font);
|
||||
}
|
||||
|
||||
layoutRuns() {
|
||||
this.textFrame.runs.forEach(textRun => textRun.layout());
|
||||
}
|
||||
|
||||
get allGlyphs(): Glyph[] {
|
||||
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;
|
||||
readonly textFrame: TextFrame;
|
||||
}
|
||||
|
||||
export class Hint {
|
||||
constructor(font: Font, pixelsPerUnit: number, useHinting: boolean) {
|
||||
constructor(font: PathfinderFont, pixelsPerUnit: number, useHinting: boolean) {
|
||||
this.useHinting = useHinting;
|
||||
|
||||
const os2Table = font.tables.os2;
|
||||
const os2Table = font.opentypeFont.tables.os2;
|
||||
this.xHeight = os2Table.sxHeight != null ? os2Table.sxHeight : 0;
|
||||
|
||||
if (!useHinting) {
|
||||
|
@ -352,3 +283,34 @@ export class Hint {
|
|||
readonly hintedXHeight: number;
|
||||
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]);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue