diff --git a/client/global_client.html b/client/global_client.html
index 41d62a4..16fdf59 100644
--- a/client/global_client.html
+++ b/client/global_client.html
@@ -221,8 +221,8 @@
}
const playerMode=url.searchParams.get("player")==="1"||
addressPath()==="/player";
-const ZoomableFlag=1,UiFlag=2,MapLayerType=0,FullScreenLayerType=9,MaxUiZoom=3;
-const MaxUiZoomFitPadding=1.5;
+const ZoomableFlag=1,UiFlag=2,MapLayerType=0,FullScreenLayerType=9;
+const UiZoomSteps=[0.5,1,2,3,4],UiZoomFitPadding=1.5;
const ButtonUp=1,ButtonDown=2,ButtonLeft=4,ButtonRight=8;
const ButtonSelect=16,ButtonA=32,ButtonB=64;
const DebugUpdateFlashMs=900;
@@ -766,13 +766,16 @@
function setViewport(layerId,width,height){
const layer=ensureLayer(layerId);
+ const sizeChanged=layer.width!==width||layer.height!==height;
+ const wasMap=isMapLayer(layer);
layer.width=width;
layer.height=height;
layer.canvas.width=width;
layer.canvas.height=height;
layer.mips=[];
layer.image=layer.ctx.createImageData(width,height);
- maybeFit();
+ if(wasMap&&sizeChanged)fit();
+ else maybeFit();
}
function putSpritePixel(layer,x,y,sprite,srcOffset){
@@ -807,19 +810,18 @@
}
function uiZoom(){
- let zoom=MaxUiZoom;
+ // Largest stepped zoom at which every UI layer still fits the window
+ // with padding: small windows shrink UI pixels, large windows grow them.
+ let zoom=UiZoomSteps[UiZoomSteps.length-1];
for(const layer of layers.values()){
- if((layer.flags&UiFlag)===0)continue;
+ if(!isUiLayer(layer)||isMapLayer(layer)||isFullScreenLayer(layer))continue;
const fit=Math.min(
- innerWidth/layer.width,
- innerHeight/layer.height
+ innerWidth/Math.max(1,layer.width),
+ innerHeight/Math.max(1,layer.height)
);
- let layerZoom=1;
- for(let scale=MaxUiZoom;scale>=2;scale--){
- if(fit>=scale*MaxUiZoomFitPadding){
- layerZoom=scale;
- break;
- }
+ let layerZoom=UiZoomSteps[0];
+ for(const step of UiZoomSteps){
+ if(fit>=step*UiZoomFitPadding)layerZoom=step;
}
zoom=Math.min(zoom,layerZoom);
}
diff --git a/client/global_client.nim b/client/global_client.nim
index 17647b1..9119a8a 100644
--- a/client/global_client.nim
+++ b/client/global_client.nim
@@ -79,7 +79,10 @@ const
UiFlag = SpriteLayerUiFlag
MapLayerKind = SpriteLayerMap
FullScreenLayerKind = SpriteLayerFullScreen
- UiZoom = 3.0'f
+ UiZoomSteps = [0.5'f, 1.0'f, 2.0'f, 3.0'f, 4.0'f]
+ ## Crisp stepped UI zooms, smallest to largest, like Polyworld's HUD.
+ UiZoomFitPadding = 1.5'f
+ ## A UI layer may take at most 1/1.5 of the window at its zoom.
when not defined(emscripten):
const NetworkPollPasses = 8
when defined(emscripten):
@@ -315,6 +318,22 @@ proc maybeFit*(app: GlobalApp) =
if app.autoFit:
app.fit()
+proc applyLayerViewport(app: GlobalApp, layerId, width, height: int) =
+ ## Updates one layer size and refits when the map world size changes.
+ var layer = app.layerIndex(layerId)
+ let
+ sizeChanged = layer.width != width or layer.height != height
+ wasMap = layer.isMapLayer
+ layer.width = width
+ layer.height = height
+ layer.image = nil
+ layer.dirty = true
+ app.layers[layerId] = layer
+ if wasMap and sizeChanged:
+ app.fit()
+ else:
+ app.maybeFit()
+
proc zoomMapAt(app: GlobalApp, mouseLogical: IVec2, scrollY: float32) =
## Zooms the map at one screen coordinate, even when UI overlays are under it.
let layer = app.mapLayer()
@@ -345,6 +364,26 @@ proc refreshDisplayScale(app: GlobalApp) =
app.window.size = logicalSize.scaledWindowSize(scale)
app.maybeFit()
+proc uiZoom(app: GlobalApp, logicalW, logicalH: float32): float32 =
+ ## Returns the largest stepped UI zoom at which every UI layer still
+ ## fits inside the window with padding to spare. Small windows shrink
+ ## the UI pixels and large windows grow them, matching the HTML client.
+ result = UiZoomSteps[^1]
+ for layer in app.layers.values:
+ if not layer.isUiLayer or layer.isMapLayer or layer.isFullScreenLayer:
+ continue
+ if layer.width <= 0 or layer.height <= 0:
+ continue
+ let fit = min(
+ logicalW / layer.width.float32,
+ logicalH / layer.height.float32
+ )
+ var layerZoom = UiZoomSteps[0]
+ for step in UiZoomSteps:
+ if fit >= step * UiZoomFitPadding:
+ layerZoom = step
+ result = min(result, layerZoom)
+
proc layerScreenRect(
app: GlobalApp,
layer: GlobalLayer,
@@ -378,8 +417,9 @@ proc layerScreenRect(
)
let
- w = layer.width.float32 * UiZoom
- h = layer.height.float32 * UiZoom
+ zoom = app.uiZoom(logicalW, logicalH)
+ w = layer.width.float32 * zoom
+ h = layer.height.float32 * zoom
case layer.kind
of 1:
(x: 0.0'f, y: 0.0'f, w: w, h: h)
@@ -443,7 +483,9 @@ proc allocateLayerImage(app: GlobalApp, layer: var GlobalLayer) =
if layer.textureId == 0:
glGenTextures(1, layer.textureId.addr)
glBindTexture(GL_TEXTURE_2D, layer.textureId)
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST.GLint)
+ # Nearest when magnified keeps pixels crisp; linear when minified so a
+ # zoomed-out map or 0.5x UI averages pixels instead of dropping rows.
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR.GLint)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST.GLint)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE.GLint)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE.GLint)
@@ -727,13 +769,7 @@ proc parseMessage*(app: GlobalApp, data: string) =
if width <= 0 or height <= 0:
app.closeNetwork()
return
- var layer = app.layerIndex(layerId)
- layer.width = width
- layer.height = height
- layer.image = nil
- layer.dirty = true
- app.layers[layerId] = layer
- app.maybeFit()
+ app.applyLayerViewport(layerId, width, height)
of 0x06:
require(3)
let
diff --git a/tests/test_client.nim b/tests/test_client.nim
index b360cab..e388af5 100644
--- a/tests/test_client.nim
+++ b/tests/test_client.nim
@@ -61,6 +61,15 @@ proc testGlobalClientWheelZoomTargetsMap() =
doAssert "zoomMapAt(event.clientX,event.clientY,event.deltaY);" in html
doAssert "addEventListener(\"wheel\",event=>{\n event.preventDefault();\n const point=mousePoint(event);" notin html
+proc testGlobalClientRefitsOnMapViewportChange() =
+ ## Tests that a map viewport size change refits even after pan or zoom.
+ echo "Testing global client refits on map viewport change"
+ let html = readClientHtml(CoworldGlobalClientRoute)
+ doAssert "const sizeChanged=layer.width!==width||layer.height!==height;" in html
+ doAssert "const wasMap=isMapLayer(layer);" in html
+ doAssert "if(wasMap&&sizeChanged)fit();" in html
+ doAssert "else maybeFit();" in html
+
proc testPlayerClientSpeaksSpriteProtocol() =
## Tests the shared player client covers the sprite protocol used by bots.
echo "Testing player client sprite protocol support"
@@ -98,6 +107,7 @@ testClientStaticPaths()
testReplayClientPreservesUri()
testGlobalClientFullScreenLayers()
testGlobalClientWheelZoomTargetsMap()
+testGlobalClientRefitsOnMapViewportChange()
testPlayerClientSpeaksSpriteProtocol()
testEmbeddedClientBodies()
echo "All tests passed"