Skip to content

Instantly share code, notes, and snippets.

@roachhd
Last active August 29, 2015 13:56
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 roachhd/5e13fa99a0afd096aa64 to your computer and use it in GitHub Desktop.
Save roachhd/5e13fa99a0afd096aa64 to your computer and use it in GitHub Desktop.
a curvy bouncing ball
<!DOCTYPE HTML>
<html>
<head>
<style>
body {
margin: 0px;
padding: 0px;
}
</style>
</head>
<body>
<div id="container"></div>
<script src="http://d3lp1msu2r81bx.cloudfront.net/kjs/js/lib/kinetic-v5.0.1.min.js"></script>
<script defer="defer">
/*
* 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, frame) {
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 curveContext = curveLayer.getContext();
var x = ball.getX();
var y = ball.getY();
var radius = ball.getRadius();
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 ballLayer = ball.getLayer();
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 Kinetic.Stage({
container: 'container',
width: 578,
height: 400
});
var curveLayer = new Kinetic.Layer();
var ballLayer = new Kinetic.Layer();
var radius = 20;
var anim;
var curve = new Kinetic.Shape({
sceneFunc: function(context) {
var height = stage.getHeight();
var width = stage.getWidth();
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 Kinetic.Circle({
x: 190,
y: 20,
radius: radius,
fillBlue: 255,
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 Kinetic.Tween({
node: ball,
fillRed: 255,
fillGreen: 0,
fillBlue: 0,
duration: 0.3,
easing: Kinetic.Easings.EaseOut
});
anim = new Kinetic.Animation(function(frame) {
updateBall(frame)
}, ballLayer);
anim.start();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment