Make world building icons uniform size and crisp
Draw every world building icon at a fixed size (1.25x tile, centered on the footprint) so all buildings' icons read at the same size regardless of footprint, and a little larger than before. Render the icon as vector via a cached QSvgRenderer at the view scale each draw instead of downscaling a pre-rasterized pixmap, which was blurry. Center the construction-site icon on the footprint with the progress percentage bottom-aligned. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y7N59FsLA5e2kuVdqe4Uhc
This commit is contained in:
@@ -101,9 +101,14 @@ const WorldIconEntry kWorldIconFiles[] = {
|
|||||||
{ BuildingType::EnemyDefenceStation, "station.svg" },
|
{ BuildingType::EnemyDefenceStation, "station.svg" },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Renders an icon SVG's glyph only (the chip background rect stripped) into a
|
// On-screen size of every world icon, as a multiple of one tile (REQ-UI-WORLD-ICON):
|
||||||
// transparent pixmap, recoloring the white glyph stroke to inkHex.
|
// a little over one tile so all buildings' icons read at the same size regardless
|
||||||
QPixmap renderWorldIcon(const QByteArray& svg, const QString& inkHex)
|
// of footprint.
|
||||||
|
const qreal kWorldIconTileFactor = 1.25;
|
||||||
|
|
||||||
|
// Produces an icon SVG containing only the glyph (the chip background rect
|
||||||
|
// stripped), with the white glyph stroke recolored to inkHex.
|
||||||
|
QByteArray worldIconSvg(const QByteArray& svg, const QString& inkHex)
|
||||||
{
|
{
|
||||||
QString s = QString::fromUtf8(svg);
|
QString s = QString::fromUtf8(svg);
|
||||||
// Drop the full-canvas chip rect only; some glyphs use their own <rect>
|
// Drop the full-canvas chip rect only; some glyphs use their own <rect>
|
||||||
@@ -113,14 +118,7 @@ QPixmap renderWorldIcon(const QByteArray& svg, const QString& inkHex)
|
|||||||
QStringLiteral("<rect\\s+width=\"100\"\\s+height=\"100\"[^>]*>"));
|
QStringLiteral("<rect\\s+width=\"100\"\\s+height=\"100\"[^>]*>"));
|
||||||
s.remove(backgroundRect);
|
s.remove(backgroundRect);
|
||||||
s.replace(QStringLiteral("#ffffff"), inkHex);
|
s.replace(QStringLiteral("#ffffff"), inkHex);
|
||||||
|
return s.toUtf8();
|
||||||
const int px = 192;
|
|
||||||
QPixmap pixmap(px, px);
|
|
||||||
pixmap.fill(Qt::transparent);
|
|
||||||
QSvgRenderer renderer(s.toUtf8());
|
|
||||||
QPainter painter(&pixmap);
|
|
||||||
renderer.render(&painter);
|
|
||||||
return pixmap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perceived luminance test; picks a dark glyph on light fills so it stays legible.
|
// Perceived luminance test; picks a dark glyph on light fills so it stays legible.
|
||||||
@@ -1279,29 +1277,36 @@ void GameWorldView::loadBuildingIcons(const std::string& configDir)
|
|||||||
continue; // A missing icon is not an error; the text glyph is used.
|
continue; // A missing icon is not an error; the text glyph is used.
|
||||||
}
|
}
|
||||||
const QByteArray svg = file.readAll();
|
const QByteArray svg = file.readAll();
|
||||||
BuildingIconPixmaps pixmaps;
|
BuildingIconRenderers renderers;
|
||||||
pixmaps.white = renderWorldIcon(svg, QStringLiteral("#ffffff"));
|
renderers.white = std::make_unique<QSvgRenderer>(
|
||||||
pixmaps.dark = renderWorldIcon(svg, QStringLiteral("#1c1c20"));
|
worldIconSvg(svg, QStringLiteral("#ffffff")));
|
||||||
m_buildingIcons[entry.type] = pixmaps;
|
renderers.dark = std::make_unique<QSvgRenderer>(
|
||||||
|
worldIconSvg(svg, QStringLiteral("#1c1c20")));
|
||||||
|
m_buildingIcons[entry.type] = std::move(renderers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool GameWorldView::drawBuildingIcon(QPainter& painter, BuildingType type,
|
bool GameWorldView::drawBuildingIcon(QPainter& painter, BuildingType type,
|
||||||
const QRectF& box, const QColor& fill) const
|
const QRectF& box, const QColor& fill) const
|
||||||
{
|
{
|
||||||
const std::map<BuildingType, BuildingIconPixmaps>::const_iterator it =
|
const std::map<BuildingType, BuildingIconRenderers>::const_iterator it =
|
||||||
m_buildingIcons.find(type);
|
m_buildingIcons.find(type);
|
||||||
if (it == m_buildingIcons.end()) { return false; }
|
if (it == m_buildingIcons.end()) { return false; }
|
||||||
|
|
||||||
// Centered square keeps the glyph undistorted on non-square footprints; the
|
// Every icon is the same fixed on-screen size, centered on the footprint,
|
||||||
// icon's own 13% margin supplies padding.
|
// regardless of footprint size (REQ-UI-WORLD-ICON).
|
||||||
const qreal side = std::min(box.width(), box.height());
|
const qreal side = static_cast<qreal>(getTilePx()) * kWorldIconTileFactor;
|
||||||
const QRectF target(box.center().x() - side / 2.0,
|
const QRectF target(box.center().x() - side / 2.0,
|
||||||
box.center().y() - side / 2.0, side, side);
|
box.center().y() - side / 2.0, side, side);
|
||||||
const QPixmap& pixmap = isLightFill(fill) ? it->second.dark : it->second.white;
|
QSvgRenderer* renderer = isLightFill(fill) ? it->second.dark.get()
|
||||||
|
: it->second.white.get();
|
||||||
|
|
||||||
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
|
// Render the glyph as vector at the view scale so it stays crisp (a
|
||||||
painter.drawPixmap(target, pixmap, pixmap.rect());
|
// pre-rasterized pixmap downscaled to tile size looked blurry).
|
||||||
|
const bool wasAntialiasing = painter.testRenderHint(QPainter::Antialiasing);
|
||||||
|
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||||
|
renderer->render(&painter, target);
|
||||||
|
painter.setRenderHint(QPainter::Antialiasing, wasAntialiasing);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1426,19 +1431,20 @@ void GameWorldView::drawBuildings(QPainter& painter)
|
|||||||
const QString pctText = QString::number(pct) + "%";
|
const QString pctText = QString::number(pct) + "%";
|
||||||
|
|
||||||
painter.setPen(bv.outline);
|
painter.setPen(bv.outline);
|
||||||
const QRectF topHalf(bboxRect.x(), bboxRect.y(),
|
// Identity symbol with the progress percentage below it
|
||||||
bboxRect.width(), bboxRect.height() * 0.5);
|
// (REQ-UI-CONSTRUCTION-PROGRESS): the world icon centered on the
|
||||||
const QRectF botHalf(bboxRect.x(),
|
// footprint where the type has one, otherwise the text glyph.
|
||||||
bboxRect.y() + bboxRect.height() * 0.5,
|
if (drawBuildingIcon(painter, s.type, bboxRect, bv.fill))
|
||||||
bboxRect.width(), bboxRect.height() * 0.5);
|
|
||||||
// Identity symbol above, progress percentage below (REQ-UI-CONSTRUCTION-PROGRESS):
|
|
||||||
// the world icon where the type has one, otherwise the text glyph.
|
|
||||||
if (drawBuildingIcon(painter, s.type, topHalf, bv.fill))
|
|
||||||
{
|
{
|
||||||
painter.drawText(botHalf, Qt::AlignCenter, pctText);
|
painter.drawText(bboxRect, Qt::AlignHCenter | Qt::AlignBottom, pctText);
|
||||||
}
|
}
|
||||||
else if (!bv.glyph.isEmpty())
|
else if (!bv.glyph.isEmpty())
|
||||||
{
|
{
|
||||||
|
const QRectF topHalf(bboxRect.x(), bboxRect.y(),
|
||||||
|
bboxRect.width(), bboxRect.height() * 0.5);
|
||||||
|
const QRectF botHalf(bboxRect.x(),
|
||||||
|
bboxRect.y() + bboxRect.height() * 0.5,
|
||||||
|
bboxRect.width(), bboxRect.height() * 0.5);
|
||||||
painter.drawText(topHalf, Qt::AlignCenter, bv.glyph);
|
painter.drawText(topHalf, Qt::AlignCenter, bv.glyph);
|
||||||
painter.drawText(botHalf, Qt::AlignCenter, pctText);
|
painter.drawText(botHalf, Qt::AlignCenter, pctText);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <random>
|
#include <random>
|
||||||
#include <set>
|
#include <set>
|
||||||
@@ -11,7 +12,6 @@
|
|||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QElapsedTimer>
|
#include <QElapsedTimer>
|
||||||
#include <QOpenGLWidget>
|
#include <QOpenGLWidget>
|
||||||
#include <QPixmap>
|
|
||||||
#include <QPoint>
|
#include <QPoint>
|
||||||
#include <QRectF>
|
#include <QRectF>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
@@ -54,6 +54,7 @@ struct ParsedReplay;
|
|||||||
class ReplayPlayer;
|
class ReplayPlayer;
|
||||||
class Simulation;
|
class Simulation;
|
||||||
class QPainter;
|
class QPainter;
|
||||||
|
class QSvgRenderer;
|
||||||
|
|
||||||
struct QPointCompare
|
struct QPointCompare
|
||||||
{
|
{
|
||||||
@@ -284,11 +285,16 @@ private:
|
|||||||
const GameConfig* m_config;
|
const GameConfig* m_config;
|
||||||
const VisualsConfig* m_visuals;
|
const VisualsConfig* m_visuals;
|
||||||
|
|
||||||
// Pre-rendered world icon glyphs per building type (REQ-UI-WORLD-ICON), in a
|
// World icon glyph renderers per building type (REQ-UI-WORLD-ICON), in a
|
||||||
// white and a dark variant so drawBuildingIcon can auto-contrast against the
|
// white and a dark variant so drawBuildingIcon can auto-contrast against the
|
||||||
// building's fill. Populated once by loadBuildingIcons().
|
// building's fill. Rendered as vector at the view scale each draw so they
|
||||||
struct BuildingIconPixmaps { QPixmap white; QPixmap dark; };
|
// stay crisp. Populated once by loadBuildingIcons().
|
||||||
std::map<BuildingType, BuildingIconPixmaps> m_buildingIcons;
|
struct BuildingIconRenderers
|
||||||
|
{
|
||||||
|
std::unique_ptr<QSvgRenderer> white;
|
||||||
|
std::unique_ptr<QSvgRenderer> dark;
|
||||||
|
};
|
||||||
|
std::map<BuildingType, BuildingIconRenderers> m_buildingIcons;
|
||||||
|
|
||||||
// Funnels all player input into the single Simulation::apply chokepoint.
|
// Funnels all player input into the single Simulation::apply chokepoint.
|
||||||
CommandManager m_commandManager;
|
CommandManager m_commandManager;
|
||||||
|
|||||||
Reference in New Issue
Block a user