package world import ( "image" "image/draw" "reflect" ) type bgTileCacheKey struct { imgPtr uintptr scaleMode BackgroundScaleMode canvasW int canvasH int srcW int srcH int } type bgTileCache struct { key bgTileCacheKey valid bool scaledTile *image.RGBA tileW int tileH int } func (g *GGDrawer) drawBackgroundFast(w *World, params RenderParams, rect RectPx) bool { th := w.Theme() bgImg := th.BackgroundImage() if bgImg == nil { return false } dst, ok := g.DC.Image().(*image.RGBA) if !ok || dst == nil { return false } canvasW := params.CanvasWidthPx() canvasH := params.CanvasHeightPx() // Clamp rect to canvas. if rect.W <= 0 || rect.H <= 0 { return true } 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 true } imgB := bgImg.Bounds() srcW := imgB.Dx() srcH := imgB.Dy() if srcW <= 0 || srcH <= 0 { return true } tileMode := th.BackgroundTileMode() anchor := th.BackgroundAnchorMode() scaleMode := th.BackgroundScaleMode() // Compute scaled tile size in pixels (scale depends on canvas size). tileW, tileH := backgroundScaledSize(srcW, srcH, canvasW, canvasH, scaleMode) if tileW <= 0 || tileH <= 0 { return true } // Prepare the tile image (possibly scaled) from cache. tile := bgImg if scaleMode != BackgroundScaleNone || tileW != srcW || tileH != srcH { rgbaTile := g.getOrBuildScaledTile(bgImg, srcW, srcH, tileW, tileH, scaleMode, canvasW, canvasH) if rgbaTile == nil { // Fallback to slow path if we cannot scale (non-RGBA weirdness). return false } tile = rgbaTile } offX, offY := w.backgroundAnchorOffsetPx(params, tileW, tileH, anchor) switch tileMode { case BackgroundTileNone: // Draw single image centered in full canvas, then clipped by rect. x := (canvasW-tileW)/2 + offX y := (canvasH-tileH)/2 + offY w.drawOneTileRGBA(dst, tile, rect, x, y) 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 { w.drawOneTileRGBA(dst, tile, rect, xx, yy) } } default: // Treat unknown as none. x := (canvasW-tileW)/2 + offX y := (canvasH-tileH)/2 + offY w.drawOneTileRGBA(dst, tile, rect, x, y) } return true } func (g *GGDrawer) getOrBuildScaledTile(img image.Image, srcW, srcH, dstW, dstH int, mode BackgroundScaleMode, canvasW, canvasH int) *image.RGBA { // Identify image pointer (themes typically provide *image.RGBA). ptr := imagePointer(img) key := bgTileCacheKey{ imgPtr: ptr, scaleMode: mode, canvasW: canvasW, canvasH: canvasH, srcW: srcW, srcH: srcH, } if g.bgCache.valid && g.bgCache.key == key && g.bgCache.scaledTile != nil && g.bgCache.tileW == dstW && g.bgCache.tileH == dstH { return g.bgCache.scaledTile } // Scale only from *image.RGBA fast; otherwise, try a generic slow path. var scaled *image.RGBA if srcRGBA, ok := img.(*image.RGBA); ok { scaled = scaleNearestRGBA(srcRGBA, dstW, dstH) } else { scaled = scaleNearestGeneric(img, dstW, dstH) } g.bgCache.key = key g.bgCache.valid = true g.bgCache.scaledTile = scaled g.bgCache.tileW = dstW g.bgCache.tileH = dstH return scaled } func imagePointer(img image.Image) uintptr { // Works well when img is a pointer type (e.g. *image.RGBA). // If not pointer, Pointer() returns 0; cache will be less effective but still correct. v := reflect.ValueOf(img) if v.Kind() == reflect.Pointer || v.Kind() == reflect.UnsafePointer { return v.Pointer() } return 0 } // scaleNearestRGBA scales src -> dst with nearest-neighbor sampling. // This is intended for background textures; performance > quality. func scaleNearestRGBA(src *image.RGBA, dstW, dstH int) *image.RGBA { if dstW <= 0 || dstH <= 0 { return nil } sb := src.Bounds() sw := sb.Dx() sh := sb.Dy() if sw <= 0 || sh <= 0 { return nil } dst := image.NewRGBA(image.Rect(0, 0, dstW, dstH)) for y := 0; y < dstH; y++ { sy := (y * sh) / dstH srcOff := (sy+sb.Min.Y)*src.Stride + sb.Min.X*4 dstOff := y * dst.Stride for x := 0; x < dstW; x++ { sx := (x * sw) / dstW si := srcOff + sx*4 di := dstOff + x*4 dst.Pix[di+0] = src.Pix[si+0] dst.Pix[di+1] = src.Pix[si+1] dst.Pix[di+2] = src.Pix[si+2] dst.Pix[di+3] = src.Pix[si+3] } } return dst } func scaleNearestGeneric(src image.Image, dstW, dstH int) *image.RGBA { if dstW <= 0 || dstH <= 0 { return nil } sb := src.Bounds() sw := sb.Dx() sh := sb.Dy() if sw <= 0 || sh <= 0 { return nil } dst := image.NewRGBA(image.Rect(0, 0, dstW, dstH)) for y := 0; y < dstH; y++ { sy := sb.Min.Y + (y*sh)/dstH for x := 0; x < dstW; x++ { sx := sb.Min.X + (x*sw)/dstW dst.Set(x, y, src.At(sx, sy)) } } return dst } // drawOneTileRGBA draws tile at (x,y) into dst, but only the portion that intersects rect. // Uses draw.Over (alpha compositing), assuming caller already cleared rect to background color. func (w *World) drawOneTileRGBA(dst *image.RGBA, tile image.Image, rect RectPx, x, y int) { tileB := tile.Bounds() tw := tileB.Dx() th := tileB.Dy() if tw <= 0 || th <= 0 { return } // Intersection of tile rect and target rect. tx0 := x ty0 := y tx1 := x + tw ty1 := y + th rx0 := rect.X ry0 := rect.Y rx1 := rect.X + rect.W ry1 := rect.Y + rect.H ix0 := max(tx0, rx0) iy0 := max(ty0, ry0) ix1 := min(tx1, rx1) iy1 := min(ty1, ry1) if ix0 >= ix1 || iy0 >= iy1 { return } dstR := image.Rect(ix0, iy0, ix1, iy1) srcPt := image.Point{X: tileB.Min.X + (ix0 - tx0), Y: tileB.Min.Y + (iy0 - ty0)} draw.Draw(dst, dstR, tile, srcPt, draw.Over) }