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 {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);

View File

@ -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);

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_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;

View File

@ -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 = {

View File

@ -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]);
}