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-09-01 19:31:40 -04:00
|
|
|
|
import {Font} from 'opentype.js';
|
2017-08-19 19:34:02 -04:00
|
|
|
|
import * as _ from 'lodash';
|
|
|
|
|
import * as base64js from 'base64-js';
|
|
|
|
|
import * as glmatrix from 'gl-matrix';
|
|
|
|
|
import * as opentype from 'opentype.js';
|
2017-08-08 14:23:30 -04:00
|
|
|
|
|
2017-08-28 20:18:44 -04:00
|
|
|
|
import {AntialiasingStrategy, AntialiasingStrategyName, NoAAStrategy} from './aa-strategy';
|
2017-09-01 21:11:44 -04:00
|
|
|
|
import {DemoAppController} from './app-controller';
|
2017-09-03 16:03:45 -04:00
|
|
|
|
import {OrthographicCamera} from "./camera";
|
2017-08-26 16:47:18 -04:00
|
|
|
|
import {ECAAMonochromeStrategy, ECAAStrategy} from './ecaa-strategy';
|
|
|
|
|
import {createFramebuffer, createFramebufferColorTexture} from './gl-utils';
|
|
|
|
|
import {createFramebufferDepthTexture, QUAD_ELEMENTS, setTextureParameters} from './gl-utils';
|
|
|
|
|
import {UniformMap} from './gl-utils';
|
|
|
|
|
import {PathfinderMeshBuffers, PathfinderMeshData} from './meshes';
|
|
|
|
|
import {PathfinderShaderProgram, ShaderMap, ShaderProgramSource} from './shader-loader';
|
2017-09-07 19:13:55 -04:00
|
|
|
|
import {BUILTIN_FONT_URI, PathfinderGlyph, SimpleTextLayout} from "./text";
|
2017-08-28 20:18:44 -04:00
|
|
|
|
import {PathfinderError, assert, expectNotNull, UINT32_SIZE, unwrapNull, panic} from './utils';
|
|
|
|
|
import {MonochromePathfinderView, Timings} from './view';
|
2017-08-26 16:47:18 -04:00
|
|
|
|
import PathfinderBufferTexture from './buffer-texture';
|
|
|
|
|
import SSAAStrategy from './ssaa-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-06 19:32:11 -04:00
|
|
|
|
const ATLAS_SIZE: glmatrix.vec2 = glmatrix.vec2.fromValues(2048, 4096);
|
2017-08-23 16:23:29 -04:00
|
|
|
|
|
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-08-25 18:39:13 -04:00
|
|
|
|
/// 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];
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-01 21:11:44 -04:00
|
|
|
|
class TextDemoController extends DemoAppController<TextDemoView> {
|
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-08-19 12:57:54 -04:00
|
|
|
|
this.fpsLabel = unwrapNull(document.getElementById('pf-fps-label'));
|
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-08-30 22:48:18 -04:00
|
|
|
|
this.loadInitialFile();
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private updateText() {
|
|
|
|
|
this.text = this.editTextArea.value;
|
|
|
|
|
this.recreateLayout();
|
|
|
|
|
|
|
|
|
|
window.jQuery(this.editTextModal).modal('hide');
|
|
|
|
|
}
|
|
|
|
|
|
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-08-28 19:47:27 -04:00
|
|
|
|
protected fileLoaded() {
|
2017-09-01 19:31:40 -04:00
|
|
|
|
this.recreateLayout();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private recreateLayout() {
|
2017-09-07 19:13:55 -04:00
|
|
|
|
this.layout = new SimpleTextLayout(this.fileData, this.text, glyph => new GlyphInstance(glyph));
|
2017-09-02 01:29:05 -04:00
|
|
|
|
this.layout.glyphStorage.partition().then((meshes: PathfinderMeshData) => {
|
2017-08-31 19:11:09 -04:00
|
|
|
|
this.meshes = meshes;
|
2017-08-31 20:08:22 -04:00
|
|
|
|
this.view.then(view => {
|
|
|
|
|
view.attachText();
|
2017-09-02 01:29:05 -04:00
|
|
|
|
view.uploadPathMetadata(this.layout.glyphStorage.uniqueGlyphs.length);
|
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-08-28 20:18:44 -04:00
|
|
|
|
updateTimings(newTimes: Timings) {
|
2017-08-24 19:18:26 -04:00
|
|
|
|
this.fpsLabel.innerHTML =
|
|
|
|
|
`${newTimes.atlasRendering} ms atlas, ${newTimes.compositing} ms compositing`;
|
2017-08-19 12:57:54 -04:00
|
|
|
|
}
|
|
|
|
|
|
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;
|
|
|
|
|
this.view.then(view => view.attachText());
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-31 19:11:09 -04:00
|
|
|
|
get pixelsPerUnit(): number {
|
2017-09-02 01:29:05 -04:00
|
|
|
|
return this._fontSize / this.layout.glyphStorage.font.unitsPerEm;
|
2017-08-31 19:11:09 -04:00
|
|
|
|
}
|
|
|
|
|
|
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-08-24 22:30:53 -04:00
|
|
|
|
private fpsLabel: HTMLElement;
|
2017-09-01 19:31:40 -04:00
|
|
|
|
private editTextModal: HTMLElement;
|
|
|
|
|
private editTextArea: HTMLTextAreaElement;
|
2017-08-22 21:25:32 -04:00
|
|
|
|
|
2017-08-23 16:23:29 -04:00
|
|
|
|
private _atlas: Atlas;
|
2017-08-25 18:39:13 -04:00
|
|
|
|
atlasGlyphs: AtlasGlyph[];
|
2017-08-22 21:25:32 -04:00
|
|
|
|
|
2017-08-24 22:30:53 -04:00
|
|
|
|
private meshes: PathfinderMeshData;
|
2017-08-22 21:25:32 -04:00
|
|
|
|
|
2017-08-29 01:11:15 -04:00
|
|
|
|
private _fontSize: number;
|
2017-08-31 19:11:09 -04:00
|
|
|
|
|
2017-09-01 19:31:40 -04:00
|
|
|
|
private text: string;
|
|
|
|
|
|
2017-09-07 19:13:55 -04:00
|
|
|
|
layout: SimpleTextLayout<GlyphInstance>;
|
2017-08-08 14:23:30 -04:00
|
|
|
|
}
|
|
|
|
|
|
2017-08-27 15:43:17 -04:00
|
|
|
|
class TextDemoView extends MonochromePathfinderView {
|
2017-08-26 15:54:25 -04:00
|
|
|
|
constructor(appController: TextDemoController,
|
2017-08-17 23:09:18 -04:00
|
|
|
|
commonShaderSource: string,
|
|
|
|
|
shaderSources: ShaderMap<ShaderProgramSource>) {
|
2017-09-02 01:29:05 -04:00
|
|
|
|
super(commonShaderSource, shaderSources);
|
2017-08-26 16:47:18 -04:00
|
|
|
|
|
2017-08-19 12:57:54 -04:00
|
|
|
|
this.appController = appController;
|
2017-09-01 19:31:40 -04:00
|
|
|
|
|
2017-09-02 15:14:10 -04:00
|
|
|
|
this.camera = new OrthographicCamera(this.canvas);
|
|
|
|
|
this.camera.onPan = () => this.onPan();
|
|
|
|
|
this.camera.onZoom = () => this.onZoom();
|
|
|
|
|
|
2017-09-01 19:31:40 -04:00
|
|
|
|
this.canvas.addEventListener('dblclick', () => this.appController.showTextEditor(), false);
|
2017-08-15 16:38:54 -04:00
|
|
|
|
}
|
|
|
|
|
|
2017-08-26 16:47:18 -04:00
|
|
|
|
protected initContext() {
|
|
|
|
|
super.initContext();
|
2017-08-12 00:26:25 -04:00
|
|
|
|
}
|
|
|
|
|
|
2017-08-31 20:08:22 -04:00
|
|
|
|
uploadPathMetadata(pathCount: number) {
|
2017-08-24 19:23:02 -04:00
|
|
|
|
const pathColors = new Uint8Array(4 * (pathCount + 1));
|
2017-08-15 00:24:58 -04:00
|
|
|
|
for (let pathIndex = 0; pathIndex < pathCount; pathIndex++) {
|
|
|
|
|
for (let channel = 0; channel < 3; channel++)
|
2017-08-24 19:23:02 -04:00
|
|
|
|
pathColors[(pathIndex + 1) * 4 + channel] = 0x00; // RGB
|
|
|
|
|
pathColors[(pathIndex + 1) * 4 + 3] = 0xff; // alpha
|
2017-08-15 00:24:58 -04:00
|
|
|
|
}
|
|
|
|
|
|
2017-09-07 22:01:55 -04:00
|
|
|
|
const pathColorsBufferTexture = new PathfinderBufferTexture(this.gl, 'uPathColors');
|
|
|
|
|
pathColorsBufferTexture.upload(this.gl, pathColors);
|
|
|
|
|
this.pathColorsBufferTextures = [pathColorsBufferTexture];
|
2017-08-15 00:24:58 -04:00
|
|
|
|
}
|
|
|
|
|
|
2017-08-25 18:39:13 -04:00
|
|
|
|
/// Lays out glyphs on the canvas.
|
|
|
|
|
private layoutGlyphs() {
|
2017-09-07 01:11:32 -04:00
|
|
|
|
this.appController.layout.layoutRuns();
|
2017-08-22 21:25:32 -04:00
|
|
|
|
|
2017-09-07 01:11:32 -04:00
|
|
|
|
const textGlyphs = this.appController.layout.glyphStorage.allGlyphs;
|
2017-08-22 21:25:32 -04:00
|
|
|
|
const glyphPositions = new Float32Array(textGlyphs.length * 8);
|
|
|
|
|
const glyphIndices = new Uint32Array(textGlyphs.length * 6);
|
|
|
|
|
|
2017-08-31 19:11:09 -04:00
|
|
|
|
for (let glyphIndex = 0; glyphIndex < textGlyphs.length; glyphIndex++) {
|
|
|
|
|
const textGlyph = textGlyphs[glyphIndex];
|
2017-09-03 19:35:10 -04:00
|
|
|
|
const rect = textGlyph.pixelRect(this.appController.pixelsPerUnit);
|
2017-08-31 19:11:09 -04:00
|
|
|
|
glyphPositions.set([
|
|
|
|
|
rect[0], rect[3],
|
|
|
|
|
rect[2], rect[3],
|
|
|
|
|
rect[0], rect[1],
|
|
|
|
|
rect[2], rect[1],
|
|
|
|
|
], glyphIndex * 8);
|
|
|
|
|
glyphIndices.set(Array.from(QUAD_ELEMENTS).map(index => index + 4 * glyphIndex),
|
|
|
|
|
glyphIndex * 6);
|
2017-08-22 21:25:32 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.glyphPositionsBuffer = unwrapNull(this.gl.createBuffer());
|
|
|
|
|
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.glyphPositionsBuffer);
|
|
|
|
|
this.gl.bufferData(this.gl.ARRAY_BUFFER, glyphPositions, this.gl.STATIC_DRAW);
|
|
|
|
|
this.glyphElementsBuffer = unwrapNull(this.gl.createBuffer());
|
|
|
|
|
this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.glyphElementsBuffer);
|
|
|
|
|
this.gl.bufferData(this.gl.ELEMENT_ARRAY_BUFFER, glyphIndices, this.gl.STATIC_DRAW);
|
2017-08-14 19:08:45 -04:00
|
|
|
|
}
|
|
|
|
|
|
2017-08-25 18:39:13 -04:00
|
|
|
|
private buildAtlasGlyphs() {
|
2017-09-07 01:11:32 -04:00
|
|
|
|
const textGlyphs = this.appController.layout.glyphStorage.allGlyphs;
|
2017-08-31 19:11:09 -04:00
|
|
|
|
const pixelsPerUnit = this.appController.pixelsPerUnit;
|
2017-08-25 18:39:13 -04:00
|
|
|
|
|
|
|
|
|
// Only build glyphs in view.
|
2017-09-02 15:14:10 -04:00
|
|
|
|
const translation = this.camera.translation;
|
|
|
|
|
const canvasRect = glmatrix.vec4.fromValues(-translation[0],
|
|
|
|
|
-translation[1],
|
|
|
|
|
-translation[0] + this.canvas.width,
|
|
|
|
|
-translation[1] + this.canvas.height);
|
2017-08-25 18:39:13 -04:00
|
|
|
|
|
|
|
|
|
let atlasGlyphs =
|
2017-09-03 19:35:10 -04:00
|
|
|
|
textGlyphs.filter(glyph => rectsIntersect(glyph.pixelRect(pixelsPerUnit), canvasRect))
|
2017-08-31 19:11:09 -04:00
|
|
|
|
.map(textGlyph => new AtlasGlyph(textGlyph.opentypeGlyph));
|
2017-08-25 18:39:13 -04:00
|
|
|
|
atlasGlyphs.sort((a, b) => a.index - b.index);
|
|
|
|
|
atlasGlyphs = _.sortedUniqBy(atlasGlyphs, glyph => glyph.index);
|
|
|
|
|
this.appController.atlasGlyphs = atlasGlyphs;
|
|
|
|
|
|
2017-08-31 19:11:09 -04:00
|
|
|
|
this.appController.atlas.layoutGlyphs(atlasGlyphs, pixelsPerUnit);
|
2017-08-25 18:39:13 -04:00
|
|
|
|
|
2017-09-02 01:29:05 -04:00
|
|
|
|
const uniqueGlyphs = this.appController.layout.glyphStorage.uniqueGlyphs;
|
2017-08-31 19:11:09 -04:00
|
|
|
|
const uniqueGlyphIndices = uniqueGlyphs.map(glyph => glyph.index);
|
2017-08-25 18:39:13 -04:00
|
|
|
|
uniqueGlyphIndices.sort((a, b) => a - b);
|
|
|
|
|
|
|
|
|
|
// TODO(pcwalton): Regenerate the IBOs to include only the glyphs we care about.
|
2017-08-31 19:11:09 -04:00
|
|
|
|
const transforms = new Float32Array((uniqueGlyphs.length + 1) * 4);
|
|
|
|
|
|
2017-08-25 18:39:13 -04:00
|
|
|
|
for (let glyphIndex = 0; glyphIndex < atlasGlyphs.length; glyphIndex++) {
|
|
|
|
|
const glyph = atlasGlyphs[glyphIndex];
|
|
|
|
|
|
|
|
|
|
let pathID = _.sortedIndexOf(uniqueGlyphIndices, glyph.index);
|
|
|
|
|
assert(pathID >= 0, "No path ID!");
|
|
|
|
|
pathID++;
|
2017-08-22 21:25:32 -04:00
|
|
|
|
|
2017-09-03 19:35:10 -04:00
|
|
|
|
const atlasOrigin = glyph.pixelOrigin(pixelsPerUnit);
|
2017-08-24 22:30:53 -04:00
|
|
|
|
const metrics = glyph.metrics;
|
2017-08-31 19:11:09 -04:00
|
|
|
|
transforms[pathID * 4 + 0] = pixelsPerUnit;
|
|
|
|
|
transforms[pathID * 4 + 1] = pixelsPerUnit;
|
2017-09-03 19:35:10 -04:00
|
|
|
|
transforms[pathID * 4 + 2] = atlasOrigin[0];
|
|
|
|
|
transforms[pathID * 4 + 3] = atlasOrigin[1];
|
2017-08-25 18:39:13 -04:00
|
|
|
|
}
|
2017-08-22 21:25:32 -04:00
|
|
|
|
|
2017-09-07 22:01:55 -04:00
|
|
|
|
const pathTransformBufferTexture = new PathfinderBufferTexture(this.gl, 'uPathTransform');
|
|
|
|
|
pathTransformBufferTexture.upload(this.gl, transforms);
|
|
|
|
|
this.pathTransformBufferTextures = [pathTransformBufferTexture];
|
2017-08-25 18:39:13 -04:00
|
|
|
|
}
|
2017-08-22 21:25:32 -04:00
|
|
|
|
|
2017-08-25 18:39:13 -04:00
|
|
|
|
private createAtlasFramebuffer() {
|
|
|
|
|
const atlasColorTexture = this.appController.atlas.ensureTexture(this.gl);
|
|
|
|
|
this.atlasDepthTexture = createFramebufferDepthTexture(this.gl, ATLAS_SIZE);
|
|
|
|
|
this.atlasFramebuffer = createFramebuffer(this.gl,
|
|
|
|
|
this.drawBuffersExt,
|
|
|
|
|
[atlasColorTexture],
|
|
|
|
|
this.atlasDepthTexture);
|
|
|
|
|
|
|
|
|
|
// Allow the antialiasing strategy to set up framebuffers as necessary.
|
2017-09-02 01:29:05 -04:00
|
|
|
|
if (this.antialiasingStrategy != null)
|
|
|
|
|
this.antialiasingStrategy.setFramebufferSize(this);
|
2017-08-25 18:39:13 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private setGlyphTexCoords() {
|
2017-09-07 01:11:32 -04:00
|
|
|
|
const textGlyphs = this.appController.layout.glyphStorage.allGlyphs;
|
2017-08-25 18:39:13 -04:00
|
|
|
|
const atlasGlyphs = this.appController.atlasGlyphs;
|
|
|
|
|
|
|
|
|
|
const atlasGlyphIndices = atlasGlyphs.map(atlasGlyph => atlasGlyph.index);
|
|
|
|
|
|
|
|
|
|
const glyphTexCoords = new Float32Array(textGlyphs.length * 8);
|
|
|
|
|
|
|
|
|
|
const currentPosition = glmatrix.vec2.create();
|
|
|
|
|
|
|
|
|
|
for (let textGlyphIndex = 0; textGlyphIndex < textGlyphs.length; textGlyphIndex++) {
|
|
|
|
|
const textGlyph = textGlyphs[textGlyphIndex];
|
2017-08-25 20:02:11 -04:00
|
|
|
|
const textGlyphMetrics = textGlyph.metrics;
|
2017-08-25 18:39:13 -04:00
|
|
|
|
|
2017-08-25 20:02:11 -04:00
|
|
|
|
let atlasGlyphIndex = _.sortedIndexOf(atlasGlyphIndices, textGlyph.index);
|
2017-08-25 18:39:13 -04:00
|
|
|
|
if (atlasGlyphIndex < 0)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
// Set texture coordinates.
|
|
|
|
|
const atlasGlyph = atlasGlyphs[atlasGlyphIndex];
|
2017-09-03 19:35:10 -04:00
|
|
|
|
const atlasGlyphRect = atlasGlyph.pixelRect(this.appController.pixelsPerUnit);
|
2017-08-25 18:39:13 -04:00
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
glyphTexCoords.set([
|
|
|
|
|
atlasGlyphBL[0], atlasGlyphTR[1],
|
|
|
|
|
atlasGlyphTR[0], atlasGlyphTR[1],
|
|
|
|
|
atlasGlyphBL[0], atlasGlyphBL[1],
|
|
|
|
|
atlasGlyphTR[0], atlasGlyphBL[1],
|
|
|
|
|
], textGlyphIndex * 8);
|
2017-08-23 16:23:29 -04:00
|
|
|
|
}
|
2017-08-22 21:25:32 -04:00
|
|
|
|
|
2017-08-25 18:39:13 -04:00
|
|
|
|
this.glyphTexCoordsBuffer = unwrapNull(this.gl.createBuffer());
|
|
|
|
|
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.glyphTexCoordsBuffer);
|
|
|
|
|
this.gl.bufferData(this.gl.ARRAY_BUFFER, glyphTexCoords, this.gl.STATIC_DRAW);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
attachText() {
|
|
|
|
|
if (this.atlasFramebuffer == null)
|
|
|
|
|
this.createAtlasFramebuffer();
|
|
|
|
|
this.layoutGlyphs();
|
|
|
|
|
|
|
|
|
|
this.rebuildAtlasIfNecessary();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private rebuildAtlasIfNecessary() {
|
|
|
|
|
this.buildAtlasGlyphs();
|
|
|
|
|
this.setGlyphTexCoords();
|
|
|
|
|
this.setDirty();
|
2017-08-22 21:25:32 -04:00
|
|
|
|
}
|
|
|
|
|
|
2017-09-02 15:14:10 -04:00
|
|
|
|
protected onPan() {
|
|
|
|
|
this.setDirty();
|
|
|
|
|
this.rebuildAtlasIfNecessary();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected onZoom() {
|
|
|
|
|
this.appController.fontSize = this.camera.scale * INITIAL_FONT_SIZE;
|
|
|
|
|
this.setDirty();
|
2017-08-25 18:39:13 -04:00
|
|
|
|
this.rebuildAtlasIfNecessary();
|
2017-08-19 01:11:52 -04:00
|
|
|
|
}
|
|
|
|
|
|
2017-08-29 01:11:15 -04:00
|
|
|
|
private setIdentityTexScaleUniform(uniforms: UniformMap) {
|
2017-08-23 22:18:24 -04:00
|
|
|
|
this.gl.uniform2f(uniforms.uTexScale, 1.0, 1.0);
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-29 01:11:15 -04:00
|
|
|
|
protected get usedSizeFactor(): glmatrix.vec2 {
|
2017-08-23 22:18:24 -04:00
|
|
|
|
const usedSize = glmatrix.vec2.create();
|
|
|
|
|
glmatrix.vec2.div(usedSize, this.appController.atlas.usedSize, ATLAS_SIZE);
|
|
|
|
|
return usedSize;
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-28 20:18:44 -04:00
|
|
|
|
protected compositeIfNecessary() {
|
2017-08-22 21:25:32 -04:00
|
|
|
|
// Set up composite state.
|
|
|
|
|
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null);
|
|
|
|
|
this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
|
|
|
|
|
this.gl.disable(this.gl.DEPTH_TEST);
|
|
|
|
|
this.gl.disable(this.gl.BLEND);
|
2017-08-24 19:18:26 -04:00
|
|
|
|
this.gl.disable(this.gl.SCISSOR_TEST);
|
2017-08-22 21:25:32 -04:00
|
|
|
|
|
|
|
|
|
// Clear.
|
|
|
|
|
this.gl.clearColor(1.0, 1.0, 1.0, 1.0);
|
|
|
|
|
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
|
|
|
|
|
|
|
|
|
|
// Set up the composite VAO.
|
|
|
|
|
const blitProgram = this.shaderPrograms.blit;
|
|
|
|
|
const attributes = blitProgram.attributes;
|
|
|
|
|
this.gl.useProgram(blitProgram.program);
|
|
|
|
|
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.glyphPositionsBuffer);
|
|
|
|
|
this.gl.vertexAttribPointer(attributes.aPosition, 2, this.gl.FLOAT, false, 0, 0);
|
|
|
|
|
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.glyphTexCoordsBuffer);
|
|
|
|
|
this.gl.vertexAttribPointer(attributes.aTexCoord, 2, this.gl.FLOAT, false, 0, 0);
|
|
|
|
|
this.gl.enableVertexAttribArray(attributes.aPosition);
|
|
|
|
|
this.gl.enableVertexAttribArray(attributes.aTexCoord);
|
|
|
|
|
this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.glyphElementsBuffer);
|
|
|
|
|
|
|
|
|
|
// 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.canvas.width, 2.0 / this.canvas.height, 1.0]);
|
|
|
|
|
glmatrix.mat4.translate(transform,
|
|
|
|
|
transform,
|
2017-09-02 15:14:10 -04:00
|
|
|
|
[this.camera.translation[0], this.camera.translation[1], 0.0]);
|
2017-08-22 21:25:32 -04:00
|
|
|
|
|
|
|
|
|
// Blit.
|
|
|
|
|
this.gl.uniformMatrix4fv(blitProgram.uniforms.uTransform, false, transform);
|
|
|
|
|
this.gl.activeTexture(this.gl.TEXTURE0);
|
2017-08-23 16:23:29 -04:00
|
|
|
|
this.gl.bindTexture(this.gl.TEXTURE_2D, this.appController.atlas.ensureTexture(this.gl));
|
2017-08-22 21:25:32 -04:00
|
|
|
|
this.gl.uniform1i(blitProgram.uniforms.uSource, 0);
|
2017-08-23 22:18:24 -04:00
|
|
|
|
this.setIdentityTexScaleUniform(blitProgram.uniforms);
|
2017-08-25 20:02:11 -04:00
|
|
|
|
this.gl.drawElements(this.gl.TRIANGLES,
|
2017-09-07 01:11:32 -04:00
|
|
|
|
this.appController.layout.glyphStorage.allGlyphs.length * 6,
|
2017-08-25 20:02:11 -04:00
|
|
|
|
this.gl.UNSIGNED_INT,
|
|
|
|
|
0);
|
2017-08-22 21:25:32 -04:00
|
|
|
|
}
|
|
|
|
|
|
2017-08-24 15:08:58 -04:00
|
|
|
|
get bgColor(): glmatrix.vec4 {
|
|
|
|
|
return glmatrix.vec4.fromValues(1.0, 1.0, 1.0, 1.0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get fgColor(): glmatrix.vec4 {
|
|
|
|
|
return glmatrix.vec4.fromValues(0.0, 0.0, 0.0, 1.0);
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-26 16:47:18 -04:00
|
|
|
|
get destFramebuffer(): WebGLFramebuffer {
|
|
|
|
|
return this.atlasFramebuffer;
|
|
|
|
|
}
|
2017-08-19 12:57:54 -04:00
|
|
|
|
|
2017-08-26 16:47:18 -04:00
|
|
|
|
get destAllocatedSize(): glmatrix.vec2 {
|
|
|
|
|
return ATLAS_SIZE;
|
|
|
|
|
}
|
2017-08-19 12:57:54 -04:00
|
|
|
|
|
2017-08-26 16:47:18 -04:00
|
|
|
|
get destUsedSize(): glmatrix.vec2 {
|
|
|
|
|
return this.appController.atlas.usedSize;
|
|
|
|
|
}
|
2017-08-19 12:57:54 -04:00
|
|
|
|
|
2017-09-06 19:32:11 -04:00
|
|
|
|
protected createAAStrategy(aaType: AntialiasingStrategyName,
|
|
|
|
|
aaLevel: number,
|
|
|
|
|
subpixelAA: boolean):
|
2017-08-28 20:18:44 -04:00
|
|
|
|
AntialiasingStrategy {
|
2017-09-06 19:32:11 -04:00
|
|
|
|
return new (ANTIALIASING_STRATEGIES[aaType])(aaLevel, subpixelAA);
|
2017-08-28 20:18:44 -04:00
|
|
|
|
}
|
2017-08-19 12:57:54 -04:00
|
|
|
|
|
2017-08-28 20:18:44 -04:00
|
|
|
|
protected updateTimings(timings: Timings) {
|
|
|
|
|
this.appController.updateTimings(timings);
|
|
|
|
|
}
|
2017-08-19 12:57:54 -04:00
|
|
|
|
|
2017-08-29 15:29:16 -04:00
|
|
|
|
protected get worldTransform() {
|
|
|
|
|
return glmatrix.mat4.create();
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-05 22:47:19 -04:00
|
|
|
|
protected get directCurveProgramName(): keyof ShaderMap<void> {
|
|
|
|
|
return 'directCurve';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected get directInteriorProgramName(): keyof ShaderMap<void> {
|
|
|
|
|
return 'directInterior';
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-22 21:25:32 -04:00
|
|
|
|
atlasFramebuffer: WebGLFramebuffer;
|
|
|
|
|
atlasDepthTexture: WebGLTexture;
|
|
|
|
|
|
|
|
|
|
glyphPositionsBuffer: WebGLBuffer;
|
|
|
|
|
glyphTexCoordsBuffer: WebGLBuffer;
|
|
|
|
|
glyphElementsBuffer: WebGLBuffer;
|
|
|
|
|
|
2017-08-26 15:54:25 -04:00
|
|
|
|
appController: TextDemoController;
|
2017-09-02 15:14:10 -04:00
|
|
|
|
|
|
|
|
|
camera: OrthographicCamera;
|
2017-08-12 00:26:25 -04:00
|
|
|
|
}
|
|
|
|
|
|
2017-08-15 16:38:54 -04:00
|
|
|
|
interface AntialiasingStrategyTable {
|
2017-08-15 18:57:52 -04:00
|
|
|
|
none: typeof NoAAStrategy;
|
|
|
|
|
ssaa: typeof SSAAStrategy;
|
2017-08-16 01:09:09 -04:00
|
|
|
|
ecaa: typeof ECAAStrategy;
|
2017-08-15 16:38:54 -04:00
|
|
|
|
}
|
|
|
|
|
|
2017-08-23 16:23:29 -04:00
|
|
|
|
class Atlas {
|
|
|
|
|
constructor() {
|
|
|
|
|
this._texture = null;
|
|
|
|
|
this._usedSize = glmatrix.vec2.create();
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-31 19:11:09 -04:00
|
|
|
|
layoutGlyphs(glyphs: AtlasGlyph[], pixelsPerUnit: number) {
|
2017-09-03 16:12:33 -04:00
|
|
|
|
let nextOrigin = glmatrix.vec2.fromValues(1.0, 1.0);
|
|
|
|
|
let shelfBottom = 2.0;
|
2017-08-23 16:23:29 -04:00
|
|
|
|
|
|
|
|
|
for (const glyph of glyphs) {
|
2017-08-31 19:11:09 -04:00
|
|
|
|
// Place the glyph, and advance the origin.
|
2017-09-03 19:35:10 -04:00
|
|
|
|
glyph.setPixelLowerLeft(nextOrigin, pixelsPerUnit);
|
|
|
|
|
nextOrigin[0] = glyph.pixelRect(pixelsPerUnit)[2] + 1.0;
|
2017-08-25 15:20:36 -04:00
|
|
|
|
|
2017-08-31 19:11:09 -04:00
|
|
|
|
// If the glyph overflowed the shelf, make a new one and reposition the glyph.
|
|
|
|
|
if (nextOrigin[0] > ATLAS_SIZE[0]) {
|
2017-09-03 16:12:33 -04:00
|
|
|
|
nextOrigin = glmatrix.vec2.fromValues(1.0, shelfBottom + 1.0);
|
2017-09-03 19:35:10 -04:00
|
|
|
|
glyph.setPixelLowerLeft(nextOrigin, pixelsPerUnit);
|
|
|
|
|
nextOrigin[0] = glyph.pixelRect(pixelsPerUnit)[2] + 1.0;
|
2017-08-31 19:11:09 -04:00
|
|
|
|
}
|
2017-08-23 16:23:29 -04:00
|
|
|
|
|
2017-08-31 19:11:09 -04:00
|
|
|
|
// Grow the shelf as necessary.
|
2017-09-03 19:35:10 -04:00
|
|
|
|
shelfBottom = Math.max(shelfBottom, glyph.pixelRect(pixelsPerUnit)[3] + 1.0);
|
2017-08-23 16:23:29 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FIXME(pcwalton): Could be more precise if we don't have a full row.
|
|
|
|
|
this._usedSize = glmatrix.vec2.fromValues(ATLAS_SIZE[0], shelfBottom);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ensureTexture(gl: WebGLRenderingContext): WebGLTexture {
|
|
|
|
|
if (this._texture != null)
|
|
|
|
|
return this._texture;
|
|
|
|
|
|
|
|
|
|
const texture = unwrapNull(gl.createTexture());
|
|
|
|
|
this._texture = texture;
|
|
|
|
|
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
|
|
|
gl.texImage2D(gl.TEXTURE_2D,
|
|
|
|
|
0,
|
|
|
|
|
gl.RGBA,
|
|
|
|
|
ATLAS_SIZE[0],
|
|
|
|
|
ATLAS_SIZE[1],
|
|
|
|
|
0,
|
|
|
|
|
gl.RGBA,
|
|
|
|
|
gl.UNSIGNED_BYTE,
|
|
|
|
|
null);
|
|
|
|
|
setTextureParameters(gl, gl.NEAREST);
|
|
|
|
|
|
|
|
|
|
return texture;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get usedSize(): glmatrix.vec2 {
|
|
|
|
|
return this._usedSize;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _texture: WebGLTexture | null;
|
|
|
|
|
private _usedSize: Size2D;
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-31 19:11:09 -04:00
|
|
|
|
class AtlasGlyph extends PathfinderGlyph {
|
|
|
|
|
constructor(glyph: opentype.Glyph) {
|
|
|
|
|
super(glyph);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class GlyphInstance extends PathfinderGlyph {
|
|
|
|
|
constructor(glyph: opentype.Glyph) {
|
|
|
|
|
super(glyph);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-15 16:38:54 -04:00
|
|
|
|
const ANTIALIASING_STRATEGIES: AntialiasingStrategyTable = {
|
|
|
|
|
none: NoAAStrategy,
|
|
|
|
|
ssaa: SSAAStrategy,
|
2017-08-24 15:08:58 -04:00
|
|
|
|
ecaa: ECAAMonochromeStrategy,
|
2017-08-15 16:38:54 -04:00
|
|
|
|
};
|
|
|
|
|
|
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();
|