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 * as base64js from 'base64-js';
|
|
|
|
import * as glmatrix from 'gl-matrix';
|
|
|
|
import * as _ from 'lodash';
|
|
|
|
import * as opentype from "opentype.js";
|
2017-09-28 17:34:48 -04:00
|
|
|
import {Metrics} from 'opentype.js';
|
2017-08-31 19:11:09 -04:00
|
|
|
|
2017-10-02 19:31:54 -04:00
|
|
|
import {B_QUAD_SIZE, parseServerTiming, PathfinderMeshData} from "./meshes";
|
2017-09-29 21:02:33 -04:00
|
|
|
import {assert, lerp, panic, UINT32_MAX, UINT32_SIZE, unwrapNull} 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-11-11 14:29:52 -05:00
|
|
|
const SQRT_2: number = Math.sqrt(2.0);
|
|
|
|
|
|
|
|
// Should match macOS 10.13 High Sierra.
|
|
|
|
//
|
|
|
|
// We multiply by sqrt(2) to compensate for the fact that dilation amounts are relative to the
|
|
|
|
// pixel square on macOS and relative to the vertex normal in Pathfinder.
|
|
|
|
const STEM_DARKENING_FACTORS: glmatrix.vec2 = glmatrix.vec2.clone([
|
|
|
|
0.0121 * SQRT_2,
|
|
|
|
0.0121 * 1.25 * SQRT_2,
|
|
|
|
]);
|
2017-10-09 17:14:24 -04:00
|
|
|
|
|
|
|
// Likewise.
|
2017-11-11 14:29:52 -05:00
|
|
|
const MAX_STEM_DARKENING_AMOUNT: glmatrix.vec2 = glmatrix.vec2.clone([0.3 * SQRT_2, 0.3 * SQRT_2]);
|
2017-10-09 17:14:24 -04:00
|
|
|
|
|
|
|
// This value is a subjective cutoff. Above this ppem value, no stem darkening is performed.
|
2017-11-11 12:41:21 -05:00
|
|
|
const MAX_STEM_DARKENING_PIXELS_PER_EM: number = 72.0;
|
2017-10-09 17:14:24 -04:00
|
|
|
|
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-26 18:38:50 -04:00
|
|
|
export interface PartitionResult {
|
2017-09-28 17:34:48 -04:00
|
|
|
meshes: PathfinderMeshData;
|
|
|
|
time: number;
|
2017-09-26 18:38:50 -04:00
|
|
|
}
|
|
|
|
|
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-28 17:34:48 -04:00
|
|
|
};
|
2017-08-31 19:11:09 -04:00
|
|
|
|
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-27 16:02:32 -04:00
|
|
|
export class PathfinderFont {
|
2017-09-28 17:34:48 -04:00
|
|
|
readonly opentypeFont: opentype.Font;
|
|
|
|
readonly data: ArrayBuffer;
|
2017-10-02 22:58:38 -04:00
|
|
|
readonly builtinFontName: string | null;
|
2017-09-28 17:34:48 -04:00
|
|
|
|
|
|
|
private metricsCache: Metrics[];
|
|
|
|
|
2017-10-02 22:58:38 -04:00
|
|
|
constructor(data: ArrayBuffer, builtinFontName: string | null) {
|
2017-09-27 16:02:32 -04:00
|
|
|
this.data = data;
|
2017-10-02 22:58:38 -04:00
|
|
|
this.builtinFontName = builtinFontName != null ? builtinFontName : null;
|
2017-09-27 16:02:32 -04:00
|
|
|
|
|
|
|
this.opentypeFont = opentype.parse(data);
|
|
|
|
if (!this.opentypeFont.isSupported())
|
|
|
|
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];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class TextRun {
|
2017-09-28 17:34:48 -04:00
|
|
|
readonly glyphIDs: number[];
|
|
|
|
advances: number[];
|
|
|
|
readonly origin: number[];
|
|
|
|
|
|
|
|
private readonly font: PathfinderFont;
|
2017-11-30 17:02:57 -05:00
|
|
|
private pixelRects: glmatrix.vec4[];
|
2017-09-28 17:34:48 -04:00
|
|
|
|
2017-09-27 16:02:32 -04:00
|
|
|
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;
|
|
|
|
}
|
2017-08-31 19:11:09 -04:00
|
|
|
|
2017-09-07 19:13:55 -04:00
|
|
|
this.origin = origin;
|
2017-09-27 16:02:32 -04:00
|
|
|
this.advances = [];
|
|
|
|
this.font = font;
|
2017-11-30 17:02:57 -05:00
|
|
|
this.pixelRects = [];
|
2017-09-07 19:13:55 -04:00
|
|
|
}
|
2017-08-31 19:11:09 -04:00
|
|
|
|
2017-09-07 19:13:55 -04:00
|
|
|
layout() {
|
2017-09-27 16:02:32 -04:00
|
|
|
this.advances = [];
|
|
|
|
let currentX = 0;
|
|
|
|
for (const glyphID of this.glyphIDs) {
|
|
|
|
this.advances.push(currentX);
|
|
|
|
currentX += this.font.opentypeFont.glyphs.get(glyphID).advanceWidth;
|
2017-09-07 19:13:55 -04:00
|
|
|
}
|
2017-08-31 19:11:09 -04:00
|
|
|
}
|
|
|
|
|
2017-11-30 17:02:57 -05:00
|
|
|
calculatePixelOriginForGlyphAt(index: number,
|
|
|
|
pixelsPerUnit: number,
|
|
|
|
rotationAngle: number,
|
|
|
|
hint: Hint,
|
|
|
|
textFrameBounds: glmatrix.vec4):
|
2017-09-27 16:02:32 -04:00
|
|
|
glmatrix.vec2 {
|
2017-11-30 17:02:57 -05:00
|
|
|
const textFrameCenter = glmatrix.vec2.clone([
|
|
|
|
0.5 * (textFrameBounds[0] + textFrameBounds[2]),
|
|
|
|
0.5 * (textFrameBounds[1] + textFrameBounds[3]),
|
|
|
|
]);
|
|
|
|
|
|
|
|
const transform = glmatrix.mat2d.create();
|
|
|
|
glmatrix.mat2d.fromTranslation(transform, textFrameCenter);
|
|
|
|
glmatrix.mat2d.rotate(transform, transform, -rotationAngle);
|
|
|
|
glmatrix.vec2.negate(textFrameCenter, textFrameCenter);
|
|
|
|
glmatrix.mat2d.translate(transform, transform, textFrameCenter);
|
|
|
|
|
|
|
|
const textGlyphOrigin = glmatrix.vec2.create();
|
|
|
|
glmatrix.vec2.add(textGlyphOrigin, [this.advances[index], 0.0], this.origin);
|
|
|
|
glmatrix.vec2.transformMat2d(textGlyphOrigin, textGlyphOrigin, transform);
|
|
|
|
|
2017-11-30 12:51:07 -05:00
|
|
|
glmatrix.vec2.scale(textGlyphOrigin, textGlyphOrigin, pixelsPerUnit);
|
2017-09-27 16:02:32 -04:00
|
|
|
return textGlyphOrigin;
|
|
|
|
}
|
|
|
|
|
2017-11-30 17:02:57 -05:00
|
|
|
pixelRectForGlyphAt(index: number): glmatrix.vec4 {
|
|
|
|
return this.pixelRects[index];
|
2017-09-27 16:02:32 -04:00
|
|
|
}
|
|
|
|
|
2017-09-29 14:58:16 -04:00
|
|
|
subpixelForGlyphAt(index: number,
|
|
|
|
pixelsPerUnit: number,
|
2017-11-30 17:02:57 -05:00
|
|
|
rotationAngle: number,
|
2017-09-29 14:58:16 -04:00
|
|
|
hint: Hint,
|
2017-11-30 17:02:57 -05:00
|
|
|
subpixelGranularity: number,
|
|
|
|
textFrameBounds: glmatrix.vec4):
|
2017-09-29 14:58:16 -04:00
|
|
|
number {
|
2017-11-30 17:02:57 -05:00
|
|
|
const textGlyphOrigin = this.calculatePixelOriginForGlyphAt(index,
|
|
|
|
pixelsPerUnit,
|
|
|
|
rotationAngle,
|
|
|
|
hint,
|
|
|
|
textFrameBounds)[0];
|
2017-09-29 14:58:16 -04:00
|
|
|
return Math.abs(Math.round(textGlyphOrigin * subpixelGranularity) % subpixelGranularity);
|
|
|
|
}
|
|
|
|
|
2017-11-30 17:02:57 -05:00
|
|
|
recalculatePixelRects(pixelsPerUnit: number,
|
|
|
|
rotationAngle: number,
|
|
|
|
hint: Hint,
|
2017-12-03 20:28:30 -05:00
|
|
|
emboldenAmount: glmatrix.vec2,
|
2017-11-30 17:02:57 -05:00
|
|
|
subpixelGranularity: number,
|
|
|
|
textFrameBounds: glmatrix.vec4):
|
|
|
|
void {
|
|
|
|
for (let index = 0; index < this.glyphIDs.length; index++) {
|
|
|
|
const metrics = unwrapNull(this.font.metricsForGlyph(this.glyphIDs[index]));
|
2017-12-03 20:28:30 -05:00
|
|
|
const unitMetrics = new UnitMetrics(metrics, rotationAngle, emboldenAmount);
|
2017-11-30 17:02:57 -05:00
|
|
|
const textGlyphOrigin = this.calculatePixelOriginForGlyphAt(index,
|
|
|
|
pixelsPerUnit,
|
|
|
|
rotationAngle,
|
|
|
|
hint,
|
|
|
|
textFrameBounds);
|
|
|
|
|
|
|
|
textGlyphOrigin[0] *= subpixelGranularity;
|
|
|
|
glmatrix.vec2.round(textGlyphOrigin, textGlyphOrigin);
|
|
|
|
textGlyphOrigin[0] /= subpixelGranularity;
|
|
|
|
|
|
|
|
const pixelRect = calculatePixelRectForGlyph(unitMetrics,
|
|
|
|
textGlyphOrigin,
|
|
|
|
pixelsPerUnit,
|
|
|
|
hint);
|
|
|
|
|
|
|
|
this.pixelRects[index] = pixelRect;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-11 19:07:11 -04:00
|
|
|
get measure(): number {
|
2017-09-27 16:02:32 -04:00
|
|
|
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;
|
2017-09-11 19:07:11 -04:00
|
|
|
}
|
2017-09-07 19:13:55 -04:00
|
|
|
}
|
2017-08-31 19:11:09 -04:00
|
|
|
|
2017-09-27 16:02:32 -04:00
|
|
|
export class TextFrame {
|
2017-09-28 17:34:48 -04:00
|
|
|
readonly runs: TextRun[];
|
|
|
|
readonly origin: glmatrix.vec3;
|
|
|
|
|
|
|
|
private readonly font: PathfinderFont;
|
|
|
|
|
2017-09-27 16:02:32 -04:00
|
|
|
constructor(runs: TextRun[], font: PathfinderFont) {
|
2017-09-07 19:13:55 -04:00
|
|
|
this.runs = runs;
|
2017-09-27 16:02:32 -04:00
|
|
|
this.origin = glmatrix.vec3.create();
|
2017-09-11 19:07:11 -04:00
|
|
|
this.font = font;
|
2017-08-31 19:11:09 -04:00
|
|
|
}
|
|
|
|
|
2017-09-27 16:02:32 -04:00
|
|
|
expandMeshes(meshes: PathfinderMeshData, glyphIDs: number[]): ExpandedMeshData {
|
2017-09-26 21:55:47 -04:00
|
|
|
const pathIDs = [];
|
2017-09-07 19:13:55 -04:00
|
|
|
for (const textRun of this.runs) {
|
2017-09-28 17:34:48 -04:00
|
|
|
for (const glyphID of textRun.glyphIDs) {
|
2017-09-27 16:02:32 -04:00
|
|
|
if (glyphID === 0)
|
|
|
|
continue;
|
|
|
|
const pathID = _.sortedIndexOf(glyphIDs, glyphID);
|
|
|
|
pathIDs.push(pathID + 1);
|
2017-09-06 17:11:58 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
2017-09-26 21:55:47 -04:00
|
|
|
meshes: meshes.expand(pathIDs),
|
|
|
|
};
|
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-27 16:02:32 -04:00
|
|
|
const lineHeight = this.font.opentypeFont.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
|
|
|
|
2017-09-27 16:02:32 -04:00
|
|
|
get totalGlyphCount(): number {
|
|
|
|
return _.sumBy(this.runs, run => run.glyphIDs.length);
|
|
|
|
}
|
|
|
|
|
|
|
|
get allGlyphIDs(): number[] {
|
|
|
|
const glyphIDs = [];
|
|
|
|
for (const run of this.runs)
|
|
|
|
glyphIDs.push(...run.glyphIDs);
|
|
|
|
return glyphIDs;
|
2017-09-07 19:13:55 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-27 16:02:32 -04:00
|
|
|
/// Stores one copy of each glyph.
|
|
|
|
export class GlyphStore {
|
2017-09-28 17:34:48 -04:00
|
|
|
readonly font: PathfinderFont;
|
|
|
|
readonly glyphIDs: number[];
|
|
|
|
|
2017-09-27 16:02:32 -04:00
|
|
|
constructor(font: PathfinderFont, glyphIDs: number[]) {
|
2017-09-07 19:13:55 -04:00
|
|
|
this.font = font;
|
2017-09-27 16:02:32 -04:00
|
|
|
this.glyphIDs = glyphIDs;
|
2017-09-07 19:13:55 -04:00
|
|
|
}
|
|
|
|
|
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.
|
2017-10-02 22:58:38 -04:00
|
|
|
let fontFace;
|
|
|
|
if (this.font.builtinFontName != null)
|
|
|
|
fontFace = { Builtin: this.font.builtinFontName };
|
|
|
|
else
|
|
|
|
fontFace = { Custom: base64js.fromByteArray(new Uint8Array(this.font.data)) };
|
|
|
|
|
2017-09-07 19:13:55 -04:00
|
|
|
const request = {
|
2017-10-02 22:58:38 -04:00
|
|
|
face: fontFace,
|
2017-09-07 19:13:55 -04:00
|
|
|
fontIndex: 0,
|
2017-09-27 16:02:32 -04:00
|
|
|
glyphs: this.glyphIDs.map(id => ({ id: id, transform: [1, 0, 0, 1, 0, 0] })),
|
|
|
|
pointSize: this.font.opentypeFont.unitsPerEm,
|
2017-09-07 19:13:55 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
// Make the request.
|
2017-10-02 19:31:54 -04:00
|
|
|
let time = 0;
|
2017-09-07 19:13:55 -04:00
|
|
|
return window.fetch(PARTITION_FONT_ENDPOINT_URI, {
|
|
|
|
body: JSON.stringify(request),
|
2017-11-13 20:23:03 -05:00
|
|
|
headers: {'Content-Type': 'application/json'} as any,
|
2017-09-28 17:34:48 -04:00
|
|
|
method: 'POST',
|
2017-10-02 19:31:54 -04:00
|
|
|
}).then(response => {
|
|
|
|
time = parseServerTiming(response.headers);
|
|
|
|
return response.arrayBuffer();
|
|
|
|
}).then(buffer => {
|
2017-09-26 18:38:50 -04:00
|
|
|
return {
|
2017-10-02 19:31:54 -04:00
|
|
|
meshes: new PathfinderMeshData(buffer),
|
|
|
|
time: time,
|
2017-09-26 18:38:50 -04:00
|
|
|
};
|
2017-09-07 19:13:55 -04:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-09-27 16:02:32 -04:00
|
|
|
indexOfGlyphWithID(glyphID: number): number | null {
|
|
|
|
const index = _.sortedIndexOf(this.glyphIDs, glyphID);
|
|
|
|
return index >= 0 ? index : null;
|
2017-09-19 20:35:11 -04:00
|
|
|
}
|
2017-09-02 01:29:05 -04:00
|
|
|
}
|
|
|
|
|
2017-09-27 16:02:32 -04:00
|
|
|
export class SimpleTextLayout {
|
2017-09-28 17:34:48 -04:00
|
|
|
readonly textFrame: TextFrame;
|
|
|
|
|
2017-09-27 16:02:32 -04:00
|
|
|
constructor(font: PathfinderFont, text: string) {
|
|
|
|
const lineHeight = font.opentypeFont.lineHeight();
|
|
|
|
const textRuns: TextRun[] = text.split("\n").map((line, lineNumber) => {
|
|
|
|
return new TextRun(line, [0.0, -lineHeight * lineNumber], font);
|
2017-09-07 01:11:32 -04:00
|
|
|
});
|
2017-09-11 19:07:11 -04:00
|
|
|
this.textFrame = new TextFrame(textRuns, 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-06 17:11:58 -04:00
|
|
|
|
2017-09-09 16:12:51 -04:00
|
|
|
export class Hint {
|
2017-09-28 17:34:48 -04:00
|
|
|
readonly xHeight: number;
|
|
|
|
readonly hintedXHeight: number;
|
2017-09-29 21:02:33 -04:00
|
|
|
readonly stemHeight: number;
|
|
|
|
readonly hintedStemHeight: number;
|
2017-09-28 17:34:48 -04:00
|
|
|
|
|
|
|
private useHinting: boolean;
|
|
|
|
|
2017-09-27 16:02:32 -04:00
|
|
|
constructor(font: PathfinderFont, pixelsPerUnit: number, useHinting: boolean) {
|
2017-09-09 16:12:51 -04:00
|
|
|
this.useHinting = useHinting;
|
|
|
|
|
2017-09-27 16:02:32 -04:00
|
|
|
const os2Table = font.opentypeFont.tables.os2;
|
2017-09-09 16:12:51 -04:00
|
|
|
this.xHeight = os2Table.sxHeight != null ? os2Table.sxHeight : 0;
|
2017-09-29 21:02:33 -04:00
|
|
|
this.stemHeight = os2Table.sCapHeight != null ? os2Table.sCapHeight : 0;
|
2017-09-09 16:12:51 -04:00
|
|
|
|
|
|
|
if (!useHinting) {
|
|
|
|
this.hintedXHeight = this.xHeight;
|
2017-09-29 21:02:33 -04:00
|
|
|
this.hintedStemHeight = this.stemHeight;
|
2017-09-09 16:12:51 -04:00
|
|
|
} else {
|
2017-11-07 20:39:29 -05:00
|
|
|
this.hintedXHeight = Math.round(Math.round(this.xHeight * pixelsPerUnit) /
|
|
|
|
pixelsPerUnit);
|
|
|
|
this.hintedStemHeight = Math.round(Math.round(this.stemHeight * pixelsPerUnit) /
|
|
|
|
pixelsPerUnit);
|
2017-09-09 16:12:51 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-29 21:02:33 -04:00
|
|
|
/// NB: This must match `hintPosition()` in `common.inc.glsl`.
|
2017-09-09 16:12:51 -04:00
|
|
|
hintPosition(position: glmatrix.vec2): glmatrix.vec2 {
|
|
|
|
if (!this.useHinting)
|
|
|
|
return position;
|
|
|
|
|
2017-09-29 21:02:33 -04:00
|
|
|
if (position[1] >= this.stemHeight) {
|
|
|
|
const y = position[1] - this.stemHeight + this.hintedStemHeight;
|
|
|
|
return glmatrix.vec2.clone([position[0], y]);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (position[1] >= this.xHeight) {
|
|
|
|
const y = lerp(this.hintedXHeight, this.hintedStemHeight,
|
|
|
|
(position[1] - this.xHeight) / (this.stemHeight - this.xHeight));
|
|
|
|
return glmatrix.vec2.clone([position[0], y]);
|
|
|
|
}
|
2017-09-09 16:12:51 -04:00
|
|
|
|
2017-09-29 21:02:33 -04:00
|
|
|
if (position[1] >= 0.0) {
|
|
|
|
const y = lerp(0.0, this.hintedXHeight, position[1] / this.xHeight);
|
|
|
|
return glmatrix.vec2.clone([position[0], y]);
|
2017-09-09 16:12:51 -04:00
|
|
|
}
|
|
|
|
|
2017-09-29 21:02:33 -04:00
|
|
|
return position;
|
2017-09-09 16:12:51 -04:00
|
|
|
}
|
|
|
|
}
|
2017-09-27 16:02:32 -04:00
|
|
|
|
2017-10-09 17:14:24 -04:00
|
|
|
export class UnitMetrics {
|
|
|
|
left: number;
|
|
|
|
right: number;
|
|
|
|
ascent: number;
|
|
|
|
descent: number;
|
|
|
|
|
2017-12-03 20:28:30 -05:00
|
|
|
constructor(metrics: Metrics, rotationAngle: number, emboldenAmount: glmatrix.vec2) {
|
2017-11-30 17:02:57 -05:00
|
|
|
const left = metrics.xMin;
|
|
|
|
const bottom = metrics.yMin;
|
2017-12-03 20:28:30 -05:00
|
|
|
const right = metrics.xMax + emboldenAmount[0] * 2;
|
|
|
|
const top = metrics.yMax + emboldenAmount[1] * 2;
|
2017-11-30 17:02:57 -05:00
|
|
|
|
|
|
|
const transform = glmatrix.mat2.create();
|
|
|
|
glmatrix.mat2.fromRotation(transform, -rotationAngle);
|
|
|
|
|
|
|
|
const lowerLeft = glmatrix.vec2.clone([Infinity, Infinity]);
|
|
|
|
const upperRight = glmatrix.vec2.clone([-Infinity, -Infinity]);
|
|
|
|
const points = [[left, bottom], [left, top], [right, top], [right, bottom]];
|
|
|
|
const transformedPoint = glmatrix.vec2.create();
|
|
|
|
for (const point of points) {
|
|
|
|
glmatrix.vec2.transformMat2(transformedPoint, point, transform);
|
|
|
|
glmatrix.vec2.min(lowerLeft, lowerLeft, transformedPoint);
|
|
|
|
glmatrix.vec2.max(upperRight, upperRight, transformedPoint);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.left = lowerLeft[0];
|
|
|
|
this.right = upperRight[0];
|
|
|
|
this.ascent = upperRight[1];
|
|
|
|
this.descent = lowerLeft[1];
|
2017-10-09 17:14:24 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function calculatePixelXMin(metrics: UnitMetrics, pixelsPerUnit: number): number {
|
|
|
|
return Math.floor(metrics.left * pixelsPerUnit);
|
2017-09-27 16:02:32 -04:00
|
|
|
}
|
|
|
|
|
2017-10-09 17:14:24 -04:00
|
|
|
export function calculatePixelDescent(metrics: UnitMetrics, pixelsPerUnit: number): number {
|
2017-11-30 17:02:57 -05:00
|
|
|
return Math.floor(metrics.descent * pixelsPerUnit);
|
2017-09-27 16:02:32 -04:00
|
|
|
}
|
|
|
|
|
2017-10-09 17:14:24 -04:00
|
|
|
function calculateSubpixelMetricsForGlyph(metrics: UnitMetrics, pixelsPerUnit: number, hint: Hint):
|
2017-09-29 14:58:16 -04:00
|
|
|
PixelMetrics {
|
2017-10-09 17:14:24 -04:00
|
|
|
const ascent = hint.hintPosition(glmatrix.vec2.fromValues(0, metrics.ascent))[1];
|
2017-09-27 16:02:32 -04:00
|
|
|
return {
|
2017-10-09 17:14:24 -04:00
|
|
|
ascent: ascent * pixelsPerUnit,
|
|
|
|
descent: metrics.descent * pixelsPerUnit,
|
|
|
|
left: metrics.left * pixelsPerUnit,
|
|
|
|
right: metrics.right * pixelsPerUnit,
|
2017-09-27 16:02:32 -04:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2017-10-09 17:14:24 -04:00
|
|
|
export function calculatePixelRectForGlyph(metrics: UnitMetrics,
|
2017-09-29 14:58:16 -04:00
|
|
|
subpixelOrigin: glmatrix.vec2,
|
2017-09-27 16:02:32 -04:00
|
|
|
pixelsPerUnit: number,
|
|
|
|
hint: Hint):
|
|
|
|
glmatrix.vec4 {
|
2017-09-29 14:58:16 -04:00
|
|
|
const pixelMetrics = calculateSubpixelMetricsForGlyph(metrics, pixelsPerUnit, hint);
|
|
|
|
return glmatrix.vec4.clone([Math.floor(subpixelOrigin[0] + pixelMetrics.left),
|
|
|
|
Math.floor(subpixelOrigin[1] + pixelMetrics.descent),
|
|
|
|
Math.ceil(subpixelOrigin[0] + pixelMetrics.right),
|
|
|
|
Math.ceil(subpixelOrigin[1] + pixelMetrics.ascent)]);
|
|
|
|
}
|
2017-10-09 17:14:24 -04:00
|
|
|
|
|
|
|
export function computeStemDarkeningAmount(pixelsPerEm: number, pixelsPerUnit: number):
|
|
|
|
glmatrix.vec2 {
|
|
|
|
const amount = glmatrix.vec2.create();
|
|
|
|
if (pixelsPerEm > MAX_STEM_DARKENING_PIXELS_PER_EM)
|
|
|
|
return amount;
|
|
|
|
|
|
|
|
glmatrix.vec2.scale(amount, STEM_DARKENING_FACTORS, pixelsPerEm);
|
2017-11-11 12:38:28 -05:00
|
|
|
glmatrix.vec2.min(amount, amount, MAX_STEM_DARKENING_AMOUNT);
|
2017-10-09 17:14:24 -04:00
|
|
|
glmatrix.vec2.scale(amount, amount, 1.0 / pixelsPerUnit);
|
|
|
|
return amount;
|
|
|
|
}
|