mirror of
https://github.com/rough-stuff/rough.git
synced 2026-01-14 17:07:58 -05:00
428 lines
14 KiB
JavaScript
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;
|
|
}
|
|
}
|