themes and styles

This commit is contained in:
IliaDenisov
2026-03-08 15:31:17 +02:00
parent e37a67bc99
commit 1c2fc30127
39 changed files with 2693 additions and 199 deletions
+185
View File
@@ -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
}