no-wrap option; pivoted exponential zoom

This commit is contained in:
Ilia Denisov
2026-03-07 11:35:18 +02:00
committed by GitHub
parent 1de621c743
commit 477e656008
22 changed files with 605 additions and 81 deletions
+117
View File
@@ -0,0 +1,117 @@
package world
// ClampCameraNoWrapViewport clamps camera center so that the VIEWPORT world-rect
// stays within the bounded world [0..worldW) x [0..worldH), when possible.
//
// This is the correct clamp for user panning when wrap is disabled.
// Margins (expanded canvas) are intentionally ignored here; they may extend outside the world.
func ClampCameraNoWrapViewport(
cameraXWorldFp, cameraYWorldFp int,
viewportW, viewportH int,
zoomFp int,
worldW, worldH int,
) (int, int) {
if zoomFp <= 0 {
panic("ClampCameraNoWrapViewport: invalid zoom")
}
if viewportW < 0 || viewportH < 0 {
panic("ClampCameraNoWrapViewport: negative viewport")
}
if worldW <= 0 || worldH <= 0 {
panic("ClampCameraNoWrapViewport: invalid world size")
}
spanW := PixelSpanToWorldFixed(viewportW, zoomFp)
spanH := PixelSpanToWorldFixed(viewportH, zoomFp)
halfW := spanW / 2
halfH := spanH / 2
cameraXWorldFp = clampCameraAxis(cameraXWorldFp, worldW, halfW)
cameraYWorldFp = clampCameraAxis(cameraYWorldFp, worldH, halfH)
return cameraXWorldFp, cameraYWorldFp
}
// ClampCameraNoWrapExpanded clamps camera center so that the EXPANDED CANVAS world-rect
// (viewport + margins) stays within the bounded world, when possible.
//
// This is stricter than viewport-based clamp and can prevent panning when margins are large.
func ClampCameraNoWrapExpanded(
cameraXWorldFp, cameraYWorldFp int,
viewportW, viewportH int,
marginX, marginY int,
zoomFp int,
worldW, worldH int,
) (int, int) {
if zoomFp <= 0 {
panic("ClampCameraNoWrapExpanded: invalid zoom")
}
if viewportW < 0 || viewportH < 0 || marginX < 0 || marginY < 0 {
panic("ClampCameraNoWrapExpanded: negative sizes")
}
if worldW <= 0 || worldH <= 0 {
panic("ClampCameraNoWrapExpanded: invalid world size")
}
canvasW := viewportW + 2*marginX
canvasH := viewportH + 2*marginY
spanW := PixelSpanToWorldFixed(canvasW, zoomFp)
spanH := PixelSpanToWorldFixed(canvasH, zoomFp)
halfW := spanW / 2
halfH := spanH / 2
cameraXWorldFp = clampCameraAxis(cameraXWorldFp, worldW, halfW)
cameraYWorldFp = clampCameraAxis(cameraYWorldFp, worldH, halfH)
return cameraXWorldFp, cameraYWorldFp
}
func clampCameraAxis(cam, worldSize, halfSpan int) int {
// If viewport/span does not fit: force center.
if 2*halfSpan > worldSize {
return worldSize / 2
}
minCam := halfSpan
maxCam := worldSize - halfSpan
if cam < minCam {
return minCam
}
if cam > maxCam {
return maxCam
}
return cam
}
// ClampRenderParamsNoWrap clamps camera center in-place when wrap is disabled.
// It uses viewport-based clamp (NOT expanded) so panning remains possible even with margins.
func (w *World) ClampRenderParamsNoWrap(p *RenderParams) {
if p == nil {
return
}
allowWrap := true
if p.Options != nil && p.Options.DisableWrapScroll {
allowWrap = false
}
if allowWrap {
return
}
zoomFp, err := p.CameraZoomFp()
if err != nil || zoomFp <= 0 {
return
}
cx, cy := ClampCameraNoWrapViewport(
p.CameraXWorldFp, p.CameraYWorldFp,
p.ViewportWidthPx, p.ViewportHeightPx,
zoomFp,
w.W, w.H,
)
p.CameraXWorldFp = cx
p.CameraYWorldFp = cy
}