This commit is contained in:
Patrick Walton 2018-11-30 15:42:19 -08:00
parent 26bbf2c3d5
commit c1407c3970
3 changed files with 470 additions and 25 deletions

View File

@ -17,6 +17,7 @@ export class Point2D {
constructor(x: number, y: number) {
this.x = x;
this.y = y;
Object.freeze(this);
}
approxEq(other: Point2D): boolean {
@ -33,9 +34,55 @@ export interface Size2D {
height: number;
}
export interface Rect {
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 {
@ -63,3 +110,11 @@ export function approxEq(a: number, b: number): boolean {
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,
};
}

View File

@ -14,8 +14,8 @@ import STENCIL_VERTEX_SHADER_SOURCE from "./stencil.vs.glsl";
import STENCIL_FRAGMENT_SHADER_SOURCE from "./stencil.fs.glsl";
import SVG from "../resources/svg/Ghostscript_Tiger.svg";
import AREA_LUT from "../resources/textures/area-lut.png";
import {Matrix2D, Point2D, Rect, Size2D, Vector3D, approxEq, lerp} from "./geometry";
import {SVGPath, Tiler} from "./tiling";
import {Matrix2D, Point2D, Rect, Size2D, Vector3D, approxEq, cross, lerp} from "./geometry";
import {SVGPath, TILE_SIZE, TileDebugger, Tiler, testIntervals} from "./tiling";
import {staticCast, unwrapNull} from "./util";
const SVGPath: (path: string) => SVGPath = require('svgpath');
@ -23,7 +23,6 @@ const parseColor: (color: string) => any = require('parse-color');
const SVG_NS: string = "http://www.w3.org/2000/svg";
const TILE_SIZE: Size2D = {width: 32.0, height: 32.0};
const STENCIL_FRAMEBUFFER_SIZE: Size2D = {
width: TILE_SIZE.width * 256,
height: TILE_SIZE.height * 256,
@ -417,7 +416,9 @@ class Scene {
const pathElements = Array.from(document.getElementsByTagName('path'));
const tiles: Tile[] = [], pathColors = [];
for (let pathElementIndex = 0;
const tileDebugger = new TileDebugger(document);
for (let pathElementIndex = 0, realPathIndex = 0;
pathElementIndex < pathElements.length;
pathElementIndex++) {
const pathElement = pathElements[pathElementIndex];
@ -450,7 +451,15 @@ class Scene {
path = flattenPath(path);
path = canonicalizePath(path);
const tiler = new Tiler(path);
realPathIndex++;
//if (realPathIndex === 73) {
//console.log("path", pathElementIndex, "svg path", path);
const tiler = new Tiler(path);
tiler.tile();
tileDebugger.addTiler(tiler, paint);
console.log("path", pathElementIndex, "tiles", tiler.getTileStrips());
//}
const boundingRect = this.boundingRectOfPath(path);
@ -464,7 +473,7 @@ class Scene {
while (true) {
let x = boundingRect.origin.x - boundingRect.origin.x % TILE_SIZE.width;
while (true) {
const tileBounds = {origin: new Point2D(x, y), size: TILE_SIZE};
const tileBounds = new Rect(new Point2D(x, y), TILE_SIZE);
const tilePath = this.clipPathToRect(path, tileBounds);
if (tilePath.toString().length > 0) {
@ -518,6 +527,13 @@ class Scene {
document.body.removeChild(svgElement);
const svgContainer = document.createElement('div');
svgContainer.style.position = 'relative';
svgContainer.style.width = "2000px";
svgContainer.style.height = "2000px";
svgContainer.appendChild(tileDebugger.svg);
document.body.appendChild(svgContainer);
console.log(tiles);
this.tiles = tiles;
this.pathColors = pathColors;
@ -537,8 +553,8 @@ class Scene {
}
});
if (minX == null || minY == null || maxX == null || maxY == null)
return {origin: new Point2D(0, 0), size: {width: 0, height: 0}};
return {origin: new Point2D(minX, minY), size: {width: maxX - minX, height: maxY - minY}};
return new Rect(new Point2D(0, 0), {width: 0, height: 0});
return new Rect(new Point2D(minX, minY), {width: maxX - minX, height: maxY - minY});
}
private clipPathToRect(path: SVGPath, tileBounds: Rect): SVGPath {
@ -772,14 +788,6 @@ function canonicalizePath(path: SVGPath): SVGPath {
});
}
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,
};
}
function waitForQuery(gl: WebGL2RenderingContext, disjointTimerQueryExt: any, query: WebGLQuery):
void {
const queryResultAvailable = disjointTimerQueryExt.QUERY_RESULT_AVAILABLE_EXT;
@ -812,6 +820,8 @@ function sampleBezier(from: Point2D, ctrl: Point2D, to: Point2D, t: number): Poi
function main(): void {
window.fetch(SVG).then(svg => {
svg.text().then(svgText => {
testIntervals();
const svg = staticCast((new DOMParser).parseFromString(svgText, 'image/svg+xml'),
XMLDocument);
const image = new Image;

View File

@ -8,8 +8,10 @@
// option. This file may not be copied, modified, or distributed
// except according to those terms.
import {Point2D} from "./geometry";
import {panic, unwrapNull} from "./util";
import {Point2D, Rect, Size2D, cross} from "./geometry";
import {panic, staticCast, unwrapNull} from "./util";
export const TILE_SIZE: Size2D = {width: 32.0, height: 32.0};
export interface SVGPath {
abs(): SVGPath;
@ -29,16 +31,20 @@ interface EndpointIndex {
export class Tiler {
private path: SVGPath;
private endpoints: SubpathEndpoints[];
private sortedEndpointIndices: EndpointIndex[];
private sortedEdges: Edge[];
private boundingRect: Rect | null;
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()) {
@ -64,24 +70,158 @@ export class Tiler {
if (!currentSubpathEndpoints.isEmpty())
this.endpoints.push(currentSubpathEndpoints);
// Sort endpoints.
this.sortedEndpointIndices = [];
// 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.sortedEndpointIndices.push({subpathIndex, 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.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.tileStrips = [];
}
tile(): void {
const activeEdges = [];
for (const endpointIndex of this.sortedEndpointIndices) {
if (this.boundingRect == null)
return;
const activeIntervals = new Intervals(this.boundingRect.maxY());;
let activeEdges: Edge[] = [];
let nextEdgeIndex = 0;
this.tileStrips = [];
let tileTop = this.boundingRect.origin.y - this.boundingRect.origin.y % TILE_SIZE.height;
while (tileTop < this.boundingRect.maxY()) {
const tileBottom = tileTop + TILE_SIZE.height;
// Populate tile strip with active intervals.
const tileStrip = new TileStrip(tileTop);
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)
tileStrip.pushEdge(new Edge(startPoint, endPoint));
else
tileStrip.pushEdge(new Edge(endPoint, startPoint));
}
// Populate tile strip with active edges.
const oldEdges = activeEdges;
activeEdges = [];
for (const activeEdge of oldEdges)
this.processEdge(activeEdge, tileStrip, activeEdges, activeIntervals, tileTop);
while (nextEdgeIndex < this.sortedEdges.length) {
const edge = this.sortedEdges[nextEdgeIndex];
if (edge.from.y > tileBottom && edge.to.y > tileBottom)
break;
this.processEdge(edge, tileStrip, activeEdges, activeIntervals, tileTop);
nextEdgeIndex++;
}
this.tileStrips.push(tileStrip);
tileTop = tileBottom;
}
}
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 processEdge(edge: Edge,
tileStrip: TileStrip,
activeEdges: Edge[],
intervals: Intervals,
tileTop: number):
void {
const tileBottom = tileTop + TILE_SIZE.height;
const clipped = this.clipEdgeY(edge, tileBottom);
if (clipped.upper != null) {
tileStrip.pushEdge(clipped.upper);
if (edge.from.x <= edge.to.x)
intervals.add(new IntervalRange(edge.from.x, edge.to.x, 1));
else
intervals.add(new IntervalRange(edge.to.x, edge.from.x, -1));
}
if (clipped.lower != null)
activeEdges.push(clipped.lower);
}
private clipEdgeY(edge: Edge, y: number): ClippedEdgesY {
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};
const from = {x: edge.from.x, y: edge.from.y, z: 1.0};
const to = {x: edge.to.x, y: edge.to.y, z: 1.0};
const clipLine = {x: 0.0, y: 1.0, z: -y };
const intersectionHC = cross(cross(from, to), clipLine);
const intersection = new Point2D(intersectionHC.x / intersectionHC.z,
intersectionHC.y / intersectionHC.z);
const fromEdge = new Edge(edge.from, intersection);
const toEdge = new Edge(intersection, edge.to);
if (edge.from.y < y)
return {upper: fromEdge, lower: toEdge};
return {upper: toEdge, lower: fromEdge};
}
private prevEdgeFromEndpoint(endpointIndex: EndpointIndex): Edge {
const subpathEndpoints = this.endpoints[endpointIndex.subpathIndex];
return new Edge(subpathEndpoints.prevEndpointOf(endpointIndex.endpointIndex),
subpathEndpoints.endpoints[endpointIndex.endpointIndex]);
}
private nextEdgeFromEndpoint(endpointIndex: EndpointIndex): Edge {
const subpathEndpoints = this.endpoints[endpointIndex.subpathIndex];
return new Edge(subpathEndpoints.endpoints[endpointIndex.endpointIndex],
subpathEndpoints.nextEndpointOf(endpointIndex.endpointIndex));
}
}
class SubpathEndpoints {
@ -123,6 +263,7 @@ export class PathSegment {
for (let i = 1; i < segment.length; i += 2)
points.push(new Point2D(parseFloat(segment[i]), parseFloat(segment[i + 1])));
this.points = points;
//console.log("PathSegment, segment=", segment, "points=", points);
this.command = segment[0];
}
@ -130,3 +271,242 @@ export class PathSegment {
return this.points[this.points.length - 1];
}
}
class Edge {
from: Point2D;
to: Point2D;
constructor(from: Point2D, to: Point2D) {
this.from = from;
this.to = to;
Object.freeze(this);
}
}
class TileStrip {
edges: Edge[];
tileTop: number;
constructor(tileTop: number) {
this.edges = [];
this.tileTop = tileTop;
}
pushEdge(edge: Edge): void {
this.edges.push(edge);
}
tileBottom(): number {
return this.tileTop + TILE_SIZE.height;
}
}
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 {
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();
}
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 firstRange = this.ranges[i];
const secondRange = new IntervalRange(value, firstRange.end, firstRange.winding);
this.ranges.splice(i + 1, 0, secondRange);
firstRange.end = value;
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): 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());
for (const tileStrip of tiler.getTileStrips()) {
const tileBottom = tileStrip.tileBottom();
let path = "";
for (const edge of tileStrip.edges) {
path += "M " + edge.from.x + " " + edge.from.y + " ";
path += "L " + edge.to.x + " " + edge.to.y + " ";
path += "L " + edge.to.x + " " + tileBottom + " ";
path += "L " + edge.from.x + " " + tileBottom + " ";
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)");
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),
]);
}