2174 lines
85 KiB
TypeScript
2174 lines
85 KiB
TypeScript
// pathfinder/client/src/text.ts
|
||
//
|
||
// Copyright © 2017 The Pathfinder Project Developers.
|
||
//
|
||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||
// option. This file may not be copied, modified, or distributed
|
||
// except according to those terms.
|
||
|
||
import * as _ from 'lodash';
|
||
import * as base64js from 'base64-js';
|
||
import * as glmatrix from 'gl-matrix';
|
||
import * as opentype from 'opentype.js';
|
||
|
||
import AppController from './app-controller';
|
||
import {SHADER_NAMES, ShaderMap, ShaderProgramSource} from './shader-loader';
|
||
|
||
const TEXT: string =
|
||
`’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.`;
|
||
|
||
const INITIAL_FONT_SIZE: number = 72.0;
|
||
|
||
const SCALE_FACTOR: number = 1.0 / 100.0;
|
||
|
||
const TIME_INTERVAL_DELAY: number = 32;
|
||
|
||
const PARTITION_FONT_ENDPOINT_URL: string = "/partition-font";
|
||
|
||
const UINT32_SIZE: number = 4;
|
||
|
||
const B_POSITION_SIZE: number = 8;
|
||
|
||
const B_PATH_INDEX_SIZE: number = 2;
|
||
|
||
const B_LOOP_BLINN_DATA_SIZE: number = 4;
|
||
const B_LOOP_BLINN_DATA_TEX_COORD_OFFSET: number = 0;
|
||
const B_LOOP_BLINN_DATA_SIGN_OFFSET: number = 2;
|
||
|
||
const B_QUAD_SIZE: number = 4 * 8;
|
||
const B_QUAD_UPPER_INDICES_OFFSET: number = 0;
|
||
const B_QUAD_LOWER_INDICES_OFFSET: number = 4 * 4;
|
||
|
||
const ATLAS_SIZE: glmatrix.vec2 = glmatrix.vec2.fromValues(3072, 3072);
|
||
|
||
interface UnlinkedShaderProgram {
|
||
vertex: WebGLShader;
|
||
fragment: WebGLShader;
|
||
}
|
||
|
||
type Matrix4D = Float32Array;
|
||
|
||
type Rect = glmatrix.vec4;
|
||
|
||
interface Point2D {
|
||
x: number;
|
||
y: number;
|
||
}
|
||
|
||
type Size2D = glmatrix.vec2;
|
||
|
||
interface UniformMap {
|
||
[uniformName: string]: WebGLUniformLocation;
|
||
}
|
||
|
||
interface AttributeMap {
|
||
[attributeName: string]: number;
|
||
}
|
||
|
||
interface UpperAndLower<T> {
|
||
upper: T;
|
||
lower: T;
|
||
}
|
||
|
||
interface AntialiasingStrategy {
|
||
// Prepares any OpenGL data. This is only called on startup and canvas resize.
|
||
init(view: PathfinderView): void;
|
||
|
||
// Uploads any mesh data. This is called whenever a new set of meshes is supplied.
|
||
attachMeshes(view: PathfinderView): void;
|
||
|
||
// This is called whenever the framebuffer has changed.
|
||
setFramebufferSize(view: PathfinderView, framebufferSize: Size2D): void;
|
||
|
||
// Returns the transformation matrix that should be applied when directly rendering.
|
||
transform(): glmatrix.mat4;
|
||
|
||
// Called before direct rendering.
|
||
//
|
||
// Typically, this redirects direct rendering to a framebuffer of some sort.
|
||
prepare(view: PathfinderView): void;
|
||
|
||
// Called after direct rendering.
|
||
//
|
||
// This usually performs the actual antialiasing and blits to the real framebuffer.
|
||
resolve(view: PathfinderView): void;
|
||
|
||
// True if direct rendering should occur.
|
||
shouldRenderDirect: boolean;
|
||
}
|
||
|
||
type ShaderType = number;
|
||
|
||
type ShaderTypeName = 'vertex' | 'fragment';
|
||
|
||
type WebGLQuery = any;
|
||
|
||
type WebGLVertexArrayObject = any;
|
||
|
||
const QUAD_POSITIONS: Float32Array = new Float32Array([
|
||
0.0, 1.0,
|
||
1.0, 1.0,
|
||
0.0, 0.0,
|
||
1.0, 0.0,
|
||
]);
|
||
|
||
const QUAD_TEX_COORDS: Float32Array = new Float32Array([
|
||
0.0, 1.0,
|
||
1.0, 1.0,
|
||
0.0, 0.0,
|
||
1.0, 0.0,
|
||
]);
|
||
|
||
const QUAD_ELEMENTS: Uint8Array = new Uint8Array([2, 0, 1, 1, 3, 2]);
|
||
|
||
// `opentype.js` monkey patches
|
||
|
||
declare module 'opentype.js' {
|
||
interface Font {
|
||
isSupported(): boolean;
|
||
}
|
||
interface Glyph {
|
||
getIndex(): number;
|
||
}
|
||
}
|
||
|
||
opentype.Font.prototype.isSupported = function() {
|
||
return (this as any).supported;
|
||
}
|
||
|
||
// Various utility functions
|
||
|
||
function assert(value: boolean, message: string) {
|
||
if (!value)
|
||
throw new PathfinderError(message);
|
||
}
|
||
|
||
function expectNotNull<T>(value: T | null, message: string): T {
|
||
if (value === null)
|
||
throw new PathfinderError(message);
|
||
return value;
|
||
}
|
||
|
||
function expectNotUndef<T>(value: T | undefined, message: string): T {
|
||
if (value === undefined)
|
||
throw new PathfinderError(message);
|
||
return value;
|
||
}
|
||
|
||
function unwrapNull<T>(value: T | null): T {
|
||
return expectNotNull(value, "Unexpected null!");
|
||
}
|
||
|
||
function unwrapUndef<T>(value: T | undefined): T {
|
||
return expectNotUndef(value, "Unexpected `undefined`!");
|
||
}
|
||
|
||
/// 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];
|
||
}
|
||
|
||
class PathfinderError extends Error {
|
||
constructor(message?: string | undefined) {
|
||
super(message);
|
||
}
|
||
}
|
||
|
||
// GL utilities
|
||
|
||
function createFramebufferColorTexture(gl: WebGLRenderingContext, size: Size2D): WebGLTexture {
|
||
// Firefox seems to have a bug whereby textures don't get marked as initialized when cleared
|
||
// if they're anything other than the first attachment of an FBO. To work around this, supply
|
||
// zero data explicitly when initializing the texture.
|
||
const zeroes = new Uint8Array(size[0] * size[1] * UINT32_SIZE);
|
||
const texture = unwrapNull(gl.createTexture());
|
||
gl.activeTexture(gl.TEXTURE0);
|
||
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||
gl.texImage2D(gl.TEXTURE_2D,
|
||
0,
|
||
gl.RGBA,
|
||
size[0],
|
||
size[1],
|
||
0,
|
||
gl.RGBA,
|
||
gl.UNSIGNED_BYTE,
|
||
zeroes);
|
||
setTextureParameters(gl, gl.NEAREST);
|
||
return texture;
|
||
}
|
||
|
||
function createFramebufferDepthTexture(gl: WebGLRenderingContext, size: Size2D): WebGLTexture {
|
||
const texture = unwrapNull(gl.createTexture());
|
||
gl.activeTexture(gl.TEXTURE0);
|
||
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||
gl.texImage2D(gl.TEXTURE_2D,
|
||
0,
|
||
gl.DEPTH_COMPONENT,
|
||
size[0],
|
||
size[1],
|
||
0,
|
||
gl.DEPTH_COMPONENT,
|
||
gl.UNSIGNED_INT,
|
||
null);
|
||
setTextureParameters(gl, gl.NEAREST);
|
||
return texture;
|
||
}
|
||
|
||
function setTextureParameters(gl: WebGLRenderingContext, filter: number) {
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
|
||
}
|
||
|
||
function createFramebuffer(gl: WebGLRenderingContext,
|
||
drawBuffersExt: any,
|
||
colorAttachments: WebGLTexture[],
|
||
depthAttachment: WebGLTexture | null):
|
||
WebGLFramebuffer {
|
||
const framebuffer = unwrapNull(gl.createFramebuffer());
|
||
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
||
|
||
const colorAttachmentCount = colorAttachments.length;
|
||
for (let colorAttachmentIndex = 0;
|
||
colorAttachmentIndex < colorAttachmentCount;
|
||
colorAttachmentIndex++) {
|
||
gl.framebufferTexture2D(gl.FRAMEBUFFER,
|
||
drawBuffersExt[`COLOR_ATTACHMENT${colorAttachmentIndex}_WEBGL`],
|
||
gl.TEXTURE_2D,
|
||
colorAttachments[colorAttachmentIndex],
|
||
0);
|
||
}
|
||
|
||
if (depthAttachment != null) {
|
||
gl.framebufferTexture2D(gl.FRAMEBUFFER,
|
||
gl.DEPTH_ATTACHMENT,
|
||
gl.TEXTURE_2D,
|
||
depthAttachment,
|
||
0);
|
||
}
|
||
|
||
assert(gl.checkFramebufferStatus(gl.FRAMEBUFFER) == gl.FRAMEBUFFER_COMPLETE,
|
||
"Framebuffer was incomplete!");
|
||
return framebuffer;
|
||
}
|
||
|
||
function initQuadVAO(view: PathfinderView, attributes: any) {
|
||
view.gl.bindBuffer(view.gl.ARRAY_BUFFER, view.quadPositionsBuffer);
|
||
view.gl.vertexAttribPointer(attributes.aPosition, 2, view.gl.FLOAT, false, 0, 0);
|
||
view.gl.bindBuffer(view.gl.ARRAY_BUFFER, view.quadTexCoordsBuffer);
|
||
view.gl.vertexAttribPointer(attributes.aTexCoord, 2, view.gl.FLOAT, false, 0, 0);
|
||
view.gl.enableVertexAttribArray(attributes.aPosition);
|
||
view.gl.enableVertexAttribArray(attributes.aTexCoord);
|
||
view.gl.bindBuffer(view.gl.ELEMENT_ARRAY_BUFFER, view.quadElementsBuffer);
|
||
}
|
||
|
||
interface Meshes<T> {
|
||
readonly bQuads: T;
|
||
readonly bVertexPositions: T;
|
||
readonly bVertexPathIDs: T;
|
||
readonly bVertexLoopBlinnData: T;
|
||
readonly coverInteriorIndices: T;
|
||
readonly coverCurveIndices: T;
|
||
readonly edgeUpperLineIndices: T;
|
||
readonly edgeLowerLineIndices: T;
|
||
readonly edgeUpperCurveIndices: T;
|
||
readonly edgeLowerCurveIndices: T;
|
||
}
|
||
|
||
type BufferType = 'ARRAY_BUFFER' | 'ELEMENT_ARRAY_BUFFER';
|
||
|
||
const BUFFER_TYPES: Meshes<BufferType> = {
|
||
bQuads: 'ARRAY_BUFFER',
|
||
bVertexPositions: 'ARRAY_BUFFER',
|
||
bVertexPathIDs: 'ARRAY_BUFFER',
|
||
bVertexLoopBlinnData: 'ARRAY_BUFFER',
|
||
coverInteriorIndices: 'ELEMENT_ARRAY_BUFFER',
|
||
coverCurveIndices: 'ELEMENT_ARRAY_BUFFER',
|
||
edgeUpperLineIndices: 'ARRAY_BUFFER',
|
||
edgeLowerLineIndices: 'ARRAY_BUFFER',
|
||
edgeUpperCurveIndices: 'ARRAY_BUFFER',
|
||
edgeLowerCurveIndices: 'ARRAY_BUFFER',
|
||
};
|
||
|
||
class PathfinderMeshData implements Meshes<ArrayBuffer> {
|
||
constructor(encodedResponse: string) {
|
||
const response = JSON.parse(encodedResponse);
|
||
if (!('Ok' in response))
|
||
throw new PathfinderError("Failed to partition the font!");
|
||
const meshes = response.Ok;
|
||
for (const bufferName of Object.keys(BUFFER_TYPES) as Array<keyof Meshes<void>>)
|
||
this[bufferName] = base64js.toByteArray(meshes[bufferName]).buffer as ArrayBuffer;
|
||
|
||
this.bQuadCount = this.bQuads.byteLength / B_QUAD_SIZE;
|
||
this.edgeUpperLineIndexCount = this.edgeUpperLineIndices.byteLength / 8;
|
||
this.edgeLowerLineIndexCount = this.edgeLowerLineIndices.byteLength / 8;
|
||
this.edgeUpperCurveIndexCount = this.edgeUpperCurveIndices.byteLength / 16;
|
||
this.edgeLowerCurveIndexCount = this.edgeLowerCurveIndices.byteLength / 16;
|
||
}
|
||
|
||
readonly bQuads: ArrayBuffer;
|
||
readonly bVertexPositions: ArrayBuffer;
|
||
readonly bVertexPathIDs: ArrayBuffer;
|
||
readonly bVertexLoopBlinnData: ArrayBuffer;
|
||
readonly coverInteriorIndices: ArrayBuffer;
|
||
readonly coverCurveIndices: ArrayBuffer;
|
||
readonly edgeUpperLineIndices: ArrayBuffer;
|
||
readonly edgeLowerLineIndices: ArrayBuffer;
|
||
readonly edgeUpperCurveIndices: ArrayBuffer;
|
||
readonly edgeLowerCurveIndices: ArrayBuffer;
|
||
|
||
readonly bQuadCount: number;
|
||
readonly edgeUpperLineIndexCount: number;
|
||
readonly edgeLowerLineIndexCount: number;
|
||
readonly edgeUpperCurveIndexCount: number;
|
||
readonly edgeLowerCurveIndexCount: number;
|
||
}
|
||
|
||
class PathfinderMeshBuffers implements Meshes<WebGLBuffer> {
|
||
constructor(gl: WebGLRenderingContext, meshData: PathfinderMeshData) {
|
||
for (const bufferName of Object.keys(BUFFER_TYPES) as Array<keyof PathfinderMeshBuffers>) {
|
||
const bufferType = gl[BUFFER_TYPES[bufferName]];
|
||
const buffer = expectNotNull(gl.createBuffer(), "Failed to create buffer!");
|
||
gl.bindBuffer(bufferType, buffer);
|
||
gl.bufferData(bufferType, meshData[bufferName], gl.STATIC_DRAW);
|
||
this[bufferName] = buffer;
|
||
}
|
||
}
|
||
|
||
readonly bQuads: WebGLBuffer;
|
||
readonly bVertexPositions: WebGLBuffer;
|
||
readonly bVertexPathIDs: WebGLBuffer;
|
||
readonly bVertexLoopBlinnData: WebGLBuffer;
|
||
readonly coverInteriorIndices: WebGLBuffer;
|
||
readonly coverCurveIndices: WebGLBuffer;
|
||
readonly edgeUpperLineIndices: WebGLBuffer;
|
||
readonly edgeUpperCurveIndices: WebGLBuffer;
|
||
readonly edgeLowerLineIndices: WebGLBuffer;
|
||
readonly edgeLowerCurveIndices: WebGLBuffer;
|
||
}
|
||
|
||
class TextDemoController extends AppController<PathfinderView> {
|
||
constructor() {
|
||
super();
|
||
this._atlas = new Atlas;
|
||
}
|
||
|
||
start() {
|
||
super.start();
|
||
|
||
this.fontSize = INITIAL_FONT_SIZE;
|
||
|
||
this.fpsLabel = unwrapNull(document.getElementById('pf-fps-label'));
|
||
|
||
const canvas = document.getElementById('pf-canvas') as HTMLCanvasElement;
|
||
this.loadFontButton = document.getElementById('pf-load-font-button') as HTMLInputElement;
|
||
this.loadFontButton.addEventListener('change', () => this.loadFont(), false);
|
||
|
||
this.aaLevelSelect = document.getElementById('pf-aa-level-select') as HTMLSelectElement;
|
||
this.aaLevelSelect.addEventListener('change', () => this.updateAALevel(), false);
|
||
this.updateAALevel();
|
||
}
|
||
|
||
protected createView(canvas: HTMLCanvasElement,
|
||
commonShaderSource: string,
|
||
shaderSources: ShaderMap<ShaderProgramSource>) {
|
||
return new PathfinderView(this, canvas, commonShaderSource, shaderSources);
|
||
}
|
||
|
||
private loadFont() {
|
||
const file = expectNotNull(this.loadFontButton.files, "No file selected!")[0];
|
||
const reader = new FileReader;
|
||
reader.addEventListener('loadend', () => {
|
||
this.fontData = reader.result;
|
||
this.fontLoaded();
|
||
}, false);
|
||
reader.readAsArrayBuffer(file);
|
||
}
|
||
|
||
private updateAALevel() {
|
||
const selectedOption = this.aaLevelSelect.selectedOptions[0];
|
||
const aaType = unwrapUndef(selectedOption.dataset.pfType) as
|
||
keyof AntialiasingStrategyTable;
|
||
const aaLevel = parseInt(unwrapUndef(selectedOption.dataset.pfLevel));
|
||
this.view.then(view => view.setAntialiasingOptions(aaType, aaLevel));
|
||
}
|
||
|
||
private fontLoaded() {
|
||
this.font = opentype.parse(this.fontData);
|
||
if (!this.font.isSupported())
|
||
throw new PathfinderError("The font type is unsupported.");
|
||
|
||
// 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.
|
||
const request = {
|
||
otf: 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,
|
||
};
|
||
|
||
window.fetch(PARTITION_FONT_ENDPOINT_URL, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(request),
|
||
}).then(response => response.text()).then(encodedMeshes => {
|
||
this.meshes = new PathfinderMeshData(encodedMeshes);
|
||
this.meshesReceived();
|
||
});
|
||
}
|
||
|
||
private meshesReceived() {
|
||
this.view.then(view => {
|
||
view.attachText();
|
||
view.uploadPathData(this.uniqueGlyphs.length);
|
||
view.attachMeshes(this.meshes);
|
||
})
|
||
}
|
||
|
||
scaleFontSize(scale: number) {
|
||
this.setFontSize(scale * this.fontSize);
|
||
}
|
||
|
||
private setFontSize(newPixelsPerEm: number) {
|
||
this.fontSize = newPixelsPerEm;
|
||
this.view.then(view => view.attachText());
|
||
}
|
||
|
||
updateTiming(newTimes: {atlasRendering: number, compositing: number}) {
|
||
this.fpsLabel.innerHTML =
|
||
`${newTimes.atlasRendering} ms atlas, ${newTimes.compositing} ms compositing`;
|
||
}
|
||
|
||
get atlas(): Atlas {
|
||
return this._atlas;
|
||
}
|
||
|
||
private loadFontButton: HTMLInputElement;
|
||
private aaLevelSelect: HTMLSelectElement;
|
||
private fpsLabel: HTMLElement;
|
||
|
||
private fontData: ArrayBuffer;
|
||
font: opentype.Font;
|
||
lineGlyphs: TextGlyph[][];
|
||
textGlyphs: TextGlyph[];
|
||
uniqueGlyphs: PathfinderGlyph[];
|
||
|
||
private _atlas: Atlas;
|
||
atlasGlyphs: AtlasGlyph[];
|
||
|
||
private meshes: PathfinderMeshData;
|
||
|
||
/// The font size in pixels per em.
|
||
fontSize: number;
|
||
}
|
||
|
||
class PathfinderView {
|
||
constructor(appController: TextDemoController,
|
||
canvas: HTMLCanvasElement,
|
||
commonShaderSource: string,
|
||
shaderSources: ShaderMap<ShaderProgramSource>) {
|
||
this.appController = appController;
|
||
|
||
this.translation = glmatrix.vec2.create();
|
||
|
||
this.canvas = canvas;
|
||
this.canvas.addEventListener('wheel', event => this.onWheel(event), false);
|
||
|
||
this.initContext();
|
||
|
||
this.antialiasingStrategy = new NoAAStrategy(0);
|
||
|
||
const shaderSource = this.compileShaders(commonShaderSource, shaderSources);
|
||
this.shaderPrograms = this.linkShaders(shaderSource);
|
||
|
||
this.atlasTransformBuffer = new PathfinderBufferTexture(this.gl, 'uPathTransform');
|
||
this.pathColorsBufferTexture = new PathfinderBufferTexture(this.gl, 'uPathColors');
|
||
|
||
window.addEventListener('resize', () => this.resizeToFit(), false);
|
||
this.resizeToFit();
|
||
}
|
||
|
||
setAntialiasingOptions(aaType: keyof AntialiasingStrategyTable, aaLevel: number) {
|
||
this.antialiasingStrategy = new (ANTIALIASING_STRATEGIES[aaType])(aaLevel);
|
||
|
||
let canvas = this.canvas;
|
||
this.antialiasingStrategy.init(this);
|
||
this.antialiasingStrategy.setFramebufferSize(this, ATLAS_SIZE);
|
||
if (this.meshData != null)
|
||
this.antialiasingStrategy.attachMeshes(this);
|
||
|
||
this.setDirty();
|
||
}
|
||
|
||
private initContext() {
|
||
// Initialize the OpenGL context.
|
||
this.gl = expectNotNull(this.canvas.getContext('webgl', { antialias: false, depth: true }),
|
||
"Failed to initialize WebGL! Check that your browser supports it.");
|
||
this.drawBuffersExt = this.gl.getExtension('WEBGL_draw_buffers');
|
||
this.colorBufferHalfFloatExt = this.gl.getExtension('EXT_color_buffer_half_float');
|
||
this.instancedArraysExt = this.gl.getExtension('ANGLE_instanced_arrays');
|
||
this.textureHalfFloatExt = this.gl.getExtension('OES_texture_half_float');
|
||
this.timerQueryExt = this.gl.getExtension('EXT_disjoint_timer_query');
|
||
this.vertexArrayObjectExt = this.gl.getExtension('OES_vertex_array_object');
|
||
this.gl.getExtension('EXT_frag_depth');
|
||
this.gl.getExtension('OES_element_index_uint');
|
||
this.gl.getExtension('OES_texture_float');
|
||
this.gl.getExtension('WEBGL_depth_texture');
|
||
|
||
// Set up our timer queries for profiling.
|
||
this.atlasRenderingTimerQuery = this.timerQueryExt.createQueryEXT();
|
||
this.compositingTimerQuery = this.timerQueryExt.createQueryEXT();
|
||
|
||
// Upload quad buffers.
|
||
this.quadPositionsBuffer = unwrapNull(this.gl.createBuffer());
|
||
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.quadPositionsBuffer);
|
||
this.gl.bufferData(this.gl.ARRAY_BUFFER, QUAD_POSITIONS, this.gl.STATIC_DRAW);
|
||
this.quadTexCoordsBuffer = unwrapNull(this.gl.createBuffer());
|
||
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.quadTexCoordsBuffer);
|
||
this.gl.bufferData(this.gl.ARRAY_BUFFER, QUAD_TEX_COORDS, this.gl.STATIC_DRAW);
|
||
this.quadElementsBuffer = unwrapNull(this.gl.createBuffer());
|
||
this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.quadElementsBuffer);
|
||
this.gl.bufferData(this.gl.ELEMENT_ARRAY_BUFFER, QUAD_ELEMENTS, this.gl.STATIC_DRAW);
|
||
}
|
||
|
||
private compileShaders(commonSource: string, shaderSources: ShaderMap<ShaderProgramSource>):
|
||
ShaderMap<UnlinkedShaderProgram> {
|
||
let shaders: Partial<ShaderMap<Partial<UnlinkedShaderProgram>>> = {};
|
||
|
||
for (const shaderKey of SHADER_NAMES) {
|
||
for (const typeName of ['vertex', 'fragment'] as Array<ShaderTypeName>) {
|
||
const type = {
|
||
vertex: this.gl.VERTEX_SHADER,
|
||
fragment: this.gl.FRAGMENT_SHADER,
|
||
}[typeName];
|
||
|
||
const source = shaderSources[shaderKey][typeName];
|
||
const shader = this.gl.createShader(type);
|
||
if (shader == null)
|
||
throw new PathfinderError("Failed to create shader!");
|
||
|
||
this.gl.shaderSource(shader, commonSource + "\n#line 1\n" + source);
|
||
this.gl.compileShader(shader);
|
||
if (this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS) == 0) {
|
||
const infoLog = this.gl.getShaderInfoLog(shader);
|
||
throw new PathfinderError(`Failed to compile ${typeName} shader ` +
|
||
`"${shaderKey}":\n${infoLog}`);
|
||
}
|
||
|
||
if (shaders[shaderKey] == null)
|
||
shaders[shaderKey] = {};
|
||
shaders[shaderKey]![typeName] = shader;
|
||
}
|
||
}
|
||
|
||
return shaders as ShaderMap<UnlinkedShaderProgram>;
|
||
}
|
||
|
||
private linkShaders(shaders: ShaderMap<UnlinkedShaderProgram>):
|
||
ShaderMap<PathfinderShaderProgram> {
|
||
let shaderProgramMap: Partial<ShaderMap<PathfinderShaderProgram>> = {};
|
||
for (const shaderName of Object.keys(shaders) as Array<keyof ShaderMap<string>>) {
|
||
shaderProgramMap[shaderName] = new PathfinderShaderProgram(this.gl,
|
||
shaderName,
|
||
shaders[shaderName]);
|
||
}
|
||
return shaderProgramMap as ShaderMap<PathfinderShaderProgram>;
|
||
}
|
||
|
||
uploadPathData(pathCount: number) {
|
||
const pathColors = new Uint8Array(4 * (pathCount + 1));
|
||
for (let pathIndex = 0; pathIndex < pathCount; pathIndex++) {
|
||
for (let channel = 0; channel < 3; channel++)
|
||
pathColors[(pathIndex + 1) * 4 + channel] = 0x00; // RGB
|
||
pathColors[(pathIndex + 1) * 4 + 3] = 0xff; // alpha
|
||
}
|
||
|
||
this.pathColorsBufferTexture.upload(this.gl, pathColors);
|
||
}
|
||
|
||
attachMeshes(meshes: PathfinderMeshData) {
|
||
this.meshData = meshes;
|
||
this.meshes = new PathfinderMeshBuffers(this.gl, meshes);
|
||
this.antialiasingStrategy.attachMeshes(this);
|
||
|
||
this.setDirty();
|
||
}
|
||
|
||
/// Lays out glyphs on the canvas.
|
||
private layoutGlyphs() {
|
||
const lineGlyphs = this.appController.lineGlyphs;
|
||
const textGlyphs = this.appController.textGlyphs;
|
||
|
||
const font = this.appController.font;
|
||
this.pixelsPerUnit = this.appController.fontSize / font.unitsPerEm;
|
||
|
||
const glyphPositions = new Float32Array(textGlyphs.length * 8);
|
||
const glyphIndices = new Uint32Array(textGlyphs.length * 6);
|
||
|
||
const os2Table = font.tables.os2;
|
||
const lineHeight = (os2Table.sTypoAscender - os2Table.sTypoDescender +
|
||
os2Table.sTypoLineGap) * this.pixelsPerUnit;
|
||
|
||
const currentPosition = glmatrix.vec2.create();
|
||
|
||
let glyphIndex = 0;
|
||
for (const line of lineGlyphs) {
|
||
for (let lineCharIndex = 0; lineCharIndex < line.length; lineCharIndex++) {
|
||
const textGlyph = textGlyphs[glyphIndex];
|
||
const glyphMetrics = textGlyph.metrics;
|
||
|
||
// 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.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);
|
||
}
|
||
|
||
private buildAtlasGlyphs() {
|
||
const textGlyphs = this.appController.textGlyphs;
|
||
|
||
// Only build glyphs in view.
|
||
const canvasRect = glmatrix.vec4.fromValues(-this.translation[0],
|
||
-this.translation[1],
|
||
-this.translation[0] + this.canvas.width,
|
||
-this.translation[1] + this.canvas.height);
|
||
|
||
let atlasGlyphs =
|
||
textGlyphs.filter(textGlyph => rectsIntersect(textGlyph.canvasRect, canvasRect))
|
||
.map(textGlyph => new AtlasGlyph(textGlyph));
|
||
atlasGlyphs.sort((a, b) => a.index - b.index);
|
||
atlasGlyphs = _.sortedUniqBy(atlasGlyphs, glyph => glyph.index);
|
||
this.appController.atlasGlyphs = atlasGlyphs;
|
||
|
||
const fontSize = this.appController.fontSize;
|
||
const unitsPerEm = this.appController.font.unitsPerEm;
|
||
|
||
this.appController.atlas.layoutGlyphs(atlasGlyphs, fontSize, unitsPerEm);
|
||
|
||
const uniqueGlyphIndices = this.appController.uniqueGlyphs.map(glyph => glyph.index);
|
||
uniqueGlyphIndices.sort((a, b) => a - b);
|
||
|
||
// TODO(pcwalton): Regenerate the IBOs to include only the glyphs we care about.
|
||
const transforms = new Float32Array((this.appController.uniqueGlyphs.length + 1) * 4);
|
||
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++;
|
||
|
||
const atlasLocation = glyph.atlasRect;
|
||
const metrics = glyph.metrics;
|
||
const left = metrics.xMin * this.pixelsPerUnit;
|
||
const bottom = metrics.yMin * this.pixelsPerUnit;
|
||
|
||
transforms[pathID * 4 + 0] = this.pixelsPerUnit;
|
||
transforms[pathID * 4 + 1] = this.pixelsPerUnit;
|
||
transforms[pathID * 4 + 2] = atlasLocation[0] - left;
|
||
transforms[pathID * 4 + 3] = atlasLocation[1] - bottom;
|
||
}
|
||
|
||
this.atlasTransformBuffer.upload(this.gl, transforms);
|
||
}
|
||
|
||
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.
|
||
this.antialiasingStrategy.setFramebufferSize(this, ATLAS_SIZE);
|
||
}
|
||
|
||
private setGlyphTexCoords() {
|
||
const textGlyphs = this.appController.textGlyphs;
|
||
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];
|
||
const textGlyphMetrics = textGlyph.metrics;
|
||
|
||
let atlasGlyphIndex = _.sortedIndexOf(atlasGlyphIndices, textGlyph.index);
|
||
if (atlasGlyphIndex < 0)
|
||
continue;
|
||
|
||
// Set texture coordinates.
|
||
const atlasGlyph = atlasGlyphs[atlasGlyphIndex];
|
||
const atlasGlyphRect = atlasGlyph.atlasRect;
|
||
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);
|
||
}
|
||
|
||
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();
|
||
}
|
||
|
||
private setDirty() {
|
||
if (this.dirty)
|
||
return;
|
||
this.dirty = true;
|
||
window.requestAnimationFrame(() => this.redraw());
|
||
}
|
||
|
||
private onWheel(event: WheelEvent) {
|
||
event.preventDefault();
|
||
|
||
if (event.ctrlKey) {
|
||
// Zoom event: see https://developer.mozilla.org/en-US/docs/Web/Events/wheel
|
||
const mouseLocation = glmatrix.vec2.fromValues(event.clientX, event.clientY);
|
||
const canvasLocation = this.canvas.getBoundingClientRect();
|
||
mouseLocation[0] -= canvasLocation.left;
|
||
mouseLocation[1] = canvasLocation.bottom - mouseLocation[1];
|
||
glmatrix.vec2.scale(mouseLocation, mouseLocation, window.devicePixelRatio);
|
||
|
||
const absoluteTranslation = glmatrix.vec2.create();
|
||
glmatrix.vec2.sub(absoluteTranslation, this.translation, mouseLocation);
|
||
glmatrix.vec2.scale(absoluteTranslation,
|
||
absoluteTranslation,
|
||
1.0 / this.appController.fontSize);
|
||
|
||
const scale = 1.0 - event.deltaY * window.devicePixelRatio * SCALE_FACTOR;
|
||
this.appController.scaleFontSize(scale);
|
||
|
||
glmatrix.vec2.scale(absoluteTranslation,
|
||
absoluteTranslation,
|
||
this.appController.fontSize);
|
||
glmatrix.vec2.add(this.translation, absoluteTranslation, mouseLocation);
|
||
return;
|
||
}
|
||
|
||
// Pan event.
|
||
const delta = glmatrix.vec2.fromValues(-event.deltaX, event.deltaY);
|
||
glmatrix.vec2.scale(delta, delta, window.devicePixelRatio);
|
||
glmatrix.vec2.add(this.translation, this.translation, delta);
|
||
|
||
this.rebuildAtlasIfNecessary();
|
||
}
|
||
|
||
private resizeToFit() {
|
||
const width = window.innerWidth;
|
||
const height = window.scrollY + window.innerHeight -
|
||
this.canvas.getBoundingClientRect().top;
|
||
const devicePixelRatio = window.devicePixelRatio;
|
||
|
||
const canvasSize = new Float32Array([width, height]) as glmatrix.vec2;
|
||
glmatrix.vec2.scale(canvasSize, canvasSize, devicePixelRatio);
|
||
|
||
this.canvas.style.width = width + 'px';
|
||
this.canvas.style.height = height + 'px';
|
||
this.canvas.width = canvasSize[0];
|
||
this.canvas.height = canvasSize[1];
|
||
|
||
this.antialiasingStrategy.init(this);
|
||
|
||
this.setDirty();
|
||
}
|
||
|
||
private redraw() {
|
||
if (this.meshes == null) {
|
||
this.dirty = false;
|
||
return;
|
||
}
|
||
|
||
// Start timing rendering.
|
||
if (this.timerQueryPollInterval == null) {
|
||
this.timerQueryExt.beginQueryEXT(this.timerQueryExt.TIME_ELAPSED_EXT,
|
||
this.atlasRenderingTimerQuery);
|
||
}
|
||
|
||
// Prepare for direct rendering.
|
||
this.antialiasingStrategy.prepare(this);
|
||
|
||
// Perform direct rendering (Loop-Blinn).
|
||
if (this.antialiasingStrategy.shouldRenderDirect)
|
||
this.renderDirect();
|
||
|
||
// Antialias.
|
||
this.antialiasingStrategy.resolve(this);
|
||
|
||
// End the timer, and start a new one.
|
||
if (this.timerQueryPollInterval == null) {
|
||
this.timerQueryExt.endQueryEXT(this.timerQueryExt.TIME_ELAPSED_EXT);
|
||
this.timerQueryExt.beginQueryEXT(this.timerQueryExt.TIME_ELAPSED_EXT,
|
||
this.compositingTimerQuery);
|
||
}
|
||
|
||
// Draw the glyphs with the resolved atlas to the default framebuffer.
|
||
this.composite();
|
||
|
||
// Finish timing, clear dirty bit and finish.
|
||
this.finishTiming();
|
||
this.dirty = false;
|
||
}
|
||
|
||
private finishTiming() {
|
||
if (this.timerQueryPollInterval != null)
|
||
return;
|
||
|
||
this.timerQueryExt.endQueryEXT(this.timerQueryExt.TIME_ELAPSED_EXT);
|
||
|
||
this.timerQueryPollInterval = window.setInterval(() => {
|
||
for (const queryName of ['atlasRenderingTimerQuery', 'compositingTimerQuery'] as
|
||
Array<'atlasRenderingTimerQuery' | 'compositingTimerQuery'>) {
|
||
if (this.timerQueryExt.getQueryObjectEXT(this[queryName],
|
||
this.timerQueryExt
|
||
.QUERY_RESULT_AVAILABLE_EXT) == 0) {
|
||
return;
|
||
}
|
||
}
|
||
|
||
const atlasRenderingTime =
|
||
this.timerQueryExt.getQueryObjectEXT(this.atlasRenderingTimerQuery,
|
||
this.timerQueryExt.QUERY_RESULT_EXT);
|
||
const compositingTime =
|
||
this.timerQueryExt.getQueryObjectEXT(this.compositingTimerQuery,
|
||
this.timerQueryExt.QUERY_RESULT_EXT);
|
||
this.appController.updateTiming({
|
||
atlasRendering: atlasRenderingTime / 1000000.0,
|
||
compositing: compositingTime / 1000000.0,
|
||
});
|
||
|
||
window.clearInterval(this.timerQueryPollInterval!);
|
||
this.timerQueryPollInterval = null;
|
||
}, TIME_INTERVAL_DELAY);
|
||
}
|
||
|
||
private setTransformUniform(uniforms: UniformMap) {
|
||
const transform = this.antialiasingStrategy.transform();
|
||
this.gl.uniformMatrix4fv(uniforms.uTransform, false, this.antialiasingStrategy.transform());
|
||
}
|
||
|
||
setFramebufferSizeUniform(uniforms: UniformMap) {
|
||
const currentViewport = this.gl.getParameter(this.gl.VIEWPORT);
|
||
this.gl.uniform2i(uniforms.uFramebufferSize, currentViewport[2], currentViewport[3]);
|
||
}
|
||
|
||
setIdentityTexScaleUniform(uniforms: UniformMap) {
|
||
this.gl.uniform2f(uniforms.uTexScale, 1.0, 1.0);
|
||
}
|
||
|
||
private get usedSizeFactor(): glmatrix.vec2 {
|
||
const usedSize = glmatrix.vec2.create();
|
||
glmatrix.vec2.div(usedSize, this.appController.atlas.usedSize, ATLAS_SIZE);
|
||
return usedSize;
|
||
}
|
||
|
||
setTransformSTAndTexScaleUniformsForAtlas(uniforms: UniformMap) {
|
||
const usedSize = this.usedSizeFactor;
|
||
this.gl.uniform4f(uniforms.uTransformST, 2.0 * usedSize[0], 2.0 * usedSize[1], -1.0, -1.0);
|
||
this.gl.uniform2f(uniforms.uTexScale, usedSize[0], usedSize[1]);
|
||
}
|
||
|
||
setTransformAndTexScaleUniformsForAtlas(uniforms: UniformMap) {
|
||
const usedSize = this.usedSizeFactor;
|
||
|
||
const transform = glmatrix.mat4.create();
|
||
glmatrix.mat4.fromTranslation(transform, [-1.0, -1.0, 0.0]);
|
||
glmatrix.mat4.scale(transform, transform, [2.0 * usedSize[0], 2.0 * usedSize[1], 1.0]);
|
||
this.gl.uniformMatrix4fv(uniforms.uTransform, false, transform);
|
||
|
||
this.gl.uniform2f(uniforms.uTexScale, usedSize[0], usedSize[1]);
|
||
}
|
||
|
||
private renderDirect() {
|
||
// Set up implicit cover state.
|
||
this.gl.depthFunc(this.gl.GREATER);
|
||
this.gl.depthMask(true);
|
||
this.gl.enable(this.gl.DEPTH_TEST);
|
||
this.gl.disable(this.gl.BLEND);
|
||
|
||
// Set up the implicit cover interior VAO.
|
||
const directInteriorProgram = this.shaderPrograms.directInterior;
|
||
this.gl.useProgram(directInteriorProgram.program);
|
||
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.meshes.bVertexPositions);
|
||
this.gl.vertexAttribPointer(directInteriorProgram.attributes.aPosition,
|
||
2,
|
||
this.gl.FLOAT,
|
||
false,
|
||
0,
|
||
0);
|
||
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.meshes.bVertexPathIDs);
|
||
this.gl.vertexAttribPointer(directInteriorProgram.attributes.aPathID,
|
||
1,
|
||
this.gl.UNSIGNED_SHORT,
|
||
false,
|
||
0,
|
||
0);
|
||
this.gl.enableVertexAttribArray(directInteriorProgram.attributes.aPosition);
|
||
this.gl.enableVertexAttribArray(directInteriorProgram.attributes.aPathID);
|
||
this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.meshes.coverInteriorIndices);
|
||
|
||
// Draw direct interior parts.
|
||
this.setTransformUniform(directInteriorProgram.uniforms);
|
||
this.setFramebufferSizeUniform(directInteriorProgram.uniforms);
|
||
this.pathColorsBufferTexture.bind(this.gl, directInteriorProgram.uniforms, 0);
|
||
this.atlasTransformBuffer.bind(this.gl, directInteriorProgram.uniforms, 1);
|
||
let indexCount = this.gl.getBufferParameter(this.gl.ELEMENT_ARRAY_BUFFER,
|
||
this.gl.BUFFER_SIZE) / UINT32_SIZE;
|
||
this.gl.drawElements(this.gl.TRIANGLES, indexCount, this.gl.UNSIGNED_INT, 0);
|
||
|
||
// Set up direct curve state.
|
||
this.gl.depthMask(false);
|
||
this.gl.enable(this.gl.BLEND);
|
||
this.gl.blendEquation(this.gl.FUNC_ADD);
|
||
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);
|
||
|
||
// Set up the direct curve VAO.
|
||
const directCurveProgram = this.shaderPrograms.directCurve;
|
||
this.gl.useProgram(directCurveProgram.program);
|
||
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.meshes.bVertexPositions);
|
||
this.gl.vertexAttribPointer(directCurveProgram.attributes.aPosition,
|
||
2,
|
||
this.gl.FLOAT,
|
||
false,
|
||
0,
|
||
0);
|
||
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.meshes.bVertexPathIDs);
|
||
this.gl.vertexAttribPointer(directCurveProgram.attributes.aPathID,
|
||
1,
|
||
this.gl.UNSIGNED_SHORT,
|
||
false,
|
||
0,
|
||
0);
|
||
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.meshes.bVertexLoopBlinnData);
|
||
this.gl.vertexAttribPointer(directCurveProgram.attributes.aTexCoord,
|
||
2,
|
||
this.gl.UNSIGNED_BYTE,
|
||
false,
|
||
B_LOOP_BLINN_DATA_SIZE,
|
||
B_LOOP_BLINN_DATA_TEX_COORD_OFFSET);
|
||
this.gl.vertexAttribPointer(directCurveProgram.attributes.aSign,
|
||
1,
|
||
this.gl.BYTE,
|
||
false,
|
||
B_LOOP_BLINN_DATA_SIZE,
|
||
B_LOOP_BLINN_DATA_SIGN_OFFSET);
|
||
this.gl.enableVertexAttribArray(directCurveProgram.attributes.aPosition);
|
||
this.gl.enableVertexAttribArray(directCurveProgram.attributes.aTexCoord);
|
||
this.gl.enableVertexAttribArray(directCurveProgram.attributes.aPathID);
|
||
this.gl.enableVertexAttribArray(directCurveProgram.attributes.aSign);
|
||
this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.meshes.coverCurveIndices);
|
||
|
||
// Draw direct curve parts.
|
||
this.setTransformUniform(directCurveProgram.uniforms);
|
||
this.setFramebufferSizeUniform(directCurveProgram.uniforms);
|
||
this.pathColorsBufferTexture.bind(this.gl, directCurveProgram.uniforms, 0);
|
||
this.atlasTransformBuffer.bind(this.gl, directCurveProgram.uniforms, 1);
|
||
indexCount = this.gl.getBufferParameter(this.gl.ELEMENT_ARRAY_BUFFER,
|
||
this.gl.BUFFER_SIZE) / UINT32_SIZE;
|
||
this.gl.drawElements(this.gl.TRIANGLES, indexCount, this.gl.UNSIGNED_INT, 0);
|
||
}
|
||
|
||
private composite() {
|
||
// 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);
|
||
this.gl.disable(this.gl.SCISSOR_TEST);
|
||
|
||
// 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,
|
||
[this.translation[0], this.translation[1], 0.0]);
|
||
|
||
// Blit.
|
||
this.gl.uniformMatrix4fv(blitProgram.uniforms.uTransform, false, transform);
|
||
this.gl.activeTexture(this.gl.TEXTURE0);
|
||
this.gl.bindTexture(this.gl.TEXTURE_2D, this.appController.atlas.ensureTexture(this.gl));
|
||
this.gl.uniform1i(blitProgram.uniforms.uSource, 0);
|
||
this.setIdentityTexScaleUniform(blitProgram.uniforms);
|
||
this.gl.drawElements(this.gl.TRIANGLES,
|
||
this.appController.textGlyphs.length * 6,
|
||
this.gl.UNSIGNED_INT,
|
||
0);
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
private canvas: HTMLCanvasElement;
|
||
|
||
gl: WebGLRenderingContext;
|
||
|
||
private colorBufferHalfFloatExt: any;
|
||
drawBuffersExt: any;
|
||
instancedArraysExt: any;
|
||
textureHalfFloatExt: any;
|
||
private timerQueryExt: any;
|
||
vertexArrayObjectExt: any;
|
||
|
||
private antialiasingStrategy: AntialiasingStrategy;
|
||
|
||
shaderPrograms: ShaderMap<PathfinderShaderProgram>;
|
||
|
||
meshes: PathfinderMeshBuffers;
|
||
meshData: PathfinderMeshData;
|
||
|
||
private atlasRenderingTimerQuery: WebGLQuery;
|
||
private compositingTimerQuery: WebGLQuery;
|
||
private timerQueryPollInterval: number | null;
|
||
|
||
private pathColorsBufferTexture: PathfinderBufferTexture;
|
||
|
||
quadPositionsBuffer: WebGLBuffer;
|
||
quadTexCoordsBuffer: WebGLBuffer;
|
||
quadElementsBuffer: WebGLBuffer;
|
||
|
||
private translation: glmatrix.vec2;
|
||
|
||
atlasFramebuffer: WebGLFramebuffer;
|
||
atlasDepthTexture: WebGLTexture;
|
||
|
||
private pixelsPerUnit: number;
|
||
|
||
glyphPositionsBuffer: WebGLBuffer;
|
||
glyphTexCoordsBuffer: WebGLBuffer;
|
||
glyphElementsBuffer: WebGLBuffer;
|
||
|
||
atlasTransformBuffer: PathfinderBufferTexture;
|
||
|
||
appController: TextDemoController;
|
||
|
||
private dirty: boolean;
|
||
}
|
||
|
||
class PathfinderShaderProgram {
|
||
constructor(gl: WebGLRenderingContext,
|
||
programName: string,
|
||
unlinkedShaderProgram: UnlinkedShaderProgram) {
|
||
this.program = expectNotNull(gl.createProgram(), "Failed to create shader program!");
|
||
for (const compiledShader of Object.values(unlinkedShaderProgram))
|
||
gl.attachShader(this.program, compiledShader);
|
||
gl.linkProgram(this.program);
|
||
|
||
if (gl.getProgramParameter(this.program, gl.LINK_STATUS) == 0) {
|
||
const infoLog = gl.getProgramInfoLog(this.program);
|
||
throw new PathfinderError(`Failed to link program "${programName}":\n${infoLog}`);
|
||
}
|
||
|
||
const uniformCount = gl.getProgramParameter(this.program, gl.ACTIVE_UNIFORMS);
|
||
const attributeCount = gl.getProgramParameter(this.program, gl.ACTIVE_ATTRIBUTES);
|
||
|
||
let uniforms: UniformMap = {};
|
||
let attributes: AttributeMap = {};
|
||
|
||
for (let uniformIndex = 0; uniformIndex < uniformCount; uniformIndex++) {
|
||
const uniformName = unwrapNull(gl.getActiveUniform(this.program, uniformIndex)).name;
|
||
uniforms[uniformName] = expectNotNull(gl.getUniformLocation(this.program, uniformName),
|
||
`Didn't find uniform "${uniformName}"!`);
|
||
}
|
||
for (let attributeIndex = 0; attributeIndex < attributeCount; attributeIndex++) {
|
||
const attributeName = unwrapNull(gl.getActiveAttrib(this.program, attributeIndex)).name;
|
||
attributes[attributeName] = attributeIndex;
|
||
}
|
||
|
||
this.uniforms = uniforms;
|
||
this.attributes = attributes;
|
||
}
|
||
|
||
readonly uniforms: UniformMap;
|
||
readonly attributes: AttributeMap;
|
||
readonly program: WebGLProgram;
|
||
}
|
||
|
||
class PathfinderBufferTexture {
|
||
constructor(gl: WebGLRenderingContext, uniformName: string) {
|
||
this.texture = expectNotNull(gl.createTexture(), "Failed to create buffer texture!");
|
||
this.size = glmatrix.vec2.create();
|
||
this.capacity = glmatrix.vec2.create();
|
||
this.uniformName = uniformName;
|
||
this.glType = 0;
|
||
}
|
||
|
||
upload(gl: WebGLRenderingContext, data: Float32Array | Uint8Array) {
|
||
gl.activeTexture(gl.TEXTURE0);
|
||
gl.bindTexture(gl.TEXTURE_2D, this.texture);
|
||
|
||
const glType = data instanceof Float32Array ? gl.FLOAT : gl.UNSIGNED_BYTE;
|
||
const area = Math.ceil(data.length / 4);
|
||
if (glType != this.glType || area > this.capacityArea) {
|
||
const width = Math.ceil(Math.sqrt(area));
|
||
const height = Math.ceil(area / width);
|
||
this.size = glmatrix.vec2.fromValues(width, height);
|
||
this.capacity = this.size;
|
||
this.glType = glType;
|
||
|
||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, glType, null);
|
||
setTextureParameters(gl, gl.NEAREST);
|
||
}
|
||
|
||
const mainDimensions = glmatrix.vec4.fromValues(0,
|
||
0,
|
||
this.capacity[0],
|
||
Math.floor(area / this.capacity[0]));
|
||
const remainderDimensions = glmatrix.vec4.fromValues(0,
|
||
mainDimensions[3],
|
||
area % this.capacity[0],
|
||
1);
|
||
const splitIndex = mainDimensions[2] * mainDimensions[3] * 4;
|
||
|
||
if (mainDimensions[2] > 0 && mainDimensions[3] > 0) {
|
||
gl.texSubImage2D(gl.TEXTURE_2D,
|
||
0,
|
||
mainDimensions[0],
|
||
mainDimensions[1],
|
||
mainDimensions[2],
|
||
mainDimensions[3],
|
||
gl.RGBA,
|
||
this.glType,
|
||
data.slice(0, splitIndex));
|
||
}
|
||
|
||
if (remainderDimensions[2] > 0) {
|
||
// Round data up to a multiple of 4 elements if necessary.
|
||
let remainderLength = data.length - splitIndex;
|
||
let remainder: Float32Array | Uint8Array;
|
||
if (remainderLength % 4 == 0) {
|
||
remainder = data.slice(splitIndex);
|
||
} else {
|
||
remainderLength += 4 - remainderLength % 4;
|
||
remainder = new (data.constructor as any)(remainderLength);
|
||
remainder.set(data.slice(splitIndex));
|
||
}
|
||
|
||
gl.texSubImage2D(gl.TEXTURE_2D,
|
||
0,
|
||
remainderDimensions[0],
|
||
remainderDimensions[1],
|
||
remainderDimensions[2],
|
||
remainderDimensions[3],
|
||
gl.RGBA,
|
||
this.glType,
|
||
remainder);
|
||
}
|
||
}
|
||
|
||
bind(gl: WebGLRenderingContext, uniforms: UniformMap, textureUnit: number) {
|
||
gl.activeTexture(gl.TEXTURE0 + textureUnit);
|
||
gl.bindTexture(gl.TEXTURE_2D, this.texture);
|
||
gl.uniform2i(uniforms[`${this.uniformName}Dimensions`],
|
||
this.capacity[0],
|
||
this.capacity[1]);
|
||
gl.uniform1i(uniforms[this.uniformName], textureUnit);
|
||
}
|
||
|
||
private get area() {
|
||
return this.size[0] * this.size[1];
|
||
}
|
||
|
||
private get capacityArea() {
|
||
return this.capacity[0] * this.capacity[1];
|
||
}
|
||
|
||
readonly texture: WebGLTexture;
|
||
readonly uniformName: string;
|
||
private size: Size2D;
|
||
private capacity: Size2D;
|
||
private glType: number;
|
||
}
|
||
|
||
class NoAAStrategy implements AntialiasingStrategy {
|
||
constructor(level: number) {
|
||
this.framebufferSize = new Float32Array([0, 0]) as Size2D;
|
||
}
|
||
|
||
init(view: PathfinderView) {}
|
||
|
||
attachMeshes(view: PathfinderView) {}
|
||
|
||
setFramebufferSize(view: PathfinderView, framebufferSize: Size2D) {
|
||
this.framebufferSize = framebufferSize;
|
||
}
|
||
|
||
transform(): glmatrix.mat4 {
|
||
return glmatrix.mat4.create();
|
||
}
|
||
|
||
prepare(view: PathfinderView) {
|
||
view.gl.bindFramebuffer(view.gl.FRAMEBUFFER, view.atlasFramebuffer);
|
||
view.gl.viewport(0, 0, this.framebufferSize[0], this.framebufferSize[1]);
|
||
view.gl.disable(view.gl.SCISSOR_TEST);
|
||
|
||
// Clear.
|
||
view.gl.clearColor(1.0, 1.0, 1.0, 1.0);
|
||
view.gl.clearDepth(0.0);
|
||
view.gl.depthMask(true);
|
||
view.gl.clear(view.gl.COLOR_BUFFER_BIT | view.gl.DEPTH_BUFFER_BIT);
|
||
}
|
||
|
||
resolve(view: PathfinderView) {}
|
||
|
||
get shouldRenderDirect() {
|
||
return true;
|
||
}
|
||
|
||
framebufferSize: Size2D;
|
||
}
|
||
|
||
class SSAAStrategy implements AntialiasingStrategy {
|
||
constructor(level: number) {
|
||
this.level = level;
|
||
this.destFramebufferSize = new Float32Array([0, 0]) as Size2D;
|
||
this.supersampledFramebufferSize = new Float32Array([0, 0]) as Size2D;
|
||
}
|
||
|
||
init(view: PathfinderView) {}
|
||
|
||
attachMeshes(view: PathfinderView) {}
|
||
|
||
setFramebufferSize(view: PathfinderView, framebufferSize: Size2D) {
|
||
this.destFramebufferSize = framebufferSize;
|
||
|
||
this.supersampledFramebufferSize = glmatrix.vec2.create();
|
||
glmatrix.vec2.mul(this.supersampledFramebufferSize,
|
||
framebufferSize,
|
||
this.supersampleScale);
|
||
|
||
this.supersampledColorTexture = unwrapNull(view.gl.createTexture());
|
||
view.gl.activeTexture(view.gl.TEXTURE0);
|
||
view.gl.bindTexture(view.gl.TEXTURE_2D, this.supersampledColorTexture);
|
||
view.gl.texImage2D(view.gl.TEXTURE_2D,
|
||
0,
|
||
view.gl.RGBA,
|
||
this.supersampledFramebufferSize[0],
|
||
this.supersampledFramebufferSize[1],
|
||
0,
|
||
view.gl.RGBA,
|
||
view.gl.UNSIGNED_BYTE,
|
||
null);
|
||
setTextureParameters(view.gl, view.gl.LINEAR);
|
||
|
||
this.supersampledDepthTexture =
|
||
createFramebufferDepthTexture(view.gl, this.supersampledFramebufferSize);
|
||
|
||
this.supersampledFramebuffer = createFramebuffer(view.gl,
|
||
view.drawBuffersExt,
|
||
[this.supersampledColorTexture],
|
||
this.supersampledDepthTexture);
|
||
|
||
view.gl.bindFramebuffer(view.gl.FRAMEBUFFER, null);
|
||
}
|
||
|
||
transform(): glmatrix.mat4 {
|
||
const scale = glmatrix.vec2.create();
|
||
glmatrix.vec2.div(scale, this.supersampledFramebufferSize, this.destFramebufferSize);
|
||
|
||
const transform = glmatrix.mat4.create();
|
||
glmatrix.mat4.fromScaling(transform, [scale[0], scale[1], 1.0]);
|
||
return transform;
|
||
}
|
||
|
||
prepare(view: PathfinderView) {
|
||
const framebufferSize = this.supersampledFramebufferSize;
|
||
const usedSize = this.usedSupersampledFramebufferSize(view);
|
||
view.gl.bindFramebuffer(view.gl.FRAMEBUFFER, this.supersampledFramebuffer);
|
||
view.gl.viewport(0, 0, framebufferSize[0], framebufferSize[1]);
|
||
view.gl.scissor(0, 0, usedSize[0], usedSize[1]);
|
||
view.gl.enable(view.gl.SCISSOR_TEST);
|
||
|
||
// Clear.
|
||
view.gl.clearColor(1.0, 1.0, 1.0, 1.0);
|
||
view.gl.clearDepth(0.0);
|
||
view.gl.depthMask(true);
|
||
view.gl.clear(view.gl.COLOR_BUFFER_BIT | view.gl.DEPTH_BUFFER_BIT);
|
||
}
|
||
|
||
resolve(view: PathfinderView) {
|
||
view.gl.bindFramebuffer(view.gl.FRAMEBUFFER, view.atlasFramebuffer);
|
||
view.gl.viewport(0, 0, ATLAS_SIZE[0], ATLAS_SIZE[1]);
|
||
view.gl.disable(view.gl.DEPTH_TEST);
|
||
|
||
// Set up the blit program VAO.
|
||
const blitProgram = view.shaderPrograms.blit;
|
||
view.gl.useProgram(blitProgram.program);
|
||
initQuadVAO(view, blitProgram.attributes);
|
||
|
||
// Resolve framebuffer.
|
||
view.gl.activeTexture(view.gl.TEXTURE0);
|
||
view.gl.bindTexture(view.gl.TEXTURE_2D, this.supersampledColorTexture);
|
||
view.gl.uniform1i(blitProgram.uniforms.uSource, 0);
|
||
view.setTransformAndTexScaleUniformsForAtlas(blitProgram.uniforms);
|
||
view.gl.bindBuffer(view.gl.ELEMENT_ARRAY_BUFFER, view.quadElementsBuffer);
|
||
view.gl.drawElements(view.gl.TRIANGLES, 6, view.gl.UNSIGNED_BYTE, 0);
|
||
}
|
||
|
||
get shouldRenderDirect() {
|
||
return true;
|
||
}
|
||
|
||
private get supersampleScale(): glmatrix.vec2 {
|
||
return glmatrix.vec2.fromValues(2, this.level == 2 ? 1 : 2);
|
||
}
|
||
|
||
private usedSupersampledFramebufferSize(view: PathfinderView): glmatrix.vec2 {
|
||
const result = glmatrix.vec2.create();
|
||
glmatrix.vec2.mul(result, view.appController.atlas.usedSize, this.supersampleScale);
|
||
return result;
|
||
}
|
||
|
||
private level: number;
|
||
private destFramebufferSize: Size2D;
|
||
private supersampledFramebufferSize: Size2D;
|
||
private supersampledColorTexture: WebGLTexture;
|
||
private supersampledDepthTexture: WebGLTexture;
|
||
private supersampledFramebuffer: WebGLFramebuffer;
|
||
}
|
||
|
||
abstract class ECAAStrategy implements AntialiasingStrategy {
|
||
constructor(level: number) {
|
||
this.framebufferSize = new Float32Array([0, 0]) as Size2D;
|
||
}
|
||
|
||
init(view: PathfinderView) {
|
||
this.bVertexPositionBufferTexture = new PathfinderBufferTexture(view.gl,
|
||
'uBVertexPosition');
|
||
this.bVertexPathIDBufferTexture = new PathfinderBufferTexture(view.gl, 'uBVertexPathID');
|
||
}
|
||
|
||
attachMeshes(view: PathfinderView) {
|
||
const bVertexPositions = new Float32Array(view.meshData.bVertexPositions);
|
||
const bVertexPathIDs = new Uint8Array(view.meshData.bVertexPathIDs);
|
||
this.bVertexPositionBufferTexture.upload(view.gl, bVertexPositions);
|
||
this.bVertexPathIDBufferTexture.upload(view.gl, bVertexPathIDs);
|
||
|
||
this.createEdgeDetectVAO(view);
|
||
this.createCoverVAO(view);
|
||
this.createLineVAOs(view);
|
||
this.createCurveVAOs(view);
|
||
this.createResolveVAO(view);
|
||
}
|
||
|
||
setFramebufferSize(view: PathfinderView, framebufferSize: Size2D) {
|
||
this.framebufferSize = framebufferSize;
|
||
|
||
this.initDirectFramebuffer(view);
|
||
this.initEdgeDetectFramebuffer(view);
|
||
this.initAAAlphaFramebuffer(view);
|
||
view.gl.bindFramebuffer(view.gl.FRAMEBUFFER, null);
|
||
}
|
||
|
||
transform(): glmatrix.mat4 {
|
||
return glmatrix.mat4.create();
|
||
}
|
||
|
||
private initDirectFramebuffer(view: PathfinderView) {
|
||
this.directColorTexture = createFramebufferColorTexture(view.gl, this.framebufferSize);
|
||
this.directPathIDTexture = createFramebufferColorTexture(view.gl, this.framebufferSize);
|
||
this.directDepthTexture = createFramebufferDepthTexture(view.gl, this.framebufferSize);
|
||
this.directFramebuffer =
|
||
createFramebuffer(view.gl,
|
||
view.drawBuffersExt,
|
||
[this.directColorTexture, this.directPathIDTexture],
|
||
this.directDepthTexture);
|
||
}
|
||
|
||
private initAAAlphaFramebuffer(view: PathfinderView) {
|
||
this.aaAlphaTexture = unwrapNull(view.gl.createTexture());
|
||
view.gl.activeTexture(view.gl.TEXTURE0);
|
||
view.gl.bindTexture(view.gl.TEXTURE_2D, this.aaAlphaTexture);
|
||
view.gl.texImage2D(view.gl.TEXTURE_2D,
|
||
0,
|
||
view.gl.RGB,
|
||
this.framebufferSize[0],
|
||
this.framebufferSize[1],
|
||
0,
|
||
view.gl.RGB,
|
||
view.textureHalfFloatExt.HALF_FLOAT_OES,
|
||
null);
|
||
setTextureParameters(view.gl, view.gl.NEAREST);
|
||
|
||
this.aaFramebuffer = createFramebuffer(view.gl,
|
||
view.drawBuffersExt,
|
||
[this.aaAlphaTexture],
|
||
view.atlasDepthTexture);
|
||
}
|
||
|
||
private createCoverVAO(view: PathfinderView) {
|
||
this.coverVAO = view.vertexArrayObjectExt.createVertexArrayOES();
|
||
view.vertexArrayObjectExt.bindVertexArrayOES(this.coverVAO);
|
||
|
||
const coverProgram = view.shaderPrograms.ecaaCover;
|
||
const attributes = coverProgram.attributes;
|
||
view.gl.useProgram(coverProgram.program);
|
||
view.gl.bindBuffer(view.gl.ARRAY_BUFFER, view.quadPositionsBuffer);
|
||
view.gl.vertexAttribPointer(attributes.aQuadPosition, 2, view.gl.FLOAT, false, 0, 0);
|
||
view.gl.bindBuffer(view.gl.ARRAY_BUFFER, view.meshes.bQuads);
|
||
view.gl.vertexAttribPointer(attributes.aUpperPointIndices,
|
||
4,
|
||
view.gl.UNSIGNED_SHORT,
|
||
false,
|
||
B_QUAD_SIZE,
|
||
B_QUAD_UPPER_INDICES_OFFSET);
|
||
view.gl.vertexAttribPointer(attributes.aLowerPointIndices,
|
||
4,
|
||
view.gl.UNSIGNED_SHORT,
|
||
false,
|
||
B_QUAD_SIZE,
|
||
B_QUAD_LOWER_INDICES_OFFSET);
|
||
view.gl.enableVertexAttribArray(attributes.aQuadPosition);
|
||
view.gl.enableVertexAttribArray(attributes.aUpperPointIndices);
|
||
view.gl.enableVertexAttribArray(attributes.aLowerPointIndices);
|
||
view.instancedArraysExt.vertexAttribDivisorANGLE(attributes.aUpperPointIndices, 1);
|
||
view.instancedArraysExt.vertexAttribDivisorANGLE(attributes.aLowerPointIndices, 1);
|
||
view.gl.bindBuffer(view.gl.ELEMENT_ARRAY_BUFFER, view.quadElementsBuffer);
|
||
|
||
view.vertexArrayObjectExt.bindVertexArrayOES(null);
|
||
}
|
||
|
||
private createLineVAOs(view: PathfinderView) {
|
||
const lineProgram = view.shaderPrograms.ecaaLine;
|
||
const attributes = lineProgram.attributes;
|
||
|
||
const vaos: Partial<UpperAndLower<WebGLVertexArrayObject>> = {};
|
||
for (const direction of ['upper', 'lower'] as Array<'upper' | 'lower'>) {
|
||
vaos[direction] = view.vertexArrayObjectExt.createVertexArrayOES();
|
||
view.vertexArrayObjectExt.bindVertexArrayOES(vaos[direction]);
|
||
|
||
const lineIndexBuffer = {
|
||
upper: view.meshes.edgeUpperLineIndices,
|
||
lower: view.meshes.edgeLowerLineIndices,
|
||
}[direction];
|
||
|
||
view.gl.useProgram(lineProgram.program);
|
||
view.gl.bindBuffer(view.gl.ARRAY_BUFFER, view.quadPositionsBuffer);
|
||
view.gl.vertexAttribPointer(attributes.aQuadPosition, 2, view.gl.FLOAT, false, 0, 0);
|
||
view.gl.bindBuffer(view.gl.ARRAY_BUFFER, lineIndexBuffer);
|
||
view.gl.vertexAttribPointer(attributes.aLineIndices,
|
||
4,
|
||
view.gl.UNSIGNED_SHORT,
|
||
false,
|
||
0,
|
||
0);
|
||
view.gl.enableVertexAttribArray(attributes.aQuadPosition);
|
||
view.gl.enableVertexAttribArray(attributes.aLineIndices);
|
||
view.instancedArraysExt.vertexAttribDivisorANGLE(attributes.aLineIndices, 1);
|
||
view.gl.bindBuffer(view.gl.ELEMENT_ARRAY_BUFFER, view.quadElementsBuffer);
|
||
}
|
||
|
||
view.vertexArrayObjectExt.bindVertexArrayOES(null);
|
||
|
||
this.lineVAOs = vaos as UpperAndLower<WebGLVertexArrayObject>;
|
||
}
|
||
|
||
private createCurveVAOs(view: PathfinderView) {
|
||
const curveProgram = view.shaderPrograms.ecaaCurve;
|
||
const attributes = curveProgram.attributes;
|
||
|
||
const vaos: Partial<UpperAndLower<WebGLVertexArrayObject>> = {};
|
||
for (const direction of ['upper', 'lower'] as Array<'upper' | 'lower'>) {
|
||
vaos[direction] = view.vertexArrayObjectExt.createVertexArrayOES();
|
||
view.vertexArrayObjectExt.bindVertexArrayOES(vaos[direction]);
|
||
|
||
const curveIndexBuffer = {
|
||
upper: view.meshes.edgeUpperCurveIndices,
|
||
lower: view.meshes.edgeLowerCurveIndices,
|
||
}[direction];
|
||
|
||
view.gl.useProgram(curveProgram.program);
|
||
view.gl.bindBuffer(view.gl.ARRAY_BUFFER, view.quadPositionsBuffer);
|
||
view.gl.vertexAttribPointer(attributes.aQuadPosition, 2, view.gl.FLOAT, false, 0, 0);
|
||
view.gl.bindBuffer(view.gl.ARRAY_BUFFER, curveIndexBuffer);
|
||
view.gl.vertexAttribPointer(attributes.aCurveEndpointIndices,
|
||
4,
|
||
view.gl.UNSIGNED_SHORT,
|
||
false,
|
||
UINT32_SIZE * 4,
|
||
0);
|
||
view.gl.vertexAttribPointer(attributes.aCurveControlPointIndex,
|
||
2,
|
||
view.gl.UNSIGNED_SHORT,
|
||
false,
|
||
UINT32_SIZE * 4,
|
||
UINT32_SIZE * 2);
|
||
view.gl.enableVertexAttribArray(attributes.aQuadPosition);
|
||
view.gl.enableVertexAttribArray(attributes.aCurveEndpointIndices);
|
||
view.gl.enableVertexAttribArray(attributes.aCurveControlPointIndex);
|
||
view.instancedArraysExt.vertexAttribDivisorANGLE(attributes.aCurveEndpointIndices, 1);
|
||
view.instancedArraysExt.vertexAttribDivisorANGLE(attributes.aCurveControlPointIndex, 1);
|
||
view.gl.bindBuffer(view.gl.ELEMENT_ARRAY_BUFFER, view.quadElementsBuffer);
|
||
}
|
||
|
||
view.vertexArrayObjectExt.bindVertexArrayOES(null);
|
||
|
||
this.curveVAOs = vaos as UpperAndLower<WebGLVertexArrayObject>;
|
||
}
|
||
|
||
private createResolveVAO(view: PathfinderView) {
|
||
this.resolveVAO = view.vertexArrayObjectExt.createVertexArrayOES();
|
||
view.vertexArrayObjectExt.bindVertexArrayOES(this.resolveVAO);
|
||
|
||
const resolveProgram = this.getResolveProgram(view);
|
||
view.gl.useProgram(resolveProgram.program);
|
||
initQuadVAO(view, resolveProgram.attributes);
|
||
|
||
view.vertexArrayObjectExt.bindVertexArrayOES(null);
|
||
}
|
||
|
||
prepare(view: PathfinderView) {
|
||
const usedSize = view.appController.atlas.usedSize;
|
||
view.gl.bindFramebuffer(view.gl.FRAMEBUFFER, this.directFramebuffer);
|
||
view.gl.viewport(0, 0, this.framebufferSize[0], this.framebufferSize[1]);
|
||
view.gl.scissor(0, 0, usedSize[0], usedSize[1]);
|
||
view.gl.enable(view.gl.SCISSOR_TEST);
|
||
|
||
// Clear out the color and depth textures.
|
||
view.drawBuffersExt.drawBuffersWEBGL([
|
||
view.drawBuffersExt.COLOR_ATTACHMENT0_WEBGL,
|
||
view.drawBuffersExt.NONE,
|
||
]);
|
||
view.gl.clearColor(1.0, 1.0, 1.0, 1.0);
|
||
view.gl.clearDepth(0.0);
|
||
view.gl.depthMask(true);
|
||
view.gl.clear(view.gl.COLOR_BUFFER_BIT | view.gl.DEPTH_BUFFER_BIT);
|
||
|
||
// Clear out the path ID texture.
|
||
view.drawBuffersExt.drawBuffersWEBGL([
|
||
view.drawBuffersExt.NONE,
|
||
view.drawBuffersExt.COLOR_ATTACHMENT1_WEBGL,
|
||
]);
|
||
view.gl.clearColor(0.0, 0.0, 0.0, 0.0);
|
||
view.gl.clear(view.gl.COLOR_BUFFER_BIT);
|
||
|
||
// Render to both textures.
|
||
view.drawBuffersExt.drawBuffersWEBGL([
|
||
view.drawBuffersExt.COLOR_ATTACHMENT0_WEBGL,
|
||
view.drawBuffersExt.COLOR_ATTACHMENT1_WEBGL,
|
||
]);
|
||
}
|
||
|
||
resolve(view: PathfinderView) {
|
||
// Detect edges if necessary.
|
||
this.detectEdgesIfNecessary(view);
|
||
|
||
// Conservatively cover.
|
||
this.cover(view);
|
||
|
||
// Antialias.
|
||
this.antialiasLines(view);
|
||
this.antialiasCurves(view);
|
||
|
||
// Resolve the antialiasing.
|
||
this.resolveAA(view);
|
||
}
|
||
|
||
private cover(view: PathfinderView) {
|
||
// Set state for conservative coverage.
|
||
const coverProgram = view.shaderPrograms.ecaaCover;
|
||
const usedSize = view.appController.atlas.usedSize;
|
||
view.gl.bindFramebuffer(view.gl.FRAMEBUFFER, this.aaFramebuffer);
|
||
view.gl.viewport(0, 0, this.framebufferSize[0], this.framebufferSize[1]);
|
||
view.gl.scissor(0, 0, usedSize[0], usedSize[1]);
|
||
view.gl.enable(view.gl.SCISSOR_TEST);
|
||
|
||
this.setCoverDepthState(view);
|
||
|
||
view.gl.blendEquation(view.gl.FUNC_ADD);
|
||
view.gl.blendFunc(view.gl.ONE, view.gl.ONE);
|
||
view.gl.enable(view.gl.BLEND);
|
||
|
||
this.clearForCover(view);
|
||
|
||
// Conservatively cover.
|
||
view.gl.useProgram(coverProgram.program);
|
||
view.vertexArrayObjectExt.bindVertexArrayOES(this.coverVAO);
|
||
const uniforms = coverProgram.uniforms;
|
||
view.setFramebufferSizeUniform(uniforms);
|
||
this.bVertexPositionBufferTexture.bind(view.gl, uniforms, 0);
|
||
this.bVertexPathIDBufferTexture.bind(view.gl, uniforms, 1);
|
||
view.atlasTransformBuffer.bind(view.gl, uniforms, 2);
|
||
view.instancedArraysExt.drawElementsInstancedANGLE(view.gl.TRIANGLES,
|
||
6,
|
||
view.gl.UNSIGNED_BYTE,
|
||
0,
|
||
view.meshData.bQuadCount);
|
||
view.vertexArrayObjectExt.bindVertexArrayOES(null);
|
||
}
|
||
|
||
private setAAState(view: PathfinderView) {
|
||
const usedSize = view.appController.atlas.usedSize;
|
||
view.gl.bindFramebuffer(view.gl.FRAMEBUFFER, this.aaFramebuffer);
|
||
view.gl.viewport(0, 0, this.framebufferSize[0], this.framebufferSize[1]);
|
||
view.gl.scissor(0, 0, usedSize[0], usedSize[1]);
|
||
view.gl.enable(view.gl.SCISSOR_TEST);
|
||
|
||
this.setAADepthState(view);
|
||
|
||
view.gl.blendEquation(view.gl.FUNC_REVERSE_SUBTRACT);
|
||
view.gl.blendFunc(view.gl.ONE, view.gl.ONE);
|
||
view.gl.enable(view.gl.BLEND);
|
||
}
|
||
|
||
private setAAUniforms(view: PathfinderView, uniforms: UniformMap) {
|
||
view.setFramebufferSizeUniform(uniforms);
|
||
this.bVertexPositionBufferTexture.bind(view.gl, uniforms, 0);
|
||
this.bVertexPathIDBufferTexture.bind(view.gl, uniforms, 1);
|
||
view.atlasTransformBuffer.bind(view.gl, uniforms, 2);
|
||
}
|
||
|
||
private antialiasLines(view: PathfinderView) {
|
||
this.setAAState(view);
|
||
|
||
const lineProgram = view.shaderPrograms.ecaaLine;
|
||
view.gl.useProgram(lineProgram.program);
|
||
const uniforms = lineProgram.uniforms;
|
||
this.setAAUniforms(view, uniforms);
|
||
|
||
for (const direction of ['upper', 'lower'] as Array<keyof UpperAndLower<void>>) {
|
||
view.vertexArrayObjectExt.bindVertexArrayOES(this.lineVAOs[direction]);
|
||
view.gl.uniform1i(uniforms.uLowerPart, direction === 'lower' ? 1 : 0);
|
||
const count = {
|
||
upper: view.meshData.edgeUpperLineIndexCount,
|
||
lower: view.meshData.edgeLowerLineIndexCount,
|
||
}[direction];
|
||
view.instancedArraysExt.drawElementsInstancedANGLE(view.gl.TRIANGLES,
|
||
6,
|
||
view.gl.UNSIGNED_BYTE,
|
||
0,
|
||
count);
|
||
}
|
||
|
||
view.vertexArrayObjectExt.bindVertexArrayOES(null);
|
||
}
|
||
|
||
private antialiasCurves(view: PathfinderView) {
|
||
this.setAAState(view);
|
||
|
||
const curveProgram = view.shaderPrograms.ecaaCurve;
|
||
view.gl.useProgram(curveProgram.program);
|
||
const uniforms = curveProgram.uniforms;
|
||
this.setAAUniforms(view, uniforms);
|
||
|
||
for (const direction of ['upper', 'lower'] as Array<keyof UpperAndLower<void>>) {
|
||
view.vertexArrayObjectExt.bindVertexArrayOES(this.curveVAOs[direction]);
|
||
view.gl.uniform1i(uniforms.uLowerPart, direction === 'lower' ? 1 : 0);
|
||
const count = {
|
||
upper: view.meshData.edgeUpperCurveIndexCount,
|
||
lower: view.meshData.edgeLowerCurveIndexCount,
|
||
}[direction];
|
||
view.instancedArraysExt.drawElementsInstancedANGLE(view.gl.TRIANGLES,
|
||
6,
|
||
view.gl.UNSIGNED_BYTE,
|
||
0,
|
||
count);
|
||
}
|
||
|
||
view.vertexArrayObjectExt.bindVertexArrayOES(null);
|
||
}
|
||
|
||
private resolveAA(view: PathfinderView) {
|
||
// Set state for ECAA resolve.
|
||
const usedSize = view.appController.atlas.usedSize;
|
||
view.gl.bindFramebuffer(view.gl.FRAMEBUFFER, view.atlasFramebuffer);
|
||
view.gl.viewport(0, 0, this.framebufferSize[0], this.framebufferSize[1]);
|
||
view.gl.scissor(0, 0, usedSize[0], usedSize[1]);
|
||
view.gl.enable(view.gl.SCISSOR_TEST);
|
||
this.setResolveDepthState(view);
|
||
view.gl.disable(view.gl.BLEND);
|
||
view.drawBuffersExt.drawBuffersWEBGL([view.drawBuffersExt.COLOR_ATTACHMENT0_WEBGL]);
|
||
|
||
// Clear out the resolve buffer, if necessary.
|
||
this.clearForResolve(view);
|
||
|
||
// Resolve.
|
||
const resolveProgram = this.getResolveProgram(view);
|
||
view.gl.useProgram(resolveProgram.program);
|
||
view.vertexArrayObjectExt.bindVertexArrayOES(this.resolveVAO);
|
||
view.setFramebufferSizeUniform(resolveProgram.uniforms);
|
||
view.gl.activeTexture(view.gl.TEXTURE0);
|
||
view.gl.bindTexture(view.gl.TEXTURE_2D, this.aaAlphaTexture);
|
||
view.gl.uniform1i(resolveProgram.uniforms.uAAAlpha, 0);
|
||
this.setResolveUniforms(view, resolveProgram);
|
||
view.setTransformSTAndTexScaleUniformsForAtlas(resolveProgram.uniforms);
|
||
view.gl.drawElements(view.gl.TRIANGLES, 6, view.gl.UNSIGNED_BYTE, 0);
|
||
view.vertexArrayObjectExt.bindVertexArrayOES(null);
|
||
}
|
||
|
||
protected abstract getResolveProgram(view: PathfinderView): PathfinderShaderProgram;
|
||
protected abstract initEdgeDetectFramebuffer(view: PathfinderView): void;
|
||
protected abstract createEdgeDetectVAO(view: PathfinderView): void;
|
||
protected abstract detectEdgesIfNecessary(view: PathfinderView): void;
|
||
protected abstract setCoverDepthState(view: PathfinderView): void;
|
||
protected abstract clearForCover(view: PathfinderView): void;
|
||
protected abstract setAADepthState(view: PathfinderView): void;
|
||
protected abstract clearForResolve(view: PathfinderView): void;
|
||
protected abstract setResolveDepthState(view: PathfinderView): void;
|
||
protected abstract setResolveUniforms(view: PathfinderView,
|
||
program: PathfinderShaderProgram): void;
|
||
|
||
abstract shouldRenderDirect: boolean;
|
||
|
||
private bVertexPositionBufferTexture: PathfinderBufferTexture;
|
||
private bVertexPathIDBufferTexture: PathfinderBufferTexture;
|
||
private directDepthTexture: WebGLTexture;
|
||
private directFramebuffer: WebGLFramebuffer;
|
||
private aaAlphaTexture: WebGLTexture;
|
||
private aaFramebuffer: WebGLFramebuffer;
|
||
private coverVAO: WebGLVertexArrayObject;
|
||
private lineVAOs: UpperAndLower<WebGLVertexArrayObject>;
|
||
private curveVAOs: UpperAndLower<WebGLVertexArrayObject>;
|
||
private resolveVAO: WebGLVertexArrayObject;
|
||
|
||
protected directColorTexture: WebGLTexture;
|
||
protected directPathIDTexture: WebGLTexture;
|
||
protected framebufferSize: Size2D;
|
||
}
|
||
|
||
class ECAAMonochromeStrategy extends ECAAStrategy {
|
||
protected getResolveProgram(view: PathfinderView): PathfinderShaderProgram {
|
||
return view.shaderPrograms.ecaaMonoResolve;
|
||
}
|
||
|
||
protected initEdgeDetectFramebuffer(view: PathfinderView) {}
|
||
|
||
protected createEdgeDetectVAO(view: PathfinderView) {}
|
||
|
||
protected detectEdgesIfNecessary(view: PathfinderView) {}
|
||
|
||
protected setCoverDepthState(view: PathfinderView) {
|
||
view.gl.depthMask(true);
|
||
view.gl.depthFunc(view.gl.ALWAYS);
|
||
view.gl.enable(view.gl.DEPTH_TEST);
|
||
}
|
||
|
||
protected clearForCover(view: PathfinderView) {
|
||
view.gl.clearColor(0.0, 0.0, 0.0, 0.0);
|
||
view.gl.clearDepth(0.0);
|
||
view.gl.clear(view.gl.COLOR_BUFFER_BIT | view.gl.DEPTH_BUFFER_BIT);
|
||
}
|
||
|
||
protected setAADepthState(view: PathfinderView) {
|
||
view.gl.disable(view.gl.DEPTH_TEST);
|
||
}
|
||
|
||
protected setResolveDepthState(view: PathfinderView) {
|
||
view.gl.depthMask(false);
|
||
view.gl.depthFunc(view.gl.NOTEQUAL);
|
||
view.gl.enable(view.gl.DEPTH_TEST);
|
||
}
|
||
|
||
protected clearForResolve(view: PathfinderView) {
|
||
view.gl.clearColor(1.0, 1.0, 1.0, 1.0);
|
||
view.gl.clear(view.gl.COLOR_BUFFER_BIT);
|
||
}
|
||
|
||
protected setResolveUniforms(view: PathfinderView, program: PathfinderShaderProgram) {
|
||
view.gl.uniform4fv(program.uniforms.uBGColor, view.bgColor);
|
||
view.gl.uniform4fv(program.uniforms.uFGColor, view.fgColor);
|
||
}
|
||
|
||
get shouldRenderDirect() {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
class ECAAMulticolorStrategy extends ECAAStrategy {
|
||
protected getResolveProgram(view: PathfinderView): PathfinderShaderProgram {
|
||
return view.shaderPrograms.ecaaMultiResolve;
|
||
}
|
||
|
||
protected initEdgeDetectFramebuffer(view: PathfinderView) {
|
||
this.bgColorTexture = createFramebufferColorTexture(view.gl, this.framebufferSize);
|
||
this.fgColorTexture = createFramebufferColorTexture(view.gl, this.framebufferSize);
|
||
this.edgeDetectFramebuffer = createFramebuffer(view.gl,
|
||
view.drawBuffersExt,
|
||
[this.bgColorTexture, this.fgColorTexture],
|
||
view.atlasDepthTexture);
|
||
}
|
||
|
||
protected createEdgeDetectVAO(view: PathfinderView) {
|
||
this.edgeDetectVAO = view.vertexArrayObjectExt.createVertexArrayOES();
|
||
view.vertexArrayObjectExt.bindVertexArrayOES(this.edgeDetectVAO);
|
||
|
||
const edgeDetectProgram = view.shaderPrograms.ecaaEdgeDetect;
|
||
view.gl.useProgram(edgeDetectProgram.program);
|
||
initQuadVAO(view, edgeDetectProgram.attributes);
|
||
|
||
view.vertexArrayObjectExt.bindVertexArrayOES(null);
|
||
}
|
||
|
||
protected detectEdgesIfNecessary(view: PathfinderView) {
|
||
// Set state for edge detection.
|
||
const edgeDetectProgram = view.shaderPrograms.ecaaEdgeDetect;
|
||
view.gl.bindFramebuffer(view.gl.FRAMEBUFFER, this.edgeDetectFramebuffer);
|
||
view.gl.viewport(0, 0, this.framebufferSize[0], this.framebufferSize[1]);
|
||
|
||
view.drawBuffersExt.drawBuffersWEBGL([
|
||
view.drawBuffersExt.COLOR_ATTACHMENT0_WEBGL,
|
||
view.drawBuffersExt.COLOR_ATTACHMENT1_WEBGL,
|
||
]);
|
||
|
||
view.gl.depthMask(true);
|
||
view.gl.depthFunc(view.gl.ALWAYS);
|
||
view.gl.enable(view.gl.DEPTH_TEST);
|
||
view.gl.disable(view.gl.BLEND);
|
||
|
||
view.gl.clearDepth(0.0);
|
||
view.gl.clearColor(0.0, 0.0, 0.0, 0.0);
|
||
view.gl.clear(view.gl.COLOR_BUFFER_BIT | view.gl.DEPTH_BUFFER_BIT);
|
||
|
||
// Perform edge detection.
|
||
view.gl.useProgram(edgeDetectProgram.program);
|
||
view.vertexArrayObjectExt.bindVertexArrayOES(this.edgeDetectVAO);
|
||
view.setFramebufferSizeUniform(edgeDetectProgram.uniforms);
|
||
view.setTransformSTAndTexScaleUniformsForAtlas(edgeDetectProgram.uniforms);
|
||
view.gl.activeTexture(view.gl.TEXTURE0);
|
||
view.gl.bindTexture(view.gl.TEXTURE_2D, this.directColorTexture);
|
||
view.gl.uniform1i(edgeDetectProgram.uniforms.uColor, 0);
|
||
view.gl.activeTexture(view.gl.TEXTURE1);
|
||
view.gl.bindTexture(view.gl.TEXTURE_2D, this.directPathIDTexture);
|
||
view.gl.uniform1i(edgeDetectProgram.uniforms.uPathID, 1);
|
||
view.gl.bindBuffer(view.gl.ELEMENT_ARRAY_BUFFER, view.quadElementsBuffer);
|
||
view.gl.drawElements(view.gl.TRIANGLES, 6, view.gl.UNSIGNED_BYTE, 0);
|
||
view.vertexArrayObjectExt.bindVertexArrayOES(null);
|
||
}
|
||
|
||
protected setCoverDepthState(view: PathfinderView) {
|
||
view.gl.depthMask(false);
|
||
view.gl.depthFunc(view.gl.ALWAYS);
|
||
view.gl.enable(view.gl.DEPTH_TEST);
|
||
}
|
||
|
||
protected clearForCover(view: PathfinderView) {
|
||
view.gl.clearColor(0.0, 0.0, 0.0, 0.0);
|
||
view.gl.clear(view.gl.COLOR_BUFFER_BIT);
|
||
}
|
||
|
||
protected setAADepthState(view: PathfinderView) {
|
||
view.gl.depthMask(false);
|
||
view.gl.depthFunc(view.gl.EQUAL);
|
||
view.gl.enable(view.gl.DEPTH_TEST);
|
||
}
|
||
|
||
protected setResolveDepthState(view: PathfinderView) {
|
||
view.gl.depthMask(false);
|
||
view.gl.depthFunc(view.gl.NOTEQUAL);
|
||
view.gl.enable(view.gl.DEPTH_TEST);
|
||
}
|
||
|
||
protected clearForResolve(view: PathfinderView) {}
|
||
|
||
protected setResolveUniforms(view: PathfinderView, program: PathfinderShaderProgram) {
|
||
view.gl.activeTexture(view.gl.TEXTURE1);
|
||
view.gl.bindTexture(view.gl.TEXTURE_2D, this.bgColorTexture);
|
||
view.gl.uniform1i(program.uniforms.uBGColor, 1);
|
||
view.gl.activeTexture(view.gl.TEXTURE2);
|
||
view.gl.bindTexture(view.gl.TEXTURE_2D, this.fgColorTexture);
|
||
view.gl.uniform1i(program.uniforms.uFGColor, 2);
|
||
}
|
||
|
||
get shouldRenderDirect() {
|
||
return true;
|
||
}
|
||
|
||
private edgeDetectFramebuffer: WebGLFramebuffer;
|
||
private edgeDetectVAO: WebGLVertexArrayObject;
|
||
private bgColorTexture: WebGLTexture;
|
||
private fgColorTexture: WebGLTexture;
|
||
}
|
||
|
||
interface AntialiasingStrategyTable {
|
||
none: typeof NoAAStrategy;
|
||
ssaa: typeof SSAAStrategy;
|
||
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 {
|
||
constructor() {
|
||
this._texture = null;
|
||
this._usedSize = glmatrix.vec2.create();
|
||
}
|
||
|
||
layoutGlyphs(glyphs: AtlasGlyph[], fontSize: number, unitsPerEm: number) {
|
||
const pixelsPerUnit = fontSize / unitsPerEm;
|
||
|
||
let nextOrigin = glmatrix.vec2.create();
|
||
let shelfBottom = 0.0;
|
||
|
||
for (const glyph of glyphs) {
|
||
const metrics = glyph.metrics;
|
||
|
||
const glyphSize = glmatrix.vec2.fromValues(metrics.xMax - metrics.xMin,
|
||
metrics.yMax - metrics.yMin);
|
||
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);
|
||
|
||
const glyphRect = glmatrix.vec4.fromValues(nextOrigin[0] + 1,
|
||
nextOrigin[1] + 1,
|
||
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.
|
||
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;
|
||
}
|
||
|
||
const ANTIALIASING_STRATEGIES: AntialiasingStrategyTable = {
|
||
none: NoAAStrategy,
|
||
ssaa: SSAAStrategy,
|
||
ecaa: ECAAMonochromeStrategy,
|
||
};
|
||
|
||
function main() {
|
||
const controller = new TextDemoController;
|
||
window.addEventListener('load', () => controller.start(), false);
|
||
}
|
||
|
||
main();
|