jsplumb
Version:
Visual connectivity for webapps
1,155 lines (1,074 loc) • 681 kB
JavaScript
/**
* jsBezier
*
* Copyright (c) 2010 - 2017 jsPlumb (hello@jsplumbtoolkit.com)
*
* licensed under the MIT license.
*
* a set of Bezier curve functions that deal with Beziers, used by jsPlumb, and perhaps useful for other people. These functions work with Bezier
* curves of arbitrary degree.
*
* - functions are all in the 'jsBezier' namespace.
*
* - all input points should be in the format {x:.., y:..}. all output points are in this format too.
*
* - all input curves should be in the format [ {x:.., y:..}, {x:.., y:..}, {x:.., y:..}, {x:.., y:..} ]
*
* - 'location' as used as an input here refers to a decimal in the range 0-1 inclusive, which indicates a point some proportion along the length
* of the curve. location as output has the same format and meaning.
*
*
* Function List:
* --------------
*
* distanceFromCurve(point, curve)
*
* Calculates the distance that the given point lies from the given Bezier. Note that it is computed relative to the center of the Bezier,
* so if you have stroked the curve with a wide pen you may wish to take that into account! The distance returned is relative to the values
* of the curve and the point - it will most likely be pixels.
*
* gradientAtPoint(curve, location)
*
* Calculates the gradient to the curve at the given location, as a decimal between 0 and 1 inclusive.
*
* gradientAtPointAlongCurveFrom (curve, location)
*
* Calculates the gradient at the point on the given curve that is 'distance' units from location.
*
* nearestPointOnCurve(point, curve)
*
* Calculates the nearest point to the given point on the given curve. The return value of this is a JS object literal, containing both the
*point's coordinates and also the 'location' of the point (see above), for example: { point:{x:551,y:150}, location:0.263365 }.
*
* pointOnCurve(curve, location)
*
* Calculates the coordinates of the point on the given Bezier curve at the given location.
*
* pointAlongCurveFrom(curve, location, distance)
*
* Calculates the coordinates of the point on the given curve that is 'distance' units from location. 'distance' should be in the same coordinate
* space as that used to construct the Bezier curve. For an HTML Canvas usage, for example, distance would be a measure of pixels.
*
* locationAlongCurveFrom(curve, location, distance)
*
* Calculates the location on the given curve that is 'distance' units from location. 'distance' should be in the same coordinate
* space as that used to construct the Bezier curve. For an HTML Canvas usage, for example, distance would be a measure of pixels.
*
* perpendicularToCurveAt(curve, location, length, distance)
*
* Calculates the perpendicular to the given curve at the given location. length is the length of the line you wish for (it will be centered
* on the point at 'location'). distance is optional, and allows you to specify a point along the path from the given location as the center of
* the perpendicular returned. The return value of this is an array of two points: [ {x:...,y:...}, {x:...,y:...} ].
*
*
*/
(function() {
var root = this;
if(typeof Math.sgn == "undefined") {
Math.sgn = function(x) { return x == 0 ? 0 : x > 0 ? 1 :-1; };
}
var Vectors = {
subtract : function(v1, v2) { return {x:v1.x - v2.x, y:v1.y - v2.y }; },
dotProduct : function(v1, v2) { return (v1.x * v2.x) + (v1.y * v2.y); },
square : function(v) { return Math.sqrt((v.x * v.x) + (v.y * v.y)); },
scale : function(v, s) { return {x:v.x * s, y:v.y * s }; }
},
maxRecursion = 64,
flatnessTolerance = Math.pow(2.0,-maxRecursion-1);
/**
* Calculates the distance that the point lies from the curve.
*
* @param point a point in the form {x:567, y:3342}
* @param curve a Bezier curve in the form [{x:..., y:...}, {x:..., y:...}, {x:..., y:...}, {x:..., y:...}]. note that this is currently
* hardcoded to assume cubiz beziers, but would be better off supporting any degree.
* @return a JS object literal containing location and distance, for example: {location:0.35, distance:10}. Location is analogous to the location
* argument you pass to the pointOnPath function: it is a ratio of distance travelled along the curve. Distance is the distance in pixels from
* the point to the curve.
*/
var _distanceFromCurve = function(point, curve) {
var candidates = [],
w = _convertToBezier(point, curve),
degree = curve.length - 1, higherDegree = (2 * degree) - 1,
numSolutions = _findRoots(w, higherDegree, candidates, 0),
v = Vectors.subtract(point, curve[0]), dist = Vectors.square(v), t = 0.0;
for (var i = 0; i < numSolutions; i++) {
v = Vectors.subtract(point, _bezier(curve, degree, candidates[i], null, null));
var newDist = Vectors.square(v);
if (newDist < dist) {
dist = newDist;
t = candidates[i];
}
}
v = Vectors.subtract(point, curve[degree]);
newDist = Vectors.square(v);
if (newDist < dist) {
dist = newDist;
t = 1.0;
}
return {location:t, distance:dist};
};
/**
* finds the nearest point on the curve to the given point.
*/
var _nearestPointOnCurve = function(point, curve) {
var td = _distanceFromCurve(point, curve);
return {point:_bezier(curve, curve.length - 1, td.location, null, null), location:td.location};
};
var _convertToBezier = function(point, curve) {
var degree = curve.length - 1, higherDegree = (2 * degree) - 1,
c = [], d = [], cdTable = [], w = [],
z = [ [1.0, 0.6, 0.3, 0.1], [0.4, 0.6, 0.6, 0.4], [0.1, 0.3, 0.6, 1.0] ];
for (var i = 0; i <= degree; i++) c[i] = Vectors.subtract(curve[i], point);
for (var i = 0; i <= degree - 1; i++) {
d[i] = Vectors.subtract(curve[i+1], curve[i]);
d[i] = Vectors.scale(d[i], 3.0);
}
for (var row = 0; row <= degree - 1; row++) {
for (var column = 0; column <= degree; column++) {
if (!cdTable[row]) cdTable[row] = [];
cdTable[row][column] = Vectors.dotProduct(d[row], c[column]);
}
}
for (i = 0; i <= higherDegree; i++) {
if (!w[i]) w[i] = [];
w[i].y = 0.0;
w[i].x = parseFloat(i) / higherDegree;
}
var n = degree, m = degree-1;
for (var k = 0; k <= n + m; k++) {
var lb = Math.max(0, k - m),
ub = Math.min(k, n);
for (i = lb; i <= ub; i++) {
var j = k - i;
w[i+j].y += cdTable[j][i] * z[j][i];
}
}
return w;
};
/**
* counts how many roots there are.
*/
var _findRoots = function(w, degree, t, depth) {
var left = [], right = [],
left_count, right_count,
left_t = [], right_t = [];
switch (_getCrossingCount(w, degree)) {
case 0 : {
return 0;
}
case 1 : {
if (depth >= maxRecursion) {
t[0] = (w[0].x + w[degree].x) / 2.0;
return 1;
}
if (_isFlatEnough(w, degree)) {
t[0] = _computeXIntercept(w, degree);
return 1;
}
break;
}
}
_bezier(w, degree, 0.5, left, right);
left_count = _findRoots(left, degree, left_t, depth+1);
right_count = _findRoots(right, degree, right_t, depth+1);
for (var i = 0; i < left_count; i++) t[i] = left_t[i];
for (var i = 0; i < right_count; i++) t[i+left_count] = right_t[i];
return (left_count+right_count);
};
var _getCrossingCount = function(curve, degree) {
var n_crossings = 0, sign, old_sign;
sign = old_sign = Math.sgn(curve[0].y);
for (var i = 1; i <= degree; i++) {
sign = Math.sgn(curve[i].y);
if (sign != old_sign) n_crossings++;
old_sign = sign;
}
return n_crossings;
};
var _isFlatEnough = function(curve, degree) {
var error,
intercept_1, intercept_2, left_intercept, right_intercept,
a, b, c, det, dInv, a1, b1, c1, a2, b2, c2;
a = curve[0].y - curve[degree].y;
b = curve[degree].x - curve[0].x;
c = curve[0].x * curve[degree].y - curve[degree].x * curve[0].y;
var max_distance_above, max_distance_below;
max_distance_above = max_distance_below = 0.0;
for (var i = 1; i < degree; i++) {
var value = a * curve[i].x + b * curve[i].y + c;
if (value > max_distance_above)
max_distance_above = value;
else if (value < max_distance_below)
max_distance_below = value;
}
a1 = 0.0; b1 = 1.0; c1 = 0.0; a2 = a; b2 = b;
c2 = c - max_distance_above;
det = a1 * b2 - a2 * b1;
dInv = 1.0/det;
intercept_1 = (b1 * c2 - b2 * c1) * dInv;
a2 = a; b2 = b; c2 = c - max_distance_below;
det = a1 * b2 - a2 * b1;
dInv = 1.0/det;
intercept_2 = (b1 * c2 - b2 * c1) * dInv;
left_intercept = Math.min(intercept_1, intercept_2);
right_intercept = Math.max(intercept_1, intercept_2);
error = right_intercept - left_intercept;
return (error < flatnessTolerance)? 1 : 0;
};
var _computeXIntercept = function(curve, degree) {
var XLK = 1.0, YLK = 0.0,
XNM = curve[degree].x - curve[0].x, YNM = curve[degree].y - curve[0].y,
XMK = curve[0].x - 0.0, YMK = curve[0].y - 0.0,
det = XNM*YLK - YNM*XLK, detInv = 1.0/det,
S = (XNM*YMK - YNM*XMK) * detInv;
return 0.0 + XLK * S;
};
var _bezier = function(curve, degree, t, left, right) {
var temp = [[]];
for (var j =0; j <= degree; j++) temp[0][j] = curve[j];
for (var i = 1; i <= degree; i++) {
for (var j =0 ; j <= degree - i; j++) {
if (!temp[i]) temp[i] = [];
if (!temp[i][j]) temp[i][j] = {};
temp[i][j].x = (1.0 - t) * temp[i-1][j].x + t * temp[i-1][j+1].x;
temp[i][j].y = (1.0 - t) * temp[i-1][j].y + t * temp[i-1][j+1].y;
}
}
if (left != null)
for (j = 0; j <= degree; j++) left[j] = temp[j][0];
if (right != null)
for (j = 0; j <= degree; j++) right[j] = temp[degree-j][j];
return (temp[degree][0]);
};
var _curveFunctionCache = {};
var _getCurveFunctions = function(order) {
var fns = _curveFunctionCache[order];
if (!fns) {
fns = [];
var f_term = function() { return function(t) { return Math.pow(t, order); }; },
l_term = function() { return function(t) { return Math.pow((1-t), order); }; },
c_term = function(c) { return function(t) { return c; }; },
t_term = function() { return function(t) { return t; }; },
one_minus_t_term = function() { return function(t) { return 1-t; }; },
_termFunc = function(terms) {
return function(t) {
var p = 1;
for (var i = 0; i < terms.length; i++) p = p * terms[i](t);
return p;
};
};
fns.push(new f_term()); // first is t to the power of the curve order
for (var i = 1; i < order; i++) {
var terms = [new c_term(order)];
for (var j = 0 ; j < (order - i); j++) terms.push(new t_term());
for (var j = 0 ; j < i; j++) terms.push(new one_minus_t_term());
fns.push(new _termFunc(terms));
}
fns.push(new l_term()); // last is (1-t) to the power of the curve order
_curveFunctionCache[order] = fns;
}
return fns;
};
/**
* calculates a point on the curve, for a Bezier of arbitrary order.
* @param curve an array of control points, eg [{x:10,y:20}, {x:50,y:50}, {x:100,y:100}, {x:120,y:100}]. For a cubic bezier this should have four points.
* @param location a decimal indicating the distance along the curve the point should be located at. this is the distance along the curve as it travels, taking the way it bends into account. should be a number from 0 to 1, inclusive.
*/
var _pointOnPath = function(curve, location) {
var cc = _getCurveFunctions(curve.length - 1),
_x = 0, _y = 0;
for (var i = 0; i < curve.length ; i++) {
_x = _x + (curve[i].x * cc[i](location));
_y = _y + (curve[i].y * cc[i](location));
}
return {x:_x, y:_y};
};
var _dist = function(p1,p2) {
return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
};
var _isPoint = function(curve) {
return curve[0].x === curve[1].x && curve[0].y === curve[1].y;
};
/**
* finds the point that is 'distance' along the path from 'location'. this method returns both the x,y location of the point and also
* its 'location' (proportion of travel along the path); the method below - _pointAlongPathFrom - calls this method and just returns the
* point.
*/
var _pointAlongPath = function(curve, location, distance) {
if (_isPoint(curve)) {
return {
point:curve[0],
location:location
};
}
var prev = _pointOnPath(curve, location),
tally = 0,
curLoc = location,
direction = distance > 0 ? 1 : -1,
cur = null;
while (tally < Math.abs(distance)) {
curLoc += (0.005 * direction);
cur = _pointOnPath(curve, curLoc);
tally += _dist(cur, prev);
prev = cur;
}
return {point:cur, location:curLoc};
};
var _length = function(curve) {
var d = new Date().getTime();
if (_isPoint(curve)) return 0;
var prev = _pointOnPath(curve, 0),
tally = 0,
curLoc = 0,
direction = 1,
cur = null;
while (curLoc < 1) {
curLoc += (0.005 * direction);
cur = _pointOnPath(curve, curLoc);
tally += _dist(cur, prev);
prev = cur;
}
console.log("length", new Date().getTime() - d);
return tally;
};
/**
* finds the point that is 'distance' along the path from 'location'.
*/
var _pointAlongPathFrom = function(curve, location, distance) {
return _pointAlongPath(curve, location, distance).point;
};
/**
* finds the location that is 'distance' along the path from 'location'.
*/
var _locationAlongPathFrom = function(curve, location, distance) {
return _pointAlongPath(curve, location, distance).location;
};
/**
* returns the gradient of the curve at the given location, which is a decimal between 0 and 1 inclusive.
*
* thanks // http://bimixual.org/AnimationLibrary/beziertangents.html
*/
var _gradientAtPoint = function(curve, location) {
var p1 = _pointOnPath(curve, location),
p2 = _pointOnPath(curve.slice(0, curve.length - 1), location),
dy = p2.y - p1.y, dx = p2.x - p1.x;
return dy === 0 ? Infinity : Math.atan(dy / dx);
};
/**
returns the gradient of the curve at the point which is 'distance' from the given location.
if this point is greater than location 1, the gradient at location 1 is returned.
if this point is less than location 0, the gradient at location 0 is returned.
*/
var _gradientAtPointAlongPathFrom = function(curve, location, distance) {
var p = _pointAlongPath(curve, location, distance);
if (p.location > 1) p.location = 1;
if (p.location < 0) p.location = 0;
return _gradientAtPoint(curve, p.location);
};
/**
* calculates a line that is 'length' pixels long, perpendicular to, and centered on, the path at 'distance' pixels from the given location.
* if distance is not supplied, the perpendicular for the given location is computed (ie. we set distance to zero).
*/
var _perpendicularToPathAt = function(curve, location, length, distance) {
distance = distance == null ? 0 : distance;
var p = _pointAlongPath(curve, location, distance),
m = _gradientAtPoint(curve, p.location),
_theta2 = Math.atan(-1 / m),
y = length / 2 * Math.sin(_theta2),
x = length / 2 * Math.cos(_theta2);
return [{x:p.point.x + x, y:p.point.y + y}, {x:p.point.x - x, y:p.point.y - y}];
};
/**
* Calculates all intersections of the given line with the given curve.
* @param x1
* @param y1
* @param x2
* @param y2
* @param curve
* @returns {Array}
*/
var _lineIntersection = function(x1, y1, x2, y2, curve) {
var a = y2 - y1,
b = x1 - x2,
c = (x1 * (y1 - y2)) + (y1 * (x2-x1)),
coeffs = _computeCoefficients(curve),
p = [
(a*coeffs[0][0]) + (b * coeffs[1][0]),
(a*coeffs[0][1])+(b*coeffs[1][1]),
(a*coeffs[0][2])+(b*coeffs[1][2]),
(a*coeffs[0][3])+(b*coeffs[1][3]) + c
],
r = _cubicRoots.apply(null, p),
intersections = [];
if (r != null) {
for (var i = 0; i < 3; i++) {
var t = r[i],
t2 = Math.pow(t, 2),
t3 = Math.pow(t, 3),
x = [
(coeffs[0][0] * t3) + (coeffs[0][1] * t2) + (coeffs[0][2] * t) + coeffs[0][3],
(coeffs[1][0] * t3) + (coeffs[1][1] * t2) + (coeffs[1][2] * t) + coeffs[1][3]
];
// check bounds of the line
var s;
if ((x2 - x1) !== 0) {
s = (x[0] - x1) / (x2 - x1);
}
else {
s = (x[1] - y1) / (y2 - y1);
}
if (t >= 0 && t <= 1.0 && s >= 0 && s <= 1.0) {
intersections.push(x);
}
}
}
return intersections;
};
/**
* Calculates all intersections of the given box with the given curve.
* @param x X position of top left corner of box
* @param y Y position of top left corner of box
* @param w width of box
* @param h height of box
* @param curve
* @returns {Array}
*/
var _boxIntersection = function(x, y, w, h, curve) {
var i = [];
i.push.apply(i, _lineIntersection(x, y, x + w, y, curve));
i.push.apply(i, _lineIntersection(x + w, y, x + w, y + h, curve));
i.push.apply(i, _lineIntersection(x + w, y + h, x, y + h, curve));
i.push.apply(i, _lineIntersection(x, y + h, x, y, curve));
return i;
};
/**
* Calculates all intersections of the given bounding box with the given curve.
* @param boundingBox Bounding box, in { x:.., y:..., w:..., h:... } format.
* @param curve
* @returns {Array}
*/
var _boundingBoxIntersection = function(boundingBox, curve) {
var i = [];
i.push.apply(i, _lineIntersection(boundingBox.x, boundingBox.y, boundingBox.x + boundingBox.w, boundingBox.y, curve));
i.push.apply(i, _lineIntersection(boundingBox.x + boundingBox.w, boundingBox.y, boundingBox.x + boundingBox.w, boundingBox.y + boundingBox.h, curve));
i.push.apply(i, _lineIntersection(boundingBox.x + boundingBox.w, boundingBox.y + boundingBox.h, boundingBox.x, boundingBox.y + boundingBox.h, curve));
i.push.apply(i, _lineIntersection(boundingBox.x, boundingBox.y + boundingBox.h, boundingBox.x, boundingBox.y, curve));
return i;
};
function _computeCoefficientsForAxis(curve, axis) {
return [
-(curve[0][axis]) + (3*curve[1][axis]) + (-3 * curve[2][axis]) + curve[3][axis],
(3*(curve[0][axis])) - (6*(curve[1][axis])) + (3*(curve[2][axis])),
-3*curve[0][axis] + 3*curve[1][axis],
curve[0][axis]
];
}
function _computeCoefficients(curve)
{
return [
_computeCoefficientsForAxis(curve, "x"),
_computeCoefficientsForAxis(curve, "y")
];
}
function sgn(x) {
return x < 0 ? -1 : x > 0 ? 1 : 0;
}
function _cubicRoots(a, b, c, d) {
var A = b / a,
B = c / a,
C = d / a,
Q = (3*B - Math.pow(A, 2))/9,
R = (9*A*B - 27*C - 2*Math.pow(A, 3))/54,
D = Math.pow(Q, 3) + Math.pow(R, 2),
S,
T,
t = [];
if (D >= 0) // complex or duplicate roots
{
S = sgn(R + Math.sqrt(D))*Math.pow(Math.abs(R + Math.sqrt(D)),(1/3));
T = sgn(R - Math.sqrt(D))*Math.pow(Math.abs(R - Math.sqrt(D)),(1/3));
t[0] = -A/3 + (S + T);
t[1] = -A/3 - (S + T)/2;
t[2] = -A/3 - (S + T)/2;
/*discard complex roots*/
if (Math.abs(Math.sqrt(3)*(S - T)/2) !== 0) {
t[1] = -1;
t[2] = -1;
}
}
else // distinct real roots
{
var th = Math.acos(R/Math.sqrt(-Math.pow(Q, 3)));
t[0] = 2*Math.sqrt(-Q)*Math.cos(th/3) - A/3;
t[1] = 2*Math.sqrt(-Q)*Math.cos((th + 2*Math.PI)/3) - A/3;
t[2] = 2*Math.sqrt(-Q)*Math.cos((th + 4*Math.PI)/3) - A/3;
}
// discard out of spec roots
for (var i = 0; i < 3; i++) {
if (t[i] < 0 || t[i] > 1.0) {
t[i] = -1;
}
}
return t;
}
var jsBezier = this.jsBezier = {
distanceFromCurve : _distanceFromCurve,
gradientAtPoint : _gradientAtPoint,
gradientAtPointAlongCurveFrom : _gradientAtPointAlongPathFrom,
nearestPointOnCurve : _nearestPointOnCurve,
pointOnCurve : _pointOnPath,
pointAlongCurveFrom : _pointAlongPathFrom,
perpendicularToCurveAt : _perpendicularToPathAt,
locationAlongCurveFrom:_locationAlongPathFrom,
getLength:_length,
lineIntersection:_lineIntersection,
boxIntersection:_boxIntersection,
boundingBoxIntersection:_boundingBoxIntersection,
version:"0.9.0"
};
if (typeof exports !== "undefined") {
exports.jsBezier = jsBezier;
}
}).call(typeof window !== 'undefined' ? window : this);
/**
* Biltong v0.4.0
*
* Various geometry functions written as part of jsPlumb and perhaps useful for others.
*
* Copyright (c) 2017 jsPlumb
* https://jsplumbtoolkit.com
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
;(function() {
"use strict";
var root = this;
var Biltong = root.Biltong = {
version:"0.4.0"
};
if (typeof exports !== "undefined") {
exports.Biltong = Biltong;
}
var _isa = function(a) { return Object.prototype.toString.call(a) === "[object Array]"; },
_pointHelper = function(p1, p2, fn) {
p1 = _isa(p1) ? p1 : [p1.x, p1.y];
p2 = _isa(p2) ? p2 : [p2.x, p2.y];
return fn(p1, p2);
},
/**
* @name Biltong.gradient
* @function
* @desc Calculates the gradient of a line between the two points.
* @param {Point} p1 First point, either as a 2 entry array or object with `left` and `top` properties.
* @param {Point} p2 Second point, either as a 2 entry array or object with `left` and `top` properties.
* @return {Float} The gradient of a line between the two points.
*/
_gradient = Biltong.gradient = function(p1, p2) {
return _pointHelper(p1, p2, function(_p1, _p2) {
if (_p2[0] == _p1[0])
return _p2[1] > _p1[1] ? Infinity : -Infinity;
else if (_p2[1] == _p1[1])
return _p2[0] > _p1[0] ? 0 : -0;
else
return (_p2[1] - _p1[1]) / (_p2[0] - _p1[0]);
});
},
/**
* @name Biltong.normal
* @function
* @desc Calculates the gradient of a normal to a line between the two points.
* @param {Point} p1 First point, either as a 2 entry array or object with `left` and `top` properties.
* @param {Point} p2 Second point, either as a 2 entry array or object with `left` and `top` properties.
* @return {Float} The gradient of a normal to a line between the two points.
*/
_normal = Biltong.normal = function(p1, p2) {
return -1 / _gradient(p1, p2);
},
/**
* @name Biltong.lineLength
* @function
* @desc Calculates the length of a line between the two points.
* @param {Point} p1 First point, either as a 2 entry array or object with `left` and `top` properties.
* @param {Point} p2 Second point, either as a 2 entry array or object with `left` and `top` properties.
* @return {Float} The length of a line between the two points.
*/
_lineLength = Biltong.lineLength = function(p1, p2) {
return _pointHelper(p1, p2, function(_p1, _p2) {
return Math.sqrt(Math.pow(_p2[1] - _p1[1], 2) + Math.pow(_p2[0] - _p1[0], 2));
});
},
/**
* @name Biltong.quadrant
* @function
* @desc Calculates the quadrant in which the angle between the two points lies.
* @param {Point} p1 First point, either as a 2 entry array or object with `left` and `top` properties.
* @param {Point} p2 Second point, either as a 2 entry array or object with `left` and `top` properties.
* @return {Integer} The quadrant - 1 for upper right, 2 for lower right, 3 for lower left, 4 for upper left.
*/
_quadrant = Biltong.quadrant = function(p1, p2) {
return _pointHelper(p1, p2, function(_p1, _p2) {
if (_p2[0] > _p1[0]) {
return (_p2[1] > _p1[1]) ? 2 : 1;
}
else if (_p2[0] == _p1[0]) {
return _p2[1] > _p1[1] ? 2 : 1;
}
else {
return (_p2[1] > _p1[1]) ? 3 : 4;
}
});
},
/**
* @name Biltong.theta
* @function
* @desc Calculates the angle between the two points.
* @param {Point} p1 First point, either as a 2 entry array or object with `left` and `top` properties.
* @param {Point} p2 Second point, either as a 2 entry array or object with `left` and `top` properties.
* @return {Float} The angle between the two points.
*/
_theta = Biltong.theta = function(p1, p2) {
return _pointHelper(p1, p2, function(_p1, _p2) {
var m = _gradient(_p1, _p2),
t = Math.atan(m),
s = _quadrant(_p1, _p2);
if ((s == 4 || s== 3)) t += Math.PI;
if (t < 0) t += (2 * Math.PI);
return t;
});
},
/**
* @name Biltong.intersects
* @function
* @desc Calculates whether or not the two rectangles intersect.
* @param {Rectangle} r1 First rectangle, as a js object in the form `{x:.., y:.., w:.., h:..}`
* @param {Rectangle} r2 Second rectangle, as a js object in the form `{x:.., y:.., w:.., h:..}`
* @return {Boolean} True if the rectangles intersect, false otherwise.
*/
_intersects = Biltong.intersects = function(r1, r2) {
var x1 = r1.x, x2 = r1.x + r1.w, y1 = r1.y, y2 = r1.y + r1.h,
a1 = r2.x, a2 = r2.x + r2.w, b1 = r2.y, b2 = r2.y + r2.h;
return ( (x1 <= a1 && a1 <= x2) && (y1 <= b1 && b1 <= y2) ) ||
( (x1 <= a2 && a2 <= x2) && (y1 <= b1 && b1 <= y2) ) ||
( (x1 <= a1 && a1 <= x2) && (y1 <= b2 && b2 <= y2) ) ||
( (x1 <= a2 && a1 <= x2) && (y1 <= b2 && b2 <= y2) ) ||
( (a1 <= x1 && x1 <= a2) && (b1 <= y1 && y1 <= b2) ) ||
( (a1 <= x2 && x2 <= a2) && (b1 <= y1 && y1 <= b2) ) ||
( (a1 <= x1 && x1 <= a2) && (b1 <= y2 && y2 <= b2) ) ||
( (a1 <= x2 && x1 <= a2) && (b1 <= y2 && y2 <= b2) );
},
/**
* @name Biltong.encloses
* @function
* @desc Calculates whether or not r2 is completely enclosed by r1.
* @param {Rectangle} r1 First rectangle, as a js object in the form `{x:.., y:.., w:.., h:..}`
* @param {Rectangle} r2 Second rectangle, as a js object in the form `{x:.., y:.., w:.., h:..}`
* @param {Boolean} [allowSharedEdges=false] If true, the concept of enclosure allows for one or more edges to be shared by the two rectangles.
* @return {Boolean} True if r1 encloses r2, false otherwise.
*/
_encloses = Biltong.encloses = function(r1, r2, allowSharedEdges) {
var x1 = r1.x, x2 = r1.x + r1.w, y1 = r1.y, y2 = r1.y + r1.h,
a1 = r2.x, a2 = r2.x + r2.w, b1 = r2.y, b2 = r2.y + r2.h,
c = function(v1, v2, v3, v4) { return allowSharedEdges ? v1 <= v2 && v3>= v4 : v1 < v2 && v3 > v4; };
return c(x1,a1,x2,a2) && c(y1,b1,y2,b2);
},
_segmentMultipliers = [null, [1, -1], [1, 1], [-1, 1], [-1, -1] ],
_inverseSegmentMultipliers = [null, [-1, -1], [-1, 1], [1, 1], [1, -1] ],
/**
* @name Biltong.pointOnLine
* @function
* @desc Calculates a point on the line from `fromPoint` to `toPoint` that is `distance` units along the length of the line.
* @param {Point} p1 First point, either as a 2 entry array or object with `left` and `top` properties.
* @param {Point} p2 Second point, either as a 2 entry array or object with `left` and `top` properties.
* @return {Point} Point on the line, in the form `{ x:..., y:... }`.
*/
_pointOnLine = Biltong.pointOnLine = function(fromPoint, toPoint, distance) {
var m = _gradient(fromPoint, toPoint),
s = _quadrant(fromPoint, toPoint),
segmentMultiplier = distance > 0 ? _segmentMultipliers[s] : _inverseSegmentMultipliers[s],
theta = Math.atan(m),
y = Math.abs(distance * Math.sin(theta)) * segmentMultiplier[1],
x = Math.abs(distance * Math.cos(theta)) * segmentMultiplier[0];
return { x:fromPoint.x + x, y:fromPoint.y + y };
},
/**
* @name Biltong.perpendicularLineTo
* @function
* @desc Calculates a line of length `length` that is perpendicular to the line from `fromPoint` to `toPoint` and passes through `toPoint`.
* @param {Point} p1 First point, either as a 2 entry array or object with `left` and `top` properties.
* @param {Point} p2 Second point, either as a 2 entry array or object with `left` and `top` properties.
* @return {Line} Perpendicular line, in the form `[ { x:..., y:... }, { x:..., y:... } ]`.
*/
_perpendicularLineTo = Biltong.perpendicularLineTo = function(fromPoint, toPoint, length) {
var m = _gradient(fromPoint, toPoint),
theta2 = Math.atan(-1 / m),
y = length / 2 * Math.sin(theta2),
x = length / 2 * Math.cos(theta2);
return [{x:toPoint.x + x, y:toPoint.y + y}, {x:toPoint.x - x, y:toPoint.y - y}];
};
}).call(typeof window !== 'undefined' ? window : this);
;
(function () {
"use strict";
/**
* Creates a Touch object.
* @param view
* @param target
* @param pageX
* @param pageY
* @param screenX
* @param screenY
* @param clientX
* @param clientY
* @returns {Touch}
* @private
*/
function _touch(view, target, pageX, pageY, screenX, screenY, clientX, clientY) {
return new Touch({
target:target,
identifier:_uuid(),
pageX: pageX,
pageY: pageY,
screenX: screenX,
screenY: screenY,
clientX: clientX || screenX,
clientY: clientY || screenY
});
}
/**
* Create a synthetic touch list from the given list of Touch objects.
* @returns {Array}
* @private
*/
function _touchList() {
var list = [];
Array.prototype.push.apply(list, arguments);
list.item = function(index) { return this[index]; };
return list;
}
/**
* Create a Touch object and then insert it into a synthetic touch list, returning the list.s
* @param view
* @param target
* @param pageX
* @param pageY
* @param screenX
* @param screenY
* @param clientX
* @param clientY
* @returns {Array}
* @private
*/
function _touchAndList(view, target, pageX, pageY, screenX, screenY, clientX, clientY) {
return _touchList(_touch.apply(null, arguments));
}
var root = this,
matchesSelector = function (el, selector, ctx) {
ctx = ctx || el.parentNode;
var possibles = ctx.querySelectorAll(selector);
for (var i = 0; i < possibles.length; i++) {
if (possibles[i] === el) {
return true;
}
}
return false;
},
_gel = function (el) {
return (typeof el == "string" || el.constructor === String) ? document.getElementById(el) : el;
},
_t = function (e) {
return e.srcElement || e.target;
},
//
// gets path info for the given event - the path from target to obj, in the event's bubble chain. if doCompute
// is false we just return target for the path.
//
_pi = function(e, target, obj, doCompute) {
if (!doCompute) return { path:[target], end:1 };
else if (typeof e.path !== "undefined" && e.path.indexOf) {
return { path: e.path, end: e.path.indexOf(obj) };
} else {
var out = { path:[], end:-1 }, _one = function(el) {
out.path.push(el);
if (el === obj) {
out.end = out.path.length - 1;
}
else if (el.parentNode != null) {
_one(el.parentNode)
}
};
_one(target);
return out;
}
},
_d = function (l, fn) {
for (var i = 0, j = l.length; i < j; i++) {
if (l[i] == fn) break;
}
if (i < l.length) l.splice(i, 1);
},
guid = 1,
//
// this function generates a guid for every handler, sets it on the handler, then adds
// it to the associated object's map of handlers for the given event. this is what enables us
// to unbind all events of some type, or all events (the second of which can be requested by the user,
// but it also used by Mottle when an element is removed.)
_store = function (obj, event, fn) {
var g = guid++;
obj.__ta = obj.__ta || {};
obj.__ta[event] = obj.__ta[event] || {};
// store each handler with a unique guid.
obj.__ta[event][g] = fn;
// set the guid on the handler.
fn.__tauid = g;
return g;
},
_unstore = function (obj, event, fn) {
obj.__ta && obj.__ta[event] && delete obj.__ta[event][fn.__tauid];
// a handler might have attached extra functions, so we unbind those too.
if (fn.__taExtra) {
for (var i = 0; i < fn.__taExtra.length; i++) {
_unbind(obj, fn.__taExtra[i][0], fn.__taExtra[i][1]);
}
fn.__taExtra.length = 0;
}
// a handler might have attached an unstore callback
fn.__taUnstore && fn.__taUnstore();
},
_curryChildFilter = function (children, obj, fn, evt) {
if (children == null) return fn;
else {
var c = children.split(","),
_fn = function (e) {
_fn.__tauid = fn.__tauid;
var t = _t(e), target = t; // t is the target element on which the event occurred. it is the
// element we will wish to pass to any callbacks.
var pathInfo = _pi(e, t, obj, children != null)
if (pathInfo.end != -1) {
for (var p = 0; p < pathInfo.end; p++) {
target = pathInfo.path[p];
for (var i = 0; i < c.length; i++) {
if (matchesSelector(target, c[i], obj)) {
fn.apply(target, arguments);
}
}
}
}
};
registerExtraFunction(fn, evt, _fn);
return _fn;
}
},
//
// registers an 'extra' function on some event listener function we were given - a function that we
// created and bound to the element as part of our housekeeping, and which we want to unbind and remove
// whenever the given function is unbound.
registerExtraFunction = function (fn, evt, newFn) {
fn.__taExtra = fn.__taExtra || [];
fn.__taExtra.push([evt, newFn]);
},
DefaultHandler = function (obj, evt, fn, children) {
if (isTouchDevice && touchMap[evt]) {
var tfn = _curryChildFilter(children, obj, fn, touchMap[evt]);
_bind(obj, touchMap[evt], tfn , fn);
}
if (evt === "focus" && obj.getAttribute("tabindex") == null) {
obj.setAttribute("tabindex", "1");
}
_bind(obj, evt, _curryChildFilter(children, obj, fn, evt), fn);
},
SmartClickHandler = function (obj, evt, fn, children) {
if (obj.__taSmartClicks == null) {
var down = function (e) {
obj.__tad = _pageLocation(e);
},
up = function (e) {
obj.__tau = _pageLocation(e);
},
click = function (e) {
if (obj.__tad && obj.__tau && obj.__tad[0] === obj.__tau[0] && obj.__tad[1] === obj.__tau[1]) {
for (var i = 0; i < obj.__taSmartClicks.length; i++)
obj.__taSmartClicks[i].apply(_t(e), [ e ]);
}
};
DefaultHandler(obj, "mousedown", down, children);
DefaultHandler(obj, "mouseup", up, children);
DefaultHandler(obj, "click", click, children);
obj.__taSmartClicks = [];
}
// store in the list of callbacks
obj.__taSmartClicks.push(fn);
// the unstore function removes this function from the object's listener list for this type.
fn.__taUnstore = function () {
_d(obj.__taSmartClicks, fn);
};
},
_tapProfiles = {
"tap": {touches: 1, taps: 1},
"dbltap": {touches: 1, taps: 2},
"contextmenu": {touches: 2, taps: 1}
},
TapHandler = function (clickThreshold, dblClickThreshold) {
return function (obj, evt, fn, children) {
// if event is contextmenu, for devices which are mouse only, we want to
// use the default bind.
if (evt == "contextmenu" && isMouseDevice)
DefaultHandler(obj, evt, fn, children);
else {
// the issue here is that this down handler gets registered only for the
// child nodes in the first registration. in fact it should be registered with
// no child selector and then on down we should cycle through the registered
// functions to see if one of them matches. on mouseup we should execute ALL of
// the functions whose children are either null or match the element.
if (obj.__taTapHandler == null) {
var tt = obj.__taTapHandler = {
tap: [],
dbltap: [],
contextmenu: [],
down: false,
taps: 0,
downSelectors: []
};
var down = function (e) {
var target = _t(e), pathInfo = _pi(e, target, obj, children != null), finished = false;
for (var p = 0; p < pathInfo.end; p++) {
if (finished) return;
target = pathInfo.path[p];
for (var i = 0; i < tt.downSelectors.length; i++) {
if (tt.downSelectors[i] == null || matchesSelector(target, tt.downSelectors[i], obj)) {
tt.down = true;
setTimeout(clearSingle, clickThreshold);
setTimeout(clearDouble, dblClickThreshold);
finished = true;
break; // we only need one match on mousedown
}
}
}
},
up = function (e) {
if (tt.down) {
var target = _t(e), currentTarget, pathInfo;
tt.taps++;
var tc = _touchCount(e);
for (var eventId in _tapProfiles) {
if (_tapProfiles.hasOwnProperty(eventId)) {
var p = _tapProfiles[eventId];
if (p.touches === tc && (p.taps === 1 || p.taps === tt.taps)) {
for (var i = 0; i < tt[eventId].length; i++) {
pathInfo = _pi(e, target, obj, tt[eventId][i][1] != null);
for (var pLoop = 0; pLoop < pathInfo.end; pLoop++) {
currentTarget = pathInfo.path[pLoop];
// this is a single event registration handler.
if (tt[eventId][i][1] == null || matchesSelector(currentTarget, tt[eventId][i][1], obj)) {
tt[eventId][i][0].apply(currentTarget, [ e ]);
break;
}
}
}
}
}
}
}
},
clearSingle = function () {
tt.down = false;
},
clearDouble = function () {
tt.taps = 0;
};
DefaultHandler(obj, "mousedown", down);
DefaultHandler(obj, "mouseup", up);
}
// add this child selector (it can be null, that's fine).
obj.__taTapHandler.downSelectors.push(children);
obj.__taTapHandler[evt].push([fn, children]);
// the unstore function removes this function from the object's listener list for this type.
fn.__taUnstore = function () {
_d(obj.__taTapHandler[evt], fn);
};
}
};
},
meeHelper = function (type, evt, obj, target) {
for (var i in obj.__tamee[type]) {
if (obj.__tamee[type].hasOwnProperty(i)) {
obj.__tamee[type][i].apply(target, [ evt ]);
}
}
},
MouseEnterExitHandler = function () {
var activeElements = [];
return function (obj, evt, fn, children) {
if (!obj.__tamee) {
// __tamee holds a flag saying whether the mouse is currently "in" the element, and a list of
// both mouseenter and mouseexit functions.
obj.__tamee = { over: false, mouseenter: [], mouseexit: [] };
// register over and out functions
var over = function (e) {
var t = _t(e);
if ((children == null && (t == obj && !obj.__tamee.over)) || (matchesSelector(t, children, obj) && (t.__tamee == null || !t.__tamee.over))) {
meeHelper("mouseenter", e, obj, t);
t.__tamee = t.__tamee || {};
t.__tamee.over = true;
activeElements.push(t);
}
},
out = function (e) {
var t = _t(e);
// is the current target one of the activeElements? and is the
// related target NOT a descendant of it?
for (var i = 0; i < activeElements.length; i++) {
if (t == activeElements[i] && !matchesSelector((e.relatedTarget || e.toElement), "*", t)) {
t.__tamee.over = false;
activeElements.splice(i, 1);
meeHelper("mouseexit", e, obj, t);
}
}
};
_bind(obj, "mouseover", _curryChildFilter(children, obj, over, "mouseover"), over);
_bind(obj, "mouseout", _curryChildFilter(children, obj, out, "mouseout"), out);
}
fn.__taUnstore = function () {
delete obj.__tamee[evt][fn.__tauid];
};
_store(obj, evt, fn);
obj.__tamee[evt][fn.__tauid] = fn;
};
},
isTouchDevice = "ontouchstart" in document.documentElement || navigator.maxTouchPoints,
isMouseDevice = "onmousedown" in document.documentElement,
touchMap = { "mousedown": "touchstart", "mouseup": "touchend", "mousemove": "touchmove" },
touchstart = "touchstart", touchend = "touchend", touchmove = "touchmove",
iev = (function () {
var rv = -1;
if (navigator.appName == 'Microsoft Internet Explorer') {
var ua = navigator.userAgent,
re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
if (re.exec(ua) != null)
rv = parseFloat(RegExp.$1);
}
return rv;
})(),
isIELT9 = iev > -1 && iev < 9,
_genLoc = function (e, prefix) {
if (e == null) return [ 0, 0 ];
var ts = _touches(e), t = _getTouch(ts, 0);
return [t[prefix + "X"], t[pref