Files
galaxy-game/client/world/zoom.go
T
2026-03-08 15:31:17 +02:00

121 lines
3.9 KiB
Go

package world
// worldFixedToCameraZoom converts a fixed-point zoom value back into the
// UI-facing floating-point representation where 1.0 means neutral zoom.
func worldFixedToCameraZoom(zoomFp int) float64 {
return float64(zoomFp) / float64(SCALE)
}
// requiredZoomToFitWorld returns the minimum fixed-point zoom needed so that
// a viewport span of viewportSpanPx pixels does not exceed a world span of
// worldSpanFp fixed-point units.
//
// The result is rounded up, not down, because the fit constraint must be
// satisfied conservatively: after correction, the visible world span must
// never be larger than the actual world span.
func requiredZoomToFitWorld(viewportSpanPx, worldSpanFp int) int {
if viewportSpanPx < 0 {
panic("requiredZoomToFitWorld: negative viewport span")
}
if worldSpanFp <= 0 {
panic("requiredZoomToFitWorld: non-positive world span")
}
if viewportSpanPx == 0 {
return 0
}
return ceilDiv(viewportSpanPx*SCALE*SCALE, worldSpanFp)
}
// correctCameraZoomFp corrects a fixed-point zoom value using two groups
// of constraints:
//
// 1. Fit-to-world constraints derived from viewport and world sizes.
// These have the highest priority and prevent the viewport from becoming
// larger than the world on any axis, which would otherwise expose wrap
// on the visible user area.
//
// 2. Optional UI zoom bounds [minZoomFp, maxZoomFp].
// A zero bound means "ignore this bound".
// If fit-to-world requires a zoom larger than maxZoomFp, the fit constraint
// wins and maxZoomFp is ignored for that case.
//
// The function returns either the corrected zoom or currentZoomFp unchanged
// when no correction is required.
func correctCameraZoomFp(
currentZoomFp int,
viewportWidthPx, viewportHeightPx int,
worldWidthFp, worldHeightFp int,
minZoomFp, maxZoomFp int,
) int {
if currentZoomFp <= 0 {
panic("correctCameraZoomFp: non-positive current zoom")
}
if viewportWidthPx < 0 || viewportHeightPx < 0 {
panic("correctCameraZoomFp: negative viewport size")
}
if worldWidthFp <= 0 || worldHeightFp <= 0 {
panic("correctCameraZoomFp: non-positive world size")
}
if minZoomFp < 0 || maxZoomFp < 0 {
panic("correctCameraZoomFp: negative zoom bound")
}
if minZoomFp > 0 && maxZoomFp > 0 && minZoomFp > maxZoomFp {
panic("correctCameraZoomFp: min zoom greater than max zoom")
}
// Start from the user zoom.
result := currentZoomFp
// Apply min bound first (only increases zoom, always valid).
if minZoomFp > 0 && result < minZoomFp {
result = minZoomFp
}
// Apply max bound tentatively. This can be overridden later by the anti-wrap constraint.
if maxZoomFp > 0 && result > maxZoomFp {
result = maxZoomFp
}
// If viewport is larger than the world on any axis at the current result zoom,
// increase zoom to the minimum value that prevents wrap in the visible area.
requiredFitX := requiredZoomToFitWorld(viewportWidthPx, worldWidthFp)
requiredFitY := requiredZoomToFitWorld(viewportHeightPx, worldHeightFp)
requiredFit := max(requiredFitX, requiredFitY)
if requiredFit > 0 && result < requiredFit {
result = requiredFit
}
// Re-apply max bound only if it does not conflict with the anti-wrap requirement.
// If anti-wrap requires zoom > maxZoomFp, anti-wrap wins.
if maxZoomFp > 0 && result > maxZoomFp && requiredFit <= maxZoomFp {
result = maxZoomFp
}
return result
}
// CorrectCameraZoom adapts fixed-point zoom correction for UI code.
//
// currentZoom is the user-facing zoom multiplier in floating-point form.
// The result is returned in the same representation.
func (w *World) CorrectCameraZoom(
currentZoom float64,
viewportWidthPx int,
viewportHeightPx int,
) float64 {
currentZoomFp := mustCameraZoomToWorldFixed(currentZoom)
correctedZoomFp := correctCameraZoomFp(
currentZoomFp,
viewportWidthPx,
viewportHeightPx,
w.W,
w.H,
MIN_ZOOM,
MAX_ZOOM,
)
return worldFixedToCameraZoom(correctedZoomFp)
}