Newton's Cannon

Wed, 10/17/2018 - 18:57

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

 

Newton Cannon

 

/** 
 * 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 + ')';
    }
};
Categories
Image
Newton's Cannon