Created
August 28, 2022 18:26
-
-
Save MartinMuzatko/a34bd2d847b3ee666ecb58c25733f1ec to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import './style.css' | |
import { createDrawFunction, objectTypes } from 'declarative-canvas' | |
interface Point { | |
x: number | |
y: number | |
} | |
interface Player extends Point { | |
width: number | |
height: number | |
speed: number | |
} | |
interface Particle extends Point { | |
originX: number | |
originY: number | |
targetX: number | |
targetY: number | |
speed: number | |
} | |
type Keys = "ArrowLeft" | "ArrowUp" | "ArrowRight" | "ArrowDown" | |
type KeyState = Record<Keys, boolean> | |
const keys = new Set<Keys>([ | |
'ArrowLeft', | |
'ArrowUp', | |
'ArrowRight', | |
'ArrowDown', | |
]) | |
const movePlayer = (player: Player, keyState: KeyState, delta: number) => { | |
let x = Number(keyState.ArrowRight) - Number(keyState.ArrowLeft) | |
let y = Number(keyState.ArrowDown) - Number(keyState.ArrowUp) | |
// Vector normalization | |
const len = Math.sqrt(x ** 2 + y ** 2) | |
if (x != 0) x = x / len | |
if (y != 0) y = y / len | |
return { | |
...player, | |
x: player.x + x * delta * player.speed, | |
y: player.y + y * delta * player.speed, | |
} | |
} | |
const angleCoords = (p1: Point, p2: Point) => Math.atan2(p2.y - p1.y, p2.x - p1.x) * (180 / Math.PI) | |
const vectorFromAngle = (angle: number): Point => ({ | |
x: Math.cos(angle), | |
y: Math.sin(angle), | |
}) | |
const lerp = (start: number, end: number, t: number) => start * (1 - t) + end * t | |
const shootParticle = (particle: Particle, player: Player, mouse: Point, delta: number) => { | |
// if (particle.x > 300) return { | |
// ...particle, | |
// x: 0, | |
// y: 0, | |
// originX: player.x, | |
// originY: player.y, | |
// } | |
const target: Point = { | |
x: player.x + mouse.x - cw, | |
y: player.y + mouse.y - ch, | |
} | |
const distance = Math.hypot(player.x - player.x + mouse.x - cw, player.y - player.y + mouse.y - ch) | |
return { | |
...particle, | |
x: lerp(player.x, player.x + mouse.x - cw, .5), | |
y: lerp(player.y, player.y + mouse.y - cw, .5), | |
// y: particle.y + delta * particle.speed, | |
} | |
} | |
const keyMap = { | |
a: 'ArrowLeft', | |
w: 'ArrowUp', | |
d: 'ArrowRight', | |
s: 'ArrowDown', | |
} | |
const cw = window.innerWidth / 2 | |
const ch = window.innerHeight / 2 | |
const loadImage = (path: string): Promise<HTMLImageElement> => new Promise((resolve) => { | |
const img = new Image() | |
img.addEventListener('load', () => resolve(img), false) | |
img.src = path | |
}) | |
const getSpiralStepCoords = (n: number): Point => { | |
const k = Math.ceil((Math.sqrt(n) - 1) / 2) | |
const t = 2 * k + 1 | |
const m = t ** 2 | |
const t2 = t - 1 | |
if (n >= m - t2) return { x: k - (m - n), y: -k } | |
if (n >= m - (t2 * 2)) return { x: -k, y: -k + ((m - t2) - n) } | |
if (n >= m - (t2 * 3)) return { x: -k + ((m - (t2 * 2)) - n), y: k } | |
else return { x: k, y: k - ((m - (t2 * 2)) - n - t2) } | |
} | |
const getBackground = (bg: HTMLImageElement, player: Player) => { | |
const size = 256 | |
const playerGrid = { | |
x: Math.round(player.x / size) * size, | |
y: Math.round(player.y / size) * size, | |
} | |
const defaultProps = { | |
width: size, | |
height: size, | |
type: objectTypes.IMAGE, | |
image: bg, | |
} | |
return Array(130).fill(0).map((_, i) => getSpiralStepCoords(i)).map((p => ({ | |
...defaultProps, | |
x: playerGrid.x + (size * p.x), | |
y: playerGrid.y + (size * p.y), | |
}))) | |
} | |
const rectCollides = (a: Player, b: Player) => | |
a.x < b.x + b.width && | |
a.x + a.width > b.x && | |
a.y < b.y + b.height && | |
a.height + a.y > b.y | |
const moveEnemy = (player: Player, enemy: Player, enemies: Player[], delta: number) => { | |
const distance = Math.hypot(enemy.x - player.x, enemy.y - player.y) | |
const x = lerp(enemy.x, player.x, 1 / ((distance / (enemy.speed * delta)))) | |
const y = lerp(enemy.y, player.y, 1 / ((distance / (enemy.speed * delta)))) | |
const isSelf = (e: Player) => e.x != enemy.x && e.y != enemy.y | |
const willCollide = enemies.filter(isSelf).reduce((acc, cur) => acc || rectCollides({ ...enemy, x, y }, cur), false) | |
if (willCollide) return enemy | |
return { | |
...enemy, | |
x, | |
y, | |
} | |
} | |
const main = async () => { | |
const canvas = document.querySelector('canvas') | |
if (!canvas) return | |
const context = canvas.getContext('2d')! | |
canvas.width = window.innerWidth | |
canvas.height = window.innerHeight | |
await 0 | |
// @ts-ignore | |
let bg = new Image() | |
loadImage('/bg.png').then(img => bg = img) | |
let player: Player = { | |
x: 0, | |
y: 0, | |
width: 20, | |
height: 40, | |
speed: 5, | |
} | |
const enemyDefaultProps = { | |
speed: 1, | |
height: 25, | |
width: 25, | |
} | |
let enemies: Player[] = [ | |
{ x: 300, y: 0, ...enemyDefaultProps }, | |
{ x: 0, y: 300, ...enemyDefaultProps }, | |
{ x: -300, y: 0, ...enemyDefaultProps }, | |
{ x: 0, y: -300, ...enemyDefaultProps }, | |
{ x: 200, y: 200, ...enemyDefaultProps }, | |
{ x: 200, y: -200, ...enemyDefaultProps }, | |
{ x: -200, y: -200, ...enemyDefaultProps }, | |
{ x: -200, y: 200, ...enemyDefaultProps }, | |
{ x: -200, y: -200, ...enemyDefaultProps }, | |
] | |
const particle: Particle = { | |
x: 0, | |
y: 0, | |
originX: 0, | |
originY: 0, | |
targetX: 0, | |
targetY: 0, | |
speed: .2, | |
} | |
const handleKeyDown = (event: KeyboardEvent) => { | |
const key = keyMap[event.key as keyof typeof keyMap] || event.key | |
if (!keys.has(key as Keys)) return | |
keyState = { ...keyState, [key]: true } | |
} | |
const handleKeyUp = (event: KeyboardEvent) => { | |
const key = keyMap[event.key as keyof typeof keyMap] || event.key | |
if (!keys.has(key as Keys)) return | |
keyState = { ...keyState, [key]: false } | |
} | |
window.addEventListener('keydown', handleKeyDown) | |
window.addEventListener('keyup', handleKeyUp) | |
let keyState = { | |
ArrowLeft: false, | |
ArrowUp: false, | |
ArrowRight: false, | |
ArrowDown: false, | |
} | |
const renderLoop = (delta: number) => { | |
requestAnimationFrame((time: number) => { | |
const render = createDrawFunction() | |
player = movePlayer(player, keyState, delta) | |
enemies = enemies.map(enemy => moveEnemy(player, enemy, enemies, delta)) | |
// setParticle(shootParticle(particle, player, mouse, delta)) | |
const playerRender = { type: objectTypes.RECT, x: player.x, y: player.y, width: player.width, height: player.height, contextProps: { fillStyle: '#ff0000' } } | |
const particleRender = { type: objectTypes.RECT, x: particle.originX + particle.x, y: particle.originY + particle.y, width: 5, height: 5, contextProps: { fillStyle: '#ff00ff' } } | |
render({ | |
camera: { position: { x: player.x, y: player.y, }, zoom: 1 }, | |
context, | |
canvasHeight: window.innerHeight, | |
canvasWidth: window.innerWidth, | |
objects: [ | |
{ type: objectTypes.RECT, x: player.x, y: player.y, width: window.innerWidth, height: window.innerHeight, contextProps: { fillStyle: '#000' } }, | |
...getBackground(bg!, player), | |
{ type: objectTypes.RECT, x: 60, y: 80, width: 20, height: 40, contextProps: { fillStyle: '#0000ff' } }, | |
{ type: objectTypes.RECT, x: 160, y: 180, width: 20, height: 40, contextProps: { fillStyle: '#0000ff' } }, | |
...enemies.map(e => ({ | |
type: objectTypes.RECT, x: e.x, y: e.y, width: e.width, height: e.height, contextProps: { fillStyle: '#ffff00' }, | |
})), | |
playerRender, | |
particleRender, | |
] | |
}) | |
renderLoop(performance.now() - time) | |
}) | |
} | |
renderLoop(0) | |
} | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment