Skip to content

Instantly share code, notes, and snippets.

@richardcpeterson
Created March 20, 2012 16:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save richardcpeterson/2138158 to your computer and use it in GitHub Desktop.
Save richardcpeterson/2138158 to your computer and use it in GitHub Desktop.
Some kind of bug in my rope sim
<!doctype html>
<html>
<head>
<script src="traer.js"></script>
<script>
/************************************ JSBUG ************************************
*
* SpiderMonkey bug description
*
* Look for the big outlined block comments like this one, labeled "JSBUG" for
* your grepping pleasure. You don't really need to understand all this code
* in order to understand the bug, although some of the rest of the code may
* prove relevant. I haven't been able to develop a minimal example of the
* bug - sorry. It seems to be dependent on context.
*
******************************************************************************/
// Size of canvas
var canvas_width = 600;
var canvas_height = 600;
// Number of nodes in rope, including invisible beginning and end nodes
var rope_node_count = 20;
var rope_length_px = 500;
var rope_segment_length = rope_length_px / rope_node_count;
var bend_smoothness_factor = 0.4; // 0 to 1
// Physics constants
var gravity = 0.0;
var drag = 0.01;
var spring_strength = 0.4;
var spring_damping = 0.0;
var handle_spring_strength = 0.2;
var frame_delay_ms = 35;
var physics;
var nodes = [];
var dragged_node = null;
var canvas;
var ctx;
var background_gradient;
var mouse = {
x: null,
y: null
};
function init() {
canvas = document.getElementById('rope_sim');
canvas.style.height = canvas_height + 'px';
canvas.style.width = canvas_width + 'px';
ctx = canvas.getContext('2d');
// Create Linear Gradients
background_gradient = ctx.createLinearGradient(0,0,0,canvas_height);
background_gradient.addColorStop(0, '#EEF');
background_gradient.addColorStop(0.7, '#CCD');
background_gradient.addColorStop(1, '#AAC');
// Start tracking mouse coordinates on mousedown
canvas.onmousedown = function(e) {
dragged_node = e.ctrlKey ? nodes[0] : nodes[nodes.length - 1];
if(e.offsetX) {
mouse.x = e.offsetX;
mouse.y = e.offsetY;
}
else if(e.layerX) {
mouse.x = e.layerX;
mouse.y = e.layerY;
}
canvas.onmousemove(e);
};
// Stop tracking mouse coordinates on mouseup
canvas.onmouseup = function(e) {
mouse.x = null;
mouse.y = null;
};
// Continue tracking mouse coordinates if mouse is down
canvas.onmousemove = function(e) {
if (mouse.x === null || mouse.y === null) {
return;
}
if(e.offsetX) {
mouse.x = e.offsetX;
mouse.y = e.offsetY;
}
else if(e.layerX) {
mouse.x = e.layerX;
mouse.y = e.layerY;
}
dragged_node.position.set(mouse.x, mouse.y, 0);
dragged_node.velocity.clear();
};
physics = new ParticleSystem(gravity, drag);
nodes = [];
// This is where we stick our rope at first
var horizontal_midpoint = canvas_width / 2;
// Make all the nodes
for (var i = 0; i < rope_node_count; i++){
nodes[i] = physics.makeParticle(
0.6, // Mass
horizontal_midpoint, // X
(canvas_height - 20) - i * rope_segment_length, // Y
0.0 // Z
);
}
// Make all the springs between the nodes
for (var i = 0; i < rope_node_count - 1; i++){
physics.makeSpring(
nodes[i],
nodes[i+1],
spring_strength,
spring_damping,
rope_segment_length
);
}
// Invisible handle at the end
nodes[rope_node_count] = physics.makeParticle(
0.2,
horizontal_midpoint,
(canvas_height - 20) - (rope_node_count - 1) * rope_segment_length, // Y
0.0
);
physics.makeSpring(
nodes[rope_node_count - 1],
nodes[rope_node_count],
handle_spring_strength,
0.5,
2
);
// Make the first node fixed (not affected by forces)
nodes[rope_node_count].makeFixed();
// Start the sim and rendering
setInterval(draw, frame_delay_ms);
}
function draw() {
var a,b,c,d;
var vectorBA,vectorBC,vectorCB,vectorCD;
var abLength,bcLength,cdLength,acLength,bdLength;
var angleBacute,angleCacute;
var angleB,angleC;
var insanity,cross_product;
var absoluteAngleBC,absoluteAngleCB;
var cp1angle,cp2angle,cpLength,cp1pinch_factor,cp2pinch_factor;
var bValue,value;
var cp1, cp2;
var height;
// Evaluate physics
physics.tick();
ctx.fillStyle = background_gradient;
ctx.clearRect(0, 0, canvas_width, canvas_height);
ctx.fillRect(0, 0, canvas_width, canvas_height);
for (var starting_node = 1; starting_node < rope_node_count - 1; starting_node++) {
// We'll be drawing a curve starting at b, and ending at c. We'll need the
// positions of the prior and following points, a and d, in order to calculate
// the control points.
ctx.beginPath();
ctx.moveTo(nodes[starting_node].position.x, nodes[starting_node].position.y);
// These are the four points along the physics chain that we will need in order to
// calculate the curve. The curve will go from b to c. We need a and d in order to
// calculate a smooth curve
a = {
x:nodes[starting_node-1].position.x,
y:nodes[starting_node-1].position.y
};
b = {
x:nodes[starting_node].position.x,
y:nodes[starting_node].position.y
};
c = {
x:nodes[starting_node + 1].position.x,
y:nodes[starting_node + 1].position.y
};
d = {
x:nodes[starting_node + 2].position.x,
y:nodes[starting_node + 2].position.y
};
// Vectors from the end points to their adjacent points. Remember that a
// vector "B to A" is calculated "A - B"
vectorBA = {
x: a.x - b.x,
y: a.y - b.y
};
vectorBC = {
x:c.x - b.x,
y:c.y - b.y
};
vectorCB = {
x:b.x - c.x,
y:b.y - c.y
};
vectorCD = {
x:d.x - c.x,
y:d.y - c.y
};
abLength = Math.sqrt(Math.pow((a.x - b.x), 2) + Math.pow ((a.y - b.y), 2));
bcLength = Math.sqrt(Math.pow((b.x - c.x), 2) + Math.pow ((b.y - c.y), 2));
cdLength = Math.sqrt(Math.pow((c.x - d.x), 2) + Math.pow ((c.y - d.y), 2));
acLength = Math.sqrt(Math.pow((a.x - c.x), 2) + Math.pow ((a.y - c.y), 2));
bdLength = Math.sqrt(Math.pow((b.x - d.x), 2) + Math.pow ((b.y - d.y), 2));
// Use law of cosines to get angle of B at ABC
angleBacute = Math.acos(
(Math.pow(abLength, 2) + Math.pow(bcLength, 2)
- Math.pow(acLength, 2)) / (2 * abLength * bcLength)
);
// Use law of cosines to get angle of C at BCD
angleCacute = Math.acos(
(Math.pow(bcLength, 2) + Math.pow(cdLength, 2)
- Math.pow(bdLength, 2)) / (2 * bcLength * cdLength)
);
// Law of cosines doesn't work for the non-angle, aka a straight line. Thus
// we have to infer this value when the law of cosines gives us a NaN
if (isNaN(angleBacute)) angleBacute = Math.PI;
if (isNaN(angleCacute)) angleCacute = Math.PI;
// We want to save our "acute" version of the angle (< PI in this
// case... yeah yeah I know the real definition of "acute"). angleB
// and angleC will have the actual angle, which may be greater than PI
angleB = angleBacute;
angleC = angleCacute;
// Use cross product of the vectors at B to determine the
// sign of the sin of the angle B. If the cross product is
// positive, angle B is less than PI and we can leave it
// as is. If the cross product is negative, angle B must be
// adjusted to its larger-than-PI value (since the law of
// cosines used above only gives us angles < PI as answers)
// This all depends on the direction the angle is measured
// (clockwise vs counterclockwise).
/***************************** JSBUG ***********************************
* Mozilla SpiderMonkey bug??
*
* Look carefully below and see if there is any way that "insanity"
* should ever be true. Carefully note the nested conitionals, which
* should never be satisfied.
**********************************************************************/
/***************************** JSBUG ***********************************
* Uncommenting any one of the following lines seems to prevent the bug
**********************************************************************/
// var foo = function(){};
// Math.pow(1,1);
// function foo(){}
// Math.sin(Math.PI);
// for (;false;){};
// with({}){};
// while(false);
// parseFloat(0);
/***************************** JSBUG ***********************************
* But uncommenting any of these lines does not seem to prevent the bug
**********************************************************************/
// ;
// 1;
// {};
// var foo = {};
// var foo = 1;
// if (true);
// if (true){};
// Math.min(0,1);
// Math.abs(1);
insanity = false;
cross_product = (vectorBA.x * vectorBC.y - vectorBC.x * vectorBA.y);
if (cross_product < 0) {
if (cross_product >= 0){
insanity = true;
}
angleB = (2 * Math.PI) - angleB;
} else {
if (cross_product < 0){
insanity = true;
}
}
/***************************** JSBUG ***********************************
* Now, at least in Firefox 11 in Ubuntu 64 bit, Ubuntu 32 bit and
* Windows Vista, and in Conkeror (unknown version) "insanity" is often
* true. Seach for the next use of the variable "insanity", and you'll
* see that it is being used to set the stroke color to "red", so we
* have a visual indicator of the presense of the bug.
*
* It seems that insanity is true when "cross_product" has a negative
* value, but the first conditional "(cross_product < 0)" fails to be
* fulfilled for some reason (thus the bug). By the time the "else"
* clause is evaluated, cross_product is evaluated properly.
**********************************************************************/
// Same cross product trick with angle C
if ((vectorCB.x * vectorCD.y - vectorCD.x * vectorCB.y) < 0) {
angleC = (2 * Math.PI) - angleC;
}
// Angle of the line segment compared to coordinate space
absoluteAngleBC = Math.atan2(c.y - b.y, c.x - b.x);
// Same thing backward. Wasting an atan2 here...
absoluteAngleCB = Math.atan2(b.y - c.y, b.x - c.x);
// This is the angle that Control Point 1 should be away from point B
cp1angle = ((Math.PI - angleB) / 2) + absoluteAngleBC;
// This is the angle that Control Point 2 should be away from point C
cp2angle = ((Math.PI - angleC) / -2) + absoluteAngleCB;
// This is the base distance that a control point should be away from
// its related base point: cp1 to b, and cp2 to c. Based on the distance
// of the two endpoints of the curve and our adjustment factor
cpLength = bcLength * bend_smoothness_factor;
// If the angles at B or C are very acute, we bring the control point
// closer. This means that as the overall line develops very tight angles,
// those angles will be reflected as sharp angles in the curve. This gives
// a natural look to the resulting overall curve
cp1pinch_factor = Math.min(1,Math.abs(angleBacute / (Math.PI * 0.4)));
cp2pinch_factor = Math.min(1,Math.abs(angleCacute / (Math.PI * 0.4)));
cp1 = {
x:Math.cos(cp1angle) * cpLength * cp1pinch_factor + b.x,
y:Math.sin(cp1angle) * cpLength * cp1pinch_factor + b.y
};
cp2 = {
x:Math.cos(cp2angle) * cpLength * cp2pinch_factor + c.x,
y:Math.sin(cp2angle) * cpLength * cp2pinch_factor + c.y
};
// "Height" away from the imagiary and arbitray z axis. This just helps us give a
// "2.5D" look to the final rendering. Each segment along the curve gets "higher"
height = starting_node / rope_node_count;
ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, c.x, c.y);
// First we draw the stroke that will make up the outline. It will be the
// outline simply because the other stroke will be drawn on top of it, smaller
// As the curve gets "higher", the blur gets blurrier, more transparent and
// farther away in the XY plane
ctx.shadowOffsetX = height*4 + 2;
ctx.shadowOffsetY = height*20 + 8;
ctx.shadowBlur = height * 10 + 8;
ctx.shadowColor = "rgba(0, 0, 0, " + ((1/3) - height * (1/3)) + ")";
// Make the line bigger as it gets "higher"
ctx.lineWidth = 20 * (height *0.5 + 0.5);
// We calculate some colors based on these
value = parseInt((starting_node * 30 / rope_node_count));
bValue = parseInt((starting_node * 45 / rope_node_count));
ctx.strokeStyle = "rgb("+value+","+value+","+bValue+")";
ctx.lineCap = "round";
ctx.stroke();
// Make sure the next pass doesn't make a shadow
ctx.shadowColor = "transparent";
ctx.lineWidth = 16 * (height *0.5 + 0.5);
value = parseInt(240 + (starting_node * 15 / rope_node_count));
bValue = parseInt(245 + (starting_node * 10 / rope_node_count));
ctx.strokeStyle = "rgb("+ value +","+ value +","+ bValue +")";
/***************************** JSBUG ***********************************
* Color the line red if the bug has been encountered
**********************************************************************/
if (insanity) ctx.strokeStyle = "red";
ctx.stroke();
}
}
</script>
</head>
<body onload="init()">
<h2><del>Richard's Canvas Experiment</del></h2>
<h1><ins>SpiderMonkey possible bug demo</ins></h1>
<div id="log"></div>
<canvas id="rope_sim" width="600" height="600"
style="border:1px solid black; margin: 20px; position: relative; float:left; clear:left;"></canvas>
<p>This demonstrates a possible bug in the SpiderMonkey javascript engine. To see notes on
the bug, look at the source of this page, see the latest code on
<a href="https://gist.github.com/2138158">GitHub</a>,
or look at the embedded code at the bottom of the page.</p>
<p>Click on the canvas element to the left, and move the large, top white end of the
cord to the right. If you are using a browser that is suceptible to this bug, you
should notice some of the cord's segments turn red. This indicates that an impossible
logic state has been reached in the code.</p>
<p>Specifically, in the following code, the variable "insanity" has been set to "true",
which appears to be a logical impossibility.</p>
<pre>
insanity = false;
cross_product = (vectorBA.x * vectorBC.y - vectorBC.x * vectorBA.y);
if (cross_product < 0) {
if (cross_product >= 0){
insanity = true;
}
angleB = (2 * Math.PI) - angleB;
} else {
if (cross_product < 0){
insanity = true;
}
}
</pre>
<p>NB: This bug does <em>not</em> occur if the Firebug console is enabled - even if it is
minimized</p>
<div style="clear:both">
<script src="https://gist.github.com/2138158.js?file=bug.html"></script>
</div>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment