Skip to content

Instantly share code, notes, and snippets.

@MartinMuzatko
Created August 28, 2022 18:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MartinMuzatko/a34bd2d847b3ee666ecb58c25733f1ec to your computer and use it in GitHub Desktop.
Save MartinMuzatko/a34bd2d847b3ee666ecb58c25733f1ec to your computer and use it in GitHub Desktop.
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