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