mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:20:47 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -246,7 +246,7 @@ export class InputHandler {
|
||||
constructor(
|
||||
private gameView: GameView,
|
||||
public uiState: UIState,
|
||||
private canvas: HTMLCanvasElement,
|
||||
private canvas: HTMLElement,
|
||||
private eventBus: EventBus,
|
||||
) {}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user