UNPKG

roughjs-es5

Version:

Create graphics using HTML Canvas or SVG with a hand-drawn, sketchy, appearance.

446 lines (418 loc) 12.5 kB
class PathToken { constructor(type, text) { this.type = type; this.text = text; } isType(type) { return this.type === type; } } class ParsedPath { constructor(d) { this.PARAMS = { A: ["rx", "ry", "x-axis-rotation", "large-arc-flag", "sweep-flag", "x", "y"], a: ["rx", "ry", "x-axis-rotation", "large-arc-flag", "sweep-flag", "x", "y"], C: ["x1", "y1", "x2", "y2", "x", "y"], c: ["x1", "y1", "x2", "y2", "x", "y"], H: ["x"], h: ["x"], L: ["x", "y"], l: ["x", "y"], M: ["x", "y"], m: ["x", "y"], Q: ["x1", "y1", "x", "y"], q: ["x1", "y1", "x", "y"], S: ["x2", "y2", "x", "y"], s: ["x2", "y2", "x", "y"], T: ["x", "y"], t: ["x", "y"], V: ["y"], v: ["y"], Z: [], z: [] }; this.COMMAND = 0; this.NUMBER = 1; this.EOD = 2; this.segments = []; this.d = d || ""; this.parseData(d); this.processPoints(); } loadFromSegments(segments) { this.segments = segments; this.processPoints(); } processPoints() { let first = null, prev = null, currentPoint = [0, 0]; for (let i = 0; i < this.segments.length; i++) { let 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; } prev = s; } } get closed() { if (typeof this._closed === 'undefined') { this._closed = false; for (let s of this.segments) { if (s.key.toLowerCase() === 'z') { this._closed = true; } } } return this._closed; } parseData(d) { var tokens = this.tokenize(d); var index = 0; var token = tokens[index]; var mode = "BOD"; this.segments = new Array(); while (!token.isType(this.EOD)) { var param_length; var params = new Array(); if (mode == "BOD") { if (token.text == "M" || token.text == "m") { index++; param_length = this.PARAMS[token.text].length; mode = token.text; } else { return this.parseData('M0,0' + d); } } else { if (token.isType(this.NUMBER)) { param_length = this.PARAMS[mode].length; } else { index++; param_length = this.PARAMS[token.text].length; mode = token.text; } } if ((index + param_length) < tokens.length) { for (var i = index; i < index + param_length; i++) { var number = tokens[i]; if (number.isType(this.NUMBER)) { params[params.length] = number.text; } else { console.error("Parameter type is not a number: " + mode + "," + number.text); return; } } var segment; if (this.PARAMS[mode]) { segment = { key: mode, data: params }; } else { console.error("Unsupported segment type: " + mode); return; } this.segments.push(segment); index += param_length; token = tokens[index]; if (mode == "M") mode = "L"; if (mode == "m") mode = "l"; } else { console.error("Path data ended before all parameters were found"); } } } tokenize(d) { var 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] = new PathToken(this.COMMAND, RegExp.$1); d = d.substr(RegExp.$1.length); } else if (d.match(/^(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)/)) { tokens[tokens.length] = new PathToken(this.NUMBER, parseFloat(RegExp.$1)); d = d.substr(RegExp.$1.length); } else { console.error("Unrecognized segment command: " + d); return null; } } tokens[tokens.length] = new PathToken(this.EOD, null); return tokens; } } export class RoughPath { constructor(d) { this.d = d; this.parsed = new ParsedPath(d); this._position = [0, 0]; this.bezierReflectionPoint = null; this.quadReflectionPoint = null; this._first = null; } get segments() { return this.parsed.segments; } get closed() { return this.parsed.closed; } get linearPoints() { if (!this._linearPoints) { const lp = []; let points = []; for (let s of this.parsed.segments) { let 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]; } } export class RoughArcConverter { // 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 constructor(from, to, radii, angle, largeArcFlag, sweepFlag) { const radPerDeg = Math.PI / 180; this._segIndex = 0; this._numSegs = 0; if (from[0] == to[0] && from[1] == to[1]) { return; } this._rx = Math.abs(radii[0]); this._ry = Math.abs(radii[1]); this._sinPhi = Math.sin(angle * radPerDeg); this._cosPhi = Math.cos(angle * radPerDeg); var x1dash = this._cosPhi * (from[0] - to[0]) / 2.0 + this._sinPhi * (from[1] - to[1]) / 2.0; var y1dash = -this._sinPhi * (from[0] - to[0]) / 2.0 + this._cosPhi * (from[1] - to[1]) / 2.0; var root; var numerator = this._rx * this._rx * this._ry * this._ry - this._rx * this._rx * y1dash * y1dash - this._ry * this._ry * x1dash * x1dash; if (numerator < 0) { let s = Math.sqrt(1 - (numerator / (this._rx * this._rx * this._ry * this._ry))); this._rx = s; 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)); } let cxdash = root * this._rx * y1dash / this._ry; let 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); this._from = from; } getNextSegment() { var cp1, cp2, to; if (this._segIndex == this._numSegs) { return null; } let cosTheta1 = Math.cos(this._theta); let sinTheta1 = Math.sin(this._theta); let theta2 = this._theta + this._delta; let cosTheta2 = Math.cos(theta2); let sinTheta2 = Math.sin(theta2); 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] ]; 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) ]; 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) { let ta = Math.atan2(uy, ux); let 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) { let outSets = []; for (const set of this.sets) { let 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++) { let 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; } let points = set.slice(0); while (points.length > count) { let areas = []; let minArea = -1; let minIndex = -1; for (let i = 1; i < (points.length - 1); i++) { let a = this.distance(points[i - 1], points[i]); let b = this.distance(points[i], points[i + 1]); let c = this.distance(points[i - 1], points[i + 1]); let s = (a + b + c) / 2.0; let 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; } }