./exe/game/readme.txt
Here is a 2d game engine written in javascript using the HTML canvas interface. 
The little robot can jet around, jump multiple times, and throw a ball.

Keyboard/Mouse controls:
	wasd/arrows: move
	mouse: throw ball
	[ : shorter trail
	] : longer trail
	- : thinner trail
	= : wider trail
	. : display physics
	h : toggle platforms/floor
	g : toggle sprite visibility

As it is, this is not really a "game" yet, since there is not really any goal. Instead, it offers an engine that can be used as a foundation for a game. It is here to show you how you can start setting up keyboard controls, jumping, gravity, friction, collisions, item interactions, and animations - all the elements that are needed to start making a platformer game. There is quite a lot of code here, so be patient if you would like to understand this one. A lot of work goes into making the physics in a game feel right: is the movement smooth? do the collisions feel solid? is the jump satisfying? In a game it should feel fun just to move around, even if there is nothing to do.

advanced practice:
take any parts that you like from this code and combine them to make your own 2D game.
./exe/game/main.html
<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title>2d_engine</title>
		<link rel="stylesheet" href="style.css">
		</style>
	</head>
	<body>
		<canvas id	= "game_space"></canvas>
		<img    src 	= "caught_flying.png" alt="" id="caught_flying" />
		<img    src 	= "caught_standing.png" alt="" id="caught_standing" />
		<img 	src	= "robot_standing.png" alt="" id="robot_standing" />
		<img 	src	= "robot_flying.png" alt="" id="right_flying" />
		<script src = "canvas.js"> </script>
		<script src = "platforms.js"> </script>
		<script src = "player.js"> </script>
		<script src = "ball.js"> </script>
		<script src = "game.js"> </script>
	</body>
</html>

./exe/game/style.css
body {
	background: #333540;
	cursor: crosshair;
}

canvas {	
	cursor : url(crosshair.png),crosshair;
	border-width : 10px;
	border-style : solid;
	border-color : #778;
	background: #fff;
	border-radius: 5px;
	padding-left : 0 ;
	padding-right : 0;
	margin-left : auto;
	margin-right : auto;
	display : block;
	max-width: 90vw;
	max-height: 90vh;
}

img {
	display: none;
}
./exe/game/game.js

let interval_id = start_animation();

function start_animation(){
	let id = setInterval( () => {
		clear();
		if (platforms === platforms1)
			move_platforms();
		new_acc();
		new_vel();
		new_pos();
		ball_vel();
		ball_pos();
		draw_ball();
		draw_trail();
		if (show_sprite)
			draw_player();
		info();
	}, 20);
	return id;
}

let show_sprite = true;

function inside_x(a ,b)	{ return (((a.x + a.w) > b.x) && (a.x < (b.x + b.w)));}
function inside_y(a ,b)	{ return (((a.y + a.h) > b.y) && (a.y < (b.y + b.h)));}

function on     (a, b)	{ return (  Math.abs(a.y + a.h - b.y) <= (a.dy - b.dy)) }
function beside (a, b) 	{ return (  Math.abs(a.x + a.w - b.x) <= (a.dx - b.dx)) }

function on_platform(me){
	for (let i = 0; i < platforms.length; i++)
	{
		if 	( me.dy >= 0 &&
		   	(on(me, platforms[i])) &&
			(inside_x(me, platforms[i])))
		{
			if (platforms[i] === me.on)
				return;
			me.on = platforms[i];
			return;
		}
	}
	// otherwise no platform was engaged
	me.on = undefined;
}

./exe/game/platforms.js
class Platform {
	constructor(name, w, h, x, y, dx, dy){
		this.name = name;
		this.w = w;
		this.h = h;
		this.x = x;
		this.y = y;
		this.dx = dx;
		this.dy = dy;
	}

	move() {
		if (this.x + this.w > canvas.width || this.x < 0)
			this.dx *= -1;

		this.x += this.dx;
		this.y += this.dy;

		this.x = Math.round(this.x);
		this.y = Math.round(this.y);
	}

	draw() {
		if (this === player.on){
		 	ctx.fillStyle = '#abc';
			ctx.fillRect(this.x, this.y, this.w, this.h);
			ctx.fillStyle = '#fff';
			ctx.fillText(this.name, this.x + this.w/2 - 5, this.y + 25);
		} else {
			ctx.fillStyle = '#468';
			ctx.fillRect(this.x, this.y, this.w, this.h);
		}
		reset_background_fill();
	}
}

//TODO make a platform yielding function 
// to replace ones that go through floor

let platforms1 = [
	new Platform("5",100,40,0,100,4,0),
	new Platform("4",200,40,0,200,6,0),
	new Platform("3",300,40,0,300,2,0),
	new Platform("2",300,40,0,400,1,0),
	new Platform("1",600,40,0,500,3,0),
	new Platform("",canvas.width,40,0,canvas.height-10,0,0),
]

	new Platform("c",canvas.width,40,0,-40,0,0),
	new Platform("r",40,canvas.height,canvas.width,0,0,0),
	new Platform("l",40,canvas.height,-40,0,0,0)

platforms2 = [
	new Platform("",canvas.width,40,0,canvas.height-10,0,0)
]

platforms3 = [
];


let platforms = platforms1;

function move_platforms() {
	platforms.forEach(platform => {
		platform.move()
		platform.draw()
	});
	reset_background_fill();
}

./exe/game/canvas.js
const canvas = document.querySelector('#game_space');
canvas.width = 1200;
canvas.height = 700;
const ctx = canvas.getContext('2d'); // essential line
const robot_standing 	= document.getElementById('robot_standing');
const robot_flying 		= document.getElementById('robot_flying');
const caught_standing 	= document.getElementById('caught_standing');
const caught_flying 	= document.getElementById('caught_flying');
ctx.font = "Bold " + 20  +"px Courier";

// need this because javascript % is 
// wierd with negative numbers
Number.prototype.mod = function(n) {
        return ((this%n)+n)%n;
};

window.oncontextmenu = function () {
  return false;
}

let animations = 
[
	[caught_flying, caught_standing ],
	[robot_flying, robot_standing ]
]

function reset_background_fill() {
	//ctx.fillStyle = "#89a";
	ctx.fillStyle = "#457";
}

reset_background_fill();

let show_info = 0;

function info() {
	if (show_info){
		ctx.fillStyle = '#000';
		ctx.fillText('====BOT=====',         10, 20  );
		ctx.fillText('   x : ' + player.x,   10, 40  );
		ctx.fillText('   y : ' + player.y,   10, 60  );
		ctx.fillText('  dx : ' + player.dx,  10, 80  );
		ctx.fillText('  dy : ' + player.dy,  10, 100 );
		ctx.fillText(' ddx : ' + player.ddx, 10, 120 );
		ctx.fillText(' ddy : ' + player.ddy, 10, 140 );
		ctx.fillText('=---BALL---=', 		 10, 160 ); 
		ctx.fillText('   x : ' + ball.x,  	 10, 180 );
		ctx.fillText('   y : ' + ball.y,  	 10, 200 );
		ctx.fillText('  dx : ' + ball.dx, 	 10, 220 );
		ctx.fillText('  dy : ' + ball.dy, 	 10, 240 );
		ctx.fillText(' ddx : ' + ball.ddx, 	 10, 260 );
		ctx.fillText(' ddy : ' + ball.ddy, 	 10, 280 );
		ctx.fillText('============',         10, 300 );
		reset_background_fill();
	}
}

function clear() {
	ctx.fillRect(0, 0, canvas.width, canvas.height);
}

./exe/game/ball.js
const PI = 3.1415926535;

const ball = {
	r: 8,
	w: 8,
	h: 8,
	x: player.x,
	y: player.y,
	dx: player.dx,
	dy: player.dy,
	ddx: player.ddx,
	ddy: player.ddy,
	caught : 1,
	on: undefined,
	friction: 0.1,
	max_vel: Infinity,
}


function follow_player(){
	ball.x = player.x + 21;
	ball.y = player.y - 6;
	ball.dx = player.dx;
	ball.dy = player.dy;
}

follow_player();


let MAX_TRAIL_RADIUS = ball.r * 8;
let TRAIL_LENGTH = 8;
const MAX_TRAIL_LENGTH = 500;
ball_trail = [];

canvas.onpointerdown = (event) => {
	if (ball.caught === 1){
		bb = canvas.getBoundingClientRect();
		let x = (event.clientX-bb.left)*(canvas.width/bb.width);
		let y = (event.clientY-bb.top)*(canvas.height/bb.height);
		x += 10;
		y += 10;
		ball.dx = Math.round((x - ball.x)/10) + player.dx;
		ball.dy = Math.round((y - ball.y)/10) + player.dy;;
		while (inside_x(ball, player) && inside_y(ball, player)){
			ball.x += ball.dx;
			ball.y += ball.dy;
		}
		ball.caught = 0;
		image = robot_standing;
	}
};

function ball_vel() {
	if (inside_x(ball, player) && inside_y(ball, player)){
		ball.caught = 1;
		image = caught_standing;
	}

	if (ball.caught)
		return;

	on_platform(ball);
	
	ball.dx += ball.ddx;
	
	if (ball.on){ //friction
		if (ball.dx > ball.on.dx)
			ball.dx -= ball.friction;
		if (ball.dx < ball.on.dx)
			ball.dx += ball.friction;
		if (Math.abs(ball.dy) < 5){
			ball.dy = 0;
		} else { 
			ball.dy *= -0.8;
		}
			
	} else {
		ball.dy += ball.ddy;
	}
}

function correct_vel(){

	if (ball.dx > ball.max_vel){
		ball.dx = ball.max_vel;
	}
	if (ball.dx < -ball.max_vel){
		ball.dx = -ball.max_vel;
	}
	if (ball.dy > ball.max_vel){
		ball.dy = ball.max_vel;
	}
	if (ball.dy < -ball.max_vel){
		ball.dy = -ball.max_vel;
	}
	ball.dx = Math.round(ball.dx);
	ball.dy = Math.round(ball.dy);
}

function ball_pos(){
	if (ball.on) {
		ball.y = ball.on.y - ball.r;
	}
	if (ball.caught){
		follow_player();
		return;
	} else {
		touching_wall();
		correct_vel();
		ball.x += ball.dx;
		ball.y += ball.dy;
	}
}


function draw_trail(){
	while (ball_trail.length > TRAIL_LENGTH){
		ball_trail.shift();
	}
	ball_trail.push([ball.x, ball.y]);
	for (let i = 0; i < ball_trail.length; i++){
		//let float_amount = Math.round((ball_trail.length - i)/2);
		let float_amount = 0;
		ctx.globalAlpha = i/(ball_trail.length)/3;
		draw_ball(ball_trail[i][0], ball_trail[i][1] - float_amount, 
			(MAX_TRAIL_RADIUS) - ((MAX_TRAIL_RADIUS - ball.r)/ball_trail.length)*i);
	}
	ctx.globalAlpha = 1;
}

function draw_ball(x = ball.x, y = ball.y, r = ball.r){
	let mem = ctx.fillStyle;
	ctx.fillStyle = "#ADE";
	ctx.beginPath();
	ctx.arc(x, y, r, 0, 2*PI, false);
	ctx.fill();
	ctx.fillStyle = "#336699";
	ctx.beginPath();
	ctx.arc(x, y, 2*r/3, 0, 2*PI, false);
	ctx.fill();
	ctx.fillStyle = mem;
}


function touching_wall(){
	//console.log("collision check");
	if (ball.x + 2*ball.r > canvas.width){	
		ball.x = canvas.width - 2*ball.r;
		ball.dx *= -0.8;
	}
	if	(ball.x < 0){
		ball.x = 0 ;
		ball.dx *= -0.8
	}

	if (ball.y + 2*ball.r > canvas.height){
		ball.y = canvas.height - 2*ball.r;
		ball.dy *= -0.8;
	}
	if (ball.y < 0){
		ball.y = 0;
		ball.dy *= -0.8;
	}
}

./exe/game/player.js
const player = {
	w 			: 40		, 
	h 			: 40		,
	x 			: 1000		, 
	y 			: 100		, 
	dx			: 0			, 
	dy			: 0			,
	ddx 		: 0			, 
	ddy 		: 1			,
	dddx        : 0         ,
	dddy        : 0         ,
	on			: undefined	,
	jumped   	: 0			,
	friction 	: 0.5		,
};

let image = robot_standing;

function new_acc() {
	player.ddx += player.dddx;
	player.ddy += player.dddy;
}

function new_vel() {
	on_platform(player);	
	player.dx += player.ddx;
	if (player.on){ //friction
		if (player.dx > player.on.dx)
			player.dx -= player.friction;
		if (player.dx < player.on.dx)
			player.dx += player.friction;
		player.dy = player.on.dy;
	} else {
		player.dy += player.ddy;
	}
}

function new_pos() { 	
	if (player.on){
		player.y = player.on.y - player.h;
	}
	
	player.x += Math.round(player.dx);
	player.y += Math.round(player.dy);

	player.x = player.x.mod(canvas.width);
	player.y = player.y.mod(canvas.height);

}

function draw_player() {
	ctx.drawImage(image, player.x, player.y, player.w, player.h);
}

// KEY LISTENERS
function keyDown(e) {
	switch (e.key){
		case ' ':
		case 'w':
		case 'ArrowUp':
			if (player.jumped === 0){
				player.on = undefined;
				player.dy = -15;
				player.jumped = 1;
			}
			break;
		case 'a':
		case 'ArrowLeft':
			player.ddx = -1;
			break;
		case 'd':
		case 'ArrowRight':
			player.ddx = 1;
			break;
		case 'Escape':
			clearInterval(interval_id);
			break;
		case 'h':
			player.dddx = -1;
			break;
		case 'l':
			player.dddx = 1;
			break;
		case 'p':
			switch (platforms){
				case platforms1:
					platforms = platforms2;
					break;
				case platforms2:
					platforms = platforms3;
					break;
				case platforms3:
					platforms = platforms1;
					break;
			}
			break;
		case '.':
			show_info ^= true;
			break;
		case '=':
			MAX_TRAIL_RADIUS += 1;
			break;
		case '-':
			MAX_TRAIL_RADIUS = Math.max(0, MAX_TRAIL_RADIUS - 1);
			break;
		case ']':
			TRAIL_LENGTH = Math.min(MAX_TRAIL_LENGTH, TRAIL_LENGTH + 1);;
			break;
		case '[':
			TRAIL_LENGTH = Math.max(0, TRAIL_LENGTH - 1);
			break;
		case 'g':
			show_sprite ^= true;
			break;

	}
}

function keyUp(e) {
	switch (e.key){
		case ' ':
		case 'w':
		case 'ArrowUp':
			player.jumped = 0;
			break;
		case 'a':
		case 'ArrowLeft':
		case 'd':
		case 'ArrowRight':
			player.ddx = 0;
			break;
		case 'h':
		case 'l':
			player.dddx = 0;
			player.ddx = 0;
			break;
	}
}

						//string: what kind of event listener
						//function: what to do when the event happens
document.addEventListener('keydown'	, keyDown);
document.addEventListener('keyup'	, keyUp);