snake2: add trail-based snake
This commit is contained in:
+1
-1
@@ -24,7 +24,7 @@
|
||||
<li><a href="life.html">Game of life</a></li>
|
||||
<li><a href="ray.html">Ray</a></li>
|
||||
<li><a href="snake.html">Snake</a></li>
|
||||
<li><a href="snake2.html">Snake (trail)</a></li>
|
||||
<li><a href="snake2.html">Snake (trail based)</a></li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Snake (trail)</title>
|
||||
<link rel="stylesheet" type="text/css" href="snake.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="status-line"></div>
|
||||
<div class="canvas-wrapper">
|
||||
<canvas id="game-canvas" tabindex="0"></canvas>
|
||||
</div>
|
||||
<script type="module" src="snake2.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,143 @@
|
||||
import Vec2d from "./vec.js";
|
||||
import { fillCircle, lerp, resizeCanvas, rgbToString } from "./common.js";
|
||||
|
||||
const HEAD_COLOR = 0xf43f5e;
|
||||
const APPLE_COLOR = 0x58f474;
|
||||
const SNAKE_RADIUS = 10;
|
||||
const TRAIL_SPACING = 2;
|
||||
const GROWTH_RATE = 10; // px per apple
|
||||
const INITIAL_BODY_LENGTH = 40; // px of visible body at startup
|
||||
|
||||
const DIRECTIONS = {
|
||||
up: new Vec2d(0, -1),
|
||||
down: new Vec2d(0, 1),
|
||||
left: new Vec2d(-1, 0),
|
||||
right: new Vec2d(1, 0)
|
||||
}
|
||||
|
||||
// state
|
||||
let apple: Vec2d | undefined;
|
||||
let head: Vec2d = new Vec2d(200, 200);
|
||||
let trail: Vec2d[] = []; // history of head positions
|
||||
let bodyLength = INITIAL_BODY_LENGTH; // px of snake body
|
||||
let direction: Vec2d = DIRECTIONS.right; // unit vector
|
||||
let speed = 200; // px/s
|
||||
|
||||
let lastTime: number | undefined;
|
||||
let paused = false;
|
||||
|
||||
function update(ctx: CanvasRenderingContext2D, time: number) {
|
||||
if (lastTime == null) lastTime = time;
|
||||
const dt = (time - lastTime) / 1000; // s
|
||||
lastTime = time;
|
||||
const bounds = new Vec2d(ctx.canvas.width, ctx.canvas.height);
|
||||
// update state
|
||||
if (!paused) {
|
||||
// move the head
|
||||
head = head.add(direction.scale(speed * dt));
|
||||
|
||||
// record the trail (history of head positions)
|
||||
const last_head = trail[0];
|
||||
if (last_head.distance(head) > 0.5) trail.unshift(head)
|
||||
|
||||
// collisions
|
||||
if (head.wrap(bounds).distance(apple!) < SNAKE_RADIUS * 2) {
|
||||
apple = randomApple(bounds);
|
||||
bodyLength += GROWTH_RATE;
|
||||
speed += 10;
|
||||
}
|
||||
|
||||
trimTrail();
|
||||
}
|
||||
|
||||
// drawing
|
||||
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
const t = (Math.sin(0.01 * time) + 1.0) * 0.5;
|
||||
const apple_size = lerp(SNAKE_RADIUS, SNAKE_RADIUS * 0.9, t)
|
||||
fillCircle(ctx, apple!, apple_size, APPLE_COLOR); // draw apple
|
||||
drawSnake(ctx);
|
||||
|
||||
window.requestAnimationFrame((t) => update(ctx, t));
|
||||
}
|
||||
|
||||
function drawSnake(ctx: CanvasRenderingContext2D) {
|
||||
const bounds = new Vec2d(ctx.canvas.width, ctx.canvas.height);
|
||||
ctx.strokeStyle = rgbToString(HEAD_COLOR);
|
||||
ctx.lineWidth = SNAKE_RADIUS * 2;
|
||||
ctx.lineCap = "round";
|
||||
ctx.lineJoin = "round";
|
||||
|
||||
ctx.beginPath();
|
||||
let prev = head.wrap(bounds);
|
||||
ctx.moveTo(prev.x, prev.y);
|
||||
let acc = 0;
|
||||
for (let i = 0; i < trail.length - 1; ++i) {
|
||||
const len = trail[i].distance(trail[i + 1]);
|
||||
acc += len;
|
||||
const pos = trail[i + 1].wrap(bounds);
|
||||
const isWrapped = prev && (Math.abs(pos.x - prev.x) > bounds.x / 2 || Math.abs(pos.y - prev.y) > bounds.y / 2);
|
||||
if (isWrapped) {
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
}
|
||||
ctx.lineTo(pos.x, pos.y);
|
||||
prev = pos;
|
||||
|
||||
if (acc >= bodyLength) { ctx.stroke(); return; }
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function trimTrail() {
|
||||
const trail_margin = 50; // px of slack to keep in the trail
|
||||
let remaining = bodyLength + trail_margin;
|
||||
let cut = trail.length;
|
||||
for (let i = 0; i < trail.length - 1; ++i) {
|
||||
const seg = trail[i].distance(trail[i + 1]);
|
||||
if (seg > remaining) {
|
||||
cut = i + 1;
|
||||
break;
|
||||
}
|
||||
remaining -= seg;
|
||||
}
|
||||
if (cut < trail.length) trail.length = cut;
|
||||
}
|
||||
|
||||
function randomApple(bounds: Vec2d) {
|
||||
return Vec2d.random(bounds.x - 2 * SNAKE_RADIUS, bounds.y - 2 * SNAKE_RADIUS);
|
||||
}
|
||||
|
||||
function init() {
|
||||
const canvas = document.getElementById("game-canvas") as HTMLCanvasElement | null;
|
||||
if (!canvas) throw new Error("unable to get canvas HTML element");
|
||||
const ctx = canvas.getContext("2d") as CanvasRenderingContext2D | null;
|
||||
if (!ctx) throw new Error("unable to get canvas 2D context");
|
||||
resizeCanvas(ctx);
|
||||
|
||||
const bounds = new Vec2d(canvas.width, canvas.height);
|
||||
apple = randomApple(bounds);
|
||||
|
||||
// seed the trail behind the head so the body is visible immediately
|
||||
for (let d = 0; d < INITIAL_BODY_LENGTH; d += TRAIL_SPACING) {
|
||||
trail.push(new Vec2d(head.x - d, head.y));
|
||||
}
|
||||
|
||||
window.onkeydown = (evt) => {
|
||||
let new_direction: Vec2d | undefined;
|
||||
switch (evt.key) {
|
||||
case "ArrowUp": case "z": new_direction = DIRECTIONS.up; break;
|
||||
case "ArrowDown": case "s": new_direction = DIRECTIONS.down; break;
|
||||
case "ArrowLeft": case "q": new_direction = DIRECTIONS.left; break;
|
||||
case "ArrowRight": case "d": new_direction = DIRECTIONS.right; break;
|
||||
case "p": paused = !paused; break;
|
||||
}
|
||||
if (!new_direction) return;
|
||||
if (new_direction.x === -direction.x && new_direction.y === -direction.y) return;
|
||||
if (new_direction.x === direction.x && new_direction.y === direction.y) return;
|
||||
direction = new_direction;
|
||||
};
|
||||
|
||||
window.requestAnimationFrame((t) => update(ctx, t));
|
||||
}
|
||||
|
||||
init();
|
||||
Reference in New Issue
Block a user