Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions docs/frontend-refresh/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

The opening menus should feel like an invitation into Glob2's living world. This
proposal keeps the game's colorful colony art, gives it a real AI-driven life
behind the menus, and replaces the old textured widget frames with a restrained
sage-and-gold presentation. The main action is prominent; secondary actions are
quieter. This is a visual refresh of the existing screens, not a replacement UI
behind the menus, and replaces the old textured widget frames with inked gel
controls on a warm, translucent membrane, drawn from one palette
(`FrontendPalette`) in the game's own hues. The main action is prominent;
secondary actions are quieter. See `docs/ui-visual-review/` for the review that
set this direction and the before/after captures. This is a visual refresh of the existing screens, not a replacement UI
or rendering engine.

## Design and coverage
Expand All @@ -30,6 +32,10 @@ panel calculation. There is no new layout system, shader, rendering backend,
worker thread, or network session.

`MenuColony` owns a separate `Game`, camera state, fixed-step accumulator and RNG.
On OpenGL its camera drifts about a tile and a half on a slow Lissajous, and the
theme composites one cloud shadow+layer pass over the whole window after every
widget has painted (`Style::afterPaint`), in place of the pass `drawMap` would
make under the panel (`DRAW_NO_CLOUDS`). Both are presentation state only.
It polls Econo, executes its order and advances the normal simulation
at 25 ticks/second. Each update runs at most two steps, dropping excessive lag.
Loading, stepping and drawing temporarily exchange the legacy global RNG and
Expand Down
135 changes: 135 additions & 0 deletions docs/ui-visual-review/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Front-end visual revamp

This directory holds the review that motivated the revamp and the captures that
show its result. The review's measurements stand as written; the "spike" it
described has been replaced by the changes summarised below.

Captures come from the real client renderer via `MenuColonyHarness capture`,
at 1152x720. `before-*.png` is the front end as it shipped after #202, #236 and
#237; `after-*.png` is this branch on the software backend; `after-gl-*.png` is
this branch on OpenGL, where cloud shadows, the camera drift and sub-tile
motion exist.

## What was wrong

The refreshed front end read as sterile because every channel that could carry
character was off at once:

- **Colour.** The chrome ran 11–38% saturation over a world at 56–75%, in a
gray-cream nothing in the game uses, with a cream veil desaturating the
colony so the pale interface could sit on it.
- **Line.** Every game sprite and every concept drawing in
`datasrc/gfx/concept-art/` is a heavy, slightly wobbly ink contour. The
interface separated surfaces by 4%-lightness steps and a 1 px hairline.
- **Life.** The "live colony" had a fixed camera, its cloud shadows never
reached the panel, and the panel was 97% opaque: a wallpaper.
- **Structure.** Nine near-cream fills across five drifted palette copies, and
three parallel drawing layers (theme hooks, `LobbyControls`,
`SettingsScreenLayout`) — a full rewrite of the theme left the Settings
panel pixel-identical.

## What changed

Six commits, each verified with the harness gates and captures.

1. **One palette.** `FrontendPalette` in `src/FrontendTheme.h`: ink, muted,
gold, violet (the water's hue, for keyboard focus), membrane, gel, scrim.
Every front-end literal routes through it.
2. **Inked gel on a membrane.** `FrontendTheme::blob()` draws a rounded
contour with per-corner radii and a gentle, rect-seeded wobble as a 2 px ink
ring around a fill with a gel highlight; `ring()` is the outline alone, for
frames drawn over list contents. Theme hooks, the main menu, the lobby's pad
helpers and the settings screen all draw through it. Panels are a warm,
translucent membrane the colony tints through (opaque under low-speed
graphics). Widget geometry and hit rects are unchanged.
3. **The world acts on the UI.** A `Style::afterPaint` hook runs after every
widget has painted; the theme uses it for one cloud shadow+layer pass over
the whole window, anchored to the colony's viewport, replacing the pass
`drawMap` used to make under the panel (`DRAW_NO_CLOUDS`). The colony camera
drifts about a tile and a half on a slow Lissajous. Both OpenGL-only by the
engine's existing guards; both presentation-only, outside the simulation.
4. **Jelly.** Buttons swell up to 2 px under the cursor and squash 1 px while
pressed; the front page fades in over its first ten frames.
5. **A glob on the front page.** The colony's worker sprite, in its team
colour, walks the band under the utility buttons. The classic sheet's faint
matte, invisible on grass, is thresholded off once at composite time.

## Backend notes

- Software: everything except cloud shadows over the UI, camera drift and the
entry fade's sub-tile motion. The membrane, ink, jelly and glob all render.
- OpenGL: all of it. Cloud strength over the sheet follows the same
`cloudMaxAlpha` setting as the world; if a maintainer finds it too strong on
the large Settings sheet, that is one constant in `FrontendTheme::afterPaint`.
- Modal dialogs composite over a frozen snapshot of the parent and refill
opaquely each frame (`OverlayScreen::executeModal`), so they stay opaque by
design.

## Follow-up: smoother motion and cleaner edges

Playtesting the built client surfaced three concrete rendering bugs behind an
overall "feels a bit 90s" reaction, none of them requiring a style change to fix:

- **Button swell had only 2-3 states.** The hover inflate was computed as
`int(hi)*2/255` — integer division on a smooth 0-255 highlight collapsed it
to whole-pixel jumps instead of a continuous grow. `FrontendTheme::swell()`
now draws the base size opaque and crossfades a one-pixel-larger outline in
over the fractional part, so the swell reads as continuous. Used by the main
menu buttons and the shared theme's button background.
- **Menu-colony camera drift was jarring.** The drift was floored to a whole
pixel before being split into tile + sub-tile fraction, so the camera sat
still for seconds near the drift's turning points and then hopped a pixel.
`MenuColony::draw()` now keeps the drift a `double` all the way to the GL
transform, matching how the in-game camera's own fraction is already a
float (`TorusView::draw`) for the same reason.
- **Contour edges read as pixel art, with notches at the corners.** `blob()`
and `ring()` rounded every corner and wobble offset to a whole pixel with no
anti-aliasing, and the wobble was applied inside the rounded-corner curves
themselves, chipping them. The contour now carries fractional insets
through to the boundary pixel (blended by coverage) and fades the wobble to
zero inside the corner curves.

None of this changes layout, hit rects or the simulation; re-verified with the
same harness gates listed below, plus a fresh `run-game-speed-tests.py` pass
(checksum `8b9c6da6`, unchanged from the original captures).

## Follow-up: green over cream, and less see-through

Further playtesting pushed back on the cream/sand chrome itself ("still feels
a bit 90s") and, separately, on the front page reading as too transparent:

- `FrontendPalette::membrane`/`gel`/`gelDisabled` are now a green tint instead
of sand, at HSL hue ~120 — sampled off the actual rendered terrain (not
picked by eye) and rotated to the cream tokens' existing saturation/lightness
before being taken up to a more saturated tier (`membrane` settled at
`(168,230,168)`, one step back from an over-saturated `(103,228,103)` that
read as too much). `ink`, `gold` and `violet` are unchanged.
- The front page's membrane alpha, deliberately lowered to 140 earlier in this
branch so the colony would read through, went too far the other way — it's
now 210 (out of 255), and the shared `panelAlpha()` default used by Settings
and custom-game moved from 214 to 235.

Re-verified the same way as the smoothing follow-up above; checksum unchanged.

## Verification

Run from the repository root, `SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy`
unless noted:

```sh
scons -j8 release=0 server=0 build/src/glob2 menu-colony-harness custom-setup-test speed-tests
build/src/MenuColonyHarness check data/menu/colony.bin # presentation, text bounds, checksums, routes
build/src/MenuColonyHarness navigation unused # enter/cancel 14 real screen loops
build/src/MenuColonyHarness sessions unused # play/quit, replay, results, editor
build/src/CustomGameSetupHarness # lobby model
xvfb-run -a build/src/CustomGameSetupHarness artifacts/ci # lobby visual (needs an X server)
xvfb-run -a python3 test/run-game-speed-tests.py # sim checksums unchanged (needs a window)
```

All pass on this branch. `MenuColonyHarness` previews now invoke the style's
`afterPaint`, mirroring `Screen::dispatchPaint`, so captures show what the game
shows; a 120-frame OpenGL `record` measured the world shifting (−9, −4) px over
80 frames and the panel band's luminance moving 198→204 as a shadow crossed it.

Per `AGENTS.md`, a maintainer playing this in motion is part of review. The
captures are evidence, not a substitute.
Binary file added docs/ui-visual-review/after-campaign.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/ui-visual-review/after-credits.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/ui-visual-review/after-custom-players.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/ui-visual-review/after-custom-rules.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/ui-visual-review/after-custom.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/ui-visual-review/after-gl-main.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/ui-visual-review/after-gl-settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/ui-visual-review/after-load.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/ui-visual-review/after-main.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/ui-visual-review/after-results.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/ui-visual-review/after-settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/ui-visual-review/before-custom.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/ui-visual-review/before-main.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/ui-visual-review/before-results.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/ui-visual-review/before-settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions libgag/include/GUIStyle.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ namespace GAGGUI
virtual ~Style() { }
// Optional presentation tick; default styles have no background work.
virtual void onFrame() { }
// Optional pass over the finished frame, after every widget has painted.
virtual void afterPaint(GAGCore::DrawableSurface*) { }
virtual void drawButtonSelection(GAGCore::DrawableSurface*, int, int, int, int) { }
virtual bool usesThemeTextColor() const { return false; }
virtual void drawFieldBackground(GAGCore::DrawableSurface*, int, int, int, int) { }
Expand Down
1 change: 1 addition & 0 deletions libgag/src/GUIBase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,7 @@ namespace GAGGUI
if ((*it)->visible)
(*it)->paint();
}
Style::style->afterPaint(gfx);
/* We need a second loop in order to have tooltip over everything else */
for (std::set<Widget *>::iterator it=widgets.begin(); it!=widgets.end(); ++it)
{
Expand Down
34 changes: 28 additions & 6 deletions src/CustomGameScreen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ CustomGameChoiceScreen::CustomGameChoiceScreen(const std::string &title,
auto &ui = *controls;
int w = std::min(gfx->getW() - 32, 1000), x = (gfx->getW() - w) / 2, height = gfx->getH();
ui.setDimensions(gfx->getW(), height);
ui.box({x - 8, 8, w + 16, height - 16}, Color(232, 237, 218), 8);
FrontendTheme::rounded(ui.surface(), x - 5, 13, w + 16, height - 16, 12, Color(12, 28, 16, 70));
FrontendTheme::blob(ui.surface(), x - 8, 8, w + 16, height - 16, 12,
Color(FrontendPalette::membrane.r, FrontendPalette::membrane.g, FrontendPalette::membrane.b, FrontendTheme::panelAlpha()), ui.ink, 2);
ui.text(x + 8, 20, this->title, "standard", w - 16);
int left = w < 800 ? 174 : 235, right = x + left + 22, rightW = w - left - 22;
ui.beginRegion(20, {x, 60, left, height - 135});
Expand Down Expand Up @@ -558,12 +560,32 @@ void CustomGameScreen::showAIProfile(int colony)
setup.colonies[colony].ai = (AI::ImplementationID)AINames::selectionOrder()[result];
}

// Both lobby screens draw their own page membrane sized to the window, so the
// theme paints only the world behind them (the widget-union panel would be a
// second, smaller sheet underneath).
void CustomGameChoiceScreen::paint()
{
if (FrontendTheme::current && GAGGUI::Style::style == FrontendTheme::current)
FrontendTheme::current->background(gfx, false);
else
Glob2Screen::paint();
}
void CustomGameScreen::paint()
{
if (FrontendTheme::current && GAGGUI::Style::style == FrontendTheme::current)
FrontendTheme::current->background(gfx, false);
else
Glob2TabScreen::paint();
}

void CustomGameScreen::renderLobby()
{
auto &ui = *controls;
int width = gfx->getW(), height = gfx->getH();
int w = std::min(width - 32, 1120), x = (width - w) / 2;
ui.box({x - 8, 8, w + 16, height - 16}, Color(232, 237, 218), 8);
FrontendTheme::rounded(ui.surface(), x - 5, 13, w + 16, height - 16, 12, Color(12, 28, 16, 70));
FrontendTheme::blob(ui.surface(), x - 8, 8, w + 16, height - 16, 12,
Color(FrontendPalette::membrane.r, FrontendPalette::membrane.g, FrontendPalette::membrane.b, FrontendTheme::panelAlpha()), ui.ink, 2);
auto speed = globalContainer->settings;
speed.gameSpeed = setup.speed;
std::vector<std::string> titles = localized({"Map", "Players & Teams", "Game Rules"});
Expand Down Expand Up @@ -595,7 +617,7 @@ void CustomGameScreen::renderLobby()
else
renderMap(x, bodyY, w, bodyH);
gfx->setClipRect();
gfx->drawLine(x, height - 85, x + w, height - 85, ui.line);
gfx->drawFilledRect(x, height - 85, w, 1, Color(ui.ink.r, ui.ink.g, ui.ink.b, 70));
std::string error = setup.validation();
if (!setup.random && !validMap)
error = tr("Select a valid map.");
Expand Down Expand Up @@ -641,7 +663,7 @@ void CustomGameScreen::renderPlayers(int x, int y, int w, int h)
int ry = top + i * rowH - offset;
ui.box({x, ry, w - 12, rowH - 8}, ui.panel);
Color color =
i < int(preview->starts.size()) ? preview->starts[i].color : Color(160, 172, 149);
i < int(preview->starts.size()) ? preview->starts[i].color : FrontendPalette::gelDisabled;
ui.box({x + 10, ry + 10, 30, 30}, color, 4);
gfx->drawRect(x + 10, ry + 10, 30, 30, ui.ink);
auto font = Toolkit::getFont("standard");
Expand Down Expand Up @@ -1031,7 +1053,7 @@ void CustomGameScreen::renderMap(int x, int y, int w, int h)
rightW, true);
int size = std::min(rightW, h - 126);
int px = rightX + (rightW - size) / 2, py = top + 51;
ui.box({px - 3, py - 3, size + 6, size + 6}, ui.line);
FrontendTheme::ring(gfx, px - 3, py - 3, size + 6, size + 6, 4, ui.ink, 0);
if (validMap)
{
preview->setScreenPosition(px - (gfx->getW() - 640) / 2, py - (gfx->getH() - 480) / 2);
Expand All @@ -1040,7 +1062,7 @@ void CustomGameScreen::renderMap(int x, int y, int w, int h)
}
else
{
ui.box({px, py, size, size}, Color(211, 223, 197));
ui.box({px, py, size, size}, FrontendPalette::gelDisabled);
ui.paragraph(px + 16, py + size / 2 - 20, size - 32,
tr(previewPending
? "Updating map preview..."
Expand Down
2 changes: 2 additions & 0 deletions src/CustomGameScreen.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class CustomGameChoiceScreen : public Glob2Screen
const std::vector<bool> &);
void onSDLEvent(SDL_Event *) override;
void onAction(Widget *, Action, int, int) override;
void paint() override;
};
class LobbyMapPreview;
class LobbyControls;
Expand All @@ -38,6 +39,7 @@ class CustomGameScreen : public Glob2TabScreen
void onGroupActivated(int) override;
void onSDLEvent(SDL_Event *) override;
void onTimer(Uint32 tick) override;
void paint() override;
void updateLayout() override;
MapHeader &getMapHeader() { return mapHeader; }
GameHeader &getGameHeader();
Expand Down
Loading
Loading