Files
rough/dist/rough.js
2018-06-26 11:20:34 -07:00

2370 lines
96 KiB
JavaScript

var rough = (function () {
'use strict';
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;
}
}
}
}
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
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);
}
}
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) {
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));
if ((minArea < 0) || (area < minArea)) {
minArea = area;
minIndex = i;
}
}
if (minIndex > 0) {
points.splice(minIndex, 1);
}
else {
break;
}
}
return points;
}
}
class Segment {
constructor(p1, p2) {
this.xi = Number.MAX_VALUE;
this.yi = Number.MAX_VALUE;
this.px1 = p1[0];
this.py1 = p1[1];
this.px2 = p2[0];
this.py2 = p2[1];
this.a = this.py2 - this.py1;
this.b = this.px1 - this.px2;
this.c = this.px2 * this.py1 - this.px1 * this.py2;
this._undefined = ((this.a === 0) && (this.b === 0) && (this.c === 0));
}
isUndefined() {
return this._undefined;
}
intersects(otherSegment) {
if (this.isUndefined() || otherSegment.isUndefined()) {
return false;
}
let grad1 = Number.MAX_VALUE;
let grad2 = Number.MAX_VALUE;
let int1 = 0, int2 = 0;
const a = this.a, b = this.b, c = this.c;
if (Math.abs(b) > 0.00001) {
grad1 = -a / b;
int1 = -c / b;
}
if (Math.abs(otherSegment.b) > 0.00001) {
grad2 = -otherSegment.a / otherSegment.b;
int2 = -otherSegment.c / otherSegment.b;
}
if (grad1 === Number.MAX_VALUE) {
if (grad2 === Number.MAX_VALUE) {
if ((-c / a) !== (-otherSegment.c / otherSegment.a)) {
return false;
}
if ((this.py1 >= Math.min(otherSegment.py1, otherSegment.py2)) && (this.py1 <= Math.max(otherSegment.py1, otherSegment.py2))) {
this.xi = this.px1;
this.yi = this.py1;
return true;
}
if ((this.py2 >= Math.min(otherSegment.py1, otherSegment.py2)) && (this.py2 <= Math.max(otherSegment.py1, otherSegment.py2))) {
this.xi = this.px2;
this.yi = this.py2;
return true;
}
return false;
}
this.xi = this.px1;
this.yi = (grad2 * this.xi + int2);
if (((this.py1 - this.yi) * (this.yi - this.py2) < -0.00001) || ((otherSegment.py1 - this.yi) * (this.yi - otherSegment.py2) < -0.00001)) {
return false;
}
if (Math.abs(otherSegment.a) < 0.00001) {
if ((otherSegment.px1 - this.xi) * (this.xi - otherSegment.px2) < -0.00001) {
return false;
}
return true;
}
return true;
}
if (grad2 === Number.MAX_VALUE) {
this.xi = otherSegment.px1;
this.yi = grad1 * this.xi + int1;
if (((otherSegment.py1 - this.yi) * (this.yi - otherSegment.py2) < -0.00001) || ((this.py1 - this.yi) * (this.yi - this.py2) < -0.00001)) {
return false;
}
if (Math.abs(a) < 0.00001) {
if ((this.px1 - this.xi) * (this.xi - this.px2) < -0.00001) {
return false;
}
return true;
}
return true;
}
if (grad1 === grad2) {
if (int1 !== int2) {
return false;
}
if ((this.px1 >= Math.min(otherSegment.px1, otherSegment.px2)) && (this.px1 <= Math.max(otherSegment.py1, otherSegment.py2))) {
this.xi = this.px1;
this.yi = this.py1;
return true;
}
if ((this.px2 >= Math.min(otherSegment.px1, otherSegment.px2)) && (this.px2 <= Math.max(otherSegment.px1, otherSegment.px2))) {
this.xi = this.px2;
this.yi = this.py2;
return true;
}
return false;
}
this.xi = ((int2 - int1) / (grad1 - grad2));
this.yi = (grad1 * this.xi + int1);
if (((this.px1 - this.xi) * (this.xi - this.px2) < -0.00001) || ((otherSegment.px1 - this.xi) * (this.xi - otherSegment.px2) < -0.00001)) {
return false;
}
return true;
}
}
class HachureIterator {
constructor(top, bottom, left, right, gap, sinAngle, cosAngle, tanAngle) {
this.deltaX = 0;
this.hGap = 0;
this.top = top;
this.bottom = bottom;
this.left = left;
this.right = right;
this.gap = gap;
this.sinAngle = sinAngle;
this.tanAngle = tanAngle;
if (Math.abs(sinAngle) < 0.0001) {
this.pos = left + gap;
}
else if (Math.abs(sinAngle) > 0.9999) {
this.pos = top + gap;
}
else {
this.deltaX = (bottom - top) * Math.abs(tanAngle);
this.pos = left - Math.abs(this.deltaX);
this.hGap = Math.abs(gap / cosAngle);
this.sLeft = new Segment([left, bottom], [left, top]);
this.sRight = new Segment([right, bottom], [right, top]);
}
}
nextLine() {
if (Math.abs(this.sinAngle) < 0.0001) {
if (this.pos < this.right) {
const line = [this.pos, this.top, this.pos, this.bottom];
this.pos += this.gap;
return line;
}
}
else if (Math.abs(this.sinAngle) > 0.9999) {
if (this.pos < this.bottom) {
const line = [this.left, this.pos, this.right, this.pos];
this.pos += this.gap;
return line;
}
}
else {
let xLower = this.pos - this.deltaX / 2;
let xUpper = this.pos + this.deltaX / 2;
let yLower = this.bottom;
let yUpper = this.top;
if (this.pos < (this.right + this.deltaX)) {
while (((xLower < this.left) && (xUpper < this.left)) || ((xLower > this.right) && (xUpper > this.right))) {
this.pos += this.hGap;
xLower = this.pos - this.deltaX / 2;
xUpper = this.pos + this.deltaX / 2;
if (this.pos > (this.right + this.deltaX)) {
return null;
}
}
const s = new Segment([xLower, yLower], [xUpper, yUpper]);
if (this.sLeft && s.intersects(this.sLeft)) {
xLower = s.xi;
yLower = s.yi;
}
if (this.sRight && s.intersects(this.sRight)) {
xUpper = s.xi;
yUpper = s.yi;
}
if (this.tanAngle > 0) {
xLower = this.right - (xLower - this.left);
xUpper = this.right - (xUpper - this.left);
}
const line = [xLower, yLower, xUpper, yUpper];
this.pos += this.hGap;
return line;
}
}
return null;
}
}
function lineLength(line) {
const p1 = line[0];
const p2 = line[1];
return Math.sqrt(Math.pow(p1[0] - p2[0], 2) + Math.pow(p1[1] - p2[1], 2));
}
function getIntersectingLines(line, points) {
const intersections = [];
const s1 = new Segment([line[0], line[1]], [line[2], line[3]]);
for (let i = 0; i < points.length; i++) {
const s2 = new Segment(points[i], points[(i + 1) % points.length]);
if (s1.intersects(s2)) {
intersections.push([s1.xi, s1.yi]);
}
}
return intersections;
}
function affine(x, y, cx, cy, sinAnglePrime, cosAnglePrime, R) {
const A = -cx * cosAnglePrime - cy * sinAnglePrime + cx;
const B = R * (cx * sinAnglePrime - cy * cosAnglePrime) + cy;
const C = cosAnglePrime;
const D = sinAnglePrime;
const E = -R * sinAnglePrime;
const F = R * cosAnglePrime;
return [
A + C * x + D * y,
B + E * x + F * y
];
}
function hachureLinesForPolygon(points, o) {
const ret = [];
if (points && points.length) {
let left = points[0][0];
let right = points[0][0];
let top = points[0][1];
let bottom = points[0][1];
for (let i = 1; i < points.length; i++) {
left = Math.min(left, points[i][0]);
right = Math.max(right, points[i][0]);
top = Math.min(top, points[i][1]);
bottom = Math.max(bottom, points[i][1]);
}
const angle = o.hachureAngle;
let gap = o.hachureGap;
if (gap < 0) {
gap = o.strokeWidth * 4;
}
gap = Math.max(gap, 0.1);
const radPerDeg = Math.PI / 180;
const hachureAngle = (angle % 180) * radPerDeg;
const cosAngle = Math.cos(hachureAngle);
const sinAngle = Math.sin(hachureAngle);
const tanAngle = Math.tan(hachureAngle);
const it = new HachureIterator(top - 1, bottom + 1, left - 1, right + 1, gap, sinAngle, cosAngle, tanAngle);
let rect;
while ((rect = it.nextLine()) != null) {
const lines = getIntersectingLines(rect, points);
for (let i = 0; i < lines.length; i++) {
if (i < (lines.length - 1)) {
const p1 = lines[i];
const p2 = lines[i + 1];
ret.push([p1, p2]);
}
}
}
}
return ret;
}
function hachureLinesForEllipse(cx, cy, width, height, o, renderer) {
const ret = [];
let rx = Math.abs(width / 2);
let ry = Math.abs(height / 2);
rx += renderer.getOffset(-rx * 0.05, rx * 0.05, o);
ry += renderer.getOffset(-ry * 0.05, ry * 0.05, o);
const angle = o.hachureAngle;
let gap = o.hachureGap;
if (gap <= 0) {
gap = o.strokeWidth * 4;
}
let fweight = o.fillWeight;
if (fweight < 0) {
fweight = o.strokeWidth / 2;
}
const radPerDeg = Math.PI / 180;
const hachureAngle = (angle % 180) * radPerDeg;
const tanAngle = Math.tan(hachureAngle);
const aspectRatio = ry / rx;
const hyp = Math.sqrt(aspectRatio * tanAngle * aspectRatio * tanAngle + 1);
const sinAnglePrime = aspectRatio * tanAngle / hyp;
const cosAnglePrime = 1 / hyp;
const gapPrime = gap / ((rx * ry / Math.sqrt((ry * cosAnglePrime) * (ry * cosAnglePrime) + (rx * sinAnglePrime) * (rx * sinAnglePrime))) / rx);
let halfLen = Math.sqrt((rx * rx) - (cx - rx + gapPrime) * (cx - rx + gapPrime));
for (let xPos = cx - rx + gapPrime; xPos < cx + rx; xPos += gapPrime) {
halfLen = Math.sqrt((rx * rx) - (cx - xPos) * (cx - xPos));
const p1 = affine(xPos, cy - halfLen, cx, cy, sinAnglePrime, cosAnglePrime, aspectRatio);
const p2 = affine(xPos, cy + halfLen, cx, cy, sinAnglePrime, cosAnglePrime, aspectRatio);
ret.push([p1, p2]);
}
return ret;
}
class HachureFiller {
constructor(renderer) {
this.renderer = renderer;
}
fillPolygon(points, o) {
return this._fillPolygon(points, o);
}
fillEllipse(cx, cy, width, height, o) {
return this._fillEllipse(cx, cy, width, height, o);
}
_fillPolygon(points, o, connectEnds = false) {
const lines = hachureLinesForPolygon(points, o);
const ops = this.renderLines(lines, o, connectEnds);
return { type: 'fillSketch', ops };
}
_fillEllipse(cx, cy, width, height, o, connectEnds = false) {
const lines = hachureLinesForEllipse(cx, cy, width, height, o, this.renderer);
const ops = this.renderLines(lines, o, connectEnds);
return { type: 'fillSketch', ops };
}
renderLines(lines, o, connectEnds) {
let ops = [];
let prevPoint = null;
for (const line of lines) {
ops = ops.concat(this.renderer.doubleLine(line[0][0], line[0][1], line[1][0], line[1][1], o));
if (connectEnds && prevPoint) {
ops = ops.concat(this.renderer.doubleLine(prevPoint[0], prevPoint[1], line[0][0], line[0][1], o));
}
prevPoint = line[1];
}
return ops;
}
}
class ZigZagFiller extends HachureFiller {
fillPolygon(points, o) {
return this._fillPolygon(points, o, true);
}
fillEllipse(cx, cy, width, height, o) {
return this._fillEllipse(cx, cy, width, height, o, true);
}
}
class HatchFiller extends HachureFiller {
fillPolygon(points, o) {
const set = this._fillPolygon(points, o);
const o2 = Object.assign({}, o, { hachureAngle: o.hachureAngle + 90 });
const set2 = this._fillPolygon(points, o2);
set.ops = set.ops.concat(set2.ops);
return set;
}
fillEllipse(cx, cy, width, height, o) {
const set = this._fillEllipse(cx, cy, width, height, o);
const o2 = Object.assign({}, o, { hachureAngle: o.hachureAngle + 90 });
const set2 = this._fillEllipse(cx, cy, width, height, o2);
set.ops = set.ops.concat(set2.ops);
return set;
}
}
class DotFiller {
constructor(renderer) {
this.renderer = renderer;
}
fillPolygon(points, o) {
o = Object.assign({}, o, { curveStepCount: 4, hachureAngle: 0 });
const lines = hachureLinesForPolygon(points, o);
return this.dotsOnLines(lines, o);
}
fillEllipse(cx, cy, width, height, o) {
o = Object.assign({}, o, { curveStepCount: 4, hachureAngle: 0 });
const lines = hachureLinesForEllipse(cx, cy, width, height, o, this.renderer);
return this.dotsOnLines(lines, o);
}
dotsOnLines(lines, o) {
let ops = [];
let gap = o.hachureGap;
if (gap < 0) {
gap = o.strokeWidth * 4;
}
gap = Math.max(gap, 0.1);
let fweight = o.fillWeight;
if (fweight < 0) {
fweight = o.strokeWidth / 2;
}
for (const line of lines) {
const length = lineLength(line);
const dl = length / gap;
const count = Math.ceil(dl) - 1;
const alpha = Math.atan((line[1][1] - line[0][1]) / (line[1][0] - line[0][0]));
for (let i = 0; i < count; i++) {
const l = gap * (i + 1);
const dy = l * Math.sin(alpha);
const dx = l * Math.cos(alpha);
const c = [line[0][0] - dx, line[0][1] + dy];
const cx = this.renderer.getOffset(c[0] - gap / 4, c[0] + gap / 4, o);
const cy = this.renderer.getOffset(c[1] - gap / 4, c[1] + gap / 4, o);
const ellipse = this.renderer.ellipse(cx, cy, fweight, fweight, o);
ops = ops.concat(ellipse.ops);
}
}
return { type: 'fillSketch', ops };
}
}
const fillers = {};
function getFiller(renderer, o) {
let fillerName = o.fillStyle || 'hachure';
if (!fillers[fillerName]) {
switch (fillerName) {
case 'zigzag':
if (!fillers[fillerName]) {
fillers[fillerName] = new ZigZagFiller(renderer);
}
break;
case 'cross-hatch':
if (!fillers[fillerName]) {
fillers[fillerName] = new HatchFiller(renderer);
}
break;
case 'dots':
if (!fillers[fillerName]) {
fillers[fillerName] = new DotFiller(renderer);
}
break;
case 'hachure':
default:
fillerName = 'hachure';
if (!fillers[fillerName]) {
fillers[fillerName] = new HachureFiller(renderer);
}
break;
}
}
return fillers[fillerName];
}
class RoughRenderer {
line(x1, y1, x2, y2, o) {
const ops = this.doubleLine(x1, y1, x2, y2, o);
return { type: 'path', ops };
}
linearPath(points, close, o) {
const len = (points || []).length;
if (len > 2) {
let ops = [];
for (let i = 0; i < (len - 1); i++) {
ops = ops.concat(this.doubleLine(points[i][0], points[i][1], points[i + 1][0], points[i + 1][1], o));
}
if (close) {
ops = ops.concat(this.doubleLine(points[len - 1][0], points[len - 1][1], points[0][0], points[0][1], o));
}
return { type: 'path', ops };
}
else if (len === 2) {
return this.line(points[0][0], points[0][1], points[1][0], points[1][1], o);
}
return { type: 'path', ops: [] };
}
polygon(points, o) {
return this.linearPath(points, true, o);
}
rectangle(x, y, width, height, o) {
const points = [
[x, y], [x + width, y], [x + width, y + height], [x, y + height]
];
return this.polygon(points, o);
}
curve(points, o) {
const o1 = this._curveWithOffset(points, 1 * (1 + o.roughness * 0.2), o);
const o2 = this._curveWithOffset(points, 1.5 * (1 + o.roughness * 0.22), o);
return { type: 'path', ops: o1.concat(o2) };
}
ellipse(x, y, width, height, o) {
const increment = (Math.PI * 2) / o.curveStepCount;
let rx = Math.abs(width / 2);
let ry = Math.abs(height / 2);
rx += this.getOffset(-rx * 0.05, rx * 0.05, o);
ry += this.getOffset(-ry * 0.05, ry * 0.05, o);
const o1 = this._ellipse(increment, x, y, rx, ry, 1, increment * this.getOffset(0.1, this.getOffset(0.4, 1, o), o), o);
const o2 = this._ellipse(increment, x, y, rx, ry, 1.5, 0, o);
return { type: 'path', ops: o1.concat(o2) };
}
arc(x, y, width, height, start, stop, closed, roughClosure, o) {
const cx = x;
const cy = y;
let rx = Math.abs(width / 2);
let ry = Math.abs(height / 2);
rx += this.getOffset(-rx * 0.01, rx * 0.01, o);
ry += this.getOffset(-ry * 0.01, ry * 0.01, o);
let strt = start;
let stp = stop;
while (strt < 0) {
strt += Math.PI * 2;
stp += Math.PI * 2;
}
if ((stp - strt) > (Math.PI * 2)) {
strt = 0;
stp = Math.PI * 2;
}
const ellipseInc = (Math.PI * 2) / o.curveStepCount;
const arcInc = Math.min(ellipseInc / 2, (stp - strt) / 2);
const o1 = this._arc(arcInc, cx, cy, rx, ry, strt, stp, 1, o);
const o2 = this._arc(arcInc, cx, cy, rx, ry, strt, stp, 1.5, o);
let ops = o1.concat(o2);
if (closed) {
if (roughClosure) {
ops = ops.concat(this.doubleLine(cx, cy, cx + rx * Math.cos(strt), cy + ry * Math.sin(strt), o));
ops = ops.concat(this.doubleLine(cx, cy, cx + rx * Math.cos(stp), cy + ry * Math.sin(stp), o));
}
else {
ops.push({ op: 'lineTo', data: [cx, cy] });
ops.push({ op: 'lineTo', data: [cx + rx * Math.cos(strt), cy + ry * Math.sin(strt)] });
}
}
return { type: 'path', ops };
}
svgPath(path, o) {
path = (path || '').replace(/\n/g, ' ').replace(/(-\s)/g, '-').replace('/(\s\s)/g', ' ');
let p = new RoughPath(path);
if (o.simplification) {
const fitter = new PathFitter(p.linearPoints, p.closed);
const d = fitter.fit(o.simplification);
p = new RoughPath(d);
}
let ops = [];
const segments = p.segments || [];
for (let i = 0; i < segments.length; i++) {
const s = segments[i];
const prev = i > 0 ? segments[i - 1] : null;
const opList = this._processSegment(p, s, prev, o);
if (opList && opList.length) {
ops = ops.concat(opList);
}
}
return { type: 'path', ops };
}
solidFillPolygon(points, o) {
const ops = [];
if (points.length) {
const offset = o.maxRandomnessOffset || 0;
const len = points.length;
if (len > 2) {
ops.push({ op: 'move', data: [points[0][0] + this.getOffset(-offset, offset, o), points[0][1] + this.getOffset(-offset, offset, o)] });
for (let i = 1; i < len; i++) {
ops.push({ op: 'lineTo', data: [points[i][0] + this.getOffset(-offset, offset, o), points[i][1] + this.getOffset(-offset, offset, o)] });
}
}
}
return { type: 'fillPath', ops };
}
patternFillPolygon(points, o) {
const filler = getFiller(this, o);
return filler.fillPolygon(points, o);
}
patternFillEllipse(cx, cy, width, height, o) {
const filler = getFiller(this, o);
return filler.fillEllipse(cx, cy, width, height, o);
}
patternFillArc(x, y, width, height, start, stop, o) {
const cx = x;
const cy = y;
let rx = Math.abs(width / 2);
let ry = Math.abs(height / 2);
rx += this.getOffset(-rx * 0.01, rx * 0.01, o);
ry += this.getOffset(-ry * 0.01, ry * 0.01, o);
let strt = start;
let stp = stop;
while (strt < 0) {
strt += Math.PI * 2;
stp += Math.PI * 2;
}
if ((stp - strt) > (Math.PI * 2)) {
strt = 0;
stp = Math.PI * 2;
}
const increment = (stp - strt) / o.curveStepCount;
const points = [];
for (let angle = strt; angle <= stp; angle = angle + increment) {
points.push([cx + rx * Math.cos(angle), cy + ry * Math.sin(angle)]);
}
points.push([cx + rx * Math.cos(stp), cy + ry * Math.sin(stp)]);
points.push([cx, cy]);
return this.patternFillPolygon(points, o);
}
///
getOffset(min, max, ops) {
return ops.roughness * ((Math.random() * (max - min)) + min);
}
doubleLine(x1, y1, x2, y2, o) {
const o1 = this._line(x1, y1, x2, y2, o, true, false);
const o2 = this._line(x1, y1, x2, y2, o, true, true);
return o1.concat(o2);
}
_line(x1, y1, x2, y2, o, move, overlay) {
const lengthSq = Math.pow((x1 - x2), 2) + Math.pow((y1 - y2), 2);
let offset = o.maxRandomnessOffset || 0;
if ((offset * offset * 100) > lengthSq) {
offset = Math.sqrt(lengthSq) / 10;
}
const halfOffset = offset / 2;
const divergePoint = 0.2 + Math.random() * 0.2;
let midDispX = o.bowing * o.maxRandomnessOffset * (y2 - y1) / 200;
let midDispY = o.bowing * o.maxRandomnessOffset * (x1 - x2) / 200;
midDispX = this.getOffset(-midDispX, midDispX, o);
midDispY = this.getOffset(-midDispY, midDispY, o);
const ops = [];
if (move) {
if (overlay) {
ops.push({
op: 'move', data: [
x1 + this.getOffset(-halfOffset, halfOffset, o),
y1 + this.getOffset(-halfOffset, halfOffset, o)
]
});
}
else {
ops.push({
op: 'move', data: [
x1 + this.getOffset(-offset, offset, o),
y1 + this.getOffset(-offset, offset, o)
]
});
}
}
if (overlay) {
ops.push({
op: 'bcurveTo', data: [
midDispX + x1 + (x2 - x1) * divergePoint + this.getOffset(-halfOffset, halfOffset, o),
midDispY + y1 + (y2 - y1) * divergePoint + this.getOffset(-halfOffset, halfOffset, o),
midDispX + x1 + 2 * (x2 - x1) * divergePoint + this.getOffset(-halfOffset, halfOffset, o),
midDispY + y1 + 2 * (y2 - y1) * divergePoint + this.getOffset(-halfOffset, halfOffset, o),
x2 + this.getOffset(-halfOffset, halfOffset, o),
y2 + this.getOffset(-halfOffset, halfOffset, o)
]
});
}
else {
ops.push({
op: 'bcurveTo', data: [
midDispX + x1 + (x2 - x1) * divergePoint + this.getOffset(-offset, offset, o),
midDispY + y1 + (y2 - y1) * divergePoint + this.getOffset(-offset, offset, o),
midDispX + x1 + 2 * (x2 - x1) * divergePoint + this.getOffset(-offset, offset, o),
midDispY + y1 + 2 * (y2 - y1) * divergePoint + this.getOffset(-offset, offset, o),
x2 + this.getOffset(-offset, offset, o),
y2 + this.getOffset(-offset, offset, o)
]
});
}
return ops;
}
_curve(points, closePoint, o) {
const len = points.length;
let ops = [];
if (len > 3) {
const b = [];
const s = 1 - o.curveTightness;
ops.push({ op: 'move', data: [points[1][0], points[1][1]] });
for (let i = 1; (i + 2) < len; i++) {
const cachedVertArray = points[i];
b[0] = [cachedVertArray[0], cachedVertArray[1]];
b[1] = [cachedVertArray[0] + (s * points[i + 1][0] - s * points[i - 1][0]) / 6, cachedVertArray[1] + (s * points[i + 1][1] - s * points[i - 1][1]) / 6];
b[2] = [points[i + 1][0] + (s * points[i][0] - s * points[i + 2][0]) / 6, points[i + 1][1] + (s * points[i][1] - s * points[i + 2][1]) / 6];
b[3] = [points[i + 1][0], points[i + 1][1]];
ops.push({ op: 'bcurveTo', data: [b[1][0], b[1][1], b[2][0], b[2][1], b[3][0], b[3][1]] });
}
if (closePoint && closePoint.length === 2) {
const ro = o.maxRandomnessOffset;
ops.push({ op: 'lineTo', data: [closePoint[0] + this.getOffset(-ro, ro, o), closePoint[1] + +this.getOffset(-ro, ro, o)] });
}
}
else if (len === 3) {
ops.push({ op: 'move', data: [points[1][0], points[1][1]] });
ops.push({
op: 'bcurveTo', data: [
points[1][0], points[1][1],
points[2][0], points[2][1],
points[2][0], points[2][1]
]
});
}
else if (len === 2) {
ops = ops.concat(this.doubleLine(points[0][0], points[0][1], points[1][0], points[1][1], o));
}
return ops;
}
_ellipse(increment, cx, cy, rx, ry, offset, overlap, o) {
const radOffset = this.getOffset(-0.5, 0.5, o) - (Math.PI / 2);
const points = [];
points.push([
this.getOffset(-offset, offset, o) + cx + 0.9 * rx * Math.cos(radOffset - increment),
this.getOffset(-offset, offset, o) + cy + 0.9 * ry * Math.sin(radOffset - increment)
]);
for (let angle = radOffset; angle < (Math.PI * 2 + radOffset - 0.01); angle = angle + increment) {
points.push([
this.getOffset(-offset, offset, o) + cx + rx * Math.cos(angle),
this.getOffset(-offset, offset, o) + cy + ry * Math.sin(angle)
]);
}
points.push([
this.getOffset(-offset, offset, o) + cx + rx * Math.cos(radOffset + Math.PI * 2 + overlap * 0.5),
this.getOffset(-offset, offset, o) + cy + ry * Math.sin(radOffset + Math.PI * 2 + overlap * 0.5)
]);
points.push([
this.getOffset(-offset, offset, o) + cx + 0.98 * rx * Math.cos(radOffset + overlap),
this.getOffset(-offset, offset, o) + cy + 0.98 * ry * Math.sin(radOffset + overlap)
]);
points.push([
this.getOffset(-offset, offset, o) + cx + 0.9 * rx * Math.cos(radOffset + overlap * 0.5),
this.getOffset(-offset, offset, o) + cy + 0.9 * ry * Math.sin(radOffset + overlap * 0.5)
]);
return this._curve(points, null, o);
}
_curveWithOffset(points, offset, o) {
const ps = [];
ps.push([
points[0][0] + this.getOffset(-offset, offset, o),
points[0][1] + this.getOffset(-offset, offset, o),
]);
ps.push([
points[0][0] + this.getOffset(-offset, offset, o),
points[0][1] + this.getOffset(-offset, offset, o),
]);
for (let i = 1; i < points.length; i++) {
ps.push([
points[i][0] + this.getOffset(-offset, offset, o),
points[i][1] + this.getOffset(-offset, offset, o),
]);
if (i === (points.length - 1)) {
ps.push([
points[i][0] + this.getOffset(-offset, offset, o),
points[i][1] + this.getOffset(-offset, offset, o),
]);
}
}
return this._curve(ps, null, o);
}
_arc(increment, cx, cy, rx, ry, strt, stp, offset, o) {
const radOffset = strt + this.getOffset(-0.1, 0.1, o);
const points = [];
points.push([
this.getOffset(-offset, offset, o) + cx + 0.9 * rx * Math.cos(radOffset - increment),
this.getOffset(-offset, offset, o) + cy + 0.9 * ry * Math.sin(radOffset - increment)
]);
for (let angle = radOffset; angle <= stp; angle = angle + increment) {
points.push([
this.getOffset(-offset, offset, o) + cx + rx * Math.cos(angle),
this.getOffset(-offset, offset, o) + cy + ry * Math.sin(angle)
]);
}
points.push([
cx + rx * Math.cos(stp),
cy + ry * Math.sin(stp)
]);
points.push([
cx + rx * Math.cos(stp),
cy + ry * Math.sin(stp)
]);
return this._curve(points, null, o);
}
_bezierTo(x1, y1, x2, y2, x, y, path, o) {
const ops = [];
const ros = [o.maxRandomnessOffset || 1, (o.maxRandomnessOffset || 1) + 0.5];
let f = [0, 0];
for (let i = 0; i < 2; i++) {
if (i === 0) {
ops.push({ op: 'move', data: [path.x, path.y] });
}
else {
ops.push({ op: 'move', data: [path.x + this.getOffset(-ros[0], ros[0], o), path.y + this.getOffset(-ros[0], ros[0], o)] });
}
f = [x + this.getOffset(-ros[i], ros[i], o), y + this.getOffset(-ros[i], ros[i], o)];
ops.push({
op: 'bcurveTo', data: [
x1 + this.getOffset(-ros[i], ros[i], o), y1 + this.getOffset(-ros[i], ros[i], o),
x2 + this.getOffset(-ros[i], ros[i], o), y2 + this.getOffset(-ros[i], ros[i], o),
f[0], f[1]
]
});
}
path.setPosition(f[0], f[1]);
return ops;
}
_processSegment(path, seg, prevSeg, o) {
let ops = [];
switch (seg.key) {
case 'M':
case 'm': {
const delta = seg.key === 'm';
if (seg.data.length >= 2) {
let x = +seg.data[0];
let y = +seg.data[1];
if (delta) {
x += path.x;
y += path.y;
}
const ro = 1 * (o.maxRandomnessOffset || 0);
x = x + this.getOffset(-ro, ro, o);
y = y + this.getOffset(-ro, ro, o);
path.setPosition(x, y);
ops.push({ op: 'move', data: [x, y] });
}
break;
}
case 'L':
case 'l': {
const delta = seg.key === 'l';
if (seg.data.length >= 2) {
let x = +seg.data[0];
let y = +seg.data[1];
if (delta) {
x += path.x;
y += path.y;
}
ops = ops.concat(this.doubleLine(path.x, path.y, x, y, o));
path.setPosition(x, y);
}
break;
}
case 'H':
case 'h': {
const delta = seg.key === 'h';
if (seg.data.length) {
let x = +seg.data[0];
if (delta) {
x += path.x;
}
ops = ops.concat(this.doubleLine(path.x, path.y, x, path.y, o));
path.setPosition(x, path.y);
}
break;
}
case 'V':
case 'v': {
const delta = seg.key === 'v';
if (seg.data.length) {
let y = +seg.data[0];
if (delta) {
y += path.y;
}
ops = ops.concat(this.doubleLine(path.x, path.y, path.x, y, o));
path.setPosition(path.x, y);
}
break;
}
case 'Z':
case 'z': {
if (path.first) {
ops = ops.concat(this.doubleLine(path.x, path.y, path.first[0], path.first[1], o));
path.setPosition(path.first[0], path.first[1]);
path.first = null;
}
break;
}
case 'C':
case 'c': {
const delta = seg.key === 'c';
if (seg.data.length >= 6) {
let x1 = +seg.data[0];
let y1 = +seg.data[1];
let x2 = +seg.data[2];
let y2 = +seg.data[3];
let x = +seg.data[4];
let y = +seg.data[5];
if (delta) {
x1 += path.x;
x2 += path.x;
x += path.x;
y1 += path.y;
y2 += path.y;
y += path.y;
}
const ob = this._bezierTo(x1, y1, x2, y2, x, y, path, o);
ops = ops.concat(ob);
path.bezierReflectionPoint = [x + (x - x2), y + (y - y2)];
}
break;
}
case 'S':
case 's': {
const delta = seg.key === 's';
if (seg.data.length >= 4) {
let x2 = +seg.data[0];
let y2 = +seg.data[1];
let x = +seg.data[2];
let y = +seg.data[3];
if (delta) {
x2 += path.x;
x += path.x;
y2 += path.y;
y += path.y;
}
let x1 = x2;
let y1 = y2;
const prevKey = prevSeg ? prevSeg.key : '';
let ref = null;
if (prevKey === 'c' || prevKey === 'C' || prevKey === 's' || prevKey === 'S') {
ref = path.bezierReflectionPoint;
}
if (ref) {
x1 = ref[0];
y1 = ref[1];
}
const ob = this._bezierTo(x1, y1, x2, y2, x, y, path, o);
ops = ops.concat(ob);
path.bezierReflectionPoint = [x + (x - x2), y + (y - y2)];
}
break;
}
case 'Q':
case 'q': {
const delta = seg.key === 'q';
if (seg.data.length >= 4) {
let x1 = +seg.data[0];
let y1 = +seg.data[1];
let x = +seg.data[2];
let y = +seg.data[3];
if (delta) {
x1 += path.x;
x += path.x;
y1 += path.y;
y += path.y;
}
const offset1 = 1 * (1 + o.roughness * 0.2);
const offset2 = 1.5 * (1 + o.roughness * 0.22);
ops.push({ op: 'move', data: [path.x + this.getOffset(-offset1, offset1, o), path.y + this.getOffset(-offset1, offset1, o)] });
let f = [x + this.getOffset(-offset1, offset1, o), y + this.getOffset(-offset1, offset1, o)];
ops.push({
op: 'qcurveTo', data: [
x1 + this.getOffset(-offset1, offset1, o), y1 + this.getOffset(-offset1, offset1, o),
f[0], f[1]
]
});
ops.push({ op: 'move', data: [path.x + this.getOffset(-offset2, offset2, o), path.y + this.getOffset(-offset2, offset2, o)] });
f = [x + this.getOffset(-offset2, offset2, o), y + this.getOffset(-offset2, offset2, o)];
ops.push({
op: 'qcurveTo', data: [
x1 + this.getOffset(-offset2, offset2, o), y1 + this.getOffset(-offset2, offset2, o),
f[0], f[1]
]
});
path.setPosition(f[0], f[1]);
path.quadReflectionPoint = [x + (x - x1), y + (y - y1)];
}
break;
}
case 'T':
case 't': {
const delta = seg.key === 't';
if (seg.data.length >= 2) {
let x = +seg.data[0];
let y = +seg.data[1];
if (delta) {
x += path.x;
y += path.y;
}
let x1 = x;
let y1 = y;
const prevKey = prevSeg ? prevSeg.key : '';
let ref = null;
if (prevKey === 'q' || prevKey === 'Q' || prevKey === 't' || prevKey === 'T') {
ref = path.quadReflectionPoint;
}
if (ref) {
x1 = ref[0];
y1 = ref[1];
}
const offset1 = 1 * (1 + o.roughness * 0.2);
const offset2 = 1.5 * (1 + o.roughness * 0.22);
ops.push({ op: 'move', data: [path.x + this.getOffset(-offset1, offset1, o), path.y + this.getOffset(-offset1, offset1, o)] });
let f = [x + this.getOffset(-offset1, offset1, o), y + this.getOffset(-offset1, offset1, o)];
ops.push({
op: 'qcurveTo', data: [
x1 + this.getOffset(-offset1, offset1, o), y1 + this.getOffset(-offset1, offset1, o),
f[0], f[1]
]
});
ops.push({ op: 'move', data: [path.x + this.getOffset(-offset2, offset2, o), path.y + this.getOffset(-offset2, offset2, o)] });
f = [x + this.getOffset(-offset2, offset2, o), y + this.getOffset(-offset2, offset2, o)];
ops.push({
op: 'qcurveTo', data: [
x1 + this.getOffset(-offset2, offset2, o), y1 + this.getOffset(-offset2, offset2, o),
f[0], f[1]
]
});
path.setPosition(f[0], f[1]);
path.quadReflectionPoint = [x + (x - x1), y + (y - y1)];
}
break;
}
case 'A':
case 'a': {
const delta = seg.key === 'a';
if (seg.data.length >= 7) {
const rx = +seg.data[0];
const ry = +seg.data[1];
const angle = +seg.data[2];
const largeArcFlag = +seg.data[3];
const sweepFlag = +seg.data[4];
let x = +seg.data[5];
let y = +seg.data[6];
if (delta) {
x += path.x;
y += path.y;
}
if (x === path.x && y === path.y) {
break;
}
if (rx === 0 || ry === 0) {
ops = ops.concat(this.doubleLine(path.x, path.y, x, y, o));
path.setPosition(x, y);
}
else {
for (let i = 0; i < 1; i++) {
const arcConverter = new RoughArcConverter([path.x, path.y], [x, y], [rx, ry], angle, largeArcFlag ? true : false, sweepFlag ? true : false);
let segment = arcConverter.getNextSegment();
while (segment) {
const ob = this._bezierTo(segment.cp1[0], segment.cp1[1], segment.cp2[0], segment.cp2[1], segment.to[0], segment.to[1], path, o);
ops = ops.concat(ob);
segment = arcConverter.getNextSegment();
}
}
}
}
break;
}
default:
break;
}
return ops;
}
}
const hasSelf = typeof self !== 'undefined';
const roughScript = hasSelf && self && self.document && self.document.currentScript && self.document.currentScript.src;
function createRenderer(config) {
if (hasSelf && roughScript && self && self.workly && config.async && (!config.noWorker)) {
const worklySource = config.worklyURL || 'https://cdn.jsdelivr.net/gh/pshihn/workly/dist/workly.min.js';
if (worklySource) {
const code = `importScripts('${worklySource}', '${roughScript}');\nworkly.expose(self.rough.createRenderer());`;
const ourl = URL.createObjectURL(new Blob([code]));
return self.workly.proxy(ourl);
}
}
return new RoughRenderer();
}
const hasSelf$1 = typeof self !== 'undefined';
class RoughGenerator {
constructor(config, surface) {
this.defaultOptions = {
maxRandomnessOffset: 2,
roughness: 1,
bowing: 1,
stroke: '#000',
strokeWidth: 1,
curveTightness: 0,
curveStepCount: 9,
fill: null,
fillStyle: 'hachure',
fillWeight: -1,
hachureAngle: -41,
hachureGap: -1
};
this.config = config || {};
this.surface = surface;
this.renderer = createRenderer(this.config);
if (this.config.options) {
this.defaultOptions = this._options(this.config.options);
}
}
_options(options) {
return options ? Object.assign({}, this.defaultOptions, options) : this.defaultOptions;
}
_drawable(shape, sets, options) {
return { shape, sets: sets || [], options: options || this.defaultOptions };
}
get lib() {
return this.renderer;
}
getCanvasSize() {
const val = (w) => {
if (w && typeof w === 'object') {
if (w.baseVal && w.baseVal.value) {
return w.baseVal.value;
}
}
return w || 100;
};
if (this.surface) {
return [val(this.surface.width), val(this.surface.height)];
}
return [100, 100];
}
computePolygonSize(points) {
if (points.length) {
let left = points[0][0];
let right = points[0][0];
let top = points[0][1];
let bottom = points[0][1];
for (let i = 1; i < points.length; i++) {
left = Math.min(left, points[i][0]);
right = Math.max(right, points[i][0]);
top = Math.min(top, points[i][1]);
bottom = Math.max(bottom, points[i][1]);
}
return [(right - left), (bottom - top)];
}
return [0, 0];
}
polygonPath(points) {
let d = '';
if (points.length) {
d = `M${points[0][0]},${points[0][1]}`;
for (let i = 1; i < points.length; i++) {
d = `${d} L${points[i][0]},${points[i][1]}`;
}
}
return d;
}
computePathSize(d) {
let size = [0, 0];
if (hasSelf$1 && self.document) {
try {
const ns = 'http://www.w3.org/2000/svg';
const svg = self.document.createElementNS(ns, 'svg');
svg.setAttribute('width', '0');
svg.setAttribute('height', '0');
const pathNode = self.document.createElementNS(ns, 'path');
pathNode.setAttribute('d', d);
svg.appendChild(pathNode);
self.document.body.appendChild(svg);
const bb = pathNode.getBBox();
if (bb) {
size[0] = bb.width || 0;
size[1] = bb.height || 0;
}
self.document.body.removeChild(svg);
}
catch (err) { }
}
const canvasSize = this.getCanvasSize();
if (!(size[0] * size[1])) {
size = canvasSize;
}
return size;
}
line(x1, y1, x2, y2, options) {
const o = this._options(options);
return this._drawable('line', [this.lib.line(x1, y1, x2, y2, o)], o);
}
rectangle(x, y, width, height, options) {
const o = this._options(options);
const paths = [];
if (o.fill) {
const points = [[x, y], [x + width, y], [x + width, y + height], [x, y + height]];
if (o.fillStyle === 'solid') {
paths.push(this.lib.solidFillPolygon(points, o));
}
else {
paths.push(this.lib.patternFillPolygon(points, o));
}
}
paths.push(this.lib.rectangle(x, y, width, height, o));
return this._drawable('rectangle', paths, o);
}
ellipse(x, y, width, height, options) {
const o = this._options(options);
const paths = [];
if (o.fill) {
if (o.fillStyle === 'solid') {
const shape = this.lib.ellipse(x, y, width, height, o);
shape.type = 'fillPath';
paths.push(shape);
}
else {
paths.push(this.lib.patternFillEllipse(x, y, width, height, o));
}
}
paths.push(this.lib.ellipse(x, y, width, height, o));
return this._drawable('ellipse', paths, o);
}
circle(x, y, diameter, options) {
const ret = this.ellipse(x, y, diameter, diameter, options);
ret.shape = 'circle';
return ret;
}
linearPath(points, options) {
const o = this._options(options);
return this._drawable('linearPath', [this.lib.linearPath(points, false, o)], o);
}
arc(x, y, width, height, start, stop, closed = false, options) {
const o = this._options(options);
const paths = [];
if (closed && o.fill) {
if (o.fillStyle === 'solid') {
const shape = this.lib.arc(x, y, width, height, start, stop, true, false, o);
shape.type = 'fillPath';
paths.push(shape);
}
else {
paths.push(this.lib.patternFillArc(x, y, width, height, start, stop, o));
}
}
paths.push(this.lib.arc(x, y, width, height, start, stop, closed, true, o));
return this._drawable('arc', paths, o);
}
curve(points, options) {
const o = this._options(options);
return this._drawable('curve', [this.lib.curve(points, o)], o);
}
polygon(points, options) {
const o = this._options(options);
const paths = [];
if (o.fill) {
if (o.fillStyle === 'solid') {
paths.push(this.lib.solidFillPolygon(points, o));
}
else {
const size = this.computePolygonSize(points);
const fillPoints = [
[0, 0],
[size[0], 0],
[size[0], size[1]],
[0, size[1]]
];
const shape = this.lib.patternFillPolygon(fillPoints, o);
shape.type = 'path2Dpattern';
shape.size = size;
shape.path = this.polygonPath(points);
paths.push(shape);
}
}
paths.push(this.lib.linearPath(points, true, o));
return this._drawable('polygon', paths, o);
}
path(d, options) {
const o = this._options(options);
const paths = [];
if (!d) {
return this._drawable('path', paths, o);
}
if (o.fill) {
if (o.fillStyle === 'solid') {
const shape = { type: 'path2Dfill', path: d, ops: [] };
paths.push(shape);
}
else {
const size = this.computePathSize(d);
const points = [
[0, 0],
[size[0], 0],
[size[0], size[1]],
[0, size[1]]
];
const shape = this.lib.patternFillPolygon(points, o);
shape.type = 'path2Dpattern';
shape.size = size;
shape.path = d;
paths.push(shape);
}
}
paths.push(this.lib.svgPath(d, o));
return this._drawable('path', paths, o);
}
toPaths(drawable) {
const sets = drawable.sets || [];
const o = drawable.options || this.defaultOptions;
const paths = [];
for (const drawing of sets) {
let path = null;
switch (drawing.type) {
case 'path':
path = {
d: this.opsToPath(drawing),
stroke: o.stroke,
strokeWidth: o.strokeWidth,
fill: 'none'
};
break;
case 'fillPath':
path = {
d: this.opsToPath(drawing),
stroke: 'none',
strokeWidth: 0,
fill: o.fill || 'none'
};
break;
case 'fillSketch':
path = this.fillSketch(drawing, o);
break;
case 'path2Dfill':
path = {
d: drawing.path || '',
stroke: 'none',
strokeWidth: 0,
fill: o.fill || 'none'
};
break;
case 'path2Dpattern': {
const size = drawing.size;
const pattern = {
x: 0, y: 0, width: 1, height: 1,
viewBox: `0 0 ${Math.round(size[0])} ${Math.round(size[1])}`,
patternUnits: 'objectBoundingBox',
path: this.fillSketch(drawing, o)
};
path = {
d: drawing.path,
stroke: 'none',
strokeWidth: 0,
pattern: pattern
};
break;
}
}
if (path) {
paths.push(path);
}
}
return paths;
}
fillSketch(drawing, o) {
let fweight = o.fillWeight;
if (fweight < 0) {
fweight = o.strokeWidth / 2;
}
return {
d: this.opsToPath(drawing),
stroke: o.fill || 'none',
strokeWidth: fweight,
fill: 'none'
};
}
opsToPath(drawing) {
let path = '';
for (const item of drawing.ops) {
const data = item.data;
switch (item.op) {
case 'move':
path += `M${data[0]} ${data[1]} `;
break;
case 'bcurveTo':
path += `C${data[0]} ${data[1]}, ${data[2]} ${data[3]}, ${data[4]} ${data[5]} `;
break;
case 'qcurveTo':
path += `Q${data[0]} ${data[1]}, ${data[2]} ${data[3]} `;
break;
case 'lineTo':
path += `L${data[0]} ${data[1]} `;
break;
}
}
return path.trim();
}
}
const hasDocument = typeof document !== 'undefined';
class RoughCanvas {
constructor(canvas, config) {
this.canvas = canvas;
this.ctx = this.canvas.getContext('2d');
this.gen = new RoughGenerator(config || null, this.canvas);
}
get generator() {
return this.gen;
}
static createRenderer() {
return new RoughRenderer();
}
line(x1, y1, x2, y2, options) {
const d = this.gen.line(x1, y1, x2, y2, options);
this.draw(d);
return d;
}
rectangle(x, y, width, height, options) {
const d = this.gen.rectangle(x, y, width, height, options);
this.draw(d);
return d;
}
ellipse(x, y, width, height, options) {
const d = this.gen.ellipse(x, y, width, height, options);
this.draw(d);
return d;
}
circle(x, y, diameter, options) {
const d = this.gen.circle(x, y, diameter, options);
this.draw(d);
return d;
}
linearPath(points, options) {
const d = this.gen.linearPath(points, options);
this.draw(d);
return d;
}
polygon(points, options) {
const d = this.gen.polygon(points, options);
this.draw(d);
return d;
}
arc(x, y, width, height, start, stop, closed = false, options) {
const d = this.gen.arc(x, y, width, height, start, stop, closed, options);
this.draw(d);
return d;
}
curve(points, options) {
const d = this.gen.curve(points, options);
this.draw(d);
return d;
}
path(d, options) {
const drawing = this.gen.path(d, options);
this.draw(drawing);
return drawing;
}
draw(drawable) {
const sets = drawable.sets || [];
const o = drawable.options || this.gen.defaultOptions;
const ctx = this.ctx;
for (const drawing of sets) {
switch (drawing.type) {
case 'path':
ctx.save();
ctx.strokeStyle = o.stroke;
ctx.lineWidth = o.strokeWidth;
this._drawToContext(ctx, drawing);
ctx.restore();
break;
case 'fillPath':
ctx.save();
ctx.fillStyle = o.fill || '';
this._drawToContext(ctx, drawing);
ctx.restore();
break;
case 'fillSketch':
this.fillSketch(ctx, drawing, o);
break;
case 'path2Dfill': {
this.ctx.save();
this.ctx.fillStyle = o.fill || '';
const p2d = new Path2D(drawing.path);
this.ctx.fill(p2d);
this.ctx.restore();
break;
}
case 'path2Dpattern': {
const doc = this.canvas.ownerDocument || (hasDocument && document);
if (doc) {
const size = drawing.size;
const hcanvas = doc.createElement('canvas');
const hcontext = hcanvas.getContext('2d');
const bbox = this.computeBBox(drawing.path);
if (bbox && (bbox.width || bbox.height)) {
hcanvas.width = this.canvas.width;
hcanvas.height = this.canvas.height;
hcontext.translate(bbox.x || 0, bbox.y || 0);
}
else {
hcanvas.width = size[0];
hcanvas.height = size[1];
}
this.fillSketch(hcontext, drawing, o);
this.ctx.save();
this.ctx.fillStyle = this.ctx.createPattern(hcanvas, 'repeat');
const p2d = new Path2D(drawing.path);
this.ctx.fill(p2d);
this.ctx.restore();
}
else {
console.error('Cannot render path2Dpattern. No defs/document defined.');
}
break;
}
}
}
}
computeBBox(d) {
if (hasDocument) {
try {
const ns = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(ns, 'svg');
svg.setAttribute('width', '0');
svg.setAttribute('height', '0');
const pathNode = self.document.createElementNS(ns, 'path');
pathNode.setAttribute('d', d);
svg.appendChild(pathNode);
document.body.appendChild(svg);
const bbox = pathNode.getBBox();
document.body.removeChild(svg);
return bbox;
}
catch (err) { }
}
return null;
}
fillSketch(ctx, drawing, o) {
let fweight = o.fillWeight;
if (fweight < 0) {
fweight = o.strokeWidth / 2;
}
ctx.save();
ctx.strokeStyle = o.fill || '';
ctx.lineWidth = fweight;
this._drawToContext(ctx, drawing);
ctx.restore();
}
_drawToContext(ctx, drawing) {
ctx.beginPath();
for (const item of drawing.ops) {
const data = item.data;
switch (item.op) {
case 'move':
ctx.moveTo(data[0], data[1]);
break;
case 'bcurveTo':
ctx.bezierCurveTo(data[0], data[1], data[2], data[3], data[4], data[5]);
break;
case 'qcurveTo':
ctx.quadraticCurveTo(data[0], data[1], data[2], data[3]);
break;
case 'lineTo':
ctx.lineTo(data[0], data[1]);
break;
}
}
if (drawing.type === 'fillPath') {
ctx.fill();
}
else {
ctx.stroke();
}
}
}
class RoughGeneratorAsync extends RoughGenerator {
// @ts-ignore
async line(x1, y1, x2, y2, options) {
const o = this._options(options);
return this._drawable('line', [await this.lib.line(x1, y1, x2, y2, o)], o);
}
// @ts-ignore
async rectangle(x, y, width, height, options) {
const o = this._options(options);
const paths = [];
if (o.fill) {
const points = [[x, y], [x + width, y], [x + width, y + height], [x, y + height]];
if (o.fillStyle === 'solid') {
paths.push(await this.lib.solidFillPolygon(points, o));
}
else {
paths.push(await this.lib.patternFillPolygon(points, o));
}
}
paths.push(await this.lib.rectangle(x, y, width, height, o));
return this._drawable('rectangle', paths, o);
}
// @ts-ignore
async ellipse(x, y, width, height, options) {
const o = this._options(options);
const paths = [];
if (o.fill) {
if (o.fillStyle === 'solid') {
const shape = await this.lib.ellipse(x, y, width, height, o);
shape.type = 'fillPath';
paths.push(shape);
}
else {
paths.push(await this.lib.patternFillEllipse(x, y, width, height, o));
}
}
paths.push(await this.lib.ellipse(x, y, width, height, o));
return this._drawable('ellipse', paths, o);
}
// @ts-ignore
async circle(x, y, diameter, options) {
const ret = await this.ellipse(x, y, diameter, diameter, options);
ret.shape = 'circle';
return ret;
}
// @ts-ignore
async linearPath(points, options) {
const o = this._options(options);
return this._drawable('linearPath', [await this.lib.linearPath(points, false, o)], o);
}
// @ts-ignore
async arc(x, y, width, height, start, stop, closed = false, options) {
const o = this._options(options);
const paths = [];
if (closed && o.fill) {
if (o.fillStyle === 'solid') {
const shape = await this.lib.arc(x, y, width, height, start, stop, true, false, o);
shape.type = 'fillPath';
paths.push(shape);
}
else {
paths.push(await this.lib.patternFillArc(x, y, width, height, start, stop, o));
}
}
paths.push(await this.lib.arc(x, y, width, height, start, stop, closed, true, o));
return this._drawable('arc', paths, o);
}
// @ts-ignore
async curve(points, options) {
const o = this._options(options);
return this._drawable('curve', [await this.lib.curve(points, o)], o);
}
// @ts-ignore
async polygon(points, options) {
const o = this._options(options);
const paths = [];
if (o.fill) {
if (o.fillStyle === 'solid') {
paths.push(await this.lib.solidFillPolygon(points, o));
}
else {
const size = this.computePolygonSize(points);
const fillPoints = [
[0, 0],
[size[0], 0],
[size[0], size[1]],
[0, size[1]]
];
const shape = await this.lib.patternFillPolygon(fillPoints, o);
shape.type = 'path2Dpattern';
shape.size = size;
shape.path = this.polygonPath(points);
paths.push(shape);
}
}
paths.push(await this.lib.linearPath(points, true, o));
return this._drawable('polygon', paths, o);
}
// @ts-ignore
async path(d, options) {
const o = this._options(options);
const paths = [];
if (!d) {
return this._drawable('path', paths, o);
}
if (o.fill) {
if (o.fillStyle === 'solid') {
const shape = { type: 'path2Dfill', path: d, ops: [] };
paths.push(shape);
}
else {
const size = this.computePathSize(d);
const points = [
[0, 0],
[size[0], 0],
[size[0], size[1]],
[0, size[1]]
];
const shape = await this.lib.patternFillPolygon(points, o);
shape.type = 'path2Dpattern';
shape.size = size;
shape.path = d;
paths.push(shape);
}
}
paths.push(await this.lib.svgPath(d, o));
return this._drawable('path', paths, o);
}
}
class RoughCanvasAsync extends RoughCanvas {
constructor(canvas, config) {
super(canvas, config);
this.genAsync = new RoughGeneratorAsync(config || null, this.canvas);
}
// @ts-ignore
get generator() {
return this.genAsync;
}
// @ts-ignore
async line(x1, y1, x2, y2, options) {
const d = await this.genAsync.line(x1, y1, x2, y2, options);
this.draw(d);
return d;
}
// @ts-ignore
async rectangle(x, y, width, height, options) {
const d = await this.genAsync.rectangle(x, y, width, height, options);
this.draw(d);
return d;
}
// @ts-ignore
async ellipse(x, y, width, height, options) {
const d = await this.genAsync.ellipse(x, y, width, height, options);
this.draw(d);
return d;
}
// @ts-ignore
async circle(x, y, diameter, options) {
const d = await this.genAsync.circle(x, y, diameter, options);
this.draw(d);
return d;
}
// @ts-ignore
async linearPath(points, options) {
const d = await this.genAsync.linearPath(points, options);
this.draw(d);
return d;
}
// @ts-ignore
async polygon(points, options) {
const d = await this.genAsync.polygon(points, options);
this.draw(d);
return d;
}
// @ts-ignore
async arc(x, y, width, height, start, stop, closed = false, options) {
const d = await this.genAsync.arc(x, y, width, height, start, stop, closed, options);
this.draw(d);
return d;
}
// @ts-ignore
async curve(points, options) {
const d = await this.genAsync.curve(points, options);
this.draw(d);
return d;
}
// @ts-ignore
async path(d, options) {
const drawing = await this.genAsync.path(d, options);
this.draw(drawing);
return drawing;
}
}
const hasDocument$1 = typeof document !== 'undefined';
class RoughSVG {
constructor(svg, config) {
this.svg = svg;
this.gen = new RoughGenerator(config || null, this.svg);
}
get generator() {
return this.gen;
}
static createRenderer() {
return new RoughRenderer();
}
get defs() {
const doc = this.svg.ownerDocument || (hasDocument$1 && document);
if (doc) {
if (!this._defs) {
const dnode = doc.createElementNS('http://www.w3.org/2000/svg', 'defs');
if (this.svg.firstChild) {
this.svg.insertBefore(dnode, this.svg.firstChild);
}
else {
this.svg.appendChild(dnode);
}
this._defs = dnode;
}
}
return this._defs || null;
}
line(x1, y1, x2, y2, options) {
const d = this.gen.line(x1, y1, x2, y2, options);
return this.draw(d);
}
rectangle(x, y, width, height, options) {
const d = this.gen.rectangle(x, y, width, height, options);
return this.draw(d);
}
ellipse(x, y, width, height, options) {
const d = this.gen.ellipse(x, y, width, height, options);
return this.draw(d);
}
circle(x, y, diameter, options) {
const d = this.gen.circle(x, y, diameter, options);
return this.draw(d);
}
linearPath(points, options) {
const d = this.gen.linearPath(points, options);
return this.draw(d);
}
polygon(points, options) {
const d = this.gen.polygon(points, options);
return this.draw(d);
}
arc(x, y, width, height, start, stop, closed = false, options) {
const d = this.gen.arc(x, y, width, height, start, stop, closed, options);
return this.draw(d);
}
curve(points, options) {
const d = this.gen.curve(points, options);
return this.draw(d);
}
path(d, options) {
const drawing = this.gen.path(d, options);
return this.draw(drawing);
}
draw(drawable) {
const sets = drawable.sets || [];
const o = drawable.options || this.gen.defaultOptions;
const doc = this.svg.ownerDocument || (hasDocument$1 && document);
const g = doc.createElementNS('http://www.w3.org/2000/svg', 'g');
for (const drawing of sets) {
let path = null;
switch (drawing.type) {
case 'path': {
path = doc.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', this.opsToPath(drawing));
path.style.stroke = o.stroke;
path.style.strokeWidth = o.strokeWidth + '';
path.style.fill = 'none';
break;
}
case 'fillPath': {
path = doc.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', this.opsToPath(drawing));
path.style.stroke = 'none';
path.style.strokeWidth = '0';
path.style.fill = o.fill;
break;
}
case 'fillSketch': {
path = this.fillSketch(doc, drawing, o);
break;
}
case 'path2Dfill': {
path = doc.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', drawing.path || '');
path.style.stroke = 'none';
path.style.strokeWidth = '0';
path.style.fill = o.fill;
break;
}
case 'path2Dpattern': {
if (!this.defs) {
console.error('Cannot render path2Dpattern. No defs/document defined.');
}
else {
const size = drawing.size;
const pattern = doc.createElementNS('http://www.w3.org/2000/svg', 'pattern');
const id = `rough-${Math.floor(Math.random() * (Number.MAX_SAFE_INTEGER || 999999))}`;
pattern.setAttribute('id', id);
pattern.setAttribute('x', '0');
pattern.setAttribute('y', '0');
pattern.setAttribute('width', '1');
pattern.setAttribute('height', '1');
pattern.setAttribute('height', '1');
pattern.setAttribute('viewBox', `0 0 ${Math.round(size[0])} ${Math.round(size[1])}`);
pattern.setAttribute('patternUnits', 'objectBoundingBox');
const patternPath = this.fillSketch(doc, drawing, o);
pattern.appendChild(patternPath);
this.defs.appendChild(pattern);
path = doc.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', drawing.path || '');
path.style.stroke = 'none';
path.style.strokeWidth = '0';
path.style.fill = `url(#${id})`;
}
break;
}
}
if (path) {
g.appendChild(path);
}
}
return g;
}
opsToPath(drawing) {
return this.gen.opsToPath(drawing);
}
fillSketch(doc, drawing, o) {
let fweight = o.fillWeight;
if (fweight < 0) {
fweight = o.strokeWidth / 2;
}
const path = doc.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', this.opsToPath(drawing));
path.style.stroke = o.fill;
path.style.strokeWidth = fweight + '';
path.style.fill = 'none';
return path;
}
}
class RoughSVGAsync extends RoughSVG {
constructor(svg, config) {
super(svg, config);
this.genAsync = new RoughGeneratorAsync(config || null, this.svg);
}
// @ts-ignore
get generator() {
return this.genAsync;
}
// @ts-ignore
async line(x1, y1, x2, y2, options) {
const d = await this.genAsync.line(x1, y1, x2, y2, options);
return this.draw(d);
}
// @ts-ignore
async rectangle(x, y, width, height, options) {
const d = await this.genAsync.rectangle(x, y, width, height, options);
return this.draw(d);
}
// @ts-ignore
async ellipse(x, y, width, height, options) {
const d = await this.genAsync.ellipse(x, y, width, height, options);
return this.draw(d);
}
// @ts-ignore
async circle(x, y, diameter, options) {
const d = await this.genAsync.circle(x, y, diameter, options);
return this.draw(d);
}
// @ts-ignore
async linearPath(points, options) {
const d = await this.genAsync.linearPath(points, options);
return this.draw(d);
}
// @ts-ignore
async polygon(points, options) {
const d = await this.genAsync.polygon(points, options);
return this.draw(d);
}
// @ts-ignore
async arc(x, y, width, height, start, stop, closed = false, options) {
const d = await this.genAsync.arc(x, y, width, height, start, stop, closed, options);
return this.draw(d);
}
// @ts-ignore
async curve(points, options) {
const d = await this.genAsync.curve(points, options);
return this.draw(d);
}
// @ts-ignore
async path(d, options) {
const drawing = await this.genAsync.path(d, options);
return this.draw(drawing);
}
}
var rough = {
canvas(canvas, config) {
if (config && config.async) {
return new RoughCanvasAsync(canvas, config);
}
return new RoughCanvas(canvas, config);
},
svg(svg, config) {
if (config && config.async) {
return new RoughSVGAsync(svg, config);
}
return new RoughSVG(svg, config);
},
createRenderer() {
return RoughCanvas.createRenderer();
},
generator(config, surface) {
if (config && config.async) {
return new RoughGeneratorAsync(config, surface);
}
return new RoughGenerator(config, surface);
}
};
return rough;
}());