themes and styles
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user