2017-08-29 21:57:43 -04:00
|
|
|
|
// pathfinder/client/src/text-demo.ts
|
2017-08-18 20:12:58 -04:00
|
|
|
|
//
|
2017-08-25 23:20:45 -04:00
|
|
|
|
// 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.
|
2017-08-08 14:23:30 -04:00
|
|
|
|
|
2017-08-19 19:34:02 -04:00
|
|
|
|
import * as base64js from 'base64-js';
|
|
|
|
|
import * as glmatrix from 'gl-matrix';
|
2017-09-28 17:34:48 -04:00
|
|
|
|
import * as _ from 'lodash';
|
2017-08-19 19:34:02 -04:00
|
|
|
|
import * as opentype from 'opentype.js';
|
2017-08-08 14:23:30 -04:00
|
|
|
|
|
2017-09-28 17:34:48 -04:00
|
|
|
|
import {Metrics} from 'opentype.js';
|
2017-09-30 01:12:09 -04:00
|
|
|
|
import {AntialiasingStrategy, AntialiasingStrategyName, NoAAStrategy} from "./aa-strategy";
|
2017-10-09 17:14:24 -04:00
|
|
|
|
import {StemDarkeningMode, SubpixelAAType} from './aa-strategy';
|
2017-09-01 21:11:44 -04:00
|
|
|
|
import {DemoAppController} from './app-controller';
|
2017-10-17 14:40:45 -04:00
|
|
|
|
import {Atlas, AtlasGlyph, SUBPIXEL_GRANULARITY} from './atlas';
|
2017-09-28 17:34:48 -04:00
|
|
|
|
import PathfinderBufferTexture from './buffer-texture';
|
2017-09-03 16:03:45 -04:00
|
|
|
|
import {OrthographicCamera} from "./camera";
|
2017-08-26 16:47:18 -04:00
|
|
|
|
import {createFramebuffer, createFramebufferColorTexture} from './gl-utils';
|
|
|
|
|
import {createFramebufferDepthTexture, QUAD_ELEMENTS, setTextureParameters} from './gl-utils';
|
|
|
|
|
import {UniformMap} from './gl-utils';
|
|
|
|
|
import {PathfinderMeshBuffers, PathfinderMeshData} from './meshes';
|
2017-10-16 22:29:13 -04:00
|
|
|
|
import {Renderer} from './renderer';
|
2017-08-26 16:47:18 -04:00
|
|
|
|
import {PathfinderShaderProgram, ShaderMap, ShaderProgramSource} from './shader-loader';
|
2017-09-28 17:34:48 -04:00
|
|
|
|
import SSAAStrategy from './ssaa-strategy';
|
2017-09-27 16:02:32 -04:00
|
|
|
|
import {calculatePixelDescent, calculatePixelRectForGlyph, PathfinderFont} from "./text";
|
2017-10-09 17:14:24 -04:00
|
|
|
|
import {BUILTIN_FONT_URI, calculatePixelXMin, computeStemDarkeningAmount} from "./text";
|
|
|
|
|
import {GlyphStore, Hint, SimpleTextLayout, UnitMetrics} from "./text";
|
2017-10-17 14:40:45 -04:00
|
|
|
|
import {TextRenderContext, TextRenderer} from './text-renderer';
|
2017-09-28 17:34:48 -04:00
|
|
|
|
import {assert, expectNotNull, panic, PathfinderError, scaleRect, UINT32_SIZE} from './utils';
|
2017-09-13 14:56:40 -04:00
|
|
|
|
import {unwrapNull} from './utils';
|
2017-10-17 14:40:45 -04:00
|
|
|
|
import {DemoView, RenderContext, Timings, TIMINGS} from './view';
|
2017-10-09 17:14:24 -04:00
|
|
|
|
import {AdaptiveMonochromeXCAAStrategy} from './xcaa-strategy';
|
2017-08-26 15:54:25 -04:00
|
|
|
|
|
2017-09-01 19:31:40 -04:00
|
|
|
|
const DEFAULT_TEXT: string =
|
2017-08-25 20:02:11 -04:00
|
|
|
|
`’Twas brillig, and the slithy toves
|
|
|
|
|
Did gyre and gimble in the wabe;
|
|
|
|
|
All mimsy were the borogoves,
|
|
|
|
|
And the mome raths outgrabe.
|
|
|
|
|
|
|
|
|
|
“Beware the Jabberwock, my son!
|
|
|
|
|
The jaws that bite, the claws that catch!
|
|
|
|
|
Beware the Jubjub bird, and shun
|
|
|
|
|
The frumious Bandersnatch!”
|
|
|
|
|
|
|
|
|
|
He took his vorpal sword in hand:
|
|
|
|
|
Long time the manxome foe he sought—
|
|
|
|
|
So rested he by the Tumtum tree,
|
|
|
|
|
And stood awhile in thought.
|
|
|
|
|
|
|
|
|
|
And as in uffish thought he stood,
|
|
|
|
|
The Jabberwock, with eyes of flame,
|
|
|
|
|
Came whiffling through the tulgey wood,
|
|
|
|
|
And burbled as it came!
|
|
|
|
|
|
|
|
|
|
One, two! One, two! And through and through
|
|
|
|
|
The vorpal blade went snicker-snack!
|
|
|
|
|
He left it dead, and with its head
|
|
|
|
|
He went galumphing back.
|
|
|
|
|
|
|
|
|
|
“And hast thou slain the Jabberwock?
|
|
|
|
|
Come to my arms, my beamish boy!
|
|
|
|
|
O frabjous day! Callooh! Callay!”
|
|
|
|
|
He chortled in his joy.
|
|
|
|
|
|
|
|
|
|
’Twas brillig, and the slithy toves
|
|
|
|
|
Did gyre and gimble in the wabe;
|
|
|
|
|
All mimsy were the borogoves,
|
|
|
|
|
And the mome raths outgrabe.`;
|
2017-08-22 21:25:32 -04:00
|
|
|
|
|
|
|
|
|
const INITIAL_FONT_SIZE: number = 72.0;
|
2017-08-10 21:38:54 -04:00
|
|
|
|
|
2017-08-31 20:08:22 -04:00
|
|
|
|
const DEFAULT_FONT: string = 'open-sans';
|
2017-08-30 22:48:18 -04:00
|
|
|
|
|
2017-08-14 19:08:45 -04:00
|
|
|
|
const B_POSITION_SIZE: number = 8;
|
|
|
|
|
|
2017-08-17 15:47:50 -04:00
|
|
|
|
const B_PATH_INDEX_SIZE: number = 2;
|
|
|
|
|
|
2017-09-01 19:31:40 -04:00
|
|
|
|
declare global {
|
|
|
|
|
interface Window {
|
|
|
|
|
jQuery(element: HTMLElement): JQuerySubset;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface JQuerySubset {
|
|
|
|
|
modal(options?: any): void;
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-19 01:11:52 -04:00
|
|
|
|
type Matrix4D = Float32Array;
|
|
|
|
|
|
2017-08-19 19:52:14 -04:00
|
|
|
|
type Rect = glmatrix.vec4;
|
2017-08-19 19:34:02 -04:00
|
|
|
|
|
2017-08-19 01:11:52 -04:00
|
|
|
|
interface Point2D {
|
|
|
|
|
x: number;
|
|
|
|
|
y: number;
|
|
|
|
|
}
|
2017-08-14 19:08:45 -04:00
|
|
|
|
|
2017-08-19 19:52:14 -04:00
|
|
|
|
type Size2D = glmatrix.vec2;
|
2017-08-15 00:24:58 -04:00
|
|
|
|
|
2017-08-12 00:26:25 -04:00
|
|
|
|
type ShaderType = number;
|
|
|
|
|
|
2017-08-22 21:25:32 -04:00
|
|
|
|
// `opentype.js` monkey patches
|
|
|
|
|
|
|
|
|
|
declare module 'opentype.js' {
|
|
|
|
|
interface Font {
|
|
|
|
|
isSupported(): boolean;
|
2017-09-07 01:11:32 -04:00
|
|
|
|
lineHeight(): number;
|
2017-08-22 21:25:32 -04:00
|
|
|
|
}
|
|
|
|
|
interface Glyph {
|
|
|
|
|
getIndex(): number;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-01 21:11:44 -04:00
|
|
|
|
class TextDemoController extends DemoAppController<TextDemoView> {
|
2017-09-28 17:34:48 -04:00
|
|
|
|
font: PathfinderFont;
|
|
|
|
|
layout: SimpleTextLayout;
|
|
|
|
|
glyphStore: GlyphStore;
|
|
|
|
|
atlasGlyphs: AtlasGlyph[];
|
|
|
|
|
|
|
|
|
|
private hintingSelect: HTMLSelectElement;
|
|
|
|
|
|
|
|
|
|
private editTextModal: HTMLElement;
|
|
|
|
|
private editTextArea: HTMLTextAreaElement;
|
|
|
|
|
|
|
|
|
|
private _atlas: Atlas;
|
|
|
|
|
|
|
|
|
|
private meshes: PathfinderMeshData;
|
|
|
|
|
|
|
|
|
|
private _fontSize: number;
|
|
|
|
|
|
|
|
|
|
private text: string;
|
|
|
|
|
|
2017-08-23 16:23:29 -04:00
|
|
|
|
constructor() {
|
2017-08-26 15:54:25 -04:00
|
|
|
|
super();
|
2017-09-01 19:31:40 -04:00
|
|
|
|
this.text = DEFAULT_TEXT;
|
2017-08-23 16:23:29 -04:00
|
|
|
|
this._atlas = new Atlas;
|
|
|
|
|
}
|
2017-08-10 21:38:54 -04:00
|
|
|
|
|
2017-08-08 14:23:30 -04:00
|
|
|
|
start() {
|
2017-08-26 15:54:25 -04:00
|
|
|
|
super.start();
|
|
|
|
|
|
2017-08-31 19:11:09 -04:00
|
|
|
|
this._fontSize = INITIAL_FONT_SIZE;
|
2017-09-01 19:31:40 -04:00
|
|
|
|
|
2017-09-09 16:12:51 -04:00
|
|
|
|
this.hintingSelect = unwrapNull(document.getElementById('pf-hinting-select')) as
|
|
|
|
|
HTMLSelectElement;
|
|
|
|
|
this.hintingSelect.addEventListener('change', () => this.hintingChanged(), false);
|
|
|
|
|
|
2017-09-01 19:31:40 -04:00
|
|
|
|
this.editTextModal = unwrapNull(document.getElementById('pf-edit-text-modal'));
|
|
|
|
|
this.editTextArea = unwrapNull(document.getElementById('pf-edit-text-area')) as
|
|
|
|
|
HTMLTextAreaElement;
|
|
|
|
|
|
|
|
|
|
const editTextOkButton = unwrapNull(document.getElementById('pf-edit-text-ok-button'));
|
|
|
|
|
editTextOkButton.addEventListener('click', () => this.updateText(), false);
|
|
|
|
|
|
2017-09-19 23:19:53 -04:00
|
|
|
|
this.loadInitialFile(this.builtinFileURI);
|
2017-08-08 14:23:30 -04:00
|
|
|
|
}
|
|
|
|
|
|
2017-09-01 19:31:40 -04:00
|
|
|
|
showTextEditor() {
|
|
|
|
|
this.editTextArea.value = this.text;
|
|
|
|
|
|
|
|
|
|
window.jQuery(this.editTextModal).modal();
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-01 21:11:44 -04:00
|
|
|
|
protected createView() {
|
|
|
|
|
return new TextDemoView(this,
|
|
|
|
|
unwrapNull(this.commonShaderSource),
|
|
|
|
|
unwrapNull(this.shaderSources));
|
2017-08-26 15:54:25 -04:00
|
|
|
|
}
|
|
|
|
|
|
2017-10-02 22:58:38 -04:00
|
|
|
|
protected fileLoaded(fileData: ArrayBuffer, builtinName: string | null) {
|
|
|
|
|
const font = new PathfinderFont(fileData, builtinName);
|
2017-09-27 16:02:32 -04:00
|
|
|
|
this.recreateLayout(font);
|
2017-09-01 19:31:40 -04:00
|
|
|
|
}
|
|
|
|
|
|
2017-09-28 17:34:48 -04:00
|
|
|
|
private hintingChanged(): void {
|
2017-10-16 22:29:13 -04:00
|
|
|
|
this.view.then(view => view.renderer.updateHinting());
|
2017-09-28 17:34:48 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private updateText(): void {
|
|
|
|
|
this.text = this.editTextArea.value;
|
|
|
|
|
|
|
|
|
|
window.jQuery(this.editTextModal).modal('hide');
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-27 16:02:32 -04:00
|
|
|
|
private recreateLayout(font: PathfinderFont) {
|
|
|
|
|
const newLayout = new SimpleTextLayout(font, this.text);
|
|
|
|
|
|
|
|
|
|
let uniqueGlyphIDs = newLayout.textFrame.allGlyphIDs;
|
|
|
|
|
uniqueGlyphIDs.sort((a, b) => a - b);
|
|
|
|
|
uniqueGlyphIDs = _.sortedUniq(uniqueGlyphIDs);
|
|
|
|
|
|
|
|
|
|
const glyphStore = new GlyphStore(font, uniqueGlyphIDs);
|
|
|
|
|
glyphStore.partition().then(result => {
|
2017-09-29 14:58:16 -04:00
|
|
|
|
const meshes = this.expandMeshes(result.meshes, uniqueGlyphIDs.length);
|
|
|
|
|
|
2017-08-31 20:08:22 -04:00
|
|
|
|
this.view.then(view => {
|
2017-09-27 16:02:32 -04:00
|
|
|
|
this.font = font;
|
2017-09-23 16:09:45 -04:00
|
|
|
|
this.layout = newLayout;
|
2017-09-27 16:02:32 -04:00
|
|
|
|
this.glyphStore = glyphStore;
|
2017-09-29 14:58:16 -04:00
|
|
|
|
this.meshes = meshes;
|
2017-09-23 16:09:45 -04:00
|
|
|
|
|
2017-08-31 20:08:22 -04:00
|
|
|
|
view.attachText();
|
2017-10-16 22:29:13 -04:00
|
|
|
|
view.renderer.uploadPathColors(1);
|
2017-09-07 19:13:55 -04:00
|
|
|
|
view.attachMeshes([this.meshes]);
|
2017-08-31 20:08:22 -04:00
|
|
|
|
});
|
2017-08-12 13:08:35 -04:00
|
|
|
|
});
|
2017-08-08 14:23:30 -04:00
|
|
|
|
}
|
|
|
|
|
|
2017-09-29 14:58:16 -04:00
|
|
|
|
private expandMeshes(meshes: PathfinderMeshData, glyphCount: number): PathfinderMeshData {
|
|
|
|
|
const pathIDs = [];
|
|
|
|
|
for (let glyphIndex = 0; glyphIndex < glyphCount; glyphIndex++) {
|
|
|
|
|
for (let subpixel = 0; subpixel < SUBPIXEL_GRANULARITY; subpixel++)
|
|
|
|
|
pathIDs.push(glyphIndex + 1);
|
|
|
|
|
}
|
|
|
|
|
return meshes.expand(pathIDs);
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-23 16:23:29 -04:00
|
|
|
|
get atlas(): Atlas {
|
|
|
|
|
return this._atlas;
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-29 01:11:15 -04:00
|
|
|
|
/// The font size in pixels per em.
|
|
|
|
|
get fontSize(): number {
|
|
|
|
|
return this._fontSize;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The font size in pixels per em.
|
|
|
|
|
set fontSize(newFontSize: number) {
|
|
|
|
|
this._fontSize = newFontSize;
|
2017-10-16 22:29:13 -04:00
|
|
|
|
this.view.then(view => view.renderer.relayoutText());
|
2017-08-29 01:11:15 -04:00
|
|
|
|
}
|
|
|
|
|
|
2017-09-30 01:12:09 -04:00
|
|
|
|
get layoutPixelsPerUnit(): number {
|
2017-09-27 16:02:32 -04:00
|
|
|
|
return this._fontSize / this.font.opentypeFont.unitsPerEm;
|
2017-08-31 19:11:09 -04:00
|
|
|
|
}
|
|
|
|
|
|
2017-09-09 16:12:51 -04:00
|
|
|
|
get useHinting(): boolean {
|
|
|
|
|
return this.hintingSelect.selectedIndex !== 0;
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-29 14:58:16 -04:00
|
|
|
|
get pathCount(): number {
|
|
|
|
|
return this.glyphStore.glyphIDs.length * SUBPIXEL_GRANULARITY;
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-30 22:48:18 -04:00
|
|
|
|
protected get builtinFileURI(): string {
|
|
|
|
|
return BUILTIN_FONT_URI;
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-31 20:08:22 -04:00
|
|
|
|
protected get defaultFile(): string {
|
|
|
|
|
return DEFAULT_FONT;
|
|
|
|
|
}
|
2017-09-28 17:34:48 -04:00
|
|
|
|
}
|
2017-09-09 16:12:51 -04:00
|
|
|
|
|
2017-10-17 14:40:45 -04:00
|
|
|
|
class TextDemoView extends DemoView implements TextRenderContext {
|
|
|
|
|
renderer: TextRenderer;
|
2017-10-16 22:29:13 -04:00
|
|
|
|
|
|
|
|
|
appController: TextDemoController;
|
|
|
|
|
|
2017-10-17 14:40:45 -04:00
|
|
|
|
get atlasGlyphs(): AtlasGlyph[] {
|
|
|
|
|
return this.appController.atlasGlyphs;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
set atlasGlyphs(newAtlasGlyphs: AtlasGlyph[]) {
|
|
|
|
|
this.appController.atlasGlyphs = newAtlasGlyphs;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get atlas(): Atlas {
|
|
|
|
|
return this.appController.atlas;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get layout(): SimpleTextLayout {
|
|
|
|
|
return this.appController.layout;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get glyphStore(): GlyphStore {
|
|
|
|
|
return this.appController.glyphStore;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get font(): PathfinderFont {
|
|
|
|
|
return this.appController.font;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get fontSize(): number {
|
|
|
|
|
return this.appController.fontSize;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get pathCount(): number {
|
|
|
|
|
return this.appController.pathCount;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get layoutPixelsPerUnit(): number {
|
|
|
|
|
return this.appController.layoutPixelsPerUnit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get useHinting(): boolean {
|
|
|
|
|
return this.appController.useHinting;
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-16 22:29:13 -04:00
|
|
|
|
protected get camera(): OrthographicCamera {
|
|
|
|
|
return this.renderer.camera;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
constructor(appController: TextDemoController,
|
|
|
|
|
commonShaderSource: string,
|
|
|
|
|
shaderSources: ShaderMap<ShaderProgramSource>) {
|
|
|
|
|
super(commonShaderSource, shaderSources);
|
|
|
|
|
|
|
|
|
|
this.appController = appController;
|
2017-10-17 14:40:45 -04:00
|
|
|
|
this.renderer = new TextRenderer(this);
|
2017-10-16 22:29:13 -04:00
|
|
|
|
|
|
|
|
|
this.canvas.addEventListener('dblclick', () => this.appController.showTextEditor(), false);
|
|
|
|
|
|
|
|
|
|
this.resizeToFit(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
attachText() {
|
|
|
|
|
this.panZoomEventsEnabled = false;
|
|
|
|
|
this.renderer.prepareToAttachText();
|
|
|
|
|
this.renderer.camera.zoomToFit();
|
|
|
|
|
this.appController.fontSize = this.renderer.camera.scale *
|
|
|
|
|
this.appController.font.opentypeFont.unitsPerEm;
|
|
|
|
|
this.renderer.finishAttachingText();
|
|
|
|
|
this.panZoomEventsEnabled = true;
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-17 14:40:45 -04:00
|
|
|
|
newTimingsReceived(newTimings: Timings) {
|
|
|
|
|
this.appController.newTimingsReceived(newTimings);
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-16 22:29:13 -04:00
|
|
|
|
protected onPan() {
|
|
|
|
|
this.renderer.viewPanned();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected onZoom() {
|
|
|
|
|
this.appController.fontSize = this.renderer.camera.scale *
|
|
|
|
|
this.appController.font.opentypeFont.unitsPerEm;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private set panZoomEventsEnabled(flag: boolean) {
|
|
|
|
|
if (flag) {
|
|
|
|
|
this.renderer.camera.onPan = () => this.onPan();
|
|
|
|
|
this.renderer.camera.onZoom = () => this.onZoom();
|
|
|
|
|
} else {
|
|
|
|
|
this.renderer.camera.onPan = null;
|
|
|
|
|
this.renderer.camera.onZoom = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-08 14:23:30 -04:00
|
|
|
|
function main() {
|
2017-08-26 15:54:25 -04:00
|
|
|
|
const controller = new TextDemoController;
|
2017-08-08 14:23:30 -04:00
|
|
|
|
window.addEventListener('load', () => controller.start(), false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
main();
|