Remove the old Pathfinder 3 demo
This commit is contained in:
parent
4062f824fd
commit
39d6b1d9c2
|
@ -1,18 +1,4 @@
|
||||||
/font-renderer/target
|
|
||||||
/partitioner/target
|
|
||||||
/path-utils/target
|
|
||||||
/utils/frontend/target
|
|
||||||
/utils/gamma-lut/target
|
/utils/gamma-lut/target
|
||||||
/demo/client/target
|
|
||||||
/demo/client/*.html
|
|
||||||
/demo/client/*.js
|
|
||||||
/demo/client/src/*.js
|
|
||||||
/demo/client/src/*.js.map
|
|
||||||
/demo/client/node_modules
|
|
||||||
/demo/client/package-lock.json
|
|
||||||
/demo/server/target
|
|
||||||
/demo/server/Rocket.toml
|
|
||||||
/demo2/dist
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
target
|
target
|
||||||
node_modules
|
node_modules
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
#version 300 es
|
|
||||||
|
|
||||||
// pathfinder/demo2/cover.fs.glsl
|
|
||||||
//
|
|
||||||
// Copyright © 2018 The Pathfinder Project Developers.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
|
||||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
|
||||||
// option. This file may not be copied, modified, or distributed
|
|
||||||
// except according to those terms.
|
|
||||||
|
|
||||||
precision highp float;
|
|
||||||
|
|
||||||
uniform sampler2D uStencilTexture;
|
|
||||||
|
|
||||||
in vec2 vTexCoord;
|
|
||||||
in float vBackdrop;
|
|
||||||
in vec4 vColor;
|
|
||||||
|
|
||||||
out vec4 oFragColor;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
float coverage = abs(texture(uStencilTexture, vTexCoord).r + vBackdrop);
|
|
||||||
oFragColor = vec4(vColor.rgb, vColor.a * coverage);
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
#version 300 es
|
|
||||||
|
|
||||||
// pathfinder/demo2/cover.vs.glsl
|
|
||||||
//
|
|
||||||
// Copyright © 2018 The Pathfinder Project Developers.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
|
||||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
|
||||||
// option. This file may not be copied, modified, or distributed
|
|
||||||
// except according to those terms.
|
|
||||||
|
|
||||||
precision highp float;
|
|
||||||
|
|
||||||
uniform vec2 uFramebufferSize;
|
|
||||||
uniform vec2 uTileSize;
|
|
||||||
uniform vec2 uStencilTextureSize;
|
|
||||||
uniform sampler2D uFillColorsTexture;
|
|
||||||
uniform vec2 uFillColorsTextureSize;
|
|
||||||
uniform vec2 uViewBoxOrigin;
|
|
||||||
|
|
||||||
in vec2 aTessCoord;
|
|
||||||
in vec2 aTileOrigin;
|
|
||||||
in int aBackdrop;
|
|
||||||
in uint aObject;
|
|
||||||
|
|
||||||
out vec2 vTexCoord;
|
|
||||||
out float vBackdrop;
|
|
||||||
out vec4 vColor;
|
|
||||||
|
|
||||||
vec2 computeTileOffset(uint tileIndex, float stencilTextureWidth) {
|
|
||||||
uint tilesPerRow = uint(stencilTextureWidth / uTileSize.x);
|
|
||||||
uvec2 tileOffset = uvec2(tileIndex % tilesPerRow, tileIndex / tilesPerRow);
|
|
||||||
return vec2(tileOffset) * uTileSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
uint tileIndex = uint(gl_InstanceID);
|
|
||||||
vec2 position = (aTileOrigin + aTessCoord) * uTileSize + uViewBoxOrigin;
|
|
||||||
vec2 texCoord = computeTileOffset(tileIndex, uStencilTextureSize.x) + aTessCoord * uTileSize;
|
|
||||||
vTexCoord = texCoord / uStencilTextureSize;
|
|
||||||
vBackdrop = float(aBackdrop);
|
|
||||||
vColor = texture(uFillColorsTexture, vec2(float(aObject) / uFillColorsTextureSize.x, 0.0));
|
|
||||||
gl_Position = vec4((position / uFramebufferSize * 2.0 - 1.0) * vec2(1.0, -1.0), 0.0, 1.0);
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
declare module "*.glsl";
|
|
||||||
declare module "*.jpg";
|
|
||||||
declare module "*.png";
|
|
||||||
declare module "*.svg";
|
|
||||||
|
|
||||||
declare function require(s: string): any;
|
|
|
@ -1,151 +0,0 @@
|
||||||
// pathfinder/demo2/geometry.ts
|
|
||||||
//
|
|
||||||
// Copyright © 2018 The Pathfinder Project Developers.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
|
||||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
|
||||||
// option. This file may not be copied, modified, or distributed
|
|
||||||
// except according to those terms.
|
|
||||||
|
|
||||||
export const EPSILON: number = 1e-6;
|
|
||||||
|
|
||||||
export class Point2D {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
|
|
||||||
constructor(x: number, y: number) {
|
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
Object.freeze(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
approxEq(other: Point2D, epsilon: number | undefined): boolean {
|
|
||||||
return approxEq(this.x, other.x, epsilon) && approxEq(this.y, other.y, epsilon);
|
|
||||||
}
|
|
||||||
|
|
||||||
lerp(other: Point2D, t: number): Point2D {
|
|
||||||
return new Point2D(lerp(this.x, other.x, t), lerp(this.y, other.y, t));
|
|
||||||
}
|
|
||||||
|
|
||||||
translate(x: number, y: number): Point2D {
|
|
||||||
return new Point2D(this.x + x, this.y + y);
|
|
||||||
}
|
|
||||||
|
|
||||||
add(other: Point2D): Point2D {
|
|
||||||
return new Point2D(this.x + other.x, this.y + other.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
sub(other: Point2D): Point2D {
|
|
||||||
return new Point2D(this.x - other.x, this.y - other.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
normalize(): Point2D {
|
|
||||||
const length = this.length();
|
|
||||||
return new Point2D(this.x / length, this.y / length);
|
|
||||||
}
|
|
||||||
|
|
||||||
length(): number {
|
|
||||||
return Math.sqrt(this.x * this.x + this.y * this.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
scale(factor: number): Point2D {
|
|
||||||
return new Point2D(this.x * factor, this.y * factor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Size2D {
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
|
|
||||||
constructor(width: number, height: number) {
|
|
||||||
this.width = width;
|
|
||||||
this.height = height;
|
|
||||||
Object.freeze(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Rect {
|
|
||||||
origin: Point2D;
|
|
||||||
size: Size2D;
|
|
||||||
|
|
||||||
constructor(origin: Point2D, size: Size2D) {
|
|
||||||
this.origin = origin;
|
|
||||||
this.size = size;
|
|
||||||
Object.freeze(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
unionWithPoint(point: Point2D): Rect {
|
|
||||||
let newOrigin = this.origin, newSize = this.size;
|
|
||||||
|
|
||||||
if (point.x < this.origin.x) {
|
|
||||||
newSize = {
|
|
||||||
width: newSize.width + newOrigin.x - point.x,
|
|
||||||
height: newSize.height,
|
|
||||||
};
|
|
||||||
newOrigin = new Point2D(point.x, newOrigin.y);
|
|
||||||
} else if (point.x > this.maxX()) {
|
|
||||||
newSize = {
|
|
||||||
width: newSize.width + point.x - this.maxX(),
|
|
||||||
height: newSize.height,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (point.y < this.origin.y) {
|
|
||||||
newSize = {
|
|
||||||
width: newSize.width,
|
|
||||||
height: newSize.height + newOrigin.y - point.y,
|
|
||||||
};
|
|
||||||
newOrigin = new Point2D(newOrigin.x, point.y);
|
|
||||||
} else if (point.y > this.maxY()) {
|
|
||||||
newSize = {
|
|
||||||
width: newSize.width,
|
|
||||||
height: newSize.height + point.y - this.maxY(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Rect(newOrigin, newSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
maxX(): number {
|
|
||||||
return this.origin.x + this.size.width;
|
|
||||||
}
|
|
||||||
|
|
||||||
maxY(): number {
|
|
||||||
return this.origin.y + this.size.height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Vector3D {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
z: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Matrix2D {
|
|
||||||
a: number; b: number;
|
|
||||||
c: number; d: number;
|
|
||||||
tx: number; ty: number;
|
|
||||||
|
|
||||||
constructor(a: number, b: number, c: number, d: number, tx: number, ty: number) {
|
|
||||||
this.a = a; this.b = b;
|
|
||||||
this.c = c; this.d = d;
|
|
||||||
this.tx = tx; this.ty = ty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function approxEq(a: number, b: number, epsilon: number | undefined): boolean {
|
|
||||||
return Math.abs(a - b) <= (epsilon == null ? EPSILON : epsilon);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function lerp(a: number, b: number, t: number): number {
|
|
||||||
return a + (b - a) * t;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function cross(a: Vector3D, b: Vector3D): Vector3D {
|
|
||||||
return {
|
|
||||||
x: a.y*b.z - a.z*b.y,
|
|
||||||
y: a.z*b.x - a.x*b.z,
|
|
||||||
z: a.x*b.y - a.y*b.x,
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<title>Pathfinder Demo</title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<link rel="stylesheet" type="text/css" media="screen" href="pathfinder.scss" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<canvas id=canvas width=640 height=480></canvas>
|
|
||||||
<label class="btn btn-primary m-2" id="open-wrapper">
|
|
||||||
Open…<input type="file" id="open" hidden>
|
|
||||||
</label>
|
|
||||||
<script src="pathfinder.ts"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,21 +0,0 @@
|
||||||
#version 300 es
|
|
||||||
|
|
||||||
// pathfinder/demo2/opaque.fs.glsl
|
|
||||||
//
|
|
||||||
// Copyright © 2018 The Pathfinder Project Developers.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
|
||||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
|
||||||
// option. This file may not be copied, modified, or distributed
|
|
||||||
// except according to those terms.
|
|
||||||
|
|
||||||
precision highp float;
|
|
||||||
|
|
||||||
in vec4 vColor;
|
|
||||||
|
|
||||||
out vec4 oFragColor;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
oFragColor = vColor;
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
#version 300 es
|
|
||||||
|
|
||||||
// pathfinder/demo2/opaque.vs.glsl
|
|
||||||
//
|
|
||||||
// Copyright © 2018 The Pathfinder Project Developers.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
|
||||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
|
||||||
// option. This file may not be copied, modified, or distributed
|
|
||||||
// except according to those terms.
|
|
||||||
|
|
||||||
precision highp float;
|
|
||||||
|
|
||||||
uniform vec2 uFramebufferSize;
|
|
||||||
uniform vec2 uTileSize;
|
|
||||||
uniform sampler2D uFillColorsTexture;
|
|
||||||
uniform vec2 uFillColorsTextureSize;
|
|
||||||
uniform vec2 uViewBoxOrigin;
|
|
||||||
|
|
||||||
in vec2 aTessCoord;
|
|
||||||
in vec2 aTileOrigin;
|
|
||||||
in uint aObject;
|
|
||||||
|
|
||||||
out vec4 vColor;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
vec2 position = (aTileOrigin + aTessCoord) * uTileSize + uViewBoxOrigin;
|
|
||||||
vColor = texture(uFillColorsTexture, vec2(float(aObject) / uFillColorsTextureSize.x, 0.0));
|
|
||||||
gl_Position = vec4((position / uFramebufferSize * 2.0 - 1.0) * vec2(1.0, -1.0), 0.0, 1.0);
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,15 +0,0 @@
|
||||||
{
|
|
||||||
"devDependencies": {
|
|
||||||
"glslify-bundle": "^5.1.1",
|
|
||||||
"glslify-deps": "^1.3.1",
|
|
||||||
"sass": "^1.15.2",
|
|
||||||
"typescript": "^3.1.6"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@types/webgl2": "0.0.4",
|
|
||||||
"bootstrap": "^4.1.3",
|
|
||||||
"parse-color": "^1.0.0",
|
|
||||||
"svg-path-outline": "^1.0.1",
|
|
||||||
"svgpath": "^2.2.1"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,301 +0,0 @@
|
||||||
// pathfinder/demo2/path-utils.ts
|
|
||||||
//
|
|
||||||
// Copyright © 2018 The Pathfinder Project Developers.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
|
||||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
|
||||||
// option. This file may not be copied, modified, or distributed
|
|
||||||
// except according to those terms.
|
|
||||||
|
|
||||||
import {Point2D, Rect, EPSILON} from "./geometry";
|
|
||||||
import {SVGPath, Edge} from "./tiling";
|
|
||||||
import { ENGINE_METHOD_DIGESTS } from "constants";
|
|
||||||
import { AssertionError } from "assert";
|
|
||||||
import { unwrapNull, unwrapUndef } from "./util";
|
|
||||||
|
|
||||||
const SVGPath: (path: string) => SVGPath = require('svgpath');
|
|
||||||
|
|
||||||
export class PathSegment {
|
|
||||||
command: string;
|
|
||||||
points: Point2D[];
|
|
||||||
|
|
||||||
constructor(segment: string[]) {
|
|
||||||
const points = [];
|
|
||||||
for (let i = 1; i < segment.length; i += 2)
|
|
||||||
points.push(new Point2D(parseFloat(segment[i]), parseFloat(segment[i + 1])));
|
|
||||||
this.points = points;
|
|
||||||
this.command = segment[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
to(): Point2D | null {
|
|
||||||
return this.points[this.points.length - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
toStringPieces(): string[] {
|
|
||||||
const pieces = [this.command];
|
|
||||||
for (const point of this.points) {
|
|
||||||
pieces.push(" " + point.x);
|
|
||||||
pieces.push(" " + point.y);
|
|
||||||
}
|
|
||||||
return pieces;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString(): string {
|
|
||||||
return this.toStringPieces().join(" ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function flattenPath(path: SVGPath): SVGPath {
|
|
||||||
let lastPoint: Point2D | null = null;
|
|
||||||
return path.unshort().abs().iterate(segmentPieces => {
|
|
||||||
let segment = new PathSegment(segmentPieces);
|
|
||||||
if (segment.command === 'C' && lastPoint != null) {
|
|
||||||
const cubicEdge = new CubicEdge(lastPoint,
|
|
||||||
segment.points[0],
|
|
||||||
segment.points[1],
|
|
||||||
segment.points[2]);
|
|
||||||
//console.log("cubic edge", cubicEdge);
|
|
||||||
const edges: Edge[] = cubicEdge.toQuadraticEdges();
|
|
||||||
/*const edges: Edge[] = [
|
|
||||||
new Edge(lastPoint,
|
|
||||||
segment.points[0].lerp(segment.points[1], 0.5),
|
|
||||||
segment.points[2]),
|
|
||||||
];*/
|
|
||||||
const newSegments = edges.map(edge => edge.toSVGPieces());
|
|
||||||
//console.log("... resulting new segments:", newSegments);
|
|
||||||
lastPoint = segment.to();
|
|
||||||
return newSegments;
|
|
||||||
}
|
|
||||||
if (segment.command === 'H' && lastPoint != null)
|
|
||||||
segment = new PathSegment(['L', segmentPieces[1], "" + lastPoint.y]);
|
|
||||||
if (segment.command === 'V' && lastPoint != null)
|
|
||||||
segment = new PathSegment(['L', "" + lastPoint.x, segmentPieces[1]]);
|
|
||||||
lastPoint = segment.to();
|
|
||||||
return [segment.toStringPieces()];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function makePathMonotonic(path: SVGPath): SVGPath {
|
|
||||||
let lastPoint: Point2D | null = null;
|
|
||||||
return path.iterate(segmentPieces => {
|
|
||||||
let segment = new PathSegment(segmentPieces);
|
|
||||||
if (segment.command === 'Q' && lastPoint != null) {
|
|
||||||
const edge = new Edge(lastPoint, segment.points[0], segment.points[1]);
|
|
||||||
const minX = Math.min(edge.from.x, edge.to.x);
|
|
||||||
const maxX = Math.max(edge.from.x, edge.to.x);
|
|
||||||
|
|
||||||
const edgesX: Edge[] = [];
|
|
||||||
if (edge.ctrl!.x < minX || edge.ctrl!.x > maxX) {
|
|
||||||
const t = (edge.from.x - edge.ctrl!.x) /
|
|
||||||
(edge.from.x - 2.0 * edge.ctrl!.x + edge.to.x);
|
|
||||||
const subdivided = edge.subdivideAt(t);
|
|
||||||
if (t < -EPSILON || t > 1.0 + EPSILON)
|
|
||||||
throw new Error("Bad t value when making monotonic X!");
|
|
||||||
edgesX.push(subdivided.prev, subdivided.next);
|
|
||||||
} else {
|
|
||||||
edgesX.push(edge);
|
|
||||||
}
|
|
||||||
|
|
||||||
const newEdges = [];
|
|
||||||
for (const edge of edgesX) {
|
|
||||||
const minY = Math.min(edge.from.y, edge.to.y);
|
|
||||||
const maxY = Math.max(edge.from.y, edge.to.y);
|
|
||||||
|
|
||||||
if (edge.ctrl!.y < minY || edge.ctrl!.y > maxY) {
|
|
||||||
const t = (edge.from.y - edge.ctrl!.y) /
|
|
||||||
(edge.from.y - 2.0 * edge.ctrl!.y + edge.to.y);
|
|
||||||
if (t < -EPSILON || t > 1.0 + EPSILON)
|
|
||||||
throw new Error("Bad t value when making monotonic Y!");
|
|
||||||
const subdivided = edge.subdivideAt(t);
|
|
||||||
newEdges.push(subdivided.prev, subdivided.next);
|
|
||||||
} else {
|
|
||||||
newEdges.push(edge);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastPoint = segment.to();
|
|
||||||
return newEdges.map(newEdge => newEdge.toSVGPieces());
|
|
||||||
}
|
|
||||||
|
|
||||||
lastPoint = segment.to();
|
|
||||||
return [segment.toStringPieces()];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Outline {
|
|
||||||
suboutlines: Suboutline[];
|
|
||||||
|
|
||||||
constructor(path: SVGPath) {
|
|
||||||
this.suboutlines = [];
|
|
||||||
let suboutline = new Suboutline;
|
|
||||||
path.iterate(segmentPieces => {
|
|
||||||
const segment = new PathSegment(segmentPieces);
|
|
||||||
if (segment.command === 'M') {
|
|
||||||
if (!suboutline.isEmpty()) {
|
|
||||||
this.suboutlines.push(suboutline);
|
|
||||||
suboutline = new Suboutline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (let pointIndex = 0; pointIndex < segment.points.length; pointIndex++) {
|
|
||||||
suboutline.points.push(new OutlinePoint(segment.points[pointIndex],
|
|
||||||
pointIndex < segment.points.length - 1));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!suboutline.isEmpty())
|
|
||||||
this.suboutlines.push(suboutline);
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateNormals(): void {
|
|
||||||
for (const suboutline of this.suboutlines)
|
|
||||||
suboutline.calculateNormals();
|
|
||||||
}
|
|
||||||
|
|
||||||
stroke(radius: number): void {
|
|
||||||
for (const suboutline of this.suboutlines)
|
|
||||||
suboutline.stroke(radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
toSVGPathString(): string {
|
|
||||||
return this.suboutlines.map(suboutline => suboutline.toSVGPathString()).join(" ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Suboutline {
|
|
||||||
points: OutlinePoint[];
|
|
||||||
normals: Point2D[] | null;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.points = [];
|
|
||||||
this.normals = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
isEmpty(): boolean {
|
|
||||||
return this.points.length === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateNormals(): void {
|
|
||||||
this.normals = [];
|
|
||||||
for (let pointIndex = 0; pointIndex < this.points.length; pointIndex++) {
|
|
||||||
const prevPointIndex = pointIndex === 0 ? this.points.length - 1 : pointIndex - 1;
|
|
||||||
const nextPointIndex = pointIndex === this.points.length - 1 ? 0 : pointIndex + 1;
|
|
||||||
const prevPoint = this.points[prevPointIndex].position;
|
|
||||||
const point = this.points[pointIndex].position;
|
|
||||||
const nextPoint = this.points[nextPointIndex].position;
|
|
||||||
let prevVector = prevPoint.sub(point), nextVector = nextPoint.sub(point);
|
|
||||||
this.normals.push(prevVector.add(nextVector).normalize());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stroke(radius: number): void {
|
|
||||||
if (this.normals == null)
|
|
||||||
throw new Error("Calculate normals first!");
|
|
||||||
const newPoints = [];
|
|
||||||
for (let pointIndex = 0; pointIndex < this.points.length; pointIndex++) {
|
|
||||||
const point = this.points[pointIndex], normal = this.normals[pointIndex];
|
|
||||||
const newPosition = point.position.sub(normal.scale(radius));
|
|
||||||
newPoints.push(new OutlinePoint(newPosition, point.offCurve));
|
|
||||||
}
|
|
||||||
for (let pointIndex = this.points.length - 1; pointIndex >= 0; pointIndex--) {
|
|
||||||
const point = this.points[pointIndex], normal = this.normals[pointIndex];
|
|
||||||
const newPosition = point.position.add(normal.scale(radius));
|
|
||||||
newPoints.push(new OutlinePoint(newPosition, point.offCurve));
|
|
||||||
}
|
|
||||||
this.points = newPoints;
|
|
||||||
this.normals = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
toSVGPathString(): string {
|
|
||||||
let string = "";
|
|
||||||
const queuedPositions = [];
|
|
||||||
for (let pointIndex = 0; pointIndex < this.points.length; pointIndex++) {
|
|
||||||
const point = this.points[pointIndex];
|
|
||||||
queuedPositions.push(point.position);
|
|
||||||
if (pointIndex > 0 && point.offCurve)
|
|
||||||
continue;
|
|
||||||
let command: string;
|
|
||||||
if (pointIndex === 0)
|
|
||||||
command = 'M';
|
|
||||||
else if (queuedPositions.length === 1)
|
|
||||||
command = 'L';
|
|
||||||
else
|
|
||||||
command = 'Q';
|
|
||||||
string += command + " ";
|
|
||||||
for (const position of queuedPositions)
|
|
||||||
string += position.x + " " + position.y + " ";
|
|
||||||
queuedPositions.splice(0);
|
|
||||||
}
|
|
||||||
string += "Z";
|
|
||||||
return string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class OutlinePoint {
|
|
||||||
position: Point2D;
|
|
||||||
offCurve: boolean;
|
|
||||||
|
|
||||||
constructor(position: Point2D, offCurve: boolean) {
|
|
||||||
this.position = position;
|
|
||||||
this.offCurve = offCurve;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CubicEdge {
|
|
||||||
from: Point2D;
|
|
||||||
ctrl0: Point2D;
|
|
||||||
ctrl1: Point2D;
|
|
||||||
to: Point2D;
|
|
||||||
|
|
||||||
constructor(from: Point2D, ctrl0: Point2D, ctrl1: Point2D, to: Point2D) {
|
|
||||||
this.from = from;
|
|
||||||
this.ctrl0 = ctrl0;
|
|
||||||
this.ctrl1 = ctrl1;
|
|
||||||
this.to = to;
|
|
||||||
}
|
|
||||||
|
|
||||||
subdivideAt(t: number): SubdividedCubicEdges {
|
|
||||||
const p0 = this.from, p1 = this.ctrl0, p2 = this.ctrl1, p3 = this.to;
|
|
||||||
const p01 = p0.lerp(p1, t), p12 = p1.lerp(p2, t), p23 = p2.lerp(p3, t);
|
|
||||||
const p012 = p01.lerp(p12, t), p123 = p12.lerp(p23, t);
|
|
||||||
const p0123 = p012.lerp(p123, t);
|
|
||||||
return {
|
|
||||||
prev: new CubicEdge(p0, p01, p012, p0123),
|
|
||||||
next: new CubicEdge(p0123, p123, p23, p3),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
toQuadraticEdges(): Edge[] {
|
|
||||||
const MAX_APPROXIMATION_ITERATIONS: number = 32;
|
|
||||||
const TOLERANCE: number = 0.1;
|
|
||||||
|
|
||||||
const results = [], worklist: CubicEdge[] = [this];
|
|
||||||
while (worklist.length > 0) {
|
|
||||||
let current = unwrapUndef(worklist.pop());
|
|
||||||
for (let iteration = 0; iteration < MAX_APPROXIMATION_ITERATIONS; iteration++) {
|
|
||||||
const deltaCtrl0 = current.from.sub(current.ctrl0.scale(3.0))
|
|
||||||
.add(current.ctrl1.scale(3.0).sub(current.to));
|
|
||||||
const deltaCtrl1 = current.ctrl0.scale(3.0)
|
|
||||||
.sub(current.from)
|
|
||||||
.add(current.to.sub(current.ctrl1.scale(3.0)));
|
|
||||||
const maxError = Math.max(deltaCtrl0.length(), deltaCtrl1.length()) / 6.0;
|
|
||||||
if (maxError < TOLERANCE)
|
|
||||||
break;
|
|
||||||
|
|
||||||
const subdivided = current.subdivideAt(0.5);
|
|
||||||
worklist.push(subdivided.next);
|
|
||||||
current = subdivided.prev;
|
|
||||||
}
|
|
||||||
|
|
||||||
const approxCtrl0 = current.ctrl0.scale(3.0).sub(current.from).scale(0.5);
|
|
||||||
const approxCtrl1 = current.ctrl1.scale(3.0).sub(current.to).scale(0.5);
|
|
||||||
results.push(new Edge(current.from, approxCtrl0.lerp(approxCtrl1, 0.5), current.to));
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SubdividedCubicEdges {
|
|
||||||
prev: CubicEdge;
|
|
||||||
next: CubicEdge;
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
@import "./node_modules/bootstrap/scss/bootstrap.scss";
|
|
||||||
|
|
||||||
html, body {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#open-wrapper {
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
|
@ -1,734 +0,0 @@
|
||||||
// pathfinder/demo2/pathfinder.ts
|
|
||||||
//
|
|
||||||
// Copyright © 2018 The Pathfinder Project Developers.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
|
||||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
|
||||||
// option. This file may not be copied, modified, or distributed
|
|
||||||
// except according to those terms.
|
|
||||||
|
|
||||||
import COVER_VERTEX_SHADER_SOURCE from "./cover.vs.glsl";
|
|
||||||
import COVER_FRAGMENT_SHADER_SOURCE from "./cover.fs.glsl";
|
|
||||||
import OPAQUE_VERTEX_SHADER_SOURCE from "./opaque.vs.glsl";
|
|
||||||
import OPAQUE_FRAGMENT_SHADER_SOURCE from "./opaque.fs.glsl";
|
|
||||||
import STENCIL_VERTEX_SHADER_SOURCE from "./stencil.vs.glsl";
|
|
||||||
import STENCIL_FRAGMENT_SHADER_SOURCE from "./stencil.fs.glsl";
|
|
||||||
import AREA_LUT from "../resources/textures/area-lut.png";
|
|
||||||
import {Matrix2D, Size2D, Rect, Point2D} from "./geometry";
|
|
||||||
import {SVGPath, TILE_SIZE} from "./tiling";
|
|
||||||
import {staticCast, unwrapNull} from "./util";
|
|
||||||
|
|
||||||
const SVGPath: (path: string) => SVGPath = require('svgpath');
|
|
||||||
|
|
||||||
const STENCIL_FRAMEBUFFER_SIZE: Size2D = {
|
|
||||||
width: TILE_SIZE.width * 256,
|
|
||||||
height: TILE_SIZE.height * 256,
|
|
||||||
};
|
|
||||||
|
|
||||||
const QUAD_VERTEX_POSITIONS: Uint8Array = new Uint8Array([
|
|
||||||
0, 0,
|
|
||||||
1, 0,
|
|
||||||
1, 1,
|
|
||||||
0, 1,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const FILL_INSTANCE_SIZE: number = 8;
|
|
||||||
const SOLID_TILE_INSTANCE_SIZE: number = 6;
|
|
||||||
const MASK_TILE_INSTANCE_SIZE: number = 8;
|
|
||||||
|
|
||||||
interface Color {
|
|
||||||
r: number;
|
|
||||||
g: number;
|
|
||||||
b: number;
|
|
||||||
a: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
type Edge = 'left' | 'top' | 'right' | 'bottom';
|
|
||||||
|
|
||||||
type FillProgram =
|
|
||||||
Program<'FramebufferSize' | 'TileSize' | 'AreaLUT',
|
|
||||||
'TessCoord' | 'FromPx' | 'ToPx' | 'FromSubpx' | 'ToSubpx' | 'TileIndex'>;
|
|
||||||
type SolidTileProgram =
|
|
||||||
Program<'FramebufferSize' |
|
|
||||||
'TileSize' |
|
|
||||||
'FillColorsTexture' | 'FillColorsTextureSize' |
|
|
||||||
'ViewBoxOrigin',
|
|
||||||
'TessCoord' | 'TileOrigin' | 'Object'>;
|
|
||||||
type MaskTileProgram =
|
|
||||||
Program<'FramebufferSize' |
|
|
||||||
'TileSize' |
|
|
||||||
'StencilTexture' | 'StencilTextureSize' |
|
|
||||||
'FillColorsTexture' | 'FillColorsTextureSize' |
|
|
||||||
'ViewBoxOrigin',
|
|
||||||
'TessCoord' | 'TileOrigin' | 'Backdrop' | 'Object'>;
|
|
||||||
|
|
||||||
class App {
|
|
||||||
private canvas: HTMLCanvasElement;
|
|
||||||
private openButton: HTMLInputElement;
|
|
||||||
private areaLUT: HTMLImageElement;
|
|
||||||
|
|
||||||
private gl: WebGL2RenderingContext;
|
|
||||||
private disjointTimerQueryExt: any;
|
|
||||||
private areaLUTTexture: WebGLTexture;
|
|
||||||
private fillColorsTexture: WebGLTexture;
|
|
||||||
private stencilTexture: WebGLTexture;
|
|
||||||
private stencilFramebuffer: WebGLFramebuffer;
|
|
||||||
private fillProgram: FillProgram;
|
|
||||||
private solidTileProgram: SolidTileProgram;
|
|
||||||
private maskTileProgram: MaskTileProgram;
|
|
||||||
private quadVertexBuffer: WebGLBuffer;
|
|
||||||
private solidTileVertexBuffer: WebGLBuffer;
|
|
||||||
private solidVertexArray: WebGLVertexArrayObject;
|
|
||||||
private batchBuffers: BatchBuffers[];
|
|
||||||
|
|
||||||
private viewBox: Rect;
|
|
||||||
|
|
||||||
private solidTileCount: number;
|
|
||||||
private shaderCount: number;
|
|
||||||
|
|
||||||
constructor(areaLUT: HTMLImageElement) {
|
|
||||||
const canvas = staticCast(document.getElementById('canvas'), HTMLCanvasElement);
|
|
||||||
const openButton = staticCast(document.getElementById('open'), HTMLInputElement);
|
|
||||||
this.canvas = canvas;
|
|
||||||
this.openButton = openButton;
|
|
||||||
this.areaLUT = areaLUT;
|
|
||||||
|
|
||||||
this.openButton.addEventListener('change', event => this.loadFile(), false);
|
|
||||||
|
|
||||||
const devicePixelRatio = window.devicePixelRatio;
|
|
||||||
canvas.width = window.innerWidth * devicePixelRatio;
|
|
||||||
canvas.height = window.innerHeight * devicePixelRatio;
|
|
||||||
canvas.style.width = window.innerWidth + "px";
|
|
||||||
canvas.style.height = window.innerHeight + "px";
|
|
||||||
|
|
||||||
const gl = unwrapNull(this.canvas.getContext('webgl2', {antialias: false}));
|
|
||||||
this.gl = gl;
|
|
||||||
gl.getExtension('EXT_color_buffer_float');
|
|
||||||
this.disjointTimerQueryExt = gl.getExtension('EXT_disjoint_timer_query');
|
|
||||||
|
|
||||||
this.areaLUTTexture = unwrapNull(gl.createTexture());
|
|
||||||
gl.bindTexture(gl.TEXTURE_2D, this.areaLUTTexture);
|
|
||||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, areaLUT);
|
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
||||||
|
|
||||||
this.fillColorsTexture = unwrapNull(gl.createTexture());
|
|
||||||
|
|
||||||
/*
|
|
||||||
const benchData = new Uint8Array(1600 * 1600 * 4);
|
|
||||||
for (let i = 0; i < benchData.length; i++)
|
|
||||||
benchData[i] = (Math.random() * 256) | 0;
|
|
||||||
const benchTexture = unwrapNull(gl.createTexture());
|
|
||||||
gl.bindTexture(gl.TEXTURE_2D, benchTexture);
|
|
||||||
const startTime = performance.now();
|
|
||||||
for (let i = 0; i < 100; i++)
|
|
||||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1600, 1600, 0, gl.RGBA, gl.UNSIGNED_BYTE, benchData);
|
|
||||||
const elapsedTime = (performance.now() - startTime) / 100;
|
|
||||||
console.log("texture upload: ", elapsedTime, "ms");
|
|
||||||
*/
|
|
||||||
|
|
||||||
this.stencilTexture = unwrapNull(gl.createTexture());
|
|
||||||
gl.bindTexture(gl.TEXTURE_2D, this.stencilTexture);
|
|
||||||
gl.texImage2D(gl.TEXTURE_2D,
|
|
||||||
0,
|
|
||||||
gl.R16F,
|
|
||||||
STENCIL_FRAMEBUFFER_SIZE.width,
|
|
||||||
STENCIL_FRAMEBUFFER_SIZE.height,
|
|
||||||
0,
|
|
||||||
gl.RED,
|
|
||||||
gl.HALF_FLOAT,
|
|
||||||
null);
|
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
||||||
|
|
||||||
this.stencilFramebuffer = unwrapNull(gl.createFramebuffer());
|
|
||||||
gl.bindFramebuffer(gl.FRAMEBUFFER, this.stencilFramebuffer);
|
|
||||||
gl.framebufferTexture2D(gl.FRAMEBUFFER,
|
|
||||||
gl.COLOR_ATTACHMENT0,
|
|
||||||
gl.TEXTURE_2D,
|
|
||||||
this.stencilTexture,
|
|
||||||
0);
|
|
||||||
if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE)
|
|
||||||
throw new Error("Stencil framebuffer incomplete!");
|
|
||||||
|
|
||||||
const maskTileProgram = new Program(gl,
|
|
||||||
COVER_VERTEX_SHADER_SOURCE,
|
|
||||||
COVER_FRAGMENT_SHADER_SOURCE,
|
|
||||||
[
|
|
||||||
'FramebufferSize',
|
|
||||||
'TileSize',
|
|
||||||
'StencilTexture',
|
|
||||||
'StencilTextureSize',
|
|
||||||
'FillColorsTexture',
|
|
||||||
'FillColorsTextureSize',
|
|
||||||
'ViewBoxOrigin',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'TessCoord',
|
|
||||||
'TileOrigin',
|
|
||||||
'TileIndex',
|
|
||||||
'Backdrop',
|
|
||||||
'Object',
|
|
||||||
]);
|
|
||||||
this.maskTileProgram = maskTileProgram;
|
|
||||||
|
|
||||||
const solidTileProgram = new Program(gl,
|
|
||||||
OPAQUE_VERTEX_SHADER_SOURCE,
|
|
||||||
OPAQUE_FRAGMENT_SHADER_SOURCE,
|
|
||||||
[
|
|
||||||
'FramebufferSize',
|
|
||||||
'TileSize',
|
|
||||||
'FillColorsTexture',
|
|
||||||
'FillColorsTextureSize',
|
|
||||||
'ViewBoxOrigin',
|
|
||||||
],
|
|
||||||
['TessCoord', 'TileOrigin', 'Object']);
|
|
||||||
this.solidTileProgram = solidTileProgram;
|
|
||||||
|
|
||||||
const fillProgram = new Program(gl,
|
|
||||||
STENCIL_VERTEX_SHADER_SOURCE,
|
|
||||||
STENCIL_FRAGMENT_SHADER_SOURCE,
|
|
||||||
['FramebufferSize', 'TileSize', 'AreaLUT'],
|
|
||||||
[
|
|
||||||
'TessCoord',
|
|
||||||
'FromPx', 'ToPx',
|
|
||||||
'FromSubpx', 'ToSubpx',
|
|
||||||
'TileIndex'
|
|
||||||
]);
|
|
||||||
this.fillProgram = fillProgram;
|
|
||||||
|
|
||||||
// Initialize quad VBO.
|
|
||||||
this.quadVertexBuffer = unwrapNull(gl.createBuffer());
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, this.quadVertexBuffer);
|
|
||||||
gl.bufferData(gl.ARRAY_BUFFER, QUAD_VERTEX_POSITIONS, gl.STATIC_DRAW);
|
|
||||||
|
|
||||||
// Initialize tile VBOs and IBOs.
|
|
||||||
this.solidTileVertexBuffer = unwrapNull(gl.createBuffer());
|
|
||||||
|
|
||||||
// Initialize solid tile VAO.
|
|
||||||
this.solidVertexArray = unwrapNull(gl.createVertexArray());
|
|
||||||
gl.bindVertexArray(this.solidVertexArray);
|
|
||||||
gl.useProgram(this.solidTileProgram.program);
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, this.quadVertexBuffer);
|
|
||||||
gl.vertexAttribPointer(solidTileProgram.attributes.TessCoord,
|
|
||||||
2,
|
|
||||||
gl.UNSIGNED_BYTE,
|
|
||||||
false,
|
|
||||||
0,
|
|
||||||
0);
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, this.solidTileVertexBuffer);
|
|
||||||
gl.vertexAttribPointer(solidTileProgram.attributes.TileOrigin,
|
|
||||||
2,
|
|
||||||
gl.SHORT,
|
|
||||||
false,
|
|
||||||
SOLID_TILE_INSTANCE_SIZE,
|
|
||||||
0);
|
|
||||||
gl.vertexAttribDivisor(solidTileProgram.attributes.TileOrigin, 1);
|
|
||||||
gl.vertexAttribIPointer(solidTileProgram.attributes.Object,
|
|
||||||
1,
|
|
||||||
gl.UNSIGNED_SHORT,
|
|
||||||
SOLID_TILE_INSTANCE_SIZE,
|
|
||||||
4);
|
|
||||||
gl.vertexAttribDivisor(solidTileProgram.attributes.Object, 1);
|
|
||||||
gl.enableVertexAttribArray(solidTileProgram.attributes.TessCoord);
|
|
||||||
gl.enableVertexAttribArray(solidTileProgram.attributes.TileOrigin);
|
|
||||||
gl.enableVertexAttribArray(solidTileProgram.attributes.Object);
|
|
||||||
|
|
||||||
this.batchBuffers = [];
|
|
||||||
|
|
||||||
this.viewBox = new Rect(new Point2D(0.0, 0.0), new Size2D(0.0, 0.0));
|
|
||||||
|
|
||||||
// Set up event handlers.
|
|
||||||
this.canvas.addEventListener('click', event => this.onClick(event), false);
|
|
||||||
|
|
||||||
this.solidTileCount = 0;
|
|
||||||
this.shaderCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
redraw(): void {
|
|
||||||
const gl = this.gl, canvas = this.canvas;
|
|
||||||
|
|
||||||
//console.log("viewBox", this.viewBox);
|
|
||||||
|
|
||||||
// Initialize timers.
|
|
||||||
let fillTimerQuery = null, solidTimerQuery = null, maskTimerQuery = null;
|
|
||||||
if (this.disjointTimerQueryExt != null) {
|
|
||||||
fillTimerQuery = unwrapNull(gl.createQuery());
|
|
||||||
solidTimerQuery = unwrapNull(gl.createQuery());
|
|
||||||
maskTimerQuery = unwrapNull(gl.createQuery());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear.
|
|
||||||
if (solidTimerQuery != null)
|
|
||||||
gl.beginQuery(this.disjointTimerQueryExt.TIME_ELAPSED_EXT, solidTimerQuery);
|
|
||||||
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
||||||
const framebufferSize = {width: canvas.width, height: canvas.height};
|
|
||||||
gl.viewport(0, 0, framebufferSize.width, framebufferSize.height);
|
|
||||||
gl.clearColor(0.85, 0.85, 0.85, 1.0);
|
|
||||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
||||||
|
|
||||||
// Draw solid tiles.
|
|
||||||
gl.bindVertexArray(this.solidVertexArray);
|
|
||||||
gl.useProgram(this.solidTileProgram.program);
|
|
||||||
gl.uniform2f(this.solidTileProgram.uniforms.FramebufferSize,
|
|
||||||
framebufferSize.width,
|
|
||||||
framebufferSize.height);
|
|
||||||
gl.uniform2f(this.solidTileProgram.uniforms.TileSize, TILE_SIZE.width, TILE_SIZE.height);
|
|
||||||
gl.activeTexture(gl.TEXTURE0);
|
|
||||||
gl.bindTexture(gl.TEXTURE_2D, this.fillColorsTexture);
|
|
||||||
gl.uniform1i(this.solidTileProgram.uniforms.FillColorsTexture, 0);
|
|
||||||
// FIXME(pcwalton): Maybe this should be an ivec2 or uvec2?
|
|
||||||
gl.uniform2f(this.solidTileProgram.uniforms.FillColorsTextureSize,
|
|
||||||
this.shaderCount,
|
|
||||||
1.0);
|
|
||||||
gl.uniform2f(this.solidTileProgram.uniforms.ViewBoxOrigin,
|
|
||||||
this.viewBox.origin.x,
|
|
||||||
this.viewBox.origin.y);
|
|
||||||
gl.disable(gl.BLEND);
|
|
||||||
gl.drawArraysInstanced(gl.TRIANGLE_FAN, 0, 4, this.solidTileCount);
|
|
||||||
if (solidTimerQuery != null)
|
|
||||||
gl.endQuery(this.disjointTimerQueryExt.TIME_ELAPSED_EXT);
|
|
||||||
|
|
||||||
// Draw batches.
|
|
||||||
if (fillTimerQuery != null)
|
|
||||||
gl.beginQuery(this.disjointTimerQueryExt.TIME_ELAPSED_EXT, fillTimerQuery);
|
|
||||||
for (const batch of this.batchBuffers) {
|
|
||||||
// Fill.
|
|
||||||
gl.bindFramebuffer(gl.FRAMEBUFFER, this.stencilFramebuffer);
|
|
||||||
gl.viewport(0, 0, STENCIL_FRAMEBUFFER_SIZE.width, STENCIL_FRAMEBUFFER_SIZE.height);
|
|
||||||
gl.clearColor(0.0, 0.0, 0.0, 0.0);
|
|
||||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
||||||
|
|
||||||
gl.bindVertexArray(batch.fillVertexArray);
|
|
||||||
gl.useProgram(this.fillProgram.program);
|
|
||||||
gl.uniform2f(this.fillProgram.uniforms.FramebufferSize,
|
|
||||||
STENCIL_FRAMEBUFFER_SIZE.width,
|
|
||||||
STENCIL_FRAMEBUFFER_SIZE.height);
|
|
||||||
gl.uniform2f(this.fillProgram.uniforms.TileSize, TILE_SIZE.width, TILE_SIZE.height);
|
|
||||||
gl.activeTexture(gl.TEXTURE0);
|
|
||||||
gl.bindTexture(gl.TEXTURE_2D, this.areaLUTTexture);
|
|
||||||
gl.uniform1i(this.fillProgram.uniforms.AreaLUT, 0);
|
|
||||||
gl.blendEquation(gl.FUNC_ADD);
|
|
||||||
gl.blendFunc(gl.ONE, gl.ONE);
|
|
||||||
gl.enable(gl.BLEND);
|
|
||||||
gl.drawArraysInstanced(gl.TRIANGLE_FAN, 0, 4, unwrapNull(batch.fillPrimitiveCount));
|
|
||||||
console.log("drawing ", batch.fillPrimitiveCount, " fills");
|
|
||||||
gl.disable(gl.BLEND);
|
|
||||||
|
|
||||||
// Read back stencil and dump it.
|
|
||||||
//this.dumpStencil();
|
|
||||||
|
|
||||||
// Draw masked tiles.
|
|
||||||
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
||||||
gl.viewport(0, 0, framebufferSize.width, framebufferSize.height);
|
|
||||||
gl.bindVertexArray(batch.maskVertexArray);
|
|
||||||
gl.useProgram(this.maskTileProgram.program);
|
|
||||||
gl.uniform2f(this.maskTileProgram.uniforms.FramebufferSize,
|
|
||||||
framebufferSize.width,
|
|
||||||
framebufferSize.height);
|
|
||||||
gl.uniform2f(this.maskTileProgram.uniforms.TileSize,
|
|
||||||
TILE_SIZE.width,
|
|
||||||
TILE_SIZE.height);
|
|
||||||
gl.activeTexture(gl.TEXTURE0);
|
|
||||||
gl.bindTexture(gl.TEXTURE_2D, this.stencilTexture);
|
|
||||||
gl.uniform1i(this.maskTileProgram.uniforms.StencilTexture, 0);
|
|
||||||
gl.uniform2f(this.maskTileProgram.uniforms.StencilTextureSize,
|
|
||||||
STENCIL_FRAMEBUFFER_SIZE.width,
|
|
||||||
STENCIL_FRAMEBUFFER_SIZE.height);
|
|
||||||
gl.activeTexture(gl.TEXTURE1);
|
|
||||||
gl.bindTexture(gl.TEXTURE_2D, this.fillColorsTexture);
|
|
||||||
gl.uniform1i(this.maskTileProgram.uniforms.FillColorsTexture, 1);
|
|
||||||
// FIXME(pcwalton): Maybe this should be an ivec2 or uvec2?
|
|
||||||
gl.uniform2f(this.maskTileProgram.uniforms.FillColorsTextureSize,
|
|
||||||
this.shaderCount,
|
|
||||||
1.0);
|
|
||||||
gl.uniform2f(this.maskTileProgram.uniforms.ViewBoxOrigin,
|
|
||||||
this.viewBox.origin.x,
|
|
||||||
this.viewBox.origin.y);
|
|
||||||
gl.blendEquation(gl.FUNC_ADD);
|
|
||||||
gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE);
|
|
||||||
gl.enable(gl.BLEND);
|
|
||||||
gl.drawArraysInstanced(gl.TRIANGLE_FAN, 0, 4, batch.maskTileCount);
|
|
||||||
gl.disable(gl.BLEND);
|
|
||||||
}
|
|
||||||
if (fillTimerQuery != null)
|
|
||||||
gl.endQuery(this.disjointTimerQueryExt.TIME_ELAPSED_EXT);
|
|
||||||
|
|
||||||
// End timer.
|
|
||||||
if (fillTimerQuery != null && solidTimerQuery != null) {
|
|
||||||
processQueries(gl, this.disjointTimerQueryExt, {
|
|
||||||
fill: fillTimerQuery,
|
|
||||||
solid: solidTimerQuery,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private dumpStencil(): void {
|
|
||||||
const gl = this.gl;
|
|
||||||
|
|
||||||
const totalStencilFramebufferSize = STENCIL_FRAMEBUFFER_SIZE.width *
|
|
||||||
STENCIL_FRAMEBUFFER_SIZE.height * 4;
|
|
||||||
const stencilData = new Float32Array(totalStencilFramebufferSize);
|
|
||||||
gl.readPixels(0, 0,
|
|
||||||
STENCIL_FRAMEBUFFER_SIZE.width, STENCIL_FRAMEBUFFER_SIZE.height,
|
|
||||||
gl.RGBA,
|
|
||||||
gl.FLOAT,
|
|
||||||
stencilData);
|
|
||||||
const stencilDumpData = new Uint8ClampedArray(totalStencilFramebufferSize);
|
|
||||||
for (let i = 0; i < stencilData.length; i++)
|
|
||||||
stencilDumpData[i] = stencilData[i] * 255.0;
|
|
||||||
const stencilDumpCanvas = document.createElement('canvas');
|
|
||||||
stencilDumpCanvas.width = STENCIL_FRAMEBUFFER_SIZE.width;
|
|
||||||
stencilDumpCanvas.height = STENCIL_FRAMEBUFFER_SIZE.height;
|
|
||||||
stencilDumpCanvas.style.width =
|
|
||||||
(STENCIL_FRAMEBUFFER_SIZE.width / window.devicePixelRatio) + "px";
|
|
||||||
stencilDumpCanvas.style.height =
|
|
||||||
(STENCIL_FRAMEBUFFER_SIZE.height / window.devicePixelRatio) + "px";
|
|
||||||
const stencilDumpCanvasContext = unwrapNull(stencilDumpCanvas.getContext('2d'));
|
|
||||||
const stencilDumpImageData = new ImageData(stencilDumpData,
|
|
||||||
STENCIL_FRAMEBUFFER_SIZE.width,
|
|
||||||
STENCIL_FRAMEBUFFER_SIZE.height);
|
|
||||||
stencilDumpCanvasContext.putImageData(stencilDumpImageData, 0, 0);
|
|
||||||
document.body.appendChild(stencilDumpCanvas);
|
|
||||||
//console.log(stencilData);
|
|
||||||
}
|
|
||||||
|
|
||||||
private loadFile(): void {
|
|
||||||
console.log("loadFile");
|
|
||||||
// TODO(pcwalton)
|
|
||||||
const file = unwrapNull(unwrapNull(this.openButton.files)[0]);
|
|
||||||
const reader = new FileReader;
|
|
||||||
reader.addEventListener('loadend', () => {
|
|
||||||
const gl = this.gl;
|
|
||||||
const arrayBuffer = staticCast(reader.result, ArrayBuffer);
|
|
||||||
const root = new RIFFChunk(new DataView(arrayBuffer));
|
|
||||||
for (const subchunk of root.subchunks(4)) {
|
|
||||||
const self = this;
|
|
||||||
|
|
||||||
const id = subchunk.stringID();
|
|
||||||
if (id === 'head') {
|
|
||||||
const headerData = subchunk.contents();
|
|
||||||
const version = headerData.getUint32(0, true);
|
|
||||||
if (version !== 0)
|
|
||||||
throw new Error("Unknown version!");
|
|
||||||
// Ignore the batch count and fetch the view box.
|
|
||||||
this.viewBox = new Rect(new Point2D(headerData.getFloat32(8, true),
|
|
||||||
headerData.getFloat32(12, true)),
|
|
||||||
new Size2D(headerData.getFloat32(16, true),
|
|
||||||
headerData.getFloat32(20, true)));
|
|
||||||
continue;
|
|
||||||
} else if (id === 'soli') {
|
|
||||||
self.solidTileCount = uploadArrayBuffer(subchunk,
|
|
||||||
this.solidTileVertexBuffer,
|
|
||||||
SOLID_TILE_INSTANCE_SIZE);
|
|
||||||
} else if (id === 'shad') {
|
|
||||||
this.shaderCount = subchunk.length() / 4;
|
|
||||||
gl.activeTexture(gl.TEXTURE0);
|
|
||||||
gl.bindTexture(gl.TEXTURE_2D, this.fillColorsTexture);
|
|
||||||
const textureDataView = subchunk.contents();
|
|
||||||
const textureData = new Uint8Array(textureDataView.buffer,
|
|
||||||
textureDataView.byteOffset,
|
|
||||||
textureDataView.byteLength);
|
|
||||||
gl.texImage2D(gl.TEXTURE_2D,
|
|
||||||
0,
|
|
||||||
gl.RGBA,
|
|
||||||
this.shaderCount,
|
|
||||||
1,
|
|
||||||
0,
|
|
||||||
gl.RGBA,
|
|
||||||
gl.UNSIGNED_BYTE,
|
|
||||||
textureData);
|
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
||||||
} else if (id === 'batc') {
|
|
||||||
const batch = new BatchBuffers(this.gl,
|
|
||||||
this.fillProgram,
|
|
||||||
this.maskTileProgram,
|
|
||||||
this.quadVertexBuffer);
|
|
||||||
for (const subsubchunk of subchunk.subchunks()) {
|
|
||||||
const id = subsubchunk.stringID();
|
|
||||||
console.log("id=", id);
|
|
||||||
if (id === 'fill') {
|
|
||||||
batch.fillPrimitiveCount = uploadArrayBuffer(subsubchunk,
|
|
||||||
batch.fillVertexBuffer,
|
|
||||||
FILL_INSTANCE_SIZE);
|
|
||||||
} else if (id === 'mask') {
|
|
||||||
batch.maskTileCount = uploadArrayBuffer(subsubchunk,
|
|
||||||
batch.maskTileVertexBuffer,
|
|
||||||
MASK_TILE_INSTANCE_SIZE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.batchBuffers.push(batch);
|
|
||||||
}
|
|
||||||
|
|
||||||
function uploadArrayBuffer(chunk: RIFFChunk,
|
|
||||||
buffer: WebGLBuffer,
|
|
||||||
instanceSize: number):
|
|
||||||
number {
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
||||||
gl.bufferData(gl.ARRAY_BUFFER, chunk.contents(), gl.DYNAMIC_DRAW);
|
|
||||||
return chunk.length() / instanceSize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.redraw();
|
|
||||||
}, false);
|
|
||||||
reader.readAsArrayBuffer(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onClick(event: MouseEvent): void {
|
|
||||||
this.redraw();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class BatchBuffers {
|
|
||||||
fillVertexBuffer: WebGLBuffer;
|
|
||||||
fillVertexArray: WebGLVertexArrayObject;
|
|
||||||
maskTileVertexBuffer: WebGLBuffer;
|
|
||||||
maskVertexArray: WebGLVertexArrayObject;
|
|
||||||
fillPrimitiveCount: number;
|
|
||||||
maskTileCount: number;
|
|
||||||
|
|
||||||
constructor(gl: WebGL2RenderingContext,
|
|
||||||
fillProgram: FillProgram,
|
|
||||||
maskTileProgram: MaskTileProgram,
|
|
||||||
quadVertexBuffer: WebGLBuffer) {
|
|
||||||
// Initialize fill VBOs.
|
|
||||||
this.fillVertexBuffer = unwrapNull(gl.createBuffer());
|
|
||||||
|
|
||||||
// Initialize fill VAO.
|
|
||||||
this.fillVertexArray = unwrapNull(gl.createVertexArray());
|
|
||||||
gl.bindVertexArray(this.fillVertexArray);
|
|
||||||
gl.useProgram(fillProgram.program);
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, quadVertexBuffer);
|
|
||||||
gl.vertexAttribPointer(fillProgram.attributes.TessCoord,
|
|
||||||
2,
|
|
||||||
gl.UNSIGNED_BYTE,
|
|
||||||
false,
|
|
||||||
0,
|
|
||||||
0);
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, this.fillVertexBuffer);
|
|
||||||
gl.vertexAttribIPointer(fillProgram.attributes.FromPx,
|
|
||||||
1,
|
|
||||||
gl.UNSIGNED_BYTE,
|
|
||||||
FILL_INSTANCE_SIZE,
|
|
||||||
0);
|
|
||||||
gl.vertexAttribDivisor(fillProgram.attributes.FromPx, 1);
|
|
||||||
gl.vertexAttribIPointer(fillProgram.attributes.ToPx,
|
|
||||||
1,
|
|
||||||
gl.UNSIGNED_BYTE,
|
|
||||||
FILL_INSTANCE_SIZE,
|
|
||||||
1);
|
|
||||||
gl.vertexAttribDivisor(fillProgram.attributes.ToPx, 1);
|
|
||||||
gl.vertexAttribPointer(fillProgram.attributes.FromSubpx,
|
|
||||||
2,
|
|
||||||
gl.UNSIGNED_BYTE,
|
|
||||||
true,
|
|
||||||
FILL_INSTANCE_SIZE,
|
|
||||||
2);
|
|
||||||
gl.vertexAttribDivisor(fillProgram.attributes.FromSubpx, 1);
|
|
||||||
gl.vertexAttribPointer(fillProgram.attributes.ToSubpx,
|
|
||||||
2,
|
|
||||||
gl.UNSIGNED_BYTE,
|
|
||||||
true,
|
|
||||||
FILL_INSTANCE_SIZE,
|
|
||||||
4);
|
|
||||||
gl.vertexAttribDivisor(fillProgram.attributes.ToSubpx, 1);
|
|
||||||
gl.vertexAttribIPointer(fillProgram.attributes.TileIndex,
|
|
||||||
1,
|
|
||||||
gl.UNSIGNED_SHORT,
|
|
||||||
FILL_INSTANCE_SIZE,
|
|
||||||
6);
|
|
||||||
gl.vertexAttribDivisor(fillProgram.attributes.TileIndex, 1);
|
|
||||||
gl.enableVertexAttribArray(fillProgram.attributes.TessCoord);
|
|
||||||
gl.enableVertexAttribArray(fillProgram.attributes.FromPx);
|
|
||||||
gl.enableVertexAttribArray(fillProgram.attributes.ToPx);
|
|
||||||
gl.enableVertexAttribArray(fillProgram.attributes.FromSubpx);
|
|
||||||
gl.enableVertexAttribArray(fillProgram.attributes.ToSubpx);
|
|
||||||
gl.enableVertexAttribArray(fillProgram.attributes.TileIndex);
|
|
||||||
|
|
||||||
// Initialize tile VBOs.
|
|
||||||
this.maskTileVertexBuffer = unwrapNull(gl.createBuffer());
|
|
||||||
|
|
||||||
// Initialize mask tile VAO.
|
|
||||||
this.maskVertexArray = unwrapNull(gl.createVertexArray());
|
|
||||||
gl.bindVertexArray(this.maskVertexArray);
|
|
||||||
gl.useProgram(maskTileProgram.program);
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, quadVertexBuffer);
|
|
||||||
gl.vertexAttribPointer(maskTileProgram.attributes.TessCoord,
|
|
||||||
2,
|
|
||||||
gl.UNSIGNED_BYTE,
|
|
||||||
false,
|
|
||||||
0,
|
|
||||||
0);
|
|
||||||
gl.bindBuffer(gl.ARRAY_BUFFER, this.maskTileVertexBuffer);
|
|
||||||
gl.vertexAttribPointer(maskTileProgram.attributes.TileOrigin,
|
|
||||||
2,
|
|
||||||
gl.SHORT,
|
|
||||||
false,
|
|
||||||
MASK_TILE_INSTANCE_SIZE,
|
|
||||||
0);
|
|
||||||
gl.vertexAttribDivisor(maskTileProgram.attributes.TileOrigin, 1);
|
|
||||||
gl.vertexAttribIPointer(maskTileProgram.attributes.Backdrop,
|
|
||||||
1,
|
|
||||||
gl.SHORT,
|
|
||||||
MASK_TILE_INSTANCE_SIZE,
|
|
||||||
4);
|
|
||||||
gl.vertexAttribDivisor(maskTileProgram.attributes.Backdrop, 1);
|
|
||||||
gl.vertexAttribIPointer(maskTileProgram.attributes.Object,
|
|
||||||
1,
|
|
||||||
gl.UNSIGNED_SHORT,
|
|
||||||
MASK_TILE_INSTANCE_SIZE,
|
|
||||||
6);
|
|
||||||
gl.vertexAttribDivisor(maskTileProgram.attributes.Object, 1);
|
|
||||||
gl.enableVertexAttribArray(maskTileProgram.attributes.TessCoord);
|
|
||||||
gl.enableVertexAttribArray(maskTileProgram.attributes.TileOrigin);
|
|
||||||
gl.enableVertexAttribArray(maskTileProgram.attributes.Backdrop);
|
|
||||||
gl.enableVertexAttribArray(maskTileProgram.attributes.Object);
|
|
||||||
|
|
||||||
this.fillPrimitiveCount = 0;
|
|
||||||
this.maskTileCount = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Program<U extends string, A extends string> {
|
|
||||||
program: WebGLProgram;
|
|
||||||
uniforms: {[key in U]: WebGLUniformLocation | null};
|
|
||||||
attributes: {[key in A]: number};
|
|
||||||
|
|
||||||
private vertexShader: WebGLShader;
|
|
||||||
private fragmentShader: WebGLShader;
|
|
||||||
|
|
||||||
constructor(gl: WebGL2RenderingContext,
|
|
||||||
vertexShaderSource: string,
|
|
||||||
fragmentShaderSource: string,
|
|
||||||
uniformNames: U[],
|
|
||||||
attributeNames: A[]) {
|
|
||||||
this.vertexShader = unwrapNull(gl.createShader(gl.VERTEX_SHADER));
|
|
||||||
gl.shaderSource(this.vertexShader, vertexShaderSource);
|
|
||||||
gl.compileShader(this.vertexShader);
|
|
||||||
if (!gl.getShaderParameter(this.vertexShader, gl.COMPILE_STATUS)) {
|
|
||||||
console.error(gl.getShaderInfoLog(this.vertexShader));
|
|
||||||
throw new Error("Vertex shader compilation failed!");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.fragmentShader = unwrapNull(gl.createShader(gl.FRAGMENT_SHADER));
|
|
||||||
gl.shaderSource(this.fragmentShader, fragmentShaderSource);
|
|
||||||
gl.compileShader(this.fragmentShader);
|
|
||||||
if (!gl.getShaderParameter(this.fragmentShader, gl.COMPILE_STATUS)) {
|
|
||||||
console.error(gl.getShaderInfoLog(this.fragmentShader));
|
|
||||||
throw new Error("Fragment shader compilation failed!");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.program = unwrapNull(gl.createProgram());
|
|
||||||
gl.attachShader(this.program, this.vertexShader);
|
|
||||||
gl.attachShader(this.program, this.fragmentShader);
|
|
||||||
gl.linkProgram(this.program);
|
|
||||||
if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) {
|
|
||||||
console.error(gl.getProgramInfoLog(this.program));
|
|
||||||
throw new Error("Program linking failed!");
|
|
||||||
}
|
|
||||||
|
|
||||||
const uniforms: {[key in U]?: WebGLUniformLocation | null} = {};
|
|
||||||
for (const uniformName of uniformNames)
|
|
||||||
uniforms[uniformName] = gl.getUniformLocation(this.program, "u" + uniformName);
|
|
||||||
this.uniforms = uniforms as {[key in U]: WebGLUniformLocation | null};
|
|
||||||
|
|
||||||
const attributes: {[key in A]?: number} = {};
|
|
||||||
for (const attributeName of attributeNames) {
|
|
||||||
attributes[attributeName] = unwrapNull(gl.getAttribLocation(this.program,
|
|
||||||
"a" + attributeName));
|
|
||||||
}
|
|
||||||
this.attributes = attributes as {[key in A]: number};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RIFFChunk {
|
|
||||||
private data: DataView;
|
|
||||||
|
|
||||||
constructor(data: DataView) {
|
|
||||||
this.data = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
stringID(): string {
|
|
||||||
return String.fromCharCode(this.data.getUint8(0),
|
|
||||||
this.data.getUint8(1),
|
|
||||||
this.data.getUint8(2),
|
|
||||||
this.data.getUint8(3));
|
|
||||||
}
|
|
||||||
|
|
||||||
length(): number {
|
|
||||||
return this.data.getUint32(4, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
contents(): DataView {
|
|
||||||
return new DataView(this.data.buffer, this.data.byteOffset + 8, this.length());
|
|
||||||
}
|
|
||||||
|
|
||||||
subchunks(initialOffset?: number | undefined): RIFFChunk[] {
|
|
||||||
const subchunks = [];
|
|
||||||
const contents = this.contents(), length = this.length();
|
|
||||||
let offset = initialOffset == null ? 0 : initialOffset;
|
|
||||||
while (offset < length) {
|
|
||||||
const subchunk = new RIFFChunk(new DataView(contents.buffer,
|
|
||||||
contents.byteOffset + offset,
|
|
||||||
length - offset));
|
|
||||||
subchunks.push(subchunk);
|
|
||||||
offset += subchunk.length() + 8;
|
|
||||||
}
|
|
||||||
return subchunks;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Queries {
|
|
||||||
fill: WebGLQuery;
|
|
||||||
solid: WebGLQuery;
|
|
||||||
};
|
|
||||||
|
|
||||||
function getQueryResult(gl: WebGL2RenderingContext, disjointTimerQueryExt: any, query: WebGLQuery):
|
|
||||||
Promise<number> {
|
|
||||||
function go(resolve: (n: number) => void): void {
|
|
||||||
const queryResultAvailable = disjointTimerQueryExt.QUERY_RESULT_AVAILABLE_EXT;
|
|
||||||
const queryResult = disjointTimerQueryExt.QUERY_RESULT_EXT;
|
|
||||||
if (!disjointTimerQueryExt.getQueryObjectEXT(query, queryResultAvailable)) {
|
|
||||||
setTimeout(() => go(resolve), 10);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve(disjointTimerQueryExt.getQueryObjectEXT(query, queryResult) / 1000000.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => go(resolve));
|
|
||||||
}
|
|
||||||
|
|
||||||
function processQueries(gl: WebGL2RenderingContext, disjointTimerQueryExt: any, queries: Queries):
|
|
||||||
void {
|
|
||||||
Promise.all([
|
|
||||||
getQueryResult(gl, disjointTimerQueryExt, queries.fill),
|
|
||||||
getQueryResult(gl, disjointTimerQueryExt, queries.solid),
|
|
||||||
]).then(results => {
|
|
||||||
const [fillResult, solidResult] = results;
|
|
||||||
console.log(fillResult, "ms fill/mask,", solidResult, "ms solid");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadAreaLUT(): Promise<HTMLImageElement> {
|
|
||||||
return new Promise((resolve, reject) => {;
|
|
||||||
const image = new Image;
|
|
||||||
image.src = AREA_LUT;
|
|
||||||
image.addEventListener('load', event => resolve(image), false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function main(): void {
|
|
||||||
loadAreaLUT().then(image => new App(image));
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => main(), false);
|
|
|
@ -1,42 +0,0 @@
|
||||||
#version 300 es
|
|
||||||
|
|
||||||
// pathfinder/demo2/stencil.fs.glsl
|
|
||||||
//
|
|
||||||
// Copyright © 2018 The Pathfinder Project Developers.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
|
||||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
|
||||||
// option. This file may not be copied, modified, or distributed
|
|
||||||
// except according to those terms.
|
|
||||||
|
|
||||||
precision highp float;
|
|
||||||
|
|
||||||
uniform sampler2D uAreaLUT;
|
|
||||||
|
|
||||||
in vec2 vFrom;
|
|
||||||
in vec2 vTo;
|
|
||||||
|
|
||||||
out vec4 oFragColor;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
// Unpack.
|
|
||||||
vec2 from = vFrom, to = vTo;
|
|
||||||
|
|
||||||
// Determine winding, and sort into a consistent order so we only need to find one root below.
|
|
||||||
bool winding = from.x < to.x;
|
|
||||||
vec2 left = winding ? from : to, right = winding ? to : from;
|
|
||||||
|
|
||||||
// Shoot a vertical ray toward the curve.
|
|
||||||
vec2 window = clamp(vec2(from.x, to.x), -0.5, 0.5);
|
|
||||||
float offset = mix(window.x, window.y, 0.5) - left.x;
|
|
||||||
float t = offset / (right.x - left.x);
|
|
||||||
|
|
||||||
// Compute position and derivative to form a line approximation.
|
|
||||||
float y = mix(left.y, right.y, t);
|
|
||||||
float d = (right.y - left.y) / (right.x - left.x);
|
|
||||||
|
|
||||||
// Look up area under that line, and scale horizontally to the window size.
|
|
||||||
float dX = window.x - window.y;
|
|
||||||
oFragColor = vec4(texture(uAreaLUT, vec2(y + 8.0, abs(d * dX)) / 16.0).r * dX);
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
#version 300 es
|
|
||||||
|
|
||||||
// pathfinder/demo2/stencil.vs.glsl
|
|
||||||
//
|
|
||||||
// Copyright © 2018 The Pathfinder Project Developers.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
|
||||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
|
||||||
// option. This file may not be copied, modified, or distributed
|
|
||||||
// except according to those terms.
|
|
||||||
|
|
||||||
precision highp float;
|
|
||||||
|
|
||||||
uniform vec2 uFramebufferSize;
|
|
||||||
uniform vec2 uTileSize;
|
|
||||||
|
|
||||||
in vec2 aTessCoord;
|
|
||||||
in uint aFromPx;
|
|
||||||
in uint aToPx;
|
|
||||||
in vec2 aFromSubpx;
|
|
||||||
in vec2 aToSubpx;
|
|
||||||
in uint aTileIndex;
|
|
||||||
|
|
||||||
out vec2 vFrom;
|
|
||||||
out vec2 vTo;
|
|
||||||
|
|
||||||
vec2 computeTileOffset(uint tileIndex, float stencilTextureWidth) {
|
|
||||||
uint tilesPerRow = uint(stencilTextureWidth / uTileSize.x);
|
|
||||||
uvec2 tileOffset = uvec2(aTileIndex % tilesPerRow, aTileIndex / tilesPerRow);
|
|
||||||
return vec2(tileOffset) * uTileSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
vec2 tileOrigin = computeTileOffset(aTileIndex, uFramebufferSize.x);
|
|
||||||
|
|
||||||
vec2 from = vec2(aFromPx & 15u, aFromPx >> 4u) + aFromSubpx;
|
|
||||||
vec2 to = vec2(aToPx & 15u, aToPx >> 4u) + aToSubpx;
|
|
||||||
|
|
||||||
vec2 position;
|
|
||||||
bool zeroArea = !(abs(from.x - to.x) > 0.1) || !(abs(uTileSize.y - min(from.y, to.y)) > 0.1);
|
|
||||||
if (aTessCoord.x < 0.5)
|
|
||||||
position.x = floor(min(from.x, to.x));
|
|
||||||
else
|
|
||||||
position.x = ceil(max(from.x, to.x));
|
|
||||||
if (aTessCoord.y < 0.5)
|
|
||||||
position.y = floor(min(from.y, to.y));
|
|
||||||
else
|
|
||||||
position.y = uTileSize.y;
|
|
||||||
|
|
||||||
vFrom = from - position;
|
|
||||||
vTo = to - position;
|
|
||||||
|
|
||||||
if (zeroArea)
|
|
||||||
gl_Position = vec4(0.0);
|
|
||||||
else
|
|
||||||
gl_Position = vec4((tileOrigin + position) / uFramebufferSize * 2.0 - 1.0, 0.0, 1.0);
|
|
||||||
}
|
|
710
demo2/tiling.ts
710
demo2/tiling.ts
|
@ -1,710 +0,0 @@
|
||||||
// pathfinder/demo2/tiling.ts
|
|
||||||
//
|
|
||||||
// Copyright © 2018 The Pathfinder Project Developers.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
|
||||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
|
||||||
// option. This file may not be copied, modified, or distributed
|
|
||||||
// except according to those terms.
|
|
||||||
|
|
||||||
import {Point2D, Rect, Size2D, cross, lerp} from "./geometry";
|
|
||||||
import {PathSegment} from "./path-utils";
|
|
||||||
import {panic, staticCast, unwrapNull} from "./util";
|
|
||||||
|
|
||||||
export const TILE_SIZE: Size2D = {width: 16.0, height: 16.0};
|
|
||||||
|
|
||||||
export interface SVGPath {
|
|
||||||
abs(): SVGPath;
|
|
||||||
translate(x: number, y: number): SVGPath;
|
|
||||||
matrix(m: number[]): SVGPath;
|
|
||||||
iterate(f: (segment: string[], index: number, x: number, y: number) => string[][] | void):
|
|
||||||
SVGPath;
|
|
||||||
unshort(): SVGPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SVGPath: (path: string) => SVGPath = require('svgpath');
|
|
||||||
|
|
||||||
interface EndpointIndex {
|
|
||||||
subpathIndex: number;
|
|
||||||
endpointIndex: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class Tiler {
|
|
||||||
private path: SVGPath;
|
|
||||||
private endpoints: SubpathEndpoints[];
|
|
||||||
private sortedEdges: Edge[];
|
|
||||||
private boundingRect: Rect | null;
|
|
||||||
private strips: Strip[];
|
|
||||||
private tileStrips: TileStrip[];
|
|
||||||
|
|
||||||
constructor(path: SVGPath) {
|
|
||||||
this.path = path;
|
|
||||||
//console.log("tiler: path=", path);
|
|
||||||
|
|
||||||
// Accumulate endpoints.
|
|
||||||
this.endpoints = [];
|
|
||||||
let currentSubpathEndpoints = new SubpathEndpoints;
|
|
||||||
path.iterate(segString => {
|
|
||||||
const segment = new PathSegment(segString);
|
|
||||||
//console.log("segment", segment);
|
|
||||||
switch (segment.command) {
|
|
||||||
case 'M':
|
|
||||||
if (!currentSubpathEndpoints.isEmpty()) {
|
|
||||||
this.endpoints.push(currentSubpathEndpoints);
|
|
||||||
currentSubpathEndpoints = new SubpathEndpoints;
|
|
||||||
}
|
|
||||||
currentSubpathEndpoints.controlPoints.push(null);
|
|
||||||
currentSubpathEndpoints.endpoints.push(segment.points[0]);
|
|
||||||
break;
|
|
||||||
case 'L':
|
|
||||||
case 'S':
|
|
||||||
// TODO(pcwalton): Canonicalize 'S'.
|
|
||||||
currentSubpathEndpoints.controlPoints.push(null);
|
|
||||||
currentSubpathEndpoints.endpoints.push(unwrapNull(segment.to()));
|
|
||||||
break;
|
|
||||||
case 'Q':
|
|
||||||
currentSubpathEndpoints.controlPoints.push(segment.points[0]);
|
|
||||||
currentSubpathEndpoints.endpoints.push(unwrapNull(segment.to()));
|
|
||||||
break;
|
|
||||||
case 'Z':
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
panic("Unexpected path command: " + segment.command);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!currentSubpathEndpoints.isEmpty())
|
|
||||||
this.endpoints.push(currentSubpathEndpoints);
|
|
||||||
|
|
||||||
// Sort edges, and accumulate bounding rect.
|
|
||||||
this.sortedEdges = [];
|
|
||||||
this.boundingRect = null;
|
|
||||||
for (let subpathIndex = 0; subpathIndex < this.endpoints.length; subpathIndex++) {
|
|
||||||
const subpathEndpoints = this.endpoints[subpathIndex];
|
|
||||||
for (let endpointIndex = 0;
|
|
||||||
endpointIndex < subpathEndpoints.endpoints.length;
|
|
||||||
endpointIndex++) {
|
|
||||||
this.sortedEdges.push(this.nextEdgeFromEndpoint({subpathIndex, endpointIndex}));
|
|
||||||
|
|
||||||
const endpoint = subpathEndpoints.endpoints[endpointIndex];
|
|
||||||
if (this.boundingRect == null)
|
|
||||||
this.boundingRect = new Rect(endpoint, {width: 0, height: 0});
|
|
||||||
else
|
|
||||||
this.boundingRect = this.boundingRect.unionWithPoint(endpoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.sortedEdges.sort((edgeA, edgeB) => {
|
|
||||||
return Math.min(edgeA.from.y, edgeA.to.y) - Math.min(edgeB.from.y, edgeB.to.y);
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
// Dump endpoints.
|
|
||||||
const allEndpoints = this.sortedEndpointIndices.map(index => {
|
|
||||||
return {
|
|
||||||
index: index,
|
|
||||||
endpoint: this.endpoints[index.subpathIndex].endpoints[index.endpointIndex],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
//console.log("allEndpoints", allEndpoints);
|
|
||||||
*/
|
|
||||||
|
|
||||||
this.strips = [];
|
|
||||||
this.tileStrips = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
tile(): void {
|
|
||||||
if (this.boundingRect == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const activeIntervals = new Intervals(this.boundingRect.maxX());;
|
|
||||||
let activeEdges: Edge[] = [];
|
|
||||||
let nextEdgeIndex = 0;
|
|
||||||
this.strips = [];
|
|
||||||
|
|
||||||
let tileTop = this.boundingRect.origin.y - this.boundingRect.origin.y % TILE_SIZE.height;
|
|
||||||
while (tileTop < this.boundingRect.maxY()) {
|
|
||||||
const strip = new Strip(tileTop);
|
|
||||||
const tileBottom = tileTop + TILE_SIZE.height;
|
|
||||||
|
|
||||||
// Populate tile strip with active intervals.
|
|
||||||
// TODO(pcwalton): Compress this.
|
|
||||||
for (const interval of activeIntervals.intervalRanges()) {
|
|
||||||
if (interval.winding === 0)
|
|
||||||
continue;
|
|
||||||
const startPoint = new Point2D(interval.start, tileTop);
|
|
||||||
const endPoint = new Point2D(interval.end, tileTop);
|
|
||||||
if (interval.winding < 0)
|
|
||||||
strip.pushEdge(new Edge(startPoint, null, endPoint));
|
|
||||||
else
|
|
||||||
strip.pushEdge(new Edge(endPoint, null, startPoint));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate tile strip with active edges.
|
|
||||||
const oldEdges = activeEdges;
|
|
||||||
activeEdges = [];
|
|
||||||
for (const activeEdge of oldEdges)
|
|
||||||
this.processEdgeY(activeEdge, strip, activeEdges, activeIntervals, tileTop);
|
|
||||||
|
|
||||||
while (nextEdgeIndex < this.sortedEdges.length) {
|
|
||||||
const edge = this.sortedEdges[nextEdgeIndex];
|
|
||||||
if (edge.from.y > tileBottom && edge.to.y > tileBottom)
|
|
||||||
break;
|
|
||||||
|
|
||||||
this.processEdgeY(edge, strip, activeEdges, activeIntervals, tileTop);
|
|
||||||
//console.log("new intervals:", JSON.stringify(activeIntervals));
|
|
||||||
nextEdgeIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.strips.push(strip);
|
|
||||||
tileTop = tileBottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cut up tile strips.
|
|
||||||
this.tileStrips = [];
|
|
||||||
for (const strip of this.strips) {
|
|
||||||
const tileStrip = this.divideStrip(strip);
|
|
||||||
if (!tileStrip.isEmpty())
|
|
||||||
this.tileStrips.push(tileStrip);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private divideStrip(strip: Strip): TileStrip {
|
|
||||||
// Sort edges.
|
|
||||||
const sortedEdges = strip.edges.slice(0);
|
|
||||||
sortedEdges.sort((edgeA, edgeB) => {
|
|
||||||
return Math.min(edgeA.from.x, edgeA.to.x) - Math.min(edgeB.from.x, edgeB.to.x);
|
|
||||||
});
|
|
||||||
|
|
||||||
const tileStrip = new TileStrip(strip.tileTop);
|
|
||||||
const boundingRect = unwrapNull(this.boundingRect);
|
|
||||||
let tileLeft = boundingRect.origin.x - boundingRect.origin.x % TILE_SIZE.width;
|
|
||||||
let activeEdges: Edge[] = [];
|
|
||||||
let nextEdgeIndex = 0;
|
|
||||||
|
|
||||||
while (tileLeft < boundingRect.maxX()) {
|
|
||||||
const tile = new Tile(tileLeft);
|
|
||||||
const tileRight = tileLeft + TILE_SIZE.width;
|
|
||||||
|
|
||||||
// Populate tile with active edges.
|
|
||||||
const oldEdges = activeEdges;
|
|
||||||
activeEdges = [];
|
|
||||||
for (const activeEdge of oldEdges)
|
|
||||||
this.processEdgeX(activeEdge, tile, activeEdges);
|
|
||||||
|
|
||||||
while (nextEdgeIndex < sortedEdges.length) {
|
|
||||||
const edge = sortedEdges[nextEdgeIndex];
|
|
||||||
if (edge.from.x > tileRight && edge.to.x > tileRight)
|
|
||||||
break;
|
|
||||||
|
|
||||||
this.processEdgeX(edge, tile, activeEdges);
|
|
||||||
nextEdgeIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tile.isEmpty())
|
|
||||||
tileStrip.pushTile(tile);
|
|
||||||
|
|
||||||
tileLeft = tileRight;
|
|
||||||
}
|
|
||||||
|
|
||||||
return tileStrip;
|
|
||||||
}
|
|
||||||
|
|
||||||
getStrips(): Strip[] {
|
|
||||||
return this.strips;
|
|
||||||
}
|
|
||||||
|
|
||||||
getTileStrips(): TileStrip[] {
|
|
||||||
return this.tileStrips;
|
|
||||||
}
|
|
||||||
|
|
||||||
getBoundingRect(): Rect {
|
|
||||||
if (this.boundingRect == null)
|
|
||||||
return new Rect(new Point2D(0, 0), {width: 0, height: 0});
|
|
||||||
|
|
||||||
const tileLeft = this.boundingRect.origin.x - this.boundingRect.origin.x % TILE_SIZE.width;
|
|
||||||
const tileRight = Math.ceil(this.boundingRect.maxX() / TILE_SIZE.width) * TILE_SIZE.width;
|
|
||||||
const tileTop = this.boundingRect.origin.y - this.boundingRect.origin.y % TILE_SIZE.height;
|
|
||||||
const tileBottom = Math.ceil(this.boundingRect.maxY() / TILE_SIZE.height) *
|
|
||||||
TILE_SIZE.height;
|
|
||||||
return new Rect(new Point2D(tileLeft, tileTop),
|
|
||||||
{width: tileRight - tileLeft, height: tileBottom - tileTop});
|
|
||||||
}
|
|
||||||
|
|
||||||
private processEdgeX(edge: Edge, tile: Tile, activeEdges: Edge[]): void {
|
|
||||||
const tileRight = tile.tileLeft + TILE_SIZE.width;
|
|
||||||
const clipped = this.clipEdgeX(edge, tileRight);
|
|
||||||
|
|
||||||
if (clipped.left != null)
|
|
||||||
tile.pushEdge(clipped.left);
|
|
||||||
|
|
||||||
if (clipped.right != null)
|
|
||||||
activeEdges.push(clipped.right);
|
|
||||||
}
|
|
||||||
|
|
||||||
private processEdgeY(edge: Edge,
|
|
||||||
tileStrip: Strip,
|
|
||||||
activeEdges: Edge[],
|
|
||||||
intervals: Intervals,
|
|
||||||
tileTop: number):
|
|
||||||
void {
|
|
||||||
const tileBottom = tileTop + TILE_SIZE.height;
|
|
||||||
const clipped = this.clipEdgeY(edge, tileBottom);
|
|
||||||
|
|
||||||
if (clipped.upper != null) {
|
|
||||||
//console.log("pushing clipped upper edge:", JSON.stringify(clipped.upper));
|
|
||||||
tileStrip.pushEdge(clipped.upper);
|
|
||||||
|
|
||||||
if (clipped.upper.from.x <= clipped.upper.to.x)
|
|
||||||
intervals.add(new IntervalRange(clipped.upper.from.x, clipped.upper.to.x, -1));
|
|
||||||
else
|
|
||||||
intervals.add(new IntervalRange(clipped.upper.to.x, clipped.upper.from.x, 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clipped.lower != null)
|
|
||||||
activeEdges.push(clipped.lower);
|
|
||||||
}
|
|
||||||
|
|
||||||
private clipEdgeX(edge: Edge, x: number): ClippedEdgesX {
|
|
||||||
const EPSILON: number = 0.00001;
|
|
||||||
|
|
||||||
if (edge.from.x < x && edge.to.x < x)
|
|
||||||
return {left: edge, right: null};
|
|
||||||
if (edge.from.x > x && edge.to.x > x)
|
|
||||||
return {left: null, right: edge};
|
|
||||||
|
|
||||||
let minT = 0.0, maxT = 1.0;
|
|
||||||
while (maxT - minT > EPSILON) {
|
|
||||||
const midT = lerp(minT, maxT, 0.5);
|
|
||||||
const edges = edge.subdivideAt(midT);
|
|
||||||
if ((edges.prev.from.x < x && edges.prev.to.x > x) ||
|
|
||||||
(edges.prev.from.x > x && edges.prev.to.x < x)) {
|
|
||||||
maxT = midT;
|
|
||||||
} else {
|
|
||||||
minT = midT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const midT = lerp(minT, maxT, 0.5);
|
|
||||||
const edges = edge.subdivideAt(midT);
|
|
||||||
if (edge.from.x < x)
|
|
||||||
return {left: edges.prev, right: edges.next};
|
|
||||||
return {left: edges.next, right: edges.prev};
|
|
||||||
}
|
|
||||||
|
|
||||||
private clipEdgeY(edge: Edge, y: number): ClippedEdgesY {
|
|
||||||
const EPSILON: number = 0.00001;
|
|
||||||
|
|
||||||
if (edge.from.y < y && edge.to.y < y)
|
|
||||||
return {upper: edge, lower: null};
|
|
||||||
if (edge.from.y > y && edge.to.y > y)
|
|
||||||
return {upper: null, lower: edge};
|
|
||||||
|
|
||||||
let minT = 0.0, maxT = 1.0;
|
|
||||||
while (maxT - minT > EPSILON) {
|
|
||||||
const midT = lerp(minT, maxT, 0.5);
|
|
||||||
const edges = edge.subdivideAt(midT);
|
|
||||||
if ((edges.prev.from.y < y && edges.prev.to.y > y) ||
|
|
||||||
(edges.prev.from.y > y && edges.prev.to.y < y)) {
|
|
||||||
maxT = midT;
|
|
||||||
} else {
|
|
||||||
minT = midT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const midT = lerp(minT, maxT, 0.5);
|
|
||||||
const edges = edge.subdivideAt(midT);
|
|
||||||
if (edge.from.y < y)
|
|
||||||
return {upper: edges.prev, lower: edges.next};
|
|
||||||
return {upper: edges.next, lower: edges.prev};
|
|
||||||
}
|
|
||||||
|
|
||||||
private nextEdgeFromEndpoint(endpointIndex: EndpointIndex): Edge {
|
|
||||||
const subpathEndpoints = this.endpoints[endpointIndex.subpathIndex];
|
|
||||||
const nextEndpointIndex =
|
|
||||||
subpathEndpoints.nextEndpointIndexOf(endpointIndex.endpointIndex);
|
|
||||||
return new Edge(subpathEndpoints.endpoints[endpointIndex.endpointIndex],
|
|
||||||
subpathEndpoints.controlPoints[nextEndpointIndex],
|
|
||||||
subpathEndpoints.endpoints[nextEndpointIndex]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SubpathEndpoints {
|
|
||||||
endpoints: Point2D[];
|
|
||||||
controlPoints: (Point2D | null)[];
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.endpoints = [];
|
|
||||||
this.controlPoints = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
isEmpty(): boolean {
|
|
||||||
return this.endpoints.length < 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
prevEndpointIndexOf(index: number): number {
|
|
||||||
const prevIndex = index - 1;
|
|
||||||
return prevIndex < 0 ? this.endpoints.length - 1 : prevIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
nextEndpointIndexOf(index: number): number {
|
|
||||||
const nextIndex = index + 1;
|
|
||||||
return nextIndex >= this.endpoints.length ? 0 : nextIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
prevEndpointOf(index: number): Point2D {
|
|
||||||
return this.endpoints[this.prevEndpointIndexOf(index)];
|
|
||||||
}
|
|
||||||
|
|
||||||
nextEndpointOf(index: number): Point2D {
|
|
||||||
return this.endpoints[this.nextEndpointIndexOf(index)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Edge {
|
|
||||||
from: Point2D;
|
|
||||||
ctrl: Point2D | null;
|
|
||||||
to: Point2D;
|
|
||||||
|
|
||||||
constructor(from: Point2D, ctrl: Point2D | null, to: Point2D) {
|
|
||||||
this.from = from;
|
|
||||||
this.ctrl = ctrl;
|
|
||||||
this.to = to;
|
|
||||||
Object.freeze(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
subdivideAt(t: number): SubdividedEdges {
|
|
||||||
if (this.ctrl == null) {
|
|
||||||
const mid = this.from.lerp(this.to, t);
|
|
||||||
return {
|
|
||||||
prev: new Edge(this.from, null, mid),
|
|
||||||
next: new Edge(mid, null, this.to),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const ctrlA = this.from.lerp(this.ctrl, t);
|
|
||||||
const ctrlB = this.ctrl.lerp(this.to, t);
|
|
||||||
const mid = ctrlA.lerp(ctrlB, t);
|
|
||||||
return {
|
|
||||||
prev: new Edge(this.from, ctrlA, mid),
|
|
||||||
next: new Edge(mid, ctrlB, this.to),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toSVGPieces(): string[] {
|
|
||||||
if (this.ctrl == null)
|
|
||||||
return ['L', "" + this.to.x, "" + this.to.y];
|
|
||||||
return ['Q', "" + this.ctrl.x, "" + this.ctrl.y, "" + this.to.x, "" + this.to.y];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SubdividedEdges {
|
|
||||||
prev: Edge;
|
|
||||||
next: Edge;
|
|
||||||
}
|
|
||||||
|
|
||||||
class Strip {
|
|
||||||
edges: Edge[];
|
|
||||||
tileTop: number;
|
|
||||||
|
|
||||||
constructor(tileTop: number) {
|
|
||||||
this.edges = [];
|
|
||||||
this.tileTop = tileTop;
|
|
||||||
}
|
|
||||||
|
|
||||||
pushEdge(edge: Edge): void {
|
|
||||||
this.edges.push(new Edge(edge.from.translate(0, -this.tileTop),
|
|
||||||
edge.ctrl == null ? null : edge.ctrl.translate(0, -this.tileTop),
|
|
||||||
edge.to.translate(0, -this.tileTop)));
|
|
||||||
}
|
|
||||||
|
|
||||||
tileBottom(): number {
|
|
||||||
return this.tileTop + TILE_SIZE.height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TileStrip {
|
|
||||||
tiles: Tile[];
|
|
||||||
tileTop: number;
|
|
||||||
|
|
||||||
constructor(tileTop: number) {
|
|
||||||
this.tiles = [];
|
|
||||||
this.tileTop = tileTop;
|
|
||||||
}
|
|
||||||
|
|
||||||
pushTile(tile: Tile): void {
|
|
||||||
this.tiles.push(tile);
|
|
||||||
}
|
|
||||||
|
|
||||||
tileBottom(): number {
|
|
||||||
return this.tileTop + TILE_SIZE.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
isEmpty(): boolean {
|
|
||||||
return this.tiles.length === 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Tile {
|
|
||||||
edges: Edge[];
|
|
||||||
tileLeft: number;
|
|
||||||
|
|
||||||
constructor(tileLeft: number) {
|
|
||||||
this.edges = [];
|
|
||||||
this.tileLeft = tileLeft;
|
|
||||||
}
|
|
||||||
|
|
||||||
pushEdge(edge: Edge): void {
|
|
||||||
this.edges.push(new Edge(edge.from.translate(-this.tileLeft, 0),
|
|
||||||
edge.ctrl == null ? null : edge.ctrl.translate(-this.tileLeft, 0),
|
|
||||||
edge.to.translate(-this.tileLeft, 0)));
|
|
||||||
}
|
|
||||||
|
|
||||||
isEmpty(): boolean {
|
|
||||||
return this.edges.length === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
isFilled(): boolean {
|
|
||||||
if (this.edges.length !== 1)
|
|
||||||
return false;
|
|
||||||
const edge = this.edges[0];
|
|
||||||
if (edge.ctrl != null)
|
|
||||||
return false;
|
|
||||||
//console.log("single edge:", JSON.stringify(edge));
|
|
||||||
const left = edge.from.x < edge.to.x ? edge.from : edge.to;
|
|
||||||
const right = edge.from.x < edge.to.x ? edge.to : edge.from;
|
|
||||||
return left.approxEq(new Point2D(0, 0), 0.1) &&
|
|
||||||
right.approxEq(new Point2D(TILE_SIZE.width, 0), 0.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ClippedEdgesX {
|
|
||||||
left: Edge | null;
|
|
||||||
right: Edge | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ClippedEdgesY {
|
|
||||||
upper: Edge | null;
|
|
||||||
lower: Edge | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
class Intervals {
|
|
||||||
private ranges: IntervalRange[];
|
|
||||||
|
|
||||||
constructor(width: number) {
|
|
||||||
this.ranges = [new IntervalRange(0, width, 0)];
|
|
||||||
}
|
|
||||||
|
|
||||||
intervalRanges(): IntervalRange[] {
|
|
||||||
return this.ranges;
|
|
||||||
}
|
|
||||||
|
|
||||||
add(range: IntervalRange): void {
|
|
||||||
//console.log("IntervalRange.add(", range, ")");
|
|
||||||
//console.log("... before ...", JSON.stringify(this));
|
|
||||||
|
|
||||||
this.splitAt(range.start);
|
|
||||||
this.splitAt(range.end);
|
|
||||||
|
|
||||||
let startIndex = this.ranges.length, endIndex = this.ranges.length;
|
|
||||||
for (let i = 0; i < this.ranges.length; i++) {
|
|
||||||
if (range.start === this.ranges[i].start)
|
|
||||||
startIndex = i;
|
|
||||||
if (range.end === this.ranges[i].end)
|
|
||||||
endIndex = i + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adjust winding numbers.
|
|
||||||
for (let i = startIndex; i < endIndex; i++)
|
|
||||||
this.ranges[i].winding += range.winding;
|
|
||||||
|
|
||||||
this.mergeAdjacent();
|
|
||||||
|
|
||||||
//console.log("... after ...", JSON.stringify(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
clear(): void {
|
|
||||||
this.ranges = [new IntervalRange(0, this.ranges[this.ranges.length - 1].end, 0)];
|
|
||||||
}
|
|
||||||
|
|
||||||
private splitAt(value: number): void {
|
|
||||||
for (let i = 0; i < this.ranges.length; i++) {
|
|
||||||
if (this.ranges[i].start < value && value < this.ranges[i].end) {
|
|
||||||
const oldRange = this.ranges[i];
|
|
||||||
const range0 = new IntervalRange(oldRange.start, value, oldRange.winding);
|
|
||||||
const range1 = new IntervalRange(value, oldRange.end, oldRange.winding);
|
|
||||||
this.ranges.splice(i, 1, range0, range1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private mergeAdjacent(): void {
|
|
||||||
let i = 0;
|
|
||||||
while (i + 1 < this.ranges.length) {
|
|
||||||
if (this.ranges[i].end === this.ranges[i + 1].start &&
|
|
||||||
this.ranges[i].winding === this.ranges[i + 1].winding) {
|
|
||||||
this.ranges[i].end = this.ranges[i + 1].end;
|
|
||||||
this.ranges.splice(i + 1, 1);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class IntervalRange {
|
|
||||||
start: number;
|
|
||||||
end: number;
|
|
||||||
winding: number;
|
|
||||||
|
|
||||||
constructor(start: number, end: number, winding: number) {
|
|
||||||
this.start = start;
|
|
||||||
this.end = end;
|
|
||||||
this.winding = winding;
|
|
||||||
}
|
|
||||||
|
|
||||||
contains(value: number): boolean {
|
|
||||||
return value >= this.start && value < this.end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debugging
|
|
||||||
|
|
||||||
const SVG_NS: string = "http://www.w3.org/2000/svg";
|
|
||||||
|
|
||||||
export class TileDebugger {
|
|
||||||
svg: SVGElement;
|
|
||||||
size: Size2D;
|
|
||||||
|
|
||||||
constructor(document: HTMLDocument) {
|
|
||||||
this.svg = staticCast(document.createElementNS(SVG_NS, 'svg'), SVGElement);
|
|
||||||
|
|
||||||
this.size = {width: 0, height: 0};
|
|
||||||
|
|
||||||
this.svg.style.position = 'absolute';
|
|
||||||
this.svg.style.left = "0";
|
|
||||||
this.svg.style.top = "0";
|
|
||||||
this.updateSVGSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
addTiler(tiler: Tiler, fillColor: string, id: string): void {
|
|
||||||
const boundingRect = tiler.getBoundingRect();
|
|
||||||
this.size.width = Math.max(this.size.width, boundingRect.maxX());
|
|
||||||
this.size.height = Math.max(this.size.height, boundingRect.maxY());
|
|
||||||
|
|
||||||
const tileStrips = tiler.getTileStrips();
|
|
||||||
for (let tileStripIndex = 0; tileStripIndex < tileStrips.length; tileStripIndex++) {
|
|
||||||
const tileStrip = tileStrips[tileStripIndex];
|
|
||||||
|
|
||||||
for (let tileIndex = 0; tileIndex < tileStrip.tiles.length; tileIndex++) {
|
|
||||||
const tile = tileStrip.tiles[tileIndex];
|
|
||||||
|
|
||||||
let path = "";
|
|
||||||
for (const edge of tile.edges) {
|
|
||||||
path += "M " + edge.from.x + " " + edge.from.y + " ";
|
|
||||||
path += "L " + edge.to.x + " " + edge.to.y + " ";
|
|
||||||
path += "L " + edge.to.x + " " + TILE_SIZE.height + " ";
|
|
||||||
path += "L " + edge.from.x + " " + TILE_SIZE.height + " ";
|
|
||||||
path += "Z ";
|
|
||||||
}
|
|
||||||
|
|
||||||
const pathElement = staticCast(document.createElementNS(SVG_NS, 'path'),
|
|
||||||
SVGPathElement);
|
|
||||||
pathElement.setAttribute('d', path);
|
|
||||||
pathElement.setAttribute('fill', fillColor);
|
|
||||||
//pathElement.setAttribute('stroke', "rgb(0, 128.0, 0)");
|
|
||||||
pathElement.setAttribute('data-tile-id', id);
|
|
||||||
pathElement.setAttribute('data-tile-index', "" + tileIndex);
|
|
||||||
pathElement.setAttribute('data-tile-strip-index', "" + tileStripIndex);
|
|
||||||
pathElement.setAttribute('transform',
|
|
||||||
"translate(" + tile.tileLeft + " " + tileStrip.tileTop + ")");
|
|
||||||
this.svg.appendChild(pathElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateSVGSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateSVGSize(): void {
|
|
||||||
this.svg.style.width = this.size.width + "px";
|
|
||||||
this.svg.style.height = this.size.height + "px";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertEq<T>(actual: T, expected: T): void {
|
|
||||||
if (JSON.stringify(expected) !== JSON.stringify(actual)) {
|
|
||||||
console.error("expected", expected, "but found", actual);
|
|
||||||
throw new Error("Assertion failed!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function testIntervals(): void {
|
|
||||||
const intervals = new Intervals(7);
|
|
||||||
intervals.add(new IntervalRange(1, 2, 1));
|
|
||||||
intervals.add(new IntervalRange(3, 4, 1));
|
|
||||||
intervals.add(new IntervalRange(5, 6, 1));
|
|
||||||
assertEq(intervals.intervalRanges(), [
|
|
||||||
new IntervalRange(0, 1, 0),
|
|
||||||
new IntervalRange(1, 2, 1),
|
|
||||||
new IntervalRange(2, 3, 0),
|
|
||||||
new IntervalRange(3, 4, 1),
|
|
||||||
new IntervalRange(4, 5, 0),
|
|
||||||
new IntervalRange(5, 6, 1),
|
|
||||||
new IntervalRange(6, 7, 0),
|
|
||||||
]);
|
|
||||||
|
|
||||||
intervals.clear();
|
|
||||||
intervals.add(new IntervalRange(1, 2, 1));
|
|
||||||
intervals.add(new IntervalRange(2, 3, 1));
|
|
||||||
assertEq(intervals.intervalRanges(), [
|
|
||||||
new IntervalRange(0, 1, 0),
|
|
||||||
new IntervalRange(1, 3, 1),
|
|
||||||
new IntervalRange(3, 7, 0),
|
|
||||||
]);
|
|
||||||
|
|
||||||
intervals.clear();
|
|
||||||
intervals.add(new IntervalRange(1, 4, 1));
|
|
||||||
intervals.add(new IntervalRange(3, 5, 1));
|
|
||||||
assertEq(intervals.intervalRanges(), [
|
|
||||||
new IntervalRange(0, 1, 0),
|
|
||||||
new IntervalRange(1, 3, 1),
|
|
||||||
new IntervalRange(3, 4, 2),
|
|
||||||
new IntervalRange(4, 5, 1),
|
|
||||||
new IntervalRange(5, 7, 0),
|
|
||||||
]);
|
|
||||||
|
|
||||||
intervals.clear();
|
|
||||||
intervals.add(new IntervalRange(2, 3.5, 1));
|
|
||||||
intervals.add(new IntervalRange(3, 5, 1));
|
|
||||||
intervals.add(new IntervalRange(6, 7, 1));
|
|
||||||
assertEq(intervals.intervalRanges(), [
|
|
||||||
new IntervalRange(0, 2, 0),
|
|
||||||
new IntervalRange(2, 3, 1),
|
|
||||||
new IntervalRange(3, 3.5, 2),
|
|
||||||
new IntervalRange(3.5, 5, 1),
|
|
||||||
new IntervalRange(5, 6, 0),
|
|
||||||
new IntervalRange(6, 7, 1),
|
|
||||||
]);
|
|
||||||
|
|
||||||
intervals.clear();
|
|
||||||
intervals.add(new IntervalRange(2, 5, 1));
|
|
||||||
intervals.add(new IntervalRange(3, 3.5, -1));
|
|
||||||
assertEq(intervals.intervalRanges(), [
|
|
||||||
new IntervalRange(0, 2, 0),
|
|
||||||
new IntervalRange(2, 3, 1),
|
|
||||||
new IntervalRange(3, 3.5, 0),
|
|
||||||
new IntervalRange(3.5, 5, 1),
|
|
||||||
new IntervalRange(5, 7, 0),
|
|
||||||
]);
|
|
||||||
|
|
||||||
intervals.clear();
|
|
||||||
intervals.add(new IntervalRange(2, 5, 1));
|
|
||||||
intervals.add(new IntervalRange(3, 3.5, -1));
|
|
||||||
intervals.add(new IntervalRange(3, 3.5, 1));
|
|
||||||
assertEq(intervals.intervalRanges(), [
|
|
||||||
new IntervalRange(0, 2, 0),
|
|
||||||
new IntervalRange(2, 5, 1),
|
|
||||||
new IntervalRange(5, 7, 0),
|
|
||||||
]);
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"strict": true,
|
|
||||||
"target": "es6"
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
// pathfinder/demo2/util.ts
|
|
||||||
//
|
|
||||||
// Copyright © 2018 The Pathfinder Project Developers.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
|
||||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
|
||||||
// option. This file may not be copied, modified, or distributed
|
|
||||||
// except according to those terms.
|
|
||||||
|
|
||||||
export function panic(msg: string): never {
|
|
||||||
throw new Error(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function staticCast<T>(value: any, constructor: { new(...args: any[]): T }): T {
|
|
||||||
if (!(value instanceof constructor))
|
|
||||||
panic("Invalid dynamic cast");
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function unwrapNull<T>(value: T | null): T {
|
|
||||||
if (value == null)
|
|
||||||
throw new Error("Unexpected null");
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function unwrapUndef<T>(value: T | undefined): T {
|
|
||||||
if (value == null)
|
|
||||||
throw new Error("Unexpected undefined");
|
|
||||||
return value;
|
|
||||||
}
|
|
Loading…
Reference in New Issue