Add geometry.js library, to handle rectangles and points

This commit is contained in:
mossroy 2013-10-21 13:25:43 +02:00
parent ce7ffa3c9e
commit 21f6efa897
2 changed files with 590 additions and 0 deletions

555
js/lib/geometry.js Normal file
View File

@ -0,0 +1,555 @@
/* 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/. */
// Geometry library.
// (c) 2011-2013 client IO
// Copied from https://github.com/DavidDurman/joint/blob/master/src/geometry.js
define(function(require) {
// Declare shorthands to the most used math functions.
var math = Math;
var abs = math.abs;
var cos = math.cos;
var sin = math.sin;
var sqrt = math.sqrt;
var mmin = math.min;
var mmax = math.max;
var atan = math.atan;
var atan2 = math.atan2;
var acos = math.acos;
var round = math.round;
var floor = math.floor;
var PI = math.PI;
var random = math.random;
var toDeg = function(rad) { return (180*rad / PI) % 360; };
var toRad = function(deg) { return (deg % 360) * PI / 180; };
var snapToGrid = function(val, gridSize) { return gridSize * Math.round(val/gridSize); };
var normalizeAngle = function(angle) { return (angle % 360) + (angle < 0 ? 360 : 0); };
// Point
// -----
// 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))`
function point(x, y) {
if (!(this instanceof point))
return new point(x, y);
var xy;
if (y === undefined && Object(x) !== x) {
xy = x.split(_.indexOf(x, "@") === -1 ? " " : "@");
this.x = parseInt(xy[0], 10);
this.y = parseInt(xy[1], 10);
} else if (Object(x) === x) {
this.x = x.x;
this.y = x.y;
} else {
this.x = x;
this.y = y;
}
}
point.prototype = {
toString: function() {
return this.x + "@" + this.y;
},
// 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 = mmin(mmax(this.x, r.x), r.x + r.width);
this.y = mmin(mmax(this.y, r.y), r.y + r.height);
return this;
},
// 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 = point(p);
// Invert the y-axis.
var y = -(p.y - this.y);
var x = p.x - this.x;
// Makes sure that the comparison with zero takes rounding errors into account.
var PRECISION = 10;
// Note that `atan2` is not defined for `x`, `y` both equal zero.
var rad = (y.toFixed(PRECISION) == 0 && x.toFixed(PRECISION) == 0) ? 0 : atan2(y, x);
// Correction for III. and IV. quadrant.
if (rad < 0) {
rad = 2*PI + rad;
}
return 180*rad / PI;
},
// Returns distance between me and point `p`.
distance: function(p) {
return line(this, p).length();
},
// 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);
},
// Offset me by the specified amount.
offset: function(dx, dy) {
this.x += dx || 0;
this.y += dy || 0;
return this;
},
magnitude: function() {
return sqrt((this.x*this.x) + (this.y*this.y)) || 0.01;
},
update: function(x, y) {
this.x = x || 0;
this.y = y || 0;
return this;
},
round: function(decimals) {
this.x = decimals ? this.x.toFixed(decimals) : round(this.x);
this.y = decimals ? this.y.toFixed(decimals) : round(this.y);
return this;
},
// Scale the line segment between (0,0) and me to have a length of len.
normalize: function(len) {
var s = (len || 1) / this.magnitude();
this.x = s * this.x;
this.y = s * this.y;
return this;
},
difference: function(p) {
return point(this.x - p.x, this.y - p.y);
},
// Converts rectangular to polar coordinates.
// An origin can be specified, otherwise it's 0@0.
toPolar: function(o) {
o = (o && point(o)) || 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(point(x,y)));
return this;
},
// Rotate point by angle around origin o.
rotate: function(o, angle) {
angle = (angle + 360) % 360;
this.toPolar(o);
this.y += toRad(angle);
var p = point.fromPolar(this.x, this.y, o);
this.x = p.x;
this.y = p.y;
return this;
},
// Move point on line starting from ref ending at me by
// distance distance.
move: function(ref, distance) {
var theta = toRad(point(ref).theta(this));
return this.offset(cos(theta) * distance, -sin(theta) * distance);
},
// 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 point(this).offset(-dx, -dy).theta(ref) - this.theta(ref);
},
equals: function(p) {
return this.x === p.x && this.y === p.y;
}
};
// Alternative constructor, from polar coordinates.
// @param {number} r Distance.
// @param {number} angle Angle in radians.
// @param {point} [optional] o Origin.
point.fromPolar = function(r, angle, o) {
o = (o && point(o)) || point(0,0);
var x = abs(r * cos(angle));
var y = abs(r * sin(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 point(o.x + x, o.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 point(floor(random() * (x2 - x1 + 1) + x1), floor(random() * (y2 - y1 + 1) + y1));
};
// Line.
// -----
function line(p1, p2) {
if (!(this instanceof line))
return new line(p1, p2);
this.start = point(p1);
this.end = point(p2);
}
line.prototype = {
toString: function() {
return this.start.toString() + ' ' + this.end.toString();
},
// @return {double} length of the line
length: function() {
return sqrt(this.squaredLength());
},
// @return {integer} length without sqrt
// @note for applications where the exact length is not necessary (e.g. compare only)
squaredLength: function() {
var x0 = this.start.x;
var y0 = this.start.y;
var x1 = this.end.x;
var y1 = this.end.y;
return (x0 -= x1)*x0 + (y0 -= y1)*y0;
},
// @return {point} my midpoint
midpoint: function() {
return point((this.start.x + this.end.x) / 2,
(this.start.y + this.end.y) / 2);
},
// @return {point} Point where I'm intersecting l.
// @see Squeak Smalltalk, LineSegment>>intersectionWith:
intersection: function(l) {
var pt1Dir = point(this.end.x - this.start.x, this.end.y - this.start.y);
var pt2Dir = point(l.end.x - l.start.x, l.end.y - l.start.y);
var det = (pt1Dir.x * pt2Dir.y) - (pt1Dir.y * pt2Dir.x);
var deltaPt = point(l.start.x - this.start.x, l.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 point(this.start.x + (alpha * pt1Dir.x / det),
this.start.y + (alpha * pt1Dir.y / det));
}
};
// Rectangle.
// ----------
function rect(x, y, w, h) {
if (!(this instanceof rect))
return new rect(x, y, w, h);
if (y === undefined) {
y = x.y;
w = x.width;
h = x.height;
x = x.x;
}
this.x = x;
this.y = y;
this.width = w;
this.height = h;
}
rect.prototype = {
toString: function() {
return this.origin().toString() + ' ' + this.corner().toString();
},
origin: function() {
return point(this.x, this.y);
},
corner: function() {
return point(this.x + this.width, this.y + this.height);
},
topRight: function() {
return point(this.x + this.width, this.y);
},
bottomLeft: function() {
return point(this.x, this.y + this.height);
},
center: function() {
return point(this.x + this.width/2, this.y + this.height/2);
},
// @return {boolean} true if rectangles intersect
intersect: function(r) {
var myOrigin = this.origin();
var myCorner = this.corner();
var rOrigin = r.origin();
var rCorner = r.corner();
if (rCorner.x <= myOrigin.x ||
rCorner.y <= myOrigin.y ||
rOrigin.x >= myCorner.x ||
rOrigin.y >= myCorner.y) return false;
return true;
},
// @return {string} (left|right|top|bottom) side which is nearest to point
// @see Squeak Smalltalk, Rectangle>>sideNearestTo:
sideNearestToPoint: function(p) {
p = point(p);
var distToLeft = p.x - this.x;
var distToRight = (this.x + this.width) - p.x;
var distToTop = p.y - this.y;
var distToBottom = (this.y + this.height) - p.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;
},
// @return {bool} true if point p is insight me
containsPoint: function(p) {
p = point(p);
if (p.x > this.x && p.x < this.x + this.width &&
p.y > this.y && p.y < this.y + this.height) {
return true;
}
return false;
},
// @return {point} a point on my boundary nearest to p
// @see Squeak Smalltalk, Rectangle>>pointNearestTo:
pointNearestToPoint: function(p) {
p = point(p);
if (this.containsPoint(p)) {
var side = this.sideNearestToPoint(p);
switch (side){
case "right": return point(this.x + this.width, p.y);
case "left": return point(this.x, p.y);
case "bottom": return point(p.x, this.y + this.height);
case "top": return point(p.x, this.y);
}
}
return p.adhereToRect(this);
},
// 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 = point(p);
var center = 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 = [
line(this.origin(), this.topRight()),
line(this.topRight(), this.corner()),
line(this.corner(), this.bottomLeft()),
line(this.bottomLeft(), this.origin())
];
var connector = 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;
},
// Move and expand me.
// @param r {rectangle} representing deltas
moveAndExpand: function(r) {
this.x += r.x;
this.y += r.y;
this.width += r.width;
this.height += r.height;
return this;
},
round: function(decimals) {
this.x = decimals ? this.x.toFixed(decimals) : round(this.x);
this.y = decimals ? this.y.toFixed(decimals) : round(this.y);
this.width = decimals ? this.width.toFixed(decimals) : round(this.width);
this.height = decimals ? this.height.toFixed(decimals) : round(this.height);
return this;
}
};
// Ellipse.
// --------
function ellipse(c, a, b) {
if (!(this instanceof ellipse))
return new ellipse(c, a, b);
c = point(c);
this.x = c.x;
this.y = c.y;
this.a = a;
this.b = b;
}
ellipse.prototype = {
toString: function() {
return point(this.x, this.y).toString() + ' ' + this.a + ' ' + this.b;
},
bbox: function() {
return rect(this.x - this.a, this.y - this.b, 2*this.a, 2*this.b);
},
// 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 = point(p);
if (angle) p.rotate(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(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 / aSquared) + (mSquared / bSquared)));
x = dx < 0 ? -x : x;
var y = m * x;
result = point(this.x + x, this.y + y);
if (angle) return result.rotate(point(this.x, this.y), -angle);
return result;
}
};
// Bezier curve.
// -------------
var bezier = {
// Cubic Bezier curve path through points.
// Ported from C# implementation by Oleg V. Polikarpotchkin and Peter Lee (http://www.codeproject.com/KB/graphics/BezierSpline.aspx).
// @param {array} points Array of points through which the smooth line will go.
// @return {array} SVG Path commands as an array
curveThroughPoints: function(points) {
var controlPoints = this.getCurveControlPoints(points);
var path = ['M', points[0].x, points[0].y];
for (var i = 0; i < controlPoints[0].length; i++) {
path.push('C', controlPoints[0][i].x, controlPoints[0][i].y, controlPoints[1][i].x, controlPoints[1][i].y, points[i+1].x, points[i+1].y);
}
return path;
},
// Get open-ended Bezier Spline Control Points.
// @param knots Input Knot Bezier spline points (At least two points!).
// @param firstControlPoints Output First Control points. Array of knots.length - 1 length.
// @param secondControlPoints Output Second Control points. Array of knots.length - 1 length.
getCurveControlPoints: function(knots) {
var firstControlPoints = [];
var secondControlPoints = [];
var n = knots.length - 1;
var i;
// Special case: Bezier curve should be a straight line.
if (n == 1) {
// 3P1 = 2P0 + P3
firstControlPoints[0] = point((2 * knots[0].x + knots[1].x) / 3,
(2 * knots[0].y + knots[1].y) / 3);
// P2 = 2P1 P0
secondControlPoints[0] = point(2 * firstControlPoints[0].x - knots[0].x,
2 * firstControlPoints[0].y - knots[0].y);
return [firstControlPoints, secondControlPoints];
}
// Calculate first Bezier control points.
// Right hand side vector.
var rhs = [];
// Set right hand side X values.
for (i = 1; i < n - 1; i++) {
rhs[i] = 4 * knots[i].x + 2 * knots[i + 1].x;
}
rhs[0] = knots[0].x + 2 * knots[1].x;
rhs[n - 1] = (8 * knots[n - 1].x + knots[n].x) / 2.0;
// Get first control points X-values.
var x = this.getFirstControlPoints(rhs);
// Set right hand side Y values.
for (i = 1; i < n - 1; ++i) {
rhs[i] = 4 * knots[i].y + 2 * knots[i + 1].y;
}
rhs[0] = knots[0].y + 2 * knots[1].y;
rhs[n - 1] = (8 * knots[n - 1].y + knots[n].y) / 2.0;
// Get first control points Y-values.
var y = this.getFirstControlPoints(rhs);
// Fill output arrays.
for (i = 0; i < n; i++) {
// First control point.
firstControlPoints.push(point(x[i], y[i]));
// Second control point.
if (i < n - 1) {
secondControlPoints.push(point(2 * knots [i + 1].x - x[i + 1],
2 * knots[i + 1].y - y[i + 1]));
} else {
secondControlPoints.push(point((knots[n].x + x[n - 1]) / 2,
(knots[n].y + y[n - 1]) / 2));
}
}
return [firstControlPoints, secondControlPoints];
},
// Solves a tridiagonal system for one of coordinates (x or y) of first Bezier control points.
// @param rhs Right hand side vector.
// @return Solution vector.
getFirstControlPoints: function(rhs) {
var n = rhs.length;
// `x` is a solution vector.
var x = [];
var tmp = [];
var b = 2.0;
x[0] = rhs[0] / b;
// Decomposition and forward substitution.
for (var i = 1; i < n; i++) {
tmp[i] = 1 / b;
b = (i < n - 1 ? 4.0 : 3.5) - tmp[i];
x[i] = (rhs[i] - x[i - 1]) / b;
}
for (i = 1; i < n; i++) {
// Backsubstitution.
x[n - i - 1] -= tmp[n - i] * x[n - i];
}
return x;
}
};
return {
toDeg: toDeg,
toRad: toRad,
snapToGrid: snapToGrid,
normalizeAngle: normalizeAngle,
point: point,
line: line,
rect: rect,
ellipse: ellipse,
bezier: bezier
};
});

View File

@ -24,6 +24,7 @@ define(function(require) {
var $ = require('jquery');
var evopediaTitle = require('title');
var evopediaArchive = require('archive');
var geometry = require('geometry');
// Due to security restrictions in the browsers,
// we can not read directly the files and run the unit tests
@ -177,5 +178,39 @@ define(function(require) {
localArchive.loadMathImage("edb3069b82c68d270f6642c171cc6293", callbackFunction);
});
module("geometry");
test("check rectangle intersection", function() {
var rect1 = new geometry.rect(0,0,2,2);
var rect2 = new geometry.rect(1,1,2,2);
var rect3 = new geometry.rect(2,2,2,2);
var rect4 = new geometry.rect(1,1,1,1);
var rect5 = new geometry.rect(3,3,2,2);
var rect6 = new geometry.rect(2,0,1,10);
ok(rect1.intersect(rect2), "rect1 intersects rect2");
ok(rect2.intersect(rect1), "rect2 intersects rect1");
ok(rect2.intersect(rect3), "rect1 intersects rect3");
ok(!rect1.intersect(rect3), "rect1 does not intersect rect3");
ok(!rect4.intersect(rect3), "rect4 does not intersect rect3");
ok(rect4.intersect(rect2), "rect4 intersects rect2");
ok(!rect5.intersect(rect1), "rect5 does not intersect rect1");
ok(!rect1.intersect(rect5), "rect1 does not intersect rect5");
ok(rect6.intersect(rect2), "rect6 intersects rect2");
ok(rect6.intersect(rect3), "rect6 intersects rect3");
ok(!rect6.intersect(rect5), "rect6 intersects rect5");
});
test("check rectangle contains a point", function() {
var rect1 = new geometry.rect(2,3,4,5);
var point1 = new geometry.point(1,1);
var point2 = new geometry.point(2,3);
var point3 = new geometry.point(4,4);
var point4 = new geometry.point(7,9);
var point5 = new geometry.point(4,6);
ok(!rect1.containsPoint(point1), "rect1 does not contain point1");
ok(!rect1.containsPoint(point2), "rect1 does not contain point2");
ok(rect1.containsPoint(point3), "rect1 contains point3");
ok(!rect1.containsPoint(point4), "rect1 does not contain point4");
ok(rect1.containsPoint(point5), "rect1 contains point5");
});
};
});