Factor out text layout into a separate class so it can be used by the text and 3D demos
This commit is contained in:
parent
e448ba7b30
commit
b75c327017
|
@ -7,3 +7,61 @@
|
||||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||||
// 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 {AntialiasingStrategy, AntialiasingStrategyName} from "./aa-strategy";
|
||||||
|
import {mat4, vec2} from "gl-matrix";
|
||||||
|
import {ShaderMap, ShaderProgramSource} from "./shader-loader";
|
||||||
|
import {PathfinderView, Timings} from "./view";
|
||||||
|
import AppController from "./app-controller";
|
||||||
|
|
||||||
|
class ThreeDController extends AppController<ThreeDView> {
|
||||||
|
protected fileLoaded(): void {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createView(canvas: HTMLCanvasElement,
|
||||||
|
commonShaderSource: string,
|
||||||
|
shaderSources: ShaderMap<ShaderProgramSource>):
|
||||||
|
ThreeDView {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected builtinFileURI: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ThreeDView extends PathfinderView {
|
||||||
|
protected resized(initialSize: boolean): void {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createAAStrategy(aaType: AntialiasingStrategyName, aaLevel: number):
|
||||||
|
AntialiasingStrategy {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected compositeIfNecessary(): void {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updateTimings(timings: Timings): void {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected panned(): void {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
destFramebuffer: WebGLFramebuffer | null;
|
||||||
|
destAllocatedSize: vec2;
|
||||||
|
destUsedSize: vec2;
|
||||||
|
protected usedSizeFactor: vec2;
|
||||||
|
protected scale: number;
|
||||||
|
protected worldTransform: mat4;
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const controller = new ThreeDController;
|
||||||
|
window.addEventListener('load', () => controller.start(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
|
|
||||||
import {AntialiasingStrategyName} from "./aa-strategy";
|
import {AntialiasingStrategyName} from "./aa-strategy";
|
||||||
import {ShaderLoader, ShaderMap, ShaderProgramSource} from './shader-loader';
|
import {ShaderLoader, ShaderMap, ShaderProgramSource} from './shader-loader';
|
||||||
import { expectNotNull, unwrapUndef, unwrapNull } from './utils';
|
import {expectNotNull, unwrapUndef, unwrapNull} from './utils';
|
||||||
import {PathfinderView} from "./view";
|
import {PathfinderView} from "./view";
|
||||||
|
|
||||||
export default abstract class AppController<View extends PathfinderView> {
|
export default abstract class AppController<View extends PathfinderView> {
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
// 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';
|
||||||
|
@ -20,6 +21,7 @@ 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 {PathfinderGlyph, TextLayout} from "./text";
|
||||||
import {PathfinderError, assert, expectNotNull, UINT32_SIZE, unwrapNull, panic} from './utils';
|
import {PathfinderError, assert, expectNotNull, UINT32_SIZE, unwrapNull, panic} from './utils';
|
||||||
import {MonochromePathfinderView, Timings} from './view';
|
import {MonochromePathfinderView, Timings} from './view';
|
||||||
import AppController from './app-controller';
|
import AppController from './app-controller';
|
||||||
|
@ -98,10 +100,6 @@ declare module 'opentype.js' {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
opentype.Font.prototype.isSupported = function() {
|
|
||||||
return (this as any).supported;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The separating axis theorem.
|
/// The separating axis theorem.
|
||||||
function rectsIntersect(a: glmatrix.vec4, b: glmatrix.vec4): boolean {
|
function rectsIntersect(a: glmatrix.vec4, b: glmatrix.vec4): boolean {
|
||||||
return a[2] > b[0] && a[3] > b[1] && a[0] < b[2] && a[1] < b[3];
|
return a[2] > b[0] && a[3] > b[1] && a[0] < b[2] && a[1] < b[3];
|
||||||
|
@ -116,7 +114,7 @@ class TextDemoController extends AppController<TextDemoView> {
|
||||||
start() {
|
start() {
|
||||||
super.start();
|
super.start();
|
||||||
|
|
||||||
this.fontSize = INITIAL_FONT_SIZE;
|
this._fontSize = INITIAL_FONT_SIZE;
|
||||||
|
|
||||||
this.fpsLabel = unwrapNull(document.getElementById('pf-fps-label'));
|
this.fpsLabel = unwrapNull(document.getElementById('pf-fps-label'));
|
||||||
|
|
||||||
|
@ -130,50 +128,9 @@ class TextDemoController extends AppController<TextDemoView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fileLoaded() {
|
protected fileLoaded() {
|
||||||
this.font = opentype.parse(this.fileData);
|
this.layout = new TextLayout(this.fileData, TEXT, glyph => new GlyphInstance(glyph));
|
||||||
if (!this.font.isSupported())
|
this.layout.partition().then((meshes: PathfinderMeshData) => {
|
||||||
throw new PathfinderError("The font type is unsupported.");
|
this.meshes = meshes;
|
||||||
|
|
||||||
// Lay out the text.
|
|
||||||
this.lineGlyphs = TEXT.split("\n").map(line => {
|
|
||||||
return this.font.stringToGlyphs(line).map(glyph => new TextGlyph(glyph));
|
|
||||||
});
|
|
||||||
this.textGlyphs = _.flatten(this.lineGlyphs);
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
// 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.fileData)),
|
|
||||||
},
|
|
||||||
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.
|
|
||||||
window.fetch(PARTITION_FONT_ENDPOINT_URL, {
|
|
||||||
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!");
|
|
||||||
const meshes = response.Ok.pathData;
|
|
||||||
this.meshes = new PathfinderMeshData(meshes);
|
|
||||||
this.meshesReceived();
|
this.meshesReceived();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -181,7 +138,7 @@ class TextDemoController extends AppController<TextDemoView> {
|
||||||
private meshesReceived() {
|
private meshesReceived() {
|
||||||
this.view.then(view => {
|
this.view.then(view => {
|
||||||
view.attachText();
|
view.attachText();
|
||||||
view.uploadPathData(this.uniqueGlyphs.length);
|
view.uploadPathData(this.layout.uniqueGlyphs.length);
|
||||||
view.attachMeshes(this.meshes);
|
view.attachMeshes(this.meshes);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -203,26 +160,28 @@ class TextDemoController extends AppController<TextDemoView> {
|
||||||
/// The font size in pixels per em.
|
/// The font size in pixels per em.
|
||||||
set fontSize(newFontSize: number) {
|
set fontSize(newFontSize: number) {
|
||||||
this._fontSize = newFontSize;
|
this._fontSize = newFontSize;
|
||||||
|
this.layout
|
||||||
this.view.then(view => view.attachText());
|
this.view.then(view => view.attachText());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get pixelsPerUnit(): number {
|
||||||
|
return this._fontSize / this.layout.font.unitsPerEm;
|
||||||
|
}
|
||||||
|
|
||||||
protected get builtinFileURI(): string {
|
protected get builtinFileURI(): string {
|
||||||
return BUILTIN_FONT_URI;
|
return BUILTIN_FONT_URI;
|
||||||
}
|
}
|
||||||
|
|
||||||
private fpsLabel: HTMLElement;
|
private fpsLabel: HTMLElement;
|
||||||
|
|
||||||
font: opentype.Font;
|
|
||||||
lineGlyphs: TextGlyph[][];
|
|
||||||
textGlyphs: TextGlyph[];
|
|
||||||
uniqueGlyphs: PathfinderGlyph[];
|
|
||||||
|
|
||||||
private _atlas: Atlas;
|
private _atlas: Atlas;
|
||||||
atlasGlyphs: AtlasGlyph[];
|
atlasGlyphs: AtlasGlyph[];
|
||||||
|
|
||||||
private meshes: PathfinderMeshData;
|
private meshes: PathfinderMeshData;
|
||||||
|
|
||||||
private _fontSize: number;
|
private _fontSize: number;
|
||||||
|
|
||||||
|
layout: TextLayout<GlyphInstance>;
|
||||||
}
|
}
|
||||||
|
|
||||||
class TextDemoView extends MonochromePathfinderView {
|
class TextDemoView extends MonochromePathfinderView {
|
||||||
|
@ -252,64 +211,23 @@ class TextDemoView extends MonochromePathfinderView {
|
||||||
|
|
||||||
/// Lays out glyphs on the canvas.
|
/// Lays out glyphs on the canvas.
|
||||||
private layoutGlyphs() {
|
private layoutGlyphs() {
|
||||||
const lineGlyphs = this.appController.lineGlyphs;
|
this.appController.layout.layoutText();
|
||||||
const textGlyphs = this.appController.textGlyphs;
|
|
||||||
|
|
||||||
const font = this.appController.font;
|
|
||||||
this.pixelsPerUnit = this.appController.fontSize / font.unitsPerEm;
|
|
||||||
|
|
||||||
|
const textGlyphs = this.appController.layout.textGlyphs;
|
||||||
const glyphPositions = new Float32Array(textGlyphs.length * 8);
|
const glyphPositions = new Float32Array(textGlyphs.length * 8);
|
||||||
const glyphIndices = new Uint32Array(textGlyphs.length * 6);
|
const glyphIndices = new Uint32Array(textGlyphs.length * 6);
|
||||||
|
|
||||||
const os2Table = font.tables.os2;
|
for (let glyphIndex = 0; glyphIndex < textGlyphs.length; glyphIndex++) {
|
||||||
const lineHeight = (os2Table.sTypoAscender - os2Table.sTypoDescender +
|
const textGlyph = textGlyphs[glyphIndex];
|
||||||
os2Table.sTypoLineGap) * this.pixelsPerUnit;
|
const rect = textGlyph.getRect(this.appController.pixelsPerUnit);
|
||||||
|
glyphPositions.set([
|
||||||
const currentPosition = glmatrix.vec2.create();
|
rect[0], rect[3],
|
||||||
|
rect[2], rect[3],
|
||||||
let glyphIndex = 0;
|
rect[0], rect[1],
|
||||||
for (const line of lineGlyphs) {
|
rect[2], rect[1],
|
||||||
for (let lineCharIndex = 0; lineCharIndex < line.length; lineCharIndex++) {
|
], glyphIndex * 8);
|
||||||
const textGlyph = textGlyphs[glyphIndex];
|
glyphIndices.set(Array.from(QUAD_ELEMENTS).map(index => index + 4 * glyphIndex),
|
||||||
const glyphMetrics = textGlyph.metrics;
|
glyphIndex * 6);
|
||||||
|
|
||||||
// Determine the atlas size.
|
|
||||||
const atlasSize = glmatrix.vec2.fromValues(glyphMetrics.xMax - glyphMetrics.xMin,
|
|
||||||
glyphMetrics.yMax - glyphMetrics.yMin);
|
|
||||||
glmatrix.vec2.scale(atlasSize, atlasSize, this.pixelsPerUnit);
|
|
||||||
glmatrix.vec2.ceil(atlasSize, atlasSize);
|
|
||||||
|
|
||||||
// Set positions.
|
|
||||||
const textGlyphBL = glmatrix.vec2.create(), textGlyphTR = glmatrix.vec2.create();
|
|
||||||
const offset = glmatrix.vec2.fromValues(glyphMetrics.leftSideBearing,
|
|
||||||
glyphMetrics.yMin);
|
|
||||||
glmatrix.vec2.scale(offset, offset, this.pixelsPerUnit);
|
|
||||||
glmatrix.vec2.add(textGlyphBL, currentPosition, offset);
|
|
||||||
glmatrix.vec2.round(textGlyphBL, textGlyphBL);
|
|
||||||
glmatrix.vec2.add(textGlyphTR, textGlyphBL, atlasSize);
|
|
||||||
|
|
||||||
glyphPositions.set([
|
|
||||||
textGlyphBL[0], textGlyphTR[1],
|
|
||||||
textGlyphTR[0], textGlyphTR[1],
|
|
||||||
textGlyphBL[0], textGlyphBL[1],
|
|
||||||
textGlyphTR[0], textGlyphBL[1],
|
|
||||||
], glyphIndex * 8);
|
|
||||||
|
|
||||||
textGlyph.canvasRect = glmatrix.vec4.fromValues(textGlyphBL[0], textGlyphBL[1],
|
|
||||||
textGlyphTR[0], textGlyphTR[1]);
|
|
||||||
|
|
||||||
// Set indices.
|
|
||||||
glyphIndices.set(Array.from(QUAD_ELEMENTS).map(index => index + 4 * glyphIndex),
|
|
||||||
glyphIndex * 6);
|
|
||||||
|
|
||||||
// Advance.
|
|
||||||
currentPosition[0] += textGlyph.advanceWidth * this.pixelsPerUnit;
|
|
||||||
|
|
||||||
glyphIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentPosition[0] = 0;
|
|
||||||
currentPosition[1] -= lineHeight;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.glyphPositionsBuffer = unwrapNull(this.gl.createBuffer());
|
this.glyphPositionsBuffer = unwrapNull(this.gl.createBuffer());
|
||||||
|
@ -321,7 +239,8 @@ class TextDemoView extends MonochromePathfinderView {
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildAtlasGlyphs() {
|
private buildAtlasGlyphs() {
|
||||||
const textGlyphs = this.appController.textGlyphs;
|
const textGlyphs = this.appController.layout.textGlyphs;
|
||||||
|
const pixelsPerUnit = this.appController.pixelsPerUnit;
|
||||||
|
|
||||||
// Only build glyphs in view.
|
// Only build glyphs in view.
|
||||||
const canvasRect = glmatrix.vec4.fromValues(-this.translation[0],
|
const canvasRect = glmatrix.vec4.fromValues(-this.translation[0],
|
||||||
|
@ -330,22 +249,21 @@ class TextDemoView extends MonochromePathfinderView {
|
||||||
-this.translation[1] + this.canvas.height);
|
-this.translation[1] + this.canvas.height);
|
||||||
|
|
||||||
let atlasGlyphs =
|
let atlasGlyphs =
|
||||||
textGlyphs.filter(textGlyph => rectsIntersect(textGlyph.canvasRect, canvasRect))
|
textGlyphs.filter(glyph => rectsIntersect(glyph.getRect(pixelsPerUnit), canvasRect))
|
||||||
.map(textGlyph => new AtlasGlyph(textGlyph));
|
.map(textGlyph => new AtlasGlyph(textGlyph.opentypeGlyph));
|
||||||
atlasGlyphs.sort((a, b) => a.index - b.index);
|
atlasGlyphs.sort((a, b) => a.index - b.index);
|
||||||
atlasGlyphs = _.sortedUniqBy(atlasGlyphs, glyph => glyph.index);
|
atlasGlyphs = _.sortedUniqBy(atlasGlyphs, glyph => glyph.index);
|
||||||
this.appController.atlasGlyphs = atlasGlyphs;
|
this.appController.atlasGlyphs = atlasGlyphs;
|
||||||
|
|
||||||
const fontSize = this.appController.fontSize;
|
this.appController.atlas.layoutGlyphs(atlasGlyphs, pixelsPerUnit);
|
||||||
const unitsPerEm = this.appController.font.unitsPerEm;
|
|
||||||
|
|
||||||
this.appController.atlas.layoutGlyphs(atlasGlyphs, fontSize, unitsPerEm);
|
const uniqueGlyphs = this.appController.layout.uniqueGlyphs;
|
||||||
|
const uniqueGlyphIndices = uniqueGlyphs.map(glyph => glyph.index);
|
||||||
const uniqueGlyphIndices = this.appController.uniqueGlyphs.map(glyph => glyph.index);
|
|
||||||
uniqueGlyphIndices.sort((a, b) => a - b);
|
uniqueGlyphIndices.sort((a, b) => a - b);
|
||||||
|
|
||||||
// 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 transforms = new Float32Array((this.appController.uniqueGlyphs.length + 1) * 4);
|
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];
|
||||||
|
|
||||||
|
@ -353,13 +271,13 @@ class TextDemoView extends MonochromePathfinderView {
|
||||||
assert(pathID >= 0, "No path ID!");
|
assert(pathID >= 0, "No path ID!");
|
||||||
pathID++;
|
pathID++;
|
||||||
|
|
||||||
const atlasLocation = glyph.atlasRect;
|
const atlasLocation = glyph.getRect(pixelsPerUnit);
|
||||||
const metrics = glyph.metrics;
|
const metrics = glyph.metrics;
|
||||||
const left = metrics.xMin * this.pixelsPerUnit;
|
const left = metrics.xMin * pixelsPerUnit;
|
||||||
const bottom = metrics.yMin * this.pixelsPerUnit;
|
const bottom = metrics.yMin * pixelsPerUnit;
|
||||||
|
|
||||||
transforms[pathID * 4 + 0] = this.pixelsPerUnit;
|
transforms[pathID * 4 + 0] = pixelsPerUnit;
|
||||||
transforms[pathID * 4 + 1] = this.pixelsPerUnit;
|
transforms[pathID * 4 + 1] = pixelsPerUnit;
|
||||||
transforms[pathID * 4 + 2] = atlasLocation[0] - left;
|
transforms[pathID * 4 + 2] = atlasLocation[0] - left;
|
||||||
transforms[pathID * 4 + 3] = atlasLocation[1] - bottom;
|
transforms[pathID * 4 + 3] = atlasLocation[1] - bottom;
|
||||||
}
|
}
|
||||||
|
@ -380,7 +298,7 @@ class TextDemoView extends MonochromePathfinderView {
|
||||||
}
|
}
|
||||||
|
|
||||||
private setGlyphTexCoords() {
|
private setGlyphTexCoords() {
|
||||||
const textGlyphs = this.appController.textGlyphs;
|
const textGlyphs = this.appController.layout.textGlyphs;
|
||||||
const atlasGlyphs = this.appController.atlasGlyphs;
|
const atlasGlyphs = this.appController.atlasGlyphs;
|
||||||
|
|
||||||
const atlasGlyphIndices = atlasGlyphs.map(atlasGlyph => atlasGlyph.index);
|
const atlasGlyphIndices = atlasGlyphs.map(atlasGlyph => atlasGlyph.index);
|
||||||
|
@ -399,7 +317,7 @@ class TextDemoView extends MonochromePathfinderView {
|
||||||
|
|
||||||
// Set texture coordinates.
|
// Set texture coordinates.
|
||||||
const atlasGlyph = atlasGlyphs[atlasGlyphIndex];
|
const atlasGlyph = atlasGlyphs[atlasGlyphIndex];
|
||||||
const atlasGlyphRect = atlasGlyph.atlasRect;
|
const atlasGlyphRect = atlasGlyph.getRect(this.appController.pixelsPerUnit);
|
||||||
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);
|
||||||
|
@ -493,7 +411,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.textGlyphs.length * 6,
|
this.appController.layout.textGlyphs.length * 6,
|
||||||
this.gl.UNSIGNED_INT,
|
this.gl.UNSIGNED_INT,
|
||||||
0);
|
0);
|
||||||
}
|
}
|
||||||
|
@ -542,8 +460,6 @@ class TextDemoView extends MonochromePathfinderView {
|
||||||
atlasFramebuffer: WebGLFramebuffer;
|
atlasFramebuffer: WebGLFramebuffer;
|
||||||
atlasDepthTexture: WebGLTexture;
|
atlasDepthTexture: WebGLTexture;
|
||||||
|
|
||||||
private pixelsPerUnit: number;
|
|
||||||
|
|
||||||
glyphPositionsBuffer: WebGLBuffer;
|
glyphPositionsBuffer: WebGLBuffer;
|
||||||
glyphTexCoordsBuffer: WebGLBuffer;
|
glyphTexCoordsBuffer: WebGLBuffer;
|
||||||
glyphElementsBuffer: WebGLBuffer;
|
glyphElementsBuffer: WebGLBuffer;
|
||||||
|
@ -557,106 +473,30 @@ interface AntialiasingStrategyTable {
|
||||||
ecaa: typeof ECAAStrategy;
|
ecaa: typeof ECAAStrategy;
|
||||||
}
|
}
|
||||||
|
|
||||||
class PathfinderGlyph {
|
|
||||||
constructor(glyph: opentype.Glyph | PathfinderGlyph) {
|
|
||||||
this.glyph = glyph instanceof PathfinderGlyph ? glyph.glyph : glyph;
|
|
||||||
this._metrics = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
get index(): number {
|
|
||||||
return (this.glyph as any).index;
|
|
||||||
}
|
|
||||||
|
|
||||||
get metrics(): opentype.Metrics {
|
|
||||||
if (this._metrics == null)
|
|
||||||
this._metrics = this.glyph.getMetrics();
|
|
||||||
return this._metrics;
|
|
||||||
}
|
|
||||||
|
|
||||||
get advanceWidth(): number {
|
|
||||||
return this.glyph.advanceWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
private glyph: opentype.Glyph;
|
|
||||||
private _metrics: opentype.Metrics | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
class TextGlyph extends PathfinderGlyph {
|
|
||||||
constructor(glyph: opentype.Glyph | PathfinderGlyph) {
|
|
||||||
super(glyph);
|
|
||||||
this._canvasRect = glmatrix.vec4.create();
|
|
||||||
}
|
|
||||||
|
|
||||||
get canvasRect() {
|
|
||||||
return this._canvasRect;
|
|
||||||
}
|
|
||||||
|
|
||||||
set canvasRect(rect: Rect) {
|
|
||||||
this._canvasRect = rect;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _canvasRect: Rect;
|
|
||||||
}
|
|
||||||
|
|
||||||
class AtlasGlyph extends PathfinderGlyph {
|
|
||||||
constructor(glyph: opentype.Glyph | PathfinderGlyph) {
|
|
||||||
super(glyph);
|
|
||||||
this._atlasRect = glmatrix.vec4.create();
|
|
||||||
}
|
|
||||||
|
|
||||||
get atlasRect() {
|
|
||||||
return this._atlasRect;
|
|
||||||
}
|
|
||||||
|
|
||||||
set atlasRect(rect: Rect) {
|
|
||||||
this._atlasRect = rect;
|
|
||||||
}
|
|
||||||
|
|
||||||
get atlasSize(): Size2D {
|
|
||||||
let atlasSize = glmatrix.vec2.create();
|
|
||||||
glmatrix.vec2.sub(atlasSize,
|
|
||||||
this._atlasRect.slice(2, 4) as glmatrix.vec2,
|
|
||||||
this._atlasRect.slice(0, 2) as glmatrix.vec2);
|
|
||||||
return atlasSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _atlasRect: Rect;
|
|
||||||
}
|
|
||||||
|
|
||||||
class Atlas {
|
class Atlas {
|
||||||
constructor() {
|
constructor() {
|
||||||
this._texture = null;
|
this._texture = null;
|
||||||
this._usedSize = glmatrix.vec2.create();
|
this._usedSize = glmatrix.vec2.create();
|
||||||
}
|
}
|
||||||
|
|
||||||
layoutGlyphs(glyphs: AtlasGlyph[], fontSize: number, unitsPerEm: number) {
|
layoutGlyphs(glyphs: AtlasGlyph[], pixelsPerUnit: number) {
|
||||||
const pixelsPerUnit = fontSize / unitsPerEm;
|
|
||||||
|
|
||||||
let nextOrigin = glmatrix.vec2.create();
|
let nextOrigin = glmatrix.vec2.create();
|
||||||
let shelfBottom = 0.0;
|
let shelfBottom = 0.0;
|
||||||
|
|
||||||
for (const glyph of glyphs) {
|
for (const glyph of glyphs) {
|
||||||
const metrics = glyph.metrics;
|
// Place the glyph, and advance the origin.
|
||||||
|
glyph.setPixelPosition(nextOrigin, pixelsPerUnit);
|
||||||
|
nextOrigin[0] = glyph.getRect(pixelsPerUnit)[2];
|
||||||
|
|
||||||
const glyphSize = glmatrix.vec2.fromValues(metrics.xMax - metrics.xMin,
|
// If the glyph overflowed the shelf, make a new one and reposition the glyph.
|
||||||
metrics.yMax - metrics.yMin);
|
if (nextOrigin[0] > ATLAS_SIZE[0]) {
|
||||||
glmatrix.vec2.scale(glyphSize, glyphSize, pixelsPerUnit);
|
|
||||||
glmatrix.vec2.ceil(glyphSize, glyphSize);
|
|
||||||
|
|
||||||
// Make a new shelf if necessary.
|
|
||||||
const initialGlyphRight = nextOrigin[0] + glyphSize[0] + 2;
|
|
||||||
if (initialGlyphRight > ATLAS_SIZE[0])
|
|
||||||
nextOrigin = glmatrix.vec2.fromValues(0.0, shelfBottom);
|
nextOrigin = glmatrix.vec2.fromValues(0.0, shelfBottom);
|
||||||
|
glyph.setPixelPosition(nextOrigin, pixelsPerUnit);
|
||||||
|
nextOrigin[0] = glyph.getRect(pixelsPerUnit)[2];
|
||||||
|
}
|
||||||
|
|
||||||
const glyphRect = glmatrix.vec4.fromValues(nextOrigin[0] + 1,
|
// Grow the shelf as necessary.
|
||||||
nextOrigin[1] + 1,
|
shelfBottom = Math.max(shelfBottom, glyph.getRect(pixelsPerUnit)[3]);
|
||||||
nextOrigin[0] + glyphSize[0] + 1,
|
|
||||||
nextOrigin[1] + glyphSize[1] + 1);
|
|
||||||
|
|
||||||
glyph.atlasRect = glyphRect;
|
|
||||||
|
|
||||||
nextOrigin[0] = glyphRect[2] + 1;
|
|
||||||
shelfBottom = Math.max(shelfBottom, glyphRect[3]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
|
@ -692,6 +532,53 @@ class Atlas {
|
||||||
private _usedSize: Size2D;
|
private _usedSize: Size2D;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AtlasGlyph extends PathfinderGlyph {
|
||||||
|
constructor(glyph: opentype.Glyph) {
|
||||||
|
super(glyph);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRect(pixelsPerUnit: number): glmatrix.vec4 {
|
||||||
|
const glyphSize = glmatrix.vec2.fromValues(this.metrics.xMax - this.metrics.xMin,
|
||||||
|
this.metrics.yMax - this.metrics.yMin);
|
||||||
|
glmatrix.vec2.scale(glyphSize, glyphSize, pixelsPerUnit);
|
||||||
|
glmatrix.vec2.ceil(glyphSize, glyphSize);
|
||||||
|
|
||||||
|
const glyphBL = glmatrix.vec2.create(), glyphTR = glmatrix.vec2.create();
|
||||||
|
glmatrix.vec2.scale(glyphBL, this.position, pixelsPerUnit);
|
||||||
|
glmatrix.vec2.add(glyphBL, glyphBL, [1.0, 1.0]);
|
||||||
|
glmatrix.vec2.add(glyphTR, glyphBL, glyphSize);
|
||||||
|
glmatrix.vec2.add(glyphTR, glyphTR, [1.0, 1.0]);
|
||||||
|
|
||||||
|
return glmatrix.vec4.fromValues(glyphBL[0], glyphBL[1], glyphTR[0], glyphTR[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GlyphInstance extends PathfinderGlyph {
|
||||||
|
constructor(glyph: opentype.Glyph) {
|
||||||
|
super(glyph);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRect(pixelsPerUnit: number): glmatrix.vec4 {
|
||||||
|
// Determine the atlas size.
|
||||||
|
const atlasSize = glmatrix.vec2.fromValues(this.metrics.xMax - this.metrics.xMin,
|
||||||
|
this.metrics.yMax - this.metrics.yMin);
|
||||||
|
glmatrix.vec2.scale(atlasSize, atlasSize, pixelsPerUnit);
|
||||||
|
glmatrix.vec2.ceil(atlasSize, atlasSize);
|
||||||
|
|
||||||
|
// Set positions.
|
||||||
|
const textGlyphBL = glmatrix.vec2.create(), textGlyphTR = glmatrix.vec2.create();
|
||||||
|
const offset = glmatrix.vec2.fromValues(this.metrics.leftSideBearing,
|
||||||
|
this.metrics.yMin);
|
||||||
|
glmatrix.vec2.add(textGlyphBL, this.position, offset);
|
||||||
|
glmatrix.vec2.scale(textGlyphBL, textGlyphBL, pixelsPerUnit);
|
||||||
|
glmatrix.vec2.round(textGlyphBL, textGlyphBL);
|
||||||
|
glmatrix.vec2.add(textGlyphTR, textGlyphBL, atlasSize);
|
||||||
|
|
||||||
|
return glmatrix.vec4.fromValues(textGlyphBL[0], textGlyphBL[1],
|
||||||
|
textGlyphTR[0], textGlyphTR[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const ANTIALIASING_STRATEGIES: AntialiasingStrategyTable = {
|
const ANTIALIASING_STRATEGIES: AntialiasingStrategyTable = {
|
||||||
none: NoAAStrategy,
|
none: NoAAStrategy,
|
||||||
ssaa: SSAAStrategy,
|
ssaa: SSAAStrategy,
|
||||||
|
|
|
@ -0,0 +1,137 @@
|
||||||
|
// 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";
|
||||||
|
|
||||||
|
const PARTITION_FONT_ENDPOINT_URI: string = "/partition-font";
|
||||||
|
|
||||||
|
opentype.Font.prototype.isSupported = function() {
|
||||||
|
return (this as any).supported;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TextLayout<Glyph extends PathfinderGlyph> {
|
||||||
|
constructor(fontData: ArrayBuffer,
|
||||||
|
text: string,
|
||||||
|
createGlyph: (glyph: opentype.Glyph) => Glyph) {
|
||||||
|
this.fontData = fontData;
|
||||||
|
this.font = opentype.parse(fontData);
|
||||||
|
assert(this.font.isSupported(), "The font type is unsupported!");
|
||||||
|
|
||||||
|
// Lay out the text.
|
||||||
|
this.lineGlyphs = text.split("\n").map(line => {
|
||||||
|
return this.font.stringToGlyphs(line).map(createGlyph);
|
||||||
|
});
|
||||||
|
this.textGlyphs = _.flatten(this.lineGlyphs);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
layoutText() {
|
||||||
|
const os2Table = this.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++) {
|
||||||
|
const textGlyph = this.textGlyphs[glyphIndex];
|
||||||
|
textGlyph.position = glmatrix.vec2.clone(currentPosition);
|
||||||
|
currentPosition[0] += textGlyph.advanceWidth;
|
||||||
|
glyphIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPosition[0] = 0;
|
||||||
|
currentPosition[1] -= lineHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly fontData: ArrayBuffer;
|
||||||
|
readonly font: Font;
|
||||||
|
readonly lineGlyphs: Glyph[][];
|
||||||
|
readonly textGlyphs: Glyph[];
|
||||||
|
readonly uniqueGlyphs: 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;
|
||||||
|
}
|
Loading…
Reference in New Issue