<!--Click the canvas to add ripples!-->
<div id="box">
  <canvas id="animation" width="400" height="400"></canvas>
  <div id="note">
    Made by <a href="https://codepen.io/ImagineProgramming" target="_blank">Bas</a>, inspired by students and education...</a>
  </div>
</div>
/*Click the canvas to add ripples!*/
$dark:   #262626;
$light:  #dcdcd2;
$vsize:  400px;

*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
} 

html, body {
  padding: 0;
  margin: 0;
}

body {
  background: #202020;
  font-family: Roboto;
  color: #454545;
  a {
    text-decoration: none;
    color: #9a9a9a;

    &:hover {
      color: #ffffff;
    }
  }
}

#note {
  text-align: center;
  width: $vsize;
  margin-top: 10px;
}

#box {
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.5);
  position: absolute;
  background: $dark;
  width: $vsize;
  height: $vsize;
  -webkit-user-select: none;  /* Chrome all / Safari all */
  -moz-user-select: none;     /* Firefox all */
  -ms-user-select: none;      /* IE 10+ */
  user-select: none; 
  canvas {
    width: $vsize;
    height: $vsize;
  }
}
View Compiled
// key codes of pressed keys translate from the numeric representation to a name via this object
var keys = {
  37: 'left',
  39: 'right',
  40: 'down',
  38: 'up'
};

// a direction name translates to a number from 1 to 4 and vice versa via this object
var directionMap = {
  'left':  2,
  'right': 1,
  'up':   4,
  'down':  3,
  1: 'right',
  2: 'left',
  3: 'down',
  4: 'up'
};

// this object contains the oposite direction for any direction
var inverseDirectionMap = {
  'left': 'right',
  'right': 'left',
  'up': 'down',
  'down': 'up'
};

// Index based on a grid 40x40 cells
function SnakeSegment(xIndex, yIndex) {
  this.x = xIndex;
  this.y = yIndex;
  this.s = 10; // size, width and height
}

SnakeSegment.prototype.step = function(width, height) {};
SnakeSegment.prototype.draw = function(context) {
  context.fillStyle = '#cecece';
  context.strokeStyle = '#000000';
  context.beginPath();
  context.rect(this.x * this.s, this.y * this.s, this.s, this.s);
  context.fill();
  context.stroke();
  context.closePath();
};

function Snake(numberOfInitialSegments, food) {
  this.reset(numberOfInitialSegments);
  this.food = food;
  this.foodHit = false;
}

Snake.prototype.reset = function(numberOfInitialSegments) {
  this.direction = 3;
  this.score = 0;
  this.t = 0; // the last time the positions were updated
  this.d = 250; // the time it takes before a position needs to be updated, in milliseconds
  this.segments = [];

  if(typeof numberOfInitialSegments !== "number" || numberOfInitialSegments < 3) {
    numberOfInitialSegments = 6;
  }

  for(var i = 0; i < numberOfInitialSegments; i += 1) {
    this.segments.push(new SnakeSegment(10 + i, 10)); 
  }
};

Snake.prototype.wrapX = function (position, width) {
  if(position.x < 0) {
    position.x = width - 1;
  }
  
  if(position.x > width - 1) {
    position.x = 0;
  }
};

Snake.prototype.wrapY = function (position, height) {
  if(position.y < 0) {
    position.y = height - 1;
  }
  
  if(position.y > height - 1) {
    position.y = 0;
  }
};

Snake.prototype.wrap = function (position, width, height) {
  this.wrapX(position, width);
  this.wrapY(position, height);
};

Snake.prototype.step = function(width, height, ignoreDelay) {
  this.foodHit = false;
  
  var now = +new Date();

  if(now - this.t > this.d || ignoreDelay) {
    this.t = now;    
    // Remove the first element, it will be the new head (tail becomes head)
    var segment = new SnakeSegment();

    // Reposition the snake based on the direction property.
    switch(this.direction) {
      case 1: 
        segment.x = this.segments[this.segments.length - 1].x + 1;
        segment.y = this.segments[this.segments.length - 1].y;
        break;
      case 2: 
        segment.x = this.segments[this.segments.length - 1].x - 1;
        segment.y = this.segments[this.segments.length - 1].y;
        break;
      case 3:
        segment.y = this.segments[this.segments.length - 1].y + 1;
        segment.x = this.segments[this.segments.length - 1].x;
        break;
      case 4: 
        segment.y = this.segments[this.segments.length - 1].y - 1;
        segment.x = this.segments[this.segments.length - 1].x;
        break;
    }

    this.wrap(segment, width / segment.s, height / segment.s);
    
    // Push the previous tail to the front of the array, making it the new head
    this.segments.push(segment);
    
    if(this.doesHeadHitFood(this.food)) {
      this.foodHit = true;
      this.score   += 1;
      this.d -= 5;
      this.food.randomPosition(width / 10, height / 10, this);
    } else {
      this.segments.shift(); // remove a tail segment only if no food was hit
    }
  }
};

Snake.prototype.doesHeadHitTail = function() {
  var head = this.segments[this.segments.length - 1];
  for(var i = 0; i < this.segments.length - 2; i += 1) {
    if(this.segments[i].x === head.x && this.segments[i].y === head.y) {
      return true;
    }
  }
};

Snake.prototype.doesHeadHitFood = function(food) {
  var head = this.segments[this.segments.length - 1];
  return (head.x === food.x && head.y === food.y);
};

// Draw all the segments 
Snake.prototype.draw = function(context) {
  for(var i = 0; i < this.segments.length; i += 1) {
    this.segments[i].draw(context);
  }
};

function Food(x, y) {
  this.reset(x, y);
}

Food.prototype.reset = function(x, y) {
  this.x = x;
  this.y = y;
  this.s = 10;
};

Food.prototype.randomPosition = function(widthMax, heightMax, snake) {
  if(snake) {
    var available = false, attempts = 0, max = (widthMax * heightMax);
    
    while(attempts < max) {
      this.x = Math.floor(Math.random() * widthMax);
      this.y = Math.floor(Math.random() * heightMax);

      for(var i = 0; i < snake.segments.length; i += 1) {
        if(this.x !== snake.segments[i].x && this.y !== snake.segments[i].y) {
          available = true;
          break;
        }
      }
      
      attempts += 1;
    }
    
    return available;
  } else {
    this.x = Math.floor(Math.random() * widthMax);
    this.y = Math.floor(Math.random() * heightMax);
    
    return true;
  }
};

Food.prototype.step = function(width, height) {};
Food.prototype.draw = function(context) {
  context.fillStyle   = '#00be00';
  context.strokeStyle = '#000000';
  context.beginPath();
  context.rect(this.x * this.s, this.y * this.s, this.s, this.s);
  context.fill();
  context.stroke();
  context.closePath();
};

function Score(snake) {
  this.snake = snake;
}

Score.prototype.step = function(width, height) {};
Score.prototype.draw = function(context) {
  context.fillStyle    = '#414141';
  context.textAlign    = 'center';
  context.textBaseline = 'middle';
  context.font         = '300 160pt Roboto';
  context.fillText(this.snake.score, context.canvas.width / 2, context.canvas.height / 2);
};

function Crosshair(snake) {
  this.x     = 0;
  this.y     = 0;
  this.w     = 0;
  this.h     = 0;
  this.snake = snake;
  this.head  = null;
};

Crosshair.prototype.step = function(width, height) {
  this.head = this.snake.segments[this.snake.segments.length - 1];
  this.x    = this.head.x;
  this.y    = this.head.y;
  this.w    = width;
  this.h    = height;
};

Crosshair.prototype.draw = function(context) {
  context.fillStyle   = 'rgba(0, 0, 0, 0.1)';
  context.strokeStyle = 'rgba(200, 200, 200, 0.15)';
  context.beginPath();
  context.rect(0, this.y * this.head.s, this.w, 10);
  context.rect(this.x * this.head.s, 0, 10, this.h);
  context.fill();
  context.stroke();
  context.closePath();
};

window.addEventListener('load', function() {
  var canvas = document.getElementById('animation'),
      context = canvas.getContext('2d'),
      renderables = [],
      food = new Food(),
      snake = new Snake(6 + Math.floor(Math.random() * 6), food),
      crosshair = new Crosshair(snake),
      score = new Score(snake);
  
  food.randomPosition(canvas.width / 10, canvas.height / 10);

  renderables.push(score);
  renderables.push(crosshair);
  renderables.push(food);
  renderables.push(snake);

  // Canvas een mooie grootte geven
  canvas.width = 400;
  canvas.height = 400;

  // Update the direction of the snake, only if the new direction is not the direct oposite of the current one
  window.addEventListener('keydown', function(e) {
    var keyCode = (e.keyCode || e.which);
    var directionName = keys[keyCode]; // get a direction name based on key code
    if(typeof directionName === 'string') { // only if we find out that this key code is known, continue
      var currentDirection = directionMap[snake.direction]; // get the name of the current direction
      
      // if the inverse of the current direction is not the new one
      if(inverseDirectionMap[currentDirection] !== directionName && currentDirection !== directionName) { 
        snake.direction = directionMap[directionName]; // set the new direction
        snake.step(canvas.width, canvas.height, true);
        
        e.preventDefault();
      }
    }
  });

  function animation() {
    context.fillStyle = '#333333';
    context.fillRect(0, 0, canvas.width, canvas.height);
    var i;
    
    for(i = 0; i < renderables.length; i += 1) {
      renderables[i].step(canvas.width, canvas.height);
    }
    
    for(i = 0; i < renderables.length; i += 1) {
      renderables[i].draw(context);
    }
    
    if(snake.doesHeadHitTail()) {
      alert('you dead');
      snake.reset(6 + Math.floor(Math.random() * 6));
    }

    requestAnimationFrame(animation);
  }

  animation();
});

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.