pathfinder/demo/client/src/text.ts

355 lines
12 KiB
TypeScript
Raw Normal View History

// 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";
import {B_QUAD_SIZE, PathfinderMeshData} from "./meshes";
import {UINT32_SIZE, UINT32_MAX, assert, panic} from "./utils";
export const BUILTIN_FONT_URI: string = "/otf/demo";
const PARTITION_FONT_ENDPOINT_URI: string = "/partition-font";
export interface ExpandedMeshData {
meshes: PathfinderMeshData;
}
2017-09-26 18:38:50 -04:00
export interface PartitionResult {
meshes: PathfinderMeshData,
time: number,
}
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;
}
opentype.Font.prototype.isSupported = function() {
return (this as any).supported;
}
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-09-07 19:13:55 -04:00
this.glyphs = text;
this.origin = origin;
}
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;
}
}
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-09-07 19:13:55 -04:00
export class TextFrame<Glyph extends PathfinderGlyph> {
constructor(runs: TextRun<Glyph>[], font: Font) {
2017-09-07 19:13:55 -04:00
this.runs = runs;
this.font = font;
}
2017-09-07 19:13:55 -04:00
expandMeshes(uniqueGlyphs: Glyph[], meshes: PathfinderMeshData): ExpandedMeshData {
const pathIDs = [];
2017-09-07 19:13:55 -04:00
for (const textRun of this.runs) {
for (const textGlyph of textRun.glyphs) {
2017-09-07 19:13:55 -04:00
const uniqueGlyphIndex = _.sortedIndexBy(uniqueGlyphs, textGlyph, 'index');
if (uniqueGlyphIndex >= 0)
pathIDs.push(uniqueGlyphIndex + 1);
}
}
return {
meshes: meshes.expand(pathIDs),
};
}
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);
const lowerLeft = glmatrix.vec2.clone([upperLeft[0], lowerRight[1]]);
const upperRight = glmatrix.vec2.clone([lowerRight[0], upperLeft[1]]);
const lineHeight = this.font.lineHeight();
lowerLeft[1] -= lineHeight;
upperRight[1] += lineHeight * 2.0;
upperRight[0] = _.defaultTo<number>(_.max(this.runs.map(run => run.measure)), 0.0);
return glmatrix.vec4.clone([lowerLeft[0], lowerLeft[1], upperRight[0], upperRight[1]]);
}
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;
private readonly font: Font;
2017-09-07 19:13:55 -04:00
}
export class GlyphStorage<Glyph extends PathfinderGlyph> {
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.
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);
}
2017-09-26 18:38:50 -04:00
partition(): Promise<PartitionResult> {
2017-09-07 19:13:55 -04:00
// 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!");
2017-09-26 18:38:50 -04:00
return {
meshes: new PathfinderMeshData(response.Ok.pathData),
time: response.Ok.time,
};
2017-09-07 19:13:55 -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));
}
layoutRuns() {
2017-09-07 19:13:55 -04:00
for (const textFrame of this.textFrames)
textFrame.runs.forEach(textRun => textRun.layout());
}
get allGlyphs(): Glyph[] {
2017-09-07 19:13:55 -04:00
return _.flatMap(this.textFrames, textRun => textRun.allGlyphs);
}
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!");
const lineHeight = font.lineHeight();
2017-09-07 19:13:55 -04:00
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.glyphStorage = new TextFrameGlyphStorage(fontData, [this.textFrame], font);
}
layoutRuns() {
2017-09-07 19:13:55 -04:00
this.textFrame.runs.forEach(textRun => textRun.layout());
}
get allGlyphs(): Glyph[] {
2017-09-07 19:13:55 -04:00
return this.textFrame.allGlyphs;
}
2017-09-07 19:13:55 -04:00
readonly textFrame: TextFrame<Glyph>;
readonly glyphStorage: TextFrameGlyphStorage<Glyph>;
}
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();
}
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);
}
private calculatePixelXMin(pixelsPerUnit: number): number {
return Math.floor(this.metrics.xMin * pixelsPerUnit);
2017-09-03 19:35:10 -04:00
}
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 {
2017-09-03 19:35:10 -04:00
const metrics = this.metrics;
const top = hint.hintPosition(glmatrix.vec2.fromValues(0, metrics.yMax))[1];
2017-09-03 19:35:10 -04:00
return {
left: this.calculatePixelXMin(pixelsPerUnit),
2017-09-03 19:35:10 -04:00
right: Math.ceil(metrics.xMax * pixelsPerUnit),
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);
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);
}
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;
}
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;
}