A 10 year old stop-motion animation artist teamed up with me to create this mini-game to practice single digit multiplication or addition.

Answer the five questions to help the pipe-cleaner guy in his quest for treasure!

We are hoping to create more episodes in future and would love to hear your feedback: send us an email.
You can play on top of this screen or check out the full screen version:

Addition or Multiplication

The program is very simple. One interesting thing about it is that it alternates between two HTML video elements - while one is being displayed, the other one is hidden and being loaded.
The video files are very small (about 200kB each), but they still could cause annoying buffering issues (delays) without this mechanism.
There is an HTML5 Canvas on top of the videos that is used to display questions, answers and messages.

The entire code is below. Here's a quick overview:

[1-15] HTML setup. The page consists of two video controls and a Canvas. They're all located in the upper left corner of the screen. The Canvas is on top of the videos.
[17-37] Declare variables.
[39-46] This function clears the Canvas when the countdown counter reaches the value of 1. This is used to clear the correct answer after a delay.
[48-62] Randomize a new question plus three incorrect answers, calculate the correct answer using the mathematical operator (+/*).
[64-77] Display the question in a comic speech bubble (67) and the four boxes with answers on the Canvas.
[79-104] When the current video finishes playing:
- switch out the videos (hide the current one and display the buffer)
- load the next video file in the buffer video(85-86)
- the current video becomes the buffer, and vice versa (88-94)
- randomize and display a new question (95-97)
- If all videos completed, display the victory message (98-103)
[106-125] Resize the videos and the Canvas to full screen. This is mainly needed if the player rotates their mobile device (horizontal/vertical). I could not use the native fullscreen capability as in some cases there is no way to get rid of the video controls.
[127-137] Reset all variables when a new game starts and show the red 'play' button (135).
[139-142] Start the first game.
[144-175] When the player clicks:
[146-150] if it's the beginning of the game, play the first video.
[151-152] if there is a question of the screen, get the Y coordinate of the click and:
[153-160] if the correct answer was clicked, play the next video and the 'correct' bleep. Clear the question and the boxes, redraw the correct answer in white. Start the countdown to clearing the correct answer (159).
[161-165] otherwise play the 'incorrect' bleep and redraw the incorrect answer in red.
[168-174] if this was the last video, restart the game.
[177-179] set up event listeners.
[180] kick off the timer mechanism.


<html>
<style>
  body {
    background-colorblack;
  }
</style>
<body>
<video id="video0" style='position:absoluteleft:0top:0' ;>
  <source src="https://od.lk/s/OF8xNjAwMTcxMTdf/0.mp4" type="video/mp4">
</video>
<video id="video1" style='position:absoluteleft:0top:0visibility:hidden'>
  <source src="https://od.lk/s/OF8xNjAwMTcxMTZf/1.mp4" type="video/mp4">
</video>
<canvas id='myCanvaswidth='636height='360style='position:absoluteleft:0top:0'></canvas>
 
<script>
let myCanvas = document.getElementById('myCanvas');
let video = [];
video[0] = document.getElementById('video0');
video[1] = document.getElementById('video1');
let bubbleImage = new Image();
bubbleImage.src = "bubble.png";
let playButton = new Image();
playButton.src = "play.png";
let sound1 = new Audio('correct.mp3')
    , sound2 = new Audio('incorrect.mp3')
    , sound3 = new Audio('win.mp3');
 
video[0].playsInline = 'true';
video[1].playsInline = 'true';
const operator = '*';
const coordinates = [89291225, -1917, -424618231];
const urls = ['https://od.lk/s/OF8xNjAwMTcxMTVf/2.mp4', 'https://od.lk/s/OF8xNjAwMTcxMTRf/3.mp4', 'https://od.lk/s/OF8xNjAwMTcxMTNf/4.mp4', 'https://od.lk/s/OF8xNjAwMTcxMTJf/5.mp4'];
let currentVidmainVideobufferVideocorrectRowfirstNumbersecondNumbercorrectAnsweraspectboxHeightcounterwindowWidthwindowHeight;
let context = myCanvas.getContext('2d');
let mode = 'init';
let answers = [];
 
function updateCounter() {
  if (counter)
    counter--;
  if (counter == 1) {
    context.clearRect(00myCanvas.widthmyCanvas.height);
  }
  window.requestAnimationFrame(updateCounter);
}
 
function newQuestion() {
  firstNumber = Math.floor(Math.random() * 9) + 1;
  secondNumber = Math.floor(Math.random() * 9) + 1;
  correctRow = Math.floor(Math.random() * 4);
  if (operator == '+')
    correctAnswer = firstNumber + secondNumber;
  else
    correctAnswer = firstNumber * secondNumber;
  answers[correctRow] = correctAnswer;
  for (let row = 0row < 4row++) {
    if (row !== correctRow) {
      answers[row] = correctAnswer + Math.floor(Math.random() * 10 + 1);
    }
  }
}
 
function displayQuestion() {
  let xCoordinate = coordinates[currentVid * 2] / aspect;
  let yCoordinate = coordinates[currentVid * 2 + 1] / aspect;
  context.drawImage(bubbleImagexCoordinateyCoordinatebubbleImage.width / aspectbubbleImage.height / aspect);
  context.fillStyle = 'black';
  context.font = 'bold 36px sans-serif';
  context.fillText(firstNumber + ' ' + operator + ' ' + secondNumberxCoordinate + 50 / aspectyCoordinate + 60 / aspect);
  for (let row = 0row < 4row++) {
    context.fillStyle = 'gray';
    context.fillRect(windowWidth * .75row * boxHeightwindowWidth * .25boxHeight * .9);
    context.fillStyle = 'black';
    context.fillText(answers[row], windowWidth * .840 / aspect + row * boxHeight);
  }
}
 
function completed() {
  currentVid++;
  if (currentVid < urls.length + 1) {
    video[bufferVideo].style.visibility = 'visible';
    video[mainVideo].style.visibility = 'hidden';
    if (currentVid < urls.length) {
      video[mainVideo].src = urls[currentVid];
      video[mainVideo].load();
    }
    if (mainVideo == 0) {
      mainVideo = 1;
      bufferVideo = 0;
    } else {
      mainVideo = 0;
      bufferVideo = 1;
    }
    mode = 'question';
    newQuestion();
    displayQuestion();
  } else {
    context.fillStyle = 'black';
    context.fillText('You winClick to restart.', windowWidth / 240);
    sound3.play();
    mode = 'gameOver';
  }
}
 
function resize() {
  windowWidth = window.innerWidth;
  windowHeight = window.innerHeight;
  aspect = 636/window.innerWidth;
  if (360 / aspect > windowHeight) {
    video[0].style.height = windowHeight;
    video[1].style.height = windowHeight;
    aspect = 360 / windowHeight;
  } else {
    video[0].style.width = windowWidth;
    video[1].style.width = windowWidth;
  }
 
  myCanvas.width = windowWidth;
  myCanvas.height = windowHeight;
  boxHeight = windowHeight / 4;
  if (mode == 'question') {
    displayQuestion();
  }
}
 
function reset() {
  counter = 0;
  video[0].load();
  video[1].load();
  currentVid = -1;
  mainVideo = 0;
  bufferVideo = 1;
  mode = 'init';
  context.clearRect(00windowWidthwindowHeight);
  context.drawImage(playButton, (windowWidth - playButton.width) / 2, (windowHeight - playButton.height) / 2playButton.width / aspectplayButton.height / aspect);
}
 
window.onload = function() {
  resize();
  reset();
};
 
myCanvas.onclick = function(e) {
  switch (mode) {
  case 'init':
    context.clearRect(00myCanvas.widthmyCanvas.height);
    video[mainVideo].play();
    mode = 'video';
    break;
  case 'question':
    let y = e.offsetY;
    if (y > correctRow * boxHeight && y < (correctRow + 1) * boxHeight) {
      video[mainVideo].play();
      mode = 'video';
      sound1.play();
      context.clearRect(00myCanvas.widthmyCanvas.height);
      context.fillStyle = 'white';
      context.fillText(correctAnswerwindowWidth * .840 / aspect + correctRow * boxHeight);
      counter = 100;
    } else {
      context.fillStyle = 'red';
      let row = Math.floor(y / boxHeight);
      context.fillText(answers[row], windowWidth * .840 / aspect + row * boxHeight);
      sound2.play();
    }
    break;
  case 'gameOver':
    video[0].src = 'https://od.lk/s/OF8xNjAwMTcxMTdf/0.mp4';
    video[1].src = 'https://od.lk/s/OF8xNjAwMTcxMTZf/1.mp4';
    video[0].style.visibility = 'visible';
    video[1].style.visibility = 'hidden';
    reset();
  }
};
 
window.onresize = resize;
video[0].onended = completed;
video[1].onended = completed;
updateCounter();
</script>
</body>
</html>




Check out these programming tutorials:

JavaScript:

Optical illusion (18 lines)

Spinning squares - visual effect (25 lines)

Oldschool fire effect (20 lines)

Fireworks (60 lines)

Animated fractal (32 lines)

Minesweeper game (100 lines)

Physics engine for beginners

Physics engine - interactive sandbox

Physics engine - silly contraption

Starfield (21 lines)

Yin Yang with a twist (4 circles and 20 lines)

Tile map editor (70 lines)

Sine scroller (30 lines)

Turtle graphics

Interactive animated sprites

Image transition effect (16 lines)

Wholla lotta quadratic curves (50 lines)

Animated particle constellations

Your first program in JavaScript: you need 5 minutes and a notepad


Fractals in Excel

Python in Blender 3d:

Domino effect (10 lines)


Wrecking ball effect (14 lines)

3d fractal in Blender Python