Approximation
This commit is contained in:
parent
2cf3be651c
commit
f18af6d650
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue