pathfinder/demo/client/src/text.ts

159 lines
5.2 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 {PathfinderMeshData} from "./meshes";
import {assert, panic} from "./utils";
export const BUILTIN_FONT_URI: string = "/otf/demo";
const PARTITION_FONT_ENDPOINT_URI: string = "/partition-font";
2017-09-02 01:29:05 -04:00
type CreateGlyphFn<Glyph> = (glyph: opentype.Glyph) => Glyph;
opentype.Font.prototype.isSupported = function() {
return (this as any).supported;
}
2017-09-02 01:29:05 -04:00
export class GlyphStorage<Glyph extends PathfinderGlyph> {
constructor(fontData: ArrayBuffer,
2017-09-02 01:29:05 -04:00
textGlyphs: Glyph[] | string,
createGlyph: CreateGlyphFn<Glyph>,
font?: Font) {
if (font == null) {
font = opentype.parse(fontData);
assert(font.isSupported(), "The font type is unsupported!");
}
2017-09-02 01:29:05 -04:00
if (typeof(textGlyphs) === 'string')
textGlyphs = font.stringToGlyphs(textGlyphs).map(createGlyph);
this.fontData = fontData;
this.textGlyphs = textGlyphs;
this.font = font;
// Determine all glyphs potentially needed.
this.uniqueGlyphs = this.textGlyphs.map(textGlyph => textGlyph);
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-02 01:29:05 -04:00
readonly fontData: ArrayBuffer;
readonly font: Font;
readonly textGlyphs: Glyph[];
readonly uniqueGlyphs: Glyph[];
}
export class TextLayout<Glyph extends PathfinderGlyph> {
constructor(fontData: ArrayBuffer, text: string, createGlyph: CreateGlyphFn<Glyph>) {
const font = opentype.parse(fontData);
assert(font.isSupported(), "The font type is unsupported!");
this.lineGlyphs = text.split("\n").map(line => font.stringToGlyphs(line).map(createGlyph));
const textGlyphs = _.flatten(this.lineGlyphs);
this.glyphStorage = new GlyphStorage(fontData, textGlyphs, createGlyph, font);
}
layoutText() {
2017-09-02 01:29:05 -04:00
const os2Table = this.glyphStorage.font.tables.os2;
const lineHeight = os2Table.sTypoAscender - os2Table.sTypoDescender +
os2Table.sTypoLineGap;
const currentPosition = glmatrix.vec2.create();
let glyphIndex = 0;
for (const line of this.lineGlyphs) {
for (let lineCharIndex = 0; lineCharIndex < line.length; lineCharIndex++) {
2017-09-02 01:29:05 -04:00
const textGlyph = this.glyphStorage.textGlyphs[glyphIndex];
textGlyph.position = glmatrix.vec2.clone(currentPosition);
currentPosition[0] += textGlyph.advanceWidth;
glyphIndex++;
}
currentPosition[0] = 0;
currentPosition[1] -= lineHeight;
}
}
readonly lineGlyphs: Glyph[][];
2017-09-02 01:29:05 -04:00
readonly glyphStorage: GlyphStorage<Glyph>;
}
export abstract class PathfinderGlyph {
constructor(glyph: opentype.Glyph) {
this.opentypeGlyph = glyph;
this._metrics = null;
this.position = 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;
}
setPixelPosition(pixelPosition: glmatrix.vec2, pixelsPerUnit: number): void {
glmatrix.vec2.scale(this.position, pixelPosition, 1.0 / pixelsPerUnit);
}
readonly opentypeGlyph: opentype.Glyph;
private _metrics: Metrics | null;
/// In font units.
position: glmatrix.vec2;
}