package world import ( "image" "image/color" ) // NOTE: // - This file provides two sample themes: LightTheme and DarkTheme. // - LightTheme uses only BackgroundColor. // - DarkTheme uses BackgroundColor + a tiled, immutable BackgroundImage. var ( ThemeLight = &LightTheme{} ThemeDark = NewDarkTheme() ) // ----------------------------- // Helpers // ----------------------------- // cHex returns an sRGB color. Alpha is 0..255. func cRGBA(r, g, b, a uint8) color.Color { return color.RGBA{R: r, G: g, B: b, A: a} } // ----------------------------- // Light Theme (color only) // ----------------------------- 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 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 CircleClassHome: // teal-ish, a bit stronger stroke return StyleOverride{ FillColor: cRGBA(32, 161, 145, 50), StrokeColor: cRGBA(32, 161, 145, 210), StrokeWidthPx: new(2.5), }, true case CircleClassAcquired: // blue return StyleOverride{ FillColor: cRGBA(70, 108, 196, 45), StrokeColor: cRGBA(70, 108, 196, 220), StrokeWidthPx: new(2.2), }, true case CircleClassOccupied: // orange return StyleOverride{ FillColor: cRGBA(222, 142, 70, 50), StrokeColor: cRGBA(222, 142, 70, 220), StrokeWidthPx: new(2.2), }, true case CircleClassFree: // 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) // ----------------------------- type DarkTheme struct { bg image.Image } 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), // brighter teal for dark bg StrokeColor: nil, StrokeWidthPx: 0, PointRadiusPx: 3.0, } } func (*DarkTheme) LineStyle() Style { return Style{ FillColor: nil, StrokeColor: cRGBA(155, 175, 235, 220), // soft bluish StrokeWidthPx: 2.0, StrokeDashes: nil, StrokeDashOffset: 0, } } func (*DarkTheme) CircleStyle() Style { return Style{ FillColor: cRGBA(186, 160, 255, 55), // soft lavender, low alpha StrokeColor: cRGBA(186, 160, 255, 200), // soft lavender 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, 230), PointRadiusPx: new(3.0), }, true case PointClassTrackIncoming: return StyleOverride{ FillColor: cRGBA(132, 219, 162, 245), PointRadiusPx: new(3.5), }, true case PointClassTrackOutgoing: return StyleOverride{ FillColor: cRGBA(245, 178, 120, 245), PointRadiusPx: new(3.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, 220), StrokeWidthPx: new(2.5), }, true case LineCLassTrackOutgoing: return StyleOverride{ StrokeColor: cRGBA(245, 178, 120, 220), StrokeWidthPx: new(2.5), }, true case LineClassMeasurement: d := []float64{6, 4} return StyleOverride{ StrokeColor: cRGBA(170, 175, 190, 200), 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 CircleClassHome: return StyleOverride{ FillColor: cRGBA(120, 214, 198, 50), StrokeColor: cRGBA(120, 214, 198, 210), StrokeWidthPx: new(2.5), }, true case CircleClassAcquired: return StyleOverride{ FillColor: cRGBA(155, 175, 235, 45), StrokeColor: cRGBA(155, 175, 235, 220), StrokeWidthPx: new(2.2), }, true case CircleClassOccupied: return StyleOverride{ FillColor: cRGBA(245, 178, 120, 45), StrokeColor: cRGBA(245, 178, 120, 220), StrokeWidthPx: new(2.2), }, true case CircleClassFree: return StyleOverride{ FillColor: cRGBA(132, 219, 162, 45), StrokeColor: cRGBA(132, 219, 162, 220), 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 }