/// const ws = new WebSocket("/events") type Packet = "tick" | { pos: { id: number, x: number, y: number } } | { game: { width: number, height: number } } | { player: { id: number, name: string } } | { die: number[] } class Snake { parts: { x: number, y: number, dx: number, dy: number }[] = [] color: string dead = false constructor(public name: string) { this.color = name_color(name) } add_part(x: number, y: number) { if (!this.parts.length) return this.parts.push({ x, y, dx: 0, dy: 0 }) const last = this.parts[this.parts.length - 1] let dx = x - last.x, dy = y - last.y; if (x > last.x + 1 || x < last.x - 1) dx *= -1, dx /= Math.abs(dx) if (y > last.y + 1 || y < last.y - 1) dy *= -1, dy /= Math.abs(dy) this.parts.push({ x, y, dx, dy }) } } let size = 0 let tick_anim = 0; const snakes = new Map() let canvas: HTMLCanvasElement let ctx: CanvasRenderingContext2D document.addEventListener("DOMContentLoaded", () => { canvas = document.createElement("canvas") ctx = canvas.getContext("2d")! document.body.append(canvas) canvas.width = 1000; canvas.height = 1000; redraw() }) function redraw() { ctx.fillStyle = "black" ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.save() const scale = canvas.width / size ctx.scale(scale, scale) for (let x = 0; x < size; x++) { for (let y = 0; y < size; y++) { ctx.strokeStyle = "grey" ctx.lineWidth = 0.02 ctx.strokeRect(x, y, 1, 1) } } ctx.translate(0.5, 0.5) for (const [xo, yo] of [[0, 0], [-size, 0], [size, 0], [0, -size], [0, size]]) { ctx.save() ctx.translate(xo, yo) for (const snake of snakes.values()) { ctx.beginPath(); for (let i = 0; i < snake.parts.length; i++) { const p = snake.parts[i]; ctx.moveTo(p.x - p.dx, p.y - p.dy) if (i == snake.parts.length - 1) ctx.lineTo(tick_lerp(p.x - p.dx, p.x), tick_lerp(p.y - p.dy, p.y)) else ctx.lineTo(p.x, p.y) } ctx.lineCap = "round" ctx.lineJoin = "round" if (snake.dead) ctx.lineWidth = tick_lerp(0.6, 0.001) else ctx.lineWidth = 0.6; ctx.strokeStyle = snake.color ctx.stroke() } ctx.restore() } ctx.restore() ctx.save() ctx.translate(scale / 2, scale / 4) for (const [xo, yo] of [[0, 0], [-size * scale, 0], [size * scale, 0], [0, -size * scale], [0, size * scale]]) { ctx.save() ctx.translate(xo, yo) for (const snake of snakes.values()) { const p = snake.parts[snake.parts.length - 1]; if (!p) continue ctx.font = "30px sans-serif" if (snake.dead) ctx.fillStyle = `rgba(1,1,1,${tick_lerp(1, 0)})` else ctx.fillStyle = "white" ctx.textAlign = "center" ctx.textBaseline = "bottom" ctx.fillText(snake.name, tick_lerp(p.x - p.dx, p.x) * scale, tick_lerp(p.y - p.dy, p.y) * scale) } ctx.restore() } ctx.restore() tick_anim += 0.1 tick_anim = Math.min(1, tick_anim) requestAnimationFrame(redraw) } function tick_lerp(f: number, t: number) { return (1 - tick_anim) * f + tick_anim * t } function name_color(name: string): string { let hash = 0; for (let i = 0; i < name.length; i++) { hash += name.charCodeAt(i); hash |= 0; hash ^= hash << 13; hash ^= hash >> 17; hash ^= hash << 5; } return `hsl(${hash % 360}deg 80% 40%)`; } ws.onerror = console.error ws.onmessage = message => { const p = JSON.parse(message.data) as Packet console.log(p); if (p == "tick") { tick_anim = 0 const d = [] for (const [k, s] of snakes) { if (s.dead) d.push(k) } d.forEach(k => snakes.delete(k)) } else if ("game" in p) { snakes.clear() size = p.game.width } else if ("player" in p) { snakes.set(p.player.id, new Snake(p.player.name)) } else if ("pos" in p) { snakes.get(p.pos.id)?.add_part(p.pos.x, p.pos.y) } else if ("die" in p) { for (const d of p.die) { const s = snakes.get(d) if (s) s.dead = true } } }