jointjs
Version:
JavaScript diagramming library
1,670 lines (1,258 loc) • 205 kB
JavaScript
/*! JointJS v3.4.4 (2021-09-27) - JavaScript diagramming library
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = global || self, factory(global.g = {}));
}(this, function (exports) { 'use strict';
// Declare shorthands to the most used math functions.
var round = Math.round;
var floor = Math.floor;
var PI = Math.PI;
var scale = {
// Return the `value` from the `domain` interval scaled to the `range` interval.
linear: function(domain, range, value) {
var domainSpan = domain[1] - domain[0];
var rangeSpan = range[1] - range[0];
return (((value - domain[0]) / domainSpan) * rangeSpan + range[0]) || 0;
}
};
var normalizeAngle = function(angle) {
return (angle % 360) + (angle < 0 ? 360 : 0);
};
var snapToGrid = function(value, gridSize) {
return gridSize * round(value / gridSize);
};
var toDeg = function(rad) {
return (180 * rad / PI) % 360;
};
var toRad = function(deg, over360) {
over360 = over360 || false;
deg = over360 ? deg : (deg % 360);
return deg * PI / 180;
};
// Return a random integer from the interval [min,max], inclusive.
var random = function(min, max) {
if (max === undefined) {
// use first argument as max, min is 0
max = (min === undefined) ? 1 : min;
min = 0;
} else if (max < min) {
// switch max and min
var temp = min;
min = max;
max = temp;
}
return floor((Math.random() * (max - min + 1)) + min);
};
// @return the bearing (cardinal direction) of the line. For example N, W, or SE.
var cos = Math.cos;
var sin = Math.sin;
var atan2 = Math.atan2;
var bearing = function(p, q) {
var lat1 = toRad(p.y);
var lat2 = toRad(q.y);
var lon1 = p.x;
var lon2 = q.x;
var dLon = toRad(lon2 - lon1);
var y = sin(dLon) * cos(lat2);
var x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLon);
var brng = toDeg(atan2(y, x));
var bearings = ['NE', 'E', 'SE', 'S', 'SW', 'W', 'NW', 'N'];
var index = brng - 22.5;
if (index < 0)
{ index += 360; }
index = parseInt(index / 45);
return bearings[index];
};
// @return {integer} length without sqrt
// @note for applications where the exact length is not necessary (e.g. compare only)
var squaredLength = function(start, end) {
var x0 = start.x;
var y0 = start.y;
var x1 = end.x;
var y1 = end.y;
return (x0 -= x1) * x0 + (y0 -= y1) * y0;
};
var length = function(start, end) {
return Math.sqrt(squaredLength(start, end));
};
/*
Point is the most basic object consisting of x/y coordinate.
Possible instantiations are:
* `Point(10, 20)`
* `new Point(10, 20)`
* `Point('10 20')`
* `Point(Point(10, 20))`
*/
var abs = Math.abs;
var cos$1 = Math.cos;
var sin$1 = Math.sin;
var sqrt = Math.sqrt;
var min = Math.min;
var max = Math.max;
var atan2$1 = Math.atan2;
var round$1 = Math.round;
var pow = Math.pow;
var PI$1 = Math.PI;
var Point = function(x, y) {
if (!(this instanceof Point)) {
return new Point(x, y);
}
if (typeof x === 'string') {
var xy = x.split(x.indexOf('@') === -1 ? ' ' : '@');
x = parseFloat(xy[0]);
y = parseFloat(xy[1]);
} else if (Object(x) === x) {
y = x.y;
x = x.x;
}
this.x = x === undefined ? 0 : x;
this.y = y === undefined ? 0 : y;
};
// Alternative constructor, from polar coordinates.
// @param {number} Distance.
// @param {number} Angle in radians.
// @param {point} [optional] Origin.
Point.fromPolar = function(distance, angle, origin) {
origin = new Point(origin);
var x = abs(distance * cos$1(angle));
var y = abs(distance * sin$1(angle));
var deg = normalizeAngle(toDeg(angle));
if (deg < 90) {
y = -y;
} else if (deg < 180) {
x = -x;
y = -y;
} else if (deg < 270) {
x = -x;
}
return new Point(origin.x + x, origin.y + y);
};
// Create a point with random coordinates that fall into the range `[x1, x2]` and `[y1, y2]`.
Point.random = function(x1, x2, y1, y2) {
return new Point(random(x1, x2), random(y1, y2));
};
Point.prototype = {
chooseClosest: function(points) {
var n = points.length;
if (n === 1) { return new Point(points[0]); }
var closest = null;
var minSqrDistance = Infinity;
for (var i = 0; i < n; i++) {
var p = new Point(points[i]);
var sqrDistance = this.squaredDistance(p);
if (sqrDistance < minSqrDistance) {
closest = p;
minSqrDistance = sqrDistance;
}
}
return closest;
},
// If point lies outside rectangle `r`, return the nearest point on the boundary of rect `r`,
// otherwise return point itself.
// (see Squeak Smalltalk, Point>>adhereTo:)
adhereToRect: function(r) {
if (r.containsPoint(this)) {
return this;
}
this.x = min(max(this.x, r.x), r.x + r.width);
this.y = min(max(this.y, r.y), r.y + r.height);
return this;
},
// Compute the angle between vector from me to p1 and the vector from me to p2.
// ordering of points p1 and p2 is important!
// theta function's angle convention:
// returns angles between 0 and 180 when the angle is counterclockwise
// returns angles between 180 and 360 to convert clockwise angles into counterclockwise ones
// returns NaN if any of the points p1, p2 is coincident with this point
angleBetween: function(p1, p2) {
var angleBetween = (this.equals(p1) || this.equals(p2)) ? NaN : (this.theta(p2) - this.theta(p1));
if (angleBetween < 0) {
angleBetween += 360; // correction to keep angleBetween between 0 and 360
}
return angleBetween;
},
// Return the bearing between me and the given point.
bearing: function(point) {
return bearing(this, point);
},
// Returns change in angle from my previous position (-dx, -dy) to my new position
// relative to ref point.
changeInAngle: function(dx, dy, ref) {
// Revert the translation and measure the change in angle around x-axis.
return this.clone().offset(-dx, -dy).theta(ref) - this.theta(ref);
},
clone: function() {
return new Point(this);
},
// Returns the cross product of this point relative to two other points
// this point is the common point
// point p1 lies on the first vector, point p2 lies on the second vector
// watch out for the ordering of points p1 and p2!
// positive result indicates a clockwise ("right") turn from first to second vector
// negative result indicates a counterclockwise ("left") turn from first to second vector
// zero indicates that the first and second vector are collinear
// note that the above directions are reversed from the usual answer on the Internet
// that is because we are in a left-handed coord system (because the y-axis points downward)
cross: function(p1, p2) {
return (p1 && p2) ? (((p2.x - this.x) * (p1.y - this.y)) - ((p2.y - this.y) * (p1.x - this.x))) : NaN;
},
difference: function(dx, dy) {
if ((Object(dx) === dx)) {
dy = dx.y;
dx = dx.x;
}
return new Point(this.x - (dx || 0), this.y - (dy || 0));
},
// Returns distance between me and point `p`.
distance: function(p) {
return length(this, p);
},
// Returns the dot product of this point with given other point
dot: function(p) {
return p ? (this.x * p.x + this.y * p.y) : NaN;
},
equals: function(p) {
return !!p &&
this.x === p.x &&
this.y === p.y;
},
// Linear interpolation
lerp: function(p, t) {
var x = this.x;
var y = this.y;
return new Point((1 - t) * x + t * p.x, (1 - t) * y + t * p.y);
},
magnitude: function() {
return sqrt((this.x * this.x) + (this.y * this.y)) || 0.01;
},
// Returns a manhattan (taxi-cab) distance between me and point `p`.
manhattanDistance: function(p) {
return abs(p.x - this.x) + abs(p.y - this.y);
},
// Move point on line starting from ref ending at me by
// distance distance.
move: function(ref, distance) {
var theta = toRad((new Point(ref)).theta(this));
var offset = this.offset(cos$1(theta) * distance, -sin$1(theta) * distance);
return offset;
},
// Scales x and y such that the distance between the point and the origin (0,0) is equal to the given length.
normalize: function(length) {
var scale = (length || 1) / this.magnitude();
return this.scale(scale, scale);
},
// Offset me by the specified amount.
offset: function(dx, dy) {
if ((Object(dx) === dx)) {
dy = dx.y;
dx = dx.x;
}
this.x += dx || 0;
this.y += dy || 0;
return this;
},
// Returns a point that is the reflection of me with
// the center of inversion in ref point.
reflection: function(ref) {
return (new Point(ref)).move(this, this.distance(ref));
},
// Rotate point by angle around origin.
// Angle is flipped because this is a left-handed coord system (y-axis points downward).
rotate: function(origin, angle) {
if (angle === 0) { return this; }
origin = origin || new Point(0, 0);
angle = toRad(normalizeAngle(-angle));
var cosAngle = cos$1(angle);
var sinAngle = sin$1(angle);
var x = (cosAngle * (this.x - origin.x)) - (sinAngle * (this.y - origin.y)) + origin.x;
var y = (sinAngle * (this.x - origin.x)) + (cosAngle * (this.y - origin.y)) + origin.y;
this.x = x;
this.y = y;
return this;
},
round: function(precision) {
var f = 1; // case 0
if (precision) {
switch (precision) {
case 1: f = 10; break;
case 2: f = 100; break;
case 3: f = 1000; break;
default: f = pow(10, precision); break;
}
}
this.x = round$1(this.x * f) / f;
this.y = round$1(this.y * f) / f;
return this;
},
// Scale point with origin.
scale: function(sx, sy, origin) {
origin = (origin && new Point(origin)) || new Point(0, 0);
this.x = origin.x + sx * (this.x - origin.x);
this.y = origin.y + sy * (this.y - origin.y);
return this;
},
snapToGrid: function(gx, gy) {
this.x = snapToGrid(this.x, gx);
this.y = snapToGrid(this.y, gy || gx);
return this;
},
squaredDistance: function(p) {
return squaredLength(this, p);
},
// Compute the angle between me and `p` and the x axis.
// (cartesian-to-polar coordinates conversion)
// Return theta angle in degrees.
theta: function(p) {
p = new Point(p);
// Invert the y-axis.
var y = -(p.y - this.y);
var x = p.x - this.x;
var rad = atan2$1(y, x); // defined for all 0 corner cases
// Correction for III. and IV. quadrant.
if (rad < 0) {
rad = 2 * PI$1 + rad;
}
return 180 * rad / PI$1;
},
toJSON: function() {
return { x: this.x, y: this.y };
},
// Converts rectangular to polar coordinates.
// An origin can be specified, otherwise it's 0@0.
toPolar: function(o) {
o = (o && new Point(o)) || new Point(0, 0);
var x = this.x;
var y = this.y;
this.x = sqrt((x - o.x) * (x - o.x) + (y - o.y) * (y - o.y)); // r
this.y = toRad(o.theta(new Point(x, y)));
return this;
},
toString: function() {
return this.x + '@' + this.y;
},
serialize: function() {
return this.x + ',' + this.y;
},
update: function(x, y) {
if ((Object(x) === x)) {
y = x.y;
x = x.x;
}
this.x = x || 0;
this.y = y || 0;
return this;
},
// Compute the angle between the vector from 0,0 to me and the vector from 0,0 to p.
// Returns NaN if p is at 0,0.
vectorAngle: function(p) {
var zero = new Point(0, 0);
return zero.angleBetween(this, p);
}
};
Point.prototype.translate = Point.prototype.offset;
// For backwards compatibility:
var point = Point;
var max$1 = Math.max;
var min$1 = Math.min;
var Line = function(p1, p2) {
if (!(this instanceof Line)) {
return new Line(p1, p2);
}
if (p1 instanceof Line) {
return new Line(p1.start, p1.end);
}
this.start = new Point(p1);
this.end = new Point(p2);
};
Line.prototype = {
// @returns the angle of incline of the line.
angle: function() {
var horizontalPoint = new Point(this.start.x + 1, this.start.y);
return this.start.angleBetween(this.end, horizontalPoint);
},
bbox: function() {
var left = min$1(this.start.x, this.end.x);
var top = min$1(this.start.y, this.end.y);
var right = max$1(this.start.x, this.end.x);
var bottom = max$1(this.start.y, this.end.y);
return new Rect(left, top, (right - left), (bottom - top));
},
// @return the bearing (cardinal direction) of the line. For example N, W, or SE.
// @returns {String} One of the following bearings : NE, E, SE, S, SW, W, NW, N.
bearing: function() {
return bearing(this.start, this.end);
},
clone: function() {
return new Line(this.start, this.end);
},
// @return {point} the closest point on the line to point `p`
closestPoint: function(p) {
return this.pointAt(this.closestPointNormalizedLength(p));
},
closestPointLength: function(p) {
return this.closestPointNormalizedLength(p) * this.length();
},
// @return {number} the normalized length of the closest point on the line to point `p`
closestPointNormalizedLength: function(p) {
var product = this.vector().dot((new Line(this.start, p)).vector());
var cpNormalizedLength = min$1(1, max$1(0, product / this.squaredLength()));
// cpNormalizedLength returns `NaN` if this line has zero length
// we can work with that - if `NaN`, return 0
if (cpNormalizedLength !== cpNormalizedLength) { return 0; } // condition evaluates to `true` if and only if cpNormalizedLength is `NaN`
// (`NaN` is the only value that is not equal to itself)
return cpNormalizedLength;
},
closestPointTangent: function(p) {
return this.tangentAt(this.closestPointNormalizedLength(p));
},
// Returns `true` if the point lies on the line.
containsPoint: function(p) {
var start = this.start;
var end = this.end;
if (start.cross(p, end) !== 0) { return false; }
// else: cross product of 0 indicates that this line and the vector to `p` are collinear
var length = this.length();
if ((new Line(start, p)).length() > length) { return false; }
if ((new Line(p, end)).length() > length) { return false; }
// else: `p` lies between start and end of the line
return true;
},
// Divides the line into two at requested `ratio` between 0 and 1.
divideAt: function(ratio) {
var dividerPoint = this.pointAt(ratio);
// return array with two lines
return [
new Line(this.start, dividerPoint),
new Line(dividerPoint, this.end)
];
},
// Divides the line into two at requested `length`.
divideAtLength: function(length) {
var dividerPoint = this.pointAtLength(length);
// return array with two new lines
return [
new Line(this.start, dividerPoint),
new Line(dividerPoint, this.end)
];
},
equals: function(l) {
return !!l &&
this.start.x === l.start.x &&
this.start.y === l.start.y &&
this.end.x === l.end.x &&
this.end.y === l.end.y;
},
// @return {point} Point where I'm intersecting a line.
// @return [point] Points where I'm intersecting a rectangle.
// @see Squeak Smalltalk, LineSegment>>intersectionWith:
intersect: function(shape, opt) {
if (shape && shape.intersectionWithLine) {
var intersection = shape.intersectionWithLine(this, opt);
// Backwards compatibility
if (intersection && (shape instanceof Line)) {
intersection = intersection[0];
}
return intersection;
}
return null;
},
intersectionWithLine: function(line) {
var pt1Dir = new Point(this.end.x - this.start.x, this.end.y - this.start.y);
var pt2Dir = new Point(line.end.x - line.start.x, line.end.y - line.start.y);
var det = (pt1Dir.x * pt2Dir.y) - (pt1Dir.y * pt2Dir.x);
var deltaPt = new Point(line.start.x - this.start.x, line.start.y - this.start.y);
var alpha = (deltaPt.x * pt2Dir.y) - (deltaPt.y * pt2Dir.x);
var beta = (deltaPt.x * pt1Dir.y) - (deltaPt.y * pt1Dir.x);
if (det === 0 || alpha * det < 0 || beta * det < 0) {
// No intersection found.
return null;
}
if (det > 0) {
if (alpha > det || beta > det) {
return null;
}
} else {
if (alpha < det || beta < det) {
return null;
}
}
return [new Point(
this.start.x + (alpha * pt1Dir.x / det),
this.start.y + (alpha * pt1Dir.y / det)
)];
},
isDifferentiable: function() {
return !this.start.equals(this.end);
},
// @return {double} length of the line
length: function() {
return length(this.start, this.end);
},
// @return {point} my midpoint
midpoint: function() {
return new Point(
(this.start.x + this.end.x) / 2,
(this.start.y + this.end.y) / 2
);
},
parallel: function(distance) {
var l = this.clone();
if (!this.isDifferentiable()) { return l; }
var start = l.start;
var end = l.end;
var eRef = start.clone().rotate(end, 270);
var sRef = end.clone().rotate(start, 90);
start.move(sRef, distance);
end.move(eRef, distance);
return l;
},
// @return {point} my point at 't' <0,1>
pointAt: function(t) {
var start = this.start;
var end = this.end;
if (t <= 0) { return start.clone(); }
if (t >= 1) { return end.clone(); }
return start.lerp(end, t);
},
pointAtLength: function(length) {
var start = this.start;
var end = this.end;
var fromStart = true;
if (length < 0) {
fromStart = false; // negative lengths mean start calculation from end point
length = -length; // absolute value
}
var lineLength = this.length();
if (length >= lineLength) { return (fromStart ? end.clone() : start.clone()); }
return this.pointAt((fromStart ? (length) : (lineLength - length)) / lineLength);
},
// @return {number} the offset of the point `p` from the line. + if the point `p` is on the right side of the line, - if on the left and 0 if on the line.
pointOffset: function(p) {
// Find the sign of the determinant of vectors (start,end), where p is the query point.
p = new Point(p);
var start = this.start;
var end = this.end;
var determinant = ((end.x - start.x) * (p.y - start.y) - (end.y - start.y) * (p.x - start.x));
return determinant / this.length();
},
rotate: function(origin, angle) {
this.start.rotate(origin, angle);
this.end.rotate(origin, angle);
return this;
},
round: function(precision) {
this.start.round(precision);
this.end.round(precision);
return this;
},
scale: function(sx, sy, origin) {
this.start.scale(sx, sy, origin);
this.end.scale(sx, sy, origin);
return this;
},
// @return {number} scale the line so that it has the requested length
setLength: function(length) {
var currentLength = this.length();
if (!currentLength) { return this; }
var scaleFactor = length / currentLength;
return this.scale(scaleFactor, scaleFactor, this.start);
},
// @return {integer} length without sqrt
// @note for applications where the exact length is not necessary (e.g. compare only)
squaredLength: function() {
return squaredLength(this.start, this.end);
},
tangentAt: function(t) {
if (!this.isDifferentiable()) { return null; }
var start = this.start;
var end = this.end;
var tangentStart = this.pointAt(t); // constrains `t` between 0 and 1
var tangentLine = new Line(start, end);
tangentLine.translate(tangentStart.x - start.x, tangentStart.y - start.y); // move so that tangent line starts at the point requested
return tangentLine;
},
tangentAtLength: function(length) {
if (!this.isDifferentiable()) { return null; }
var start = this.start;
var end = this.end;
var tangentStart = this.pointAtLength(length);
var tangentLine = new Line(start, end);
tangentLine.translate(tangentStart.x - start.x, tangentStart.y - start.y); // move so that tangent line starts at the point requested
return tangentLine;
},
toString: function() {
return this.start.toString() + ' ' + this.end.toString();
},
serialize: function() {
return this.start.serialize() + ' ' + this.end.serialize();
},
translate: function(tx, ty) {
this.start.translate(tx, ty);
this.end.translate(tx, ty);
return this;
},
// @return vector {point} of the line
vector: function() {
return new Point(this.end.x - this.start.x, this.end.y - this.start.y);
}
};
// For backwards compatibility:
Line.prototype.intersection = Line.prototype.intersect;
// For backwards compatibility:
var line = Line;
var sqrt$1 = Math.sqrt;
var round$2 = Math.round;
var pow$1 = Math.pow;
var Ellipse = function(c, a, b) {
if (!(this instanceof Ellipse)) {
return new Ellipse(c, a, b);
}
if (c instanceof Ellipse) {
return new Ellipse(new Point(c.x, c.y), c.a, c.b);
}
c = new Point(c);
this.x = c.x;
this.y = c.y;
this.a = a;
this.b = b;
};
Ellipse.fromRect = function(rect) {
rect = new Rect(rect);
return new Ellipse(rect.center(), rect.width / 2, rect.height / 2);
};
Ellipse.prototype = {
bbox: function() {
return new Rect(this.x - this.a, this.y - this.b, 2 * this.a, 2 * this.b);
},
/**
* @returns {g.Point}
*/
center: function() {
return new Point(this.x, this.y);
},
clone: function() {
return new Ellipse(this);
},
/**
* @param {g.Point} p
* @returns {boolean}
*/
containsPoint: function(p) {
return this.normalizedDistance(p) <= 1;
},
equals: function(ellipse) {
return !!ellipse &&
ellipse.x === this.x &&
ellipse.y === this.y &&
ellipse.a === this.a &&
ellipse.b === this.b;
},
// inflate by dx and dy
// @param dx {delta_x} representing additional size to x
// @param dy {delta_y} representing additional size to y -
// dy param is not required -> in that case y is sized by dx
inflate: function(dx, dy) {
if (dx === undefined) {
dx = 0;
}
if (dy === undefined) {
dy = dx;
}
this.a += 2 * dx;
this.b += 2 * dy;
return this;
},
intersectionWithLine: function(line) {
var intersections = [];
var a1 = line.start;
var a2 = line.end;
var rx = this.a;
var ry = this.b;
var dir = line.vector();
var diff = a1.difference(new Point(this));
var mDir = new Point(dir.x / (rx * rx), dir.y / (ry * ry));
var mDiff = new Point(diff.x / (rx * rx), diff.y / (ry * ry));
var a = dir.dot(mDir);
var b = dir.dot(mDiff);
var c = diff.dot(mDiff) - 1.0;
var d = b * b - a * c;
if (d < 0) {
return null;
} else if (d > 0) {
var root = sqrt$1(d);
var ta = (-b - root) / a;
var tb = (-b + root) / a;
if ((ta < 0 || 1 < ta) && (tb < 0 || 1 < tb)) {
// if ((ta < 0 && tb < 0) || (ta > 1 && tb > 1)) outside else inside
return null;
} else {
if (0 <= ta && ta <= 1) { intersections.push(a1.lerp(a2, ta)); }
if (0 <= tb && tb <= 1) { intersections.push(a1.lerp(a2, tb)); }
}
} else {
var t = -b / a;
if (0 <= t && t <= 1) {
intersections.push(a1.lerp(a2, t));
} else {
// outside
return null;
}
}
return intersections;
},
// Find point on me where line from my center to
// point p intersects my boundary.
// @param {number} angle If angle is specified, intersection with rotated ellipse is computed.
intersectionWithLineFromCenterToPoint: function(p, angle) {
p = new Point(p);
if (angle) { p.rotate(new Point(this.x, this.y), angle); }
var dx = p.x - this.x;
var dy = p.y - this.y;
var result;
if (dx === 0) {
result = this.bbox().pointNearestToPoint(p);
if (angle) { return result.rotate(new Point(this.x, this.y), -angle); }
return result;
}
var m = dy / dx;
var mSquared = m * m;
var aSquared = this.a * this.a;
var bSquared = this.b * this.b;
var x = sqrt$1(1 / ((1 / aSquared) + (mSquared / bSquared)));
x = dx < 0 ? -x : x;
var y = m * x;
result = new Point(this.x + x, this.y + y);
if (angle) { return result.rotate(new Point(this.x, this.y), -angle); }
return result;
},
/**
* @param {g.Point} point
* @returns {number} result < 1 - inside ellipse, result == 1 - on ellipse boundary, result > 1 - outside
*/
normalizedDistance: function(point) {
var x0 = point.x;
var y0 = point.y;
var a = this.a;
var b = this.b;
var x = this.x;
var y = this.y;
return ((x0 - x) * (x0 - x)) / (a * a) + ((y0 - y) * (y0 - y)) / (b * b);
},
round: function(precision) {
var f = 1; // case 0
if (precision) {
switch (precision) {
case 1: f = 10; break;
case 2: f = 100; break;
case 3: f = 1000; break;
default: f = pow$1(10, precision); break;
}
}
this.x = round$2(this.x * f) / f;
this.y = round$2(this.y * f) / f;
this.a = round$2(this.a * f) / f;
this.b = round$2(this.b * f) / f;
return this;
},
/** Compute angle between tangent and x axis
* @param {g.Point} p Point of tangency, it has to be on ellipse boundaries.
* @returns {number} angle between tangent and x axis
*/
tangentTheta: function(p) {
var refPointDelta = 30;
var x0 = p.x;
var y0 = p.y;
var a = this.a;
var b = this.b;
var center = this.bbox().center();
var m = center.x;
var n = center.y;
var q1 = x0 > center.x + a / 2;
var q3 = x0 < center.x - a / 2;
var y, x;
if (q1 || q3) {
y = x0 > center.x ? y0 - refPointDelta : y0 + refPointDelta;
x = (a * a / (x0 - m)) - (a * a * (y0 - n) * (y - n)) / (b * b * (x0 - m)) + m;
} else {
x = y0 > center.y ? x0 + refPointDelta : x0 - refPointDelta;
y = (b * b / (y0 - n)) - (b * b * (x0 - m) * (x - m)) / (a * a * (y0 - n)) + n;
}
return (new Point(x, y)).theta(p);
},
toString: function() {
return (new Point(this.x, this.y)).toString() + ' ' + this.a + ' ' + this.b;
}
};
// For backwards compatibility:
var ellipse = Ellipse;
var abs$1 = Math.abs;
var cos$2 = Math.cos;
var sin$2 = Math.sin;
var min$2 = Math.min;
var max$2 = Math.max;
var round$3 = Math.round;
var pow$2 = Math.pow;
var Rect = function(x, y, w, h) {
if (!(this instanceof Rect)) {
return new Rect(x, y, w, h);
}
if ((Object(x) === x)) {
y = x.y;
w = x.width;
h = x.height;
x = x.x;
}
this.x = x === undefined ? 0 : x;
this.y = y === undefined ? 0 : y;
this.width = w === undefined ? 0 : w;
this.height = h === undefined ? 0 : h;
};
Rect.fromEllipse = function(e) {
e = new Ellipse(e);
return new Rect(e.x - e.a, e.y - e.b, 2 * e.a, 2 * e.b);
};
Rect.fromPointUnion = function() {
var points = [], len = arguments.length;
while ( len-- ) points[ len ] = arguments[ len ];
if (points.length === 0) { return null; }
var p = new Point();
var minX, minY, maxX, maxY;
minX = minY = Infinity;
maxX = maxY = -Infinity;
for (var i = 0; i < points.length; i++) {
p.update(points[i]);
var x = p.x;
var y = p.y;
if (x < minX) { minX = x; }
if (x > maxX) { maxX = x; }
if (y < minY) { minY = y; }
if (y > maxY) { maxY = y; }
}
return new Rect(minX, minY, maxX - minX, maxY - minY);
};
Rect.fromRectUnion = function() {
var rects = [], len = arguments.length;
while ( len-- ) rects[ len ] = arguments[ len ];
if (rects.length === 0) { return null; }
var r = new Rect();
var minX, minY, maxX, maxY;
minX = minY = Infinity;
maxX = maxY = -Infinity;
for (var i = 0; i < rects.length; i++) {
r.update(rects[i]);
var x = r.x;
var y = r.y;
var mX = x + r.width;
var mY = y + r.height;
if (x < minX) { minX = x; }
if (mX > maxX) { maxX = mX; }
if (y < minY) { minY = y; }
if (mY > maxY) { maxY = mY; }
}
return new Rect(minX, minY, maxX - minX, maxY - minY);
};
Rect.prototype = {
// Find my bounding box when I'm rotated with the center of rotation in the center of me.
// @return r {rectangle} representing a bounding box
bbox: function(angle) {
if (!angle) { return this.clone(); }
var theta = toRad(angle);
var st = abs$1(sin$2(theta));
var ct = abs$1(cos$2(theta));
var w = this.width * ct + this.height * st;
var h = this.width * st + this.height * ct;
return new Rect(this.x + (this.width - w) / 2, this.y + (this.height - h) / 2, w, h);
},
bottomLeft: function() {
return new Point(this.x, this.y + this.height);
},
bottomLine: function() {
return new Line(this.bottomLeft(), this.bottomRight());
},
bottomMiddle: function() {
return new Point(this.x + this.width / 2, this.y + this.height);
},
center: function() {
return new Point(this.x + this.width / 2, this.y + this.height / 2);
},
clone: function() {
return new Rect(this);
},
// @return {bool} true if point p is inside me.
containsPoint: function(p) {
p = new Point(p);
return p.x >= this.x && p.x <= this.x + this.width && p.y >= this.y && p.y <= this.y + this.height;
},
// @return {bool} true if rectangle `r` is inside me.
containsRect: function(r) {
var r0 = new Rect(this).normalize();
var r1 = new Rect(r).normalize();
var w0 = r0.width;
var h0 = r0.height;
var w1 = r1.width;
var h1 = r1.height;
if (!w0 || !h0 || !w1 || !h1) {
// At least one of the dimensions is 0
return false;
}
var x0 = r0.x;
var y0 = r0.y;
var x1 = r1.x;
var y1 = r1.y;
w1 += x1;
w0 += x0;
h1 += y1;
h0 += y0;
return x0 <= x1 && w1 <= w0 && y0 <= y1 && h1 <= h0;
},
corner: function() {
return new Point(this.x + this.width, this.y + this.height);
},
// @return {boolean} true if rectangles are equal.
equals: function(r) {
var mr = (new Rect(this)).normalize();
var nr = (new Rect(r)).normalize();
return mr.x === nr.x && mr.y === nr.y && mr.width === nr.width && mr.height === nr.height;
},
// inflate by dx and dy, recompute origin [x, y]
// @param dx {delta_x} representing additional size to x
// @param dy {delta_y} representing additional size to y -
// dy param is not required -> in that case y is sized by dx
inflate: function(dx, dy) {
if (dx === undefined) {
dx = 0;
}
if (dy === undefined) {
dy = dx;
}
this.x -= dx;
this.y -= dy;
this.width += 2 * dx;
this.height += 2 * dy;
return this;
},
// @return {rect} if rectangles intersect, {null} if not.
intersect: function(r) {
var myOrigin = this.origin();
var myCorner = this.corner();
var rOrigin = r.origin();
var rCorner = r.corner();
// No intersection found
if (rCorner.x <= myOrigin.x ||
rCorner.y <= myOrigin.y ||
rOrigin.x >= myCorner.x ||
rOrigin.y >= myCorner.y) { return null; }
var x = max$2(myOrigin.x, rOrigin.x);
var y = max$2(myOrigin.y, rOrigin.y);
return new Rect(x, y, min$2(myCorner.x, rCorner.x) - x, min$2(myCorner.y, rCorner.y) - y);
},
intersectionWithLine: function(line) {
var r = this;
var rectLines = [r.topLine(), r.rightLine(), r.bottomLine(), r.leftLine()];
var points = [];
var dedupeArr = [];
var pt, i;
var n = rectLines.length;
for (i = 0; i < n; i++) {
pt = line.intersect(rectLines[i]);
if (pt !== null && dedupeArr.indexOf(pt.toString()) < 0) {
points.push(pt);
dedupeArr.push(pt.toString());
}
}
return points.length > 0 ? points : null;
},
// Find point on my boundary where line starting
// from my center ending in point p intersects me.
// @param {number} angle If angle is specified, intersection with rotated rectangle is computed.
intersectionWithLineFromCenterToPoint: function(p, angle) {
p = new Point(p);
var center = new Point(this.x + this.width / 2, this.y + this.height / 2);
var result;
if (angle) { p.rotate(center, angle); }
// (clockwise, starting from the top side)
var sides = [
this.topLine(),
this.rightLine(),
this.bottomLine(),
this.leftLine()
];
var connector = new Line(center, p);
for (var i = sides.length - 1; i >= 0; --i) {
var intersection = sides[i].intersection(connector);
if (intersection !== null) {
result = intersection;
break;
}
}
if (result && angle) { result.rotate(center, -angle); }
return result;
},
leftLine: function() {
return new Line(this.topLeft(), this.bottomLeft());
},
leftMiddle: function() {
return new Point(this.x, this.y + this.height / 2);
},
maxRectScaleToFit: function(rect, origin) {
rect = new Rect(rect);
origin || (origin = rect.center());
var sx1, sx2, sx3, sx4, sy1, sy2, sy3, sy4;
var ox = origin.x;
var oy = origin.y;
// Here we find the maximal possible scale for all corner points (for x and y axis) of the rectangle,
// so when the scale is applied the point is still inside the rectangle.
sx1 = sx2 = sx3 = sx4 = sy1 = sy2 = sy3 = sy4 = Infinity;
// Top Left
var p1 = rect.topLeft();
if (p1.x < ox) {
sx1 = (this.x - ox) / (p1.x - ox);
}
if (p1.y < oy) {
sy1 = (this.y - oy) / (p1.y - oy);
}
// Bottom Right
var p2 = rect.bottomRight();
if (p2.x > ox) {
sx2 = (this.x + this.width - ox) / (p2.x - ox);
}
if (p2.y > oy) {
sy2 = (this.y + this.height - oy) / (p2.y - oy);
}
// Top Right
var p3 = rect.topRight();
if (p3.x > ox) {
sx3 = (this.x + this.width - ox) / (p3.x - ox);
}
if (p3.y < oy) {
sy3 = (this.y - oy) / (p3.y - oy);
}
// Bottom Left
var p4 = rect.bottomLeft();
if (p4.x < ox) {
sx4 = (this.x - ox) / (p4.x - ox);
}
if (p4.y > oy) {
sy4 = (this.y + this.height - oy) / (p4.y - oy);
}
return {
sx: min$2(sx1, sx2, sx3, sx4),
sy: min$2(sy1, sy2, sy3, sy4)
};
},
maxRectUniformScaleToFit: function(rect, origin) {
var scale = this.maxRectScaleToFit(rect, origin);
return min$2(scale.sx, scale.sy);
},
// Move and expand me.
// @param r {rectangle} representing deltas
moveAndExpand: function(r) {
this.x += r.x || 0;
this.y += r.y || 0;
this.width += r.width || 0;
this.height += r.height || 0;
return this;
},
// Normalize the rectangle; i.e., make it so that it has a non-negative width and height.
// If width < 0 the function swaps the left and right corners,
// and it swaps the top and bottom corners if height < 0
// like in http://qt-project.org/doc/qt-4.8/qrectf.html#normalized
normalize: function() {
var newx = this.x;
var newy = this.y;
var newwidth = this.width;
var newheight = this.height;
if (this.width < 0) {
newx = this.x + this.width;
newwidth = -this.width;
}
if (this.height < 0) {
newy = this.y + this.height;
newheight = -this.height;
}
this.x = newx;
this.y = newy;
this.width = newwidth;
this.height = newheight;
return this;
},
// Offset me by the specified amount.
offset: function(dx, dy) {
// pretend that this is a point and call offset()
// rewrites x and y according to dx and dy
return Point.prototype.offset.call(this, dx, dy);
},
origin: function() {
return new Point(this.x, this.y);
},
// @return {point} a point on my boundary nearest to the given point.
// @see Squeak Smalltalk, Rectangle>>pointNearestTo:
pointNearestToPoint: function(point) {
point = new Point(point);
if (this.containsPoint(point)) {
var side = this.sideNearestToPoint(point);
switch (side) {
case 'right':
return new Point(this.x + this.width, point.y);
case 'left':
return new Point(this.x, point.y);
case 'bottom':
return new Point(point.x, this.y + this.height);
case 'top':
return new Point(point.x, this.y);
}
}
return point.adhereToRect(this);
},
rightLine: function() {
return new Line(this.topRight(), this.bottomRight());
},
rightMiddle: function() {
return new Point(this.x + this.width, this.y + this.height / 2);
},
round: function(precision) {
var f = 1; // case 0
if (precision) {
switch (precision) {
case 1: f = 10; break;
case 2: f = 100; break;
case 3: f = 1000; break;
default: f = pow$2(10, precision); break;
}
}
this.x = round$3(this.x * f) / f;
this.y = round$3(this.y * f) / f;
this.width = round$3(this.width * f) / f;
this.height = round$3(this.height * f) / f;
return this;
},
// Scale rectangle with origin.
scale: function(sx, sy, origin) {
origin = this.origin().scale(sx, sy, origin);
this.x = origin.x;
this.y = origin.y;
this.width *= sx;
this.height *= sy;
return this;
},
// @return {string} (left|right|top|bottom) side which is nearest to point
// @see Squeak Smalltalk, Rectangle>>sideNearestTo:
sideNearestToPoint: function(point) {
point = new Point(point);
var distToLeft = point.x - this.x;
var distToRight = (this.x + this.width) - point.x;
var distToTop = point.y - this.y;
var distToBottom = (this.y + this.height) - point.y;
var closest = distToLeft;
var side = 'left';
if (distToRight < closest) {
closest = distToRight;
side = 'right';
}
if (distToTop < closest) {
closest = distToTop;
side = 'top';
}
if (distToBottom < closest) {
// closest = distToBottom;
side = 'bottom';
}
return side;
},
snapToGrid: function(gx, gy) {
var origin = this.origin().snapToGrid(gx, gy);
var corner = this.corner().snapToGrid(gx, gy);
this.x = origin.x;
this.y = origin.y;
this.width = corner.x - origin.x;
this.height = corner.y - origin.y;
return this;
},
toJSON: function() {
return { x: this.x, y: this.y, width: this.width, height: this.height };
},
topLine: function() {
return new Line(this.topLeft(), this.topRight());
},
topMiddle: function() {
return new Point(this.x + this.width / 2, this.y);
},
topRight: function() {
return new Point(this.x + this.width, this.y);
},
toString: function() {
return this.origin().toString() + ' ' + this.corner().toString();
},
// @return {rect} representing the union of both rectangles.
union: function(rect) {
return Rect.fromRectUnion(this, rect);
},
update: function(x, y, w, h) {
if ((Object(x) === x)) {
y = x.y;
w = x.width;
h = x.height;
x = x.x;
}
this.x = x || 0;
this.y = y || 0;
this.width = w || 0;
this.height = h || 0;
return this;
}
};
Rect.prototype.bottomRight = Rect.prototype.corner;
Rect.prototype.topLeft = Rect.prototype.origin;
Rect.prototype.translate = Rect.prototype.offset;
// For backwards compatibility:
var rect = Rect;
var abs$2 = Math.abs;
var Polyline = function(points) {
if (!(this instanceof Polyline)) {
return new Polyline(points);
}
if (typeof points === 'string') {
return new Polyline.parse(points);
}
this.points = (Array.isArray(points) ? points.map(Point) : []);
};
Polyline.parse = function(svgString) {
svgString = svgString.trim();
if (svgString === '') { return new Polyline(); }
var points = [];
var coords = svgString.split(/\s*,\s*|\s+/);
var n = coords.length;
for (var i = 0; i < n; i += 2) {
points.push({ x: +coords[i], y: +coords[i + 1] });
}
return new Polyline(points);
};
Polyline.prototype = {
bbox: function() {
var x1 = Infinity;
var x2 = -Infinity;
var y1 = Infinity;
var y2 = -Infinity;
var points = this.points;
var numPoints = points.length;
if (numPoints === 0) { return null; } // if points array is empty
for (var i = 0; i < numPoints; i++) {
var point = points[i];
var x = point.x;
var y = point.y;
if (x < x1) { x1 = x; }
if (x > x2) { x2 = x; }
if (y < y1) { y1 = y; }
if (y > y2) { y2 = y; }
}
return new Rect(x1, y1, x2 - x1, y2 - y1);
},
clone: function() {
var points = this.points;
var numPoints = points.length;
if (numPoin