Hokey implementation of path stroking
This commit is contained in:
parent
27ba918192
commit
2aba5fdcfc
|
@ -31,6 +31,27 @@ export class Point2D {
|
||||||
translate(x: number, y: number): Point2D {
|
translate(x: number, y: number): Point2D {
|
||||||
return new Point2D(this.x + x, this.y + y);
|
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 {
|
export interface Size2D {
|
||||||
|
|
|
@ -13,8 +13,26 @@ import {SVGPath} from "./tiling";
|
||||||
|
|
||||||
const SVGPath: (path: string) => SVGPath = require('svgpath');
|
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 {
|
export function flattenPath(path: SVGPath): SVGPath {
|
||||||
return path.abs().iterate(segment => {
|
return path.unshort().abs().iterate(segment => {
|
||||||
if (segment[0] === 'C') {
|
if (segment[0] === 'C') {
|
||||||
const ctrl0 = new Point2D(parseFloat(segment[segment.length - 6]),
|
const ctrl0 = new Point2D(parseFloat(segment[segment.length - 6]),
|
||||||
parseFloat(segment[segment.length - 5]));
|
parseFloat(segment[segment.length - 5]));
|
||||||
|
@ -35,7 +53,7 @@ export function flattenPath(path: SVGPath): SVGPath {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function canonicalizePath(path: SVGPath): SVGPath {
|
export function canonicalizePath(path: SVGPath): SVGPath {
|
||||||
return path.abs().iterate(segment => {
|
return path.unshort().abs().iterate(segment => {
|
||||||
if (segment[0] === 'H')
|
if (segment[0] === 'H')
|
||||||
return [['L', segment[1], '0']];
|
return [['L', segment[1], '0']];
|
||||||
if (segment[0] === 'V')
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ import STENCIL_FRAGMENT_SHADER_SOURCE from "./stencil.fs.glsl";
|
||||||
import SVG from "../resources/svg/Ghostscript_Tiger.svg";
|
import SVG from "../resources/svg/Ghostscript_Tiger.svg";
|
||||||
import AREA_LUT from "../resources/textures/area-lut.png";
|
import AREA_LUT from "../resources/textures/area-lut.png";
|
||||||
import {Matrix2D, Point2D, Rect, Size2D, Vector3D, approxEq, cross, lerp} from "./geometry";
|
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 {SVGPath, TILE_SIZE, TileDebugger, Tiler, testIntervals, TileStrip} from "./tiling";
|
||||||
import {staticCast, unwrapNull} from "./util";
|
import {staticCast, unwrapNull} from "./util";
|
||||||
|
|
||||||
|
@ -486,19 +486,13 @@ class Scene {
|
||||||
const style = window.getComputedStyle(pathElement);
|
const style = window.getComputedStyle(pathElement);
|
||||||
if (style.fill != null && style.fill !== 'none') {
|
if (style.fill != null && style.fill !== 'none') {
|
||||||
fillCount++;
|
fillCount++;
|
||||||
this.addPath(paths, pathColors, style.fill, pathString);
|
this.addPath(paths, pathColors, style.fill, pathString, null);
|
||||||
}
|
}
|
||||||
if (style.stroke != null && style.stroke !== 'none') {
|
if (style.stroke != null && style.stroke !== 'none') {
|
||||||
strokeCount++;
|
strokeCount++;
|
||||||
/*
|
|
||||||
const strokeWidth =
|
const strokeWidth =
|
||||||
style.strokeWidth == null ? 1.0 : parseFloat(style.strokeWidth);
|
style.strokeWidth == null ? 1.0 : parseFloat(style.strokeWidth);
|
||||||
console.log("stroking path:", pathString, strokeWidth);
|
this.addPath(paths, pathColors, style.stroke, pathString, strokeWidth);
|
||||||
try {
|
|
||||||
const strokedPathString = svgPathOutline(pathString, strokeWidth, {joints: 1});
|
|
||||||
this.addPath(paths, pathColors, style.stroke, strokedPathString);
|
|
||||||
} catch (e) {}
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log("", fillCount, "fills,", strokeCount, "strokes");
|
console.log("", fillCount, "fills,", strokeCount, "strokes");
|
||||||
|
@ -554,7 +548,12 @@ class Scene {
|
||||||
this.pathColors = pathColors;
|
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;
|
const color = parseColor(paint).rgba;
|
||||||
pathColors.push({
|
pathColors.push({
|
||||||
r: color[0],
|
r: color[0],
|
||||||
|
@ -563,7 +562,7 @@ class Scene {
|
||||||
a: Math.round(color[3] * 255.),
|
a: Math.round(color[3] * 255.),
|
||||||
});
|
});
|
||||||
|
|
||||||
let path = SVGPath(pathString);
|
let path: SVGPath = SVGPath(pathString);
|
||||||
path = path.matrix([
|
path = path.matrix([
|
||||||
GLOBAL_TRANSFORM.a, GLOBAL_TRANSFORM.b,
|
GLOBAL_TRANSFORM.a, GLOBAL_TRANSFORM.b,
|
||||||
GLOBAL_TRANSFORM.c, GLOBAL_TRANSFORM.d,
|
GLOBAL_TRANSFORM.c, GLOBAL_TRANSFORM.d,
|
||||||
|
@ -572,6 +571,16 @@ class Scene {
|
||||||
|
|
||||||
path = flattenPath(path);
|
path = flattenPath(path);
|
||||||
path = canonicalizePath(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);
|
paths.push(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
// except according to those terms.
|
// except according to those terms.
|
||||||
|
|
||||||
import {Point2D, Rect, Size2D, cross, lerp} from "./geometry";
|
import {Point2D, Rect, Size2D, cross, lerp} from "./geometry";
|
||||||
|
import {PathSegment} from "./path-utils";
|
||||||
import {panic, staticCast, unwrapNull} from "./util";
|
import {panic, staticCast, unwrapNull} from "./util";
|
||||||
|
|
||||||
export const TILE_SIZE: Size2D = {width: 16.0, height: 16.0};
|
export const TILE_SIZE: Size2D = {width: 16.0, height: 16.0};
|
||||||
|
@ -19,6 +20,7 @@ export interface SVGPath {
|
||||||
matrix(m: number[]): SVGPath;
|
matrix(m: number[]): SVGPath;
|
||||||
iterate(f: (segment: string[], index: number, x: number, y: number) => string[][] | void):
|
iterate(f: (segment: string[], index: number, x: number, y: number) => string[][] | void):
|
||||||
SVGPath;
|
SVGPath;
|
||||||
|
unshort(): SVGPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SVGPath: (path: string) => SVGPath = require('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 {
|
class Edge {
|
||||||
from: Point2D;
|
from: Point2D;
|
||||||
ctrl: Point2D | null;
|
ctrl: Point2D | null;
|
||||||
|
|
Loading…
Reference in New Issue