Files
rough/bin/path.js
2018-06-18 15:32:41 -07:00

428 lines
14 KiB
JavaScript

function isType(token, type) {
return token.type === type;
}
const PARAMS = {
A: 7,
a: 7,
C: 6,
c: 6,
H: 1,
h: 1,
L: 2,
l: 2,
M: 2,
m: 2,
Q: 4,
q: 4,
S: 4,
s: 4,
T: 4,
t: 2,
V: 1,
v: 1,
Z: 0,
z: 0
};
class ParsedPath {
constructor(d) {
this.COMMAND = 0;
this.NUMBER = 1;
this.EOD = 2;
this.segments = [];
this.parseData(d);
this.processPoints();
}
tokenize(d) {
const tokens = new Array();
while (d !== '') {
if (d.match(/^([ \t\r\n,]+)/)) {
d = d.substr(RegExp.$1.length);
}
else if (d.match(/^([aAcChHlLmMqQsStTvVzZ])/)) {
tokens[tokens.length] = { type: this.COMMAND, text: RegExp.$1 };
d = d.substr(RegExp.$1.length);
}
else if (d.match(/^(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)/)) {
tokens[tokens.length] = { type: this.NUMBER, text: `${parseFloat(RegExp.$1)}` };
d = d.substr(RegExp.$1.length);
}
else {
console.error('Unrecognized segment command: ' + d);
return [];
}
}
tokens[tokens.length] = { type: this.EOD, text: '' };
return tokens;
}
parseData(d) {
const tokens = this.tokenize(d);
let index = 0;
let token = tokens[index];
let mode = 'BOD';
this.segments = new Array();
while (!isType(token, this.EOD)) {
let param_length;
const params = new Array();
if (mode === 'BOD') {
if (token.text === 'M' || token.text === 'm') {
index++;
param_length = PARAMS[token.text];
mode = token.text;
}
else {
this.parseData('M0,0' + d);
return;
}
}
else {
if (isType(token, this.NUMBER)) {
param_length = PARAMS[mode];
}
else {
index++;
param_length = PARAMS[token.text];
mode = token.text;
}
}
if ((index + param_length) < tokens.length) {
for (let i = index; i < index + param_length; i++) {
const numbeToken = tokens[i];
if (isType(numbeToken, this.NUMBER)) {
params[params.length] = +numbeToken.text;
}
else {
console.error('Parameter type is not a number: ' + mode + ',' + numbeToken.text);
return;
}
}
if (typeof PARAMS[mode] === 'number') {
const segment = { key: mode, data: params };
this.segments.push(segment);
index += param_length;
token = tokens[index];
if (mode === 'M')
mode = 'L';
if (mode === 'm')
mode = 'l';
}
else {
console.error('Unsupported segment type: ' + mode);
return;
}
}
else {
console.error('Path data ended before all parameters were found');
}
}
}
get closed() {
if (typeof this._closed === 'undefined') {
this._closed = false;
for (const s of this.segments) {
if (s.key.toLowerCase() === 'z') {
this._closed = true;
}
}
}
return this._closed;
}
processPoints() {
let first = null;
let currentPoint = [0, 0];
for (let i = 0; i < this.segments.length; i++) {
const s = this.segments[i];
switch (s.key) {
case 'M':
case 'L':
case 'T':
s.point = [s.data[0], s.data[1]];
break;
case 'm':
case 'l':
case 't':
s.point = [s.data[0] + currentPoint[0], s.data[1] + currentPoint[1]];
break;
case 'H':
s.point = [s.data[0], currentPoint[1]];
break;
case 'h':
s.point = [s.data[0] + currentPoint[0], currentPoint[1]];
break;
case 'V':
s.point = [currentPoint[0], s.data[0]];
break;
case 'v':
s.point = [currentPoint[0], s.data[0] + currentPoint[1]];
break;
case 'z':
case 'Z':
if (first) {
s.point = [first[0], first[1]];
}
break;
case 'C':
s.point = [s.data[4], s.data[5]];
break;
case 'c':
s.point = [s.data[4] + currentPoint[0], s.data[5] + currentPoint[1]];
break;
case 'S':
s.point = [s.data[2], s.data[3]];
break;
case 's':
s.point = [s.data[2] + currentPoint[0], s.data[3] + currentPoint[1]];
break;
case 'Q':
s.point = [s.data[2], s.data[3]];
break;
case 'q':
s.point = [s.data[2] + currentPoint[0], s.data[3] + currentPoint[1]];
break;
case 'A':
s.point = [s.data[5], s.data[6]];
break;
case 'a':
s.point = [s.data[5] + currentPoint[0], s.data[6] + currentPoint[1]];
break;
}
if (s.key === 'm' || s.key === 'M') {
first = null;
}
if (s.point) {
currentPoint = s.point;
if (!first) {
first = s.point;
}
}
if (s.key === 'z' || s.key === 'Z') {
first = null;
}
}
}
}
export class RoughPath {
constructor(d) {
this._position = [0, 0];
this._first = null;
this.bezierReflectionPoint = null;
this.quadReflectionPoint = null;
this.parsed = new ParsedPath(d);
}
get segments() {
return this.parsed.segments;
}
get closed() {
return this.parsed.closed;
}
get linearPoints() {
if (!this._linearPoints) {
const lp = [];
let points = [];
for (const s of this.parsed.segments) {
const key = s.key.toLowerCase();
if (key === 'm' || key === 'z') {
if (points.length) {
lp.push(points);
points = [];
}
if (key === 'z') {
continue;
}
}
if (s.point) {
points.push(s.point);
}
}
if (points.length) {
lp.push(points);
points = [];
}
this._linearPoints = lp;
}
return this._linearPoints;
}
get first() {
return this._first;
}
set first(v) {
this._first = v;
}
setPosition(x, y) {
this._position = [x, y];
if (!this._first) {
this._first = [x, y];
}
}
get position() {
return this._position;
}
get x() {
return this._position[0];
}
get y() {
return this._position[1];
}
}
// Algorithm as described in https://www.w3.org/TR/SVG/implnote.html
// Code adapted from nsSVGPathDataParser.cpp in Mozilla
// https://hg.mozilla.org/mozilla-central/file/17156fbebbc8/content/svg/content/src/nsSVGPathDataParser.cpp#l887
export class RoughArcConverter {
constructor(from, to, radii, angle, largeArcFlag, sweepFlag) {
this._segIndex = 0;
this._numSegs = 0;
this._rx = 0;
this._ry = 0;
this._sinPhi = 0;
this._cosPhi = 0;
this._C = [0, 0];
this._theta = 0;
this._delta = 0;
this._T = 0;
this._from = from;
if (from[0] === to[0] && from[1] === to[1]) {
return;
}
const radPerDeg = Math.PI / 180;
this._rx = Math.abs(radii[0]);
this._ry = Math.abs(radii[1]);
this._sinPhi = Math.sin(angle * radPerDeg);
this._cosPhi = Math.cos(angle * radPerDeg);
const x1dash = this._cosPhi * (from[0] - to[0]) / 2.0 + this._sinPhi * (from[1] - to[1]) / 2.0;
const y1dash = -this._sinPhi * (from[0] - to[0]) / 2.0 + this._cosPhi * (from[1] - to[1]) / 2.0;
let root = 0;
const numerator = this._rx * this._rx * this._ry * this._ry - this._rx * this._rx * y1dash * y1dash - this._ry * this._ry * x1dash * x1dash;
if (numerator < 0) {
const s = Math.sqrt(1 - (numerator / (this._rx * this._rx * this._ry * this._ry)));
this._rx = this._rx * s;
this._ry = this._ry * s;
root = 0;
}
else {
root = (largeArcFlag === sweepFlag ? -1.0 : 1.0) *
Math.sqrt(numerator / (this._rx * this._rx * y1dash * y1dash + this._ry * this._ry * x1dash * x1dash));
}
const cxdash = root * this._rx * y1dash / this._ry;
const cydash = -root * this._ry * x1dash / this._rx;
this._C = [0, 0];
this._C[0] = this._cosPhi * cxdash - this._sinPhi * cydash + (from[0] + to[0]) / 2.0;
this._C[1] = this._sinPhi * cxdash + this._cosPhi * cydash + (from[1] + to[1]) / 2.0;
this._theta = this.calculateVectorAngle(1.0, 0.0, (x1dash - cxdash) / this._rx, (y1dash - cydash) / this._ry);
let dtheta = this.calculateVectorAngle((x1dash - cxdash) / this._rx, (y1dash - cydash) / this._ry, (-x1dash - cxdash) / this._rx, (-y1dash - cydash) / this._ry);
if ((!sweepFlag) && (dtheta > 0)) {
dtheta -= 2 * Math.PI;
}
else if (sweepFlag && (dtheta < 0)) {
dtheta += 2 * Math.PI;
}
this._numSegs = Math.ceil(Math.abs(dtheta / (Math.PI / 2)));
this._delta = dtheta / this._numSegs;
this._T = (8 / 3) * Math.sin(this._delta / 4) * Math.sin(this._delta / 4) / Math.sin(this._delta / 2);
}
getNextSegment() {
if (this._segIndex === this._numSegs) {
return null;
}
const cosTheta1 = Math.cos(this._theta);
const sinTheta1 = Math.sin(this._theta);
const theta2 = this._theta + this._delta;
const cosTheta2 = Math.cos(theta2);
const sinTheta2 = Math.sin(theta2);
const to = [
this._cosPhi * this._rx * cosTheta2 - this._sinPhi * this._ry * sinTheta2 + this._C[0],
this._sinPhi * this._rx * cosTheta2 + this._cosPhi * this._ry * sinTheta2 + this._C[1]
];
const cp1 = [
this._from[0] + this._T * (-this._cosPhi * this._rx * sinTheta1 - this._sinPhi * this._ry * cosTheta1),
this._from[1] + this._T * (-this._sinPhi * this._rx * sinTheta1 + this._cosPhi * this._ry * cosTheta1)
];
const cp2 = [
to[0] + this._T * (this._cosPhi * this._rx * sinTheta2 + this._sinPhi * this._ry * cosTheta2),
to[1] + this._T * (this._sinPhi * this._rx * sinTheta2 - this._cosPhi * this._ry * cosTheta2)
];
this._theta = theta2;
this._from = [to[0], to[1]];
this._segIndex++;
return {
cp1: cp1,
cp2: cp2,
to: to
};
}
calculateVectorAngle(ux, uy, vx, vy) {
const ta = Math.atan2(uy, ux);
const tb = Math.atan2(vy, vx);
if (tb >= ta)
return tb - ta;
return 2 * Math.PI - (ta - tb);
}
}
export class PathFitter {
constructor(sets, closed) {
this.sets = sets;
this.closed = closed;
}
fit(simplification) {
const outSets = [];
for (const set of this.sets) {
const length = set.length;
let estLength = Math.floor(simplification * length);
if (estLength < 5) {
if (length <= 5) {
continue;
}
estLength = 5;
}
outSets.push(this.reduce(set, estLength));
}
let d = '';
for (const set of outSets) {
for (let i = 0; i < set.length; i++) {
const point = set[i];
if (i === 0) {
d += 'M' + point[0] + ',' + point[1];
}
else {
d += 'L' + point[0] + ',' + point[1];
}
}
if (this.closed) {
d += 'z ';
}
}
return d;
}
distance(p1, p2) {
return Math.sqrt(Math.pow(p1[0] - p2[0], 2) + Math.pow(p1[1] - p2[1], 2));
}
reduce(set, count) {
if (set.length <= count) {
return set;
}
const points = set.slice(0);
while (points.length > count) {
const areas = [];
let minArea = -1;
let minIndex = -1;
for (let i = 1; i < (points.length - 1); i++) {
const a = this.distance(points[i - 1], points[i]);
const b = this.distance(points[i], points[i + 1]);
const c = this.distance(points[i - 1], points[i + 1]);
const s = (a + b + c) / 2.0;
const area = Math.sqrt(s * (s - a) * (s - b) * (s - c));
areas.push(area);
if ((minArea < 0) || (area < minArea)) {
minArea = area;
minIndex = i;
}
}
if (minIndex > 0) {
points.splice(minIndex, 1);
}
else {
break;
}
}
return points;
}
}