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"