186 lines
4.8 KiB
Go
186 lines
4.8 KiB
Go
package world
|
|
|
|
import (
|
|
"image"
|
|
)
|
|
|
|
func (w *World) drawBackground(drawer PrimitiveDrawer, params RenderParams, rect RectPx) {
|
|
th := w.Theme()
|
|
bgImg := th.BackgroundImage()
|
|
if bgImg == nil {
|
|
return
|
|
}
|
|
|
|
canvasW := params.CanvasWidthPx()
|
|
canvasH := params.CanvasHeightPx()
|
|
|
|
// Clamp rect to canvas.
|
|
if rect.W <= 0 || rect.H <= 0 {
|
|
return
|
|
}
|
|
if rect.X < 0 {
|
|
rect.W += rect.X
|
|
rect.X = 0
|
|
}
|
|
if rect.Y < 0 {
|
|
rect.H += rect.Y
|
|
rect.Y = 0
|
|
}
|
|
if rect.X+rect.W > canvasW {
|
|
rect.W = canvasW - rect.X
|
|
}
|
|
if rect.Y+rect.H > canvasH {
|
|
rect.H = canvasH - rect.Y
|
|
}
|
|
if rect.W <= 0 || rect.H <= 0 {
|
|
return
|
|
}
|
|
|
|
imgB := bgImg.Bounds()
|
|
imgW := imgB.Dx()
|
|
imgH := imgB.Dy()
|
|
if imgW <= 0 || imgH <= 0 {
|
|
return
|
|
}
|
|
|
|
tileMode := th.BackgroundTileMode()
|
|
anchor := th.BackgroundAnchorMode()
|
|
scaleMode := th.BackgroundScaleMode()
|
|
|
|
// Compute scaled tile size.
|
|
tileW, tileH := backgroundScaledSize(imgW, imgH, canvasW, canvasH, scaleMode)
|
|
if tileW <= 0 || tileH <= 0 {
|
|
return
|
|
}
|
|
|
|
offX, offY := w.backgroundAnchorOffsetPx(params, tileW, tileH, anchor)
|
|
|
|
drawer.Save()
|
|
drawer.ResetClip()
|
|
drawer.ClipRect(float64(rect.X), float64(rect.Y), float64(rect.W), float64(rect.H))
|
|
|
|
switch tileMode {
|
|
case BackgroundTileNone:
|
|
// Center image within full canvas (not within rect), then clip handles partial.
|
|
// This is important so that dirty redraw matches full redraw.
|
|
x := (canvasW-tileW)/2 + offX
|
|
y := (canvasH-tileH)/2 + offY
|
|
drawBackgroundOne(drawer, bgImg, x, y, imgW, imgH, tileW, tileH, scaleMode)
|
|
|
|
case BackgroundTileRepeat:
|
|
originX := offX
|
|
originY := offY
|
|
|
|
startX := floorDiv(rect.X-originX, tileW)*tileW + originX
|
|
startY := floorDiv(rect.Y-originY, tileH)*tileH + originY
|
|
|
|
for yy := startY; yy < rect.Y+rect.H; yy += tileH {
|
|
for xx := startX; xx < rect.X+rect.W; xx += tileW {
|
|
drawBackgroundOne(drawer, bgImg, xx, yy, imgW, imgH, tileW, tileH, scaleMode)
|
|
}
|
|
}
|
|
|
|
default:
|
|
// Fallback: behave like none.
|
|
x := (canvasW-tileW)/2 + offX
|
|
y := (canvasH-tileH)/2 + offY
|
|
drawBackgroundOne(drawer, bgImg, x, y, imgW, imgH, tileW, tileH, scaleMode)
|
|
}
|
|
|
|
drawer.Restore()
|
|
}
|
|
|
|
func drawBackgroundOne(drawer PrimitiveDrawer, img image.Image, x, y, srcW, srcH, dstW, dstH int, scaleMode BackgroundScaleMode) {
|
|
if scaleMode == BackgroundScaleNone && dstW == srcW && dstH == srcH {
|
|
drawer.DrawImage(img, x, y)
|
|
return
|
|
}
|
|
// For Fit/Fill, or if dst size differs, draw scaled.
|
|
drawer.DrawImageScaled(img, x, y, dstW, dstH)
|
|
}
|
|
|
|
// backgroundScaledSize computes uniform scaled destination size for the background image.
|
|
// For None: returns source size.
|
|
// For Fit: fits inside canvas.
|
|
// For Fill: covers canvas.
|
|
func backgroundScaledSize(srcW, srcH, canvasW, canvasH int, mode BackgroundScaleMode) (int, int) {
|
|
if srcW <= 0 || srcH <= 0 || canvasW <= 0 || canvasH <= 0 {
|
|
return 0, 0
|
|
}
|
|
|
|
switch mode {
|
|
case BackgroundScaleNone:
|
|
return srcW, srcH
|
|
|
|
case BackgroundScaleFit, BackgroundScaleFill:
|
|
// Uniform scale: choose ratio based on min/max.
|
|
// Use integer math to avoid float; keep it stable across frames.
|
|
// We compute scale as rational and then round destination size.
|
|
// Let scale = canvasW/srcW vs canvasH/srcH.
|
|
// Fit uses min(scaleW, scaleH). Fill uses max(scaleW, scaleH).
|
|
//
|
|
// We'll compute dstW = round(srcW*scale), dstH = round(srcH*scale).
|
|
// Using float64 here is acceptable: this is UI-only and deterministic enough, and we already use gg float.
|
|
scaleW := float64(canvasW) / float64(srcW)
|
|
scaleH := float64(canvasH) / float64(srcH)
|
|
|
|
scale := scaleW
|
|
if mode == BackgroundScaleFit {
|
|
if scaleH < scale {
|
|
scale = scaleH
|
|
}
|
|
} else {
|
|
if scaleH > scale {
|
|
scale = scaleH
|
|
}
|
|
}
|
|
|
|
dstW := int(scale*float64(srcW) + 0.5)
|
|
dstH := int(scale*float64(srcH) + 0.5)
|
|
if dstW < 1 {
|
|
dstW = 1
|
|
}
|
|
if dstH < 1 {
|
|
dstH = 1
|
|
}
|
|
return dstW, dstH
|
|
|
|
default:
|
|
return srcW, srcH
|
|
}
|
|
}
|
|
|
|
// backgroundAnchorOffsetPx computes a stable pixel offset for background anchoring.
|
|
// - Viewport anchor: offset is always 0 (background fixed to viewport/canvas pixels).
|
|
// - World anchor: offset depends on camera world position and zoom so that background moves with pan.
|
|
func (w *World) backgroundAnchorOffsetPx(params RenderParams, tileW, tileH int, anchor BackgroundAnchorMode) (int, int) {
|
|
if anchor == BackgroundAnchorViewport {
|
|
return 0, 0
|
|
}
|
|
|
|
zoomFp, err := params.CameraZoomFp()
|
|
if err != nil || zoomFp <= 0 {
|
|
return 0, 0
|
|
}
|
|
|
|
canvasW := params.CanvasWidthPx()
|
|
canvasH := params.CanvasHeightPx()
|
|
|
|
spanW := PixelSpanToWorldFixed(canvasW, zoomFp)
|
|
spanH := PixelSpanToWorldFixed(canvasH, zoomFp)
|
|
|
|
worldLeft := params.CameraXWorldFp - spanW/2
|
|
worldTop := params.CameraYWorldFp - spanH/2
|
|
|
|
pxX := worldSpanFixedToCanvasPx(worldLeft, zoomFp)
|
|
pxY := worldSpanFixedToCanvasPx(worldTop, zoomFp)
|
|
|
|
if tileW > 0 {
|
|
pxX = -wrap(pxX, tileW)
|
|
}
|
|
if tileH > 0 {
|
|
pxY = -wrap(pxY, tileH)
|
|
}
|
|
return pxX, pxY
|
|
}
|