Hokey implementation of path stroking

This commit is contained in:
Patrick Walton 2018-12-03 17:16:44 -08:00
parent 27ba918192
commit 2aba5fdcfc
4 changed files with 179 additions and 31 deletions

View File

@ -31,6 +31,27 @@ export class Point2D {
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 interface Size2D {

View File

@ -13,8 +13,26 @@ import {SVGPath} from "./tiling";
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;
//console.log("PathSegment, segment=", segment, "points=", points);
this.command = segment[0];
}
to(): Point2D | null {
return this.points[this.points.length - 1];
}
}
export function flattenPath(path: SVGPath): SVGPath {
return path.abs().iterate(segment => {
return path.unshort().abs().iterate(segment => {
if (segment[0] === 'C') {
const ctrl0 = new Point2D(parseFloat(segment[segment.length - 6]),
parseFloat(segment[segment.length - 5]));
@ -35,7 +53,7 @@ export function flattenPath(path: SVGPath): SVGPath {
}
export function canonicalizePath(path: SVGPath): SVGPath {
return path.abs().iterate(segment => {
return path.unshort().abs().iterate(segment => {
if (segment[0] === 'H')
return [['L', segment[1], '0']];
if (segment[0] === 'V')
@ -44,3 +62,119 @@ export function canonicalizePath(path: SVGPath): SVGPath {
});
}
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;
}
}

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 {canonicalizePath, flattenPath} from "./path-utils";
import {canonicalizePath, flattenPath, Outline} from "./path-utils";
import {SVGPath, TILE_SIZE, TileDebugger, Tiler, testIntervals, TileStrip} from "./tiling";
import {staticCast, unwrapNull} from "./util";
@ -486,19 +486,13 @@ class Scene {
const style = window.getComputedStyle(pathElement);
if (style.fill != null && style.fill !== 'none') {
fillCount++;
this.addPath(paths, pathColors, style.fill, pathString);
this.addPath(paths, pathColors, style.fill, pathString, null);
}
if (style.stroke != null && style.stroke !== 'none') {
strokeCount++;
/*
const strokeWidth =
style.strokeWidth == null ? 1.0 : parseFloat(style.strokeWidth);
console.log("stroking path:", pathString, strokeWidth);
try {
const strokedPathString = svgPathOutline(pathString, strokeWidth, {joints: 1});
this.addPath(paths, pathColors, style.stroke, strokedPathString);
} catch (e) {}
*/
this.addPath(paths, pathColors, style.stroke, pathString, strokeWidth);
}
}
console.log("", fillCount, "fills,", strokeCount, "strokes");
@ -554,7 +548,12 @@ class Scene {
this.pathColors = pathColors;
}
private addPath(paths: SVGPath[], pathColors: any[], paint: string, pathString: string): void {
private addPath(paths: SVGPath[],
pathColors: any[],
paint: string,
pathString: string,
strokeWidth: number | null):
void {
const color = parseColor(paint).rgba;
pathColors.push({
r: color[0],
@ -563,7 +562,7 @@ class Scene {
a: Math.round(color[3] * 255.),
});
let path = SVGPath(pathString);
let path: SVGPath = SVGPath(pathString);
path = path.matrix([
GLOBAL_TRANSFORM.a, GLOBAL_TRANSFORM.b,
GLOBAL_TRANSFORM.c, GLOBAL_TRANSFORM.d,
@ -572,6 +571,16 @@ class Scene {
path = flattenPath(path);
path = canonicalizePath(path);
if (strokeWidth != null) {
const outline = new Outline(path);
outline.calculateNormals();
outline.stroke(strokeWidth * GLOBAL_TRANSFORM.a);
const strokedPathString = outline.toSVGPathString();
path = SVGPath(strokedPathString);
console.log(path.toString());
}
paths.push(path);
}
}

View File

@ -9,6 +9,7 @@
// 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};
@ -19,6 +20,7 @@ export interface 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');
@ -359,24 +361,6 @@ class SubpathEndpoints {
}
}
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;
//console.log("PathSegment, segment=", segment, "points=", points);
this.command = segment[0];
}
to(): Point2D | null {
return this.points[this.points.length - 1];
}
}
class Edge {
from: Point2D;
ctrl: Point2D | null;