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 {
|
||||
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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue