diff --git a/src/components/snake/canvas.ts b/src/components/snake/canvas.ts new file mode 100644 index 0000000..67c2005 --- /dev/null +++ b/src/components/snake/canvas.ts @@ -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); +} diff --git a/src/components/snake/game-engine.ts b/src/components/snake/game-engine.ts new file mode 100644 index 0000000..000f578 --- /dev/null +++ b/src/components/snake/game-engine.ts @@ -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(); + } +} diff --git a/src/components/snake/index.tsx b/src/components/snake/index.tsx index 4ec612c..e2d087a 100644 --- a/src/components/snake/index.tsx +++ b/src/components/snake/index.tsx @@ -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 (