diff --git a/demo2/geometry.ts b/demo2/geometry.ts index f3881876..d8c8fcc0 100644 --- a/demo2/geometry.ts +++ b/demo2/geometry.ts @@ -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 { diff --git a/demo2/path-utils.ts b/demo2/path-utils.ts index e05a436f..f4eb8f5e 100644 --- a/demo2/path-utils.ts +++ b/demo2/path-utils.ts @@ -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; + } +} diff --git a/demo2/pathfinder.ts b/demo2/pathfinder.ts index a1639cb8..0e0a7a9d 100644 --- a/demo2/pathfinder.ts +++ b/demo2/pathfinder.ts @@ -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); } } diff --git a/demo2/tiling.ts b/demo2/tiling.ts index 6456b6a2..3478387c 100644 --- a/demo2/tiling.ts +++ b/demo2/tiling.ts @@ -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;