696 lines
18 KiB
Go
696 lines
18 KiB
Go
package world
|
|
|
|
import (
|
|
"image"
|
|
"image/color"
|
|
"sync"
|
|
)
|
|
|
|
// StyleID references a fully-materialized style stored in StyleTable.
|
|
type StyleID int
|
|
|
|
const (
|
|
// StyleIDInvalid means "no style". It should not be used for rendering.
|
|
StyleIDInvalid StyleID = 0
|
|
|
|
// Built-in default styles (stable IDs).
|
|
StyleIDDefaultLine StyleID = 1
|
|
StyleIDDefaultCircle StyleID = 2
|
|
StyleIDDefaultPoint StyleID = 3
|
|
)
|
|
|
|
// Default priorities (smaller draws earlier), step=100.
|
|
const (
|
|
DefaultPriorityLine = 100
|
|
DefaultPriorityCircle = 200
|
|
DefaultPriorityPoint = 300
|
|
)
|
|
|
|
var (
|
|
transparentColor color.Color = &color.RGBA{A: 0}
|
|
)
|
|
|
|
// TransparentFill returns a reusable fully transparent color value.
|
|
//
|
|
// It is intended for callers that want to explicitly disable fill while still
|
|
// setting a non-nil FillColor override.
|
|
func TransparentFill() color.Color { return transparentColor }
|
|
|
|
// Style is a fully resolved style used by the renderer.
|
|
// All fields are concrete values; no "optional" markers here.
|
|
// Optionality is handled by StyleOverride during style creation.
|
|
type Style struct {
|
|
// FillColor is used for Fill() operations (points/circles typically).
|
|
// If nil, the renderer may treat it as "do not fill" depending on primitive.
|
|
FillColor color.Color
|
|
|
|
// StrokeColor is used for Stroke() operations (lines typically).
|
|
// If nil, the renderer may treat it as "do not stroke" depending on primitive.
|
|
StrokeColor color.Color
|
|
|
|
// StrokeWidthPx is a screen-space stroke width in pixels.
|
|
StrokeWidthPx float64
|
|
|
|
// StrokeDashes is the dash pattern in pixels. nil/empty means "solid".
|
|
StrokeDashes []float64
|
|
|
|
// StrokeDashOffset is the dash phase in pixels.
|
|
StrokeDashOffset float64
|
|
|
|
// PointRadiusPx is a screen-space radius for Point markers.
|
|
PointRadiusPx float64
|
|
}
|
|
|
|
// StyleOverride describes partial modifications applied to a base Style.
|
|
// Fields set to nil mean "do not override".
|
|
type StyleOverride struct {
|
|
FillColor color.Color
|
|
StrokeColor color.Color
|
|
StrokeWidthPx *float64
|
|
StrokeDashes *[]float64
|
|
StrokeDashOffset *float64
|
|
PointRadiusPx *float64
|
|
}
|
|
|
|
// IsZero reports whether override does not specify any fields.
|
|
func (o StyleOverride) IsZero() bool {
|
|
return o.FillColor == nil &&
|
|
o.StrokeColor == nil &&
|
|
o.StrokeWidthPx == nil &&
|
|
o.StrokeDashes == nil &&
|
|
o.StrokeDashOffset == nil &&
|
|
o.PointRadiusPx == nil
|
|
}
|
|
|
|
// Apply applies override to base style and returns a new fully resolved style.
|
|
// It copies slices defensively to avoid aliasing.
|
|
func (o StyleOverride) Apply(base Style) Style {
|
|
out := base
|
|
|
|
if o.FillColor != nil {
|
|
out.FillColor = o.FillColor
|
|
}
|
|
if o.StrokeColor != nil {
|
|
out.StrokeColor = o.StrokeColor
|
|
}
|
|
if o.StrokeWidthPx != nil {
|
|
out.StrokeWidthPx = *o.StrokeWidthPx
|
|
}
|
|
if o.StrokeDashes != nil {
|
|
// Copy to avoid future mutation by caller.
|
|
src := *o.StrokeDashes
|
|
if src == nil {
|
|
out.StrokeDashes = nil
|
|
} else {
|
|
dst := make([]float64, len(src))
|
|
copy(dst, src)
|
|
out.StrokeDashes = dst
|
|
}
|
|
}
|
|
if o.StrokeDashOffset != nil {
|
|
out.StrokeDashOffset = *o.StrokeDashOffset
|
|
}
|
|
if o.PointRadiusPx != nil {
|
|
out.PointRadiusPx = *o.PointRadiusPx
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
// StyleTable stores fully resolved styles and provides stable lookups by StyleID.
|
|
// It also holds three built-in defaults for Line/Circle/Point.
|
|
type StyleTable struct {
|
|
mu sync.RWMutex
|
|
nextID StyleID
|
|
styles map[StyleID]Style
|
|
}
|
|
|
|
// NewStyleTable creates a new style table with built-in default styles.
|
|
// The default values are intentionally simple and stable.
|
|
func NewStyleTable() *StyleTable {
|
|
t := &StyleTable{
|
|
nextID: StyleIDDefaultPoint + 1,
|
|
styles: make(map[StyleID]Style, 16),
|
|
}
|
|
|
|
// Defaults: conservative, deterministic.
|
|
// Colors: opaque black. (Callers can override.)
|
|
white := color.RGBA{R: 255, G: 255, B: 255, A: 255}
|
|
|
|
t.styles[StyleIDDefaultLine] = Style{
|
|
FillColor: nil,
|
|
StrokeColor: white,
|
|
StrokeWidthPx: 2.0,
|
|
StrokeDashes: nil,
|
|
StrokeDashOffset: 0,
|
|
PointRadiusPx: 0,
|
|
}
|
|
|
|
t.styles[StyleIDDefaultCircle] = Style{
|
|
FillColor: white,
|
|
StrokeColor: nil,
|
|
StrokeWidthPx: 0,
|
|
StrokeDashes: nil,
|
|
StrokeDashOffset: 0,
|
|
PointRadiusPx: 0,
|
|
}
|
|
|
|
t.styles[StyleIDDefaultPoint] = Style{
|
|
FillColor: white,
|
|
StrokeColor: nil,
|
|
StrokeWidthPx: 0,
|
|
StrokeDashes: nil,
|
|
StrokeDashOffset: 0,
|
|
PointRadiusPx: 2.0,
|
|
}
|
|
|
|
return t
|
|
}
|
|
|
|
// Get returns a style by id.
|
|
func (t *StyleTable) Get(id StyleID) (Style, bool) {
|
|
t.mu.RLock()
|
|
defer t.mu.RUnlock()
|
|
s, ok := t.styles[id]
|
|
if !ok {
|
|
return Style{}, false
|
|
}
|
|
// Defensive copy of slices.
|
|
if s.StrokeDashes != nil {
|
|
cp := make([]float64, len(s.StrokeDashes))
|
|
copy(cp, s.StrokeDashes)
|
|
s.StrokeDashes = cp
|
|
}
|
|
return s, true
|
|
}
|
|
|
|
// AddDerived creates a new style based on baseID with an override applied.
|
|
// It returns the new style ID.
|
|
func (t *StyleTable) AddDerived(baseID StyleID, override StyleOverride) StyleID {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
|
|
base, ok := t.styles[baseID]
|
|
if !ok {
|
|
panic("StyleTable.AddDerived: unknown base style ID")
|
|
}
|
|
|
|
derived := override.Apply(base)
|
|
|
|
id := t.nextID
|
|
t.nextID++
|
|
|
|
// Defensive copy of slices on store.
|
|
if derived.StrokeDashes != nil {
|
|
cp := make([]float64, len(derived.StrokeDashes))
|
|
copy(cp, derived.StrokeDashes)
|
|
derived.StrokeDashes = cp
|
|
}
|
|
|
|
t.styles[id] = derived
|
|
return id
|
|
}
|
|
|
|
// AddStyle stores a fully resolved style as a new StyleID.
|
|
// It defensively copies slice fields.
|
|
func (t *StyleTable) AddStyle(s Style) StyleID {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
|
|
id := t.nextID
|
|
t.nextID++
|
|
|
|
if s.StrokeDashes != nil {
|
|
cp := make([]float64, len(s.StrokeDashes))
|
|
copy(cp, s.StrokeDashes)
|
|
s.StrokeDashes = cp
|
|
}
|
|
|
|
t.styles[id] = s
|
|
return id
|
|
}
|
|
|
|
// Count returns the number of styles stored in the table.
|
|
// Intended for tests/diagnostics.
|
|
func (t *StyleTable) Count() int {
|
|
t.mu.RLock()
|
|
defer t.mu.RUnlock()
|
|
return len(t.styles)
|
|
}
|
|
|
|
// BackgroundTileMode defines how the background image is tiled.
|
|
type BackgroundTileMode uint8
|
|
|
|
const (
|
|
BackgroundTileNone BackgroundTileMode = iota
|
|
BackgroundTileRepeat
|
|
)
|
|
|
|
// BackgroundAnchorMode defines whether the background image scrolls with the world or stays fixed to viewport.
|
|
type BackgroundAnchorMode uint8
|
|
|
|
const (
|
|
BackgroundAnchorWorld BackgroundAnchorMode = iota
|
|
BackgroundAnchorViewport
|
|
)
|
|
|
|
// BackgroundScaleMode defines how the background image is scaled.
|
|
// (Step 1: defined for API completeness; used later when rendering background image.)
|
|
type BackgroundScaleMode uint8
|
|
|
|
const (
|
|
BackgroundScaleNone BackgroundScaleMode = iota
|
|
BackgroundScaleFit
|
|
BackgroundScaleFill
|
|
)
|
|
|
|
// StyleTheme describes a cohesive style set (theme) for rendering.
|
|
// Step 1: we store it in World and use it for background and default base styles.
|
|
// Step 2+: theme-relative overrides and background image drawing.
|
|
type StyleTheme interface {
|
|
ID() string
|
|
Name() string
|
|
|
|
BackgroundColor() color.Color
|
|
BackgroundImage() image.Image
|
|
|
|
BackgroundTileMode() BackgroundTileMode
|
|
BackgroundScaleMode() BackgroundScaleMode
|
|
BackgroundAnchorMode() BackgroundAnchorMode
|
|
|
|
PointStyle() Style
|
|
LineStyle() Style
|
|
CircleStyle() Style
|
|
|
|
// Class overrides (relative to base kind style).
|
|
// Return (override, true) when class is supported; (zero, false) means "no override".
|
|
PointClassOverride(class PointClassID) (StyleOverride, bool)
|
|
LineClassOverride(class LineClassID) (StyleOverride, bool)
|
|
CircleClassOverride(class CircleClassID) (StyleOverride, bool)
|
|
}
|
|
|
|
// DefaultTheme is a conservative theme matching built-in default styles.
|
|
type DefaultTheme struct{}
|
|
|
|
func (DefaultTheme) ID() string { return "default" }
|
|
func (DefaultTheme) Name() string { return "Default" }
|
|
|
|
func (DefaultTheme) BackgroundColor() color.Color { return color.RGBA{A: 255} }
|
|
func (DefaultTheme) BackgroundImage() image.Image { return nil }
|
|
|
|
func (DefaultTheme) BackgroundTileMode() BackgroundTileMode { return BackgroundTileNone }
|
|
func (DefaultTheme) BackgroundScaleMode() BackgroundScaleMode { return BackgroundScaleNone }
|
|
func (DefaultTheme) BackgroundAnchorMode() BackgroundAnchorMode { return BackgroundAnchorWorld }
|
|
|
|
func (DefaultTheme) PointStyle() Style {
|
|
s, _ := NewStyleTable().Get(StyleIDDefaultPoint)
|
|
return s
|
|
}
|
|
func (DefaultTheme) LineStyle() Style {
|
|
s, _ := NewStyleTable().Get(StyleIDDefaultLine)
|
|
return s
|
|
}
|
|
func (DefaultTheme) CircleStyle() Style {
|
|
s, _ := NewStyleTable().Get(StyleIDDefaultCircle)
|
|
return s
|
|
}
|
|
|
|
func (DefaultTheme) PointClassOverride(PointClassID) (StyleOverride, bool) {
|
|
return StyleOverride{}, false
|
|
}
|
|
func (DefaultTheme) LineClassOverride(LineClassID) (StyleOverride, bool) {
|
|
return StyleOverride{}, false
|
|
}
|
|
func (DefaultTheme) CircleClassOverride(CircleClassID) (StyleOverride, bool) {
|
|
return StyleOverride{}, false
|
|
}
|
|
|
|
// This file provides two sample themes for demos and UI integration:
|
|
// LightTheme uses only background color, while DarkTheme also carries a
|
|
// prebuilt tiled texture image.
|
|
|
|
var (
|
|
// ThemeLight is the shared light theme instance used by the client package.
|
|
ThemeLight = &LightTheme{}
|
|
// ThemeDark is the shared dark theme instance used by the client package.
|
|
ThemeDark = NewDarkTheme()
|
|
)
|
|
|
|
// -----------------------------
|
|
// Helpers
|
|
// -----------------------------
|
|
|
|
// cRGBA constructs an sRGB color from 8-bit RGBA channels.
|
|
func cRGBA(r, g, b, a uint8) color.Color { return color.RGBA{R: r, G: g, B: b, A: a} }
|
|
|
|
// -----------------------------
|
|
// Light Theme (color only)
|
|
// -----------------------------
|
|
|
|
// LightTheme is a soft high-contrast theme intended for bright backgrounds.
|
|
type LightTheme struct{}
|
|
|
|
func (LightTheme) ID() string { return "theme.light.v1" }
|
|
func (LightTheme) Name() string { return "Light (Soft)" }
|
|
|
|
func (LightTheme) BackgroundColor() color.Color { return cRGBA(244, 246, 248, 255) } // #F4F6F8
|
|
func (LightTheme) BackgroundImage() image.Image { return nil }
|
|
|
|
func (LightTheme) BackgroundTileMode() BackgroundTileMode { return BackgroundTileNone }
|
|
func (LightTheme) BackgroundScaleMode() BackgroundScaleMode { return BackgroundScaleNone }
|
|
func (LightTheme) BackgroundAnchorMode() BackgroundAnchorMode { return BackgroundAnchorWorld }
|
|
|
|
// Base styles per primitive kind (full Style, not override).
|
|
func (LightTheme) PointStyle() Style {
|
|
return Style{
|
|
FillColor: cRGBA(32, 161, 145, 255), // soft teal
|
|
StrokeColor: nil,
|
|
StrokeWidthPx: 0,
|
|
PointRadiusPx: 3.0,
|
|
}
|
|
}
|
|
|
|
func (LightTheme) LineStyle() Style {
|
|
return Style{
|
|
FillColor: nil,
|
|
StrokeColor: cRGBA(70, 108, 196, 220), // soft blue
|
|
StrokeWidthPx: 2.0,
|
|
StrokeDashes: nil,
|
|
StrokeDashOffset: 0,
|
|
}
|
|
}
|
|
|
|
func (LightTheme) CircleStyle() Style {
|
|
return Style{
|
|
FillColor: cRGBA(133, 110, 201, 60), // soft purple with low alpha
|
|
StrokeColor: cRGBA(133, 110, 201, 200), // soft purple
|
|
StrokeWidthPx: 2.0,
|
|
}
|
|
}
|
|
|
|
// Point class overrides.
|
|
func (LightTheme) PointClassOverride(class PointClassID) (StyleOverride, bool) {
|
|
switch class {
|
|
case PointClassDefault:
|
|
return StyleOverride{}, false
|
|
|
|
case PointClassTrackUnknown:
|
|
// muted gray-blue
|
|
return StyleOverride{
|
|
FillColor: cRGBA(120, 135, 160, 230),
|
|
PointRadiusPx: new(3.0),
|
|
}, true
|
|
|
|
case PointClassTrackIncoming:
|
|
// soft green
|
|
return StyleOverride{
|
|
FillColor: cRGBA(76, 171, 107, 240),
|
|
PointRadiusPx: new(3.5),
|
|
}, true
|
|
|
|
case PointClassTrackOutgoing:
|
|
// soft orange
|
|
return StyleOverride{
|
|
FillColor: cRGBA(222, 142, 70, 240),
|
|
PointRadiusPx: new(3.5),
|
|
}, true
|
|
|
|
case PointClassUnidentifiedPlanet:
|
|
// soft orange
|
|
return StyleOverride{
|
|
FillColor: cRGBA(192, 192, 192, 255),
|
|
PointRadiusPx: new(2.5),
|
|
}, true
|
|
|
|
default:
|
|
return StyleOverride{}, false
|
|
}
|
|
}
|
|
|
|
func (LightTheme) LineClassOverride(class LineClassID) (StyleOverride, bool) {
|
|
switch class {
|
|
case LineClassDefault:
|
|
return StyleOverride{}, false
|
|
|
|
case LineClassTrackIncoming:
|
|
return StyleOverride{
|
|
StrokeColor: cRGBA(76, 171, 107, 220),
|
|
StrokeWidthPx: new(2.5),
|
|
}, true
|
|
|
|
case LineCLassTrackOutgoing:
|
|
return StyleOverride{
|
|
StrokeColor: cRGBA(222, 142, 70, 220),
|
|
StrokeWidthPx: new(2.5),
|
|
}, true
|
|
|
|
case LineClassMeasurement:
|
|
// dashed neutral line
|
|
d := []float64{6, 4}
|
|
return StyleOverride{
|
|
StrokeColor: cRGBA(100, 110, 125, 200),
|
|
StrokeWidthPx: new(1.8),
|
|
StrokeDashes: &d,
|
|
StrokeDashOffset: new(0.),
|
|
}, true
|
|
|
|
default:
|
|
return StyleOverride{}, false
|
|
}
|
|
}
|
|
|
|
func (LightTheme) CircleClassOverride(class CircleClassID) (StyleOverride, bool) {
|
|
switch class {
|
|
case CircleClassDefault:
|
|
return StyleOverride{}, false
|
|
|
|
case CircleClassLocalPlanet:
|
|
// blue
|
|
return StyleOverride{
|
|
FillColor: cRGBA(70, 108, 196, 45),
|
|
StrokeColor: cRGBA(70, 108, 196, 220),
|
|
StrokeWidthPx: new(2.2),
|
|
}, true
|
|
|
|
case CircleClassOthersPlanet:
|
|
// orange
|
|
return StyleOverride{
|
|
FillColor: cRGBA(222, 142, 70, 50),
|
|
StrokeColor: cRGBA(222, 142, 70, 220),
|
|
StrokeWidthPx: new(2.2),
|
|
}, true
|
|
|
|
case CircleClassFreePlanet:
|
|
// green
|
|
return StyleOverride{
|
|
FillColor: cRGBA(76, 171, 107, 45),
|
|
StrokeColor: cRGBA(76, 171, 107, 220),
|
|
StrokeWidthPx: new(2.2),
|
|
}, true
|
|
|
|
default:
|
|
return StyleOverride{}, false
|
|
}
|
|
}
|
|
|
|
// -----------------------------
|
|
// Dark Theme (color + tiled image)
|
|
// -----------------------------
|
|
|
|
// DarkTheme is a dark theme with an optional reusable background tile.
|
|
type DarkTheme struct {
|
|
bg image.Image
|
|
}
|
|
|
|
// NewDarkTheme constructs a DarkTheme with its immutable texture tile prepared.
|
|
func NewDarkTheme() *DarkTheme {
|
|
return &DarkTheme{bg: makeDarkBackgroundTile(96, 96)}
|
|
}
|
|
|
|
func (*DarkTheme) ID() string { return "theme.dark.v1" }
|
|
func (*DarkTheme) Name() string { return "Dark (Soft + Texture)" }
|
|
|
|
func (*DarkTheme) BackgroundColor() color.Color { return cRGBA(30, 32, 38, 255) } // #1E2026
|
|
func (t *DarkTheme) BackgroundImage() image.Image {
|
|
return nil
|
|
// This image is immutable after creation.
|
|
// return t.bg
|
|
}
|
|
|
|
func (*DarkTheme) BackgroundTileMode() BackgroundTileMode { return BackgroundTileRepeat }
|
|
func (*DarkTheme) BackgroundScaleMode() BackgroundScaleMode { return BackgroundScaleNone }
|
|
func (*DarkTheme) BackgroundAnchorMode() BackgroundAnchorMode { return BackgroundAnchorViewport }
|
|
|
|
// Base styles for dark theme.
|
|
func (*DarkTheme) PointStyle() Style {
|
|
return Style{
|
|
FillColor: cRGBA(120, 214, 198, 255),
|
|
StrokeColor: nil,
|
|
StrokeWidthPx: 0,
|
|
PointRadiusPx: 3.0,
|
|
}
|
|
}
|
|
|
|
func (*DarkTheme) LineStyle() Style {
|
|
return Style{
|
|
FillColor: nil,
|
|
StrokeColor: cRGBA(155, 175, 235, 255),
|
|
StrokeWidthPx: 2.0,
|
|
StrokeDashes: nil,
|
|
StrokeDashOffset: 0,
|
|
}
|
|
}
|
|
|
|
func (*DarkTheme) CircleStyle() Style {
|
|
return Style{
|
|
FillColor: nil, // cRGBA(186, 160, 255, 255),
|
|
StrokeColor: cRGBA(186, 160, 255, 255),
|
|
StrokeWidthPx: 2.0,
|
|
}
|
|
}
|
|
|
|
// Point class overrides.
|
|
func (*DarkTheme) PointClassOverride(class PointClassID) (StyleOverride, bool) {
|
|
switch class {
|
|
case PointClassDefault:
|
|
return StyleOverride{}, false
|
|
|
|
case PointClassTrackUnknown:
|
|
return StyleOverride{
|
|
FillColor: cRGBA(150, 160, 175, 255),
|
|
PointRadiusPx: new(3.0),
|
|
}, true
|
|
|
|
case PointClassTrackIncoming:
|
|
return StyleOverride{
|
|
FillColor: cRGBA(132, 219, 162, 255),
|
|
PointRadiusPx: new(3.5),
|
|
}, true
|
|
|
|
case PointClassTrackOutgoing:
|
|
return StyleOverride{
|
|
FillColor: cRGBA(245, 178, 120, 255),
|
|
PointRadiusPx: new(3.5),
|
|
}, true
|
|
|
|
case PointClassUnidentifiedPlanet:
|
|
return StyleOverride{
|
|
FillColor: cRGBA(192, 192, 192, 255),
|
|
PointRadiusPx: new(2.5),
|
|
}, true
|
|
|
|
default:
|
|
return StyleOverride{}, false
|
|
}
|
|
}
|
|
|
|
func (*DarkTheme) LineClassOverride(class LineClassID) (StyleOverride, bool) {
|
|
switch class {
|
|
case LineClassDefault:
|
|
return StyleOverride{}, false
|
|
|
|
case LineClassTrackIncoming:
|
|
return StyleOverride{
|
|
StrokeColor: cRGBA(132, 219, 162, 255),
|
|
StrokeWidthPx: new(2.5),
|
|
}, true
|
|
|
|
case LineCLassTrackOutgoing:
|
|
return StyleOverride{
|
|
StrokeColor: cRGBA(245, 178, 120, 255),
|
|
StrokeWidthPx: new(2.5),
|
|
}, true
|
|
|
|
case LineClassMeasurement:
|
|
d := []float64{6, 4}
|
|
return StyleOverride{
|
|
StrokeColor: cRGBA(170, 175, 190, 255),
|
|
StrokeWidthPx: new(1.8),
|
|
StrokeDashes: &d,
|
|
StrokeDashOffset: new(0.),
|
|
}, true
|
|
|
|
default:
|
|
return StyleOverride{}, false
|
|
}
|
|
}
|
|
|
|
func (*DarkTheme) CircleClassOverride(class CircleClassID) (StyleOverride, bool) {
|
|
switch class {
|
|
case CircleClassDefault:
|
|
return StyleOverride{}, false
|
|
|
|
case CircleClassLocalPlanet:
|
|
return StyleOverride{
|
|
FillColor: cRGBA(155, 175, 235, 255),
|
|
StrokeColor: cRGBA(155, 175, 235, 255),
|
|
StrokeWidthPx: new(2.2),
|
|
}, true
|
|
|
|
case CircleClassOthersPlanet:
|
|
return StyleOverride{
|
|
FillColor: cRGBA(245, 178, 120, 255),
|
|
StrokeColor: cRGBA(245, 178, 120, 255),
|
|
StrokeWidthPx: new(2.2),
|
|
}, true
|
|
|
|
case CircleClassFreePlanet:
|
|
return StyleOverride{
|
|
FillColor: cRGBA(132, 219, 162, 255),
|
|
StrokeColor: cRGBA(132, 219, 162, 255),
|
|
StrokeWidthPx: new(2.2),
|
|
}, true
|
|
|
|
default:
|
|
return StyleOverride{}, false
|
|
}
|
|
}
|
|
|
|
// makeDarkBackgroundTile creates a subtle, low-contrast texture tile.
|
|
// It is intentionally simple: a faint grid + a few diagonal accents.
|
|
// The tile is meant to be repeated.
|
|
func makeDarkBackgroundTile(w, h int) image.Image {
|
|
if w <= 0 || h <= 0 {
|
|
return nil
|
|
}
|
|
img := image.NewRGBA(image.Rect(0, 0, w, h))
|
|
|
|
// Base is transparent; background color is drawn separately.
|
|
// We draw subtle strokes with low alpha.
|
|
grid := color.RGBA{R: 255, G: 255, B: 255, A: 12} // very faint
|
|
diag := color.RGBA{R: 255, G: 255, B: 255, A: 18} // slightly stronger
|
|
dots := color.RGBA{R: 255, G: 255, B: 255, A: 10} // faint dots
|
|
|
|
// Grid spacing (pixels).
|
|
const step = 12
|
|
|
|
// Vertical grid lines.
|
|
for x := 0; x < w; x += step {
|
|
for y := 0; y < h; y++ {
|
|
img.SetRGBA(x, y, grid)
|
|
}
|
|
}
|
|
// Horizontal grid lines.
|
|
for y := 0; y < h; y += step {
|
|
for x := 0; x < w; x++ {
|
|
img.SetRGBA(x, y, grid)
|
|
}
|
|
}
|
|
|
|
// Diagonal accents (sparse).
|
|
for x := 0; x < w; x += step * 2 {
|
|
for i := 0; i < step && x+i < w && i < h; i++ {
|
|
img.SetRGBA(x+i, i, diag)
|
|
}
|
|
}
|
|
|
|
// Small dot pattern.
|
|
for y := step / 2; y < h; y += step {
|
|
for x := step / 2; x < w; x += step {
|
|
img.SetRGBA(x, y, dots)
|
|
}
|
|
}
|
|
|
|
return img
|
|
}
|