Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 15 additions & 13 deletions client/global_client.html
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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){
Expand Down Expand Up @@ -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);
}
Expand Down
58 changes: 47 additions & 11 deletions client/global_client.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions tests/test_client.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -98,6 +107,7 @@ testClientStaticPaths()
testReplayClientPreservesUri()
testGlobalClientFullScreenLayers()
testGlobalClientWheelZoomTargetsMap()
testGlobalClientRefitsOnMapViewportChange()
testPlayerClientSpeaksSpriteProtocol()
testEmbeddedClientBodies()
echo "All tests passed"