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 }