delete canvas2D map canvas — WebGL is the only renderer left

After every map-anchored visual moved to WebGL (terrain, territory,
structures, names, selection boxes, ghost preview, move chevrons,
FX) there's nothing drawing to the canvas2D context. Rip it out.
This commit is contained in:
evanpelle
2026-05-16 22:21:17 -07:00
parent 5002dfdc2a
commit b2f84aad33
4 changed files with 36 additions and 128 deletions
+28 -7
View File
@@ -236,7 +236,7 @@ function mountWebGLDebugRenderer(
transformHandler: import("./graphics/TransformHandler").TransformHandler,
gameView: GameView,
eventBus: EventBus,
): { builder: WebGLFrameBuilder; syncCamera: () => void } {
): { builder: WebGLFrameBuilder } {
const gameMap = terrainMap.gameMap;
const mapWidth = gameMap.width();
const mapHeight = gameMap.height();
@@ -368,7 +368,17 @@ function mountWebGLDebugRenderer(
view.setSelectedUnits(e.unit ? [e.unit.id()] : []);
});
return { builder: new WebGLFrameBuilder(view), syncCamera };
// Self-driving RAF: syncCamera reads the latest camera state from
// TransformHandler, pushes it to WebGL, and synchronously invokes the
// renderer's captured frame callback (which draws). One RAF = one
// synchronized camera-update + WebGL render.
const driveFrame = (): void => {
syncCamera();
requestAnimationFrame(driveFrame);
};
requestAnimationFrame(driveFrame);
return { builder: new WebGLFrameBuilder(view) };
}
async function createClientGame(
@@ -412,23 +422,34 @@ async function createClientGame(
lobbyConfig.gameStartInfo.players,
);
const canvas = createCanvas();
// Transparent fullscreen overlay used purely as the pointer-event /
// bounding-rect target for InputHandler + TransformHandler. The actual
// map drawing happens on the WebGL canvas created in mountWebGLDebugRenderer.
const inputOverlay = document.createElement("div");
inputOverlay.id = "game-input-overlay";
inputOverlay.style.position = "fixed";
inputOverlay.style.left = "0";
inputOverlay.style.top = "0";
inputOverlay.style.width = "100%";
inputOverlay.style.height = "100%";
inputOverlay.style.touchAction = "none";
document.body.appendChild(inputOverlay);
const soundManager = new SoundManager(eventBus, userSettings);
try {
const gameRenderer = createRenderer(
canvas,
inputOverlay,
gameView,
eventBus,
lobbyConfig.playerRole,
);
const { builder: webglBuilder, syncCamera } = mountWebGLDebugRenderer(
const { builder: webglBuilder } = mountWebGLDebugRenderer(
gameMap,
gameRenderer.transformHandler,
gameView,
eventBus,
);
gameRenderer.onPreRender = syncCamera;
console.log(
`creating private game got difficulty: ${lobbyConfig.gameStartInfo.config.difficulty}`,
@@ -439,7 +460,7 @@ async function createClientGame(
clientID,
eventBus,
gameRenderer,
new InputHandler(gameView, gameRenderer.uiState, canvas, eventBus),
new InputHandler(gameView, gameRenderer.uiState, inputOverlay, eventBus),
transport,
worker,
gameView,
+1 -1
View File
@@ -246,7 +246,7 @@ export class InputHandler {
constructor(
private gameView: GameView,
public uiState: UIState,
private canvas: HTMLCanvasElement,
private canvas: HTMLElement,
private eventBus: EventBus,
) {}
+6 -119
View File
@@ -37,12 +37,12 @@ import { UnitDisplay } from "./layers/UnitDisplay";
import { WinModal } from "./layers/WinModal";
export function createRenderer(
canvas: HTMLCanvasElement,
inputEl: HTMLElement,
game: GameView,
eventBus: EventBus,
playerRole: string | null,
): GameRenderer {
const transformHandler = new TransformHandler(game, eventBus, canvas);
const transformHandler = new TransformHandler(game, eventBus, inputEl);
const userSettings = new UserSettings();
const uiState: UIState = {
@@ -298,9 +298,7 @@ export function createRenderer(
];
return new GameRenderer(
game,
eventBus,
canvas,
transformHandler,
uiState,
layers,
@@ -309,56 +307,26 @@ export function createRenderer(
}
export class GameRenderer {
private context: CanvasRenderingContext2D;
private layerTickState = new Map<Layer, { lastTickAtMs: number }>();
private renderFramesSinceLastTick: number = 0;
private renderLayerDurationsSinceLastTick: Record<string, number> = {};
public onPreRender: (() => void) | null = null;
constructor(
private game: GameView,
private eventBus: EventBus,
private canvas: HTMLCanvasElement,
public transformHandler: TransformHandler,
public uiState: UIState,
private layers: Layer[],
private performanceOverlay: PerformanceOverlay,
) {
const context = canvas.getContext("2d", { alpha: true });
if (context === null) throw new Error("2d context not supported");
this.context = context;
}
) {}
initialize() {
this.eventBus.on(RedrawGraphicsEvent, () => this.redraw());
this.layers.forEach((l) => l.init?.());
// only append the canvas if it's not already in the document to avoid reparenting side-effects
if (!document.body.contains(this.canvas)) {
document.body.appendChild(this.canvas);
}
window.addEventListener("resize", () => this.resizeCanvas());
this.resizeCanvas();
window.addEventListener("resize", () =>
this.transformHandler.updateCanvasBoundingRect(),
);
//show whole map on startup
this.transformHandler.centerAll(0.9);
let rafId = requestAnimationFrame(() => this.renderGame());
this.canvas.addEventListener("contextlost", () => {
cancelAnimationFrame(rafId);
});
this.canvas.addEventListener("contextrestored", () => {
this.redraw();
rafId = requestAnimationFrame(() => this.renderGame());
});
}
resizeCanvas() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
this.transformHandler.updateCanvasBoundingRect();
//this.redraw()
}
redraw() {
@@ -369,86 +337,10 @@ export class GameRenderer {
});
}
renderGame() {
const shouldProfileFrame = FrameProfiler.isEnabled();
if (shouldProfileFrame) {
FrameProfiler.clear();
}
const start = performance.now();
this.onPreRender?.();
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
const handleTransformState = (
needsTransform: boolean,
active: boolean,
): boolean => {
if (needsTransform && !active) {
this.context.save();
this.transformHandler.handleTransform(this.context);
return true;
} else if (!needsTransform && active) {
this.context.restore();
return false;
}
return active;
};
let isTransformActive = false;
for (const layer of this.layers) {
const needsTransform = layer.shouldTransform?.() ?? false;
isTransformActive = handleTransformState(
needsTransform,
isTransformActive,
);
if (shouldProfileFrame) {
const layerStart = FrameProfiler.start();
layer.renderLayer?.(this.context);
FrameProfiler.end(
layer.constructor?.name ?? "UnknownLayer",
layerStart,
);
} else {
layer.renderLayer?.(this.context);
}
}
handleTransformState(false, isTransformActive); // Ensure context is clean after rendering
this.transformHandler.resetChanged();
requestAnimationFrame(() => this.renderGame());
const duration = performance.now() - start;
if (shouldProfileFrame) {
const layerDurations = FrameProfiler.consume();
this.renderFramesSinceLastTick++;
for (const [name, ms] of Object.entries(layerDurations)) {
this.renderLayerDurationsSinceLastTick[name] =
(this.renderLayerDurationsSinceLastTick[name] ?? 0) + ms;
}
this.performanceOverlay.updateFrameMetrics(duration, layerDurations);
}
if (duration > 50) {
console.warn(
`tick ${this.game.ticks()} took ${duration}ms to render frame`,
);
}
}
tick() {
const nowMs = performance.now();
const shouldProfileTick = FrameProfiler.isEnabled();
if (shouldProfileTick) {
this.performanceOverlay.updateRenderPerTickMetrics(
this.renderFramesSinceLastTick,
this.renderLayerDurationsSinceLastTick,
);
this.renderFramesSinceLastTick = 0;
this.renderLayerDurationsSinceLastTick = {};
}
const tickLayerDurations: Record<string, number> = {};
for (const layer of this.layers) {
@@ -482,9 +374,4 @@ export class GameRenderer {
this.performanceOverlay.updateTickLayerMetrics(tickLayerDurations);
}
}
resize(width: number, height: number): void {
this.canvas.width = Math.ceil(width / window.devicePixelRatio);
this.canvas.height = Math.ceil(height / window.devicePixelRatio);
}
}
+1 -1
View File
@@ -40,7 +40,7 @@ export class TransformHandler {
constructor(
private game: GameView,
private eventBus: EventBus,
private canvas: HTMLCanvasElement,
private canvas: HTMLElement,
) {
this._boundingRect = this.canvas.getBoundingClientRect();
this.eventBus.on(ZoomEvent, (e) => this.onZoom(e));