This is a snake game I made with Vanilla Javascript.
A Pen by F A R I A T on CodePen.
| <div class="container noselect"> | |
| <div class="wrapper"> | |
| <button id="replay"> | |
| <i class="fas fa-play"></i> | |
| RESTART | |
| </button> | |
| <div id="canvas"> | |
| </div> | |
| <div id="ui"> | |
| <h2>SCORE | |
| </h2> | |
| <span id="score">00</span> | |
| </div> | |
| </div> | |
| <div id="credit"> | |
| <h1>SNAKE</h1> <span>by Fariat</span> | |
| </div> | |
| </div> |
| let replay = document.querySelector("#replay"); | |
| let score = document.querySelector("#score"); | |
| const canvas = document.createElement("canvas"); | |
| document.querySelector("#canvas").appendChild(canvas); | |
| const ctx = canvas ? canvas.getContext("2d") : null; | |
| function cnvRes(width = 400, height = 400) { | |
| ctx.canvas.width = width; | |
| ctx.canvas.height = height; | |
| } | |
| let _vars = { | |
| snake: undefined, | |
| snakeLength: undefined, | |
| food: undefined, | |
| currentHue: undefined, | |
| segments: undefined, | |
| historyPath: [], | |
| gameOver: false, | |
| tails: [], | |
| update: undefined, | |
| maxScore: window.localStorage.getItem("maxScore") || undefined, | |
| effects: [] | |
| }; | |
| let _helpers = { | |
| collision(isSelfCol, snakeHead) { | |
| if (isSelfCol) { | |
| if (snakeHead.x == _vars.food.x && snakeHead.y == _vars.food.y) { | |
| _vars.food.respawnFood(); | |
| _vars.tails.push(new Snake(_vars.snakeLength - 1, "tail")); | |
| _vars.snakeLength++; | |
| _vars.snake.delay - 0.5; | |
| } | |
| } else { | |
| for (let i = 1; i < _vars.historyPath.length; i++) { | |
| if ( | |
| snakeHead.x == _vars.historyPath[i].x && | |
| snakeHead.y == _vars.historyPath[i].y | |
| ) { | |
| _vars.gameOver = true; | |
| } | |
| } | |
| } | |
| }, | |
| randHue() { | |
| return Math.floor(Math.random() * 360); | |
| }, | |
| randCor(newCors) { | |
| let randX = | |
| (Math.floor(Math.random() * _vars.segments) * ctx.canvas.width) / | |
| _vars.segments; | |
| let randY = | |
| (Math.floor(Math.random() * _vars.segments) * ctx.canvas.height) / | |
| _vars.segments; | |
| if (newCors) { | |
| randX = | |
| (Math.floor(Math.random() * _vars.segments) * ctx.canvas.width) / | |
| _vars.segments; | |
| randY = | |
| (Math.floor(Math.random() * _vars.segments) * ctx.canvas.height) / | |
| _vars.segments; | |
| return { randX, randY }; | |
| } else { | |
| return { randX, randY }; | |
| } | |
| }, | |
| positionLogger(limit, loc) { | |
| _vars.historyPath.push(loc); | |
| if (_vars.historyPath.length > limit) { | |
| _vars.historyPath.shift(); | |
| } | |
| }, | |
| hsl2rgb(hue, saturation, lightness) { | |
| if (hue == undefined) { | |
| return [0, 0, 0]; | |
| } | |
| var chroma = (1 - Math.abs(2 * lightness - 1)) * saturation; | |
| var huePrime = hue / 60; | |
| var secondComponent = chroma * (1 - Math.abs((huePrime % 2) - 1)); | |
| huePrime = Math.floor(huePrime); | |
| var red; | |
| var green; | |
| var blue; | |
| if (huePrime === 0) { | |
| red = chroma; | |
| green = secondComponent; | |
| blue = 0; | |
| } else if (huePrime === 1) { | |
| red = secondComponent; | |
| green = chroma; | |
| blue = 0; | |
| } else if (huePrime === 2) { | |
| red = 0; | |
| green = chroma; | |
| blue = secondComponent; | |
| } else if (huePrime === 3) { | |
| red = 0; | |
| green = secondComponent; | |
| blue = chroma; | |
| } else if (huePrime === 4) { | |
| red = secondComponent; | |
| green = 0; | |
| blue = chroma; | |
| } else if (huePrime === 5) { | |
| red = chroma; | |
| green = 0; | |
| blue = secondComponent; | |
| } | |
| var lightnessAdjustment = lightness - chroma / 2; | |
| red += lightnessAdjustment; | |
| green += lightnessAdjustment; | |
| blue += lightnessAdjustment; | |
| return [ | |
| Math.round(red * 255), | |
| Math.round(green * 255), | |
| Math.round(blue * 255) | |
| ]; | |
| }, | |
| lerp(start, end, t) { | |
| return start * (1 - t) + end * t; | |
| } | |
| }; | |
| let _input = { | |
| left: false, | |
| down: false, | |
| right: true, | |
| up: false, | |
| listen() { | |
| addEventListener( | |
| "keydown", | |
| (e) => { | |
| switch (e.key) { | |
| case "ArrowLeft": | |
| if (!this.right) { | |
| this.left = true; | |
| this.down = false; | |
| this.right = false; | |
| this.up = false; | |
| } | |
| break; | |
| case "ArrowRight": | |
| if (!this.left) { | |
| this.left = false; | |
| this.down = false; | |
| this.right = true; | |
| this.up = false; | |
| } | |
| break; | |
| case "ArrowUp": | |
| if (!this.down) { | |
| this.left = false; | |
| this.down = false; | |
| this.right = false; | |
| this.up = true; | |
| } | |
| break; | |
| case "ArrowDown": | |
| if (!this.up) { | |
| this.left = false; | |
| this.down = true; | |
| this.right = false; | |
| this.up = false; | |
| } | |
| break; | |
| default: | |
| break; | |
| } | |
| }, | |
| false | |
| ); | |
| } | |
| }; | |
| class Snake { | |
| constructor(i, type) { | |
| this.x = type == "tail" ? _vars.historyPath[i].x : 0; | |
| this.y = type == "tail" ? _vars.historyPath[i].y : 0; | |
| this.type = type; | |
| this.index = i; | |
| this.delay = 10; | |
| this.localDelay = 10; | |
| this.size = ctx.canvas.width / _vars.segments; | |
| this.color = "white"; | |
| } | |
| draw() { | |
| ctx.lineWidth = 1; | |
| ctx.fillStyle = this.color; | |
| ctx.strokeStyle = "#181825"; | |
| ctx.strokeRect(this.x, this.y, this.size, this.size); | |
| ctx.fillRect(this.x, this.y, this.size, this.size); | |
| } | |
| update() { | |
| this.draw(); | |
| if (this.localDelay < 0) { | |
| if (this.type == "tail") { | |
| this.x = _vars.historyPath[this.index].x; | |
| this.y = _vars.historyPath[this.index].y; | |
| } else { | |
| this.localDelay = this.delay; | |
| if (_input.left) { | |
| this.x -= ctx.canvas.width / _vars.segments; | |
| } | |
| if (_input.right) { | |
| this.x += ctx.canvas.width / _vars.segments; | |
| } | |
| if (_input.up) { | |
| this.y -= ctx.canvas.width / _vars.segments; | |
| } | |
| if (_input.down) { | |
| this.y += ctx.canvas.width / _vars.segments; | |
| } | |
| if (this.x + ctx.canvas.width / _vars.segments > ctx.canvas.width) { | |
| this.x = 0; | |
| } | |
| if (this.y + ctx.canvas.height / _vars.segments > ctx.canvas.width) { | |
| this.y = 0; | |
| } | |
| if (this.y < 0) { | |
| this.y = ctx.canvas.height - ctx.canvas.height / _vars.segments; | |
| } | |
| if (this.x < 0) { | |
| this.x = ctx.canvas.width - ctx.canvas.width / _vars.segments; | |
| } | |
| _helpers.collision(true, { ...this }); | |
| _helpers.collision(false, { ...this }); | |
| _helpers.positionLogger(_vars.snakeLength, { x: this.x, y: this.y }); | |
| } | |
| } else { | |
| this.localDelay--; | |
| } | |
| } | |
| } | |
| class Food extends Snake { | |
| constructor() { | |
| super(); | |
| this.x = | |
| (Math.floor(Math.random() * _vars.segments) * ctx.canvas.width) / | |
| _vars.segments; | |
| this.y = | |
| (Math.floor(Math.random() * _vars.segments) * ctx.canvas.height) / | |
| _vars.segments; | |
| this.color = _vars.currentHue = `hsl(${_helpers.randHue()}, 100%, 55%)`; | |
| } | |
| draw() { | |
| ctx.save(); | |
| ctx.shadowColor = this.color; | |
| ctx.shadowBlur = 50; | |
| ctx.fillStyle = this.color; | |
| ctx.fillRect(this.x, this.y, this.size, this.size); | |
| ctx.restore(); | |
| } | |
| respawnFood() { | |
| _vars.effects.push( | |
| new Effect( | |
| _vars.food.x, | |
| _vars.food.y, | |
| _vars.currentHue, | |
| _vars.food.size, | |
| _vars.effects.length - 1 | |
| ) | |
| ); | |
| this.color = _vars.currentHue = `hsl(${_helpers.randHue()}, 100%, 50%)`; | |
| this.x = | |
| (Math.floor(Math.random() * _vars.segments) * ctx.canvas.width) / | |
| _vars.segments; | |
| this.y = | |
| (Math.floor(Math.random() * _vars.segments) * ctx.canvas.height) / | |
| _vars.segments; | |
| for (let i = 0; i < _vars.historyPath.length; i++) { | |
| if ( | |
| this.x == _vars.historyPath[i].x && | |
| this.y == _vars.historyPath[i].y | |
| ) { | |
| this.respawnFood(); | |
| } | |
| } | |
| } | |
| } | |
| class Effect { | |
| constructor(x, y, color, size, i) { | |
| this.x = x; | |
| this.y = y; | |
| this.color = color; | |
| this.size = size; | |
| this.ttl = 0; | |
| this.angle = 1; | |
| this.index = i; | |
| } | |
| draw() { | |
| let hsl = this.color | |
| .split("") | |
| .filter((l) => l.match(/[^hsl()$% ]/g)) | |
| .join("") | |
| .split(",") | |
| .map((n) => +n); | |
| let [r, g, b] = _helpers.hsl2rgb(hsl[0], hsl[1] / 100, hsl[2] / 100); | |
| ctx.lineWidth = 2; | |
| ctx.strokeStyle = `rgb(${r},${g},${b},${1 / this.ttl})`; | |
| ctx.save(); | |
| ctx.translate(this.x + this.size / 2, this.y + this.size / 2); | |
| ctx.rotate((this.angle * Math.PI) / 180.0); | |
| ctx.strokeRect(-this.size / 2, -this.size / 2, this.size, this.size); | |
| ctx.restore(); | |
| } | |
| update() { | |
| this.draw(); | |
| if (this.size > 0) { | |
| this.ttl >= 100 ? _vars.effects.splice(this.i + 1, 1) : (this.ttl += 0.5); | |
| this.angle += 5; | |
| this.size += 0.06; | |
| console.log(); | |
| } else { | |
| this.size = 10; | |
| } | |
| } | |
| } | |
| function scoreManager() { | |
| let currentScore = _vars.snakeLength - 1; | |
| score.innerText = currentScore.toString(); | |
| } | |
| function setup() { | |
| cnvRes(); | |
| _input.listen(); | |
| _vars.segments = 32; | |
| _vars.snakeLength = 1; | |
| _vars.snake = new Snake("head"); | |
| _vars.food = new Food(); | |
| loop(); | |
| } | |
| function loop() { | |
| _vars.update = setInterval(() => { | |
| if (!_vars.gameOver) { | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| _vars.snake.update(); | |
| if (_vars.tails.length) { | |
| for (let i = 0; i < _vars.tails.length; i++) { | |
| _vars.tails[i].update(); | |
| } | |
| } | |
| _vars.food.draw(); | |
| scoreManager(); | |
| for (let i = 0; i < _vars.effects.length; i++) { | |
| _vars.effects[i].update(); | |
| } | |
| } else { | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| gameOver(); | |
| } | |
| }, 1000 / 60); | |
| } | |
| setup(); | |
| replay.addEventListener("click", () => { | |
| reset(); | |
| }); | |
| function gameOver() { | |
| _vars.maxScore ? null : (_vars.maxScore = _vars.snakeLength - 1); | |
| _vars.snakeLength - 1 > _vars.maxScore | |
| ? (_vars.maxScore = _vars.snakeLength - 1) | |
| : null; | |
| window.localStorage.setItem("maxScore", _vars.maxScore); | |
| ctx.fillStyle = "#4cffd7"; | |
| ctx.textAlign = "center"; | |
| ctx.font = "bold 30px Poppins, sans-serif"; | |
| ctx.fillText("GAME OVER", ctx.canvas.width / 2, ctx.canvas.height / 2); | |
| ctx.font = "15px Poppins, sans-serif"; | |
| ctx.fillText( | |
| `SCORE ${_vars.snakeLength - 1}`, | |
| ctx.canvas.width / 2, | |
| ctx.canvas.height / 2 + 60 | |
| ); | |
| ctx.fillText( | |
| `MAXSCORE ${_vars.maxScore}`, | |
| ctx.canvas.width / 2, | |
| ctx.canvas.height / 2 + 80 | |
| ); | |
| } | |
| function reset() { | |
| clearInterval(_vars.update); | |
| _vars.snake = undefined; | |
| _vars.snakeLength = undefined; | |
| _vars.food = undefined; | |
| _vars.currentHue = undefined; | |
| _vars.segments = undefined; | |
| _vars.historyPath = []; | |
| _vars.gameOver = false; | |
| _vars.tails = []; | |
| _vars.update = undefined; | |
| _input.left = false; | |
| _input.down = false; | |
| _input.right = true; | |
| _input.up = false; | |
| setup(); | |
| } |
This is a snake game I made with Vanilla Javascript.
A Pen by F A R I A T on CodePen.
| @font-face { | |
| font-family: "game"; | |
| src: url("https://fonts.googleapis.com/css2?family=Poppins:wght@500;800&display=swap"); | |
| } | |
| * { | |
| padding: 0; | |
| margin: 0; | |
| box-sizing: border-box; | |
| } | |
| button:focus { | |
| outline: 0; | |
| } | |
| html, | |
| body { | |
| height: 100%; | |
| font-family: "Poppins", sans-serif; | |
| color: #6e7888; | |
| } | |
| body { | |
| background-color: #222738; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| color: #6e7888; | |
| } | |
| canvas { | |
| background-color: #181825; | |
| } | |
| .container { | |
| display: flex; | |
| width: 100%; | |
| height: 100%; | |
| flex-flow: column wrap; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| #ui { | |
| display: flex; | |
| align-items: center; | |
| font-size: 10px; | |
| flex-flow: column; | |
| margin-left: 10px; | |
| } | |
| h2 { | |
| font-weight: 200; | |
| transform: rotate(270deg); | |
| } | |
| #score { | |
| margin-top: 20px; | |
| font-size: 30px; | |
| font-weight: 800; | |
| } | |
| .noselect { | |
| user-select: none; | |
| } | |
| #replay { | |
| font-size: 10px; | |
| padding: 10px 20px; | |
| background: #6e7888; | |
| border: none; | |
| color: #222738; | |
| border-radius: 20px; | |
| font-weight: 800; | |
| transform: rotate(270deg); | |
| cursor: pointer; | |
| transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| #replay:hover { | |
| background: #a6aab5; | |
| background: #4cffd7; | |
| transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| #replay svg { | |
| margin-right: 8px; | |
| } | |
| @media (max-width: 600px) { | |
| #replay { | |
| margin-bottom: 20px; | |
| } | |
| #replay, | |
| h2 { | |
| transform: rotate(0deg); | |
| } | |
| #ui { | |
| flex-flow: row wrap; | |
| margin-bottom: 20px; | |
| } | |
| #score { | |
| margin-top: 0; | |
| margin-left: 20px; | |
| } | |
| .container { | |
| flex-flow: column wrap; | |
| } | |
| } | |
| #credit { | |
| width: 100%; | |
| bottom: 40px; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-weight: 600; | |
| color: inherit; | |
| text-transform: uppercase; | |
| padding-left: 35px; | |
| } | |
| #credit span { | |
| font-size: 10px; | |
| margin-left: 20px; | |
| color: inherit; | |
| letter-spacing: 4px; | |
| } | |
| #credit h1 { | |
| font-size: 25px; | |
| } | |
| .wrapper { | |
| display: flex; | |
| flex-flow: row wrap; | |
| justify-content: center; | |
| align-items: center; | |
| margin-bottom: 20px; | |
| } |