373 lines
9.0 KiB
Go
373 lines
9.0 KiB
Go
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 {
|
|
// 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
|
|
}
|