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 * zoom each frame so the on-screen label size stays constant regardless of
* how far the camera is zoomed. * 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; const ATTACK_LABEL_OUTLINE_WIDTH = 1.2;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -397,6 +397,9 @@ export class WorldTextPass {
private rebuildInstances(now: number, zoom: number): void { private rebuildInstances(now: number, zoom: number): void {
let count = 0; 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) { for (const popup of this.active) {
const elapsed = now - popup.startMs; const elapsed = now - popup.startMs;
@@ -442,7 +445,8 @@ export class WorldTextPass {
// Attack troop labels — persistent, no fade. Controller interpolates // Attack troop labels — persistent, no fade. Controller interpolates
// positions before pushing. Scale is divided by zoom so the label keeps // 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. // 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) { for (const label of this.attackTroopLabels) {
layoutString( layoutString(
label.text, label.text,
@@ -478,8 +482,9 @@ export class WorldTextPass {
const label = this.ghostCostLabel; const label = this.ghostCostLabel;
if (label) { if (label) {
const invZoom = 1 / Math.max(zoom, 0.0001); const invZoom = 1 / Math.max(zoom, 0.0001);
const ghostScale = this.settings.ghostCost.screenScale * invZoom; const ghostScale = this.settings.ghostCost.screenScale * dpr * invZoom;
const ghostY = label.y + this.settings.ghostCost.screenYOffset * invZoom; const ghostY =
label.y + this.settings.ghostCost.screenYOffset * dpr * invZoom;
layoutString( layoutString(
label.text, label.text,
this.glyph, this.glyph,
@@ -536,7 +541,11 @@ export class WorldTextPass {
gl.useProgram(this.program); gl.useProgram(this.program);
gl.uniformMatrix3fv(this.uCamera, false, cameraMatrix); gl.uniformMatrix3fv(this.uCamera, false, cameraMatrix);
gl.uniform1f(this.uZoom, zoom); 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.uniform1f(this.uDistRange, this.distanceRange);
gl.activeTexture(gl.TEXTURE0); gl.activeTexture(gl.TEXTURE0);
+2 -2
View File
@@ -270,8 +270,8 @@
"cullZoom": 0.3 "cullZoom": 0.3
}, },
"ghostCost": { "ghostCost": {
"screenScale": 30, "screenScale": 18,
"screenYOffset": 50 "screenYOffset": 25
}, },
"spawnOverlay": { "spawnOverlay": {
"highlightRadius": 9, "highlightRadius": 9,