use canvas element

This commit is contained in:
Michael Peters 2024-07-31 20:56:22 -07:00
parent 94e844f121
commit 32d5ea292d
3 changed files with 141 additions and 2 deletions

View File

@ -0,0 +1,33 @@
import { Engine, Vec2, vec2 } from './game-engine';
const BOARD_SIZE = 600; // px
const SQUARE_SIZE = 30; // px
export default function runCanvas(canvas: HTMLCanvasElement) {
const engine = new Engine(canvas);
const snake = [vec2(1, 1), vec2(2, 1)];
const apple = vec2(8, 8);
function clearCanvas() {
engine.rect(0, 0, canvas.width, canvas.height);
}
function drawSquare(pos: Vec2) {
engine.rect(pos.x * SQUARE_SIZE, pos.y * SQUARE_SIZE, SQUARE_SIZE, SQUARE_SIZE);
}
function render() {
engine.setFillStyle('#333333');
clearCanvas();
engine.setFillStyle('#277edb');
for (const square of snake) {
drawSquare(square);
}
engine.setFillStyle('#e01851');
drawSquare(apple);
}
engine.run(render);
}

View File

@ -0,0 +1,98 @@
export const TAU = 6.283185307179586;
export class Vec2 {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
str() {
return `<${this.x}, ${this.y}>`;
}
eq(o: Vec2) {
return this.x === o.x && this.y === o.y;
}
add(o: Vec2) {
return new Vec2(this.x + o.x, this.y + o.y);
}
sub(o: Vec2) {
return new Vec2(this.x - o.x, this.y - o.y);
}
mul(scalar: number) {
return new Vec2(this.x * scalar, this.y * scalar);
}
div(scalar: number) {
return new Vec2(this.x / scalar, this.y / scalar);
}
length() {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
norm() {
const length = this.length();
return new Vec2(this.x / length, this.y / length);
}
static dot(a: Vec2, b: Vec2) {
return a.x * b.x + a.y * b.y;
}
// magnitude of 3d cross product's z-term
static crossMag(a: Vec2, b: Vec2) {
return a.x * b.y - a.y * b.x;
}
// returns the angle needed to rotate a to b about the origin
static angle(a: Vec2, b: Vec2) {
const an = a.norm();
const bn = b.norm();
const cos = Vec2.dot(an, bn);
const sin = Vec2.crossMag(an, bn);
const theta = Math.atan2(sin, cos);
return theta;
}
}
export function vec2(x: number, y: number) {
return new Vec2(x, y);
}
export class Engine {
ctx: CanvasRenderingContext2D;
camera = vec2(0, 0);
constructor(canvasElement: HTMLCanvasElement) {
const ctx = canvasElement.getContext('2d');
if (ctx === null) throw Error('unable to get 2d canvas context');
this.ctx = ctx;
}
setFillStyle(style: string) {
this.ctx.fillStyle = style;
}
rect(x: number, y: number, w: number, h: number) {
const c = this.camera;
this.ctx.fillRect(x - c.x, y - c.y, w, h);
}
run(render: () => void) {
function loop() {
render();
window.requestAnimationFrame(loop);
}
loop();
}
}

View File

@ -1,6 +1,7 @@
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { FC, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import './index.scss';
import runCanvas from './canvas';
const BOARD_SIZE = 600; // px
const SQUARE_SIZE = 30; // px
@ -48,7 +49,6 @@ const SnakePage: FC = () => {
const move = moves[dir];
const nextSquare = { x: head.x + move.x, y: head.y + move.y };
const newSnake = [...snake.slice(1), nextSquare];
console.log({ snake, newSnake });
setSnake(newSnake);
},
[snake],
@ -90,8 +90,16 @@ const SnakePage: FC = () => {
),
[apple],
);
const boardRef = useRef(null);
useLayoutEffect(() => {
if (boardRef.current === null) throw Error('boardRef.current is null');
runCanvas(boardRef.current);
}, []);
return (
<div id="snake">
<canvas id="board" ref={boardRef} width={BOARD_SIZE} height={BOARD_SIZE} />
<svg id="board" width={BOARD_SIZE} height={BOARD_SIZE}>
{snakeElements}
{appleElement}