// Theo Jansen's STRANDBEEST
//
// Code adapted from user heltonbiker at Stack Overflow
//
function fmod(a, b) {
if (a < 0) {
return b - (-a) % b
} else {
return a % b
}
}
function fromPoint(p, d, theta) {
return p.add(Vector.create([Math.cos(theta), Math.sin(theta)]).x(d))
}
function radians(d) {
return d * Math.PI / 180
}
// Return 2-dimensional vector cross product of p and q.
function cross2(p, q) {
var P = p.elements
var Q = q.elements
return P[0] * Q[1] - P[1] * Q[0]
}
// Return a point R that's distance l1 from p1, and distance l2 from p2,
// and p1-p2-R is clockwise.
function inter(p1, l1, p2, l2) {
var D = p2.subtract(p1) // Vector from p1 to p2.
var d = D.modulus() // Dist from p2 to p1.
var a = (l1*l1 - l2*l2 + d*d) / (2*d) // Dist from p1 to radical line.
var M = p1.add(D.x(a / d)) // Intersection of D w/radical line
var h = Math.sqrt(l1*l1 - a*a) // Distance from M to R1 or R2.
var R = D.x(h / d)
var r = Vector.create([-R.elements[1], R.elements[0]])
// There are two results, but only one (the correct side of the
// line) must be chosen
var R1 = M.add(r)
if (cross2(D, R1.subtract(p1)) < 0) {
return M.subtract(r)
} else {
return R1
}
}
function Beest() {
this.angle = 0;
this.lines = ["AC", "CD", "BD", "BE", "CE", "DF",
"BF", "FG", "EG", "GH", "EH"];
this.magic = ["Bx", "By"].concat(this.lines);
this.update();
}
Beest.prototype = {
constructor: Beest,
update: function () {
var text = ""
for (var i = 0; i < this.magic.length; ++i) {
var m = this.magic[i]
this[m] = parseFloat(params[m]);
}
this.footprint = [];
this.linkageBroken = false;
this.analyzedFootprint = false;
this.tolerance = 2; // Range of values of Y that count as "ground"
this.Ymax = 0;
this.liftheight = 35;
this.lifttolerance = 15;
this.maxliftheight = 60;
this.maxlifttolerance = 20;
},
addPoint: function (label, p) {
p.angle = this.angle
p.label = label
this.points.push(p)
this[label] = p
},
footprintGrounded: function (i) {
return (Math.abs(this.Ymax - this.footprint[i].elements[1])
< this.tolerance)
},
footprintLifted: function (i) {
return (Math.abs((this.Ymax - this.liftheight) - this.footprint[i].elements[1])
< this.lifttolerance)
},
analyzeFootprint: function () {
var f = this.footprint;
this.Ymax = 0; // Extremal value of Y: counts as "ground"
this.Ymin = 1000000;
for (var i = 0; i < f.length; ++i) {
this.Ymax = Math.max(this.Ymax, f[i].elements[1]);
this.Ymin = Math.min(this.Ymin, f[i].elements[1]);
}
var groundAngle = 0; // Angle spent on the ground.
var liftAngle = 0;
var minVx = 1e10;
var maxVx = -1e10;
for (var i = 0; i < f.length; ++i) {
if (this.footprintGrounded(i)) {
var j = (i + 1) % f.length
var a = f[j].angle
var b = f[i].angle
var dt
if (a < b) {
dt = b - a
} else {
dt = a - b - 360
}
groundAngle += dt
if (dt > 0) {
var vx = (f[j].elements[0] - f[i].elements[0]) / dt
minVx = Math.min(minVx, vx)
maxVx = Math.max(maxVx, vx)
}
}
if (this.footprintLifted(i)) {
var j = (i + 1) % f.length
var a = f[j].angle
var b = f[i].angle
var dt
if (a < b) {
dt = b - a
} else {
dt = a - b - 360
}
liftAngle += dt
}
}
this.analyzedFootprint = true
var text = ""
for (var i = 0; i < this.magic.length; ++i) {
var m = this.magic[i]
text += m + "=" + this[m] + "; "
}
text += "groundScore: " + (groundAngle / 360.0).toFixed(3);
text += "; dragScore: " + (Math.max(- maxVx + minVx)).toFixed(3);
text += "; liftScore: " + (liftAngle / 360.0).toFixed(3);
var maxliftscore = (this.Ymax - this.Ymin - this.maxliftheight) / this.maxlifttolerance;
if(maxliftscore < 0.0) {
maxliftscore = 0.0;
}
//write(text);
setFinished('ground', (groundAngle / 360.0), 'drag', (Math.max(- maxVx + minVx)), 'lift', (liftAngle / 360.0), 'maxlift', maxliftscore);
isFinished = 1;
},
tick: function (dt) {
this.angle += speed * dt;
this.points = []
this.addPoint("A", Vector.create([0,0]))
this.addPoint("B", Vector.create([this.Bx, -this.By]))
this.addPoint("C", fromPoint(this.A, this.AC, radians(this.angle)))
this.addPoint("D", inter(this.C, this.CD, this.B, this.BD))
this.addPoint("E", inter(this.B, this.BE, this.C, this.CE))
this.addPoint("F", inter(this.D, this.DF, this.B, this.BF))
this.addPoint("G", inter(this.F, this.FG, this.E, this.EG))
this.addPoint("H", inter(this.G, this.GH, this.E, this.EH))
if (isNaN(this.H.elements[0]) || isNaN(this.H.elements[1])) {
this.linkageBroken = true;
setFailed("Broken Linkage");
isFinished = 1;
} else {
this.footprint.push(this.H)
}
var footprintComplete = false
while (this.footprint[0].angle - 360 > this.angle) {
this.footprint.shift()
footprintComplete = true
}
if (!this.analyzedFootprint
&& !this.linkageBroken
&& footprintComplete)
{
this.analyzeFootprint()
}
},
}
var speed = -60; // Speed of crank rotation, degrees/sec.
var lastFrame;
var beest;
var isFinished = 0;
function beestTick() {
var t = (new Date()).getTime()
var dt = Math.min(1.0 / 30, (t - lastFrame) / 1000.0)
lastFrame = t
beest.tick(params.dt)
}
lastFrame = (new Date()).getTime();
beest = new Beest();
while(!isFinished) {
beestTick();
}