2017-08-31 19:11:09 -04:00
|
|
|
// pathfinder/client/src/text.ts
|
|
|
|
//
|
|
|
|
// Copyright © 2017 The Pathfinder Project Developers.
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
|
|
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
|
|
|
// option. This file may not be copied, modified, or distributed
|
|
|
|
// except according to those terms.
|
|
|
|
|
|
|
|
import {Font, Metrics} from 'opentype.js';
|
|
|
|
import * as base64js from 'base64-js';
|
|
|
|
import * as glmatrix from 'gl-matrix';
|
|
|
|
import * as _ from 'lodash';
|
|
|
|
import * as opentype from "opentype.js";
|
|
|
|
|
2017-09-06 17:11:58 -04:00
|
|
|
import {B_QUAD_SIZE, PathfinderMeshData} from "./meshes";
|
|
|
|
import {UINT32_SIZE, UINT32_MAX, assert, panic} from "./utils";
|
2017-08-31 19:11:09 -04:00
|
|
|
|
2017-08-31 20:08:22 -04:00
|
|
|
export const BUILTIN_FONT_URI: string = "/otf/demo";
|
|
|
|
|
2017-08-31 19:11:09 -04:00
|
|
|
const PARTITION_FONT_ENDPOINT_URI: string = "/partition-font";
|
|
|
|
|
2017-09-06 17:11:58 -04:00
|
|
|
export interface ExpandedMeshData {
|
|
|
|
meshes: PathfinderMeshData;
|
|
|
|
}
|
|
|
|
|
2017-09-02 01:29:05 -04:00
|
|
|
type CreateGlyphFn<Glyph> = (glyph: opentype.Glyph) => Glyph;
|
|
|
|
|
2017-09-03 19:35:10 -04:00
|
|
|
export interface PixelMetrics {
|
|
|
|
left: number;
|
|
|
|
right: number;
|
|
|
|
ascent: number;
|
|
|
|
descent: number;
|
|
|
|
}
|
|
|
|
|
2017-08-31 19:11:09 -04:00
|
|
|
opentype.Font.prototype.isSupported = function() {
|
|
|
|
return (this as any).supported;
|
|
|
|
}
|
|
|
|
|
2017-09-07 01:11:32 -04:00
|
|
|
opentype.Font.prototype.lineHeight = function() {
|
|
|
|
const os2Table = this.tables.os2;
|
|
|
|
return os2Table.sTypoAscender - os2Table.sTypoDescender + os2Table.sTypoLineGap;
|
|
|
|
};
|
|
|
|
|
2017-09-07 19:13:55 -04:00
|
|
|
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);
|
2017-08-31 19:11:09 -04:00
|
|
|
|
2017-09-07 19:13:55 -04:00
|
|
|
this.glyphs = text;
|
|
|
|
this.origin = origin;
|
|
|
|
}
|
2017-08-31 19:11:09 -04:00
|
|
|
|
2017-09-07 19:13:55 -04:00
|
|
|
layout() {
|
|
|
|
let currentX = this.origin[0];
|
|
|
|
for (const glyph of this.glyphs) {
|
|
|
|
glyph.origin = glmatrix.vec2.fromValues(currentX, this.origin[1]);
|
|
|
|
currentX += glyph.advanceWidth;
|
|
|
|
}
|
2017-08-31 19:11:09 -04:00
|
|
|
}
|
|
|
|
|
2017-09-11 19:07:11 -04:00
|
|
|
get measure(): number {
|
|
|
|
const lastGlyph = _.last(this.glyphs);
|
|
|
|
return lastGlyph == null ? 0.0 : lastGlyph.origin[0] + lastGlyph.advanceWidth;
|
|
|
|
}
|
|
|
|
|
2017-09-07 19:13:55 -04:00
|
|
|
readonly glyphs: Glyph[];
|
|
|
|
readonly origin: number[];
|
|
|
|
}
|
2017-08-31 19:11:09 -04:00
|
|
|
|
2017-09-07 19:13:55 -04:00
|
|
|
export class TextFrame<Glyph extends PathfinderGlyph> {
|
2017-09-11 19:07:11 -04:00
|
|
|
constructor(runs: TextRun<Glyph>[], font: Font) {
|
2017-09-07 19:13:55 -04:00
|
|
|
this.runs = runs;
|
2017-09-11 19:07:11 -04:00
|
|
|
this.font = font;
|
2017-08-31 19:11:09 -04:00
|
|
|
}
|
|
|
|
|
2017-09-07 19:13:55 -04:00
|
|
|
expandMeshes(uniqueGlyphs: Glyph[], meshes: PathfinderMeshData): ExpandedMeshData {
|
2017-09-06 17:11:58 -04:00
|
|
|
const bQuads = _.chunk(new Uint32Array(meshes.bQuads), B_QUAD_SIZE / UINT32_SIZE);
|
|
|
|
const bVertexPositions = new Float32Array(meshes.bVertexPositions);
|
|
|
|
const bVertexPathIDs = new Uint16Array(meshes.bVertexPathIDs);
|
|
|
|
const bVertexLoopBlinnData = new Uint32Array(meshes.bVertexLoopBlinnData);
|
|
|
|
|
2017-09-26 16:32:22 -04:00
|
|
|
const edgeUpperCurveIndices = new Uint32Array(meshes.edgeUpperCurveIndices);
|
|
|
|
const edgeLowerCurveIndices = new Uint32Array(meshes.edgeLowerCurveIndices);
|
|
|
|
for (let indexIndex = 3; indexIndex < edgeUpperCurveIndices.length; indexIndex += 4)
|
|
|
|
edgeUpperCurveIndices[indexIndex] = 0;
|
|
|
|
for (let indexIndex = 3; indexIndex < edgeLowerCurveIndices.length; indexIndex += 4)
|
|
|
|
edgeLowerCurveIndices[indexIndex] = 0;
|
|
|
|
|
2017-09-06 17:11:58 -04:00
|
|
|
const expandedBQuads: number[] = [];
|
|
|
|
const expandedBVertexPositions: number[] = [];
|
|
|
|
const expandedBVertexPathIDs: number[] = [];
|
|
|
|
const expandedBVertexLoopBlinnData: number[] = [];
|
|
|
|
const expandedCoverInteriorIndices: number[] = [];
|
|
|
|
const expandedCoverCurveIndices: number[] = [];
|
2017-09-26 16:32:22 -04:00
|
|
|
const expandedEdgeUpperCurveIndices: number[] = [];
|
|
|
|
const expandedEdgeUpperLineIndices: number[] = [];
|
|
|
|
const expandedEdgeLowerCurveIndices: number[] = [];
|
|
|
|
const expandedEdgeLowerLineIndices: number[] = [];
|
2017-09-06 17:11:58 -04:00
|
|
|
|
2017-09-07 01:11:32 -04:00
|
|
|
let textGlyphIndex = 0;
|
2017-09-07 19:13:55 -04:00
|
|
|
for (const textRun of this.runs) {
|
2017-09-07 01:11:32 -04:00
|
|
|
for (const textGlyph of textRun.glyphs) {
|
2017-09-07 19:13:55 -04:00
|
|
|
const uniqueGlyphIndex = _.sortedIndexBy(uniqueGlyphs, textGlyph, 'index');
|
2017-09-07 01:11:32 -04:00
|
|
|
if (uniqueGlyphIndex < 0)
|
|
|
|
continue;
|
|
|
|
const firstBVertexIndex = _.sortedIndex(bVertexPathIDs, uniqueGlyphIndex + 1);
|
|
|
|
if (firstBVertexIndex < 0)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
// Copy over vertices.
|
|
|
|
let bVertexIndex = firstBVertexIndex;
|
|
|
|
const firstExpandedBVertexIndex = expandedBVertexPathIDs.length;
|
|
|
|
while (bVertexIndex < bVertexPathIDs.length &&
|
|
|
|
bVertexPathIDs[bVertexIndex] === uniqueGlyphIndex + 1) {
|
|
|
|
expandedBVertexPositions.push(bVertexPositions[bVertexIndex * 2 + 0],
|
|
|
|
bVertexPositions[bVertexIndex * 2 + 1]);
|
|
|
|
expandedBVertexPathIDs.push(textGlyphIndex + 1);
|
|
|
|
expandedBVertexLoopBlinnData.push(bVertexLoopBlinnData[bVertexIndex]);
|
|
|
|
bVertexIndex++;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Copy over indices.
|
|
|
|
copyIndices(expandedCoverInteriorIndices,
|
|
|
|
new Uint32Array(meshes.coverInteriorIndices),
|
|
|
|
firstExpandedBVertexIndex,
|
|
|
|
firstBVertexIndex,
|
|
|
|
bVertexIndex);
|
|
|
|
copyIndices(expandedCoverCurveIndices,
|
|
|
|
new Uint32Array(meshes.coverCurveIndices),
|
|
|
|
firstExpandedBVertexIndex,
|
|
|
|
firstBVertexIndex,
|
|
|
|
bVertexIndex);
|
2017-09-06 17:11:58 -04:00
|
|
|
|
2017-09-26 16:32:22 -04:00
|
|
|
copyIndices(expandedEdgeUpperLineIndices,
|
|
|
|
new Uint32Array(meshes.edgeUpperLineIndices),
|
|
|
|
firstExpandedBVertexIndex,
|
|
|
|
firstBVertexIndex,
|
|
|
|
bVertexIndex);
|
|
|
|
copyIndices(expandedEdgeUpperCurveIndices,
|
|
|
|
new Uint32Array(edgeUpperCurveIndices),
|
|
|
|
firstExpandedBVertexIndex,
|
|
|
|
firstBVertexIndex,
|
|
|
|
bVertexIndex,
|
|
|
|
indexIndex => indexIndex % 4 < 3);
|
|
|
|
copyIndices(expandedEdgeLowerLineIndices,
|
|
|
|
new Uint32Array(meshes.edgeLowerLineIndices),
|
|
|
|
firstExpandedBVertexIndex,
|
|
|
|
firstBVertexIndex,
|
|
|
|
bVertexIndex);
|
|
|
|
copyIndices(expandedEdgeLowerCurveIndices,
|
|
|
|
new Uint32Array(edgeLowerCurveIndices),
|
|
|
|
firstExpandedBVertexIndex,
|
|
|
|
firstBVertexIndex,
|
|
|
|
bVertexIndex,
|
|
|
|
indexIndex => indexIndex % 4 < 3);
|
|
|
|
|
2017-09-07 01:11:32 -04:00
|
|
|
// Copy over B-quads.
|
|
|
|
let firstBQuadIndex =
|
|
|
|
_.findIndex(bQuads, bQuad => bVertexPathIDs[bQuad[0]] == uniqueGlyphIndex + 1);
|
|
|
|
if (firstBQuadIndex < 0)
|
|
|
|
firstBQuadIndex = bQuads.length;
|
|
|
|
const indexDelta = firstExpandedBVertexIndex - firstBVertexIndex;
|
|
|
|
for (let bQuadIndex = firstBQuadIndex; bQuadIndex < bQuads.length; bQuadIndex++) {
|
|
|
|
const bQuad = bQuads[bQuadIndex];
|
|
|
|
if (bVertexPathIDs[bQuad[0]] !== uniqueGlyphIndex + 1)
|
|
|
|
break;
|
|
|
|
for (let indexIndex = 0; indexIndex < B_QUAD_SIZE / UINT32_SIZE; indexIndex++) {
|
|
|
|
const srcIndex = bQuad[indexIndex];
|
|
|
|
if (srcIndex === UINT32_MAX)
|
|
|
|
expandedBQuads.push(srcIndex);
|
|
|
|
else
|
|
|
|
expandedBQuads.push(srcIndex + indexDelta);
|
|
|
|
}
|
2017-09-06 17:11:58 -04:00
|
|
|
}
|
2017-09-07 01:11:32 -04:00
|
|
|
|
|
|
|
textGlyphIndex++;
|
2017-09-06 17:11:58 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
meshes: new PathfinderMeshData({
|
|
|
|
bQuads: new Uint32Array(expandedBQuads).buffer as ArrayBuffer,
|
|
|
|
bVertexPositions: new Float32Array(expandedBVertexPositions).buffer as ArrayBuffer,
|
|
|
|
bVertexPathIDs: new Uint16Array(expandedBVertexPathIDs).buffer as ArrayBuffer,
|
|
|
|
bVertexLoopBlinnData: new Uint32Array(expandedBVertexLoopBlinnData).buffer as
|
|
|
|
ArrayBuffer,
|
|
|
|
coverInteriorIndices: new Uint32Array(expandedCoverInteriorIndices).buffer as
|
|
|
|
ArrayBuffer,
|
|
|
|
coverCurveIndices: new Uint32Array(expandedCoverCurveIndices).buffer as
|
|
|
|
ArrayBuffer,
|
2017-09-26 16:32:22 -04:00
|
|
|
edgeUpperCurveIndices: new Uint32Array(expandedEdgeUpperCurveIndices).buffer as
|
|
|
|
ArrayBuffer,
|
|
|
|
edgeUpperLineIndices: new Uint32Array(expandedEdgeUpperLineIndices).buffer as
|
|
|
|
ArrayBuffer,
|
|
|
|
edgeLowerCurveIndices: new Uint32Array(expandedEdgeLowerCurveIndices).buffer as
|
|
|
|
ArrayBuffer,
|
|
|
|
edgeLowerLineIndices: new Uint32Array(expandedEdgeLowerLineIndices).buffer as
|
|
|
|
ArrayBuffer,
|
2017-09-06 17:11:58 -04:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-11 19:07:11 -04:00
|
|
|
get bounds(): glmatrix.vec4 {
|
|
|
|
if (this.runs.length === 0)
|
|
|
|
return glmatrix.vec4.create();
|
|
|
|
|
|
|
|
const upperLeft = glmatrix.vec2.clone(this.runs[0].origin);
|
|
|
|
const lowerRight = glmatrix.vec2.clone(_.last(this.runs)!.origin);
|
|
|
|
|
2017-09-13 17:49:47 -04:00
|
|
|
const lowerLeft = glmatrix.vec2.clone([upperLeft[0], lowerRight[1]]);
|
|
|
|
const upperRight = glmatrix.vec2.clone([lowerRight[0], upperLeft[1]]);
|
|
|
|
|
2017-09-11 19:07:11 -04:00
|
|
|
const lineHeight = this.font.lineHeight();
|
2017-09-13 17:49:47 -04:00
|
|
|
lowerLeft[1] -= lineHeight;
|
|
|
|
upperRight[1] += lineHeight * 2.0;
|
2017-09-11 19:07:11 -04:00
|
|
|
|
2017-09-13 17:49:47 -04:00
|
|
|
upperRight[0] = _.defaultTo<number>(_.max(this.runs.map(run => run.measure)), 0.0);
|
2017-09-11 19:07:11 -04:00
|
|
|
|
2017-09-13 17:49:47 -04:00
|
|
|
return glmatrix.vec4.clone([lowerLeft[0], lowerLeft[1], upperRight[0], upperRight[1]]);
|
2017-09-11 19:07:11 -04:00
|
|
|
}
|
2017-09-07 19:13:55 -04:00
|
|
|
|
|
|
|
get allGlyphs(): Glyph[] {
|
|
|
|
return _.flatMap(this.runs, run => run.glyphs);
|
|
|
|
}
|
|
|
|
|
|
|
|
readonly runs: TextRun<Glyph>[];
|
|
|
|
readonly origin: glmatrix.vec3;
|
2017-09-11 19:07:11 -04:00
|
|
|
|
|
|
|
private readonly font: Font;
|
2017-09-07 19:13:55 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
export class GlyphStorage<Glyph extends PathfinderGlyph> {
|
2017-09-19 20:35:11 -04:00
|
|
|
constructor(fontData: ArrayBuffer, glyphs: Glyph[], font?: Font) {
|
2017-09-07 19:13:55 -04:00
|
|
|
if (font == null) {
|
|
|
|
font = opentype.parse(fontData);
|
|
|
|
assert(font.isSupported(), "The font type is unsupported!");
|
|
|
|
}
|
|
|
|
|
|
|
|
this.fontData = fontData;
|
|
|
|
this.font = font;
|
|
|
|
|
|
|
|
// Determine all glyphs potentially needed.
|
2017-09-19 20:35:11 -04:00
|
|
|
this.uniqueGlyphs = glyphs;
|
2017-09-07 19:13:55 -04:00
|
|
|
this.uniqueGlyphs.sort((a, b) => a.index - b.index);
|
|
|
|
this.uniqueGlyphs = _.sortedUniqBy(this.uniqueGlyphs, glyph => glyph.index);
|
|
|
|
}
|
|
|
|
|
|
|
|
partition(): Promise<PathfinderMeshData> {
|
|
|
|
// Build the partitioning request to the server.
|
|
|
|
//
|
|
|
|
// FIXME(pcwalton): If this is a builtin font, don't resend it to the server!
|
|
|
|
const request = {
|
|
|
|
face: {
|
|
|
|
Custom: base64js.fromByteArray(new Uint8Array(this.fontData)),
|
|
|
|
},
|
|
|
|
fontIndex: 0,
|
|
|
|
glyphs: this.uniqueGlyphs.map(glyph => {
|
|
|
|
const metrics = glyph.metrics;
|
|
|
|
return {
|
|
|
|
id: glyph.index,
|
|
|
|
transform: [1, 0, 0, 1, 0, 0],
|
|
|
|
};
|
|
|
|
}),
|
|
|
|
pointSize: this.font.unitsPerEm,
|
|
|
|
};
|
|
|
|
|
|
|
|
// Make the request.
|
|
|
|
return window.fetch(PARTITION_FONT_ENDPOINT_URI, {
|
|
|
|
method: 'POST',
|
|
|
|
headers: {'Content-Type': 'application/json'},
|
|
|
|
body: JSON.stringify(request),
|
|
|
|
}).then(response => response.text()).then(responseText => {
|
|
|
|
const response = JSON.parse(responseText);
|
|
|
|
if (!('Ok' in response))
|
|
|
|
panic("Failed to partition the font!");
|
|
|
|
return new PathfinderMeshData(response.Ok.pathData);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-09-19 20:35:11 -04:00
|
|
|
readonly fontData: ArrayBuffer;
|
|
|
|
readonly font: Font;
|
|
|
|
readonly uniqueGlyphs: Glyph[];
|
|
|
|
}
|
|
|
|
|
|
|
|
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));
|
|
|
|
}
|
|
|
|
|
2017-09-07 01:11:32 -04:00
|
|
|
layoutRuns() {
|
2017-09-07 19:13:55 -04:00
|
|
|
for (const textFrame of this.textFrames)
|
|
|
|
textFrame.runs.forEach(textRun => textRun.layout());
|
2017-09-07 01:11:32 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
get allGlyphs(): Glyph[] {
|
2017-09-07 19:13:55 -04:00
|
|
|
return _.flatMap(this.textFrames, textRun => textRun.allGlyphs);
|
2017-09-07 01:11:32 -04:00
|
|
|
}
|
|
|
|
|
2017-09-07 19:13:55 -04:00
|
|
|
readonly textFrames: TextFrame<Glyph>[];
|
2017-09-02 01:29:05 -04:00
|
|
|
}
|
|
|
|
|
2017-09-07 19:13:55 -04:00
|
|
|
export class SimpleTextLayout<Glyph extends PathfinderGlyph> {
|
2017-09-02 01:29:05 -04:00
|
|
|
constructor(fontData: ArrayBuffer, text: string, createGlyph: CreateGlyphFn<Glyph>) {
|
|
|
|
const font = opentype.parse(fontData);
|
|
|
|
assert(font.isSupported(), "The font type is unsupported!");
|
|
|
|
|
2017-09-11 19:07:11 -04:00
|
|
|
const lineHeight = font.lineHeight();
|
2017-09-07 19:13:55 -04:00
|
|
|
const textRuns: TextRun<Glyph>[] = text.split("\n").map((line, lineNumber) => {
|
2017-09-07 01:11:32 -04:00
|
|
|
return new TextRun<Glyph>(line, [0.0, -lineHeight * lineNumber], font, createGlyph);
|
|
|
|
});
|
2017-09-11 19:07:11 -04:00
|
|
|
this.textFrame = new TextFrame(textRuns, font);
|
2017-08-31 19:11:09 -04:00
|
|
|
|
2017-09-19 20:35:11 -04:00
|
|
|
this.glyphStorage = new TextFrameGlyphStorage(fontData, [this.textFrame], font);
|
2017-09-07 01:11:32 -04:00
|
|
|
}
|
2017-08-31 19:11:09 -04:00
|
|
|
|
2017-09-07 01:11:32 -04:00
|
|
|
layoutRuns() {
|
2017-09-07 19:13:55 -04:00
|
|
|
this.textFrame.runs.forEach(textRun => textRun.layout());
|
2017-09-07 01:11:32 -04:00
|
|
|
}
|
2017-08-31 19:11:09 -04:00
|
|
|
|
2017-09-07 01:11:32 -04:00
|
|
|
get allGlyphs(): Glyph[] {
|
2017-09-07 19:13:55 -04:00
|
|
|
return this.textFrame.allGlyphs;
|
2017-08-31 19:11:09 -04:00
|
|
|
}
|
|
|
|
|
2017-09-07 19:13:55 -04:00
|
|
|
readonly textFrame: TextFrame<Glyph>;
|
2017-09-19 20:35:11 -04:00
|
|
|
readonly glyphStorage: TextFrameGlyphStorage<Glyph>;
|
2017-08-31 19:11:09 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
export abstract class PathfinderGlyph {
|
|
|
|
constructor(glyph: opentype.Glyph) {
|
|
|
|
this.opentypeGlyph = glyph;
|
|
|
|
this._metrics = null;
|
2017-09-03 19:35:10 -04:00
|
|
|
this.origin = glmatrix.vec2.create();
|
2017-08-31 19:11:09 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2017-09-03 19:35:10 -04:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2017-09-15 15:57:32 -04:00
|
|
|
private calculatePixelXMin(pixelsPerUnit: number): number {
|
|
|
|
return Math.floor(this.metrics.xMin * pixelsPerUnit);
|
2017-09-03 19:35:10 -04:00
|
|
|
}
|
|
|
|
|
2017-09-09 16:12:51 -04:00
|
|
|
private calculatePixelDescent(pixelsPerUnit: number): number {
|
|
|
|
return Math.ceil(-this.metrics.yMin * pixelsPerUnit);
|
|
|
|
}
|
|
|
|
|
2017-09-15 15:57:32 -04:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2017-09-09 16:12:51 -04:00
|
|
|
protected pixelMetrics(hint: Hint, pixelsPerUnit: number): PixelMetrics {
|
2017-09-03 19:35:10 -04:00
|
|
|
const metrics = this.metrics;
|
2017-09-09 16:12:51 -04:00
|
|
|
const top = hint.hintPosition(glmatrix.vec2.fromValues(0, metrics.yMax))[1];
|
2017-09-03 19:35:10 -04:00
|
|
|
return {
|
2017-09-15 15:57:32 -04:00
|
|
|
left: this.calculatePixelXMin(pixelsPerUnit),
|
2017-09-03 19:35:10 -04:00
|
|
|
right: Math.ceil(metrics.xMax * pixelsPerUnit),
|
2017-09-09 16:12:51 -04:00
|
|
|
ascent: Math.ceil(top * pixelsPerUnit),
|
|
|
|
descent: this.calculatePixelDescent(pixelsPerUnit),
|
2017-09-03 19:35:10 -04:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2017-09-11 22:09:43 -04:00
|
|
|
calculatePixelOrigin(hint: Hint, pixelsPerUnit: number): glmatrix.vec2 {
|
2017-09-03 19:35:10 -04:00
|
|
|
const textGlyphOrigin = glmatrix.vec2.clone(this.origin);
|
|
|
|
glmatrix.vec2.scale(textGlyphOrigin, textGlyphOrigin, pixelsPerUnit);
|
2017-09-11 22:09:43 -04:00
|
|
|
return textGlyphOrigin;
|
|
|
|
}
|
|
|
|
|
|
|
|
pixelRect(hint: Hint, pixelsPerUnit: number): glmatrix.vec4 {
|
|
|
|
const pixelMetrics = this.pixelMetrics(hint, pixelsPerUnit);
|
|
|
|
const textGlyphOrigin = this.calculatePixelOrigin(hint, pixelsPerUnit);
|
2017-09-03 19:35:10 -04:00
|
|
|
glmatrix.vec2.round(textGlyphOrigin, textGlyphOrigin);
|
|
|
|
|
2017-09-15 15:57:32 -04:00
|
|
|
return glmatrix.vec4.fromValues(textGlyphOrigin[0] + pixelMetrics.left,
|
2017-09-03 19:35:10 -04:00
|
|
|
textGlyphOrigin[1] - pixelMetrics.descent,
|
|
|
|
textGlyphOrigin[0] + pixelMetrics.right,
|
|
|
|
textGlyphOrigin[1] + pixelMetrics.ascent);
|
|
|
|
|
2017-08-31 19:11:09 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
readonly opentypeGlyph: opentype.Glyph;
|
|
|
|
|
|
|
|
private _metrics: Metrics | null;
|
|
|
|
|
2017-09-03 19:35:10 -04:00
|
|
|
/// In font units, relative to (0, 0).
|
|
|
|
origin: glmatrix.vec2;
|
2017-08-31 19:11:09 -04:00
|
|
|
}
|
2017-09-06 17:11:58 -04:00
|
|
|
|
2017-09-09 16:12:51 -04:00
|
|
|
export class Hint {
|
|
|
|
constructor(font: Font, pixelsPerUnit: number, useHinting: boolean) {
|
|
|
|
this.useHinting = useHinting;
|
|
|
|
|
|
|
|
const os2Table = font.tables.os2;
|
|
|
|
this.xHeight = os2Table.sxHeight != null ? os2Table.sxHeight : 0;
|
|
|
|
|
|
|
|
if (!useHinting) {
|
|
|
|
this.hintedXHeight = this.xHeight;
|
|
|
|
} else {
|
|
|
|
this.hintedXHeight = Math.ceil(Math.ceil(this.xHeight * pixelsPerUnit) /
|
|
|
|
pixelsPerUnit);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
hintPosition(position: glmatrix.vec2): glmatrix.vec2 {
|
|
|
|
if (!this.useHinting)
|
|
|
|
return position;
|
|
|
|
|
|
|
|
if (position[1] < 0.0)
|
|
|
|
return position;
|
|
|
|
|
|
|
|
if (position[1] >= this.hintedXHeight) {
|
|
|
|
return glmatrix.vec2.fromValues(position[0],
|
|
|
|
position[1] - this.xHeight + this.hintedXHeight);
|
|
|
|
}
|
|
|
|
|
|
|
|
return glmatrix.vec2.fromValues(position[0],
|
|
|
|
position[1] / this.xHeight * this.hintedXHeight);
|
|
|
|
}
|
|
|
|
|
|
|
|
readonly xHeight: number;
|
|
|
|
readonly hintedXHeight: number;
|
|
|
|
private useHinting: boolean;
|
|
|
|
}
|
|
|
|
|
2017-09-06 17:11:58 -04:00
|
|
|
function copyIndices(destIndices: number[],
|
|
|
|
srcIndices: Uint32Array,
|
|
|
|
firstExpandedIndex: number,
|
|
|
|
firstIndex: number,
|
2017-09-26 16:32:22 -04:00
|
|
|
lastIndex: number,
|
|
|
|
validateIndex?: (indexIndex: number) => boolean) {
|
|
|
|
if (firstIndex === lastIndex)
|
|
|
|
return;
|
|
|
|
|
2017-09-06 17:11:58 -04:00
|
|
|
// FIXME(pcwalton): Use binary search instead of linear search.
|
2017-09-26 16:32:22 -04:00
|
|
|
let indexIndex = _.findIndex(srcIndices, srcIndex => {
|
|
|
|
return srcIndex >= firstIndex && srcIndex < lastIndex;
|
|
|
|
});
|
2017-09-06 17:11:58 -04:00
|
|
|
if (indexIndex < 0)
|
|
|
|
return;
|
2017-09-26 16:32:22 -04:00
|
|
|
|
|
|
|
const indexDelta = firstExpandedIndex - firstIndex;
|
2017-09-06 17:11:58 -04:00
|
|
|
while (indexIndex < srcIndices.length) {
|
|
|
|
const index = srcIndices[indexIndex];
|
2017-09-26 16:32:22 -04:00
|
|
|
if (validateIndex == null || validateIndex(indexIndex)) {
|
|
|
|
if (index < firstIndex || index >= lastIndex)
|
|
|
|
break;
|
|
|
|
destIndices.push(index + indexDelta);
|
|
|
|
} else {
|
|
|
|
destIndices.push(index);
|
|
|
|
}
|
2017-09-06 17:11:58 -04:00
|
|
|
indexIndex++;
|
|
|
|
}
|
|
|
|
}
|