generated from michael/webpack-base
use canvas element
This commit is contained in:
parent
94e844f121
commit
32d5ea292d
33
src/components/snake/canvas.ts
Normal file
33
src/components/snake/canvas.ts
Normal 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);
|
||||||
|
}
|
98
src/components/snake/game-engine.ts
Normal file
98
src/components/snake/game-engine.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@ -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 './index.scss';
|
||||||
|
import runCanvas from './canvas';
|
||||||
|
|
||||||
const BOARD_SIZE = 600; // px
|
const BOARD_SIZE = 600; // px
|
||||||
const SQUARE_SIZE = 30; // px
|
const SQUARE_SIZE = 30; // px
|
||||||
@ -48,7 +49,6 @@ const SnakePage: FC = () => {
|
|||||||
const move = moves[dir];
|
const move = moves[dir];
|
||||||
const nextSquare = { x: head.x + move.x, y: head.y + move.y };
|
const nextSquare = { x: head.x + move.x, y: head.y + move.y };
|
||||||
const newSnake = [...snake.slice(1), nextSquare];
|
const newSnake = [...snake.slice(1), nextSquare];
|
||||||
console.log({ snake, newSnake });
|
|
||||||
setSnake(newSnake);
|
setSnake(newSnake);
|
||||||
},
|
},
|
||||||
[snake],
|
[snake],
|
||||||
@ -90,8 +90,16 @@ const SnakePage: FC = () => {
|
|||||||
),
|
),
|
||||||
[apple],
|
[apple],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const boardRef = useRef(null);
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (boardRef.current === null) throw Error('boardRef.current is null');
|
||||||
|
runCanvas(boardRef.current);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="snake">
|
<div id="snake">
|
||||||
|
<canvas id="board" ref={boardRef} width={BOARD_SIZE} height={BOARD_SIZE} />
|
||||||
<svg id="board" width={BOARD_SIZE} height={BOARD_SIZE}>
|
<svg id="board" width={BOARD_SIZE} height={BOARD_SIZE}>
|
||||||
{snakeElements}
|
{snakeElements}
|
||||||
{appleElement}
|
{appleElement}
|
||||||
|
Loading…
Reference in New Issue
Block a user