render player names efficiently

This commit is contained in:
evanpelle
2024-08-11 13:47:10 -07:00
parent 13808f4d70
commit 1070a5171a
13 changed files with 207 additions and 108 deletions
+232
View File
@@ -0,0 +1,232 @@
import {Colord} from "colord";
import {Cell, MutableGame, Game, PlayerEvent, Tile, TileEvent, Player, Execution, BoatEvent} from "../../core/Game";
import {Theme} from "../../core/Settings";
import {DragEvent, ZoomEvent} from "../InputHandler";
import {calculateBoundingBox, placeName} from "../NameBoxCalculator";
import {PseudoRandom} from "../../core/PseudoRandom";
import {BoatAttackExecution} from "../../core/execution/BoatAttackExecution";
import {NameRenderer} from "./NameRenderer";
export class GameRenderer {
private tempCanvas;
private scale: number = .8
private offsetX: number = 0
private offsetY: number = 100
private context: CanvasRenderingContext2D
private imageData: ImageData
private nameRenderer: NameRenderer;
constructor(private gs: Game, private theme: Theme, private canvas: HTMLCanvasElement) {
this.context = canvas.getContext("2d")
this.nameRenderer = new NameRenderer(gs, theme)
}
initialize() {
this.canvas = document.createElement('canvas');
this.context = this.canvas.getContext('2d');
// Set canvas style to fill the screen
this.canvas.style.position = 'fixed';
this.canvas.style.left = '0';
this.canvas.style.top = '0';
this.canvas.style.width = '100%';
this.canvas.style.height = '100%';
this.imageData = this.context.getImageData(0, 0, this.gs.width(), this.gs.height())
this.initImageData()
this.nameRenderer.initialize()
document.body.appendChild(this.canvas);
window.addEventListener('resize', () => this.resizeCanvas());
this.resizeCanvas();
requestAnimationFrame(() => this.renderGame());
}
initImageData() {
this.gs.forEachTile((tile) => {
//const color = this.theme.terrainColor(tile.terrain())
this.paintTile(tile)
})
}
resizeCanvas() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
//this.redraw()
}
renderGame() {
// Clear the canvas
this.context.setTransform(1, 0, 0, 1, 0, 0);
this.context.clearRect(0, 0, this.gs.width(), this.gs.height());
// Set background
this.context.fillStyle = this.theme.backgroundColor().toHex();
this.context.fillRect(0, 0, this.gs.width(), this.gs.height());
// Disable image smoothing for pixelated effect
if (this.scale > 3) {
this.context.imageSmoothingEnabled = false;
} else {
this.context.imageSmoothingEnabled = true;
}
// Apply zoom and pan
this.context.setTransform(
this.scale,
0,
0,
this.scale,
this.gs.width() / 2 - this.offsetX * this.scale,
this.gs.height() / 2 - this.offsetY * this.scale
);
if (this.tempCanvas != null) {
// Draw the game content from the temp canvas
this.context.drawImage(
this.tempCanvas,
-this.gs.width() / 2,
-this.gs.height() / 2,
this.gs.width(),
this.gs.height()
);
}
const [upperLeft, bottomRight] = this.boundingRect()
this.nameRenderer.render(this.context, this.scale, upperLeft, bottomRight)
// const paths = this.gs.executions().map(e => e as Execution).filter(e => e instanceof BoatAttackExecution).map(e => e as BoatAttackExecution).filter(e => e.path != null).map(e => e.path)
// paths.forEach(p => {
// p.forEach(t => {
// this.paintCell(t.cell(), new Colord({r: 255, g: 255, b: 255}))
// })
// })
requestAnimationFrame(() => this.renderGame());
}
tick() {
// Create a temporary canvas for the game content
this.tempCanvas = document.createElement('canvas');
const tempCtx = this.tempCanvas.getContext('2d');
this.tempCanvas.width = this.gs.width();
this.tempCanvas.height = this.gs.height();
// Put the ImageData on the temp canvas
tempCtx.putImageData(this.imageData, 0, 0);
this.nameRenderer.tick()
}
tileUpdate(event: TileEvent) {
this.paintTile(event.tile)
event.tile.neighbors().forEach(t => this.paintTile(t))
}
playerEvent(event: PlayerEvent) {
}
boatEvent(event: BoatEvent) {
this.paintCell(event.boat.tile().cell(), new Colord({r: 255, g: 255, b: 255}))
this.gs.neighbors(event.boat.tile()).forEach(t => this.paintTile(t))
}
resize(width: number, height: number): void {
this.canvas.width = Math.ceil(width / window.devicePixelRatio);
this.canvas.height = Math.ceil(height / window.devicePixelRatio);
}
paintTile(tile: Tile) {
let terrainColor = this.theme.terrainColor(tile.terrain())
this.paintCell(tile.cell(), terrainColor)
const owner = tile.owner()
if (owner.isPlayer()) {
if (tile.isBorder()) {
this.paintCell(tile.cell(), this.theme.borderColor(owner.id()))
} else {
this.paintCell(tile.cell(), this.theme.territoryColor(owner.id()))
}
}
}
paintCell(cell: Cell, color: Colord) {
const index = (cell.y * this.gs.width()) + cell.x
const offset = index * 4
this.imageData.data[offset] = color.rgba.r;
this.imageData.data[offset + 1] = color.rgba.g;
this.imageData.data[offset + 2] = color.rgba.b;
this.imageData.data[offset + 3] = color.rgba.a * 255 | 0
}
onZoom(event: ZoomEvent) {
const oldScale = this.scale;
const zoomFactor = 1 + event.delta / 600;
this.scale *= zoomFactor;
// Clamp the scale to prevent extreme zooming
this.scale = Math.max(0.1, Math.min(10, this.scale));
const canvasRect = this.canvas.getBoundingClientRect();
const canvasX = event.x - canvasRect.left;
const canvasY = event.y - canvasRect.top;
// Calculate the world point we want to zoom towards
const zoomPointX = (canvasX - this.gs.width() / 2) / oldScale + this.offsetX;
const zoomPointY = (canvasY - this.gs.height() / 2) / oldScale + this.offsetY;
// Adjust the offset
this.offsetX = zoomPointX - (canvasX - this.gs.width() / 2) / this.scale;
this.offsetY = zoomPointY - (canvasY - this.gs.height() / 2) / this.scale;
}
onMove(event: DragEvent) {
this.offsetX -= event.deltaX / this.scale;
this.offsetY -= event.deltaY / this.scale;
}
screenToWorldCoordinates(screenX: number, screenY: number): Cell {
const canvasRect = this.canvas.getBoundingClientRect();
const canvasX = screenX - canvasRect.left;
const canvasY = screenY - canvasRect.top;
// Calculate the world point we want to zoom towards
const centerX = (canvasX - this.gs.width() / 2) / this.scale + this.offsetX;
const centerY = (canvasY - this.gs.height() / 2) / this.scale + this.offsetY;
const gameX = centerX + this.gs.width() / 2
const gameY = centerY + this.gs.height() / 2
return new Cell(Math.floor(gameX), Math.floor(gameY));
}
boundingRect(): [Cell, Cell] {
// Calculate the world point we want to zoom towards
const LeftX = (- this.gs.width() / 2) / this.scale + this.offsetX;
const TopY = (- this.gs.height() / 2) / this.scale + this.offsetY;
const gameLeftX = LeftX + this.gs.width() / 2
const gameTopY = TopY + this.gs.height() / 2
// Calculate the world point we want to zoom towards
const rightX = (screen.width - this.gs.width() / 2) / this.scale + this.offsetX;
const rightY = (screen.height - this.gs.height() / 2) / this.scale + this.offsetY;
const gameRightX = rightX + this.gs.width() / 2
const gameBottomY = rightY + this.gs.height() / 2
return [new Cell(Math.floor(gameLeftX), Math.floor(gameTopY)), new Cell(Math.floor(gameRightX), Math.floor(gameBottomY))]
}
}
+121
View File
@@ -0,0 +1,121 @@
import PriorityQueue from "priority-queue-typescript"
import {Cell, Game, Player} from "../../core/Game"
import {PseudoRandom} from "../../core/PseudoRandom"
import {Theme} from "../../core/Settings"
import {calculateBoundingBox} from "../NameBoxCalculator"
class RenderInfo {
constructor(public player: Player, public lastRendered: number, public location: Cell, public fontSize: number) { }
}
export class NameRenderer {
private lastChecked = 0
private refreshRate = 1000
private rand = new PseudoRandom(10)
private renderInfo: Map<Player, RenderInfo> = new Map()
private context: CanvasRenderingContext2D
private canvas: HTMLCanvasElement
private toRender: PriorityQueue<RenderInfo> = new PriorityQueue<RenderInfo>(1000, (a: RenderInfo, b: RenderInfo) => a.lastRendered - b.lastRendered);
private seenPlayers: Set<Player> = new Set()
constructor(private game: Game, private theme: Theme) {
}
public initialize() {
this.canvas = document.createElement('canvas');
this.context = this.canvas.getContext('2d');
this.canvas.style.position = 'fixed';
this.canvas.style.left = '0';
this.canvas.style.top = '0';
this.canvas.width = this.game.width();
this.canvas.height = this.game.height();
}
public render(mainContex: CanvasRenderingContext2D, scale: number, uppperLeft: Cell, bottomRight: Cell) {
// mainContex.drawImage(
// this.canvas,
// -this.game.width() / 2,
// -this.game.height() / 2,
// this.game.width(),
// this.game.height()
// )
for (const render of this.toRender) {
if (render.player.isAlive()) {
this.renderPlayerInfo(render, mainContex, scale, uppperLeft, bottomRight)
}
}
}
public tick() {
const now = Date.now()
if (now - this.lastChecked > this.refreshRate) {
this.lastChecked = now
for (const player of this.game.players()) {
if (!this.seenPlayers.has(player)) {
this.toRender.add(new RenderInfo(player, 0, null, null))
this.seenPlayers.add(player)
}
}
}
while (!this.toRender.empty() && now - this.toRender.peek().lastRendered > this.refreshRate) {
const renderInfo = this.toRender.poll()
this.calculateRenderInfo(renderInfo)
renderInfo.lastRendered = now + this.rand.nextInt(-50, 50)
this.toRender.add(renderInfo)
}
}
calculateRenderInfo(render: RenderInfo): boolean {
let wasUpdated = false
render.lastRendered = Date.now() + this.rand.nextInt(0, 100)
wasUpdated = true
const box = calculateBoundingBox(render.player)
const centerX = box.min.x + ((box.max.x - box.min.x) / 2)
const centerY = box.min.y + ((box.max.y - box.min.y) / 2)
render.location = new Cell(centerX, centerY)
render.fontSize = Math.max(Math.min(box.max.x - box.min.x, box.max.y - box.min.y) / render.player.info().name.length / 2, 2)
return wasUpdated
}
renderPlayerInfo(render: RenderInfo, context: CanvasRenderingContext2D, scale: number, uppperLeft: Cell, bottomRight: Cell) {
// console.log(`scale: ${scale}, fontSize: ${render.fontSize}, mult: ${scale * render.fontSize}`)
if (render.fontSize * scale < 10) {
return
}
const nameCenterX = Math.floor(render.location.x - this.game.width() / 2)
const nameCenterY = Math.floor(render.location.y - this.game.height() / 2)
if (render.location.x < uppperLeft.x || render.location.x > bottomRight.x || render.location.y < uppperLeft.y || render.location.y > bottomRight.y) {
return
}
// if (nameCenterX, ) {
// }
context.textRendering = "optimizeSpeed";
context.font = `${render.fontSize}px Arial`;
context.fillStyle = this.theme.playerInfoColor(render.player.id()).toHex();
context.textAlign = 'center';
context.textBaseline = 'middle';
context.fillText(render.player.info().name, nameCenterX, nameCenterY - render.fontSize / 2);
context.fillText(String(Math.floor(render.player.troops())), nameCenterX, nameCenterY + render.fontSize);
}
}