diff --git a/bouncing-balls-vim-motions/README.md b/bouncing-balls-vim-motions/README.md
new file mode 100644
index 00000000..09f103a7
--- /dev/null
+++ b/bouncing-balls-vim-motions/README.md
@@ -0,0 +1,11 @@
+This is a simple game that is intended to help a user
+become accoustomed to using vim motions to control a pointer.
+
+
+The game consists of several balls generated at random that bounce across the screen.
+When the pointer is collides with a ball the ball is destroyed.
+
+H- moves LEFT
+J- UP
+K- DOWN
+L- RIGHT
diff --git a/bouncing-balls-vim-motions/index.html b/bouncing-balls-vim-motions/index.html
new file mode 100644
index 00000000..4071373f
--- /dev/null
+++ b/bouncing-balls-vim-motions/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+ Bouncing balls
+
+
+
+
+ bouncing balls
+
+
+
+
+
+
\ No newline at end of file
diff --git a/bouncing-balls-vim-motions/main.js b/bouncing-balls-vim-motions/main.js
new file mode 100644
index 00000000..be363567
--- /dev/null
+++ b/bouncing-balls-vim-motions/main.js
@@ -0,0 +1,199 @@
+// setup canvas
+const canvas = document.querySelector('canvas');
+const ctx = canvas.getContext('2d');
+
+const width = canvas.width = window.innerWidth;
+const height = canvas.height = window.innerHeight;
+
+const para = document.querySelector('p');
+
+// function to generate random number
+function random(min, max) {
+ const num = Math.floor(Math.random() * (max - min + 1)) + min;
+ return num;
+}
+
+// function to generate random color
+function randomRGB() {
+ return `rgb(${random(0, 255)},${random(0, 255)},${random(0, 255)})`;
+}
+
+class Shape {
+ constructor(x, y, velX, velY, exists) {
+ this.x = x;
+ this.y = y;
+ this.exists = true;
+ this.velX = velX;
+ this.velY = velY;
+ }
+}
+
+class EvilCircle extends Shape {
+ constructor(x, y) {
+ super(x, y, 20, 20);
+ this.color = "white";
+ this.size = 30;
+ window.addEventListener("keydown", (e) => {
+ switch (e.key) {
+ case "h":
+ this.x -= this.velX;
+ break;
+ case "l":
+ this.x += this.velX;
+ break;
+ case "k":
+ this.y -= this.velY;
+ break;
+ case "j":
+ this.y += this.velY;
+ break;
+ }
+ });
+ }
+ draw() {
+ ctx.beginPath();
+ ctx.lineWidth = 3;
+ ctx.strokeStyle = this.color;
+ ctx.arc(this.x, this.y, this.size, 0, 2 * Math.PI);
+ ctx.stroke();
+ }
+ checkBounds() {
+ if ((this.x + this.size) >= width) {
+ this.x = width - this.size;
+ }
+
+ if ((this.x - this.size) <= 0) {
+ this.x = this.size;
+ }
+
+ if ((this.y + this.size) >= height) {
+ this.y = height - this.size;
+ }
+
+ if ((this.y - this.size) <= 0) {
+ this.y = this.size;
+ }
+ }
+
+ collisionDetect() {
+ for (const ball of balls) {
+ if (ball.exists) {
+ const dx = this.x - ball.x;
+ const dy = this.y - ball.y;
+ const distance = Math.sqrt(dx * dx + dy * dy);
+
+ if (distance < this.size + ball.size) {
+ ball.exists = false;
+ }
+ }
+ }
+ }
+
+
+}
+
+class Ball extends Shape {
+ constructor(x, y, velX, velY, color, size) {
+ super(x, y, velX, velY);
+ this.color = color;
+ this.size = size;
+ }
+
+
+ draw() {
+ ctx.beginPath();
+ ctx.fillStyle = this.color;
+ ctx.arc(this.x, this.y, this.size, 0, 2 * Math.PI);
+ ctx.fill();
+ }
+
+ update() {
+ if ((this.x + this.size) >= width) {
+ this.velX = -(this.velX);
+ }
+
+ if ((this.x - this.size) <= 0) {
+ this.velX = -(this.velX);
+ }
+
+ if ((this.y + this.size) >= height) {
+ this.velY = -(this.velY);
+ }
+
+ if ((this.y - this.size) <= 0) {
+ this.velY = -(this.velY);
+ }
+
+ this.x += this.velX;
+ this.y += this.velY;
+ }
+
+ collisionDetect() {
+ for (const ball of balls) {
+ if (!(this === ball) && ball.exists) {
+ const dx = this.x - ball.x;
+ const dy = this.y - ball.y;
+ const distance = Math.sqrt(dx * dx + dy * dy);
+
+ if (distance < this.size + ball.size) {
+ ball.color = this.color = randomRGB();
+ this.velX = -(this.velX);
+ this.velY = -(this.velY);
+ }
+ }
+ }
+ }
+
+}
+
+const balls = [];
+
+while (balls.length < 25) {
+ const size = random(10, 20);
+ const ball = new Ball(
+ // ball position always drawn at least one ball width
+ // away from the edge of the canvas, to avoid drawing errors
+ random(0 + size, width - size),
+ random(0 + size, height - size),
+ random(-7, 7),
+ random(-7, 7),
+ randomRGB(),
+ size
+ );
+
+ balls.push(ball);
+}
+
+const evilCircle = new EvilCircle(random(0, width), random(0, height));
+
+let count = 0;
+
+function loop() {
+ ctx.fillStyle = "rgba(0, 0, 0, 0.25)";
+ ctx.fillRect(0, 0, width, height);
+
+ for (const ball of balls) {
+ if (ball.exists) {
+ ball.draw();
+ ball.update();
+ ball.collisionDetect();
+ }
+ }
+ count = 0;
+ for (const ball of balls) {
+ if (ball.exists) {
+ count++;
+ }
+ }
+ para.textContent = `Ball count: ${count}`;
+ if (count > 0) {
+ evilCircle.draw();
+ evilCircle.checkBounds();
+ evilCircle.collisionDetect();
+ }
+
+ requestAnimationFrame(loop);
+ }
+
+
+loop();
\ No newline at end of file
diff --git a/bouncing-balls-vim-motions/style.css b/bouncing-balls-vim-motions/style.css
new file mode 100644
index 00000000..130e9764
--- /dev/null
+++ b/bouncing-balls-vim-motions/style.css
@@ -0,0 +1,33 @@
+html, body {
+ margin: 0;
+}
+
+html {
+ font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ height: 100%;
+}
+
+body {
+ overflow: hidden;
+ height: inherit;
+}
+
+h1 {
+ font-size: 2rem;
+ letter-spacing: -1px;
+ position: absolute;
+ margin: 0;
+ top: -4px;
+ right: 5px;
+
+ color: transparent;
+ text-shadow: 0 0 4px white;
+}
+
+p {
+ position: absolute;
+ margin: 0;
+ top: 35px;
+ right: 5px;
+ color: #aaa;
+}