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-11-07 20:24:19 -05:00
|
|
|
|
import {AAOptions, DemoAppController} from './app-controller';
|
2017-10-17 16:37:36 -04:00
|
|
|
|
import {Atlas, ATLAS_SIZE, AtlasGlyph, GlyphKey, SUBPIXEL_GRANULARITY} from './atlas';
|
2017-09-28 17:34:48 -04:00
|
|
|
|
import PathfinderBufferTexture from './buffer-texture';
|
2017-10-17 15:10:20 -04:00
|
|
|
|
import {CameraView, 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-11-20 19:47:52 -05:00
|
|
|
|
protected createView(gammaLUT: HTMLImageElement,
|
|
|
|
|
commonShaderSource: string,
|
|
|
|
|
shaderSources: ShaderMap<ShaderProgramSource>):
|
|
|
|
|
TextDemoView {
|
|
|
|
|
return new TextDemoView(this, gammaLUT, commonShaderSource, 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-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 {
|
2017-10-17 16:37:36 -04:00
|
|
|
|
renderer: TextDemoRenderer;
|
2017-10-16 22:29:13 -04:00
|
|
|
|
|
|
|
|
|
appController: TextDemoController;
|
|
|
|
|
|
2017-10-17 15:10:20 -04:00
|
|
|
|
get cameraView(): CameraView {
|
|
|
|
|
return this.canvas;
|
|
|
|
|
}
|
|
|
|
|
|
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 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,
|
2017-11-07 17:13:13 -05:00
|
|
|
|
gammaLUT: HTMLImageElement,
|
2017-10-16 22:29:13 -04:00
|
|
|
|
commonShaderSource: string,
|
|
|
|
|
shaderSources: ShaderMap<ShaderProgramSource>) {
|
2017-11-07 17:13:13 -05:00
|
|
|
|
super(gammaLUT, commonShaderSource, shaderSources);
|
2017-10-16 22:29:13 -04:00
|
|
|
|
|
|
|
|
|
this.appController = appController;
|
2017-10-17 16:37:36 -04:00
|
|
|
|
this.renderer = new TextDemoRenderer(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-10-17 16:37:36 -04:00
|
|
|
|
class TextDemoRenderer extends TextRenderer {
|
|
|
|
|
renderContext: TextDemoView;
|
|
|
|
|
|
|
|
|
|
glyphPositionsBuffer: WebGLBuffer;
|
|
|
|
|
glyphTexCoordsBuffer: WebGLBuffer;
|
|
|
|
|
glyphElementsBuffer: WebGLBuffer;
|
|
|
|
|
|
2017-11-15 17:36:59 -05:00
|
|
|
|
private glyphBounds: Float32Array;
|
|
|
|
|
|
2017-10-17 16:37:36 -04:00
|
|
|
|
get layout(): SimpleTextLayout {
|
|
|
|
|
return this.renderContext.appController.layout;
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-15 17:36:59 -05:00
|
|
|
|
get backgroundColor(): glmatrix.vec4 {
|
|
|
|
|
return glmatrix.vec4.create();
|
|
|
|
|
}
|
2017-10-17 16:37:36 -04:00
|
|
|
|
|
|
|
|
|
prepareToAttachText(): void {
|
|
|
|
|
if (this.atlasFramebuffer == null)
|
|
|
|
|
this.createAtlasFramebuffer();
|
|
|
|
|
|
|
|
|
|
this.layoutText();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
finishAttachingText(): void {
|
|
|
|
|
this.buildGlyphs();
|
|
|
|
|
this.renderContext.setDirty();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setAntialiasingOptions(aaType: AntialiasingStrategyName,
|
|
|
|
|
aaLevel: number,
|
2017-11-07 20:24:19 -05:00
|
|
|
|
aaOptions: AAOptions):
|
2017-10-17 16:37:36 -04:00
|
|
|
|
void {
|
2017-11-07 20:24:19 -05:00
|
|
|
|
super.setAntialiasingOptions(aaType, aaLevel, aaOptions);
|
2017-10-17 16:37:36 -04:00
|
|
|
|
|
|
|
|
|
// Need to relayout because changing AA options can cause font dilation to change...
|
|
|
|
|
this.layoutText();
|
|
|
|
|
this.buildGlyphs();
|
|
|
|
|
this.renderContext.setDirty();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
relayoutText(): void {
|
|
|
|
|
this.layoutText();
|
|
|
|
|
this.buildGlyphs();
|
|
|
|
|
this.renderContext.setDirty();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateHinting(): void {
|
|
|
|
|
// Need to relayout the text because the pixel bounds of the glyphs can change from this...
|
|
|
|
|
this.layoutText();
|
|
|
|
|
this.buildGlyphs();
|
|
|
|
|
this.renderContext.setDirty();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
viewPanned(): void {
|
|
|
|
|
this.buildGlyphs();
|
|
|
|
|
this.renderContext.setDirty();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected compositeIfNecessary(): void {
|
2017-10-26 23:15:41 -04:00
|
|
|
|
const renderContext = this.renderContext;
|
|
|
|
|
const gl = renderContext.gl;
|
|
|
|
|
|
2017-10-17 16:37:36 -04:00
|
|
|
|
// Set up composite state.
|
2017-10-26 23:15:41 -04:00
|
|
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
|
|
|
gl.viewport(0, 0, renderContext.cameraView.width, renderContext.cameraView.height);
|
|
|
|
|
gl.disable(gl.DEPTH_TEST);
|
|
|
|
|
gl.disable(gl.SCISSOR_TEST);
|
|
|
|
|
gl.blendEquation(gl.FUNC_REVERSE_SUBTRACT);
|
|
|
|
|
gl.blendFuncSeparate(gl.ONE, gl.ONE, gl.ZERO, gl.ONE);
|
|
|
|
|
gl.enable(gl.BLEND);
|
2017-10-17 16:37:36 -04:00
|
|
|
|
|
|
|
|
|
// Clear.
|
2017-10-26 23:15:41 -04:00
|
|
|
|
gl.clearColor(1.0, 1.0, 1.0, 1.0);
|
|
|
|
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
2017-10-17 16:37:36 -04:00
|
|
|
|
|
2017-11-07 20:24:19 -05:00
|
|
|
|
// Set the appropriate program.
|
|
|
|
|
const programName = this.gammaCorrectionMode === 'off' ? 'blitLinear' : 'blitGamma';
|
|
|
|
|
const blitProgram = this.renderContext.shaderPrograms[programName];
|
|
|
|
|
|
2017-10-17 16:37:36 -04:00
|
|
|
|
// Set up the composite VAO.
|
|
|
|
|
const attributes = blitProgram.attributes;
|
2017-11-02 14:14:45 -04:00
|
|
|
|
gl.useProgram(blitProgram.program);
|
|
|
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.glyphPositionsBuffer);
|
|
|
|
|
gl.vertexAttribPointer(attributes.aPosition, 2, gl.FLOAT, false, 0, 0);
|
|
|
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.glyphTexCoordsBuffer);
|
|
|
|
|
gl.vertexAttribPointer(attributes.aTexCoord, 2, gl.FLOAT, false, 0, 0);
|
|
|
|
|
gl.enableVertexAttribArray(attributes.aPosition);
|
|
|
|
|
gl.enableVertexAttribArray(attributes.aTexCoord);
|
|
|
|
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glyphElementsBuffer);
|
2017-10-17 16:37:36 -04:00
|
|
|
|
|
|
|
|
|
// Create the transform.
|
|
|
|
|
const transform = glmatrix.mat4.create();
|
|
|
|
|
glmatrix.mat4.fromTranslation(transform, [-1.0, -1.0, 0.0]);
|
|
|
|
|
glmatrix.mat4.scale(transform, transform, [
|
|
|
|
|
2.0 / this.renderContext.cameraView.width,
|
|
|
|
|
2.0 / this.renderContext.cameraView.height,
|
|
|
|
|
1.0,
|
|
|
|
|
]);
|
|
|
|
|
glmatrix.mat4.translate(transform,
|
|
|
|
|
transform,
|
|
|
|
|
[this.camera.translation[0], this.camera.translation[1], 0.0]);
|
|
|
|
|
|
|
|
|
|
// Blit.
|
2017-11-02 14:14:45 -04:00
|
|
|
|
gl.uniformMatrix4fv(blitProgram.uniforms.uTransform, false, transform);
|
|
|
|
|
gl.activeTexture(gl.TEXTURE0);
|
2017-10-17 16:37:36 -04:00
|
|
|
|
const destTexture = this.renderContext
|
|
|
|
|
.atlas
|
|
|
|
|
.ensureTexture(this.renderContext);
|
2017-11-02 14:14:45 -04:00
|
|
|
|
gl.bindTexture(gl.TEXTURE_2D, destTexture);
|
|
|
|
|
gl.uniform1i(blitProgram.uniforms.uSource, 0);
|
2017-10-17 16:37:36 -04:00
|
|
|
|
this.setIdentityTexScaleUniform(blitProgram.uniforms);
|
2017-11-07 20:24:19 -05:00
|
|
|
|
this.bindGammaLUT(glmatrix.vec3.clone([1.0, 1.0, 1.0]), 1, blitProgram.uniforms);
|
2017-10-17 16:37:36 -04:00
|
|
|
|
const totalGlyphCount = this.layout.textFrame.totalGlyphCount;
|
2017-11-02 14:14:45 -04:00
|
|
|
|
gl.drawElements(gl.TRIANGLES, totalGlyphCount * 6, gl.UNSIGNED_INT, 0);
|
2017-10-17 16:37:36 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private layoutText(): void {
|
2017-11-02 14:14:45 -04:00
|
|
|
|
const renderContext = this.renderContext;
|
|
|
|
|
const gl = renderContext.gl;
|
|
|
|
|
|
2017-10-17 16:37:36 -04:00
|
|
|
|
this.layout.layoutRuns();
|
|
|
|
|
|
|
|
|
|
const textBounds = this.layout.textFrame.bounds;
|
|
|
|
|
this.camera.bounds = textBounds;
|
|
|
|
|
|
|
|
|
|
const totalGlyphCount = this.layout.textFrame.totalGlyphCount;
|
|
|
|
|
const glyphPositions = new Float32Array(totalGlyphCount * 8);
|
|
|
|
|
const glyphIndices = new Uint32Array(totalGlyphCount * 6);
|
|
|
|
|
|
|
|
|
|
const hint = this.createHint();
|
|
|
|
|
const displayPixelsPerUnit = this.displayPixelsPerUnit;
|
|
|
|
|
const layoutPixelsPerUnit = this.layoutPixelsPerUnit;
|
|
|
|
|
|
|
|
|
|
let globalGlyphIndex = 0;
|
|
|
|
|
for (const run of this.layout.textFrame.runs) {
|
|
|
|
|
for (let glyphIndex = 0;
|
|
|
|
|
glyphIndex < run.glyphIDs.length;
|
|
|
|
|
glyphIndex++, globalGlyphIndex++) {
|
|
|
|
|
const rect = run.pixelRectForGlyphAt(glyphIndex,
|
|
|
|
|
layoutPixelsPerUnit,
|
|
|
|
|
displayPixelsPerUnit,
|
|
|
|
|
hint,
|
|
|
|
|
this.stemDarkeningAmount,
|
|
|
|
|
SUBPIXEL_GRANULARITY);
|
|
|
|
|
glyphPositions.set([
|
|
|
|
|
rect[0], rect[3],
|
|
|
|
|
rect[2], rect[3],
|
|
|
|
|
rect[0], rect[1],
|
|
|
|
|
rect[2], rect[1],
|
|
|
|
|
], globalGlyphIndex * 8);
|
|
|
|
|
|
|
|
|
|
for (let glyphIndexIndex = 0;
|
|
|
|
|
glyphIndexIndex < QUAD_ELEMENTS.length;
|
|
|
|
|
glyphIndexIndex++) {
|
|
|
|
|
glyphIndices[glyphIndexIndex + globalGlyphIndex * 6] =
|
|
|
|
|
QUAD_ELEMENTS[glyphIndexIndex] + 4 * globalGlyphIndex;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-02 14:14:45 -04:00
|
|
|
|
this.glyphPositionsBuffer = unwrapNull(gl.createBuffer());
|
|
|
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.glyphPositionsBuffer);
|
|
|
|
|
gl.bufferData(gl.ARRAY_BUFFER, glyphPositions, gl.STATIC_DRAW);
|
|
|
|
|
this.glyphElementsBuffer = unwrapNull(gl.createBuffer());
|
|
|
|
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glyphElementsBuffer);
|
|
|
|
|
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, glyphIndices, gl.STATIC_DRAW);
|
2017-10-17 16:37:36 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private buildGlyphs(): void {
|
|
|
|
|
const font = this.renderContext.font;
|
|
|
|
|
const glyphStore = this.renderContext.glyphStore;
|
|
|
|
|
const layoutPixelsPerUnit = this.layoutPixelsPerUnit;
|
|
|
|
|
const displayPixelsPerUnit = this.displayPixelsPerUnit;
|
|
|
|
|
|
|
|
|
|
const textFrame = this.layout.textFrame;
|
|
|
|
|
const hint = this.createHint();
|
|
|
|
|
|
|
|
|
|
// Only build glyphs in view.
|
|
|
|
|
const translation = this.camera.translation;
|
|
|
|
|
const canvasRect = glmatrix.vec4.clone([
|
|
|
|
|
-translation[0],
|
|
|
|
|
-translation[1],
|
|
|
|
|
-translation[0] + this.renderContext.cameraView.width,
|
|
|
|
|
-translation[1] + this.renderContext.cameraView.height,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const atlasGlyphs = [];
|
|
|
|
|
for (const run of textFrame.runs) {
|
|
|
|
|
for (let glyphIndex = 0; glyphIndex < run.glyphIDs.length; glyphIndex++) {
|
|
|
|
|
const pixelRect = run.pixelRectForGlyphAt(glyphIndex,
|
|
|
|
|
layoutPixelsPerUnit,
|
|
|
|
|
displayPixelsPerUnit,
|
|
|
|
|
hint,
|
|
|
|
|
this.stemDarkeningAmount,
|
|
|
|
|
SUBPIXEL_GRANULARITY);
|
|
|
|
|
if (!rectsIntersect(pixelRect, canvasRect))
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
const glyphID = run.glyphIDs[glyphIndex];
|
|
|
|
|
const glyphStoreIndex = glyphStore.indexOfGlyphWithID(glyphID);
|
|
|
|
|
if (glyphStoreIndex == null)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
const subpixel = run.subpixelForGlyphAt(glyphIndex,
|
|
|
|
|
layoutPixelsPerUnit,
|
|
|
|
|
hint,
|
|
|
|
|
SUBPIXEL_GRANULARITY);
|
|
|
|
|
const glyphKey = new GlyphKey(glyphID, subpixel);
|
|
|
|
|
atlasGlyphs.push(new AtlasGlyph(glyphStoreIndex, glyphKey));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.buildAtlasGlyphs(atlasGlyphs);
|
|
|
|
|
|
|
|
|
|
// TODO(pcwalton): Regenerate the IBOs to include only the glyphs we care about.
|
|
|
|
|
|
|
|
|
|
this.setGlyphTexCoords();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private setGlyphTexCoords(): void {
|
|
|
|
|
const textFrame = this.layout.textFrame;
|
|
|
|
|
const font = this.renderContext.font;
|
|
|
|
|
const atlasGlyphs = this.renderContext.atlasGlyphs;
|
|
|
|
|
|
|
|
|
|
const hint = this.createHint();
|
|
|
|
|
const layoutPixelsPerUnit = this.layoutPixelsPerUnit;
|
|
|
|
|
const displayPixelsPerUnit = this.displayPixelsPerUnit;
|
|
|
|
|
|
|
|
|
|
const atlasGlyphKeys = atlasGlyphs.map(atlasGlyph => atlasGlyph.glyphKey.sortKey);
|
|
|
|
|
|
|
|
|
|
this.glyphBounds = new Float32Array(textFrame.totalGlyphCount * 8);
|
|
|
|
|
|
|
|
|
|
let globalGlyphIndex = 0;
|
|
|
|
|
for (const run of textFrame.runs) {
|
|
|
|
|
for (let glyphIndex = 0;
|
|
|
|
|
glyphIndex < run.glyphIDs.length;
|
|
|
|
|
glyphIndex++, globalGlyphIndex++) {
|
|
|
|
|
const textGlyphID = run.glyphIDs[glyphIndex];
|
|
|
|
|
|
|
|
|
|
const subpixel = run.subpixelForGlyphAt(glyphIndex,
|
|
|
|
|
layoutPixelsPerUnit,
|
|
|
|
|
hint,
|
|
|
|
|
SUBPIXEL_GRANULARITY);
|
|
|
|
|
|
|
|
|
|
const glyphKey = new GlyphKey(textGlyphID, subpixel);
|
|
|
|
|
|
|
|
|
|
const atlasGlyphIndex = _.sortedIndexOf(atlasGlyphKeys, glyphKey.sortKey);
|
|
|
|
|
if (atlasGlyphIndex < 0)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
// Set texture coordinates.
|
|
|
|
|
const atlasGlyph = atlasGlyphs[atlasGlyphIndex];
|
|
|
|
|
const atlasGlyphMetrics = font.metricsForGlyph(atlasGlyph.glyphKey.id);
|
|
|
|
|
if (atlasGlyphMetrics == null)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
const atlasGlyphUnitMetrics = new UnitMetrics(atlasGlyphMetrics,
|
|
|
|
|
this.stemDarkeningAmount);
|
|
|
|
|
|
|
|
|
|
const atlasGlyphPixelOrigin =
|
|
|
|
|
atlasGlyph.calculateSubpixelOrigin(displayPixelsPerUnit);
|
|
|
|
|
const atlasGlyphRect = calculatePixelRectForGlyph(atlasGlyphUnitMetrics,
|
|
|
|
|
atlasGlyphPixelOrigin,
|
|
|
|
|
displayPixelsPerUnit,
|
|
|
|
|
hint);
|
|
|
|
|
const atlasGlyphBL = atlasGlyphRect.slice(0, 2) as glmatrix.vec2;
|
|
|
|
|
const atlasGlyphTR = atlasGlyphRect.slice(2, 4) as glmatrix.vec2;
|
|
|
|
|
glmatrix.vec2.div(atlasGlyphBL, atlasGlyphBL, ATLAS_SIZE);
|
|
|
|
|
glmatrix.vec2.div(atlasGlyphTR, atlasGlyphTR, ATLAS_SIZE);
|
|
|
|
|
|
|
|
|
|
this.glyphBounds.set([
|
|
|
|
|
atlasGlyphBL[0], atlasGlyphTR[1],
|
|
|
|
|
atlasGlyphTR[0], atlasGlyphTR[1],
|
|
|
|
|
atlasGlyphBL[0], atlasGlyphBL[1],
|
|
|
|
|
atlasGlyphTR[0], atlasGlyphBL[1],
|
|
|
|
|
], globalGlyphIndex * 8);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.glyphTexCoordsBuffer = unwrapNull(this.renderContext.gl.createBuffer());
|
|
|
|
|
this.renderContext.gl.bindBuffer(this.renderContext.gl.ARRAY_BUFFER,
|
|
|
|
|
this.glyphTexCoordsBuffer);
|
|
|
|
|
this.renderContext.gl.bufferData(this.renderContext.gl.ARRAY_BUFFER,
|
|
|
|
|
this.glyphBounds,
|
|
|
|
|
this.renderContext.gl.STATIC_DRAW);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private setIdentityTexScaleUniform(uniforms: UniformMap): void {
|
|
|
|
|
this.renderContext.gl.uniform2f(uniforms.uTexScale, 1.0, 1.0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The separating axis theorem.
|
|
|
|
|
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];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function main(): void {
|
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();
|