diff --git a/internal/render/common/styles.go b/internal/render/common/styles.go index eefb1c31..9e663dee 100644 --- a/internal/render/common/styles.go +++ b/internal/render/common/styles.go @@ -147,10 +147,22 @@ var ( style.SetZIndex(-99), ) - FooterWrapperLayout = style.Style{ Direction: style.DirectionHorizontal, AlignItems: style.AlignItemsCenter, Gap: 5, } ) + +func CardsBackgroundStyleForTheme(theme Theme) style.StyleOptions { + blur := DefaultBackgroundBlur + if theme.BackgroundBlur != nil { + blur = *theme.BackgroundBlur + } + return style.NewStyle( + style.SetBorderRadius(BorderRadius2XL), + style.SetBlur(blur), + style.SetPosition(style.PositionAbsolute), + style.SetZIndex(-99), + ) +} diff --git a/internal/render/common/theme.go b/internal/render/common/theme.go index 73eea5a4..4e620e09 100644 --- a/internal/render/common/theme.go +++ b/internal/render/common/theme.go @@ -24,6 +24,9 @@ type Theme struct { // Optional background image bundled with the theme. // Used as the default background when no explicit background is provided. Background image.Image + // BackgroundBlur overrides the default blur applied to the background image + // at render time. nil = use DefaultBackgroundBlur, non-nil = use this value. + BackgroundBlur *float64 // BackgroundOverlay is rendered behind cards, on top of the background image. // seed is derived from the account ID for deterministic patterns. diff --git a/internal/stats/render/period/v2/cards.go b/internal/stats/render/period/v2/cards.go index 57cccbf9..f2c54505 100644 --- a/internal/stats/render/period/v2/cards.go +++ b/internal/stats/render/period/v2/cards.go @@ -128,11 +128,12 @@ func generateCards(stats fetch.AccountStatsOverPeriod, cards period.Cards, _ []m opts.Background = common.AddWN8BackgroundBranding(opts.Background, stats.RegularBattles.Vehicles, seed) } + bgStyle := common.CardsBackgroundStyleForTheme(theme) var layers []*facepaint.Block - layers = append(layers, facepaint.MustNewImageContent(common.CardsBackgroundStyle, opts.Background)) + layers = append(layers, facepaint.MustNewImageContent(bgStyle, opts.Background)) if theme.BackgroundOverlay != nil { if overlay := theme.BackgroundOverlay(opts.Background.Bounds(), seed); overlay != nil { - layers = append(layers, facepaint.MustNewImageContent(common.CardsBackgroundStyle, overlay)) + layers = append(layers, facepaint.MustNewImageContent(bgStyle, overlay)) } } layers = append(layers, cardsFrame) diff --git a/internal/stats/render/session/v2/cards.go b/internal/stats/render/session/v2/cards.go index 1e08a443..6572bf6e 100644 --- a/internal/stats/render/session/v2/cards.go +++ b/internal/stats/render/session/v2/cards.go @@ -110,11 +110,12 @@ func generateCards(sessionData, careerData fetch.AccountStatsOverPeriod, cards s opts.Background = common.AddWN8BackgroundBranding(opts.Background, sessionData.RegularBattles.Vehicles, seed) } + bgStyle := common.CardsBackgroundStyleForTheme(theme) var layers []*facepaint.Block - layers = append(layers, facepaint.MustNewImageContent(common.CardsBackgroundStyle, opts.Background)) + layers = append(layers, facepaint.MustNewImageContent(bgStyle, opts.Background)) if theme.BackgroundOverlay != nil { if overlay := theme.BackgroundOverlay(opts.Background.Bounds(), seed); overlay != nil { - layers = append(layers, facepaint.MustNewImageContent(common.CardsBackgroundStyle, overlay)) + layers = append(layers, facepaint.MustNewImageContent(bgStyle, overlay)) } } layers = append(layers, cardsFrame) diff --git a/internal/stats/render/themes/spring2026/assets.go b/internal/stats/render/themes/spring2026/assets.go index 3612e543..41dd9f62 100644 --- a/internal/stats/render/themes/spring2026/assets.go +++ b/internal/stats/render/themes/spring2026/assets.go @@ -8,11 +8,9 @@ import ( _ "image/png" "path" "strings" - - "github.com/nao1215/imaging" ) -//go:embed assets/background.jpg +//go:embed assets/background_blurred.jpg var backgroundBytes []byte //go:embed assets/petals/processed @@ -29,7 +27,6 @@ func init() { if err != nil { panic("spring2026: failed to decode background: " + err.Error()) } - backgroundImage = imaging.Blur(backgroundImage, 3) backgroundBytes = nil entries, err := petalsFS.ReadDir("assets/petals/processed") diff --git a/internal/stats/render/themes/spring2026/assets/.gitignore b/internal/stats/render/themes/spring2026/assets/.gitignore new file mode 100644 index 00000000..e97ba92c --- /dev/null +++ b/internal/stats/render/themes/spring2026/assets/.gitignore @@ -0,0 +1 @@ +background_blurred.jpg diff --git a/internal/stats/render/themes/spring2026/assets/depthmap.png b/internal/stats/render/themes/spring2026/assets/depthmap.png new file mode 100644 index 00000000..e1586d15 Binary files /dev/null and b/internal/stats/render/themes/spring2026/assets/depthmap.png differ diff --git a/internal/stats/render/themes/spring2026/assets/generate.go b/internal/stats/render/themes/spring2026/assets/generate.go index 35212e04..3c583194 100644 --- a/internal/stats/render/themes/spring2026/assets/generate.go +++ b/internal/stats/render/themes/spring2026/assets/generate.go @@ -6,6 +6,7 @@ import ( "fmt" "image" "image/color" + "image/jpeg" "image/png" "math" "os" @@ -37,6 +38,11 @@ var tints = []tintPreset{ } func main() { + generatePetals() + generateBlurredBackground() +} + +func generatePetals() { sourceDir := filepath.Join("petals", "source") outDir := filepath.Join("petals", "processed") @@ -103,6 +109,70 @@ func main() { fmt.Printf("generated %d processed petal images\n", idx) } +func generateBlurredBackground() { + const ( + minSigma = 0.5 + maxSigma = 8.0 + levels = 8 + quality = 95 + ) + + bg, err := imaging.Open("background.jpg") + if err != nil { + panic(fmt.Sprintf("failed to open background.jpg: %v", err)) + } + + dm, err := imaging.Open("depthmap.png") + if err != nil { + panic(fmt.Sprintf("failed to open depthmap.png: %v", err)) + } + + w, h := bg.Bounds().Dx(), bg.Bounds().Dy() + dm = imaging.Resize(dm, w, h, imaging.Linear) + + stack := make([]*image.NRGBA, levels) + for i := range levels { + sigma := minSigma + (maxSigma-minSigma)*float64(i)/float64(levels-1) + stack[i] = imaging.Blur(bg, sigma) + fmt.Printf(" blur level %d/%d (sigma=%.1f)\n", i+1, levels, sigma) + } + + out := image.NewNRGBA(image.Rect(0, 0, w, h)) + for y := range h { + for x := range w { + r, _, _, _ := dm.At(x, y).RGBA() + depth := float64(r>>8) / 255.0 // 1.0 = close (bright), 0.0 = far (dark) + + // close → low index (less blur), far → high index (more blur) + idx := (1.0 - depth) * float64(levels-1) + lo := int(math.Floor(idx)) + hi := int(math.Ceil(idx)) + lo = max(lo, 0) + hi = min(hi, levels-1) + frac := idx - float64(lo) + + c1 := stack[lo].NRGBAAt(x, y) + c2 := stack[hi].NRGBAAt(x, y) + out.SetNRGBA(x, y, color.NRGBA{ + R: uint8(lerp(float64(c1.R), float64(c2.R), frac)), + G: uint8(lerp(float64(c1.G), float64(c2.G), frac)), + B: uint8(lerp(float64(c1.B), float64(c2.B), frac)), + A: 255, + }) + } + } + + f, err := os.Create("background_blurred.jpg") + if err != nil { + panic(err) + } + defer f.Close() + if err := jpeg.Encode(f, out, &jpeg.Options{Quality: quality}); err != nil { + panic(err) + } + fmt.Println("generated depth-blurred background") +} + // trimAlpha crops transparent borders, keeping at least minPad pixels of padding. func trimAlpha(img *image.NRGBA, minPad int) *image.NRGBA { b := img.Bounds() diff --git a/internal/stats/render/themes/spring2026/theme.go b/internal/stats/render/themes/spring2026/theme.go index 8517119a..089d1853 100644 --- a/internal/stats/render/themes/spring2026/theme.go +++ b/internal/stats/render/themes/spring2026/theme.go @@ -17,8 +17,10 @@ var ( ) func Theme() common.Theme { + noBlur := 0.0 return common.Theme{ - Background: backgroundImage, + Background: backgroundImage, + BackgroundBlur: &noBlur, Frame: style.NewStyle(func(s *style.Style) { s.PaddingLeft = 15 s.PaddingRight = 15