TSLint the demo

This commit is contained in:
Patrick Walton 2017-09-28 14:34:48 -07:00
parent 6217577674
commit e8135fbfe0
21 changed files with 1089 additions and 1048 deletions

View File

@ -31,5 +31,9 @@
"ts-loader": "^2.3.7",
"typescript": "^2.5.2",
"webpack": "^3.5.6"
},
"devDependencies": {
"tslint": "^5.7.0",
"tslint-loader": "^3.5.3"
}
}

View File

@ -9,21 +9,21 @@
// except according to those terms.
import * as glmatrix from 'gl-matrix';
import * as _ from "lodash";
import * as opentype from "opentype.js";
import {mat4, vec2} from "gl-matrix";
import {AntialiasingStrategy, AntialiasingStrategyName, NoAAStrategy} from "./aa-strategy";
import {DemoAppController} from "./app-controller";
import PathfinderBufferTexture from "./buffer-texture";
import {PerspectiveCamera} from "./camera";
import {mat4, vec2} from "gl-matrix";
import {PathfinderMeshData} from "./meshes";
import {ShaderMap, ShaderProgramSource} from "./shader-loader";
import {BUILTIN_FONT_URI, ExpandedMeshData} from "./text";
import { Hint, TextFrame, TextRun, GlyphStore, PathfinderFont } from "./text";
import {PathfinderError, assert, panic, unwrapNull} from "./utils";
import {PathfinderDemoView, Timings} from "./view";
import SSAAStrategy from "./ssaa-strategy";
import * as _ from "lodash";
import PathfinderBufferTexture from "./buffer-texture";
import {BUILTIN_FONT_URI, ExpandedMeshData} from "./text";
import {GlyphStore, Hint, PathfinderFont, TextFrame, TextRun} from "./text";
import {assert, panic, PathfinderError, unwrapNull} from "./utils";
import {PathfinderDemoView, Timings} from "./view";
const TEXT_AVAILABLE_WIDTH: number = 150000;
const TEXT_PADDING: number = 2000;
@ -94,6 +94,14 @@ interface MonumentSide {
}
class ThreeDController extends DemoAppController<ThreeDView> {
textFrames: TextFrame[];
glyphStore: GlyphStore;
private baseMeshes: PathfinderMeshData;
private expandedMeshes: ExpandedMeshData[];
private monumentPromise: Promise<MonumentSide[]>;
start() {
super.start();
@ -104,15 +112,34 @@ class ThreeDController extends DemoAppController<ThreeDView> {
this.loadInitialFile(this.builtinFileURI);
}
protected fileLoaded(fileData: ArrayBuffer): void {
const font = new PathfinderFont(fileData);
this.monumentPromise.then(monument => this.layoutMonument(font, fileData, monument));
}
protected createView(): ThreeDView {
return new ThreeDView(this,
unwrapNull(this.commonShaderSource),
unwrapNull(this.shaderSources));
}
protected get builtinFileURI(): string {
return BUILTIN_FONT_URI;
}
protected get defaultFile(): string {
return FONT;
}
private parseTextData(textData: any): MonumentSide[] {
const sides = [];
for (let sideIndex = 0; sideIndex < 4; sideIndex++)
sides[sideIndex] = { upper: { lines: [] }, lower: { lines: [] } };
for (const nameData of textData.monument) {
const side = parseInt(nameData.side) - 1;
const row = parseInt(nameData.row) - 1;
const number = parseInt(nameData.number) - 1;
const side = parseInt(nameData.side, 10) - 1;
const row = parseInt(nameData.row, 10) - 1;
const index = parseInt(nameData.number, 10) - 1;
if (sides[side] == null)
continue;
@ -121,29 +148,24 @@ class ThreeDController extends DemoAppController<ThreeDView> {
if (lines[row] == null)
lines[row] = { names: [] };
lines[row].names[number] = nameData.name;
lines[row].names[index] = nameData.name;
}
return sides.map(side => ({ lines: side.upper.lines.concat(side.lower.lines) }));
}
protected fileLoaded(fileData: ArrayBuffer): void {
const font = new PathfinderFont(fileData);
this.monumentPromise.then(monument => this.layoutMonument(font, fileData, monument));
}
private layoutMonument(font: PathfinderFont, fileData: ArrayBuffer, monument: MonumentSide[]) {
this.textFrames = [];
let glyphsNeeded: number[] = [];
for (const monumentSide of monument) {
let textRuns = [];
const textRuns = [];
for (let lineNumber = 0; lineNumber < monumentSide.lines.length; lineNumber++) {
const line = monumentSide.lines[lineNumber];
const lineY = -lineNumber * font.opentypeFont.lineHeight();
const lineGlyphs = line.names.map(string => {
const glyphs = font.opentypeFont.stringToGlyphs(string);
const lineGlyphs = line.names.map(name => {
const glyphs = font.opentypeFont.stringToGlyphs(name);
const glyphIDs = glyphs.map(glyph => (glyph as any).index);
const width = _.sumBy(glyphs, glyph => glyph.advanceWidth);
return { glyphs: glyphIDs, width: width };
@ -156,7 +178,7 @@ class ThreeDController extends DemoAppController<ThreeDView> {
let currentX = 0.0;
for (const glyphInfo of lineGlyphs) {
const textRunOrigin = [currentX, lineY];
const textRun = new TextRun(glyphInfo.glyphs, textRunOrigin, font);
const textRun = new TextRun(glyphInfo.glyphs, textRunOrigin, font);
textRun.layout();
textRuns.push(textRun);
currentX += glyphInfo.width + spacing;
@ -184,31 +206,27 @@ class ThreeDController extends DemoAppController<ThreeDView> {
});
});
}
protected createView(): ThreeDView {
return new ThreeDView(this,
unwrapNull(this.commonShaderSource),
unwrapNull(this.shaderSources));
}
protected get builtinFileURI(): string {
return BUILTIN_FONT_URI;
}
protected get defaultFile(): string {
return FONT;
}
textFrames: TextFrame[];
glyphStore: GlyphStore;
private baseMeshes: PathfinderMeshData;
private expandedMeshes: ExpandedMeshData[];
private monumentPromise: Promise<MonumentSide[]>;
}
class ThreeDView extends PathfinderDemoView {
destFramebuffer: WebGLFramebuffer | null = null;
camera: PerspectiveCamera;
protected usedSizeFactor: glmatrix.vec2 = glmatrix.vec2.clone([1.0, 1.0]);
protected directCurveProgramName: keyof ShaderMap<void> = 'direct3DCurve';
protected directInteriorProgramName: keyof ShaderMap<void> = 'direct3DInterior';
protected depthFunction: number = this.gl.LESS;
private _scale: number;
private appController: ThreeDController;
private cubeVertexPositionBuffer: WebGLBuffer;
private cubeIndexBuffer: WebGLBuffer;
constructor(appController: ThreeDController,
commonShaderSource: string,
shaderSources: ShaderMap<ShaderProgramSource>) {
@ -233,7 +251,7 @@ class ThreeDView extends PathfinderDemoView {
protected pathColorsForObject(textFrameIndex: number): Uint8Array {
const textFrame = this.appController.textFrames[textFrameIndex];
const pathCount = textFrame.totalGlyphCount;
const pathColors = new Uint8Array(4 * (pathCount + 1));
for (let pathIndex = 0; pathIndex < pathCount; pathIndex++)
pathColors.set(TEXT_COLOR, (pathIndex + 1) * 4);
@ -244,7 +262,7 @@ class ThreeDView extends PathfinderDemoView {
protected pathTransformsForObject(textFrameIndex: number): Float32Array {
const textFrame = this.appController.textFrames[textFrameIndex];
const pathCount = textFrame.totalGlyphCount;
const hint = new Hint(this.appController.glyphStore.font, PIXELS_PER_UNIT, false);
const pathTransforms = new Float32Array(4 * (pathCount + 1));
@ -316,18 +334,40 @@ class ThreeDView extends PathfinderDemoView {
this.appController.newTimingsReceived(_.pick(this.lastTimings, ['rendering']));
}
protected clearForDirectRendering(): void {
this.gl.clearColor(1.0, 1.0, 1.0, 1.0);
this.gl.clearDepth(1.0);
this.gl.depthMask(true);
this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
}
protected getModelviewTransform(objectIndex: number): glmatrix.mat4 {
const transform = glmatrix.mat4.create();
glmatrix.mat4.rotateY(transform, transform, Math.PI / 2.0 * objectIndex);
glmatrix.mat4.translate(transform, transform, TEXT_TRANSLATION);
return transform;
}
// Cheap but effective backface culling.
protected shouldRenderObject(objectIndex: number): boolean {
const translation = this.camera.translation;
const extent = TEXT_TRANSLATION[2] * TEXT_SCALE[2];
switch (objectIndex) {
case 0: return translation[2] < -extent;
case 1: return translation[0] < -extent;
case 2: return translation[2] > extent;
default: return translation[0] > extent;
}
}
get destAllocatedSize(): glmatrix.vec2 {
return glmatrix.vec2.fromValues(this.canvas.width, this.canvas.height);
}
destFramebuffer: WebGLFramebuffer | null = null;
get destUsedSize(): glmatrix.vec2 {
return this.destAllocatedSize;
}
protected usedSizeFactor: glmatrix.vec2 = glmatrix.vec2.clone([1.0, 1.0]);
private calculateWorldTransform(modelviewTranslation: glmatrix.vec3,
modelviewScale: glmatrix.vec3):
glmatrix.mat4 {
@ -349,49 +389,9 @@ class ThreeDView extends PathfinderDemoView {
return transform;
}
protected clearForDirectRendering(): void {
this.gl.clearColor(1.0, 1.0, 1.0, 1.0);
this.gl.clearDepth(1.0);
this.gl.depthMask(true);
this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
}
protected get worldTransform() {
return this.calculateWorldTransform(glmatrix.vec3.create(), TEXT_SCALE);
}
protected getModelviewTransform(objectIndex: number): glmatrix.mat4 {
const transform = glmatrix.mat4.create();
glmatrix.mat4.rotateY(transform, transform, Math.PI / 2.0 * objectIndex);
glmatrix.mat4.translate(transform, transform, TEXT_TRANSLATION);
return transform;
}
// Cheap but effective backface culling.
protected shouldRenderObject(objectIndex: number): boolean {
const translation = this.camera.translation;
const extent = TEXT_TRANSLATION[2] * TEXT_SCALE[2];
switch (objectIndex) {
case 0: return translation[2] < -extent;
case 1: return translation[0] < -extent;
case 2: return translation[2] > extent;
default: return translation[0] > extent;
}
}
protected directCurveProgramName: keyof ShaderMap<void> = 'direct3DCurve';
protected directInteriorProgramName: keyof ShaderMap<void> = 'direct3DInterior';
protected depthFunction: number = this.gl.LESS;
private _scale: number;
private appController: ThreeDController;
private cubeVertexPositionBuffer: WebGLBuffer;
private cubeIndexBuffer: WebGLBuffer;
camera: PerspectiveCamera;
}
function main() {

View File

@ -15,6 +15,9 @@ import {PathfinderDemoView} from './view';
export type AntialiasingStrategyName = 'none' | 'ssaa' | 'ecaa';
export abstract class AntialiasingStrategy {
// True if direct rendering should occur.
shouldRenderDirect: boolean;
// Prepares any OpenGL data. This is only called on startup and canvas resize.
init(view: PathfinderDemoView): void {
this.setFramebufferSize(view);
@ -43,12 +46,11 @@ export abstract class AntialiasingStrategy {
//
// This usually blits to the real framebuffer.
abstract resolve(view: PathfinderDemoView): void;
// True if direct rendering should occur.
shouldRenderDirect: boolean;
}
export class NoAAStrategy extends AntialiasingStrategy {
framebufferSize: glmatrix.vec2;
constructor(level: number, subpixelAA: boolean) {
super();
this.framebufferSize = glmatrix.vec2.create();
@ -77,6 +79,4 @@ export class NoAAStrategy extends AntialiasingStrategy {
get shouldRenderDirect() {
return true;
}
framebufferSize: glmatrix.vec2;
}

View File

@ -9,12 +9,16 @@
// except according to those terms.
import {AntialiasingStrategyName} from "./aa-strategy";
import {FilePickerView} from "./file-picker";
import {ShaderLoader, ShaderMap, ShaderProgramSource} from './shader-loader';
import {expectNotNull, unwrapUndef, unwrapNull} from './utils';
import {expectNotNull, unwrapNull, unwrapUndef} from './utils';
import {PathfinderDemoView, Timings, TIMINGS} from "./view";
import { FilePickerView } from "./file-picker";
export abstract class AppController {
protected canvas: HTMLCanvasElement;
protected screenshotButton: HTMLButtonElement | null;
start() {
const canvas = document.getElementById('pf-canvas') as HTMLCanvasElement;
}
@ -36,16 +40,25 @@ export abstract class AppController {
.then(data => this.fileLoaded(data));
}
protected canvas: HTMLCanvasElement;
protected screenshotButton: HTMLButtonElement | null;
protected abstract fileLoaded(data: ArrayBuffer): void;
protected abstract get defaultFile(): string;
}
export abstract class DemoAppController<View extends PathfinderDemoView> extends AppController {
view: Promise<View>;
protected abstract readonly builtinFileURI: string;
protected filePickerView: FilePickerView | null;
protected commonShaderSource: string | null;
protected shaderSources: ShaderMap<ShaderProgramSource> | null;
private aaLevelSelect: HTMLSelectElement | null;
private subpixelAASwitch: HTMLInputElement | null;
private fpsLabel: HTMLElement | null;
constructor() {
super();
}
@ -164,13 +177,15 @@ export abstract class DemoAppController<View extends PathfinderDemoView> extends
this.fpsLabel.classList.remove('invisible');
}
protected abstract createView(): View;
private updateAALevel() {
let aaType: AntialiasingStrategyName, aaLevel: number;
if (this.aaLevelSelect != null) {
const selectedOption = this.aaLevelSelect.selectedOptions[0];
const aaValues = unwrapNull(/^([a-z-]+)(?:-([0-9]+))?$/.exec(selectedOption.value));
aaType = aaValues[1] as AntialiasingStrategyName;
aaLevel = aaValues[2] === "" ? 1 : parseInt(aaValues[2]);
aaLevel = aaValues[2] === "" ? 1 : parseInt(aaValues[2], 10);
} else {
aaType = 'none';
aaLevel = 0;
@ -204,19 +219,4 @@ export abstract class DemoAppController<View extends PathfinderDemoView> extends
// Fetch the file.
this.fetchFile(selectedOption.value, this.builtinFileURI);
}
protected abstract createView(): View;
protected abstract readonly builtinFileURI: string;
view: Promise<View>;
protected filePickerView: FilePickerView | null;
protected commonShaderSource: string | null;
protected shaderSources: ShaderMap<ShaderProgramSource> | null;
private aaLevelSelect: HTMLSelectElement | null;
private subpixelAASwitch: HTMLInputElement | null;
private fpsLabel: HTMLElement | null;
}

View File

@ -11,17 +11,18 @@
import * as glmatrix from 'gl-matrix';
import * as opentype from "opentype.js";
import { AppController, DemoAppController } from "./app-controller";
import {PathfinderMeshData} from "./meshes";
import { BUILTIN_FONT_URI, TextFrame, TextRun, ExpandedMeshData, GlyphStore, PathfinderFont } from "./text";
import { assert, unwrapNull, PathfinderError } from "./utils";
import { PathfinderDemoView, Timings, MonochromePathfinderView } from "./view";
import { ShaderMap, ShaderProgramSource } from "./shader-loader";
import { AntialiasingStrategy, AntialiasingStrategyName, NoAAStrategy } from "./aa-strategy";
import SSAAStrategy from './ssaa-strategy';
import { OrthographicCamera } from './camera';
import { ECAAStrategy, ECAAMonochromeStrategy } from './ecaa-strategy';
import {AntialiasingStrategy, AntialiasingStrategyName, NoAAStrategy} from "./aa-strategy";
import {AppController, DemoAppController} from "./app-controller";
import PathfinderBufferTexture from './buffer-texture';
import {OrthographicCamera} from './camera';
import {ECAAMonochromeStrategy, ECAAStrategy} from './ecaa-strategy';
import {PathfinderMeshData} from "./meshes";
import {ShaderMap, ShaderProgramSource} from "./shader-loader";
import SSAAStrategy from './ssaa-strategy';
import {BUILTIN_FONT_URI, ExpandedMeshData, GlyphStore, PathfinderFont, TextFrame} from "./text";
import {TextRun} from "./text";
import {assert, PathfinderError, unwrapNull} from "./utils";
import {MonochromePathfinderView, PathfinderDemoView, Timings } from "./view";
const STRING: string = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
@ -33,9 +34,9 @@ const MIN_FONT_SIZE: number = 6;
const MAX_FONT_SIZE: number = 200;
const ANTIALIASING_STRATEGIES: AntialiasingStrategyTable = {
ecaa: ECAAMonochromeStrategy,
none: NoAAStrategy,
ssaa: SSAAStrategy,
ecaa: ECAAMonochromeStrategy,
};
interface ElapsedTime {
@ -50,6 +51,24 @@ interface AntialiasingStrategyTable {
}
class BenchmarkAppController extends DemoAppController<BenchmarkTestView> {
font: PathfinderFont | null;
textRun: TextRun | null;
protected readonly defaultFile: string = FONT;
protected readonly builtinFileURI: string = BUILTIN_FONT_URI;
private resultsModal: HTMLDivElement;
private resultsTableBody: HTMLTableSectionElement;
private resultsPartitioningTimeLabel: HTMLSpanElement;
private glyphStore: GlyphStore;
private baseMeshes: PathfinderMeshData;
private expandedMeshes: ExpandedMeshData;
private pixelsPerEm: number;
private elapsedTimes: ElapsedTime[];
private partitionTime: number;
start() {
super.start();
@ -104,8 +123,8 @@ class BenchmarkAppController extends DemoAppController<BenchmarkTestView> {
view.uploadPathTransforms(1);
view.uploadHints();
view.attachMeshes([expandedMeshes.meshes]);
})
})
});
});
}
protected createView(): BenchmarkTestView {
@ -128,7 +147,7 @@ class BenchmarkAppController extends DemoAppController<BenchmarkTestView> {
renderedPromise.then(elapsedTime => {
this.elapsedTimes.push({ size: this.pixelsPerEm, time: elapsedTime });
if (this.pixelsPerEm == MAX_FONT_SIZE) {
if (this.pixelsPerEm === MAX_FONT_SIZE) {
this.showResults();
return;
}
@ -156,27 +175,29 @@ class BenchmarkAppController extends DemoAppController<BenchmarkTestView> {
window.jQuery(this.resultsModal).modal();
}
protected readonly defaultFile: string = FONT;
protected readonly builtinFileURI: string = BUILTIN_FONT_URI;
private resultsModal: HTMLDivElement;
private resultsTableBody: HTMLTableSectionElement;
private resultsPartitioningTimeLabel: HTMLSpanElement;
private glyphStore: GlyphStore;
private baseMeshes: PathfinderMeshData;
private expandedMeshes: ExpandedMeshData;
private pixelsPerEm: number;
private elapsedTimes: ElapsedTime[];
private partitionTime: number;
font: PathfinderFont | null;
textRun: TextRun | null;
}
class BenchmarkTestView extends MonochromePathfinderView {
destFramebuffer: WebGLFramebuffer | null = null;
renderingPromiseCallback: ((time: number) => void) | null;
readonly bgColor: glmatrix.vec4 = glmatrix.vec4.clone([1.0, 1.0, 1.0, 0.0]);
readonly fgColor: glmatrix.vec4 = glmatrix.vec4.clone([0.0, 0.0, 0.0, 1.0]);
protected usedSizeFactor: glmatrix.vec2 = glmatrix.vec2.clone([1.0, 1.0]);
protected directCurveProgramName: keyof ShaderMap<void> = 'directCurve';
protected directInteriorProgramName: keyof ShaderMap<void> = 'directInterior';
protected depthFunction: number = this.gl.GREATER;
protected camera: OrthographicCamera;
private _pixelsPerEm: number = 32.0;
private readonly appController: BenchmarkAppController;
constructor(appController: BenchmarkAppController,
commonShaderSource: string,
shaderSources: ShaderMap<ShaderProgramSource>) {
@ -189,6 +210,15 @@ class BenchmarkTestView extends MonochromePathfinderView {
this.camera.onZoom = () => this.setDirty();
}
uploadHints(): void {
const glyphCount = unwrapNull(this.appController.textRun).glyphIDs.length;
const pathHints = new Float32Array((glyphCount + 1) * 4);
const pathHintsBufferTexture = new PathfinderBufferTexture(this.gl, 'uPathHints');
pathHintsBufferTexture.upload(this.gl, pathHints);
this.pathHintsBufferTexture = pathHintsBufferTexture;
}
protected createAAStrategy(aaType: AntialiasingStrategyName,
aaLevel: number,
subpixelAA: boolean):
@ -241,17 +271,6 @@ class BenchmarkTestView extends MonochromePathfinderView {
}
}
uploadHints(): void {
const glyphCount = unwrapNull(this.appController.textRun).glyphIDs.length;
const pathHints = new Float32Array((glyphCount + 1) * 4);
const pathHintsBufferTexture = new PathfinderBufferTexture(this.gl, 'uPathHints');
pathHintsBufferTexture.upload(this.gl, pathHints);
this.pathHintsBufferTexture = pathHintsBufferTexture;
}
destFramebuffer: WebGLFramebuffer | null = null;
get destAllocatedSize(): glmatrix.vec2 {
return glmatrix.vec2.clone([this.canvas.width, this.canvas.height]);
}
@ -260,10 +279,6 @@ class BenchmarkTestView extends MonochromePathfinderView {
return this.destAllocatedSize;
}
private readonly appController: BenchmarkAppController;
protected usedSizeFactor: glmatrix.vec2 = glmatrix.vec2.clone([1.0, 1.0]);
protected get worldTransform() {
const transform = glmatrix.mat4.create();
const translation = this.camera.translation;
@ -293,20 +308,6 @@ class BenchmarkTestView extends MonochromePathfinderView {
this.uploadPathTransforms(1);
this.setDirty();
}
renderingPromiseCallback: ((time: number) => void) | null;
private _pixelsPerEm: number = 32.0;
readonly bgColor: glmatrix.vec4 = glmatrix.vec4.clone([1.0, 1.0, 1.0, 0.0]);
readonly fgColor: glmatrix.vec4 = glmatrix.vec4.clone([0.0, 0.0, 0.0, 1.0]);
protected directCurveProgramName: keyof ShaderMap<void> = 'directCurve';
protected directInteriorProgramName: keyof ShaderMap<void> = 'directInterior';
protected depthFunction: number = this.gl.GREATER;
protected camera: OrthographicCamera;
}
function main() {

View File

@ -14,6 +14,13 @@ import {setTextureParameters, UniformMap} from './gl-utils';
import {expectNotNull} from './utils';
export default class PathfinderBufferTexture {
readonly texture: WebGLTexture;
readonly uniformName: string;
private size: glmatrix.vec2;
private capacity: glmatrix.vec2;
private glType: number;
constructor(gl: WebGLRenderingContext, uniformName: string) {
this.texture = expectNotNull(gl.createTexture(), "Failed to create buffer texture!");
this.size = glmatrix.vec2.create();
@ -28,7 +35,7 @@ export default class PathfinderBufferTexture {
const glType = data instanceof Float32Array ? gl.FLOAT : gl.UNSIGNED_BYTE;
const area = Math.ceil(data.length / 4);
if (glType != this.glType || area > this.capacityArea) {
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);
@ -65,7 +72,7 @@ export default class PathfinderBufferTexture {
// Round data up to a multiple of 4 elements if necessary.
let remainderLength = data.length - splitIndex;
let remainder: Float32Array | Uint8Array;
if (remainderLength % 4 == 0) {
if (remainderLength % 4 === 0) {
remainder = data.slice(splitIndex);
} else {
remainderLength += 4 - remainderLength % 4;
@ -101,11 +108,4 @@ export default class PathfinderBufferTexture {
private get capacityArea() {
return this.capacity[0] * this.capacity[1];
}
readonly texture: WebGLTexture;
readonly uniformName: string;
private size: glmatrix.vec2;
private capacity: glmatrix.vec2;
private glType: number;
}

View File

@ -64,17 +64,30 @@ interface PerspectiveMovementKeys {
}
export abstract class Camera {
protected canvas: HTMLCanvasElement;
constructor(canvas: HTMLCanvasElement) {
this.canvas = canvas;
}
abstract zoomIn(): void;
abstract zoomOut(): void;
protected canvas: HTMLCanvasElement;
}
export class OrthographicCamera extends Camera {
onPan: (() => void) | null;
onZoom: (() => void) | null;
translation: glmatrix.vec2;
scale: number;
private _bounds: glmatrix.vec4;
private readonly minScale: number;
private readonly maxScale: number;
private readonly scaleBounds: boolean;
private readonly ignoreBounds: boolean;
constructor(canvas: HTMLCanvasElement, options?: OrthographicCameraOptions) {
super(canvas);
@ -122,6 +135,31 @@ export class OrthographicCamera extends Camera {
this.zoom(scale, mouseLocation);
}
zoomToFit(): void {
const upperLeft = glmatrix.vec2.clone([this._bounds[0], this._bounds[1]]);
const lowerRight = glmatrix.vec2.clone([this._bounds[2], this._bounds[3]]);
const width = this._bounds[2] - this._bounds[0];
const height = Math.abs(this._bounds[1] - this._bounds[3]);
// Scale appropriately.
this.scale = Math.min(this.canvas.width / width, this.canvas.height / height);
// Center.
this.translation = glmatrix.vec2.create();
glmatrix.vec2.lerp(this.translation, upperLeft, lowerRight, 0.5);
glmatrix.vec2.scale(this.translation, this.translation, -this.scale);
this.translation[0] += this.canvas.width * 0.5;
this.translation[1] += this.canvas.height * 0.5;
}
zoomIn(): void {
this.zoom(ORTHOGRAPHIC_ZOOM_IN_FACTOR, this.centerPoint);
}
zoomOut(): void {
this.zoom(ORTHOGRAPHIC_ZOOM_OUT_FACTOR, this.centerPoint);
}
private onMouseDown(event: MouseEvent): void {
this.canvas.classList.add('pf-grabbing');
}
@ -172,31 +210,6 @@ export class OrthographicCamera extends Camera {
}
}
zoomToFit(): void {
const upperLeft = glmatrix.vec2.clone([this._bounds[0], this._bounds[1]]);
const lowerRight = glmatrix.vec2.clone([this._bounds[2], this._bounds[3]]);
const width = this._bounds[2] - this._bounds[0];
const height = Math.abs(this._bounds[1] - this._bounds[3]);
// Scale appropriately.
this.scale = Math.min(this.canvas.width / width, this.canvas.height / height);
// Center.
this.translation = glmatrix.vec2.create();
glmatrix.vec2.lerp(this.translation, upperLeft, lowerRight, 0.5);
glmatrix.vec2.scale(this.translation, this.translation, -this.scale);
this.translation[0] += this.canvas.width * 0.5;
this.translation[1] += this.canvas.height * 0.5;
}
zoomIn(): void {
this.zoom(ORTHOGRAPHIC_ZOOM_IN_FACTOR, this.centerPoint);
}
zoomOut(): void {
this.zoom(ORTHOGRAPHIC_ZOOM_OUT_FACTOR, this.centerPoint);
}
private zoom(scale: number, point: glmatrix.vec2): void {
const absoluteTranslation = glmatrix.vec2.create();
glmatrix.vec2.sub(absoluteTranslation, this.translation, point);
@ -227,22 +240,23 @@ export class OrthographicCamera extends Camera {
set bounds(newBounds: glmatrix.vec4) {
this._bounds = glmatrix.vec4.clone(newBounds);
}
onPan: (() => void) | null;
onZoom: (() => void) | null;
private _bounds: glmatrix.vec4;
translation: glmatrix.vec2;
scale: number;
private readonly minScale: number;
private readonly maxScale: number;
private readonly scaleBounds: boolean;
private readonly ignoreBounds: boolean;
}
export class PerspectiveCamera extends Camera {
onChange: (() => void) | null;
translation: glmatrix.vec3;
/// Yaw and pitch Euler angles.
rotation: glmatrix.vec2;
private movementDelta: glmatrix.vec3;
// If W, A, S, D are pressed
private wasdPress: PerspectiveMovementKeys;
private movementInterval: number | null;
private readonly innerCollisionExtent: number;
constructor(canvas: HTMLCanvasElement, options?: PerspectiveCameraOptions) {
super(canvas);
@ -272,6 +286,14 @@ export class PerspectiveCamera extends Camera {
]);
}
zoomIn(): void {
// TODO(pcwalton)
}
zoomOut(): void {
// TODO(pcwalton)
}
private onMouseDown(event: MouseEvent): void {
if (document.pointerLockElement !== this.canvas) {
this.canvas.requestPointerLock();
@ -391,26 +413,4 @@ export class PerspectiveCamera extends Camera {
glmatrix.mat4.rotateY(matrix, matrix, this.rotation[0]);
return matrix;
}
zoomIn(): void {
// TODO(pcwalton)
}
zoomOut(): void {
// TODO(pcwalton)
}
onChange: (() => void) | null;
translation: glmatrix.vec3;
/// Yaw and pitch Euler angles.
rotation: glmatrix.vec2;
private movementDelta: glmatrix.vec3;
// If W, A, S, D are pressed
private wasdPress: PerspectiveMovementKeys;
private movementInterval: number | null;
private readonly innerCollisionExtent: number;
}

View File

@ -11,6 +11,7 @@
import * as glmatrix from 'gl-matrix';
import {AntialiasingStrategy} from './aa-strategy';
import PathfinderBufferTexture from './buffer-texture';
import {createFramebuffer, createFramebufferColorTexture} from './gl-utils';
import {createFramebufferDepthTexture, setTextureParameters, UniformMap} from './gl-utils';
import {WebGLVertexArrayObject} from './gl-utils';
@ -18,7 +19,6 @@ import {B_QUAD_LOWER_INDICES_OFFSET, B_QUAD_SIZE, B_QUAD_UPPER_INDICES_OFFSET} f
import {PathfinderShaderProgram} from './shader-loader';
import {UINT32_SIZE, unwrapNull} from './utils';
import {MonochromePathfinderView} from './view';
import PathfinderBufferTexture from './buffer-texture';
interface UpperAndLower<T> {
upper: T;
@ -26,6 +26,27 @@ interface UpperAndLower<T> {
}
export abstract class ECAAStrategy extends AntialiasingStrategy {
abstract shouldRenderDirect: boolean;
protected directColorTexture: WebGLTexture;
protected directPathIDTexture: WebGLTexture;
protected aaDepthTexture: WebGLTexture;
protected supersampledFramebufferSize: glmatrix.vec2;
protected destFramebufferSize: glmatrix.vec2;
protected subpixelAA: boolean;
private bVertexPositionBufferTexture: PathfinderBufferTexture;
private bVertexPathIDBufferTexture: PathfinderBufferTexture;
private directFramebuffer: WebGLFramebuffer;
private aaAlphaTexture: WebGLTexture;
private aaFramebuffer: WebGLFramebuffer;
private coverVAO: WebGLVertexArrayObject;
private lineVAOs: UpperAndLower<WebGLVertexArrayObject>;
private curveVAOs: UpperAndLower<WebGLVertexArrayObject>;
private resolveVAO: WebGLVertexArrayObject;
constructor(level: number, subpixelAA: boolean) {
super();
@ -67,6 +88,58 @@ export abstract class ECAAStrategy extends AntialiasingStrategy {
view.gl.bindFramebuffer(view.gl.FRAMEBUFFER, null);
}
prepare(view: MonochromePathfinderView) {
const usedSize = this.supersampledUsedSize(view);
view.gl.bindFramebuffer(view.gl.FRAMEBUFFER, this.directFramebuffer);
view.gl.viewport(0,
0,
this.supersampledFramebufferSize[0],
this.supersampledFramebufferSize[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,
]);
}
antialias(view: MonochromePathfinderView) {
// Detect edges if necessary.
this.detectEdgesIfNecessary(view);
// Conservatively cover.
this.cover(view);
// Antialias.
this.antialiasLines(view);
this.antialiasCurves(view);
}
resolve(view: MonochromePathfinderView) {
// Resolve the antialiasing.
this.resolveAA(view);
}
get transform(): glmatrix.mat4 {
return glmatrix.mat4.create();
}
@ -82,6 +155,30 @@ export abstract class ECAAStrategy extends AntialiasingStrategy {
this.directDepthTexture);
}
protected setCoverDepthState(view: MonochromePathfinderView): void {
view.gl.disable(view.gl.DEPTH_TEST);
}
protected setResolveDepthState(view: MonochromePathfinderView): void {
view.gl.disable(view.gl.DEPTH_TEST);
}
protected supersampledUsedSize(view: MonochromePathfinderView): glmatrix.vec2 {
const usedSize = glmatrix.vec2.create();
glmatrix.vec2.mul(usedSize, view.destUsedSize, this.supersampleScale);
return usedSize;
}
protected abstract getResolveProgram(view: MonochromePathfinderView): PathfinderShaderProgram;
protected abstract initEdgeDetectFramebuffer(view: MonochromePathfinderView): void;
protected abstract createEdgeDetectVAO(view: MonochromePathfinderView): void;
protected abstract detectEdgesIfNecessary(view: MonochromePathfinderView): void;
protected abstract clearForCover(view: MonochromePathfinderView): void;
protected abstract setAADepthState(view: MonochromePathfinderView): void;
protected abstract clearForResolve(view: MonochromePathfinderView): void;
protected abstract setResolveUniforms(view: MonochromePathfinderView,
program: PathfinderShaderProgram): void;
private initAAAlphaFramebuffer(view: MonochromePathfinderView) {
this.aaAlphaTexture = unwrapNull(view.gl.createTexture());
view.gl.activeTexture(view.gl.TEXTURE0);
@ -148,8 +245,8 @@ export abstract class ECAAStrategy extends AntialiasingStrategy {
view.vertexArrayObjectExt.bindVertexArrayOES(vaos[direction]);
const lineIndexBuffer = {
upper: view.meshes[0].edgeUpperLineIndices,
lower: view.meshes[0].edgeLowerLineIndices,
upper: view.meshes[0].edgeUpperLineIndices,
}[direction];
view.gl.useProgram(lineProgram.program);
@ -183,8 +280,8 @@ export abstract class ECAAStrategy extends AntialiasingStrategy {
view.vertexArrayObjectExt.bindVertexArrayOES(vaos[direction]);
const curveIndexBuffer = {
upper: view.meshes[0].edgeUpperCurveIndices,
lower: view.meshes[0].edgeLowerCurveIndices,
upper: view.meshes[0].edgeUpperCurveIndices,
}[direction];
view.gl.useProgram(curveProgram.program);
@ -227,58 +324,6 @@ export abstract class ECAAStrategy extends AntialiasingStrategy {
view.vertexArrayObjectExt.bindVertexArrayOES(null);
}
prepare(view: MonochromePathfinderView) {
const usedSize = this.supersampledUsedSize(view);;
view.gl.bindFramebuffer(view.gl.FRAMEBUFFER, this.directFramebuffer);
view.gl.viewport(0,
0,
this.supersampledFramebufferSize[0],
this.supersampledFramebufferSize[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,
]);
}
antialias(view: MonochromePathfinderView) {
// Detect edges if necessary.
this.detectEdgesIfNecessary(view);
// Conservatively cover.
this.cover(view);
// Antialias.
this.antialiasLines(view);
this.antialiasCurves(view);
}
resolve(view: MonochromePathfinderView) {
// Resolve the antialiasing.
this.resolveAA(view);
}
private cover(view: MonochromePathfinderView) {
// Set state for conservative coverage.
const coverProgram = view.shaderPrograms.ecaaCover;
@ -350,8 +395,8 @@ export abstract class ECAAStrategy extends AntialiasingStrategy {
view.vertexArrayObjectExt.bindVertexArrayOES(this.lineVAOs[direction]);
view.gl.uniform1i(uniforms.uLowerPart, direction === 'lower' ? 1 : 0);
const count = {
upper: view.meshData[0].edgeUpperLineIndexCount,
lower: view.meshData[0].edgeLowerLineIndexCount,
upper: view.meshData[0].edgeUpperLineIndexCount,
}[direction];
view.instancedArraysExt.drawElementsInstancedANGLE(view.gl.TRIANGLES,
6,
@ -375,8 +420,8 @@ export abstract class ECAAStrategy extends AntialiasingStrategy {
view.vertexArrayObjectExt.bindVertexArrayOES(this.curveVAOs[direction]);
view.gl.uniform1i(uniforms.uLowerPart, direction === 'lower' ? 1 : 0);
const count = {
upper: view.meshData[0].edgeUpperCurveIndexCount,
lower: view.meshData[0].edgeLowerCurveIndexCount,
upper: view.meshData[0].edgeUpperCurveIndexCount,
}[direction];
view.instancedArraysExt.drawElementsInstancedANGLE(view.gl.TRIANGLES,
6,
@ -422,30 +467,6 @@ export abstract class ECAAStrategy extends AntialiasingStrategy {
view.vertexArrayObjectExt.bindVertexArrayOES(null);
}
protected setCoverDepthState(view: MonochromePathfinderView): void {
view.gl.disable(view.gl.DEPTH_TEST);
}
protected setResolveDepthState(view: MonochromePathfinderView): void {
view.gl.disable(view.gl.DEPTH_TEST);
}
protected supersampledUsedSize(view: MonochromePathfinderView): glmatrix.vec2 {
const usedSize = glmatrix.vec2.create();
glmatrix.vec2.mul(usedSize, view.destUsedSize, this.supersampleScale);
return usedSize;
}
protected abstract getResolveProgram(view: MonochromePathfinderView): PathfinderShaderProgram;
protected abstract initEdgeDetectFramebuffer(view: MonochromePathfinderView): void;
protected abstract createEdgeDetectVAO(view: MonochromePathfinderView): void;
protected abstract detectEdgesIfNecessary(view: MonochromePathfinderView): void;
protected abstract clearForCover(view: MonochromePathfinderView): void;
protected abstract setAADepthState(view: MonochromePathfinderView): void;
protected abstract clearForResolve(view: MonochromePathfinderView): void;
protected abstract setResolveUniforms(view: MonochromePathfinderView,
program: PathfinderShaderProgram): void;
protected get directDepthTexture(): WebGLTexture | null {
return null;
}
@ -453,27 +474,6 @@ export abstract class ECAAStrategy extends AntialiasingStrategy {
protected get supersampleScale(): glmatrix.vec2 {
return glmatrix.vec2.fromValues(this.subpixelAA ? 3.0 : 1.0, 1.0);
}
abstract shouldRenderDirect: boolean;
private bVertexPositionBufferTexture: PathfinderBufferTexture;
private bVertexPathIDBufferTexture: PathfinderBufferTexture;
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 aaDepthTexture: WebGLTexture;
protected supersampledFramebufferSize: glmatrix.vec2;
protected destFramebufferSize: glmatrix.vec2;
protected subpixelAA: boolean;
}
export class ECAAMonochromeStrategy extends ECAAStrategy {
@ -515,6 +515,13 @@ export class ECAAMonochromeStrategy extends ECAAStrategy {
}
export class ECAAMulticolorStrategy extends ECAAStrategy {
private _directDepthTexture: WebGLTexture;
private edgeDetectFramebuffer: WebGLFramebuffer;
private edgeDetectVAO: WebGLVertexArrayObject;
private bgColorTexture: WebGLTexture;
private fgColorTexture: WebGLTexture;
protected getResolveProgram(view: MonochromePathfinderView): PathfinderShaderProgram {
return view.shaderPrograms.ecaaMultiResolve;
}
@ -627,11 +634,4 @@ export class ECAAMulticolorStrategy extends ECAAStrategy {
protected get directDepthTexture(): WebGLTexture {
return this._directDepthTexture;
}
private _directDepthTexture: WebGLTexture;
private edgeDetectFramebuffer: WebGLFramebuffer;
private edgeDetectVAO: WebGLVertexArrayObject;
private bgColorTexture: WebGLTexture;
private fgColorTexture: WebGLTexture;
}

View File

@ -11,17 +11,21 @@
import {expectNotNull} from "./utils";
export class FilePickerView {
static create(): FilePickerView | null {
const element = document.getElementById('pf-file-select') as (HTMLInputElement | null);
return element == null ? null : new FilePickerView(element);
}
onFileLoaded: ((fileData: ArrayBuffer) => void) | null;
private readonly element: HTMLInputElement;
private constructor(element: HTMLInputElement) {
this.element = element;
this.onFileLoaded = null;
element.addEventListener('change', event => this.loadFile(event), false);
}
static create(): FilePickerView | null {
const element = document.getElementById('pf-file-select') as (HTMLInputElement | null);
return element == null ? null : new FilePickerView(element);
}
open() {
this.element.click();
}
@ -36,8 +40,4 @@ export class FilePickerView {
}, false);
reader.readAsArrayBuffer(file);
}
onFileLoaded: ((fileData: ArrayBuffer) => void) | null;
private readonly element: HTMLInputElement;
}

View File

@ -10,7 +10,7 @@
import * as glmatrix from 'gl-matrix';
import {UINT32_SIZE, assert, unwrapNull} from './utils';
import {assert, UINT32_SIZE, unwrapNull} from './utils';
export type WebGLVertexArrayObject = any;
@ -98,7 +98,7 @@ export function createFramebuffer(gl: WebGLRenderingContext,
0);
}
assert(gl.checkFramebufferStatus(gl.FRAMEBUFFER) == gl.FRAMEBUFFER_COMPLETE,
assert(gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE,
"Framebuffer was incomplete!");
return framebuffer;
}

View File

@ -11,20 +11,20 @@
import * as glmatrix from 'gl-matrix';
import * as opentype from "opentype.js";
import {Font} from 'opentype.js';
import {AppController} from "./app-controller";
import {OrthographicCamera} from "./camera";
import {FilePickerView} from './file-picker';
import {B_QUAD_SIZE, B_QUAD_UPPER_LEFT_VERTEX_OFFSET} from "./meshes";
import {B_QUAD_UPPER_RIGHT_VERTEX_OFFSET} from "./meshes";
import {B_QUAD_UPPER_CONTROL_POINT_VERTEX_OFFSET, B_QUAD_LOWER_LEFT_VERTEX_OFFSET} from "./meshes";
import {B_QUAD_LOWER_LEFT_VERTEX_OFFSET, B_QUAD_UPPER_CONTROL_POINT_VERTEX_OFFSET} from "./meshes";
import {B_QUAD_LOWER_RIGHT_VERTEX_OFFSET} from "./meshes";
import {B_QUAD_LOWER_CONTROL_POINT_VERTEX_OFFSET, PathfinderMeshData} from "./meshes";
import {SVGLoader, BUILTIN_SVG_URI} from './svg-loader';
import {B_QUAD_SIZE, B_QUAD_UPPER_LEFT_VERTEX_OFFSET} from "./meshes";
import {BUILTIN_SVG_URI, SVGLoader} from './svg-loader';
import {BUILTIN_FONT_URI, TextRun} from "./text";
import { GlyphStore, TextFrame, PathfinderFont } from "./text";
import {unwrapNull, UINT32_SIZE, UINT32_MAX, assert} from "./utils";
import {GlyphStore, PathfinderFont, TextFrame} from "./text";
import {assert, UINT32_MAX, UINT32_SIZE, unwrapNull} from "./utils";
import {PathfinderView} from "./view";
import {Font} from 'opentype.js';
const CHARACTER: string = 'A';
@ -49,6 +49,22 @@ const SVG_SCALE: number = 1.0;
type FileType = 'font' | 'svg';
class MeshDebuggerAppController extends AppController {
meshes: PathfinderMeshData | null;
protected readonly defaultFile: string = FONT;
private file: PathfinderFont | SVGLoader | null;
private fileType: FileType;
private fileData: ArrayBuffer | null;
private openModal: HTMLElement;
private openFileSelect: HTMLSelectElement;
private fontPathSelectGroup: HTMLElement;
private fontPathSelect: HTMLSelectElement;
private filePicker: FilePickerView;
private view: MeshDebuggerView;
start() {
super.start();
@ -78,6 +94,43 @@ class MeshDebuggerAppController extends AppController {
this.loadInitialFile(BUILTIN_FONT_URI);
}
protected fileLoaded(fileData: ArrayBuffer): void {
while (this.fontPathSelect.lastChild != null)
this.fontPathSelect.removeChild(this.fontPathSelect.lastChild);
this.fontPathSelectGroup.classList.remove('pf-display-none');
if (this.fileType === 'font')
this.fontLoaded(fileData);
else if (this.fileType === 'svg')
this.svgLoaded(fileData);
}
protected loadPath(opentypeGlyph?: opentype.Glyph | null) {
window.jQuery(this.openModal).modal('hide');
let promise: Promise<PathfinderMeshData>;
if (this.file instanceof PathfinderFont && this.fileData != null) {
if (opentypeGlyph == null) {
const glyphIndex = parseInt(this.fontPathSelect.selectedOptions[0].value, 10);
opentypeGlyph = this.file.opentypeFont.glyphs.get(glyphIndex);
}
const glyphStorage = new GlyphStore(this.file, [(opentypeGlyph as any).index]);
promise = glyphStorage.partition().then(result => result.meshes);
} else if (this.file instanceof SVGLoader) {
promise = this.file.partition(this.fontPathSelect.selectedIndex);
} else {
return;
}
promise.then(meshes => {
this.meshes = meshes;
this.view.attachMeshes();
});
}
private showOpenDialog(): void {
window.jQuery(this.openModal).modal();
}
@ -98,18 +151,6 @@ class MeshDebuggerAppController extends AppController {
this.fetchFile(results[2], BUILTIN_URIS[this.fileType]);
}
protected fileLoaded(fileData: ArrayBuffer): void {
while (this.fontPathSelect.lastChild != null)
this.fontPathSelect.removeChild(this.fontPathSelect.lastChild);
this.fontPathSelectGroup.classList.remove('pf-display-none');
if (this.fileType === 'font')
this.fontLoaded(fileData);
else if (this.fileType === 'svg')
this.svgLoaded(fileData);
}
private fontLoaded(fileData: ArrayBuffer): void {
this.file = new PathfinderFont(fileData);
this.fileData = fileData;
@ -141,50 +182,13 @@ class MeshDebuggerAppController extends AppController {
this.fontPathSelect.appendChild(newOption);
}
}
protected loadPath(opentypeGlyph?: opentype.Glyph | null) {
window.jQuery(this.openModal).modal('hide');
let promise: Promise<PathfinderMeshData>;
if (this.file instanceof PathfinderFont && this.fileData != null) {
if (opentypeGlyph == null) {
const glyphIndex = parseInt(this.fontPathSelect.selectedOptions[0].value);
opentypeGlyph = this.file.opentypeFont.glyphs.get(glyphIndex);
}
const glyphStorage = new GlyphStore(this.file, [(opentypeGlyph as any).index]);
promise = glyphStorage.partition().then(result => result.meshes);
} else if (this.file instanceof SVGLoader) {
promise = this.file.partition(this.fontPathSelect.selectedIndex);
} else {
return;
}
promise.then(meshes => {
this.meshes = meshes;
this.view.attachMeshes();
})
}
protected readonly defaultFile: string = FONT;
private file: PathfinderFont | SVGLoader | null;
private fileType: FileType;
private fileData: ArrayBuffer | null;
meshes: PathfinderMeshData | null;
private openModal: HTMLElement;
private openFileSelect: HTMLSelectElement;
private fontPathSelectGroup: HTMLElement;
private fontPathSelect: HTMLSelectElement;
private filePicker: FilePickerView;
private view: MeshDebuggerView;
}
class MeshDebuggerView extends PathfinderView {
camera: OrthographicCamera;
private appController: MeshDebuggerAppController;
constructor(appController: MeshDebuggerAppController) {
super();
@ -310,14 +314,10 @@ class MeshDebuggerView extends PathfinderView {
context.restore();
}
private appController: MeshDebuggerAppController;
camera: OrthographicCamera;
}
function getPosition(positions: Float32Array, vertexIndex: number): Float32Array | null {
if (vertexIndex == UINT32_MAX)
if (vertexIndex === UINT32_MAX)
return null;
return new Float32Array([positions[vertexIndex * 2 + 0], -positions[vertexIndex * 2 + 1]]);
}

View File

@ -10,20 +10,20 @@
import * as base64js from 'base64-js';
import { PathfinderError, expectNotNull, panic, UINT32_SIZE, UINT32_MAX } from './utils';
import * as _ from 'lodash';
import { expectNotNull, panic, PathfinderError, UINT32_MAX, UINT32_SIZE } from './utils';
const BUFFER_TYPES: Meshes<BufferType> = {
bQuads: 'ARRAY_BUFFER',
bVertexPositions: 'ARRAY_BUFFER',
bVertexPathIDs: 'ARRAY_BUFFER',
bVertexLoopBlinnData: 'ARRAY_BUFFER',
coverInteriorIndices: 'ELEMENT_ARRAY_BUFFER',
bVertexPathIDs: 'ARRAY_BUFFER',
bVertexPositions: 'ARRAY_BUFFER',
coverCurveIndices: 'ELEMENT_ARRAY_BUFFER',
edgeUpperLineIndices: 'ARRAY_BUFFER',
coverInteriorIndices: 'ELEMENT_ARRAY_BUFFER',
edgeLowerCurveIndices: 'ARRAY_BUFFER',
edgeLowerLineIndices: 'ARRAY_BUFFER',
edgeUpperCurveIndices: 'ARRAY_BUFFER',
edgeLowerCurveIndices: 'ARRAY_BUFFER',
edgeUpperLineIndices: 'ARRAY_BUFFER',
};
export const B_QUAD_SIZE: number = 4 * 8;
@ -54,6 +54,23 @@ export interface Meshes<T> {
}
export class PathfinderMeshData implements Meshes<ArrayBuffer> {
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;
constructor(meshes: Meshes<string | ArrayBuffer>) {
for (const bufferName of Object.keys(BUFFER_TYPES) as Array<keyof Meshes<void>>) {
const meshBuffer = meshes[bufferName];
@ -177,53 +194,26 @@ export class PathfinderMeshData implements Meshes<ArrayBuffer> {
return new PathfinderMeshData({
bQuads: new Uint32Array(expandedBQuads).buffer as ArrayBuffer,
bVertexPositions: new Float32Array(expandedBVertexPositions).buffer as ArrayBuffer,
bVertexPathIDs: new Uint16Array(expandedBVertexPathIDs).buffer as ArrayBuffer,
bVertexLoopBlinnData: new Uint32Array(expandedBVertexLoopBlinnData).buffer as
ArrayBuffer,
coverInteriorIndices: new Uint32Array(expandedCoverInteriorIndices).buffer as
ArrayBuffer,
bVertexPathIDs: new Uint16Array(expandedBVertexPathIDs).buffer as ArrayBuffer,
bVertexPositions: new Float32Array(expandedBVertexPositions).buffer as ArrayBuffer,
coverCurveIndices: new Uint32Array(expandedCoverCurveIndices).buffer as ArrayBuffer,
edgeUpperCurveIndices: new Uint32Array(expandedEdgeUpperCurveIndices).buffer as
ArrayBuffer,
edgeUpperLineIndices: new Uint32Array(expandedEdgeUpperLineIndices).buffer as
coverInteriorIndices: new Uint32Array(expandedCoverInteriorIndices).buffer as
ArrayBuffer,
edgeLowerCurveIndices: new Uint32Array(expandedEdgeLowerCurveIndices).buffer as
ArrayBuffer,
edgeLowerLineIndices: new Uint32Array(expandedEdgeLowerLineIndices).buffer as
ArrayBuffer,
})
edgeUpperCurveIndices: new Uint32Array(expandedEdgeUpperCurveIndices).buffer as
ArrayBuffer,
edgeUpperLineIndices: new Uint32Array(expandedEdgeUpperLineIndices).buffer as
ArrayBuffer,
});
}
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;
}
export 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;
@ -234,6 +224,16 @@ export class PathfinderMeshBuffers implements Meshes<WebGLBuffer> {
readonly edgeUpperCurveIndices: WebGLBuffer;
readonly edgeLowerLineIndices: WebGLBuffer;
readonly edgeLowerCurveIndices: 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;
}
}
}
function copyIndices(destIndices: number[],
@ -271,7 +271,7 @@ function findFirstBQuadIndex(bQuads: Uint32Array, queryPathID: number): number |
const thisPathID = bQuads[mid * B_QUAD_FIELD_COUNT];
if (queryPathID <= thisPathID)
high = mid;
else
else
low = mid + 1;
}
return bQuads[low * B_QUAD_FIELD_COUNT] === queryPathID ? low : null;

View File

@ -9,7 +9,7 @@
// except according to those terms.
import {AttributeMap, UniformMap} from './gl-utils';
import {PathfinderError, expectNotNull, unwrapNull} from './utils';
import {expectNotNull, PathfinderError, unwrapNull} from './utils';
export interface UnlinkedShaderProgram {
vertex: WebGLShader;
@ -37,60 +37,60 @@ export const SHADER_NAMES: Array<keyof ShaderMap<void>> = [
const SHADER_URLS: ShaderMap<ShaderProgramURLs> = {
blit: {
vertex: "/glsl/gles2/blit.vs.glsl",
fragment: "/glsl/gles2/blit.fs.glsl",
},
directCurve: {
vertex: "/glsl/gles2/direct-curve.vs.glsl",
fragment: "/glsl/gles2/direct-curve.fs.glsl",
},
directInterior: {
vertex: "/glsl/gles2/direct-interior.vs.glsl",
fragment: "/glsl/gles2/direct-interior.fs.glsl",
},
direct3DCurve: {
vertex: "/glsl/gles2/direct-3d-curve.vs.glsl",
fragment: "/glsl/gles2/direct-curve.fs.glsl",
},
direct3DInterior: {
vertex: "/glsl/gles2/direct-3d-interior.vs.glsl",
fragment: "/glsl/gles2/direct-interior.fs.glsl",
},
ssaaSubpixelResolve: {
vertex: "/glsl/gles2/ssaa-subpixel-resolve.vs.glsl",
fragment: "/glsl/gles2/ssaa-subpixel-resolve.fs.glsl",
},
ecaaEdgeDetect: {
vertex: "/glsl/gles2/ecaa-edge-detect.vs.glsl",
fragment: "/glsl/gles2/ecaa-edge-detect.fs.glsl",
},
ecaaCover: {
vertex: "/glsl/gles2/ecaa-cover.vs.glsl",
fragment: "/glsl/gles2/ecaa-cover.fs.glsl",
},
ecaaLine: {
vertex: "/glsl/gles2/ecaa-line.vs.glsl",
fragment: "/glsl/gles2/ecaa-line.fs.glsl",
},
ecaaCurve: {
vertex: "/glsl/gles2/ecaa-curve.vs.glsl",
fragment: "/glsl/gles2/ecaa-curve.fs.glsl",
},
ecaaMonoResolve: {
vertex: "/glsl/gles2/ecaa-mono-resolve.vs.glsl",
fragment: "/glsl/gles2/ecaa-mono-resolve.fs.glsl",
},
ecaaMonoSubpixelResolve: {
vertex: "/glsl/gles2/ecaa-mono-subpixel-resolve.vs.glsl",
fragment: "/glsl/gles2/ecaa-mono-subpixel-resolve.fs.glsl",
},
ecaaMultiResolve: {
vertex: "/glsl/gles2/ecaa-multi-resolve.vs.glsl",
fragment: "/glsl/gles2/ecaa-multi-resolve.fs.glsl",
vertex: "/glsl/gles2/blit.vs.glsl",
},
demo3DMonument: {
vertex: "/glsl/gles2/demo-3d-monument.vs.glsl",
fragment: "/glsl/gles2/demo-3d-monument.fs.glsl",
vertex: "/glsl/gles2/demo-3d-monument.vs.glsl",
},
direct3DCurve: {
fragment: "/glsl/gles2/direct-curve.fs.glsl",
vertex: "/glsl/gles2/direct-3d-curve.vs.glsl",
},
direct3DInterior: {
fragment: "/glsl/gles2/direct-interior.fs.glsl",
vertex: "/glsl/gles2/direct-3d-interior.vs.glsl",
},
directCurve: {
fragment: "/glsl/gles2/direct-curve.fs.glsl",
vertex: "/glsl/gles2/direct-curve.vs.glsl",
},
directInterior: {
fragment: "/glsl/gles2/direct-interior.fs.glsl",
vertex: "/glsl/gles2/direct-interior.vs.glsl",
},
ecaaCover: {
fragment: "/glsl/gles2/ecaa-cover.fs.glsl",
vertex: "/glsl/gles2/ecaa-cover.vs.glsl",
},
ecaaCurve: {
fragment: "/glsl/gles2/ecaa-curve.fs.glsl",
vertex: "/glsl/gles2/ecaa-curve.vs.glsl",
},
ecaaEdgeDetect: {
fragment: "/glsl/gles2/ecaa-edge-detect.fs.glsl",
vertex: "/glsl/gles2/ecaa-edge-detect.vs.glsl",
},
ecaaLine: {
fragment: "/glsl/gles2/ecaa-line.fs.glsl",
vertex: "/glsl/gles2/ecaa-line.vs.glsl",
},
ecaaMonoResolve: {
fragment: "/glsl/gles2/ecaa-mono-resolve.fs.glsl",
vertex: "/glsl/gles2/ecaa-mono-resolve.vs.glsl",
},
ecaaMonoSubpixelResolve: {
fragment: "/glsl/gles2/ecaa-mono-subpixel-resolve.fs.glsl",
vertex: "/glsl/gles2/ecaa-mono-subpixel-resolve.vs.glsl",
},
ecaaMultiResolve: {
fragment: "/glsl/gles2/ecaa-multi-resolve.fs.glsl",
vertex: "/glsl/gles2/ecaa-multi-resolve.vs.glsl",
},
ssaaSubpixelResolve: {
fragment: "/glsl/gles2/ssaa-subpixel-resolve.fs.glsl",
vertex: "/glsl/gles2/ssaa-subpixel-resolve.vs.glsl",
},
};
@ -122,31 +122,35 @@ interface ShaderProgramURLs {
}
export class ShaderLoader {
common: Promise<string>;
shaders: Promise<ShaderMap<ShaderProgramSource>>;
load() {
this.common = window.fetch(COMMON_SHADER_URL).then(response => response.text());
const shaderKeys = Object.keys(SHADER_URLS) as Array<keyof ShaderMap<string>>;
let promises = [];
const promises = [];
for (const shaderKey of shaderKeys) {
promises.push(Promise.all([
window.fetch(SHADER_URLS[shaderKey].vertex).then(response => response.text()),
window.fetch(SHADER_URLS[shaderKey].fragment).then(response => response.text()),
]).then(results => { return { vertex: results[0], fragment: results[1] } }));
]).then(results => ({ vertex: results[0], fragment: results[1] })));
}
this.shaders = Promise.all(promises).then(promises => {
let shaderMap: Partial<ShaderMap<ShaderProgramSource>> = {};
const shaderMap: Partial<ShaderMap<ShaderProgramSource>> = {};
for (let keyIndex = 0; keyIndex < shaderKeys.length; keyIndex++)
shaderMap[shaderKeys[keyIndex]] = promises[keyIndex];
return shaderMap as ShaderMap<ShaderProgramSource>;
});
}
common: Promise<string>;
shaders: Promise<ShaderMap<ShaderProgramSource>>;
}
export class PathfinderShaderProgram {
readonly uniforms: UniformMap;
readonly attributes: AttributeMap;
readonly program: WebGLProgram;
constructor(gl: WebGLRenderingContext,
programName: string,
unlinkedShaderProgram: UnlinkedShaderProgram) {
@ -155,7 +159,7 @@ export class PathfinderShaderProgram {
gl.attachShader(this.program, compiledShader);
gl.linkProgram(this.program);
if (gl.getProgramParameter(this.program, gl.LINK_STATUS) == 0) {
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}`);
}
@ -163,8 +167,8 @@ export class PathfinderShaderProgram {
const uniformCount = gl.getProgramParameter(this.program, gl.ACTIVE_UNIFORMS);
const attributeCount = gl.getProgramParameter(this.program, gl.ACTIVE_ATTRIBUTES);
let uniforms: UniformMap = {};
let attributes: AttributeMap = {};
const uniforms: UniformMap = {};
const attributes: AttributeMap = {};
for (let uniformIndex = 0; uniformIndex < uniformCount; uniformIndex++) {
const uniformName = unwrapNull(gl.getActiveUniform(this.program, uniformIndex)).name;
@ -179,8 +183,4 @@ export class PathfinderShaderProgram {
this.uniforms = uniforms;
this.attributes = attributes;
}
readonly uniforms: UniformMap;
readonly attributes: AttributeMap;
readonly program: WebGLProgram;
}

View File

@ -11,11 +11,20 @@
import * as glmatrix from 'gl-matrix';
import {AntialiasingStrategy} from './aa-strategy';
import {createFramebufferDepthTexture, createFramebuffer, setTextureParameters} from './gl-utils';
import {createFramebuffer, createFramebufferDepthTexture, setTextureParameters} from './gl-utils';
import {unwrapNull} from './utils';
import {PathfinderDemoView} from './view';
export default class SSAAStrategy extends AntialiasingStrategy {
private level: number;
private subpixelAA: boolean;
private destFramebufferSize: glmatrix.vec2;
private supersampledFramebufferSize: glmatrix.vec2;
private supersampledColorTexture: WebGLTexture;
private supersampledDepthTexture: WebGLTexture;
private supersampledFramebuffer: WebGLFramebuffer;
constructor(level: number, subpixelAA: boolean) {
super();
this.level = level;
@ -111,7 +120,7 @@ export default class SSAAStrategy extends AntialiasingStrategy {
}
private get supersampleScale(): glmatrix.vec2 {
return glmatrix.vec2.fromValues(this.subpixelAA ? 3 : 2, this.level == 2 ? 1 : 2);
return glmatrix.vec2.fromValues(this.subpixelAA ? 3 : 2, this.level === 2 ? 1 : 2);
}
private usedSupersampledFramebufferSize(view: PathfinderDemoView): glmatrix.vec2 {
@ -119,14 +128,4 @@ export default class SSAAStrategy extends AntialiasingStrategy {
glmatrix.vec2.mul(result, view.destUsedSize, this.supersampleScale);
return result;
}
private level: number;
private subpixelAA: boolean;
private destFramebufferSize: glmatrix.vec2;
private supersampledFramebufferSize: glmatrix.vec2;
private supersampledColorTexture: WebGLTexture;
private supersampledDepthTexture: WebGLTexture;
private supersampledFramebuffer: WebGLFramebuffer;
}

View File

@ -11,17 +11,17 @@
import * as glmatrix from 'gl-matrix';
import * as _ from 'lodash';
import {DemoAppController} from './app-controller';
import {AntialiasingStrategy, AntialiasingStrategyName, NoAAStrategy} from "./aa-strategy";
import {DemoAppController} from './app-controller';
import PathfinderBufferTexture from "./buffer-texture";
import {OrthographicCamera} from "./camera";
import {ECAAStrategy, ECAAMulticolorStrategy} from "./ecaa-strategy";
import {ECAAMulticolorStrategy, ECAAStrategy} from "./ecaa-strategy";
import {PathfinderMeshData} from "./meshes";
import {ShaderMap, ShaderProgramSource} from './shader-loader';
import { SVGLoader, BUILTIN_SVG_URI } from './svg-loader';
import SSAAStrategy from "./ssaa-strategy";
import {BUILTIN_SVG_URI, SVGLoader} from './svg-loader';
import {panic, unwrapNull} from './utils';
import {PathfinderDemoView, Timings} from './view';
import SSAAStrategy from "./ssaa-strategy";
import PathfinderBufferTexture from "./buffer-texture";
const parseColor = require('parse-color');
@ -30,9 +30,9 @@ const SVG_NS: string = "http://www.w3.org/2000/svg";
const DEFAULT_FILE: string = 'tiger';
const ANTIALIASING_STRATEGIES: AntialiasingStrategyTable = {
ecaa: ECAAMulticolorStrategy,
none: NoAAStrategy,
ssaa: SSAAStrategy,
ecaa: ECAAMulticolorStrategy,
};
interface AntialiasingStrategyTable {
@ -42,6 +42,12 @@ interface AntialiasingStrategyTable {
}
class SVGDemoController extends DemoAppController<SVGDemoView> {
loader: SVGLoader;
protected readonly builtinFileURI: string = BUILTIN_SVG_URI;
private meshes: PathfinderMeshData;
start() {
super.start();
@ -55,7 +61,7 @@ class SVGDemoController extends DemoAppController<SVGDemoView> {
this.loader.partition().then(meshes => {
this.meshes = meshes;
this.meshesReceived();
})
});
}
protected createView() {
@ -64,8 +70,6 @@ class SVGDemoController extends DemoAppController<SVGDemoView> {
unwrapNull(this.shaderSources));
}
protected readonly builtinFileURI: string = BUILTIN_SVG_URI;
protected get defaultFile(): string {
return DEFAULT_FILE;
}
@ -78,15 +82,19 @@ class SVGDemoController extends DemoAppController<SVGDemoView> {
view.camera.bounds = this.loader.bounds;
view.camera.zoomToFit();
})
});
}
loader: SVGLoader;
private meshes: PathfinderMeshData;
}
class SVGDemoView extends PathfinderDemoView {
camera: OrthographicCamera;
protected depthFunction: number = this.gl.GREATER;
protected usedSizeFactor: glmatrix.vec2 = glmatrix.vec2.fromValues(1.0, 1.0);
private appController: SVGDemoController;
constructor(appController: SVGDemoController,
commonShaderSource: string,
shaderSources: ShaderMap<ShaderProgramSource>) {
@ -156,8 +164,6 @@ class SVGDemoView extends PathfinderDemoView {
this.appController.newTimingsReceived(_.pick(this.lastTimings, ['rendering']));
}
protected usedSizeFactor: glmatrix.vec2 = glmatrix.vec2.fromValues(1.0, 1.0);
protected get worldTransform() {
const transform = glmatrix.mat4.create();
const translation = this.camera.translation;
@ -177,12 +183,6 @@ class SVGDemoView extends PathfinderDemoView {
protected get directInteriorProgramName(): keyof ShaderMap<void> {
return 'directInterior';
}
protected depthFunction: number = this.gl.GREATER;
private appController: SVGDemoController;
camera: OrthographicCamera;
}
function main() {

View File

@ -12,8 +12,8 @@ import * as glmatrix from 'gl-matrix';
import * as _ from 'lodash';
import 'path-data-polyfill.js';
import {panic, unwrapNull} from "./utils";
import {PathfinderMeshData} from "./meshes";
import {panic, unwrapNull} from "./utils";
export const BUILTIN_SVG_URI: string = "/svg/demo";
@ -39,6 +39,15 @@ export interface PathInstance {
}
export class SVGLoader {
pathInstances: PathInstance[];
scale: number;
bounds: glmatrix.vec4;
private svg: SVGSVGElement;
private fileData: ArrayBuffer;
private paths: any[];
constructor() {
this.scale = 1.0;
this.svg = unwrapNull(document.getElementById('pf-svg')) as Element as SVGSVGElement;
@ -57,6 +66,22 @@ export class SVGLoader {
this.attachSVG(svgElement);
}
partition(pathIndex?: number | undefined): Promise<PathfinderMeshData> {
// Make the request.
const paths = pathIndex == null ? this.paths : [this.paths[pathIndex]];
return window.fetch(PARTITION_SVG_PATHS_ENDPOINT_URL, {
body: JSON.stringify({ paths: paths }),
headers: {'Content-Type': 'application/json'},
method: 'POST',
}).then(response => response.text()).then(responseText => {
const response = JSON.parse(responseText);
if (!('Ok' in response))
panic("Failed to partition the font!");
const meshes = response.Ok.pathData;
return new PathfinderMeshData(meshes);
});
}
private attachSVG(svgElement: SVGSVGElement) {
// Clear out the current document.
let kid;
@ -69,7 +94,7 @@ export class SVGLoader {
// Scan for geometry elements.
this.pathInstances.length = 0;
const queue: Array<Element> = [this.svg];
const queue: Element[] = [this.svg];
let element;
while ((element = queue.pop()) != null) {
let kid = element.lastChild;
@ -86,7 +111,7 @@ export class SVGLoader {
if (style.stroke !== 'none') {
this.pathInstances.push({
element: element,
stroke: parseInt(style.strokeWidth!),
stroke: parseInt(style.strokeWidth!, 10),
});
}
}
@ -134,28 +159,4 @@ export class SVGLoader {
this.bounds = glmatrix.vec4.clone([minX, minY, maxX, maxY]);
}
partition(pathIndex?: number | undefined): Promise<PathfinderMeshData> {
// Make the request.
const paths = pathIndex == null ? this.paths : [this.paths[pathIndex]];
return window.fetch(PARTITION_SVG_PATHS_ENDPOINT_URL, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ paths: paths }),
}).then(response => response.text()).then(responseText => {
const response = JSON.parse(responseText);
if (!('Ok' in response))
panic("Failed to partition the font!");
const meshes = response.Ok.pathData;
return new PathfinderMeshData(meshes);
});
}
private svg: SVGSVGElement;
private fileData: ArrayBuffer;
scale: number;
pathInstances: PathInstance[];
private paths: any[];
bounds: glmatrix.vec4;
}

View File

@ -8,13 +8,15 @@
// 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 _ from 'lodash';
import * as opentype from 'opentype.js';
import {Metrics} from 'opentype.js';
import {AntialiasingStrategy, AntialiasingStrategyName, NoAAStrategy} from './aa-strategy';
import {DemoAppController} from './app-controller';
import PathfinderBufferTexture from './buffer-texture';
import {OrthographicCamera} from "./camera";
import {ECAAMonochromeStrategy, ECAAStrategy} from './ecaa-strategy';
import {createFramebuffer, createFramebufferColorTexture} from './gl-utils';
@ -22,14 +24,12 @@ import {createFramebufferDepthTexture, QUAD_ELEMENTS, setTextureParameters} from
import {UniformMap} from './gl-utils';
import {PathfinderMeshBuffers, PathfinderMeshData} from './meshes';
import {PathfinderShaderProgram, ShaderMap, ShaderProgramSource} from './shader-loader';
import {BUILTIN_FONT_URI, Hint, SimpleTextLayout, GlyphStore, calculatePixelXMin} from "./text";
import SSAAStrategy from './ssaa-strategy';
import {calculatePixelDescent, calculatePixelRectForGlyph, PathfinderFont} from "./text";
import {PathfinderError, UINT32_SIZE, assert, expectNotNull, scaleRect, panic} from './utils';
import {BUILTIN_FONT_URI, calculatePixelXMin, GlyphStore, Hint, SimpleTextLayout} from "./text";
import {assert, expectNotNull, panic, PathfinderError, scaleRect, UINT32_SIZE} from './utils';
import {unwrapNull} from './utils';
import { MonochromePathfinderView, Timings, TIMINGS } from './view';
import PathfinderBufferTexture from './buffer-texture';
import SSAAStrategy from './ssaa-strategy';
import { Metrics } from 'opentype.js';
const DEFAULT_TEXT: string =
`Twas brillig, and the slithy toves
@ -123,6 +123,24 @@ function rectsIntersect(a: glmatrix.vec4, b: glmatrix.vec4): boolean {
}
class TextDemoController extends DemoAppController<TextDemoView> {
font: PathfinderFont;
layout: SimpleTextLayout;
glyphStore: GlyphStore;
atlasGlyphs: AtlasGlyph[];
private hintingSelect: HTMLSelectElement;
private editTextModal: HTMLElement;
private editTextArea: HTMLTextAreaElement;
private _atlas: Atlas;
private meshes: PathfinderMeshData;
private _fontSize: number;
private text: string;
constructor() {
super();
this.text = DEFAULT_TEXT;
@ -154,14 +172,8 @@ class TextDemoController extends DemoAppController<TextDemoView> {
window.jQuery(this.editTextModal).modal();
}
private hintingChanged(): void {
this.view.then(view => view.updateHinting());
}
private updateText(): void {
this.text = this.editTextArea.value;
window.jQuery(this.editTextModal).modal('hide');
createHint(): Hint {
return new Hint(this.font, this.pixelsPerUnit, this.useHinting);
}
protected createView() {
@ -175,6 +187,16 @@ class TextDemoController extends DemoAppController<TextDemoView> {
this.recreateLayout(font);
}
private hintingChanged(): void {
this.view.then(view => view.updateHinting());
}
private updateText(): void {
this.text = this.editTextArea.value;
window.jQuery(this.editTextModal).modal('hide');
}
private recreateLayout(font: PathfinderFont) {
const newLayout = new SimpleTextLayout(font, this.text);
@ -220,10 +242,6 @@ class TextDemoController extends DemoAppController<TextDemoView> {
return this.hintingSelect.selectedIndex !== 0;
}
createHint(): Hint {
return new Hint(this.font, this.pixelsPerUnit, this.useHinting);
}
protected get builtinFileURI(): string {
return BUILTIN_FONT_URI;
}
@ -232,27 +250,25 @@ class TextDemoController extends DemoAppController<TextDemoView> {
return DEFAULT_FONT;
}
font: PathfinderFont;
private hintingSelect: HTMLSelectElement;
private editTextModal: HTMLElement;
private editTextArea: HTMLTextAreaElement;
private _atlas: Atlas;
atlasGlyphs: AtlasGlyph[];
private meshes: PathfinderMeshData;
private _fontSize: number;
private text: string;
layout: SimpleTextLayout;
glyphStore: GlyphStore;
}
class TextDemoView extends MonochromePathfinderView {
atlasFramebuffer: WebGLFramebuffer;
atlasDepthTexture: WebGLTexture;
glyphPositionsBuffer: WebGLBuffer;
glyphTexCoordsBuffer: WebGLBuffer;
glyphElementsBuffer: WebGLBuffer;
appController: TextDemoController;
camera: OrthographicCamera;
readonly bgColor: glmatrix.vec4 = glmatrix.vec4.fromValues(1.0, 1.0, 1.0, 0.0);
readonly fgColor: glmatrix.vec4 = glmatrix.vec4.fromValues(0.0, 0.0, 0.0, 1.0);
protected depthFunction: number = this.gl.GREATER;
constructor(appController: TextDemoController,
commonShaderSource: string,
shaderSources: ShaderMap<ShaderProgramSource>) {
@ -261,13 +277,40 @@ class TextDemoView extends MonochromePathfinderView {
this.appController = appController;
this.camera = new OrthographicCamera(this.canvas, {
minScale: MIN_SCALE,
maxScale: MAX_SCALE,
minScale: MIN_SCALE,
});
this.canvas.addEventListener('dblclick', () => this.appController.showTextEditor(), false);
}
attachText() {
this.panZoomEventsEnabled = false;
if (this.atlasFramebuffer == null)
this.createAtlasFramebuffer();
this.layoutText();
this.camera.zoomToFit();
this.appController.fontSize = this.camera.scale *
this.appController.font.opentypeFont.unitsPerEm;
this.buildAtlasGlyphs();
this.setDirty();
this.panZoomEventsEnabled = true;
}
relayoutText() {
this.layoutText();
this.buildAtlasGlyphs();
this.setDirty();
}
updateHinting(): void {
this.buildAtlasGlyphs();
this.setDirty();
}
protected initContext() {
super.initContext();
}
@ -287,12 +330,113 @@ class TextDemoView extends MonochromePathfinderView {
return pathColors;
}
protected pathTransformsForObject(objectIndex: number): Float32Array {
const glyphCount = this.appController.glyphStore.glyphIDs.length;
const atlasGlyphs = this.appController.atlasGlyphs;
const pixelsPerUnit = this.appController.pixelsPerUnit;
const transforms = new Float32Array((glyphCount + 1) * 4);
for (const glyph of atlasGlyphs) {
const pathID = glyph.glyphStoreIndex + 1;
const atlasOrigin = glyph.calculatePixelOrigin(pixelsPerUnit);
transforms[pathID * 4 + 0] = pixelsPerUnit;
transforms[pathID * 4 + 1] = pixelsPerUnit;
transforms[pathID * 4 + 2] = atlasOrigin[0];
transforms[pathID * 4 + 3] = atlasOrigin[1];
}
return transforms;
}
protected onPan() {
this.buildAtlasGlyphs();
this.setDirty();
}
protected onZoom() {
this.appController.fontSize = this.camera.scale *
this.appController.font.opentypeFont.unitsPerEm;
this.buildAtlasGlyphs();
this.setDirty();
}
protected compositeIfNecessary() {
// 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.SCISSOR_TEST);
this.gl.blendEquation(this.gl.FUNC_ADD);
this.gl.blendFuncSeparate(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA,
this.gl.ONE, this.gl.ONE);
this.gl.enable(this.gl.BLEND);
// 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.camera.translation[0],
this.camera.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.layout.textFrame.totalGlyphCount * 6,
this.gl.UNSIGNED_INT,
0);
}
protected clearForDirectRendering(): void {
this.gl.clearColor(0.0, 0.0, 0.0, 0.0);
this.gl.clearDepth(0.0);
this.gl.depthMask(true);
this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
}
protected createAAStrategy(aaType: AntialiasingStrategyName,
aaLevel: number,
subpixelAA: boolean):
AntialiasingStrategy {
return new (ANTIALIASING_STRATEGIES[aaType])(aaLevel, subpixelAA);
}
protected newTimingsReceived() {
this.appController.newTimingsReceived(this.lastTimings);
}
/// Lays out glyphs on the canvas.
private layoutText() {
const layout = this.appController.layout;
layout.layoutRuns();
let textBounds = layout.textFrame.bounds;
const textBounds = layout.textFrame.bounds;
this.camera.bounds = textBounds;
const totalGlyphCount = layout.textFrame.totalGlyphCount;
@ -389,28 +533,6 @@ class TextDemoView extends MonochromePathfinderView {
this.setGlyphTexCoords();
}
protected pathTransformsForObject(objectIndex: number): Float32Array {
const glyphCount = this.appController.glyphStore.glyphIDs.length;
const atlasGlyphs = this.appController.atlasGlyphs;
const pixelsPerUnit = this.appController.pixelsPerUnit;
const transforms = new Float32Array((glyphCount + 1) * 4);
for (let glyphIndex = 0; glyphIndex < atlasGlyphs.length; glyphIndex++) {
const glyph = atlasGlyphs[glyphIndex];
const pathID = glyph.glyphStoreIndex + 1;
const atlasOrigin = glyph.calculatePixelOrigin(pixelsPerUnit);
transforms[pathID * 4 + 0] = pixelsPerUnit;
transforms[pathID * 4 + 1] = pixelsPerUnit;
transforms[pathID * 4 + 2] = atlasOrigin[0];
transforms[pathID * 4 + 3] = atlasOrigin[1];
}
return transforms;
}
private createAtlasFramebuffer() {
const atlasColorTexture = this.appController.atlas.ensureTexture(this.gl);
this.atlasDepthTexture = createFramebufferDepthTexture(this.gl, ATLAS_SIZE);
@ -443,7 +565,7 @@ class TextDemoView extends MonochromePathfinderView {
glyphIndex++, globalGlyphIndex++) {
const textGlyphID = run.glyphIDs[glyphIndex];
let atlasGlyphIndex = _.sortedIndexOf(atlasGlyphIDs, textGlyphID);
const atlasGlyphIndex = _.sortedIndexOf(atlasGlyphIDs, textGlyphID);
if (atlasGlyphIndex < 0)
continue;
@ -477,45 +599,6 @@ class TextDemoView extends MonochromePathfinderView {
this.gl.bufferData(this.gl.ARRAY_BUFFER, glyphTexCoords, this.gl.STATIC_DRAW);
}
attachText() {
this.panZoomEventsEnabled = false;
if (this.atlasFramebuffer == null)
this.createAtlasFramebuffer();
this.layoutText();
this.camera.zoomToFit();
this.appController.fontSize = this.camera.scale *
this.appController.font.opentypeFont.unitsPerEm;
this.buildAtlasGlyphs();
this.setDirty();
this.panZoomEventsEnabled = true;
}
relayoutText() {
this.layoutText();
this.buildAtlasGlyphs();
this.setDirty();
}
protected onPan() {
this.buildAtlasGlyphs();
this.setDirty();
}
protected onZoom() {
this.appController.fontSize = this.camera.scale *
this.appController.font.opentypeFont.unitsPerEm;
this.buildAtlasGlyphs();
this.setDirty();
}
updateHinting(): void {
this.buildAtlasGlyphs();
this.setDirty();
}
private setIdentityTexScaleUniform(uniforms: UniformMap) {
this.gl.uniform2f(uniforms.uTexScale, 1.0, 1.0);
}
@ -526,64 +609,6 @@ class TextDemoView extends MonochromePathfinderView {
return usedSize;
}
protected compositeIfNecessary() {
// 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.SCISSOR_TEST);
this.gl.blendEquation(this.gl.FUNC_ADD);
this.gl.blendFuncSeparate(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA,
this.gl.ONE, this.gl.ONE);
this.gl.enable(this.gl.BLEND);
// 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.camera.translation[0],
this.camera.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.layout.textFrame.totalGlyphCount * 6,
this.gl.UNSIGNED_INT,
0);
}
protected clearForDirectRendering(): void {
this.gl.clearColor(0.0, 0.0, 0.0, 0.0);
this.gl.clearDepth(0.0);
this.gl.depthMask(true);
this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
}
private set panZoomEventsEnabled(flag: boolean) {
if (flag) {
this.camera.onPan = () => this.onPan();
@ -594,9 +619,6 @@ class TextDemoView extends MonochromePathfinderView {
}
}
readonly bgColor: glmatrix.vec4 = glmatrix.vec4.fromValues(1.0, 1.0, 1.0, 0.0);
readonly fgColor: glmatrix.vec4 = glmatrix.vec4.fromValues(0.0, 0.0, 0.0, 1.0);
get destFramebuffer(): WebGLFramebuffer {
return this.atlasFramebuffer;
}
@ -609,17 +631,6 @@ class TextDemoView extends MonochromePathfinderView {
return this.appController.atlas.usedSize;
}
protected createAAStrategy(aaType: AntialiasingStrategyName,
aaLevel: number,
subpixelAA: boolean):
AntialiasingStrategy {
return new (ANTIALIASING_STRATEGIES[aaType])(aaLevel, subpixelAA);
}
protected newTimingsReceived() {
this.appController.newTimingsReceived(this.lastTimings);
}
protected get worldTransform(): glmatrix.mat4 {
const transform = glmatrix.mat4.create();
glmatrix.mat4.translate(transform, transform, [-1.0, -1.0, 0.0]);
@ -634,19 +645,6 @@ class TextDemoView extends MonochromePathfinderView {
protected get directInteriorProgramName(): keyof ShaderMap<void> {
return 'directInterior';
}
protected depthFunction: number = this.gl.GREATER;
atlasFramebuffer: WebGLFramebuffer;
atlasDepthTexture: WebGLTexture;
glyphPositionsBuffer: WebGLBuffer;
glyphTexCoordsBuffer: WebGLBuffer;
glyphElementsBuffer: WebGLBuffer;
appController: TextDemoController;
camera: OrthographicCamera;
}
interface AntialiasingStrategyTable {
@ -656,6 +654,9 @@ interface AntialiasingStrategyTable {
}
class Atlas {
private _texture: WebGLTexture | null;
private _usedSize: Size2D;
constructor() {
this._texture = null;
this._usedSize = glmatrix.vec2.create();
@ -725,12 +726,13 @@ class Atlas {
get usedSize(): glmatrix.vec2 {
return this._usedSize;
}
private _texture: WebGLTexture | null;
private _usedSize: Size2D;
}
class AtlasGlyph {
readonly glyphStoreIndex: number;
readonly glyphID: number;
readonly origin: glmatrix.vec2;
constructor(glyphStoreIndex: number, glyphID: number) {
this.glyphStoreIndex = glyphStoreIndex;
this.glyphID = glyphID;
@ -756,16 +758,12 @@ class AtlasGlyph {
private setPixelOrigin(pixelOrigin: glmatrix.vec2, pixelsPerUnit: number): void {
glmatrix.vec2.scale(this.origin, pixelOrigin, 1.0 / pixelsPerUnit);
}
readonly glyphStoreIndex: number;
readonly glyphID: number;
readonly origin: glmatrix.vec2;
}
const ANTIALIASING_STRATEGIES: AntialiasingStrategyTable = {
ecaa: ECAAMonochromeStrategy,
none: NoAAStrategy,
ssaa: SSAAStrategy,
ecaa: ECAAMonochromeStrategy,
};
function main() {

View File

@ -8,14 +8,14 @@
// option. This file may not be copied, modified, or distributed
// except according to those terms.
import {Metrics} from 'opentype.js';
import * as base64js from 'base64-js';
import * as glmatrix from 'gl-matrix';
import * as _ from 'lodash';
import * as opentype from "opentype.js";
import {Metrics} from 'opentype.js';
import {B_QUAD_SIZE, PathfinderMeshData} from "./meshes";
import { UINT32_SIZE, UINT32_MAX, assert, panic, unwrapNull } from "./utils";
import {assert, panic, UINT32_MAX, UINT32_SIZE, unwrapNull} from "./utils";
export const BUILTIN_FONT_URI: string = "/otf/demo";
@ -26,8 +26,8 @@ export interface ExpandedMeshData {
}
export interface PartitionResult {
meshes: PathfinderMeshData,
time: number,
meshes: PathfinderMeshData;
time: number;
}
export interface PixelMetrics {
@ -39,7 +39,7 @@ export interface PixelMetrics {
opentype.Font.prototype.isSupported = function() {
return (this as any).supported;
}
};
opentype.Font.prototype.lineHeight = function() {
const os2Table = this.tables.os2;
@ -47,6 +47,11 @@ opentype.Font.prototype.lineHeight = function() {
};
export class PathfinderFont {
readonly opentypeFont: opentype.Font;
readonly data: ArrayBuffer;
private metricsCache: Metrics[];
constructor(data: ArrayBuffer) {
this.data = data;
@ -62,14 +67,15 @@ export class PathfinderFont {
this.metricsCache[glyphID] = this.opentypeFont.glyphs.get(glyphID).getMetrics();
return this.metricsCache[glyphID];
}
readonly opentypeFont: opentype.Font;
readonly data: ArrayBuffer;
private metricsCache: Metrics[];
}
export class TextRun {
readonly glyphIDs: number[];
advances: number[];
readonly origin: number[];
private readonly font: PathfinderFont;
constructor(text: number[] | string, origin: number[], font: PathfinderFont) {
if (typeof(text) === 'string') {
this.glyphIDs = font.opentypeFont
@ -93,12 +99,6 @@ export class TextRun {
}
}
private pixelMetricsForGlyphAt(index: number, pixelsPerUnit: number, hint: Hint):
PixelMetrics {
const metrics = unwrapNull(this.font.metricsForGlyph(index));
return calculatePixelMetricsForGlyph(metrics, pixelsPerUnit, hint);
}
calculatePixelOriginForGlyphAt(index: number, pixelsPerUnit: number, hint: Hint):
glmatrix.vec2 {
const textGlyphOrigin = glmatrix.vec2.clone(this.origin);
@ -121,13 +121,19 @@ export class TextRun {
return lastAdvance + this.font.opentypeFont.glyphs.get(lastGlyphID).advanceWidth;
}
readonly glyphIDs: number[];
advances: number[];
readonly origin: number[];
private readonly font: PathfinderFont;
private pixelMetricsForGlyphAt(index: number, pixelsPerUnit: number, hint: Hint):
PixelMetrics {
const metrics = unwrapNull(this.font.metricsForGlyph(index));
return calculatePixelMetricsForGlyph(metrics, pixelsPerUnit, hint);
}
}
export class TextFrame {
readonly runs: TextRun[];
readonly origin: glmatrix.vec3;
private readonly font: PathfinderFont;
constructor(runs: TextRun[], font: PathfinderFont) {
this.runs = runs;
this.origin = glmatrix.vec3.create();
@ -137,8 +143,7 @@ export class TextFrame {
expandMeshes(meshes: PathfinderMeshData, glyphIDs: number[]): ExpandedMeshData {
const pathIDs = [];
for (const textRun of this.runs) {
for (let glyphIndex = 0; glyphIndex < textRun.glyphIDs.length; glyphIndex++) {
const glyphID = textRun.glyphIDs[glyphIndex];
for (const glyphID of textRun.glyphIDs) {
if (glyphID === 0)
continue;
const pathID = _.sortedIndexOf(glyphIDs, glyphID);
@ -180,15 +185,13 @@ export class TextFrame {
glyphIDs.push(...run.glyphIDs);
return glyphIDs;
}
readonly runs: TextRun[];
readonly origin: glmatrix.vec3;
private readonly font: PathfinderFont;
}
/// Stores one copy of each glyph.
export class GlyphStore {
readonly font: PathfinderFont;
readonly glyphIDs: number[];
constructor(font: PathfinderFont, glyphIDs: number[]) {
this.font = font;
this.glyphIDs = glyphIDs;
@ -209,9 +212,9 @@ export class GlyphStore {
// Make the request.
return window.fetch(PARTITION_FONT_ENDPOINT_URI, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(request),
headers: {'Content-Type': 'application/json'},
method: 'POST',
}).then(response => response.text()).then(responseText => {
const response = JSON.parse(responseText);
if (!('Ok' in response))
@ -227,12 +230,11 @@ export class GlyphStore {
const index = _.sortedIndexOf(this.glyphIDs, glyphID);
return index >= 0 ? index : null;
}
readonly font: PathfinderFont;
readonly glyphIDs: number[];
}
export class SimpleTextLayout {
readonly textFrame: TextFrame;
constructor(font: PathfinderFont, text: string) {
const lineHeight = font.opentypeFont.lineHeight();
const textRuns: TextRun[] = text.split("\n").map((line, lineNumber) => {
@ -244,11 +246,14 @@ export class SimpleTextLayout {
layoutRuns() {
this.textFrame.runs.forEach(textRun => textRun.layout());
}
readonly textFrame: TextFrame;
}
export class Hint {
readonly xHeight: number;
readonly hintedXHeight: number;
private useHinting: boolean;
constructor(font: PathfinderFont, pixelsPerUnit: number, useHinting: boolean) {
this.useHinting = useHinting;
@ -278,10 +283,6 @@ export class Hint {
return glmatrix.vec2.fromValues(position[0],
position[1] / this.xHeight * this.hintedXHeight);
}
readonly xHeight: number;
readonly hintedXHeight: number;
private useHinting: boolean;
}
export function calculatePixelXMin(metrics: Metrics, pixelsPerUnit: number): number {
@ -296,10 +297,10 @@ function calculatePixelMetricsForGlyph(metrics: Metrics, pixelsPerUnit: number,
PixelMetrics {
const top = hint.hintPosition(glmatrix.vec2.fromValues(0, metrics.yMax))[1];
return {
left: calculatePixelXMin(metrics, pixelsPerUnit),
right: Math.ceil(metrics.xMax * pixelsPerUnit),
ascent: Math.ceil(top * pixelsPerUnit),
descent: calculatePixelDescent(metrics, pixelsPerUnit),
left: calculatePixelXMin(metrics, pixelsPerUnit),
right: Math.ceil(metrics.xMax * pixelsPerUnit),
};
}

View File

@ -11,13 +11,13 @@
import * as glmatrix from 'gl-matrix';
import {AntialiasingStrategy, AntialiasingStrategyName, NoAAStrategy} from "./aa-strategy";
import PathfinderBufferTexture from './buffer-texture';
import {Camera} from "./camera";
import {QUAD_ELEMENTS, UniformMap} from './gl-utils';
import {PathfinderMeshBuffers, PathfinderMeshData} from './meshes';
import {PathfinderShaderProgram, SHADER_NAMES, ShaderMap} from './shader-loader';
import {ShaderProgramSource, UnlinkedShaderProgram} from './shader-loader';
import {PathfinderError, UINT32_SIZE, expectNotNull, unwrapNull} from './utils';
import PathfinderBufferTexture from './buffer-texture';
import {expectNotNull, PathfinderError, UINT32_SIZE, unwrapNull} from './utils';
const TIME_INTERVAL_DELAY: number = 32;
@ -40,18 +40,24 @@ const QUAD_TEX_COORDS: Float32Array = new Float32Array([
]);
export const TIMINGS: {[name: string]: string} = {
rendering: "Rendering",
compositing: "Compositing",
}
rendering: "Rendering",
};
export interface Timings {
rendering: number;
compositing: number;
rendering: number;
}
declare class WebGLQuery {}
export abstract class PathfinderView {
protected canvas: HTMLCanvasElement;
protected camera: Camera;
private dirty: boolean;
constructor() {
this.dirty = false;
@ -61,6 +67,29 @@ export abstract class PathfinderView {
this.resizeToFit(true);
}
zoomIn(): void {
this.camera.zoomIn();
}
zoomOut(): void {
this.camera.zoomOut();
}
protected resized(): void {
this.setDirty();
}
protected setDirty() {
if (this.dirty)
return;
this.dirty = true;
window.requestAnimationFrame(() => this.redraw());
}
protected redraw() {
this.dirty = false;
}
private resizeToFit(initialSize: boolean) {
const width = window.innerWidth;
@ -84,38 +113,42 @@ export abstract class PathfinderView {
this.resized();
}
protected resized(): void {
this.setDirty();
}
protected setDirty() {
if (this.dirty)
return;
this.dirty = true;
window.requestAnimationFrame(() => this.redraw());
}
protected redraw() {
this.dirty = false;
}
zoomIn(): void {
this.camera.zoomIn();
}
zoomOut(): void {
this.camera.zoomOut();
}
protected canvas: HTMLCanvasElement;
protected camera: Camera;
private dirty: boolean;
}
export abstract class PathfinderDemoView extends PathfinderView {
gl: WebGLRenderingContext;
shaderPrograms: ShaderMap<PathfinderShaderProgram>;
drawBuffersExt: any;
instancedArraysExt: any;
textureHalfFloatExt: any;
vertexArrayObjectExt: any;
quadPositionsBuffer: WebGLBuffer;
quadTexCoordsBuffer: WebGLBuffer;
quadElementsBuffer: WebGLBuffer;
meshes: PathfinderMeshBuffers[];
meshData: PathfinderMeshData[];
pathTransformBufferTextures: PathfinderBufferTexture[];
pathHintsBufferTexture: PathfinderBufferTexture | null;
protected timerQueryExt: any;
protected antialiasingStrategy: AntialiasingStrategy | null;
protected colorBufferHalfFloatExt: any;
protected pathColorsBufferTextures: PathfinderBufferTexture[];
protected lastTimings: Timings;
private atlasRenderingTimerQuery: WebGLQuery;
private compositingTimerQuery: WebGLQuery;
private timerQueryPollInterval: number | null;
private wantsScreenshot: boolean;
constructor(commonShaderSource: string, shaderSources: ShaderMap<ShaderProgramSource>) {
super();
@ -140,7 +173,7 @@ export abstract class PathfinderDemoView extends PathfinderView {
subpixelAA: boolean) {
this.antialiasingStrategy = this.createAAStrategy(aaType, aaLevel, subpixelAA);
let canvas = this.canvas;
const canvas = this.canvas;
this.antialiasingStrategy.init(this);
if (this.meshData != null)
this.antialiasingStrategy.attachMeshes(this);
@ -156,6 +189,82 @@ export abstract class PathfinderDemoView extends PathfinderView {
this.setDirty();
}
initQuadVAO(attributes: any) {
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.quadPositionsBuffer);
this.gl.vertexAttribPointer(attributes.aPosition, 2, this.gl.FLOAT, false, 0, 0);
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.quadTexCoordsBuffer);
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.quadElementsBuffer);
}
setFramebufferSizeUniform(uniforms: UniformMap) {
const currentViewport = this.gl.getParameter(this.gl.VIEWPORT);
this.gl.uniform2i(uniforms.uFramebufferSize, currentViewport[2], currentViewport[3]);
}
setTransformSTUniform(uniforms: UniformMap, objectIndex: number) {
// FIXME(pcwalton): Lossy conversion from a 4x4 matrix to an ST matrix is ugly and fragile.
// Refactor.
const transform = glmatrix.mat4.clone(this.worldTransform);
glmatrix.mat4.mul(transform, transform, this.getModelviewTransform(objectIndex));
const translation = glmatrix.vec4.clone([transform[12], transform[13], 0.0, 1.0]);
this.gl.uniform4f(uniforms.uTransformST,
transform[0],
transform[5],
transform[12],
transform[13]);
}
setTransformSTAndTexScaleUniformsForDest(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]);
}
setTransformAndTexScaleUniformsForDest(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]);
}
queueScreenshot() {
this.wantsScreenshot = true;
this.setDirty();
}
uploadPathColors(objectCount: number) {
this.pathColorsBufferTextures = [];
for (let objectIndex = 0; objectIndex < objectCount; objectIndex++) {
const pathColorsBufferTexture = new PathfinderBufferTexture(this.gl, 'uPathColors');
const pathColors = this.pathColorsForObject(objectIndex);
pathColorsBufferTexture.upload(this.gl, pathColors);
this.pathColorsBufferTextures.push(pathColorsBufferTexture);
}
}
uploadPathTransforms(objectCount: number) {
this.pathTransformBufferTextures = [];
for (let objectIndex = 0; objectIndex < objectCount; objectIndex++) {
const pathTransformBufferTexture = new PathfinderBufferTexture(this.gl,
'uPathTransform');
const pathTransforms = this.pathTransformsForObject(objectIndex);
pathTransformBufferTexture.upload(this.gl, pathTransforms);
this.pathTransformBufferTextures.push(pathTransformBufferTexture);
}
}
protected resized(): void {
super.resized();
@ -194,65 +303,6 @@ export abstract class PathfinderDemoView extends PathfinderView {
this.compositingTimerQuery = this.timerQueryExt.createQueryEXT();
}
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<'vertex' | 'fragment'>) {
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>;
}
initQuadVAO(attributes: any) {
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.quadPositionsBuffer);
this.gl.vertexAttribPointer(attributes.aPosition, 2, this.gl.FLOAT, false, 0, 0);
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.quadTexCoordsBuffer);
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.quadElementsBuffer);
}
setFramebufferSizeUniform(uniforms: UniformMap) {
const currentViewport = this.gl.getParameter(this.gl.VIEWPORT);
this.gl.uniform2i(uniforms.uFramebufferSize, currentViewport[2], currentViewport[3]);
}
protected redraw() {
super.redraw();
@ -309,19 +359,79 @@ export abstract class PathfinderDemoView extends PathfinderView {
protected renderingFinished(): void {}
setTransformSTUniform(uniforms: UniformMap, objectIndex: number) {
// FIXME(pcwalton): Lossy conversion from a 4x4 matrix to an ST matrix is ugly and fragile.
// Refactor.
const transform = glmatrix.mat4.clone(this.worldTransform);
glmatrix.mat4.mul(transform, transform, this.getModelviewTransform(objectIndex));
protected getModelviewTransform(pathIndex: number): glmatrix.mat4 {
return glmatrix.mat4.create();
}
const translation = glmatrix.vec4.clone([transform[12], transform[13], 0.0, 1.0]);
protected drawSceneryIfNecessary(): void {}
this.gl.uniform4f(uniforms.uTransformST,
transform[0],
transform[5],
transform[12],
transform[13]);
protected clearForDirectRendering(): void {
this.gl.clearColor(1.0, 1.0, 1.0, 1.0);
this.gl.clearDepth(0.0);
this.gl.depthMask(true);
this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
}
protected shouldRenderObject(objectIndex: number): boolean {
return true;
}
protected newTimingsReceived() {}
protected abstract pathColorsForObject(objectIndex: number): Uint8Array;
protected abstract pathTransformsForObject(objectIndex: number): Float32Array;
protected abstract get depthFunction(): number;
protected abstract createAAStrategy(aaType: AntialiasingStrategyName,
aaLevel: number,
subpixelAA: boolean):
AntialiasingStrategy;
protected abstract compositeIfNecessary(): void;
private compileShaders(commonSource: string, shaderSources: ShaderMap<ShaderProgramSource>):
ShaderMap<UnlinkedShaderProgram> {
const shaders: Partial<ShaderMap<Partial<UnlinkedShaderProgram>>> = {};
for (const shaderKey of SHADER_NAMES) {
for (const typeName of ['vertex', 'fragment'] as Array<'vertex' | 'fragment'>) {
const type = {
fragment: this.gl.FRAGMENT_SHADER,
vertex: this.gl.VERTEX_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> {
const 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>;
}
private setTransformUniform(uniforms: UniformMap, objectIndex: number) {
@ -450,7 +560,7 @@ export abstract class PathfinderDemoView extends PathfinderView {
Array<'atlasRenderingTimerQuery' | 'compositingTimerQuery'>) {
if (this.timerQueryExt.getQueryObjectEXT(this[queryName],
this.timerQueryExt
.QUERY_RESULT_AVAILABLE_EXT) == 0) {
.QUERY_RESULT_AVAILABLE_EXT) === 0) {
return;
}
}
@ -462,8 +572,8 @@ export abstract class PathfinderDemoView extends PathfinderView {
this.timerQueryExt.getQueryObjectEXT(this.compositingTimerQuery,
this.timerQueryExt.QUERY_RESULT_EXT);
this.lastTimings = {
rendering: atlasRenderingTime / 1000000.0,
compositing: compositingTime / 1000000.0,
rendering: atlasRenderingTime / 1000000.0,
};
this.newTimingsReceived();
@ -473,28 +583,6 @@ export abstract class PathfinderDemoView extends PathfinderView {
}, TIME_INTERVAL_DELAY);
}
setTransformSTAndTexScaleUniformsForDest(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]);
}
setTransformAndTexScaleUniformsForDest(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]);
}
queueScreenshot() {
this.wantsScreenshot = true;
this.setDirty();
}
private takeScreenshot() {
const width = this.canvas.width, height = this.canvas.height;
const scratchCanvas = document.createElement('canvas');
@ -512,61 +600,6 @@ export abstract class PathfinderDemoView extends PathfinderView {
document.body.removeChild(scratchLink);
}
protected getModelviewTransform(pathIndex: number): glmatrix.mat4 {
return glmatrix.mat4.create();
}
protected drawSceneryIfNecessary(): void {}
protected clearForDirectRendering(): void {
this.gl.clearColor(1.0, 1.0, 1.0, 1.0);
this.gl.clearDepth(0.0);
this.gl.depthMask(true);
this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
}
protected shouldRenderObject(objectIndex: number): boolean {
return true;
}
uploadPathColors(objectCount: number) {
this.pathColorsBufferTextures = [];
for (let objectIndex = 0; objectIndex < objectCount; objectIndex++) {
const pathColorsBufferTexture = new PathfinderBufferTexture(this.gl, 'uPathColors');
const pathColors = this.pathColorsForObject(objectIndex);
pathColorsBufferTexture.upload(this.gl, pathColors);
this.pathColorsBufferTextures.push(pathColorsBufferTexture);
}
}
uploadPathTransforms(objectCount: number) {
this.pathTransformBufferTextures = [];
for (let objectIndex = 0; objectIndex < objectCount; objectIndex++) {
const pathTransformBufferTexture = new PathfinderBufferTexture(this.gl,
'uPathTransform');
const pathTransforms = this.pathTransformsForObject(objectIndex);
pathTransformBufferTexture.upload(this.gl, pathTransforms);
this.pathTransformBufferTextures.push(pathTransformBufferTexture);
}
}
protected newTimingsReceived() {}
protected abstract pathColorsForObject(objectIndex: number): Uint8Array;
protected abstract pathTransformsForObject(objectIndex: number): Float32Array;
protected abstract get depthFunction(): number;
protected abstract createAAStrategy(aaType: AntialiasingStrategyName,
aaLevel: number,
subpixelAA: boolean):
AntialiasingStrategy;
protected abstract compositeIfNecessary(): void;
abstract get destFramebuffer(): WebGLFramebuffer | null;
abstract get destAllocatedSize(): glmatrix.vec2;
@ -578,38 +611,6 @@ export abstract class PathfinderDemoView extends PathfinderView {
protected abstract get directCurveProgramName(): keyof ShaderMap<void>;
protected abstract get directInteriorProgramName(): keyof ShaderMap<void>;
protected antialiasingStrategy: AntialiasingStrategy | null;
gl: WebGLRenderingContext;
shaderPrograms: ShaderMap<PathfinderShaderProgram>;
protected colorBufferHalfFloatExt: any;
drawBuffersExt: any;
instancedArraysExt: any;
textureHalfFloatExt: any;
protected timerQueryExt: any;
vertexArrayObjectExt: any;
quadPositionsBuffer: WebGLBuffer;
quadTexCoordsBuffer: WebGLBuffer;
quadElementsBuffer: WebGLBuffer;
meshes: PathfinderMeshBuffers[];
meshData: PathfinderMeshData[];
pathTransformBufferTextures: PathfinderBufferTexture[];
pathHintsBufferTexture: PathfinderBufferTexture | null;
protected pathColorsBufferTextures: PathfinderBufferTexture[];
private atlasRenderingTimerQuery: WebGLQuery;
private compositingTimerQuery: WebGLQuery;
private timerQueryPollInterval: number | null;
protected lastTimings: Timings;
private wantsScreenshot: boolean;
}
export abstract class MonochromePathfinderView extends PathfinderDemoView {

27
demo/client/tslint.json Normal file
View File

@ -0,0 +1,27 @@
{
"defaultSeverity": "error",
"extends": [
"tslint:recommended"
],
"jsRules": {
"quotemark": false
},
"rules": {
"quotemark": false,
"interface-name": false,
"curly": false,
"member-access": false,
"max-classes-per-file": false,
"arrow-parens": false,
"one-variable-per-declaration": false,
"object-literal-shorthand": false,
"no-empty": false,
"variable-name": false,
"no-bitwise": false,
"new-parens": false,
"no-conditional-assignment": false,
"no-shadowed-variable": false,
"no-var-requires": false
},
"rulesDirectory": []
}

View File

@ -13,6 +13,15 @@ module.exports = {
},
module: {
rules: [
{
test: /src\/[a-zA-Z0-9_-]+\.tsx?$/,
enforce: 'pre',
loader: 'tslint-loader',
exclude: /node_modules/,
options: {
configFile: "tslint.json",
},
},
{
test: /src\/[a-zA-Z0-9_-]+\.tsx?$/,
use: 'ts-loader',