Approximation

This commit is contained in:
Patrick Walton 2018-12-04 13:35:50 -05:00
parent 2cf3be651c
commit f18af6d650
4 changed files with 139 additions and 12 deletions

View File

@ -8,8 +8,11 @@
// option. This file may not be copied, modified, or distributed
// except according to those terms.
import {Point2D} from "./geometry";
import {SVGPath} from "./tiling";
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');
@ -48,11 +51,16 @@ export function flattenPath(path: SVGPath): SVGPath {
return path.unshort().abs().iterate(segmentPieces => {
let segment = new PathSegment(segmentPieces);
if (segment.command === 'C' && lastPoint != null) {
const ctrl10 = segment.points[0].scale(3.0).sub(lastPoint).scale(0.5);
const ctrl11 = segment.points[1].scale(3.0).sub(segment.points[2]).scale(0.5);
const to = segment.points[2];
const ctrl = ctrl10.lerp(ctrl11, 0.5);
segment = new PathSegment(['Q', "" + ctrl.x, "" + ctrl.y, "" + to.x, "" + to.y]);
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 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]);
@ -63,6 +71,53 @@ export function flattenPath(path: SVGPath): SVGPath {
});
}
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[];
@ -92,7 +147,6 @@ export class Outline {
}
stroke(radius: number): void {
console.log("stroking, radius=", radius);
for (const suboutline of this.suboutlines)
suboutline.stroke(radius);
}
@ -180,3 +234,63 @@ export class OutlinePoint {
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;
}

View File

@ -17,7 +17,7 @@ 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, cross, lerp} from "./geometry";
import {flattenPath, Outline} from "./path-utils";
import {flattenPath, Outline, makePathMonotonic} from "./path-utils";
import {SVGPath, TILE_SIZE, TileDebugger, Tiler, testIntervals, TileStrip} from "./tiling";
import {staticCast, unwrapNull} from "./util";
@ -318,7 +318,7 @@ class App {
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
const framebufferSize = {width: canvas.width, height: canvas.height};
gl.viewport(0, 0, framebufferSize.width, framebufferSize.height);
gl.clearColor(1.0, 1.0, 1.0, 1.0);
gl.clearColor(0.85, 0.85, 0.85, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.bindVertexArray(this.opaqueVertexArray);
@ -573,7 +573,7 @@ class Scene {
if (strokeWidth != null) {
const outline = new Outline(path);
outline.calculateNormals();
outline.stroke(strokeWidth * GLOBAL_TRANSFORM.a * 5.0);
outline.stroke(strokeWidth * GLOBAL_TRANSFORM.a);
const strokedPathString = outline.toSVGPathString();
/*
@ -594,6 +594,7 @@ class Scene {
*/
path = SVGPath(strokedPathString);
path = makePathMonotonic(path);
}
paths.push(path);

View File

@ -361,7 +361,7 @@ class SubpathEndpoints {
}
}
class Edge {
export class Edge {
from: Point2D;
ctrl: Point2D | null;
to: Point2D;
@ -390,6 +390,12 @@ class Edge {
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 {

View File

@ -23,3 +23,9 @@ export function unwrapNull<T>(value: T | null): T {
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;
}