Physics Simulator with Curve Detection

Instructions: Throw the ball around with your cursor.

Konva Physics Simulator with Curve Detectionview raw
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.rawgit.com/konvajs/konva/1.7.6/konva.min.js"></script>
<meta charset="utf-8">
<title>Konva Physics Simulator Demo</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
background-color: #F0F0F0;
}
</style>
</head>
<body>
<div id="container"></div>
<script>
var width = window.innerWidth;
var height = window.innerHeight;

/*
* Vector math functions
*/
function dot(a, b) {
return ((a.x * b.x) + (a.y * b.y));
}
function magnitude(a) {
return Math.sqrt((a.x * a.x) + (a.y * a.y));
}
function normalize(a) {
var mag = magnitude(a);

if(mag === 0) {
return {
x: 0,
y: 0
};
}
else {
return {
x: a.x / mag,
y: a.y / mag
};
}
}
function add(a, b) {
return {
x: a.x + b.x,
y: a.y + b.y
};
}
function angleBetween(a, b) {
return Math.acos(dot(a, b) / (magnitude(a) * magnitude(b)));
}
function rotate(a, angle) {
var ca = Math.cos(angle);
var sa = Math.sin(angle);
var rx = a.x * ca - a.y * sa;
var ry = a.x * sa + a.y * ca;
return {
x: rx * -1,
y: ry * -1
};
}
function invert(a) {
return {
x: a.x * -1,
y: a.y * -1
};
}
/*
* this cross product function has been simplified by
* setting x and y to zero because vectors a and b
* lie in the canvas plane
*/
function cross(a, b) {
return {
x: 0,
y: 0,
z: (a.x * b.y) - (b.x * a.y)
};
}
function getNormal(curve, ball) {
var curveLayer = curve.getLayer();
var context = curveLayer.getContext();
var testRadius = 20;
// pixels
var totalX = 0;
var totalY = 0;
var x = ball.getX();
var y = ball.getY();
/*
* check various points around the center point
* to determine the normal vector
*/
for(var n = 0; n < 20; n++) {
var angle = n * 2 * Math.PI / 20;
var offsetX = testRadius * Math.cos(angle);
var offsetY = testRadius * Math.sin(angle);
var testX = x + offsetX;
var testY = y + offsetY;
if(!context._context.isPointInPath(testX, testY)) {
totalX += offsetX;
totalY += offsetY;
}
}

var normal;

if(totalX === 0 && totalY === 0) {
normal = {
x: 0,
y: -1
};
}
else {
normal = {
x: totalX,
y: totalY
};
}

return normalize(normal);
}
function handleCurveCollision(ball, curve) {
var curveLayer = curve.getLayer();
var x = ball.getX();
var y = ball.getY();

var curveDamper = 0.05;
// 5% energy loss
if(curveLayer.getIntersection({x:x, y:y})) {
var normal = getNormal(curve, ball);
if(normal !== null) {
var angleToNormal = angleBetween(normal, invert(ball.velocity));
var crossProduct = cross(normal, ball.velocity);
var polarity = crossProduct.z > 0 ? 1 : -1;
var collisonAngle = polarity * angleToNormal * 2;
var collisionVector = rotate(ball.velocity, collisonAngle);

ball.velocity.x = collisionVector.x;
ball.velocity.y = collisionVector.y;
ball.velocity.x *= (1 - curveDamper);
ball.velocity.y *= (1 - curveDamper);

x += normal.x;
if(ball.velocity.y > 0.1) {
y += normal.y;
}
else {
y += normal.y / 10;
}
ball.x(x).y(y);
}

tween.finish();
}

}
function updateBall(frame) {
var timeDiff = frame.timeDiff;
var stage = ball.getStage();
var height = stage.getHeight();
var width = stage.getWidth();
var x = ball.getX();
var y = ball.getY();
var radius = ball.getRadius();

tween.reverse();

// physics variables
var gravity = 10;
// px / second^2
var speedIncrementFromGravityEachFrame = gravity * timeDiff / 1000;
var collisionDamper = 0.2;
// 20% energy loss
var floorFriction = 5;
// px / second^2
var floorFrictionSpeedReduction = floorFriction * timeDiff / 1000;

// if ball is being dragged and dropped
if(ball.isDragging()) {
var mousePos = stage.getPointerPosition();

if(mousePos) {
var mouseX = mousePos.x;
var mouseY = mousePos.y;

var c = 0.06 * timeDiff;
ball.velocity = {
x: c * (mouseX - ball.lastMouseX),
y: c * (mouseY - ball.lastMouseY)
};

ball.lastMouseX = mouseX;
ball.lastMouseY = mouseY;
}
}
else {
// gravity
ball.velocity.y += speedIncrementFromGravityEachFrame;
x += ball.velocity.x;
y += ball.velocity.y;

// ceiling condition
if(y < radius) {
y = radius;
ball.velocity.y *= -1;
ball.velocity.y *= (1 - collisionDamper);
}

// floor condition
if(y > (height - radius)) {
y = height - radius;
ball.velocity.y *= -1;
ball.velocity.y *= (1 - collisionDamper);
}

// floor friction
if(y == height - radius) {
if(ball.velocity.x > 0.1) {
ball.velocity.y -= floorFrictionSpeedReduction;
}
else if(ball.velocity.x < -0.1) {
ball.velocity.x += floorFrictionSpeedReduction;
}
else {
ball.velocity.x = 0;
}
}

// right wall condition
if(x > (width - radius)) {
x = width - radius;
ball.velocity.x *= -1;
ball.velocity.x *= (1 - collisionDamper);
}

// left wall condition
if(x < radius) {
x = radius;
ball.velocity.x *= -1;
ball.velocity.x *= (1 - collisionDamper);
}

ball.setPosition({x:x, y:y});

/*
* if the ball comes into contact with the
* curve, then bounce it in the direction of the
* curve's surface normal
*/
collision = handleCurveCollision(ball, curve);

}
}

var stage = new Konva.Stage({
container: 'container',
width: width,
height: height
});

var curveLayer = new Konva.Layer();
var ballLayer = new Konva.Layer();
var radius = 20;
var anim;

var curve = new Konva.Shape({
sceneFunc: function(context) {
context.beginPath();
context.moveTo(40, height);
context.bezierCurveTo(width * 0.2, -1 * height * 0.5, width * 0.7, height * 1.3, width, height * 0.5);
context.lineTo(width, height);
context.lineTo(40, height);
context.closePath();
context.fillShape(this);
},
fill: '#8dbdff'
});

curveLayer.add(curve);

// create ball
var ball = new Konva.Circle({
x: 190,
y: 20,
radius: radius,
fill: 'blue',
draggable: true,
opacity: 0.8
});

// custom property
ball.velocity = {
x: 0,
y: 0
};

ball.on('dragstart', function() {
ball.velocity = {
x: 0,
y: 0
};
anim.start();
});

ball.on('mousedown', function() {
anim.stop();
});

ball.on('mouseover', function() {
document.body.style.cursor = 'pointer';
});

ball.on('mouseout', function() {
document.body.style.cursor = 'default';
});

ballLayer.add(ball);
stage.add(curveLayer);
stage.add(ballLayer);

var tween = new Konva.Tween({
node: ball,
fill: 'red',
duration: 0.3,
easing: Konva.Easings.EaseOut
});

anim = new Konva.Animation(function(frame) {
updateBall(frame);
}, ballLayer);

anim.start();
</script>

</body>
</html>