mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 14:41:35 +00:00
Enhance WorkerCanvas2DRenderer with terrain handling improvements
- Added support for terrain base RGBA values to optimize rendering. - Refactored tile marking logic to utilize map dimensions for improved accuracy. - Updated view size handling to prevent unnecessary canvas resizing. - Introduced methods to rebuild terrain base and refresh terrain for better visual fidelity. - Cleaned up initialization and cleanup processes for better resource management.
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import { Theme } from "../configuration/Config";
|
||||
import { PastelTheme } from "../configuration/PastelTheme";
|
||||
import { PastelThemeDark } from "../configuration/PastelThemeDark";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { TerrainMapData } from "../game/TerrainMapLoader";
|
||||
import { GameRunner } from "../GameRunner";
|
||||
@@ -14,12 +16,15 @@ export class WorkerCanvas2DRenderer {
|
||||
private rasterCanvas: OffscreenCanvas | null = null;
|
||||
private rasterCtx: Offscreen2D | null = null;
|
||||
private rasterImage: ImageData | null = null;
|
||||
private terrainBaseRgba: Uint8Array | null = null;
|
||||
|
||||
private gameViewAdapter: GameViewAdapter | null = null;
|
||||
private gameRunner: GameRunner | null = null;
|
||||
private theme: Theme | null = null;
|
||||
|
||||
private ready = false;
|
||||
private mapWidth = 1;
|
||||
private mapHeight = 1;
|
||||
|
||||
private viewScale = 1;
|
||||
private viewOffsetX = 0;
|
||||
@@ -58,6 +63,11 @@ export class WorkerCanvas2DRenderer {
|
||||
this.gameRunner = gameRunner;
|
||||
this.theme = theme;
|
||||
|
||||
const mapW = gameRunner.game.width();
|
||||
const mapH = gameRunner.game.height();
|
||||
this.mapWidth = mapW;
|
||||
this.mapHeight = mapH;
|
||||
|
||||
this.gameViewAdapter = new GameViewAdapter(
|
||||
gameRunner.game,
|
||||
mapData,
|
||||
@@ -66,8 +76,6 @@ export class WorkerCanvas2DRenderer {
|
||||
cosmeticsByClientID,
|
||||
);
|
||||
|
||||
const mapW = gameRunner.game.width();
|
||||
const mapH = gameRunner.game.height();
|
||||
this.rasterCanvas = new OffscreenCanvas(mapW, mapH);
|
||||
this.rasterCtx = this.rasterCanvas.getContext("2d", {
|
||||
alpha: true,
|
||||
@@ -94,6 +102,7 @@ export class WorkerCanvas2DRenderer {
|
||||
|
||||
// First paint.
|
||||
this.rebuildPaletteFromGame();
|
||||
this.rebuildTerrainBase();
|
||||
this.markAllDirty();
|
||||
this.tick();
|
||||
}
|
||||
@@ -105,9 +114,12 @@ export class WorkerCanvas2DRenderer {
|
||||
this.rasterCanvas = null;
|
||||
this.rasterCtx = null;
|
||||
this.rasterImage = null;
|
||||
this.terrainBaseRgba = null;
|
||||
this.gameViewAdapter = null;
|
||||
this.gameRunner = null;
|
||||
this.theme = null;
|
||||
this.mapWidth = 1;
|
||||
this.mapHeight = 1;
|
||||
this.dirtyChunkFlags = new Uint8Array(0);
|
||||
this.dirtyChunkQueue = new Uint32Array(0);
|
||||
this.dirtyHead = 0;
|
||||
@@ -117,8 +129,13 @@ export class WorkerCanvas2DRenderer {
|
||||
|
||||
setViewSize(width: number, height: number): void {
|
||||
if (!this.canvas) return;
|
||||
this.canvas.width = Math.max(1, Math.floor(width));
|
||||
this.canvas.height = Math.max(1, Math.floor(height));
|
||||
const nextWidth = Math.max(1, Math.floor(width));
|
||||
const nextHeight = Math.max(1, Math.floor(height));
|
||||
if (this.canvas.width === nextWidth && this.canvas.height === nextHeight) {
|
||||
return;
|
||||
}
|
||||
this.canvas.width = nextWidth;
|
||||
this.canvas.height = nextHeight;
|
||||
}
|
||||
|
||||
setViewTransform(scale: number, offsetX: number, offsetY: number): void {
|
||||
@@ -160,13 +177,15 @@ export class WorkerCanvas2DRenderer {
|
||||
}
|
||||
|
||||
refreshTerrain(): void {
|
||||
this.rebuildTerrainBase();
|
||||
this.markAllDirty();
|
||||
}
|
||||
|
||||
markTile(tile: TileRef): void {
|
||||
if (!this.ready || !this.gameRunner) return;
|
||||
const x = this.gameRunner.game.x(tile);
|
||||
const y = this.gameRunner.game.y(tile);
|
||||
if (!this.ready) return;
|
||||
// TileRef is a linear index (y * width + x).
|
||||
const x = tile % this.mapWidth;
|
||||
const y = (tile / this.mapWidth) | 0;
|
||||
this.markChunkAt(x, y);
|
||||
}
|
||||
|
||||
@@ -188,14 +207,29 @@ export class WorkerCanvas2DRenderer {
|
||||
!this.theme ||
|
||||
!this.gameViewAdapter ||
|
||||
!this.rasterCtx ||
|
||||
!this.rasterImage
|
||||
!this.rasterImage ||
|
||||
!this.terrainBaseRgba
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mapW = this.gameRunner.game.width();
|
||||
const mapH = this.gameRunner.game.height();
|
||||
const data = this.rasterImage.data;
|
||||
const mapW = this.mapWidth;
|
||||
const mapH = this.mapHeight;
|
||||
const out = this.rasterImage.data;
|
||||
const base = this.terrainBaseRgba;
|
||||
const state = this.gameRunner.game.tileStateView();
|
||||
const row0 = this.paletteRow0;
|
||||
const maxSmallId = this.paletteMaxSmallId;
|
||||
|
||||
const falloutR = row0[0] ?? 120;
|
||||
const falloutG = row0[1] ?? 255;
|
||||
const falloutB = row0[2] ?? 71;
|
||||
const ownerMask = 0xfff;
|
||||
const falloutBit = 0x2000;
|
||||
|
||||
const mix65 = (a: number, b: number): number =>
|
||||
((a * 35 + b * 65 + 50) / 100) | 0;
|
||||
const mix50 = (a: number, b: number): number => (a + b + 1) >> 1;
|
||||
|
||||
const budgetMs = 6;
|
||||
const start = performance.now();
|
||||
@@ -219,52 +253,59 @@ export class WorkerCanvas2DRenderer {
|
||||
for (let y = sy; y < ey; y++) {
|
||||
const row = y * mapW;
|
||||
for (let x = sx; x < ex; x++) {
|
||||
const tile = this.gameRunner.game.ref(x, y);
|
||||
const tile = row + x;
|
||||
const s = state[tile];
|
||||
const owner = s & ownerMask;
|
||||
const hasFallout = (s & falloutBit) !== 0;
|
||||
|
||||
let r = 0,
|
||||
g = 0,
|
||||
b = 0,
|
||||
a = 255;
|
||||
const p = tile * 4;
|
||||
const tr = base[p];
|
||||
const tg = base[p + 1];
|
||||
const tb = base[p + 2];
|
||||
|
||||
if (this.gameRunner.game.hasFallout(tile)) {
|
||||
const idx = 0;
|
||||
r = this.paletteRow0[idx] ?? 120;
|
||||
g = this.paletteRow0[idx + 1] ?? 255;
|
||||
b = this.paletteRow0[idx + 2] ?? 71;
|
||||
} else if (this.gameRunner.game.hasOwner(tile)) {
|
||||
const ownerSmallId = this.gameRunner.game.ownerID(tile);
|
||||
const slot = 10 + Math.max(0, ownerSmallId);
|
||||
const idx = slot * 4;
|
||||
if (idx + 2 < this.paletteRow0.length) {
|
||||
r = this.paletteRow0[idx];
|
||||
g = this.paletteRow0[idx + 1];
|
||||
b = this.paletteRow0[idx + 2];
|
||||
} else {
|
||||
const rgba = this.theme.terrainColor(
|
||||
this.gameRunner.game,
|
||||
tile,
|
||||
).rgba;
|
||||
r = rgba.r;
|
||||
g = rgba.g;
|
||||
b = rgba.b;
|
||||
a = rgba.a ?? 255;
|
||||
}
|
||||
} else {
|
||||
const rgba = this.theme.terrainColor(
|
||||
this.gameRunner.game,
|
||||
tile,
|
||||
).rgba;
|
||||
r = rgba.r;
|
||||
g = rgba.g;
|
||||
b = rgba.b;
|
||||
a = rgba.a ?? 255;
|
||||
// Fast path: terrain only.
|
||||
if (owner === 0 && !hasFallout) {
|
||||
out[p] = tr;
|
||||
out[p + 1] = tg;
|
||||
out[p + 2] = tb;
|
||||
out[p + 3] = 255;
|
||||
continue;
|
||||
}
|
||||
|
||||
const p = (row + x) * 4;
|
||||
data[p] = r;
|
||||
data[p + 1] = g;
|
||||
data[p + 2] = b;
|
||||
data[p + 3] = a;
|
||||
let r = tr;
|
||||
let g = tg;
|
||||
let b = tb;
|
||||
|
||||
if (owner !== 0) {
|
||||
// Player colors start at slot 10.
|
||||
if (owner <= maxSmallId) {
|
||||
const idx = (10 + owner) * 4;
|
||||
if (idx + 2 < row0.length) {
|
||||
let pr = row0[idx];
|
||||
let pg = row0[idx + 1];
|
||||
let pb = row0[idx + 2];
|
||||
|
||||
if (hasFallout) {
|
||||
pr = mix50(pr, falloutR);
|
||||
pg = mix50(pg, falloutG);
|
||||
pb = mix50(pb, falloutB);
|
||||
}
|
||||
|
||||
r = mix65(tr, pr);
|
||||
g = mix65(tg, pg);
|
||||
b = mix65(tb, pb);
|
||||
}
|
||||
}
|
||||
} else if (hasFallout) {
|
||||
r = mix50(tr, falloutR);
|
||||
g = mix50(tg, falloutG);
|
||||
b = mix50(tb, falloutB);
|
||||
}
|
||||
|
||||
out[p] = r;
|
||||
out[p + 1] = g;
|
||||
out[p + 2] = b;
|
||||
out[p + 3] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,4 +419,114 @@ export class WorkerCanvas2DRenderer {
|
||||
this.paletteRow1 = row1;
|
||||
this.hasExternalPalette = false;
|
||||
}
|
||||
|
||||
private rebuildTerrainBase(): void {
|
||||
if (!this.gameRunner || !this.theme || !this.rasterImage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mapW = this.mapWidth;
|
||||
const mapH = this.mapHeight;
|
||||
const numTiles = mapW * mapH;
|
||||
const terrain = this.gameRunner.game.terrainDataView();
|
||||
const base = new Uint8Array(numTiles * 4);
|
||||
|
||||
const isDark = this.theme instanceof PastelThemeDark;
|
||||
const isPastel =
|
||||
this.theme instanceof PastelTheme ||
|
||||
this.theme instanceof PastelThemeDark;
|
||||
|
||||
if (isPastel) {
|
||||
// Decode terrain directly from packed terrain bytes (fast, no allocations).
|
||||
const shoreR = isDark ? 134 : 204;
|
||||
const shoreG = isDark ? 133 : 203;
|
||||
const shoreB = isDark ? 88 : 158;
|
||||
|
||||
const shorelineWaterR = isDark ? 50 : 100;
|
||||
const shorelineWaterG = isDark ? 50 : 143;
|
||||
const shorelineWaterB = isDark ? 50 : 255;
|
||||
|
||||
const waterBaseR = isDark ? 14 : 70;
|
||||
const waterBaseG = isDark ? 11 : 132;
|
||||
const waterBaseB = isDark ? 30 : 180;
|
||||
|
||||
for (let t = 0; t < numTiles; t++) {
|
||||
const b = terrain[t];
|
||||
const isLand = (b & 0x80) !== 0;
|
||||
const isShoreline = (b & 0x40) !== 0;
|
||||
const mag = b & 0x1f;
|
||||
|
||||
let r = 0,
|
||||
g = 0,
|
||||
bb = 0;
|
||||
|
||||
if (isLand && isShoreline) {
|
||||
r = shoreR;
|
||||
g = shoreG;
|
||||
bb = shoreB;
|
||||
} else if (!isLand) {
|
||||
// Water (ocean + lake share the same formula here).
|
||||
if (isShoreline) {
|
||||
r = shorelineWaterR;
|
||||
g = shorelineWaterG;
|
||||
bb = shorelineWaterB;
|
||||
} else if (isDark) {
|
||||
if (mag < 10) {
|
||||
const adj = 9 - mag;
|
||||
r = Math.max(waterBaseR + adj, 0);
|
||||
g = Math.max(waterBaseG + adj, 0);
|
||||
bb = Math.max(waterBaseB + adj, 0);
|
||||
} else {
|
||||
r = waterBaseR;
|
||||
g = waterBaseG;
|
||||
bb = waterBaseB;
|
||||
}
|
||||
} else {
|
||||
const m = mag < 10 ? mag : 10;
|
||||
const adj = 1 - m;
|
||||
r = Math.max(waterBaseR + adj, 0);
|
||||
g = Math.max(waterBaseG + adj, 0);
|
||||
bb = Math.max(waterBaseB + adj, 0);
|
||||
}
|
||||
} else {
|
||||
// Land (non-shore)
|
||||
if (mag < 10) {
|
||||
r = isDark ? 140 : 190;
|
||||
g = (isDark ? 170 : 220) - 2 * mag;
|
||||
bb = isDark ? 88 : 138;
|
||||
} else if (mag < 20) {
|
||||
r = (isDark ? 150 : 200) + 2 * mag;
|
||||
g = (isDark ? 133 : 183) + 2 * mag;
|
||||
bb = (isDark ? 88 : 138) + 2 * mag;
|
||||
} else {
|
||||
const half = mag >> 1;
|
||||
r = (isDark ? 180 : 230) + half;
|
||||
g = (isDark ? 180 : 230) + half;
|
||||
bb = (isDark ? 180 : 230) + half;
|
||||
}
|
||||
}
|
||||
|
||||
const p = t * 4;
|
||||
base[p] = r;
|
||||
base[p + 1] = g;
|
||||
base[p + 2] = bb;
|
||||
base[p + 3] = 255;
|
||||
}
|
||||
} else {
|
||||
// Fallback for other themes: call the theme once per tile (slow but only on init/theme change).
|
||||
for (let t = 0; t < numTiles; t++) {
|
||||
const rgba = this.theme.terrainColor(
|
||||
this.gameRunner.game,
|
||||
t as TileRef,
|
||||
).rgba;
|
||||
const p = t * 4;
|
||||
base[p] = rgba.r;
|
||||
base[p + 1] = rgba.g;
|
||||
base[p + 2] = rgba.b;
|
||||
base[p + 3] = rgba.a ?? 255;
|
||||
}
|
||||
}
|
||||
|
||||
this.terrainBaseRgba = base;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user