mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-14 21:06:07 +00:00
Fix stretched icons (#2316)
## Description: Fixes #2257 (well, a part of it) This PR implements a new async helper function in client Utils.ts ( getSvgAspectRatio), which caches and then retrieves aspect ratios for Svg files. (I am not married to it being in Utils.ts, I just couldn't find a better place to put it, and it seemed like something that should be accessible to many files). It then implements this helper in RadialMenu.ts to fix incorrect aspect ratios. It uses the default value for the smaller side, and the computed value for the larger side. Before fixing the stretching in EventsDisplay.ts, this PR first consolidates the hard coded html for toggle buttons into a helper function (renderToggleButton). Afterwards, it adds width calculation to this helper function using getSvgAspectRatio. ## Potential flaws - getSvgAspectRatio might potentially be in the wrong file - EventsDisplay.ts consolidation may be out of scope, but it seemed necessary to make clean changes. It also introduces a slight delay before toggle buttons are loaded once a player enters the match - If the icon is not cached yet, the RadialMenu implementation shows the old stretched icon for a split second. In regular gameplay this should probably not be noticeable, - For some reason (I guess because of slightly too many characters) prettier split up exactly one of the renderToggleButton calls which makes it ugly, but thats what prettier wants lol - Not sure if I should add any tests? If so feel free to tell me, but I didn't so far. ## Screenshots Before <img width="323" height="416" alt="image" src="https://github.com/user-attachments/assets/709493c8-c8d6-48c1-9d44-4d6608d7da93" /> After <img width="323" height="390" alt="image" src="https://github.com/user-attachments/assets/a786c83e-2ef9-47d6-9858-1aeaa4ae548a" /> ## 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 ## Please put your Discord username so you can be contacted if a bug or regression is found: Lavodan --------- Co-authored-by: Evan <evanpelle@gmail.com>
This commit is contained in:
@@ -245,3 +245,56 @@ export function isInIframe(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSvgAspectRatio(src: string): Promise<number | null> {
|
||||
const self = getSvgAspectRatio as any;
|
||||
self.svgAspectRatioCache ??= new Map();
|
||||
|
||||
const cached = self.svgAspectRatioCache.get(src);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
||||
const resp = await fetch(src, { signal: controller.signal });
|
||||
clearTimeout(timeoutId);
|
||||
if (!resp.ok) throw new Error(`Fetch failed: ${resp.status}`);
|
||||
const text = await resp.text();
|
||||
|
||||
// Try parse viewBox
|
||||
const vbMatch = text.match(/viewBox="([^"]+)"/i);
|
||||
if (vbMatch) {
|
||||
const parts = vbMatch[1]
|
||||
.trim()
|
||||
.split(/[\s,]+/)
|
||||
.map(Number);
|
||||
if (parts.length === 4 && parts.every((n) => !Number.isNaN(n))) {
|
||||
const [, , vbW, vbH] = parts;
|
||||
if (vbW > 0 && vbH > 0) {
|
||||
const ratio = vbW / vbH;
|
||||
self.svgAspectRatioCache.set(src, ratio);
|
||||
return ratio;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to width/height attributes (may be with units; strip px)
|
||||
const widthMatch = text.match(/<svg[^>]*\swidth="([^"]+)"/i);
|
||||
const heightMatch = text.match(/<svg[^>]*\sheight="([^"]+)"/i);
|
||||
if (widthMatch && heightMatch) {
|
||||
const parseNum = (s: string) => Number(s.replace(/[^0-9.]/g, ""));
|
||||
const w = parseNum(widthMatch[1]);
|
||||
const h = parseNum(heightMatch[1]);
|
||||
if (w > 0 && h > 0) {
|
||||
const ratio = w / h;
|
||||
self.svgAspectRatioCache.set(src, ratio);
|
||||
return ratio;
|
||||
}
|
||||
}
|
||||
// Not an SVG or no usable metadata
|
||||
} catch (e) {
|
||||
// fetch may fail due to CORS or non-SVG..
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -131,6 +131,22 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderToggleButton(src: string, category: MessageCategory) {
|
||||
// Adding the literal for the default size ensures tailwind will generate the class
|
||||
const toggleButtonSizeMap = { default: "h-5" };
|
||||
return this.renderButton({
|
||||
content: html`<img
|
||||
src="${src}"
|
||||
class="${toggleButtonSizeMap["default"]}"
|
||||
style="filter: ${this.eventsFilters.get(category)
|
||||
? "grayscale(1) opacity(0.5)"
|
||||
: "none"}"
|
||||
/>`,
|
||||
onClick: () => this.toggleEventFilter(category),
|
||||
className: "cursor-pointer pointer-events-auto",
|
||||
});
|
||||
}
|
||||
|
||||
private toggleHidden() {
|
||||
this._hidden = !this._hidden;
|
||||
if (this._hidden) {
|
||||
@@ -935,76 +951,20 @@ export class EventsDisplay extends LitElement implements Layer {
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex gap-4">
|
||||
${this.renderButton({
|
||||
content: html`<img
|
||||
src="${swordIcon}"
|
||||
class="w-5 h-5"
|
||||
style="filter: ${this.eventsFilters.get(
|
||||
MessageCategory.ATTACK,
|
||||
)
|
||||
? "grayscale(1) opacity(0.5)"
|
||||
: "none"}"
|
||||
/>`,
|
||||
onClick: () =>
|
||||
this.toggleEventFilter(MessageCategory.ATTACK),
|
||||
className: "cursor-pointer pointer-events-auto",
|
||||
})}
|
||||
${this.renderButton({
|
||||
content: html`<img
|
||||
src="${nukeIcon}"
|
||||
class="w-5 h-5"
|
||||
style="filter: ${this.eventsFilters.get(
|
||||
MessageCategory.NUKE,
|
||||
)
|
||||
? "grayscale(1) opacity(0.5)"
|
||||
: "none"}"
|
||||
/>`,
|
||||
onClick: () =>
|
||||
this.toggleEventFilter(MessageCategory.NUKE),
|
||||
className: "cursor-pointer pointer-events-auto",
|
||||
})}
|
||||
${this.renderButton({
|
||||
content: html`<img
|
||||
src="${donateGoldIcon}"
|
||||
class="w-5 h-5"
|
||||
style="filter: ${this.eventsFilters.get(
|
||||
MessageCategory.TRADE,
|
||||
)
|
||||
? "grayscale(1) opacity(0.5)"
|
||||
: "none"}"
|
||||
/>`,
|
||||
onClick: () =>
|
||||
this.toggleEventFilter(MessageCategory.TRADE),
|
||||
className: "cursor-pointer pointer-events-auto",
|
||||
})}
|
||||
${this.renderButton({
|
||||
content: html`<img
|
||||
src="${allianceIcon}"
|
||||
class="w-5 h-5"
|
||||
style="filter: ${this.eventsFilters.get(
|
||||
MessageCategory.ALLIANCE,
|
||||
)
|
||||
? "grayscale(1) opacity(0.5)"
|
||||
: "none"}"
|
||||
/>`,
|
||||
onClick: () =>
|
||||
this.toggleEventFilter(MessageCategory.ALLIANCE),
|
||||
className: "cursor-pointer pointer-events-auto",
|
||||
})}
|
||||
${this.renderButton({
|
||||
content: html`<img
|
||||
src="${chatIcon}"
|
||||
class="w-5 h-5"
|
||||
style="filter: ${this.eventsFilters.get(
|
||||
MessageCategory.CHAT,
|
||||
)
|
||||
? "grayscale(1) opacity(0.5)"
|
||||
: "none"}"
|
||||
/>`,
|
||||
onClick: () =>
|
||||
this.toggleEventFilter(MessageCategory.CHAT),
|
||||
className: "cursor-pointer pointer-events-auto",
|
||||
})}
|
||||
${this.renderToggleButton(
|
||||
swordIcon,
|
||||
MessageCategory.ATTACK,
|
||||
)}
|
||||
${this.renderToggleButton(nukeIcon, MessageCategory.NUKE)}
|
||||
${this.renderToggleButton(
|
||||
donateGoldIcon,
|
||||
MessageCategory.TRADE,
|
||||
)}
|
||||
${this.renderToggleButton(
|
||||
allianceIcon,
|
||||
MessageCategory.ALLIANCE,
|
||||
)}
|
||||
${this.renderToggleButton(chatIcon, MessageCategory.CHAT)}
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
${this.latestGoldAmount !== null
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as d3 from "d3";
|
||||
import backIcon from "../../../../resources/images/BackIconWhite.svg";
|
||||
import { EventBus, GameEvent } from "../../../core/EventBus";
|
||||
import { CloseViewEvent } from "../../InputHandler";
|
||||
import { translateText } from "../../Utils";
|
||||
import { getSvgAspectRatio, translateText } from "../../Utils";
|
||||
import { Layer } from "./Layer";
|
||||
import {
|
||||
CenterButtonElement,
|
||||
@@ -542,7 +542,7 @@ export class RadialMenu implements Layer {
|
||||
.style("opacity", disabled ? 0.5 : 1)
|
||||
.text(d.data.text);
|
||||
} else {
|
||||
content
|
||||
const imgSel = content
|
||||
.append("image")
|
||||
.attr("xlink:href", d.data.icon!)
|
||||
.attr("width", this.config.iconSize)
|
||||
@@ -551,6 +551,25 @@ export class RadialMenu implements Layer {
|
||||
.attr("y", arc.centroid(d)[1] - this.config.iconSize / 2)
|
||||
.attr("opacity", disabled ? 0.5 : 1);
|
||||
|
||||
getSvgAspectRatio(d.data.icon!).then((aspect) => {
|
||||
if (!aspect || aspect === 1) return;
|
||||
|
||||
let width = this.config.iconSize;
|
||||
let height = this.config.iconSize;
|
||||
const biggerLength = Math.round(width * aspect);
|
||||
if (aspect > 1) {
|
||||
width = biggerLength;
|
||||
} else {
|
||||
height = biggerLength;
|
||||
}
|
||||
|
||||
imgSel
|
||||
.attr("width", width)
|
||||
.attr("height", height)
|
||||
.attr("x", arc.centroid(d)[0] - width / 2)
|
||||
.attr("y", arc.centroid(d)[1] - height / 2);
|
||||
});
|
||||
|
||||
if (this.params && d.data.cooldown?.(this.params)) {
|
||||
const cooldown = Math.ceil(d.data.cooldown?.(this.params));
|
||||
content
|
||||
|
||||
Reference in New Issue
Block a user