Fix WorldTextPass labels scaling with device-pixel-ratio

Attack troop labels, the ghost-cost chip, and the bonusPopup
minScreenScale floor were dividing by `zoom`, which Camera.ts stores
as device-pixels-per-world-unit (canvasW = cssWidth * dpr). The result
was constant device-pixel size — labels rendered ~2x larger on DPR=1
displays than on retina. Multiply each screen-relative scale by dpr so
the on-screen size stays constant in CSS pixels.

Retune the now-correct sizes: attack labels 34 -> 17, ghost-cost
screenScale 30 -> 18, screenYOffset 50 -> 25.
This commit is contained in:
evanpelle
2026-06-03 21:40:00 -07:00
parent 96c032850d
commit 986f0b61bf
2 changed files with 16 additions and 7 deletions
+14 -5
View File
@@ -44,7 +44,7 @@ const GHOST_COST_OUTLINE_WIDTH = 1.4;
* zoom each frame so the on-screen label size stays constant regardless of
* how far the camera is zoomed.
*/
const ATTACK_LABEL_SCREEN_SCALE = 34.0;
const ATTACK_LABEL_SCREEN_SCALE = 17.0;
const ATTACK_LABEL_OUTLINE_WIDTH = 1.2;
// ---------------------------------------------------------------------------
@@ -397,6 +397,9 @@ export class WorldTextPass {
private rebuildInstances(now: number, zoom: number): void {
let count = 0;
// canvasW in Camera is cssWidth*dpr, so `zoom` is device-px-per-world-unit.
// Multiply screen-relative scales by dpr to keep a constant CSS-pixel size.
const dpr = window.devicePixelRatio || 1;
for (const popup of this.active) {
const elapsed = now - popup.startMs;
@@ -442,7 +445,8 @@ export class WorldTextPass {
// Attack troop labels — persistent, no fade. Controller interpolates
// positions before pushing. Scale is divided by zoom so the label keeps
// a constant on-screen size regardless of how zoomed-in the camera is.
const attackScale = ATTACK_LABEL_SCREEN_SCALE / Math.max(zoom, 0.0001);
const attackScale =
(ATTACK_LABEL_SCREEN_SCALE * dpr) / Math.max(zoom, 0.0001);
for (const label of this.attackTroopLabels) {
layoutString(
label.text,
@@ -478,8 +482,9 @@ export class WorldTextPass {
const label = this.ghostCostLabel;
if (label) {
const invZoom = 1 / Math.max(zoom, 0.0001);
const ghostScale = this.settings.ghostCost.screenScale * invZoom;
const ghostY = label.y + this.settings.ghostCost.screenYOffset * invZoom;
const ghostScale = this.settings.ghostCost.screenScale * dpr * invZoom;
const ghostY =
label.y + this.settings.ghostCost.screenYOffset * dpr * invZoom;
layoutString(
label.text,
this.glyph,
@@ -536,7 +541,11 @@ export class WorldTextPass {
gl.useProgram(this.program);
gl.uniformMatrix3fv(this.uCamera, false, cameraMatrix);
gl.uniform1f(this.uZoom, zoom);
gl.uniform1f(this.uMinScreenScale, this.settings.bonusPopup.minScreenScale);
const dpr = window.devicePixelRatio || 1;
gl.uniform1f(
this.uMinScreenScale,
this.settings.bonusPopup.minScreenScale * dpr,
);
gl.uniform1f(this.uDistRange, this.distanceRange);
gl.activeTexture(gl.TEXTURE0);
+2 -2
View File
@@ -270,8 +270,8 @@
"cullZoom": 0.3
},
"ghostCost": {
"screenScale": 30,
"screenYOffset": 50
"screenScale": 18,
"screenYOffset": 25
},
"spawnOverlay": {
"highlightRadius": 9,