Browse Source

initial commit

master
Yorick van Pelt 5 years ago
commit
f966206486
  1. 50
      LemmingSim.html
  2. 670
      decomp.js
  3. 205
      graphics.js
  4. 4
      jquery-1.11.0.min.js
  5. 663
      lemming_sim_student.js
  6. 9275
      matter.js
  7. 74
      mouse.js
  8. 844
      pathseg.js
  9. 65
      robotbody.svg

50
LemmingSim.html

@ -0,0 +1,50 @@
<!--
Copyright 2016 Harmen de Weerd
Copyright 2017 Johannes Keyser
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title>Lemmings</title>
<script language="JavaScript" src="pathseg.js"></script>
<script language="JavaScript" src="mouse.js"></script>
<script language="JavaScript" src="graphics.js"></script>
<script language="JavaScript" src="lemming_sim_student.js"></script>
<script language="JavaScript" src="matter.js"></script>
<script language="JavaScript" src="jquery-1.11.0.min.js"></script>
<script language="JavaScript" src="decomp.js"></script>
</head>
<body onload="init();">
<object id="robotbodySVG" type="image/svg+xml" data="robotbody.svg" width=0 ></object>
<canvas id="arenaLemming" width="450" height="450" style="cursor: initial;"></canvas>
<br>
<table border="0">
<tbody>
<tr><th colspan="2" align="center">Simulation properties</th><td></td></tr>
<tr><td>Simulation steps: <span id="SimStepLabel"></span></td></tr>
</tr>
<tr><br></tr>
<tr><th colspan="2" align="center">Selected robot info</th></tr>
<tr>
<td colspan="1" align="center">
<canvas id="bayLemming" width="110" height="110" style="border:solid 2px black;"></canvas>
</td>
<td>Sensor values<span id="SensorLabel"></span></td>
</tr>
</tbody></table>
</body>
</html>

670
decomp.js

@ -0,0 +1,670 @@
!function(e){"object"==typeof exports?module.exports=e():"function"==typeof define&&define.amd?define(e):"undefined"!=typeof window?window.decomp=e():"undefined"!=typeof global?global.decomp=e():"undefined"!=typeof self&&(self.decomp=e())}(function(){var define,module,exports;
return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
var Scalar = require('./Scalar');
module.exports = Line;
/**
* Container for line-related functions
* @class Line
*/
function Line(){};
/**
* Compute the intersection between two lines.
* @static
* @method lineInt
* @param {Array} l1 Line vector 1
* @param {Array} l2 Line vector 2
* @param {Number} precision Precision to use when checking if the lines are parallel
* @return {Array} The intersection point.
*/
Line.lineInt = function(l1,l2,precision){
precision = precision || 0;
var i = [0,0]; // point
var a1, b1, c1, a2, b2, c2, det; // scalars
a1 = l1[1][1] - l1[0][1];
b1 = l1[0][0] - l1[1][0];
c1 = a1 * l1[0][0] + b1 * l1[0][1];
a2 = l2[1][1] - l2[0][1];
b2 = l2[0][0] - l2[1][0];
c2 = a2 * l2[0][0] + b2 * l2[0][1];
det = a1 * b2 - a2*b1;
if (!Scalar.eq(det, 0, precision)) { // lines are not parallel
i[0] = (b2 * c1 - b1 * c2) / det;
i[1] = (a1 * c2 - a2 * c1) / det;
}
return i;
};
/**
* Checks if two line segments intersects.
* @method segmentsIntersect
* @param {Array} p1 The start vertex of the first line segment.
* @param {Array} p2 The end vertex of the first line segment.
* @param {Array} q1 The start vertex of the second line segment.
* @param {Array} q2 The end vertex of the second line segment.
* @return {Boolean} True if the two line segments intersect
*/
Line.segmentsIntersect = function(p1, p2, q1, q2){
var dx = p2[0] - p1[0];
var dy = p2[1] - p1[1];
var da = q2[0] - q1[0];
var db = q2[1] - q1[1];
// segments are parallel
if(da*dy - db*dx == 0)
return false;
var s = (dx * (q1[1] - p1[1]) + dy * (p1[0] - q1[0])) / (da * dy - db * dx)
var t = (da * (p1[1] - q1[1]) + db * (q1[0] - p1[0])) / (db * dx - da * dy)
return (s>=0 && s<=1 && t>=0 && t<=1);
};
},{"./Scalar":4}],2:[function(require,module,exports){
module.exports = Point;
/**
* Point related functions
* @class Point
*/
function Point(){};
/**
* Get the area of a triangle spanned by the three given points. Note that the area will be negative if the points are not given in counter-clockwise order.
* @static
* @method area
* @param {Array} a
* @param {Array} b
* @param {Array} c
* @return {Number}
*/
Point.area = function(a,b,c){
return (((b[0] - a[0])*(c[1] - a[1]))-((c[0] - a[0])*(b[1] - a[1])));
};
Point.left = function(a,b,c){
return Point.area(a,b,c) > 0;
};
Point.leftOn = function(a,b,c) {
return Point.area(a, b, c) >= 0;
};
Point.right = function(a,b,c) {
return Point.area(a, b, c) < 0;
};
Point.rightOn = function(a,b,c) {
return Point.area(a, b, c) <= 0;
};
var tmpPoint1 = [],
tmpPoint2 = [];
/**
* Check if three points are collinear
* @method collinear
* @param {Array} a
* @param {Array} b
* @param {Array} c
* @param {Number} [thresholdAngle=0] Threshold angle to use when comparing the vectors. The function will return true if the angle between the resulting vectors is less than this value. Use zero for max precision.
* @return {Boolean}
*/
Point.collinear = function(a,b,c,thresholdAngle) {
if(!thresholdAngle)
return Point.area(a, b, c) == 0;
else {
var ab = tmpPoint1,
bc = tmpPoint2;
ab[0] = b[0]-a[0];
ab[1] = b[1]-a[1];
bc[0] = c[0]-b[0];
bc[1] = c[1]-b[1];
var dot = ab[0]*bc[0] + ab[1]*bc[1],
magA = Math.sqrt(ab[0]*ab[0] + ab[1]*ab[1]),
magB = Math.sqrt(bc[0]*bc[0] + bc[1]*bc[1]),
angle = Math.acos(dot/(magA*magB));
return angle < thresholdAngle;
}
};
Point.sqdist = function(a,b){
var dx = b[0] - a[0];
var dy = b[1] - a[1];
return dx * dx + dy * dy;
};
},{}],3:[function(require,module,exports){
var Line = require("./Line")
, Point = require("./Point")
, Scalar = require("./Scalar")
module.exports = Polygon;
/**
* Polygon class.
* @class Polygon
* @constructor
*/
function Polygon(){
/**
* Vertices that this polygon consists of. An array of array of numbers, example: [[0,0],[1,0],..]
* @property vertices
* @type {Array}
*/
this.vertices = [];
}
/**
* Get a vertex at position i. It does not matter if i is out of bounds, this function will just cycle.
* @method at
* @param {Number} i
* @return {Array}
*/
Polygon.prototype.at = function(i){
var v = this.vertices,
s = v.length;
return v[i < 0 ? i % s + s : i % s];
};
/**
* Get first vertex
* @method first
* @return {Array}
*/
Polygon.prototype.first = function(){
return this.vertices[0];
};
/**
* Get last vertex
* @method last
* @return {Array}
*/
Polygon.prototype.last = function(){
return this.vertices[this.vertices.length-1];
};
/**
* Clear the polygon data
* @method clear
* @return {Array}
*/
Polygon.prototype.clear = function(){
this.vertices.length = 0;
};
/**
* Append points "from" to "to"-1 from an other polygon "poly" onto this one.
* @method append
* @param {Polygon} poly The polygon to get points from.
* @param {Number} from The vertex index in "poly".
* @param {Number} to The end vertex index in "poly". Note that this vertex is NOT included when appending.
* @return {Array}
*/
Polygon.prototype.append = function(poly,from,to){
if(typeof(from) == "undefined") throw new Error("From is not given!");
if(typeof(to) == "undefined") throw new Error("To is not given!");
if(to-1 < from) throw new Error("lol1");
if(to > poly.vertices.length) throw new Error("lol2");
if(from < 0) throw new Error("lol3");
for(var i=from; i<to; i++){
this.vertices.push(poly.vertices[i]);
}
};
/**
* Make sure that the polygon vertices are ordered counter-clockwise.
* @method makeCCW
*/
Polygon.prototype.makeCCW = function(){
var br = 0,
v = this.vertices;
// find bottom right point
for (var i = 1; i < this.vertices.length; ++i) {
if (v[i][1] < v[br][1] || (v[i][1] == v[br][1] && v[i][0] > v[br][0])) {
br = i;
}
}
// reverse poly if clockwise
if (!Point.left(this.at(br - 1), this.at(br), this.at(br + 1))) {
this.reverse();
}
};
/**
* Reverse the vertices in the polygon
* @method reverse
*/
Polygon.prototype.reverse = function(){
var tmp = [];
for(var i=0, N=this.vertices.length; i!==N; i++){
tmp.push(this.vertices.pop());
}
this.vertices = tmp;
};
/**
* Check if a point in the polygon is a reflex point
* @method isReflex
* @param {Number} i
* @return {Boolean}
*/
Polygon.prototype.isReflex = function(i){
return Point.right(this.at(i - 1), this.at(i), this.at(i + 1));
};
var tmpLine1=[],
tmpLine2=[];
/**
* Check if two vertices in the polygon can see each other
* @method canSee
* @param {Number} a Vertex index 1
* @param {Number} b Vertex index 2
* @return {Boolean}
*/
Polygon.prototype.canSee = function(a,b) {
var p, dist, l1=tmpLine1, l2=tmpLine2;
if (Point.leftOn(this.at(a + 1), this.at(a), this.at(b)) && Point.rightOn(this.at(a - 1), this.at(a), this.at(b))) {
return false;
}
dist = Point.sqdist(this.at(a), this.at(b));
for (var i = 0; i !== this.vertices.length; ++i) { // for each edge
if ((i + 1) % this.vertices.length === a || i === a) // ignore incident edges
continue;
if (Point.leftOn(this.at(a), this.at(b), this.at(i + 1)) && Point.rightOn(this.at(a), this.at(b), this.at(i))) { // if diag intersects an edge
l1[0] = this.at(a);
l1[1] = this.at(b);
l2[0] = this.at(i);
l2[1] = this.at(i + 1);
p = Line.lineInt(l1,l2);
if (Point.sqdist(this.at(a), p) < dist) { // if edge is blocking visibility to b
return false;
}
}
}
return true;
};
/**
* Copy the polygon from vertex i to vertex j.
* @method copy
* @param {Number} i
* @param {Number} j
* @param {Polygon} [targetPoly] Optional target polygon to save in.
* @return {Polygon} The resulting copy.
*/
Polygon.prototype.copy = function(i,j,targetPoly){
var p = targetPoly || new Polygon();
p.clear();
if (i < j) {
// Insert all vertices from i to j
for(var k=i; k<=j; k++)
p.vertices.push(this.vertices[k]);
} else {
// Insert vertices 0 to j
for(var k=0; k<=j; k++)
p.vertices.push(this.vertices[k]);
// Insert vertices i to end
for(var k=i; k<this.vertices.length; k++)
p.vertices.push(this.vertices[k]);
}
return p;
};
/**
* Decomposes the polygon into convex pieces. Returns a list of edges [[p1,p2],[p2,p3],...] that cuts the polygon.
* Note that this algorithm has complexity O(N^4) and will be very slow for polygons with many vertices.
* @method getCutEdges
* @return {Array}
*/
Polygon.prototype.getCutEdges = function() {
var min=[], tmp1=[], tmp2=[], tmpPoly = new Polygon();
var nDiags = Number.MAX_VALUE;
for (var i = 0; i < this.vertices.length; ++i) {
if (this.isReflex(i)) {
for (var j = 0; j < this.vertices.length; ++j) {
if (this.canSee(i, j)) {
tmp1 = this.copy(i, j, tmpPoly).getCutEdges();
tmp2 = this.copy(j, i, tmpPoly).getCutEdges();
for(var k=0; k<tmp2.length; k++)
tmp1.push(tmp2[k]);
if (tmp1.length < nDiags) {
min = tmp1;
nDiags = tmp1.length;
min.push([this.at(i), this.at(j)]);
}
}
}
}
}
return min;
};
/**
* Decomposes the polygon into one or more convex sub-Polygons.
* @method decomp
* @return {Array} An array or Polygon objects.
*/
Polygon.prototype.decomp = function(){
var edges = this.getCutEdges();
if(edges.length > 0)
return this.slice(edges);
else
return [this];
};
/**
* Slices the polygon given one or more cut edges. If given one, this function will return two polygons (false on failure). If many, an array of polygons.
* @method slice
* @param {Array} cutEdges A list of edges, as returned by .getCutEdges()
* @return {Array}
*/
Polygon.prototype.slice = function(cutEdges){
if(cutEdges.length == 0) return [this];
if(cutEdges instanceof Array && cutEdges.length && cutEdges[0] instanceof Array && cutEdges[0].length==2 && cutEdges[0][0] instanceof Array){
var polys = [this];
for(var i=0; i<cutEdges.length; i++){
var cutEdge = cutEdges[i];
// Cut all polys
for(var j=0; j<polys.length; j++){
var poly = polys[j];
var result = poly.slice(cutEdge);
if(result){
// Found poly! Cut and quit
polys.splice(j,1);
polys.push(result[0],result[1]);
break;
}
}
}
return polys;
} else {
// Was given one edge
var cutEdge = cutEdges;
var i = this.vertices.indexOf(cutEdge[0]);
var j = this.vertices.indexOf(cutEdge[1]);
if(i != -1 && j != -1){
return [this.copy(i,j),
this.copy(j,i)];
} else {
return false;
}
}
};
/**
* Checks that the line segments of this polygon do not intersect each other.
* @method isSimple
* @param {Array} path An array of vertices e.g. [[0,0],[0,1],...]
* @return {Boolean}
* @todo Should it check all segments with all others?
*/
Polygon.prototype.isSimple = function(){
var path = this.vertices;
// Check
for(var i=0; i<path.length-1; i++){
for(var j=0; j<i-1; j++){
if(Line.segmentsIntersect(path[i], path[i+1], path[j], path[j+1] )){
return false;
}
}
}
// Check the segment between the last and the first point to all others
for(var i=1; i<path.length-2; i++){
if(Line.segmentsIntersect(path[0], path[path.length-1], path[i], path[i+1] )){
return false;
}
}
return true;
};
function getIntersectionPoint(p1, p2, q1, q2, delta){
delta = delta || 0;
var a1 = p2[1] - p1[1];
var b1 = p1[0] - p2[0];
var c1 = (a1 * p1[0]) + (b1 * p1[1]);
var a2 = q2[1] - q1[1];
var b2 = q1[0] - q2[0];
var c2 = (a2 * q1[0]) + (b2 * q1[1]);
var det = (a1 * b2) - (a2 * b1);
if(!Scalar.eq(det,0,delta))
return [((b2 * c1) - (b1 * c2)) / det, ((a1 * c2) - (a2 * c1)) / det]
else
return [0,0]
}
/**
* Quickly decompose the Polygon into convex sub-polygons.
* @method quickDecomp
* @param {Array} result
* @param {Array} [reflexVertices]
* @param {Array} [steinerPoints]
* @param {Number} [delta]
* @param {Number} [maxlevel]
* @param {Number} [level]
* @return {Array}
*/
Polygon.prototype.quickDecomp = function(result,reflexVertices,steinerPoints,delta,maxlevel,level){
maxlevel = maxlevel || 100;
level = level || 0;
delta = delta || 25;
result = typeof(result)!="undefined" ? result : [];
reflexVertices = reflexVertices || [];
steinerPoints = steinerPoints || [];
var upperInt=[0,0], lowerInt=[0,0], p=[0,0]; // Points
var upperDist=0, lowerDist=0, d=0, closestDist=0; // scalars
var upperIndex=0, lowerIndex=0, closestIndex=0; // Integers
var lowerPoly=new Polygon(), upperPoly=new Polygon(); // polygons
var poly = this,
v = this.vertices;
if(v.length < 3) return result;
level++;
if(level > maxlevel){
console.warn("quickDecomp: max level ("+maxlevel+") reached.");
return result;
}
for (var i = 0; i < this.vertices.length; ++i) {
if (poly.isReflex(i)) {
reflexVertices.push(poly.vertices[i]);
upperDist = lowerDist = Number.MAX_VALUE;
for (var j = 0; j < this.vertices.length; ++j) {
if (Point.left(poly.at(i - 1), poly.at(i), poly.at(j))
&& Point.rightOn(poly.at(i - 1), poly.at(i), poly.at(j - 1))) { // if line intersects with an edge
p = getIntersectionPoint(poly.at(i - 1), poly.at(i), poly.at(j), poly.at(j - 1)); // find the point of intersection
if (Point.right(poly.at(i + 1), poly.at(i), p)) { // make sure it's inside the poly
d = Point.sqdist(poly.vertices[i], p);
if (d < lowerDist) { // keep only the closest intersection
lowerDist = d;
lowerInt = p;
lowerIndex = j;
}
}
}
if (Point.left(poly.at(i + 1), poly.at(i), poly.at(j + 1))
&& Point.rightOn(poly.at(i + 1), poly.at(i), poly.at(j))) {
p = getIntersectionPoint(poly.at(i + 1), poly.at(i), poly.at(j), poly.at(j + 1));
if (Point.left(poly.at(i - 1), poly.at(i), p)) {
d = Point.sqdist(poly.vertices[i], p);
if (d < upperDist) {
upperDist = d;
upperInt = p;
upperIndex = j;
}
}
}
}
// if there are no vertices to connect to, choose a point in the middle
if (lowerIndex == (upperIndex + 1) % this.vertices.length) {
//console.log("Case 1: Vertex("+i+"), lowerIndex("+lowerIndex+"), upperIndex("+upperIndex+"), poly.size("+this.vertices.length+")");
p[0] = (lowerInt[0] + upperInt[0]) / 2;
p[1] = (lowerInt[1] + upperInt[1]) / 2;
steinerPoints.push(p);
if (i < upperIndex) {
//lowerPoly.insert(lowerPoly.end(), poly.begin() + i, poly.begin() + upperIndex + 1);
lowerPoly.append(poly, i, upperIndex+1);
lowerPoly.vertices.push(p);
upperPoly.vertices.push(p);
if (lowerIndex != 0){
//upperPoly.insert(upperPoly.end(), poly.begin() + lowerIndex, poly.end());
upperPoly.append(poly,lowerIndex,poly.vertices.length);
}
//upperPoly.insert(upperPoly.end(), poly.begin(), poly.begin() + i + 1);
upperPoly.append(poly,0,i+1);
} else {
if (i != 0){
//lowerPoly.insert(lowerPoly.end(), poly.begin() + i, poly.end());
lowerPoly.append(poly,i,poly.vertices.length);
}
//lowerPoly.insert(lowerPoly.end(), poly.begin(), poly.begin() + upperIndex + 1);
lowerPoly.append(poly,0,upperIndex+1);
lowerPoly.vertices.push(p);
upperPoly.vertices.push(p);
//upperPoly.insert(upperPoly.end(), poly.begin() + lowerIndex, poly.begin() + i + 1);
upperPoly.append(poly,lowerIndex,i+1);
}
} else {
// connect to the closest point within the triangle
//console.log("Case 2: Vertex("+i+"), closestIndex("+closestIndex+"), poly.size("+this.vertices.length+")\n");
if (lowerIndex > upperIndex) {
upperIndex += this.vertices.length;
}
closestDist = Number.MAX_VALUE;
if(upperIndex < lowerIndex){
return result;
}
for (var j = lowerIndex; j <= upperIndex; ++j) {
if (Point.leftOn(poly.at(i - 1), poly.at(i), poly.at(j))
&& Point.rightOn(poly.at(i + 1), poly.at(i), poly.at(j))) {
d = Point.sqdist(poly.at(i), poly.at(j));
if (d < closestDist) {
closestDist = d;
closestIndex = j % this.vertices.length;
}
}
}
if (i < closestIndex) {
lowerPoly.append(poly,i,closestIndex+1);
if (closestIndex != 0){
upperPoly.append(poly,closestIndex,v.length);
}
upperPoly.append(poly,0,i+1);
} else {
if (i != 0){
lowerPoly.append(poly,i,v.length);
}
lowerPoly.append(poly,0,closestIndex+1);
upperPoly.append(poly,closestIndex,i+1);
}
}
// solve smallest poly first
if (lowerPoly.vertices.length < upperPoly.vertices.length) {
lowerPoly.quickDecomp(result,reflexVertices,steinerPoints,delta,maxlevel,level);
upperPoly.quickDecomp(result,reflexVertices,steinerPoints,delta,maxlevel,level);
} else {
upperPoly.quickDecomp(result,reflexVertices,steinerPoints,delta,maxlevel,level);
lowerPoly.quickDecomp(result,reflexVertices,steinerPoints,delta,maxlevel,level);
}
return result;
}
}
result.push(this);
return result;
};
/**
* Remove collinear points in the polygon.
* @method removeCollinearPoints
* @param {Number} [precision] The threshold angle to use when determining whether two edges are collinear. Use zero for finest precision.
* @return {Number} The number of points removed
*/
Polygon.prototype.removeCollinearPoints = function(precision){
var num = 0;
for(var i=this.vertices.length-1; this.vertices.length>3 && i>=0; --i){
if(Point.collinear(this.at(i-1),this.at(i),this.at(i+1),precision)){
// Remove the middle point
this.vertices.splice(i%this.vertices.length,1);
i--; // Jump one point forward. Otherwise we may get a chain removal
num++;
}
}
return num;
};
},{"./Line":1,"./Point":2,"./Scalar":4}],4:[function(require,module,exports){
module.exports = Scalar;
/**
* Scalar functions
* @class Scalar
*/
function Scalar(){}
/**
* Check if two scalars are equal
* @static
* @method eq
* @param {Number} a
* @param {Number} b
* @param {Number} [precision]
* @return {Boolean}
*/
Scalar.eq = function(a,b,precision){
precision = precision || 0;
return Math.abs(a-b) < precision;
};
},{}],5:[function(require,module,exports){
module.exports = {
Polygon : require("./Polygon"),
Point : require("./Point"),
};
},{"./Point":2,"./Polygon":3}]},{},[5])
(5)
});
;

205
graphics.js

@ -0,0 +1,205 @@
/* Interactive elements
* Model script
* Copyright 2016 Harmen de Weerd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
showGraphicsWarnings = true;
GraphicsConstants = {
RESIZE_AREA: 5,
ACTION_NONE: 0,
ACTION_DRAG: 1,
ACTION_E_RESIZE: 2,
ACTION_W_RESIZE: 4,
ACTION_S_RESIZE: 8,
ACTION_N_RESIZE: 16,
ACTION_NE_RESIZE: 18,
ACTION_NW_RESIZE: 20,
ACTION_SE_RESIZE: 10,
ACTION_SW_RESIZE: 12
};
function ImageElement(img) {
this.image = img;
this.x = 0;
this.y = 0;
this.rotation = 0;
this.getWidth = function() {
return this.image.width;
};
this.getHeight = function() {
return this.image.height;
};
this.draw = function(ctx, x = this.x, y = this.y) {
var centerX, centerY;
centerX = Math.floor(this.image.width/2);
centerY = Math.floor(this.image.height/2);
ctx.save();
ctx.translate(x + centerX, y + centerY);
ctx.rotate(this.rotation);
ctx.translate(-centerX, -centerY);
ctx.drawImage(this.image, 0, 0);
ctx.restore();
};
}
function makeInteractiveElement(graphicsElem, container) {
graphicsElem.repertoire = GraphicsConstants.ACTION_DRAG;
graphicsElem.isDraggable = function() {
return (this.repertoire & GraphicsConstants.ACTION_DRAG) > 0;
};
graphicsElem.isResizable = function() {
return (this.repertoire & GraphicsConstants.ACTION_RESIZE) > 0;
};
graphicsElem.currentAction = GraphicsConstants.ACTION_NONE;
/* graphicsElem.onResize = null;
graphicsElem.onDrag = null;
graphicsElem.onDragging = null;
graphicsElem.onDrop = null;*/
graphicsElem.startAction = function(event) {
this.reference = {x: event.target.mouse.x, y: event.target.mouse.y};
this.originalDimensions = {x: this.x,
y: this.y,
width: this.getWidth(),
height: this.getHeight()};
this.currentAction = this.getLocalAction(event.target.mouse.x - this.x,
event.target.mouse.y - this.y);
switch (this.currentAction) {
case GraphicsConstants.ACTION_DRAG:
if (this.onDrag && !this.onDrag(this, event)) {
this.currentAction = GraphicsConstants.ACTION_NONE;
}
break;
case GraphicsConstants.ACTION_SE_RESIZE:
case GraphicsConstants.ACTION_SW_RESIZE:
case GraphicsConstants.ACTION_S_RESIZE:
case GraphicsConstants.ACTION_NW_RESIZE:
case GraphicsConstants.ACTION_NE_RESIZE:
case GraphicsConstants.ACTION_W_RESIZE:
case GraphicsConstants.ACTION_S_RESIZE:
case GraphicsConstants.ACTION_N_RESIZE:
case GraphicsConstants.ACTION_E_RESIZE:
if (this.onResize && !this.onResize(this, event, this.currentAction)) {
this.currentAction = GraphicsConstants.ACTION_NONE;
}
break;
}
};
graphicsElem.endAction = function(event) {
if (this.currentAction == GraphicsConstants.ACTION_DRAG && this.onDrop) {
this.onDrop(this, event);
}
this.currentAction = GraphicsConstants.ACTION_NONE;
};
graphicsElem.continueAction = function(event) {
var x, y;
x = event.target.mouse.x;
y = event.target.mouse.y;
if (this.constraints != null) {
x = Math.min(this.constraints.right + this.reference.x,
Math.max(this.constraints.left + this.reference.x, x));
y = Math.min(this.constraints.bottom + this.reference.y,
Math.max(this.constraints.top + this.reference.y, y));
}
if ((this.currentAction & GraphicsConstants.ACTION_DRAG) > 0) {
this.x = this.originalDimensions.x + x - this.reference.x;
this.y = this.originalDimensions.y + y - this.reference.y;
} else {
if ((this.currentAction & GraphicsConstants.ACTION_S_RESIZE) > 0) {
this.elem.setHeight(this.originalDimensions.height + y - this.reference.y);
}
if ((this.currentAction & GraphicsConstants.ACTION_E_RESIZE) > 0) {
this.elem.setWidth(this.originalDimensions.width + x - this.reference.x);
}
if ((this.currentAction & GraphicsConstants.ACTION_N_RESIZE) > 0) {
this.y = this.originalDimensions.y + y - this.reference.y;
this.elem.setHeight(this.originalDimensions.height - y + this.reference.y);
}
if ((this.currentAction & GraphicsConstants.ACTION_W_RESIZE) > 0) {
this.x = this.originalDimensions.x + x - this.reference.x;
this.elem.setWidth(this.originalDimensions.width - x + this.reference.x);
}
}
if (this.onDragging) {
this.onDragging(this, event.target);
}
};
graphicsElem.getActionLabel = function(x, y) {
switch (this.getLocalAction(x,y)) {
case GraphicsConstants.ACTION_W_RESIZE:
case GraphicsConstants.ACTION_E_RESIZE:
return "col-resize";
case GraphicsConstants.ACTION_S_RESIZE:
case GraphicsConstants.ACTION_N_RESIZE:
return "row-resize";
case GraphicsConstants.ACTION_NW_RESIZE:
return "nw-resize";
case GraphicsConstants.ACTION_NE_RESIZE:
return "ne-resize";
case GraphicsConstants.ACTION_SE_RESIZE:
return "se-resize";
case GraphicsConstants.ACTION_SW_RESIZE:
return "sw-resize";
case GraphicsConstants.ACTION_DRAG:
return "pointer";
default:
return undefined;
}
};
graphicsElem.getLocalAction = function(x, y) {
if ((this.repertoire & GraphicsConstants.ACTION_W_RESIZE) > 0 &&
x < GraphicsConstants.RESIZE_AREA) {
if ((this.repertoire & GraphicsConstants.ACTION_N_RESIZE) > 0 &&
y < GraphicsConstants.RESIZE_AREA) {
return GraphicsConstants.ACTION_NW_RESIZE;
} else if ((this.repertoire & GraphicsConstants.ACTION_S_RESIZE) > 0 &&
y > this.getHeight() - GraphicsConstants.RESIZE_AREA) {
return GraphicsConstants.ACTION_SW_RESIZE;
}
return GraphicsConstants.ACTION_W_RESIZE;
} else if ((this.repertoire & GraphicsConstants.ACTION_E_RESIZE) > 0 &&
x > this.getWidth() - GraphicsConstants.RESIZE_AREA) {
if ((this.repertoire & GraphicsConstants.ACTION_N_RESIZE) > 0 &&
y < GraphicsConstants.RESIZE_AREA) {
return GraphicsConstants.ACTION_NE_RESIZE;
} else if ((this.repertoire & GraphicsConstants.ACTION_S_RESIZE) > 0 &&
y > this.getHeight() - GraphicsConstants.RESIZE_AREA) {
return GraphicsConstants.ACTION_SE_RESIZE;
}
return GraphicsConstants.ACTION_E_RESIZE;
} else if ((this.repertoire & GraphicsConstants.ACTION_N_RESIZE) > 0 &&
y < GraphicsConstants.RESIZE_AREA) {
return GraphicsConstants.ACTION_N_RESIZE;
} else if ((this.repertoire & GraphicsConstants.ACTION_S_RESIZE) > 0 &&
y > this.getHeight() - GraphicsConstants.RESIZE_AREA) {
return GraphicsConstants.ACTION_S_RESIZE;
}
if (this.isDraggable) {
return GraphicsConstants.ACTION_DRAG;
}
return undefined;
};
if (container.addInteractiveElement) {
container.addInteractiveElement(graphicsElem);
} else if (showGraphicsWarnings) {
console.warn("Container does not support mouse tracking: element will not be interactive.");
}
return graphicsElem;
}

4
jquery-1.11.0.min.js vendored

File diff suppressed because one or more lines are too long

663
lemming_sim_student.js

@ -0,0 +1,663 @@
/* Lemmings - robot and GUI script.
*
* Copyright 2016 Harmen de Weerd
* Copyright 2017 Johannes Keyser, James Cooke, George Kachergis
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// Description of robot(s), and attached sensor(s) used by InstantiateRobot()
RobotInfo = [
{body: null, // for MatterJS body, added by InstantiateRobot()
color: [255, 255, 255], // color of the robot shape
init: {x: 50, y: 50, angle: 0}, // initial position and orientation
sensors: [ // define an array of sensors on the robot
// define one sensor
{sense: senseDistance, // function handle, determines type of sensor
minVal: 0, // minimum detectable distance, in pixels
maxVal: 50, // maximum detectable distance, in pixels
attachAngle: Math.PI/4, // where the sensor is mounted on robot body
lookAngle: 0, // direction the sensor is looking (relative to center-out)
id: 'distR', // a unique, arbitrary ID of the sensor, for printing/debugging
color: [150, 0, 0], // sensor color [in RGB], to distinguish them
parent: null, // robot object the sensor is attached to, added by InstantiateRobot
value: null // sensor value, i.e. distance in pixels; updated by sense() function
},
// define another sensor
{sense: senseDistance, minVal: 0, maxVal: 50, attachAngle: -Math.PI/4,
lookAngle: 0, id: 'distL', color: [0, 150, 0], parent: null, value: null
}
]
}
];
// Simulation settings; please change anything that you think makes sense.
simInfo = {
maxSteps: 50000, // maximal number of simulation steps to run
airDrag: 0.1, // "air" friction of enviroment; 0 is vacuum, 0.9 is molasses
boxFric: 0.005, // friction between boxes during collisions
boxMass: 0.01, // mass of boxes
boxSize: 20, // size of the boxes, in pixels
robotSize: 13, // approximate robot radius, in pixels (note the SVG gets scaled down)
robotMass: 0.4, // robot mass (a.u)
gravity: 0, // constant acceleration in Y-direction
bayRobot: null, // currently selected robot
baySensor: null, // currently selected sensor
bayScale: 3, // scale within 2nd, inset canvas showing robot in it's "bay"
doContinue: true, // whether to continue simulation, set in HTML
debugSensors: false, // plot sensor rays and mark detected objects
debugMouse: false, // allow dragging any object with the mouse
engine: null, // MatterJS 2D physics engine
world: null, // world object (composite of all objects in MatterJS engine)
runner: null, // object for running MatterJS engine
height: null, // set in HTML file; height of arena (world canvas), in pixels
width: null, // set in HTML file; width of arena (world canvas), in pixels
curSteps: 0 // increased by simStep()
};
robots = new Array();
sensors = new Array();
function init() { // called once when loading HTML file
const robotBay = document.getElementById("bayLemming"),
arena = document.getElementById("arenaLemming"),
height = arena.height,
width = arena.width;
simInfo.height = height;
simInfo.width = width;
/* Create a MatterJS engine and world. */
simInfo.engine = Matter.Engine.create();
simInfo.world = simInfo.engine.world;
simInfo.world.gravity.y = simInfo.gravity;
simInfo.engine.timing.timeScale = 1;
/* Create walls and boxes, and add them to the world. */
// note that "roles" are custom properties for rendering (not from MatterJS)
function getWall(x, y, width, height) {
return Matter.Bodies.rectangle(x, y, width, height,
{isStatic: true, role: 'wall',
color:[150, 150, 150]});
};
const wall_lo = getWall(width/2, height-5, width-5, 5),
wall_hi = getWall(width/2, 5, width-5, 5),
wall_le = getWall(5, height/2, 5, height-15),
wall_ri = getWall(width-5, height/2, 5, height-15);
Matter.World.add(simInfo.world, [wall_lo, wall_hi, wall_le, wall_ri]);
/* Add a bunch of boxes in a neat grid. */
function getBox(x, y) {
// flip coin for red vs blue and add rgb
colFlag = Math.round(Math.random()); // random 0,1 variable for box color
if (colFlag == 1 ){
color = [0, 0, 200];
}
else {
color = [200, 0, 0];
}
box = Matter.Bodies.rectangle(x, y, simInfo.boxSize, simInfo.boxSize,
{frictionAir: simInfo.airDrag,
friction: simInfo.boxFric,
mass: simInfo.boxMass,
role: 'box',
color: color});
return box;
};
const startX = 100, startY = 100,
nBoxX = 5, nBoxY = 5,
gapX = 40, gapY = 30,
stack = Matter.Composites.stack(startX, startY,
nBoxX, nBoxY,
gapX, gapY, getBox);
Matter.World.add(simInfo.world, stack);
/* Add debugging mouse control for dragging objects. */
if (simInfo.debugMouse){
const mouseConstraint = Matter.MouseConstraint.create(simInfo.engine,
{mouse: Matter.Mouse.create(arena),
// spring stiffness mouse ~ object
constraint: {stiffness: 0.5}});
Matter.World.add(simInfo.world, mouseConstraint);
}
// Add the tracker functions from mouse.js
addMouseTracker(arena);
addMouseTracker(robotBay);
/* Running the MatterJS physics engine (without rendering). */
simInfo.runner = Matter.Runner.create({fps: 60, isFixed: false});
Matter.Runner.start(simInfo.runner, simInfo.engine);
// register function simStep() as callback to MatterJS's engine events
Matter.Events.on(simInfo.engine, 'tick', simStep);
/* Create robot(s). */
setRobotNumber(1); // requires defined simInfo.world
loadBay(robots[0]);
};
function convrgb(values) {
return 'rgb(' + values.join(', ') + ')';
};
function rotate(robot, torque=0) {
/* Apply a torque to the robot to rotate it.
*
* Parameters
* torque - rotational force to apply to the body.
* Try values around +/- 0.005.
*/
robot.body.torque = torque;
};
function drive(robot, force=0) {
/* Apply a force to the robot to move it.
*
* Parameters
* force - force to apply to the body.
* Try values around +/- 0.0005.
*/
const orientation = robot.body.angle,
force_vec = Matter.Vector.create(force, 0),
move_vec = Matter.Vector.rotate(force_vec, orientation);
Matter.Body.applyForce(robot.body, robot.body.position , move_vec);
};
function senseDistance() {
/* Distance sensor simulation based on ray casting. Called from sensor
* object, returns nothing, updates a new reading into this.value.
*
* Idea: Cast a ray with a certain length from the sensor, and check
* via collision detection if objects intersect with the ray.
* To determine distance, run a Binary search on ray length.
* Note: Sensor ray needs to ignore robot (parts), or start outside of it.
* The latter is easy with the current circular shape of the robots.
* Note: Order of tests are optimized by starting with max ray length, and
* then only testing the maximal number of initially resulting objects.
* Note: The sensor's "ray" could have any other (convex) shape;
* currently it's just a very thin rectangle.
*/
const context = document.getElementById('arenaLemming').getContext('2d');
var bodies = Matter.Composite.allBodies(simInfo.engine.world);
const robotAngle = this.parent.body.angle,
attachAngle = this.attachAngle,
rayAngle = robotAngle + attachAngle + this.lookAngle;
const rPos = this.parent.body.position,
rSize = simInfo.robotSize,
startPoint = {x: rPos.x + (rSize+1) * Math.cos(robotAngle + attachAngle),
y: rPos.y + (rSize+1) * Math.sin(robotAngle + attachAngle)};
function getEndpoint(rayLength) {
return {x: rPos.x + (rSize + rayLength) * Math.cos(rayAngle),
y: rPos.y + (rSize + rayLength) * Math.sin(rayAngle)};
};
function sensorRay(bodies, rayLength) {
// Cast ray of supplied length and return the bodies that collide with it.
const rayWidth = 1e-100,
endPoint = getEndpoint(rayLength);
rayX = (endPoint.x + startPoint.x) / 2,
rayY = (endPoint.y + startPoint.y) / 2,
rayRect = Matter.Bodies.rectangle(rayX, rayY, rayLength, rayWidth,
{isSensor: true, isStatic: true,
angle: rayAngle, role: 'sensor'});
var collidedBodies = [];
for (var bb = 0; bb < bodies.length; bb++) {
var body = bodies[bb];
// coarse check on body boundaries, to increase performance:
if (Matter.Bounds.overlaps(body.bounds, rayRect.bounds)) {
for (var pp = body.parts.length === 1 ? 0 : 1; pp < body.parts.length; pp++) {
var part = body.parts[pp];
// finer, more costly check on actual geometry:
if (Matter.Bounds.overlaps(part.bounds, rayRect.bounds)) {
const collision = Matter.SAT.collides(part, rayRect);
if (collision.collided) {
collidedBodies.push(body);
break;
}
}
}
}
}
return collidedBodies;
};
// call 1x with full length, and check all bodies in the world;
// in subsequent calls, only check the bodies resulting here
var rayLength = this.maxVal;
bodies = sensorRay(bodies, rayLength);
// if some collided, search for maximal ray length without collisions
if (bodies.length > 0) {
var lo = 0,
hi = rayLength;
while (lo < rayLength) {
if (sensorRay(bodies, rayLength).length > 0) {
hi = rayLength;
}
else {
lo = rayLength;
}
rayLength = Math.floor(lo + (hi-lo)/2);
}
}
// increase length to (barely) touch closest body (if any)
rayLength += 1;
bodies = sensorRay(bodies, rayLength);
if (simInfo.debugSensors) { // if invisible, check order of object drawing
// draw the resulting ray
endPoint = getEndpoint(rayLength);
context.beginPath();
context.moveTo(startPoint.x, startPoint.y);
context.lineTo(endPoint.x, endPoint.y);
context.strokeStyle = this.parent.info.color;
context.lineWidth = 0.5;
context.stroke();
// mark all objects's lines intersecting with the ray
for (var bb = 0; bb < bodies.length; bb++) {
var vertices = bodies[bb].vertices;
context.moveTo(vertices[0].x, vertices[0].y);
for (var vv = 1; vv < vertices.length; vv += 1) {
context.lineTo(vertices[vv].x, vertices[vv].y);
}
context.closePath();
}
context.stroke();
}
// indicate if the sensor exceeded its maximum length by returning infinity
if (rayLength > this.maxVal) {
rayLength = Infinity;
}
else {
// apply mild noise on the sensor reading, and clamp between valid values
function gaussNoise(sigma=1) {
const x0 = 1.0 - Math.random();
const x1 = 1.0 - Math.random();
return sigma * Math.sqrt(-2 * Math.log(x0)) * Math.cos(2 * Math.PI * x1);
};
rayLength = Math.floor(rayLength + gaussNoise(3));
rayLength = Matter.Common.clamp(rayLength, this.minVal, this.maxVal);
}
this.value = rayLength;
};
function dragSensor(sensor, event) {
const robotBay = document.getElementById('bayLemming'),
bCenter = {x: robotBay.width/2,
y: robotBay.height/2},
rSize = simInfo.robotSize,
bScale = simInfo.bayScale,
sSize = sensor.getWidth(),
mAngle = Math.atan2( event.mouse.x - bCenter.x,
-(event.mouse.y - bCenter.y));
sensor.info.attachAngle = mAngle;
sensor.x = bCenter.x - sSize - bScale * rSize * Math.sin(-mAngle);
sensor.y = bCenter.y - sSize - bScale * rSize * Math.cos( mAngle);
repaintBay();
}
function loadSensor(sensor, event) {
loadSensorInfo(sensor.sensor);
}
function loadSensorInfo(sensorInfo) {
simInfo.baySensor = sensorInfo;
}
function loadBay(robot) {
simInfo.bayRobot = robot;
sensors = new Array();
const robotBay = document.getElementById("bayLemming");
const bCenter = {x: robotBay.width/2,
y: robotBay.height/2},
rSize = simInfo.robotSize,
bScale = simInfo.bayScale;
for (var ss = 0; ss < robot.info.sensors.length; ++ss) {
const curSensor = robot.sensors[ss],
attachAngle = curSensor.attachAngle;
// put current sensor into global variable, make mouse-interactive
sensors[ss] = makeInteractiveElement(new SensorGraphics(curSensor),
document.getElementById("bayLemming"));
const sSize = sensors[ss].getWidth();
sensors[ss].x = bCenter.x - sSize - bScale * rSize * Math.sin(-attachAngle);
sensors[ss].y = bCenter.y - sSize - bScale * rSize * Math.cos( attachAngle);
sensors[ss].onDragging = dragSensor;
sensors[ss].onDrag = loadSensor;
}
repaintBay();
}
function SensorGraphics(sensorInfo) {
this.info = sensorInfo;
this.plotSensor = plotSensor;
// add functions getWidth/getHeight for graphics.js & mouse.js,
// to enable dragging the sensor in the robot bay
this.getWidth = function() { return 6; };
this.getHeight = function() { return 6; };
}
functio