<div class="container">
<div class="score">
score: <span class="score_val">0</span>
</div>
<div class="canvas-wrapper">
<canvas width="400" height="400"></canvas>
</div>
<div class="controls">
<button class="btn-start">Start</button>
<button class="btn-pause">Pause</button>
<div class="range range-difficulty">
<label for="difficulty">Speed</label>
<div class="range_inputWrapper">
<span class="range_inputSlider"></span>
<div class="range_inputValue"></div>
<input type="range" name="difficulty"
value="8" min="1" max="10" step="1" >
</div>
</div>
<div class="range range-columns">
<label for="columns">Cells</label>
<div class="range_inputWrapper">
<span class="range_inputSlider"></span>
<div class="range_inputValue"></div>
<input type="range" name="columns"
value="20" min="10" max="100" step="10">
</div>
</div>
</div>
</div>
* {
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
margin: 0;
background: darken(#00796B, 5%);
font-family: 'Varela Round';
}
body {
display: flex;
justify-content: center;
align-items: center;
}
.container {
width: 90%;
max-width: 400px;
border: 5px solid #FFA000;
border-radius: 3px;
box-shadow: 10px 10px 50px 15px rgba(black, 0.3);
background-color: #455A64;
position: relative;
}
.canvas-wrapper {
width: 100%;
height: 0;
padding-bottom: 100%;
position: relative;
}
canvas {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
height: 100%;
width: 100%;
background-color: #37474F;
}
.controls {
width: 100%;
padding-top: 20px;
padding-bottom: 0;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}
button[class^="btn"] {
background-color: #FFA000;
padding: 5px 20px;
text-transform: uppercase;
font-family: 'Varela Round';
font-size: 14px;
color: white;
font-weight: bold;
border: none;
border-radius: 3px;
letter-spacing: 0.3em;
height: 30px;
line-height: 30px;
padding: 0 10px;
box-shadow: 0 0 10px 5px rgba(black, 0.2);
outline: none;
cursor: pointer;
transition: box-shadow 0.3s, background 0.3s;
text-align: center;
margin: 0 20px 0;
margin-bottom: 20px;
&:hover {
background: lighten(#FFA000, 10%);
box-shadow: 0 0 20px 5px rgba(black, 0.3);
}
@media(min-width: 450px) {
& {
width: 150px;
}
}
}
button[disabled] {
background: darken(#FFA000, 10%);
}
.range {
width: 100%;
margin-bottom: 20px;
margin: 0 20px 20px;
display: flex;
align-items: center;
label {
color: white;
text-transform: uppercase;
margin-right: 20px;
width: 30%;
}
&_inputWrapper {
width: 100%;
position: relative;
height: 20px;
background-color: #fff;
box-shadow: 0 0 10px 5px rgba(black, 0.2);
}
&_inputValue {
position: absolute;
left: 0;
top: 0;
height: 100%;
color: white;
font-size: 14px;
height: 100%;
padding-left: 3px;
line-height: 20px;
}
&_inputSlider {
transform-origin: left top;
position: absolute;
top: 0;
left: 0;
background: #FFA000;
display: inline-block;
width: 100%;
height: 100%;
transform: scaleX(0);
transition: transform 0.2s ease-out;
}
input {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
margin: 0;
opacity: 0;
cursor: ew-resize;
z-index: 1
}
}
.score {
position: absolute;
top: 10px;
left: 0;
z-index: 1;
width: 100%;
text-align: center;
font-weight: bold;
font-size: 30px;
opacity: 0.3;
text-transform: uppercase;
color: white;
}
.dpad {
width: 100px;
margin: 0 auto;
margin-bottom: 20px;
display: none;
}
button[class^="control"] {
background: #FFA000;
border: none;
border-radius: 3px;
}
.control-up {
display: block;
}
.control-down svg {
transform: rotate(180deg)
}
.control-left svg {
transform: rotate(90deg)
}
.control-right svg {
transform: rotate(-90deg)
}
View Compiled
let cellsNo = 20
let cellSize = 400 / cellsNo
let difficulty = 1
let score = 0
const canvas = document.querySelector('canvas')
const ctx = canvas.getContext('2d')
const btnStart = document.querySelector('.btn-start')
const btnPause = document.querySelector('.btn-pause')
const scoreVal = document.querySelector('.score_val')
let direction
const DIR = {
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40
}
// ctx.strokeStyle = '#616161'
ctx.strokeStyle = '#27373F'
ctx.fillStyle = '#fff'
let snake = []
let food = null
let paused = false
let needsGrowth = false
let lastUpdate, lastFood, tick
let state
let flash = false
let lastKeyPressed
function update() {
tick = Date.now()
if (hasCollisions()) {
flash = true
return
}
if (tick - lastUpdate > 500 / difficulty) {
if (lastKeyPressed && lastKeyPressed !== direction) {
setDirection(lastKeyPressed)
}
moveSnake()
lastUpdate = tick
}
if (tick - lastFood > foodTreshold()) {
putFood()
}
if (headMeetsFood()) {
needsGrowth = true
food = null
putFood()
setScore(score + difficulty)
}
}
function foodTreshold() {
return (5000 / difficulty) * cellsNo
}
function hasCollisions() {
const head = snake[0]
const check = snake.concat([])
check.shift()
return check.find(
c => c.x === head.x && c.y === head.y
)
}
function snakeContains(cell) {
return snake.find(c => c.x === cell.x && c.y === cell.y)
}
function headMeetsFood() {
const head = snake[0]
return food && (head.x == food.x && head.y === food.y)
}
function moveSnake() {
const head = snake[0]
const next = Object.assign({}, head)
switch(direction) {
case DIR.LEFT:
--next.x;
break;
case DIR.UP:
--next.y;
break;
case DIR.RIGHT:
++next.x;
break;
case DIR.DOWN:
++next.y;
break;
}
if (next.x >= cellsNo) next.x = 0
if (next.y >= cellsNo) next.y = 0
if (next.x < 0) next.x = cellsNo -1
if (next.y < 0) next.y = cellsNo -1
if (!needsGrowth) {
snake.pop()
}
needsGrowth = false
snake.unshift(next);
}
function putFood() {
do {
food = {
x: ~~(Math.random() * (cellsNo - 1)),
y: ~~(Math.random() * (cellsNo - 1))
}
} while (snakeContains(food))
lastFood = tick
}
function draw() {
ctx.clearRect(0, 0, 400, 400)
drawCells()
drawFood()
if (flash && ~~(Date.now() / 100) % 2 === 0) {
return
}
drawSnake()
}
function drawCells() {
for (var i = 0; i < cellsNo; ++i)
for (var j = 0; j < cellsNo; ++j)
drawCell(i, j)
}
function drawFood() {
if (food) {
ctx.fillStyle = '#4FC3F7'
fillCell(food.x, food.y)
ctx.fillStyle = '#fff'
}
}
function drawCell(i, j) {
ctx.strokeRect(
i * cellSize,
j * cellSize,
cellSize, cellSize
)
}
function drawSnake() {
snake.forEach(
({x, y}) => fillCell(x, y)
)
}
function fillCell(x, y) {
ctx.beginPath()
ctx.rect(
x * cellSize,
y * cellSize,
cellSize, cellSize
)
ctx.closePath()
ctx.fill()
ctx.stroke()
}
function setScore(next) {
score = next
scoreVal.textContent = score
}
function startGame() {
btnStart.textContent = 'restart'
flash = false
lastKeyPressed = null
food = null
setScore(0)
direction = DIR.LEFT
lastFood = lastUpdate = Date.now()
paused = false
setTimeout(putFood, 1000)
const startX = cellsNo/2
snake = [startX, startX+1, startX+2, startX+3].map(
x => ({x, y: 15})
)
}
function loop() {
requestAnimationFrame(loop)
draw()
if (paused) return
update()
}
requestAnimationFrame(loop)
btnStart.addEventListener('click', startGame)
btnPause.addEventListener('click', pause)
function pause() {
paused = !paused
btnPause.textContent = paused ? 'resume' : 'pause'
}
window.addEventListener('keydown', onKeyDown)
function onKeyDown({keyCode}) {
switch (true) {
case (keyCode === DIR.DOWN && direction === DIR.UP):
case (keyCode === DIR.UP && direction === DIR.DOWN):
case (keyCode === DIR.LEFT && direction === DIR.RIGHT):
case (keyCode === DIR.RIGHT && direction === DIR.LEFT):
return
}
lastKeyPressed = keyCode
}
function setDirection(keyCode) {
direction = keyCode
}
function checkFood() {
if (!food) return
if (food.x >= cellsNo) {
food.x = cellsNo -1
}
if (food.y >= cellsNo) {
food.y = cellsNo -1
}
}
class RangeSlider {
constructor(el, cb) {
this.input = el.querySelector('input')
this.slider = el.querySelector('.range_inputSlider')
this.value = el.querySelector('.range_inputValue')
this.input.addEventListener('input', _ => this.onChange())
this.input.addEventListener('keydown', e => {
e.preventDefault()
})
this.onChangeCallback = cb
this.onChange()
}
onChange() {
this.value.textContent = this.input.value
this.slider.style.transform = `scaleX(${(this.input.value / this.input.step) / 10})`
this.onChangeCallback(this.input.value)
}
}
new RangeSlider(
document.querySelector('.range-difficulty'),
value => difficulty = Number(value)
)
new RangeSlider(
document.querySelector('.range-columns'),
value => {
cellsNo = Number(value)
cellSize = 400 / cellsNo
checkFood()
}
)
// --- TOUCH CONTROLS
var isPointerDown, pointerStart, pointerPos
function onTouchStart(e) {
const {clientX, clientY} = e.touches[0]
isPointerDown = true
pointerStart = {x: clientX, y: clientY}
pointerPos = Object.assign({}, pointerStart)
}
function onTouchMove(e) {
const {clientX, clientY} = e.touches[0]
pointerPos = {x: clientX, y: clientY}
}
function onTouchEnd() {
if (!isPointerDown) return
isPointerDown = false
const deltaX = pointerStart.x - pointerPos.x
const deltaY = pointerStart.y - pointerPos.y
const keyCode = touchToKeyCode(deltaX, deltaY)
if (keyCode) onKeyDown({keyCode})
}
function touchToKeyCode(x, y) {
if (Math.abs(x) > Math.abs(y)) {
if (x < -1) {
keyCode = DIR.RIGHT
} else if (x > 1) {
keyCode = DIR.LEFT
}
} else {
if (y < -1) {
keyCode = DIR.DOWN
} else if (y > 1) {
keyCode = DIR.UP
}
}
return keyCode
}
canvas.addEventListener('touchstart', onTouchStart)
window.addEventListener('touchmove', onTouchMove)
window.addEventListener('touchend', onTouchEnd)
View Compiled
This Pen doesn't use any external JavaScript resources.