Newton's cannonball was a thought experiment Isaac Newton used to hypothesize that the force of gravity was universal...
https://en.wikipedia.org/wiki/Newton%27s_cannonball
The cannon fires a ball that falls to earth. The cannon adds some power to the next shot and the cannonball goes a bit farther. Eventually the ball flies with enough velocity to break the planets gravity and falls into orbit. The ball literally falls around the planet indefinitely.
I built this in Game Maker in like 2002...ish but, of course I didn't need to do the math as gravity was built in.
I am not a Mathologer by any means but, I did want to figure it out for myself and after some research and fiddling I was able.
Github Repository
https://github.com/061375/Newtons-Cannonball
Online Demo
http://demo.jeremyheminger.com/Newtons-Cannonball/
Code Pen
https://codepen.io/061375/pen/zmWvMp

/**
* Newtons Cannon
* https://en.wikipedia.org/wiki/Newton%27s_cannonball
* @author Jeremy Heminger <contact@jeremyheminger.com>
* @github https://github.com/061375/Newtons-Cannonball
* @website http://jeremyheminger.com
*
* @version 1.0.4
* @date October 2018
*
* Credit: Vector class from here: https://codepen.io/akm2/pen/rHIsa
*
*
* */
const G = 6.674e-11,
WW = window.innerWidth,
HH = window.innerHeight;
// initialize variables
var canvas, ctx, _cannon, _cannonball, _planet, _objects, c_speed = 0.1 , cspeed = 0.8, isrunnning = true, W, H, hW, hH;
// initalize listeners
window.addEventListener('load', init, false);
window.addEventListener('blur', stopLoop, false);
window.addEventListener('focus', startLoop, false);
window.addEventListener('resize', resizeHandler, false);
/**
* initalize
* @function init
* */
function init(e, b_reset) {
W = document.getElementById('container').clientWidth;
H = document.getElementById('container').clientHeight;
hW = (W/2);
hH = (H/2);
PLANETRADIUS = (H/3);
if (undefined === b_reset) {
// create the canvas
canvas = document.createElement('canvas');
ctx = canvas.getContext('2d');
}
// instantiate our classes
_planet = new Planet(hW,hH,PLANETRADIUS,-90);
_cannon = new Cannon(_planet.get().x,_planet.get().y,PLANETRADIUS,-90);
_cannonball = new Cannonball(_cannon.get().x,_cannon.get().cannon.breach.y);
// add the objects in the order want them drawn
// from back to front
_objects = [
_cannonball,
_planet,
_cannon
];
if (undefined === b_reset) {
// add the canvas
document.getElementById('container').appendChild(canvas);
makeStars(50);
}
canvas.height = H;
canvas.width = W;
if (undefined === b_reset) {
// start rendering
this.render();
}
}
/**
* make some stars
* @method makeStars
* @param {Number}
* */
function makeStars(n) {
for(let i=0; i<n; i++) {
let s = document.createElement('div');
s.setAttribute('class','star');
s.style.left = (Math.random() * WW)+'px';
s.style.top = (Math.random() * HH)+'px';
document.getElementById('stars').appendChild(s);
}
}
/**
* just like the name implies
* @method stopLoop
* */
function stopLoop() {
isrunnning = false;
console.log('stop loop');
}
/**
* just like the name implies
* @method startLoop
* */
function startLoop() {
isrunnning = true;
}
/**
* set the size of the canvas
* @note currently this really only works once
* @function resizeHandler
* */
function resizeHandler() {
canvas.height = H;
canvas.width = W;
_objects = [];
_planet = null;
_cannon = null;
_cannonball = null;
isrunnning = true;
init(null, true);
}
/**
* clear the canvas
* @function clear
* */
function clear() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
/**
* render the visible stuff
* @method render
* */
function render() {
requestAnimationFrame(render);
// if true then render
if(isrunnning) {
// clear
this.clear();
// save
ctx.save();
// loop everything
for(let i=0; i<_objects.length; i++) {
_objects[i].loop();
_objects[i].draw();
}
// restore
ctx.restore();
}
}
/**
*
* @function trig
* @param {Number}
* @param {Number}
* @param {Number}
* @param {Number}
* @param {Boolean}
* @returns {Mixed}
* */
function trig(x,y,r,d,array) {
if(d<0)d+=360;
if(d>360)d-=360;
let a = d * Math.PI / 180;
let xpos = r * Math.cos(a);
let ypos = r * Math.sin(a);
if(array) {
return [
xpos+x,
ypos+y
]
}else{
return {
x:xpos+x,
y:ypos+y
}
}
}
/**
*
* @function distance
* @param {Number}
* @param {Number}
* @param {Number}
* @param {Number}
* @returns {Number}
* */
function distance(x1,y1,x2,y2) {
return Math.hypot(x2-x1, y2-y1);
}
/**
*
* @class
* */
class Planet {
/**
* @param {Number}
* @param {Number}
* @param {Number}
* @param {Number}
* */
constructor(x,y,r,d) {
this.vars = {
x:x,
y:y,
r:r,
d:d,
mass:12e+12,
color:'#008000'
}
}
/**
* no need for a loop here but its called automatically
* @method loop
* */
loop() {}
/**
* @method draw
* */
draw() {
ctx.beginPath();
// handle the shadow on resize
let s1 = 300;
let s2 = 800;
if (H < 550) {
s1 = 100;
s2 = 300;
}
var grd=ctx.createRadialGradient(75,50,s1,90,60,s2);
grd.addColorStop(0,this.vars.color);
grd.addColorStop(1,'#000');
ctx.fillStyle=grd;
ctx.arc(this.vars.x,this.vars.y,this.vars.r,0,Math.PI*2,true);
ctx.closePath();
ctx.fill();
}
/**
* @method get
* @returns {Object}
* */
get() {
return this.vars;
}
}
/**
* @class Canon
* */
class Cannon {
/**
* @param {Number}
* @param {Number}
* @param {Number}
* @param {Number}
* */
constructor(x,y,r,d) {
// @var {Number} An amount to resize objects in relation to the planet size
this.divsize = 25;
// @var {Number}
this.r = r;
// @var {Number}
this.d = d;
// @var {Object}
this.pos = trig(x,y,r,d,false);
// @var {Object}
this.vars = {
x:this.pos.x,
y:this.pos.y,
mountain:{
coords:[],
color:'#008000'
},
cannon:{
barrel:{
x:0,
y:0,
w:0,
h:0
},
breach:{
x:0,
y:0,
r:0
},
x:0,
y:0,
r:0,
color:'#000000'
}
}
this.mountain();
this.makecannon();
}
/**
* no need for a loop here but its called automatically
* @method loop
* */
loop() {}
/**
* @method draw
* */
draw() {
ctx.fillStyle=this.vars.cannon.color;
// cannon wheel
ctx.beginPath();
ctx.arc(this.vars.cannon.x,this.vars.cannon.y,this.vars.cannon.r,0,Math.PI*2,true);
ctx.closePath();
ctx.fill();
// cannon breach
ctx.beginPath();
ctx.arc(this.vars.cannon.breach.x,this.vars.cannon.breach.y,this.vars.cannon.breach.r,0,Math.PI*2,true);
ctx.closePath();
ctx.fill();
// cannon barrel
ctx.beginPath();
ctx.fillRect(
this.vars.cannon.barrel.x,
this.vars.cannon.barrel.y,
this.vars.cannon.barrel.w,
this.vars.cannon.barrel.h
);
// mountain
ctx.fillStyle=this.vars.mountain.color;
ctx.beginPath();
ctx.moveTo(this.vars.mountain.coords[0][0],this.vars.mountain.coords[0][1]);
for (let i = 0;i<this.vars.mountain.coords.length; i++) {
ctx.lineTo(
this.vars.mountain.coords[i][0],
this.vars.mountain.coords[i][1]
);
}
ctx.fill();
}
/**
* establish the mountains polygon
* @method mountain
* */
mountain() {
// create the polygon coords for the mountain
this.vars.mountain.coords[0] = trig(this.pos.x,this.pos.y+5,(this.r/(this.divsize/2)),(this.d-90),true);
this.vars.mountain.coords[1] = trig(this.pos.x,this.pos.y,(this.r/(this.divsize/5)),this.d-10,true);
this.vars.mountain.coords[2] = trig(this.pos.x,this.pos.y,(this.r/(this.divsize/5)),this.d+10,true);
this.vars.mountain.coords[3] = trig(this.pos.x,this.pos.y+5,(this.r/(this.divsize/2)),(this.d+90),true);
}
/**
* @method makecanon
* */
makecannon() {
// cannon wheel
let xy = trig(this.pos.x,this.pos.y,(this.r/(this.divsize/6)),this.d,false);
this.vars.cannon.x = xy.x;
this.vars.cannon.y = xy.y+5;
this.vars.cannon.r = Math.abs((this.r/this.divsize)-5);
// breach
xy = trig(this.vars.cannon.x,this.vars.cannon.y,this.vars.cannon.r,-120);
this.vars.cannon.breach.x = xy.x;
this.vars.cannon.breach.y = xy.y;
this.vars.cannon.breach.r = this.vars.cannon.r/2;
this.vars.cannon.barrel.x = xy.x;
this.vars.cannon.barrel.y = xy.y-3;
this.vars.cannon.barrel.w = this.vars.cannon.r*3;
this.vars.cannon.barrel.h = this.vars.cannon.r;
}
/**
* @method get
* @returns {Object}
* */
get() {
return this.vars;
}
}
/**
* @method Canonball
* */
class Cannonball {
constructor(x,y) {
//
Vector.call(this, x, y);
// @var {Number}
this.startx = x;
// @var {Number}
this.starty = y;
// @var {Number}
this.deadtimer = 0; // this counts until the reset
// @var {Number}
this.resettimer = 1; // raise this to set a pause beween resets
// @var {Object}
this.vars = {
x:x,
y:y,
r:3,
dead:false,
speed: new Vector(cspeed,0),
dir:0,
mass:2e+2,
planet:{},
color:'#fff'
};
}
/**
*
* @method loop
* */
loop() {
//
this.getplanet();
//
if(this.collision())
this.destroy();
//
if(this.vars.dead) {
// move the cannonball off the stage
this.vars.x = 1000000;
// increment the timer
this.deadtimer++;
// if timer greater than reset
if(this.deadtimer > this.resettimer)
this.reset();
}else{
// THIS IS WHERE THE MAGIC HAPPENS
// The math that is
// @todo - reduce the code here
// some of this can be combined
// init empty vector
let a = new Vector(0,0);
this.vars.planet.rv.x -= this.vars.x;
this.vars.planet.rv.y -= this.vars.y;
// I was using V.length() however it isn't updated by the class
// the maths to do so is easy enough so I just called thew standard method
let d = this.vars.planet.distance;
let n = this.vars.planet.rv.normalize();
// stuck in the gravity well
if ( d < 20 ) {
n.x *= Math.pow(d/20,5);
n.y *= Math.pow(d/20,5);
}
let m = _planet.get().mass;
m = m + this.vars.mass;
a.x += n.x*(G*m)/(d*d);
a.y += n.y*(G*m)/(d*d);
this.vars.speed.x += a.x;
this.vars.speed.y += a.y;
this.vars.x+=this.vars.speed.x;
this.vars.y+=this.vars.speed.y;
}
}
/**
* @method draw
* */
draw() {
ctx.fillStyle=this.vars.color;
ctx.beginPath();
ctx.arc(this.vars.x,this.vars.y,this.vars.r,0,Math.PI*2,true);
ctx.closePath();
ctx.fill();
}
/**
* @method
* */
addSpeed(d) {
this.vars.speed.add(d);
}
/**
* comment
* @method getplanet
* */
getplanet() {
this.vars.planet.rv = new Vector(_planet.get().x, _planet.get().y);
this.vars.planet.r = _planet.get().r;
// get the distance to the planet
this.vars.planet.distance = distance(
this.vars.x,
this.vars.y,
this.vars.planet.rv.x,
this.vars.planet.rv.y
);
}
/**
*
* @method collision
* */
collision() {
// check the distance to the planet
if(this.vars.planet.distance < this.vars.planet.r)
{
return true;
}else{
return false;
}
}
/**
*
* @method destroy
* */
destroy() {
this.vars.dead = true;
}
/**
*
* @method reset
* */
reset() {
this.deadtimer = 0;
// place it back home
this.vars.x = this.startx;
this.vars.y = this.starty;
// reset var
this.vars.dead = false;
// increment speed
cspeed += c_speed;
this.vars.speed = new Vector(cspeed,0);
}
/**
*
* @method get
* @returns {Object}
* */
get() {
return {
x:this.vars.x,
y:this.vars.y
}
}
}
/**
* @NOTE - I always thought that in the abstract Vectors and Tensors were essentially Objects
* This class proves my observation
**/
/**
* Vector
*/
function Vector(x, y) {
this.x = x || 0;
this.y = y || 0;
}
Vector.add = function(a, b) {
return new Vector(a.x + b.x, a.y + b.y);
};
Vector.sub = function(a, b) {
return new Vector(a.x - b.x, a.y - b.y);
};
Vector.scale = function(v, s) {
return v.clone().scale(s);
};
Vector.random = function() {
return new Vector(
Math.random() * 2 - 1,
Math.random() * 2 - 1
);
};
/**
* comment
* @method
* */
Vector.prototype = {
set: function(x, y) {
if (typeof x === 'object') {
y = x.y;
x = x.x;
}
this.x = x || 0;
this.y = y || 0;
return this;
},
add: function(v) {
this.x += v.x;
this.y += v.y;
return this;
},
sub: function(v) {
this.x -= v.x;
this.y -= v.y;
return this;
},
scale: function(s) {
this.x *= s;
this.y *= s;
return this;
},
length: function() {
return Math.sqrt(this.x * this.x + this.y * this.y);
},
lengthSq: function() {
return this.x * this.x + this.y * this.y;
},
normalize: function() {
var m = Math.sqrt(this.x * this.x + this.y * this.y);
if (m) {
this.x /= m;
this.y /= m;
}
return this;
},
angle: function() {
return Math.atan2(this.y, this.x);
},
angleTo: function(v) {
var dx = v.x - this.x,
dy = v.y - this.y;
return Math.atan2(dy, dx);
},
distanceTo: function(v) {
var dx = v.x - this.x,
dy = v.y - this.y;
return Math.sqrt(dx * dx + dy * dy);
},
distanceToSq: function(v) {
var dx = v.x - this.x,
dy = v.y - this.y;
return dx * dx + dy * dy;
},
lerp: function(v, t) {
this.x += (v.x - this.x) * t;
this.y += (v.y - this.y) * t;
return this;
},
clone: function() {
return new Vector(this.x, this.y);
},
toString: function() {
return '(x:' + this.x + ', y:' + this.y + ')';
}
};


