Improve icons readability (#1321)

## Description:

REQUIRES THE LEVEL PR TO BE MERGED FIRST (as it is using the same layer)
https://github.com/openfrontio/OpenFrontIO/pull/1305

#### Icons only zoom level

![image](https://github.com/user-attachments/assets/2ce30a5b-281c-425a-add6-55745c930859)

#### Icons and sprite zoom level

![image](https://github.com/user-attachments/assets/ad8018d7-0537-4baf-b97e-d38b0ccec6cd)

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
- [x] I understand that submitting code with bugs that could have been
caught through manual testing blocks releases and new features for all
contributors

## Please put your Discord username so you can be contacted if a bug or
regression is found:

Vivacious Box

---------

Co-authored-by: Scott Anderson <662325+scottanderson@users.noreply.github.com>
This commit is contained in:
Vivacious Box
2025-07-03 05:27:49 +02:00
committed by GitHub
parent adb0d07074
commit 501fb3a078
2 changed files with 80 additions and 24 deletions
@@ -20,11 +20,12 @@ class StructureRenderInfo {
public owner: PlayerID,
public pixiContainer: PIXI.Container,
public level: number = 0,
public underConstruction: boolean = true,
) {}
}
const ZOOM_THRESHOLD = 2.8; // below this zoom level, structures are not rendered
const ZOOM_THRESHOLD = 2.5;
const ICON_SIZE = 24;
const OFFSET_ZOOM_Y = 15; // offset for the y position of the icon to avoid hiding the structure beneath
const OFFSET_ZOOM_Y = 12; // offset for the y position of the icon to avoid hiding the structure beneath
export class StructureIconsLayer implements Layer {
private pixicanvas: HTMLCanvasElement;
@@ -110,7 +111,7 @@ export class StructureIconsLayer implements Layer {
}
resizeCanvas() {
if (this.renderer.view) {
if (this.renderer) {
this.pixicanvas.width = window.innerWidth;
this.pixicanvas.height = window.innerHeight;
this.renderer.resize(innerWidth, innerHeight, 1);
@@ -143,10 +144,14 @@ export class StructureIconsLayer implements Layer {
if (this.seenUnits.has(unitView)) {
const render = this.findRenderByUnit(unitView);
if (render) {
this.checkForConstructionState(render, unitView);
this.checkForOwnershipChange(render, unitView);
this.checkForLevelChange(render, unitView);
}
} else if (this.structures.has(unitView.type())) {
} else if (
this.structures.has(unitView.type()) ||
unitView.type() === UnitType.Construction
) {
this.addNewStructure(unitView);
}
}
@@ -159,8 +164,23 @@ export class StructureIconsLayer implements Layer {
}
}
private checkForConstructionState(
render: StructureRenderInfo,
unit: UnitView,
) {
if (
render.underConstruction &&
render.unit.type() !== UnitType.Construction
) {
render.underConstruction = false;
render.pixiContainer?.destroy();
render.pixiContainer = this.createPixiSprite(unit);
this.shouldRedraw = true;
}
}
private checkForOwnershipChange(render: StructureRenderInfo, unit: UnitView) {
if (render && render.owner !== unit.owner().id()) {
if (render.owner !== unit.owner().id()) {
render.owner = unit.owner().id();
render.pixiContainer?.destroy();
render.pixiContainer = this.createPixiSprite(unit);
@@ -169,7 +189,7 @@ export class StructureIconsLayer implements Layer {
}
private checkForLevelChange(render: StructureRenderInfo, unit: UnitView) {
if (render && render.level !== unit.level()) {
if (render.level !== unit.level()) {
render.level = unit.level();
render.pixiContainer?.destroy();
render.pixiContainer = this.createPixiSprite(unit);
@@ -200,22 +220,41 @@ export class StructureIconsLayer implements Layer {
}
private createTexture(unit: UnitView): PIXI.Texture {
const cacheKey = `${unit.owner().id()}-${unit.type()}`;
const isConstruction = unit.type() === UnitType.Construction;
const constructionType = unit.constructionType();
if (isConstruction && constructionType === undefined) {
console.warn(
`Unit ${unit.id()} is a construction but has no construction type.`,
);
return PIXI.Texture.EMPTY;
}
const structureType = isConstruction ? constructionType! : unit.type();
const cacheKey = isConstruction
? `construction-${structureType}`
: `${unit.owner().id()}-${structureType}`;
if (this.textureCache.has(cacheKey)) {
return this.textureCache.get(cacheKey)!;
}
const structureCanvas = document.createElement("canvas");
structureCanvas.width = ICON_SIZE;
structureCanvas.height = ICON_SIZE;
const context = structureCanvas.getContext("2d")!;
context.fillStyle = this.theme
.territoryColor(unit.owner())
.lighten(0.1)
.toRgbString();
const borderColor = this.theme
.borderColor(unit.owner())
.darken(0.2)
.toRgbString();
let borderColor: string;
if (isConstruction) {
context.fillStyle = "rgb(198, 198, 198)";
borderColor = "rgb(128, 127, 127)";
} else {
context.fillStyle = this.theme
.territoryColor(unit.owner())
.lighten(0.06)
.toRgbString();
borderColor = this.theme
.borderColor(unit.owner())
.darken(0.08)
.toRgbString();
}
context.strokeStyle = borderColor;
context.beginPath();
context.arc(
@@ -228,9 +267,9 @@ export class StructureIconsLayer implements Layer {
context.fill();
context.lineWidth = 1;
context.stroke();
const structureInfo = this.structures.get(unit.type());
const structureInfo = this.structures.get(structureType);
if (!structureInfo?.image) {
console.warn(`Image not loaded for unit type: ${unit.type()}`);
console.warn(`Image not loaded for unit type: ${structureType}`);
return PIXI.Texture.from(structureCanvas);
}
context.drawImage(
@@ -266,10 +305,17 @@ export class StructureIconsLayer implements Layer {
text.position.y = -ICON_SIZE / 2 - 2;
parentContainer.addChild(text);
}
parentContainer.position.set(
Math.round(screenPos.x),
Math.round(screenPos.y - this.transformHandler.scale * OFFSET_ZOOM_Y),
);
const posX = Math.round(screenPos.x);
let posY = Math.round(screenPos.y);
if (this.transformHandler.scale >= ZOOM_THRESHOLD) {
// Adjust the y position based on zoom level to avoid hiding the structure beneath
posY = Math.round(
screenPos.y - this.transformHandler.scale * OFFSET_ZOOM_Y,
);
} else {
posY = Math.round(screenPos.y);
}
parentContainer.position.set(posX, posY);
parentContainer.scale.set(Math.min(1, this.transformHandler.scale));
this.stage.addChild(parentContainer);
return parentContainer;
@@ -298,9 +344,14 @@ export class StructureIconsLayer implements Layer {
new Cell(worldX, worldY),
);
screenPos.x = Math.round(screenPos.x);
screenPos.y = Math.round(
screenPos.y - this.transformHandler.scale * OFFSET_ZOOM_Y,
);
if (this.transformHandler.scale >= ZOOM_THRESHOLD) {
// Adjust the y position based on zoom level to avoid hiding the structure beneath
screenPos.y = Math.round(
screenPos.y - this.transformHandler.scale * OFFSET_ZOOM_Y,
);
} else {
screenPos.y = Math.round(screenPos.y);
}
// Check if the sprite is on screen (with margin for partial visibility)
const margin = ICON_SIZE;
@@ -329,6 +380,7 @@ export class StructureIconsLayer implements Layer {
unitView.owner().id(),
this.createPixiSprite(unitView),
unitView.level(),
unitView.type() === UnitType.Construction,
);
this.renders.push(render);
this.computeNewLocation(render);
@@ -24,6 +24,7 @@ const selectedUnitColor = colord({ r: 0, g: 255, b: 255 });
const BASE_BORDER_RADIUS = 16.5;
const BASE_TERRITORY_RADIUS = 13.5;
const RADIUS_SCALE_FACTOR = 0.5;
const ZOOM_THRESHOLD = 2.5; // below this zoom level, structures are not rendered
interface UnitRenderConfig {
icon: string;
@@ -151,6 +152,9 @@ export class StructureLayer implements Layer {
}
renderLayer(context: CanvasRenderingContext2D) {
if (this.transformHandler.scale <= ZOOM_THRESHOLD) {
return;
}
context.drawImage(
this.canvas,
-this.game.width() / 2,