From cab3e1475f54416328dc6a4145b1a03aa155cb45 Mon Sep 17 00:00:00 2001 From: scamiv <6170744+scamiv@users.noreply.github.com> Date: Fri, 6 Feb 2026 01:29:13 +0100 Subject: [PATCH] perf(NameLayer): harden canvas coords --- src/client/graphics/TransformHandler.ts | 19 ++++++++---- src/client/graphics/layers/NameLayer.ts | 39 ++++++++++++++++++------- 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/src/client/graphics/TransformHandler.ts b/src/client/graphics/TransformHandler.ts index 6cf43cd77..cf8b339ca 100644 --- a/src/client/graphics/TransformHandler.ts +++ b/src/client/graphics/TransformHandler.ts @@ -70,11 +70,15 @@ export class TransformHandler { ); } - worldToScreenCoordinates(cell: Cell): { x: number; y: number } { + worldToCanvasCoordinates(cell: Cell): { x: number; y: number } { + return this.worldToCanvasCoordinatesXY(cell.x, cell.y); + } + + worldToCanvasCoordinatesXY(x: number, y: number): { x: number; y: number } { // Step 1: Convert from Cell coordinates to game coordinates // (reverse of Math.floor operation - we'll use the exact values) - const gameX = cell.x; - const gameY = cell.y; + const gameX = x; + const gameY = y; // Step 2: Reverse the game center offset calculation // Original: gameX = centerX + this.game.width() / 2 @@ -90,10 +94,15 @@ export class TransformHandler { const canvasY = (centerY - this.offsetY) * this.scale + this.game.height() / 2; + return { x: canvasX, y: canvasY }; + } + + worldToScreenCoordinates(cell: Cell): { x: number; y: number } { // Step 4: Convert canvas coordinates back to screen coordinates + const canvasPos = this.worldToCanvasCoordinates(cell); const canvasRect = this.boundingRect(); - const screenX = canvasX + canvasRect.left; - const screenY = canvasY + canvasRect.top; + const screenX = canvasPos.x + canvasRect.left; + const screenY = canvasPos.y + canvasRect.top; return { x: screenX, y: screenY }; } diff --git a/src/client/graphics/layers/NameLayer.ts b/src/client/graphics/layers/NameLayer.ts index 15da13915..20ee1414d 100644 --- a/src/client/graphics/layers/NameLayer.ts +++ b/src/client/graphics/layers/NameLayer.ts @@ -1,6 +1,6 @@ import { EventBus } from "../../../core/EventBus"; import { Theme } from "../../../core/configuration/Config"; -import { AllPlayers, Cell, nukeTypes, PlayerID } from "../../../core/game/Game"; +import { AllPlayers, nukeTypes, PlayerID } from "../../../core/game/Game"; import { GameUpdateType, UnitUpdate } from "../../../core/game/GameUpdates"; import { GameView, PlayerView } from "../../../core/game/GameView"; import { AlternateViewEvent } from "../../InputHandler"; @@ -324,6 +324,12 @@ export class NameLayer implements Layer { const fontFamily = this.theme.font(); const scale = this.transformHandler.scale; const tick = this.game.ticks(); + const [topLeft, bottomRight] = this.transformHandler.screenBoundingRect(); + const minX = topLeft.x; + const maxX = bottomRight.x; + const minY = topLeft.y; + const maxY = bottomRight.y; + const fontCache = new Map(); for (const player of this.game.playerViews()) { if (!player.isAlive()) { @@ -344,15 +350,16 @@ export class NameLayer implements Layer { continue; } - const worldCell = new Cell(nameLocation.x, nameLocation.y); - if (!this.transformHandler.isOnScreen(worldCell)) { + const worldX = nameLocation.x; + const worldY = nameLocation.y; + if (worldX <= minX || worldX >= maxX || worldY <= minY || worldY >= maxY) { continue; } - const screenPos = - this.transformHandler.worldToScreenCoordinates(worldCell); - const x = Math.round(screenPos.x); - const y = Math.round(screenPos.y); + const canvasPos = + this.transformHandler.worldToCanvasCoordinatesXY(worldX, worldY); + const x = Math.round(canvasPos.x); + const y = Math.round(canvasPos.y); const elementScale = Math.min(baseSize * 0.25, 3); const visualScale = scale * elementScale; @@ -364,7 +371,12 @@ export class NameLayer implements Layer { const iconPx = Math.max(8, Math.round(iconBasePx * visualScale)); ctx.save(); - ctx.font = `${fontPx}px ${fontFamily}`; + let font = fontCache.get(fontPx); + if (!font) { + font = `${fontPx}px ${fontFamily}`; + fontCache.set(fontPx, font); + } + ctx.font = font; ctx.fillStyle = this.theme.textColor(player); ctx.textBaseline = "middle"; ctx.textAlign = "left"; @@ -380,6 +392,7 @@ export class NameLayer implements Layer { iconsY, iconPx, fontFamily, + nowMs, ); const flag = player.cosmetics.flag ?? null; @@ -487,6 +500,7 @@ export class NameLayer implements Layer { centerY: number, iconPx: number, fontFamily: string, + nowMs: number, ): void { const myPlayer = this.game.myPlayer(); @@ -510,7 +524,7 @@ export class NameLayer implements Layer { icons.push({ kind: "image", src: traitorIcon, - alpha: this.getTraitorIconAlpha(remainingSeconds), + alpha: this.getTraitorIconAlpha(remainingSeconds, nowMs), }); } @@ -652,7 +666,10 @@ export class NameLayer implements Layer { } } - private getTraitorIconAlpha(remainingSeconds: number): number { + private getTraitorIconAlpha( + remainingSeconds: number, + nowMs: number, + ): number { if (remainingSeconds > 15) return 1; const clampedSeconds = Math.max(0, Math.min(15, remainingSeconds)); @@ -662,7 +679,7 @@ export class NameLayer implements Layer { const minDuration = 0.2; const duration = minDuration + (maxDuration - minDuration) * easedProgress; - const t = performance.now() / 1000; + const t = nowMs / 1000; const phase = (t % duration) / duration; const triangle = phase < 0.5 ? phase * 2 : 2 - phase * 2; return 0.3 + 0.7 * triangle;