Animate HUD troop/population bars with transform instead of width (#4319)

## Problem

The troop and population ratio bars in `ControlPanel` and
`PlayerInfoOverlay` update their inline `width` on every game tick, with
a `transition-[width] duration-200` to smooth the change. But `width` is
a layout property — animating it forces the browser to **recalculate
layout for the surrounding HUD components every animation frame**. Since
the width changes every tick, this kept the whole HUD in a near-constant
relayout loop and showed up as jank.

## Fix

Keep the smooth animation, but drive it with `transform`
(GPU-composited, no layout) instead of `width`:

- Replace the two flex `width: %` segments with absolutely-positioned,
full-width bars.
- Segment 1: `transform: scaleX(green/100)` anchored to the left edge
(`origin-left`).
- Segment 2: `transform: translateX(green%) scaleX(orange/100)` so it
stays flush against the first segment.
- Animate with `transition-transform duration-200 ease-out`.

Because `transform` is composited rather than laid out, the bars animate
smoothly **without** triggering the per-frame HUD relayout.

The segments are now always mounted (`scaleX(0)` when empty) instead of
conditionally rendered, which also prevents the transition from
resetting as values cross zero.

Files:
- `src/client/hud/layers/ControlPanel.ts` (mobile + desktop troop bars:
malibu-blue / aquarius)
- `src/client/hud/layers/PlayerInfoOverlay.ts` (sky-700 / malibu-blue)

A grep confirmed these were the only `transition-[width]` usages in the
client.

## Testing

- `eslint --fix` / prettier ran clean via the pre-commit hook.
- CSS-only change; no sim/behavioral logic touched.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Evan
2026-06-17 08:56:50 -07:00
committed by GitHub
parent 83cd864018
commit 64409cae4d
2 changed files with 30 additions and 39 deletions
+20 -26
View File
@@ -347,19 +347,16 @@ export class ControlPanel extends LitElement implements Controller {
<div
class="w-full h-6 border border-gray-600 rounded-md bg-gray-900/60 overflow-hidden relative"
>
<div class="h-full flex">
${greenPercent > 0
? html`<div
class="h-full bg-malibu-blue transition-[width] duration-200"
style="width: ${greenPercent}%;"
></div>`
: ""}
${orangePercent > 0
? html`<div
class="h-full bg-aquarius transition-[width] duration-200"
style="width: ${orangePercent}%;"
></div>`
: ""}
<div class="relative h-full">
<div
class="absolute inset-y-0 left-0 w-full origin-left bg-malibu-blue transition-transform duration-200 ease-out"
style="transform: scaleX(${greenPercent / 100});"
></div>
<div
class="absolute inset-y-0 left-0 w-full origin-left bg-aquarius transition-transform duration-200 ease-out"
style="transform: translateX(${greenPercent}%) scaleX(${orangePercent /
100});"
></div>
</div>
<div
class="absolute inset-0 flex items-center justify-between px-1.5 text-xs font-bold leading-none pointer-events-none"
@@ -402,19 +399,16 @@ export class ControlPanel extends LitElement implements Controller {
<div
class="w-full h-6 border border-gray-600 rounded-md bg-gray-900/60 overflow-hidden relative"
>
<div class="h-full flex">
${greenPercent > 0
? html`<div
class="h-full bg-malibu-blue transition-[width] duration-200"
style="width: ${greenPercent}%;"
></div>`
: ""}
${orangePercent > 0
? html`<div
class="h-full bg-aquarius transition-[width] duration-200"
style="width: ${orangePercent}%;"
></div>`
: ""}
<div class="relative h-full">
<div
class="absolute inset-y-0 left-0 w-full origin-left bg-malibu-blue transition-transform duration-200 ease-out"
style="transform: scaleX(${greenPercent / 100});"
></div>
<div
class="absolute inset-y-0 left-0 w-full origin-left bg-aquarius transition-transform duration-200 ease-out"
style="transform: translateX(${greenPercent}%) scaleX(${orangePercent /
100});"
></div>
</div>
<div
class="absolute inset-0 flex items-center text-lg font-bold leading-none pointer-events-none"
+10 -13
View File
@@ -414,19 +414,16 @@ export class PlayerInfoOverlay extends LitElement implements Controller {
<div
class="w-full h-5 lg:h-6 border border-gray-600 rounded-md bg-gray-900/60 overflow-hidden relative"
>
<div class="h-full flex">
${greenPercent > 0
? html`<div
class="h-full bg-sky-700 transition-[width] duration-200"
style="width: ${greenPercent}%;"
></div>`
: ""}
${orangePercent > 0
? html`<div
class="h-full bg-malibu-blue transition-[width] duration-200"
style="width: ${orangePercent}%;"
></div>`
: ""}
<div class="relative h-full">
<div
class="absolute inset-y-0 left-0 w-full origin-left bg-sky-700 transition-transform duration-200 ease-out"
style="transform: scaleX(${greenPercent / 100});"
></div>
<div
class="absolute inset-y-0 left-0 w-full origin-left bg-malibu-blue transition-transform duration-200 ease-out"
style="transform: translateX(${greenPercent}%) scaleX(${orangePercent /
100});"
></div>
</div>
<div
class="absolute inset-0 flex items-center justify-between px-1.5 text-sm font-bold leading-none pointer-events-none"