UNPKG

hummus-recipe

Version:

A powerful PDF tool for NodeJS based on HummusJS

1,187 lines (1,016 loc) 43.6 kB
/* N-Gon border box for odd numbered side shapes used to deal with object rotation. -------------------------------------------------------------- ========= | m ---------------------------o---------------------------- | | | | -------------------------- | ------------------------- | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | radius | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | height | | | | | | | | | | | | | | | | | | | | | | | | | | -- o -- n-gon center | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | -------------------------- | ------------------------- | | ==== | | --------o------------------- | -------------------------- M | lw | | -------- | ------------------ | ----------------------------- ==== | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | deltaYY | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | ----- | ------------------ | ------------------------- | | | | --------o--------------------o---------------------------- | | S ------------------------------------------------------------- ========= |============================ width ===========================| Legend: m minimum x, y of original border box (minX, minY) M maximum x, y of original border box (maxX, maxY) S starting origin of final border box lw line width The even number sided polygons have no trouble rotating due to the fact that their border box coincides with the n-gon center point. That is not the case for odd number sided polygons. In this case a deltaYY value must be computed to produce a final border box with a center that coincides with n-gon center. deltaYY = minY + radius * 2 - maxY */ function odd(n) { return (n % 2 !== 0); } function toRadians(angle) { return angle * (Math.PI / 180); } function toDegrees(radians) { return radians * (180 / Math.PI); } function endPoint(x, y, l, angle) { const radians = toRadians(angle); return [(x + l * Math.cos(radians)), (y + l * Math.sin(radians))]; } function boundingBox(coords) { let boundBox = [coords[0][0], coords[0][1], coords[0][0], coords[0][1]]; for (const coord of coords) { boundBox[0] = (boundBox[0] > coord[0]) ? coord[0] : boundBox[0]; boundBox[1] = (boundBox[1] > coord[1]) ? coord[1] : boundBox[1]; boundBox[2] = (boundBox[2] < coord[0]) ? coord[0] : boundBox[2]; boundBox[3] = (boundBox[3] < coord[1]) ? coord[1] : boundBox[3]; } return boundBox; } function _n_gon(sides, cx, cy, radius, options = {}) { let lineWidth = 0; if (options.stroke || options.color || !options.fill) { lineWidth = (options.lineWidth) ? options.lineWidth : (options.width) ? options.width : 2; } let ngon = []; let angle = 360 / sides; let startingAngle = (odd(sides)) ? 270 : 270 - (angle / 2); let n_radius = radius - lineWidth / 2; for (let i = 0; i < sides; i++) { const point = endPoint(cx, cy, n_radius, startingAngle + angle * i); ngon.push(point); } options.deltaYY = 0; // used in polygon to deal with ngon rotation if (options.rotation !== undefined && odd(sides)) { let boundBox = boundingBox(ngon); options.deltaYY = boundBox[1] + n_radius * 2 - boundBox[3]; } if (options.rotationVertice) { options.rotationOrigin = ngon[(options.rotationVertice - 1) % (sides)]; } return ngon; } /** * Draw an N-sided regular polygon * @name n_gon * @function * @memberof Recipe * @param {number} cx - x-coordinate of center point of regular polygon * @param {number} cy - y-coordinate of center point of regular polygon * @param {number} radius - The radius, distance from the center of the polygon to a vertice. * @param {number} [sides=3] - the number of sides of the regular polygon * @param {Object} [options] - The options * @param {string|number[]} [options.color] - HexColor or DecimalColor * @param {string|number[]} [options.stroke] - HexColor or DecimalColor * @param {string|number[]} [options.fill] - HexColor or DecimalColor * @param {number} [options.lineWidth] - The line width * @param {number} [options.opacity] - The opacity * @param {number[]} [options.dash] - The dash style [number, number] * @param {number} [options.rotation=0] - Accept: +/- 0 through 360. * @param {number[]} [options.rotationOrigin=[cx,cy]] - [originX, originY] * @param {number} [options.rotationVertice] - the number of the vertice to be used as rotation origin * @param {number} [options.skewX] - the angle skew off the x-axis * @param {number} [options.skewY] - the angle skew off the y-axis. */ exports.n_gon = function n_gon(cx, cy, radius, sides = 3, options = {}) { const MIN_SIDES = 3; // Handle optional 'sides' event when options present. if (typeof sides === 'object') { options = sides; sides = MIN_SIDES; } if (sides < MIN_SIDES) { sides = MIN_SIDES; } const ngon = _n_gon(sides, cx, cy, radius, options); this.polygon(ngon, options); if (options.rotationVertice) { delete options['rotationOrigin']; // cleanup n-gon generated point } if (options.debug) { this.circle(cx, cy, radius, { width: 1, stroke: '#00ff00' }); this.circle(cx, cy, 2, { fill: '#ff0000' }); } return this; }; function _oddStar(ngon) { let starPath = []; let points = ngon.length; let interval = Math.floor(points / 2); for (let i = 0; i < points; i++) { starPath.push(ngon[i * interval % points]); } return starPath; } /** * Draw an N pointed star * @name star * @function * @memberof Recipe * @param {number} cx - x-coordinate of center point of regular polygon * @param {number} cy - y-coordinate of center point of regular polygon * @param {number} [points=5] - number of points on star * @param {Object} [options] - The options * @param {string|number[]} [options.color] - HexColor or DecimalColor * @param {string|number[]} [options.stroke] - HexColor or DecimalColor * @param {string|number[]} [options.fill] - HexColor or DecimalColor * @param {number} [options.lineWidth] - The line width * @param {number} [options.opacity] - The opacity * @param {number[]} [options.dash] - The dash style [number, number] * @param {number} [options.rotation] - Accept: +/- 0 through 360. Default: 0 * @param {number[]} [options.rotationOrigin] - [originX, originY] Default: x, y * @param {number} [options.skewX] - the angle skew off the x-axis * @param {number} [options.skewY] - the angle skew off the y-axis. */ exports.star = function star(cx, cy, radius, points = 5, options = {}) { let starPath = []; let ngon; const MIN_POINTS = 5; // Handle optional 'points' event when options present. if (typeof points === 'object') { options = points; points = MIN_POINTS; } if (points < MIN_POINTS) { points = MIN_POINTS; } const starOptions = Object.assign({}, options); if (odd(points)) { starPath = _oddStar(_n_gon(points, cx, cy, radius, options)); } else { let offset = -1; let halfPoints = points / 2; let interval = halfPoints - 1; let userRotation = (options.rotation) ? options.rotation : 0; if (odd(halfPoints)) { starOptions.rotation = 0 + userRotation; ngon = _n_gon(halfPoints, cx, cy, radius, starOptions); let deltaY = starOptions.deltaYY; if (halfPoints === 3) { starPath = ngon; } else { starPath = _oddStar(ngon); } this.polygon(starPath, starOptions); starOptions.rotation += (360 / points); starOptions.deltaYY = deltaY; } else { // Want a point of star to be top-most starOptions.rotation = (360 / points / 2) + userRotation; ngon = _n_gon(points, cx, cy, radius); for (let i = 0; i < points; i++) { let j = i * interval % points; if (j === 0) { offset++; if (offset > 0) { this.polygon(starPath, starOptions); starPath = []; } } starPath.push(ngon[j + offset]); } } } this.polygon(starPath, starOptions); if (options.debug) { this.circle(cx, cy, radius, { width: 1, stroke: '#00ff00' }); this.circle(cx, cy, 2, { fill: '#ff0000' }); } return this; }; function rotate(ox, oy, p, q, angle) { let [x, y] = [ox, oy]; angle = angle % 360; // keep angle within realistic bounds if (angle !== 0) { [x, y] = [x - p, y - q]; let theta; switch (angle) { case 90: case -270: [x, y] = [-y + p, x + q]; break; case -90: case 270: [x, y] = [y + p, -x + q]; break; case -180: case 180: [x, y] = [-x + p, -y + q]; break; default: theta = toRadians(angle); [x, y] = [ (x * Math.cos(theta)) - (y * Math.sin(theta)) + p, (x * Math.sin(theta)) + (y * Math.cos(theta)) + q ]; } } return [x, y]; } function center(ngon) { let [minX, minY, maxX, maxY] = boundingBox(ngon); let width = maxX - minX; let height = maxY - minY; return [minX + width / 2, minY + height / 2]; } function translate(dx, dy, ngon) { let object = ngon.slice(); for (const coord of object) { coord[0] += dx; coord[1] += dy; } return object; } function flipX(y, ngon) { let object = ngon.slice(); for (const coord of object) { coord[1] = 2 * y - coord[1]; } return object; } function flipY(x, ngon) { let object = ngon.slice(); for (const coord of object) { coord[0] = 2 * x - coord[0]; } return object; } /** * Draw a triangle, by specifying three side lengths, two side lengths and one inclusive angle, one side length and two adjacent angles, or with a set of vertices. * @name triangle * @function * @memberof Recipe * @param {number} x - x-coordinate used to position triangle, by default associated with left vertex of triangle base. * @param {number} y - y-coordinate used to position triangle, by default associated with left vertex of triangle base. * @param {number[]} traits - the data defining the triangle. Angles are specified as degrees, sides in units of points (1/72 in.). * @param {Object} [options] - The options * @param {string} [options.traitID='sss'] - indicates what type of data is being passed in the traits parameter. * ('sss'- three side lengths, 'sas' - side-angle-side (sideA, <C, sideB), 'asa' - angle-side-angle (<B, sideC, <A), * or 'vtx' - three vertex points [x,y]) * @param {string} [options.position='b'] - the position of the triangle to be set at the given x,y coordinates. * The values can be one of: 'A' - the A vertex (right vertex of triangle base), 'B' - the B vertex (left vertex of triangle base), * 'C' - the C vertex (apex of triangle), 'centroid', 'circumcenter', or 'incenter' of the triangle. * @param {Boolean} [options.flipX=false] - flip triangle up to down through rotation point. * @param {Boolean} [options.flipY=false] - flip triangle right to left through rotation point. * @param {string|number[]} [options.color] - HexColor or DecimalColor * @param {string|number[]} [options.stroke] - HexColor or DecimalColor * @param {string|number[]} [options.fill] - HexColor or DecimalColor * @param {number} [options.lineWidth] - The line width * @param {number} [options.opacity] - The opacity * @param {number[]} [options.dash] - The dash style [number, number] * @param {number} [options.rotation] - Accept: +/- 0 through 360. Default: 0 * @param {number[]} [options.rotationOrigin] - [originX, originY] Default: x, y * @param {number} [options.skewX] - the angle skew off the x-axis * @param {number} [options.skewY] - the angle skew off the y-axis. */ exports.triangle = function triangle(x, y, traits, options = {}) { let traitID = options.traitID || options.traitsID || 'sss'; let position = (options.position) ? options.position.toLowerCase() : 'default'; let triopts = Object.assign({}, options); if (traits.length !== 3) { throw new Error('Triangle requires 3 traits (sides/angles) for definition.'); } traitID = traitID.toLowerCase(); let pt, radius, trigon; let triangle = new Triangle(x, y, traitID, traits); let cc; let ic; switch (position) { case 'centroid': pt = triangle.centroid; break; case 'circumcenter': cc = triangle.circumcenter; [pt, radius] = [cc.point, cc.radius]; break; case 'incenter': ic = triangle.incenter; [pt, radius] = [ic.point, ic.radius]; triangle.incenter = [x, y]; // have to update incenter because tranlation will change it. break; case 'a': pt = new Point(triangle.A); break; case 'b': pt = new Point(triangle.B); break; case 'c': pt = new Point(triangle.C); break; default: pt = new Point(triangle.B); trigon = triangle.vertices; break; } if (!trigon) { trigon = translate(x - pt.x, y - pt.y, triangle.vertices); if (options.rotation && options.rotation !== 0) { triopts.rotationOrigin = [x, y]; } } if (options.flipX) { trigon = flipX(y, trigon); } if (options.flipY) { trigon = flipY(x, trigon); } this.polygon(trigon, triopts); if (options.debug) { let angle = triopts.rotation || 0; let [rx, ry] = (triopts.rotationOrigin) ? triopts.rotationOrigin: center(triangle.vertices); // When rotation involved, easiest to just create new triangle // with rotated points so that labelling will work properly. if (angle !== 0 || options.flipX || options.flipY) { let tgon = []; for (const vertex of trigon) { tgon.push(rotate(vertex[0], vertex[1], rx, ry, angle)); } triangle = new Triangle(tgon[0][0], tgon[0][1], 'vtx', tgon); } this.circle(x, y, 2, { color: 'red', width: .5 }); if (radius) { this.circle(x, y, radius, { color: 'green', width: .5 }); } else if (position === 'centroid') { const ma_A = new Line(triangle.A, triangle.BC.midpoint); const mb_B = new Line(triangle.B, triangle.AC.midpoint); const mc_C = new Line(triangle.C, triangle.AB.midpoint); this.line([ [ma_A.point(1).x, ma_A.point(1).y], [ma_A.point(2).x, ma_A.point(2).y] ], { color: 'green', width: .5 }); this.line([ [mb_B.point(1).x, mb_B.point(1).y], [mb_B.point(2).x, mb_B.point(2).y] ], { color: 'green', width: .5 }); this.line([ [mc_C.point(1).x, mc_C.point(1).y], [mc_C.point(2).x, mc_C.point(2).y] ], { color: 'green', width: .5 }); } this.circle(triangle.incenter.point.x, triangle.incenter.point.y, triangle.incenter.radius, { color: 'green', width: .5 }); let ctr_A = new Line(triangle.incenter.point, triangle.A); let ap = ctr_A.extend(10); this.text('A', ap.x - 5, ap.y - 5, { color: '#a10439', size: 12 }); // this.circle(ap.x, ap.y,10, {color:'green', width:.5}); let ctr_B = new Line(triangle.incenter.point, triangle.B); ap = ctr_B.extend(10); this.text('B', ap.x - 5, ap.y - 5, { color: '#a10439', size: 12 }); // this.circle(ap.x, ap.y,10, {color:'green', width:.5}); let ctr_C = new Line(triangle.incenter.point, triangle.C); ap = ctr_C.extend(10); this.text('C', ap.x - 5, ap.y - 5, { color: '#a10439', size: 12 }); // this.circle(ap.x, ap.y,10, {color:'green', width:.5}); let toSideA = new Line(triangle.A, triangle.BC.midpoint); let toSideB = new Line(triangle.B, triangle.AC.midpoint); let toSideC = new Line(triangle.C, triangle.AB.midpoint); ap = toSideA.extend(10); this.text('a', ap.x - 3, ap.y - 5, { size: 10 }); // this.circle(ap.x, ap.y,6, {color:'red', width:.5}); ap = toSideB.extend(10); this.text('b', ap.x - 3, ap.y - 5, { size: 10 }); // this.circle(ap.x, ap.y,6, {color:'red', width:.5}); ap = toSideC.extend(10); this.text('c', ap.x - 3, ap.y - 5, { size: 10 }); // this.circle(ap.x, ap.y,6, {color:'red', width:.5}); } return this; }; // Reference of triangle sides (a,b,c) and vertices (A,B,C) // C // / \ // a / \ b // /_____\ // B c A const Triangle = class Triangle { constructor(x, y, traitID, traits) { let a, b, c, angA, angB, angC; let sss, BC, AC, AB; switch (traitID.toLowerCase()) { case 'sss': sss = traits.slice().sort((a, b) => { return a - b; }); if (sss[0] + sss[1] <= sss[2]) { throw new Error('Not a valid triangle inequality (sum of 2 shortest sides must be greater than third side'); } [a, b, c] = traits; break; case 'sas': [a, angC, b] = traits; c = Math.sqrt(a * a + b * b - 2 * a * b * Math.cos(toRadians(angC))); break; case 'asa': [angB, c, angA] = traits; angC = 180 - angA - angB; if (angC <= 0) { throw new Error('Not a valid triangle angle specification (sum of 2 angles must less than 180)'); } a = c * Math.sin(toRadians(angA)) / Math.sin(toRadians(angC)); b = c * Math.sin(toRadians(angB)) / Math.sin(toRadians(angC)); break; case 'vtx': this._B = traits[0]; this._C = traits[1]; this._A = traits[2]; BC = new Line(this._B, this._C); AC = new Line(this._A, this._C); AB = new Line(this._A, this._B); a = BC.length; b = AC.length; c = AB.length; break; default: throw new Error(`Unhandled trait identification ${traitID}`); } // At this point, all side lengths are known. if (!angA) { angA = toDegrees(Math.acos((b * b + c * c - a * a) / (2 * b * c))); } if (!angB) { angB = toDegrees(Math.acos((a * a + c * c - b * b) / (2 * a * c))); } if (!angC) { angC = 180 - angA - angB; } [this._x, this._y] = [x, y]; [this._a, this._b, this._c] = [a, b, c]; [this._angA, this._angB, this._angC] = [angA, angB, angC]; this._perimeter = a + b + c; if (!this._A) { this._A = [x + c, y]; } if (!this._B) { this._B = [x, y]; } if (!this._C) { this._C = endPoint(x, y, a, -angB); } } get A() { return this._A; } get B() { return this._B; } get C() { return this._C; } get AC() { if (!this._AC) { this._AC = new Line(this._A, this._C); } return this._AC; } get AB() { if (!this._AB) { this._AB = new Line(this._A, this._B); } return this._AB; } get BC() { if (!this._BC) { this._BC = new Line(this._B, this._C); } return this._BC; } get perimeter() { return this._perimeter; } get area() { if (!this._area) { // Heron's formula let s = this.perimeter / 2; // semi-perimeter this._area = Math.sqrt(s * (s - this._a) * (s - this._b) * (s - this._c)); } return this._area; } get vertices() { return [this._B, this._C, this._A]; } // The centroid is the point where all three medians of the triangle // intersect. A median is the line running from a vertex to the midpoint // of the side opposite the vertex. get centroid() { if (!this._centroid) { let AB = new Line(this._A, this._B); let AC = new Line(this._A, this._C); let mAB = AB.midpoint; let mAC = AC.midpoint; let C_mAB = new Line(this._C, mAB); let B_mAC = new Line(this._B, mAC); this._centroid = C_mAB.intersect(B_mAC); } return this._centroid; } // The intersection of the perpendicular bisectors of // each side midpoint defines the circumcenter. get circumcenter() { if (!this._circumcenter) { // Algorithm in use is defining a circle from three noncolinear planar points // http://www.ambrsoft.com/TrigoCalc/Circle3D.htm let [x1, y1] = this._A; let [x2, y2] = this._B; let [x3, y3] = this._C; let x1y1_sq = x1 * x1 + y1 * y1; let x2y2_sq = x2 * x2 + y2 * y2; let x3y3_sq = x3 * x3 + y3 * y3; let A = x1 * (y2 - y3) - y1 * (x2 - x3) + x2 * y3 - x3 * y2; let B = x1y1_sq * (y3 - y2) + x2y2_sq * (y1 - y3) + x3y3_sq * (y2 - y1); let C = x1y1_sq * (x2 - x3) + x2y2_sq * (x3 - x1) + x3y3_sq * (x1 - x2); // let D = x1y1_sq*(x3*y2 - x2*y3) + x2y2_sq*(x1*y3 - x3*y1) + x3y3_sq*(x2*y1 - x1*y2); let a2 = 2 * A; let x = -(B / a2); let y = -(C / a2); let r = new Line(x, y, x1, y1); this._circumcenter = { point: new Point(x, y), radius: r.length }; } return this._circumcenter; } set incenter(center) { this._incenter = { point: new Point(center[0], center[1]), radius: this._incenter.radius }; } get incenter() { if (!this._incenter) { // https://www.mathopenref.com/coordincenter.html let x = (this._a * this._A[0] + this._b * this._B[0] + this._c * this._C[0]) / this.perimeter; let y = (this._a * this._A[1] + this._b * this._B[1] + this._c * this._C[1]) / this.perimeter; let radius = 2 * this.area / this.perimeter; this._incenter = { point: new Point(x, y), radius: radius }; } return this._incenter; } }; // /** // * Determine if given point is inside given polygon // * @param {number} x coordinate of point // * @param {number} y coordinate of point // * @param {number[]} ngon array of x,y coordinate pairs of polygon. // * @returns 1 when point inside polygon, 0 when outside polygon. // */ // function pointInPolygon(x, y, ngon) { // // http://alienryderflex.com/polygon/ // let oddNodes = 0; // let polyCorners = ngon.length; // let j = polyCorners - 1; // for (i = 0; i < polyCorners; i++) { // if ((ngon[i][1] < y && y <= ngon[j][1] || // ngon[j][1] < y && y <= ngon[i][1]) && // (ngon[i][0] <= x || x >= ngon[j][0])) { // oddNodes ^= (ngon[i][0] + (y - ngon[i][1]) / (ngon[j][1] - ngon[i][1]) * (ngon[j][0] - ngon[i][0]) < x); // } // j = i; // } // return oddNodes; // } /** * Draw an arrow * @name arrow * @function * @memberof Recipe * @param {number} x x-coordinate position * @param {number} y y-coordinate position * @param {Object} [options] arrow and polygon options * @param {number} [options.type=0] indicates the type of arrow head to produce. (0-'triangle', 1-'dart', 2-'kite') * Number or name may be used. Note, that the value of base offset in head option overrides this value. * @param {number|number[]} [options.head=[10,20,0]] defines the length, width and base offset of arrow head. * A single number can be used to assign both the length and width of arrow, giving the base offset value as zero. * @param {number|number[]} [options.shaft=[10,10]] defines the length and width of the arrow shaft. * @param {Boolean} [options.double=false] indicate double headed arrow production. * @param {string} [options.at] position and/or rotate at "head" or "tail" of arrow instead of at center. */ exports.arrow = function arrow(x, y, options = {}) { let defaultHeadLength = 10; let nock = null; let ox = x; let debug = options.debug; let headTypes = { 0: 0, triangle: 0, 1: .5, dart: .5, 2: -1, kite: -1 }; let shaftLength = defaultHeadLength; let shaftWidth = defaultHeadLength; let headLength = defaultHeadLength; let headWidth = defaultHeadLength * 2; let baseOffset = 0; // Extract user dimensions of arrow head. // This will either be a simple number, or // 3 component array containing the data // elements to build a KITE shaped quadrilateral. if (options.head !== void(0)) { [headLength, headWidth, baseOffset] = (Array.isArray(options.head)) ? options.head: [options.head]; if (headWidth === void(0)) { shaftWidth = shaftLength = headLength; headWidth = headLength * 2; } if (baseOffset === void(0)) { baseOffset = 0; } } if (headLength === void(0) || headLength === 0) { headLength = defaultHeadLength; } if (headWidth === void(0) || headWidth === 0) { headWidth = headLength * 2; } // Extract user dimensions of arrow shaft. if (options.shaft !== void(0)) { [shaftLength, shaftWidth] = (Array.isArray(options.shaft)) ? options.shaft: [options.shaft]; if (shaftWidth === void(0)) { shaftWidth = shaftLength; } } if (shaftWidth > headWidth) { shaftWidth = headWidth; } else if (shaftWidth === 0) { shaftWidth = headWidth / 2; } if (baseOffset === 0 && options.type) { let type = headTypes[options.type]; if (type !== void(0)) { baseOffset = type * headLength; // a percentage of the arrow head length. } } // Short cut for caller, so they don't have to specify rotation origin. if (options.at && options.rotation && !options.rotationOrigin) { options = Object.assign({}, options, { rotationOrigin: [x, y] }); } // Adjust coordinates of drop point at head, tail, or middle of arrow. // ('default' choice represents center of arrow and default rotation point) if (options.double) { switch (options.at) { case 'head': x -= headLength; break; case 'tail': x += shaftLength + headLength; break; default: x += shaftLength / 2; } nock = new Kite(x - shaftLength, y, headLength, headWidth, baseOffset); } else { switch (options.at) { case 'head': x -= headLength; break; case 'tail': x += shaftLength; break; default: x += (shaftLength - headLength) / 2; } } const head = new Kite(x, y, headLength, headWidth, baseOffset); const arrow = new Arrow(x, y, head, shaftLength, shaftWidth, nock); const halfShaft = shaftWidth / 2; const KE = head.KE; // When the KE line is vertical, it means that the base offset was zero // Consequently, the arrow head will be a triangle, not a KITE. Note // that the intersection computation would also fail for vertical lines. if (!KE.isVertical) { const shaft_top = new Line(head.I[0], y - halfShaft, head.E[0], y - halfShaft); const ke_shaft_intercept = KE.intersect(shaft_top); arrow.joinShaft(ke_shaft_intercept); } if (options.double) { // Starting at arrow head tip (Pt I below), // moving clockwise to next point. // K' K // /|_________|\ // /tl tr\ // I' * * I // \bl_________br/ // \| |/ // T' T this.polygon([ arrow.tip.I, // tip point of arrow arrow.tip.T, arrow.shaft('br'), // lower connection point to arrow tip arrow.shaft('bl'), arrow.nock.Tp, // drawing reverse arrow head at nock/tail of arrow arrow.nock.Ip, arrow.nock.Kp, arrow.shaft('tl'), arrow.shaft('tr'), // upper connection point to arrow tip arrow.tip.K, arrow.tip.I ], options); } else { // Starting at arrow head tip (Pt I below), // moving clockwise to next point. // K _ // _ tl________|\ | SW: shaftWidth // | | tr\ | SL: shaftLength // SW-| | * I |-HW HW: headWidth // |_ |_________br/ | HL: headLength // bl |/ _| // T // |_________|__| // | | // SL HL this.polygon([ arrow.tip.I, // tip point of arrow arrow.tip.T, arrow.shaft('br'), // lower connection point to arrow tip arrow.shaft('bl'), arrow.shaft('tl'), arrow.shaft('tr'), // upper connection point to arrow tip arrow.tip.K, arrow.tip.I ], options); // back to point of arrow to close polygon } if (debug) { this.circle(ox, y, 2, { color: 'red' }); if (debug === 2) { // Display Kite reference points const tc = 'red'; const cc = 'red'; const ktc = 'blue'; const kcc = 'green'; this.text('E', arrow.tip.E[0] - 3, arrow.tip.E[1] - 4, { size: 9, color: ktc }); this.circle(arrow.tip.E[0], arrow.tip.E[1], 6, { color: kcc, width: .5 }); this.text('K', arrow.tip.K[0] - 3, arrow.tip.K[1] - 10, { size: 9, color: ktc }); this.circle(arrow.tip.K[0], arrow.tip.K[1] - 6, 6, { color: kcc, width: .5 }); this.text('i', arrow.tip.I[0] + 5, arrow.tip.I[1] - 4, { size: 9, color: ktc }); this.circle(arrow.tip.I[0] + 6, arrow.tip.I[1], 6, { color: kcc, width: .5 }); this.text('T', arrow.tip.T[0] - 2, arrow.tip.T[1] + 3, { size: 9, color: ktc }); this.circle(arrow.tip.T[0], arrow.tip.T[1] + 6, 6, { color: kcc, width: .5 }); let br = arrow.shaft('br'); this.text('br', br[0] - 4, br[1] - 11, { size: 9, color: tc }); this.circle(br[0], br[1] - 8, 6, { color: cc, width: .5 }); let bl = arrow.shaft('bl'); this.text('bl', bl[0] + 4, bl[1] - 11, { size: 9, color: tc }); this.circle(bl[0] + 8, bl[1] - 8, 6, { color: cc, width: .5 }); let tl = arrow.shaft('tl'); this.text('tl', tl[0] + 5, tl[1] + 2, { size: 9, color: tc }); this.circle(tl[0] + 8, tl[1] + 7, 6, { color: cc, width: .5 }); let tr = arrow.shaft('tr'); this.text('tr', tr[0] - 3, tr[1] + 2, { size: 9, color: tc }); this.circle(tr[0], tr[1] + 7, 6, { color: cc, width: .5 }); } } return this; }; // Reference points on KITE quadrangle When a KITE becomes a Dart ... degenerates to a Triangle // // K K // ------ K * o | o // | o | o * o | o // o | o * o | o // w o | o * o | o // i o | o * o | o // d E | I E I E I // t o | o * o | o // h o | o * o | o // o | o * o | o // | o | o * o | o // ------ T * o | o // T T // |________________________|_______________| // base offset height const Kite = class Kite { constructor(x, y, width, height, baseOffset = 0) { this._x = x; this._y = y; this._width = width; this._height = height; // To produce Dart shapes, baseOffset can be positive // but it cannot exceed the height of the arrow head. this._baseOffset = (baseOffset >= height) ? (height - 1) : baseOffset; this._type = (baseOffset > 0) ? 'dart' : (baseOffset === 0) ? 'triangle' : 'kite'; this._K = new Point(x, y - (height / 2)); this._I = new Point(x + width, y); this._T = new Point(x, y + (height / 2)); this._E = new Point(x + this._baseOffset, y); } get K() { return [this._K.x, this._K.y]; } get I() { return [this._I.x, this._I.y]; } get T() { return [this._T.x, this._T.y]; } get E() { return [this._E.x, this._E.y]; } // create points I&E prime (flip, 180 degrees) to change direction of Kite on X-axis get Ip() { return [this._I.x - (2 * this._width), this._I.y]; } get Ep() { return [this._E.x + (2 * this._baseOffset), this._E.y]; } get Kp() { return [this._K.x, this._K.y]; } // no different than K or T, just here for consistency usage get Tp() { return [this._T.x, this._T.y]; } get KE() { // line segment between points K and E if (!this._KE) { this._KE = new Line(this._K.x, this._K.y, this._E.x, this._E.y); } return this._KE; } get TE() { // line segment between points T and E if (!this._TE) { this._TE = new Line(this._T.x, this._T.y, this._E.x, this._E.y); } return this._TE; } get type() { return this._type; } // position(x, y) { // } }; const Arrow = class Arrow { constructor(x, y, arrowhead, shaftLength, shaftWidth, nock) { this._x = x; this._y = y; this._tip = arrowhead; this._nock = nock; // for double headed arrows this._shaftLength = shaftLength; this._shaftWidth = shaftWidth; this._connectAt_tr = new Point(this._x, this._y - shaftWidth / 2); // top, right this._connectAt_br = new Point(this._x, this._y + shaftWidth / 2); // bottom, right } get tip() { return this._tip; } get nock() { return this._nock; } joinShaft(pointTR) { this._connectAt_tr.x = pointTR.x; this._connectAt_br.x = pointTR.x; } shaft(point) { switch (point) { case 'br': return [this._connectAt_br.x, this._connectAt_br.y]; case 'bl': return [this._x - this._shaftLength, this._connectAt_br.y]; case 'tl': return [this._x - this._shaftLength, this._connectAt_tr.y]; case 'tr': return [this._connectAt_tr.x, this._connectAt_tr.y]; } } }; const Point = class Point { constructor(x, y) { if (Array.isArray(x)) { this._x = x[0]; this._y = x[1]; } else { this._x = x; this._y = y; } } get x() { return this._x; } get y() { return this._y; } get point() { return [this._x, this._y]; } set x(xx) { this._x = xx; } set y(yy) { this._y = yy; } set point(pnt) { [this._x, this._y] = pnt; } }; const Line = class Line { constructor(x1, y1, x2, y2) { // Allow user to supply Points or Arrays instead of individual coordinates if ((x1 instanceof Point || Array.isArray(x1)) && !(y1 instanceof Point || Array.isArray(y1))) { throw new Error('2nd parameter not an instance of Point or Array'); } if (typeof x1 === 'number') { // individual coordinates this._pt1 = new Point(x1, y1); this._pt2 = new Point(x2, y2); } else { if (x1 instanceof Point) { this._pt1 = x1; } else { this._pt1 = new Point(x1); // assuming array } if (y1 instanceof Point) { this._pt2 = y1; } else { this._pt2 = new Point(y1); // assuming array } } } get isVertical() { return this._pt1.x === this._pt2.x; } point(ep) { return (ep === 1) ? this._pt1 : (ep === 2) ? this._pt2 : null; } get midpoint() { if (!this._midpoint) { let dx = (this._pt2.x - this._pt1.x) / 2; let dy = (this._pt2.y - this._pt1.y) / 2; this._midPoint = new Point(this._pt1.x + dx, this._pt1.y + dy); } return this._midPoint; } get length() { if (!this._length) { this._length = Math.sqrt(Math.pow(this._pt2.x - this._pt1.x, 2) + Math.pow(this._pt2.y - this._pt1.y, 2)); } return this._length; } get slope() { if (!this._slope) { this._slope = Math.abs(this._pt2.x - this._pt1.x) < .0001 ? Infinity : (this._pt2.y - this._pt1.y) / (this._pt2.x - this._pt1.x); } return this._slope; } get inv_slope() { // inverse slope return -(1 / this.slope); } extend(distance, ptNbr = 2) { let slope = this.slope; let ept = this.point(ptNbr); let opt = this.point((ptNbr === 1) ? 2 : 1); let x = ept.x, y = ept.y; let epsilon = .0001; let newLen, deltaX = 0, deltaY = 0; let ss; switch (slope) { case 0: deltaX = distance; newLen = Math.abs(opt.x - (x + deltaX)); break; case Infinity: deltaY = distance; newLen = Math.abs(opt.y - (y + deltaY)); break; default: ss = Math.sqrt(1 / (1 + Math.pow(slope, 2))); deltaX = distance * ss; deltaY = slope * deltaX; newLen = Math.sqrt(Math.pow(opt.x - (x + deltaX), 2) + Math.pow(opt.y - (y + deltaY), 2)); break; } // Since there are 2 possible solutions, the idea is to choose the one which // effectively gives a distance equivalent to the length of the line plus the given distance. let direction = (Math.abs(newLen - (this.length + distance)) < epsilon) ? 1 : -1; x += direction * deltaX; y += direction * deltaY; return new Point(x, y); } // Equation to solve for intersection of two line segments // when given 4 sets of points representing the segments. // due to sign negation in program, terms don't match here. // // (x2y1 - x1y2)(x4 - x3) - (x4y3 - x3y4)(x2 - x1) c1(b2) - c2(b1) // x = ----------------------------------------------- --------------- // (x2 - x1)(y4 - y3) - (x4 - x3)(y2 - y1) b1(a2) - b2(a1) // // (x2y1 - x1y2)(y4 - y3) - (x4y3 - x3y4)(y2 - y1) c1(a2) - c2(a1) // y = ----------------------------------------------- --------------- // (x2 - x1)(y4 - y3) - (x4 - x3)(y2 - y1) b1(a2) - b2(a1) intersect(CD) { let A = this._pt1, B = this._pt2; let C = CD, D = CD; let x1 = A.x, y1 = A.y; let x2 = B.x, y2 = B.y; let x3 = C.point(1).x, y3 = C.point(1).y; let x4 = D.point(2).x, y4 = D.point(2).y; // Line AB represented as a1x + b1y = c1 let a1 = y2 - y1; //let b1 = x2 - x1; let b1 = x1 - x2; //let c1 = b1*y1 - a1*x1; let c1 = b1 * y1 + a1 * x1; // Line CD represented as a2x + b2y = c2 let a2 = y4 - y3; //let b2 = x4 - x3; let b2 = x3 - x4; //let c2 = b2*y3 - a2*x3; let c2 = b2 * y3 + a2 * x3; // If the lines are parallel their slopes will be the same // causing the determinantinator to be zero, so check for that first. //let determinant = a2*b1 - a1*b2; let determinant = a1 * b2 - a2 * b1; if (determinant === 0) { return null; } let x = (b2 * c1 - b1 * c2) / determinant; //let y = (a2*c1 - a1*c2) / determinant; let y = (a1 * c2 - a2 * c1) / determinant; return new Point(x, y); } };