1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-07 22:15:38 -05:00

rename backend to core

This commit is contained in:
bbedward
2025-11-12 23:12:31 -05:00
parent 0fdc0748cf
commit db584b7897
280 changed files with 265 additions and 265 deletions

View File

@@ -0,0 +1,453 @@
package dank16
import (
"fmt"
"math"
"github.com/lucasb-eyer/go-colorful"
)
type RGB struct {
R, G, B float64
}
type HSV struct {
H, S, V float64
}
func HexToRGB(hex string) RGB {
if hex[0] == '#' {
hex = hex[1:]
}
var r, g, b uint8
fmt.Sscanf(hex, "%02x%02x%02x", &r, &g, &b)
return RGB{
R: float64(r) / 255.0,
G: float64(g) / 255.0,
B: float64(b) / 255.0,
}
}
func RGBToHex(rgb RGB) string {
r := math.Max(0, math.Min(1, rgb.R))
g := math.Max(0, math.Min(1, rgb.G))
b := math.Max(0, math.Min(1, rgb.B))
return fmt.Sprintf("#%02x%02x%02x", int(r*255), int(g*255), int(b*255))
}
func RGBToHSV(rgb RGB) HSV {
max := math.Max(math.Max(rgb.R, rgb.G), rgb.B)
min := math.Min(math.Min(rgb.R, rgb.G), rgb.B)
delta := max - min
var h float64
if delta == 0 {
h = 0
} else if max == rgb.R {
h = math.Mod((rgb.G-rgb.B)/delta, 6.0) / 6.0
} else if max == rgb.G {
h = ((rgb.B-rgb.R)/delta + 2.0) / 6.0
} else {
h = ((rgb.R-rgb.G)/delta + 4.0) / 6.0
}
if h < 0 {
h += 1.0
}
var s float64
if max == 0 {
s = 0
} else {
s = delta / max
}
return HSV{H: h, S: s, V: max}
}
func HSVToRGB(hsv HSV) RGB {
h := hsv.H * 6.0
c := hsv.V * hsv.S
x := c * (1.0 - math.Abs(math.Mod(h, 2.0)-1.0))
m := hsv.V - c
var r, g, b float64
switch int(h) {
case 0:
r, g, b = c, x, 0
case 1:
r, g, b = x, c, 0
case 2:
r, g, b = 0, c, x
case 3:
r, g, b = 0, x, c
case 4:
r, g, b = x, 0, c
case 5:
r, g, b = c, 0, x
default:
r, g, b = c, 0, x
}
return RGB{R: r + m, G: g + m, B: b + m}
}
func sRGBToLinear(c float64) float64 {
if c <= 0.04045 {
return c / 12.92
}
return math.Pow((c+0.055)/1.055, 2.4)
}
func Luminance(hex string) float64 {
rgb := HexToRGB(hex)
return 0.2126*sRGBToLinear(rgb.R) + 0.7152*sRGBToLinear(rgb.G) + 0.0722*sRGBToLinear(rgb.B)
}
func ContrastRatio(hexFg, hexBg string) float64 {
lumFg := Luminance(hexFg)
lumBg := Luminance(hexBg)
lighter := math.Max(lumFg, lumBg)
darker := math.Min(lumFg, lumBg)
return (lighter + 0.05) / (darker + 0.05)
}
func getLstar(hex string) float64 {
rgb := HexToRGB(hex)
col := colorful.Color{R: rgb.R, G: rgb.G, B: rgb.B}
L, _, _ := col.Lab()
return L * 100.0 // go-colorful uses 0-1, we need 0-100 for DPS
}
// Lab to hex, clamping if needed
func labToHex(L, a, b float64) string {
c := colorful.Lab(L/100.0, a, b) // back to 0-1 for go-colorful
r, g, b2 := c.Clamped().RGB255()
return fmt.Sprintf("#%02x%02x%02x", r, g, b2)
}
// Adjust brightness while keeping the same hue
func retoneToL(hex string, Ltarget float64) string {
rgb := HexToRGB(hex)
col := colorful.Color{R: rgb.R, G: rgb.G, B: rgb.B}
L, a, b := col.Lab()
L100 := L * 100.0
scale := 1.0
if L100 != 0 {
scale = Ltarget / L100
}
a2, b2 := a*scale, b*scale
// Don't let it get too saturated
maxChroma := 0.4
if math.Hypot(a2, b2) > maxChroma {
k := maxChroma / math.Hypot(a2, b2)
a2 *= k
b2 *= k
}
return labToHex(Ltarget, a2, b2)
}
func DeltaPhiStar(hexFg, hexBg string, negativePolarity bool) float64 {
Lf := getLstar(hexFg)
Lb := getLstar(hexBg)
phi := 1.618
inv := 0.618
lc := math.Pow(math.Abs(math.Pow(Lb, phi)-math.Pow(Lf, phi)), inv)*1.414 - 40
if negativePolarity {
lc += 5
}
return lc
}
func DeltaPhiStarContrast(hexFg, hexBg string, isLightMode bool) float64 {
negativePolarity := !isLightMode
return DeltaPhiStar(hexFg, hexBg, negativePolarity)
}
func EnsureContrast(hexColor, hexBg string, minRatio float64, isLightMode bool) string {
currentRatio := ContrastRatio(hexColor, hexBg)
if currentRatio >= minRatio {
return hexColor
}
rgb := HexToRGB(hexColor)
hsv := RGBToHSV(rgb)
for step := 1; step < 30; step++ {
delta := float64(step) * 0.02
if isLightMode {
newV := math.Max(0, hsv.V-delta)
candidate := RGBToHex(HSVToRGB(HSV{H: hsv.H, S: hsv.S, V: newV}))
if ContrastRatio(candidate, hexBg) >= minRatio {
return candidate
}
newV = math.Min(1, hsv.V+delta)
candidate = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: hsv.S, V: newV}))
if ContrastRatio(candidate, hexBg) >= minRatio {
return candidate
}
} else {
newV := math.Min(1, hsv.V+delta)
candidate := RGBToHex(HSVToRGB(HSV{H: hsv.H, S: hsv.S, V: newV}))
if ContrastRatio(candidate, hexBg) >= minRatio {
return candidate
}
newV = math.Max(0, hsv.V-delta)
candidate = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: hsv.S, V: newV}))
if ContrastRatio(candidate, hexBg) >= minRatio {
return candidate
}
}
}
return hexColor
}
func EnsureContrastDPS(hexColor, hexBg string, minLc float64, isLightMode bool) string {
currentLc := DeltaPhiStarContrast(hexColor, hexBg, isLightMode)
if currentLc >= minLc {
return hexColor
}
rgb := HexToRGB(hexColor)
hsv := RGBToHSV(rgb)
for step := 1; step < 50; step++ {
delta := float64(step) * 0.015
if isLightMode {
newV := math.Max(0, hsv.V-delta)
candidate := RGBToHex(HSVToRGB(HSV{H: hsv.H, S: hsv.S, V: newV}))
if DeltaPhiStarContrast(candidate, hexBg, isLightMode) >= minLc {
return candidate
}
newV = math.Min(1, hsv.V+delta)
candidate = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: hsv.S, V: newV}))
if DeltaPhiStarContrast(candidate, hexBg, isLightMode) >= minLc {
return candidate
}
} else {
newV := math.Min(1, hsv.V+delta)
candidate := RGBToHex(HSVToRGB(HSV{H: hsv.H, S: hsv.S, V: newV}))
if DeltaPhiStarContrast(candidate, hexBg, isLightMode) >= minLc {
return candidate
}
newV = math.Max(0, hsv.V-delta)
candidate = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: hsv.S, V: newV}))
if DeltaPhiStarContrast(candidate, hexBg, isLightMode) >= minLc {
return candidate
}
}
}
return hexColor
}
// Nudge L* until contrast is good enough. Keeps hue intact unlike HSV fiddling.
func EnsureContrastDPSLstar(hexColor, hexBg string, minLc float64, isLightMode bool) string {
current := DeltaPhiStarContrast(hexColor, hexBg, isLightMode)
if current >= minLc {
return hexColor
}
fg := HexToRGB(hexColor)
cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B}
Lf, af, bf := cf.Lab()
dir := 1.0
if isLightMode {
dir = -1.0 // light mode = darker text
}
step := 0.5
for i := 0; i < 120; i++ {
Lf = math.Max(0, math.Min(100, Lf+dir*step))
cand := labToHex(Lf, af, bf)
if DeltaPhiStarContrast(cand, hexBg, isLightMode) >= minLc {
return cand
}
}
return hexColor
}
type PaletteOptions struct {
IsLight bool
Background string
UseDPS bool
}
func ensureContrastAuto(hexColor, hexBg string, target float64, opts PaletteOptions) string {
if opts.UseDPS {
return EnsureContrastDPSLstar(hexColor, hexBg, target, opts.IsLight)
}
return EnsureContrast(hexColor, hexBg, target, opts.IsLight)
}
func DeriveContainer(primary string, isLight bool) string {
rgb := HexToRGB(primary)
hsv := RGBToHSV(rgb)
if isLight {
containerV := math.Min(hsv.V*1.77, 1.0)
containerS := hsv.S * 0.32
return RGBToHex(HSVToRGB(HSV{H: hsv.H, S: containerS, V: containerV}))
}
containerV := hsv.V * 0.463
containerS := math.Min(hsv.S*1.834, 1.0)
return RGBToHex(HSVToRGB(HSV{H: hsv.H, S: containerS, V: containerV}))
}
func GeneratePalette(primaryColor string, opts PaletteOptions) []string {
baseColor := DeriveContainer(primaryColor, opts.IsLight)
rgb := HexToRGB(baseColor)
hsv := RGBToHSV(rgb)
palette := make([]string, 0, 16)
var normalTextTarget, secondaryTarget float64
if opts.UseDPS {
normalTextTarget = 40.0
secondaryTarget = 35.0
} else {
normalTextTarget = 4.5
secondaryTarget = 3.0
}
var bgColor string
if opts.Background != "" {
bgColor = opts.Background
} else if opts.IsLight {
bgColor = "#f8f8f8"
} else {
bgColor = "#1a1a1a"
}
palette = append(palette, bgColor)
hueShift := (hsv.H - 0.6) * 0.12
satBoost := 1.15
redH := math.Mod(0.0+hueShift+1.0, 1.0)
var redColor string
if opts.IsLight {
redColor = RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.80*satBoost, 1.0), V: 0.55}))
palette = append(palette, ensureContrastAuto(redColor, bgColor, normalTextTarget, opts))
} else {
redColor = RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.65*satBoost, 1.0), V: 0.80}))
palette = append(palette, ensureContrastAuto(redColor, bgColor, normalTextTarget, opts))
}
greenH := math.Mod(0.33+hueShift+1.0, 1.0)
var greenColor string
if opts.IsLight {
greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(math.Max(hsv.S*0.9, 0.80)*satBoost, 1.0), V: 0.45}))
palette = append(palette, ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
} else {
greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(0.42*satBoost, 1.0), V: 0.84}))
palette = append(palette, ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
}
yellowH := math.Mod(0.15+hueShift+1.0, 1.0)
var yellowColor string
if opts.IsLight {
yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.75*satBoost, 1.0), V: 0.50}))
palette = append(palette, ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts))
} else {
yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.38*satBoost, 1.0), V: 0.86}))
palette = append(palette, ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts))
}
var blueColor string
if opts.IsLight {
blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.9, 0.7), V: hsv.V * 1.1}))
palette = append(palette, ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
} else {
blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.8, 0.6), V: math.Min(hsv.V*1.6, 1.0)}))
palette = append(palette, ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
}
magH := hsv.H - 0.03
if magH < 0 {
magH += 1.0
}
var magColor string
hr := HexToRGB(primaryColor)
hh := RGBToHSV(hr)
if opts.IsLight {
magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Max(hh.S*0.9, 0.7), V: hh.V * 0.85}))
palette = append(palette, ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
} else {
magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: hh.S * 0.8, V: hh.V * 0.75}))
palette = append(palette, ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
}
cyanH := hsv.H + 0.08
if cyanH > 1.0 {
cyanH -= 1.0
}
palette = append(palette, ensureContrastAuto(primaryColor, bgColor, normalTextTarget, opts))
if opts.IsLight {
palette = append(palette, "#1a1a1a")
palette = append(palette, "#2e2e2e")
} else {
palette = append(palette, "#abb2bf")
palette = append(palette, "#5c6370")
}
if opts.IsLight {
brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.70*satBoost, 1.0), V: 0.65}))
palette = append(palette, ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts))
brightGreen := RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(math.Max(hsv.S*0.85, 0.75)*satBoost, 1.0), V: 0.55}))
palette = append(palette, ensureContrastAuto(brightGreen, bgColor, secondaryTarget, opts))
brightYellow := RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.68*satBoost, 1.0), V: 0.60}))
palette = append(palette, ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts))
hr := HexToRGB(primaryColor)
hh := RGBToHSV(hr)
brightBlue := RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Min(hh.S*1.1, 1.0), V: math.Min(hh.V*1.2, 1.0)}))
palette = append(palette, ensureContrastAuto(brightBlue, bgColor, secondaryTarget, opts))
brightMag := RGBToHex(HSVToRGB(HSV{H: magH, S: math.Max(hsv.S*0.9, 0.75), V: math.Min(hsv.V*1.25, 1.0)}))
palette = append(palette, ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts))
brightCyan := RGBToHex(HSVToRGB(HSV{H: cyanH, S: math.Max(hsv.S*0.75, 0.65), V: math.Min(hsv.V*1.25, 1.0)}))
palette = append(palette, ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
} else {
brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.50*satBoost, 1.0), V: 0.88}))
palette = append(palette, ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts))
brightGreen := RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(0.35*satBoost, 1.0), V: 0.88}))
palette = append(palette, ensureContrastAuto(brightGreen, bgColor, secondaryTarget, opts))
brightYellow := RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.30*satBoost, 1.0), V: 0.91}))
palette = append(palette, ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts))
// Make it way brighter for type names in dark mode
brightBlue := retoneToL(primaryColor, 85.0)
palette = append(palette, brightBlue)
brightMag := RGBToHex(HSVToRGB(HSV{H: magH, S: math.Max(hsv.S*0.7, 0.6), V: math.Min(hsv.V*1.3, 0.9)}))
palette = append(palette, ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts))
brightCyanH := hsv.H + 0.02
if brightCyanH > 1.0 {
brightCyanH -= 1.0
}
brightCyan := RGBToHex(HSVToRGB(HSV{H: brightCyanH, S: math.Max(hsv.S*0.6, 0.5), V: math.Min(hsv.V*1.2, 0.85)}))
palette = append(palette, ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
}
if opts.IsLight {
palette = append(palette, "#1a1a1a")
} else {
palette = append(palette, "#ffffff")
}
return palette
}

View File

@@ -0,0 +1,727 @@
package dank16
import (
"encoding/json"
"math"
"testing"
)
func TestHexToRGB(t *testing.T) {
tests := []struct {
name string
input string
expected RGB
}{
{
name: "black with hash",
input: "#000000",
expected: RGB{R: 0.0, G: 0.0, B: 0.0},
},
{
name: "white with hash",
input: "#ffffff",
expected: RGB{R: 1.0, G: 1.0, B: 1.0},
},
{
name: "red without hash",
input: "ff0000",
expected: RGB{R: 1.0, G: 0.0, B: 0.0},
},
{
name: "purple",
input: "#625690",
expected: RGB{R: 0.3843137254901961, G: 0.33725490196078434, B: 0.5647058823529412},
},
{
name: "mid gray",
input: "#808080",
expected: RGB{R: 0.5019607843137255, G: 0.5019607843137255, B: 0.5019607843137255},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := HexToRGB(tt.input)
if !floatEqual(result.R, tt.expected.R) || !floatEqual(result.G, tt.expected.G) || !floatEqual(result.B, tt.expected.B) {
t.Errorf("HexToRGB(%s) = %v, expected %v", tt.input, result, tt.expected)
}
})
}
}
func TestRGBToHex(t *testing.T) {
tests := []struct {
name string
input RGB
expected string
}{
{
name: "black",
input: RGB{R: 0.0, G: 0.0, B: 0.0},
expected: "#000000",
},
{
name: "white",
input: RGB{R: 1.0, G: 1.0, B: 1.0},
expected: "#ffffff",
},
{
name: "red",
input: RGB{R: 1.0, G: 0.0, B: 0.0},
expected: "#ff0000",
},
{
name: "clamping above 1.0",
input: RGB{R: 1.5, G: 0.5, B: 0.5},
expected: "#ff7f7f",
},
{
name: "clamping below 0.0",
input: RGB{R: -0.5, G: 0.5, B: 0.5},
expected: "#007f7f",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := RGBToHex(tt.input)
if result != tt.expected {
t.Errorf("RGBToHex(%v) = %s, expected %s", tt.input, result, tt.expected)
}
})
}
}
func TestRGBToHSV(t *testing.T) {
tests := []struct {
name string
input RGB
expected HSV
}{
{
name: "black",
input: RGB{R: 0.0, G: 0.0, B: 0.0},
expected: HSV{H: 0.0, S: 0.0, V: 0.0},
},
{
name: "white",
input: RGB{R: 1.0, G: 1.0, B: 1.0},
expected: HSV{H: 0.0, S: 0.0, V: 1.0},
},
{
name: "red",
input: RGB{R: 1.0, G: 0.0, B: 0.0},
expected: HSV{H: 0.0, S: 1.0, V: 1.0},
},
{
name: "green",
input: RGB{R: 0.0, G: 1.0, B: 0.0},
expected: HSV{H: 0.3333333333333333, S: 1.0, V: 1.0},
},
{
name: "blue",
input: RGB{R: 0.0, G: 0.0, B: 1.0},
expected: HSV{H: 0.6666666666666666, S: 1.0, V: 1.0},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := RGBToHSV(tt.input)
if !floatEqual(result.H, tt.expected.H) || !floatEqual(result.S, tt.expected.S) || !floatEqual(result.V, tt.expected.V) {
t.Errorf("RGBToHSV(%v) = %v, expected %v", tt.input, result, tt.expected)
}
})
}
}
func TestHSVToRGB(t *testing.T) {
tests := []struct {
name string
input HSV
expected RGB
}{
{
name: "black",
input: HSV{H: 0.0, S: 0.0, V: 0.0},
expected: RGB{R: 0.0, G: 0.0, B: 0.0},
},
{
name: "white",
input: HSV{H: 0.0, S: 0.0, V: 1.0},
expected: RGB{R: 1.0, G: 1.0, B: 1.0},
},
{
name: "red",
input: HSV{H: 0.0, S: 1.0, V: 1.0},
expected: RGB{R: 1.0, G: 0.0, B: 0.0},
},
{
name: "green",
input: HSV{H: 0.3333333333333333, S: 1.0, V: 1.0},
expected: RGB{R: 0.0, G: 1.0, B: 0.0},
},
{
name: "blue",
input: HSV{H: 0.6666666666666666, S: 1.0, V: 1.0},
expected: RGB{R: 0.0, G: 0.0, B: 1.0},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := HSVToRGB(tt.input)
if !floatEqual(result.R, tt.expected.R) || !floatEqual(result.G, tt.expected.G) || !floatEqual(result.B, tt.expected.B) {
t.Errorf("HSVToRGB(%v) = %v, expected %v", tt.input, result, tt.expected)
}
})
}
}
func TestLuminance(t *testing.T) {
tests := []struct {
name string
input string
expected float64
}{
{
name: "black",
input: "#000000",
expected: 0.0,
},
{
name: "white",
input: "#ffffff",
expected: 1.0,
},
{
name: "red",
input: "#ff0000",
expected: 0.2126,
},
{
name: "green",
input: "#00ff00",
expected: 0.7152,
},
{
name: "blue",
input: "#0000ff",
expected: 0.0722,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Luminance(tt.input)
if !floatEqual(result, tt.expected) {
t.Errorf("Luminance(%s) = %f, expected %f", tt.input, result, tt.expected)
}
})
}
}
func TestContrastRatio(t *testing.T) {
tests := []struct {
name string
fg string
bg string
expected float64
}{
{
name: "black on white",
fg: "#000000",
bg: "#ffffff",
expected: 21.0,
},
{
name: "white on black",
fg: "#ffffff",
bg: "#000000",
expected: 21.0,
},
{
name: "same color",
fg: "#808080",
bg: "#808080",
expected: 1.0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ContrastRatio(tt.fg, tt.bg)
if !floatEqual(result, tt.expected) {
t.Errorf("ContrastRatio(%s, %s) = %f, expected %f", tt.fg, tt.bg, result, tt.expected)
}
})
}
}
func TestEnsureContrast(t *testing.T) {
tests := []struct {
name string
color string
bg string
minRatio float64
isLightMode bool
}{
{
name: "already sufficient contrast dark mode",
color: "#ffffff",
bg: "#000000",
minRatio: 4.5,
isLightMode: false,
},
{
name: "already sufficient contrast light mode",
color: "#000000",
bg: "#ffffff",
minRatio: 4.5,
isLightMode: true,
},
{
name: "needs adjustment dark mode",
color: "#404040",
bg: "#1a1a1a",
minRatio: 4.5,
isLightMode: false,
},
{
name: "needs adjustment light mode",
color: "#c0c0c0",
bg: "#f8f8f8",
minRatio: 4.5,
isLightMode: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := EnsureContrast(tt.color, tt.bg, tt.minRatio, tt.isLightMode)
actualRatio := ContrastRatio(result, tt.bg)
if actualRatio < tt.minRatio {
t.Errorf("EnsureContrast(%s, %s, %f, %t) = %s with ratio %f, expected ratio >= %f",
tt.color, tt.bg, tt.minRatio, tt.isLightMode, result, actualRatio, tt.minRatio)
}
})
}
}
func TestGeneratePalette(t *testing.T) {
tests := []struct {
name string
base string
opts PaletteOptions
}{
{
name: "dark theme default",
base: "#625690",
opts: PaletteOptions{IsLight: false},
},
{
name: "light theme default",
base: "#625690",
opts: PaletteOptions{IsLight: true},
},
{
name: "light theme with custom background",
base: "#625690",
opts: PaletteOptions{
IsLight: true,
Background: "#fafafa",
},
},
{
name: "dark theme with custom background",
base: "#625690",
opts: PaletteOptions{
IsLight: false,
Background: "#0a0a0a",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := GeneratePalette(tt.base, tt.opts)
if len(result) != 16 {
t.Errorf("GeneratePalette returned %d colors, expected 16", len(result))
}
for i, color := range result {
if len(color) != 7 || color[0] != '#' {
t.Errorf("Color at index %d (%s) is not a valid hex color", i, color)
}
}
if tt.opts.Background != "" && result[0] != tt.opts.Background {
t.Errorf("Background color = %s, expected %s", result[0], tt.opts.Background)
} else if !tt.opts.IsLight && tt.opts.Background == "" && result[0] != "#1a1a1a" {
t.Errorf("Dark mode background = %s, expected #1a1a1a", result[0])
} else if tt.opts.IsLight && tt.opts.Background == "" && result[0] != "#f8f8f8" {
t.Errorf("Light mode background = %s, expected #f8f8f8", result[0])
}
if tt.opts.IsLight && result[15] != "#1a1a1a" {
t.Errorf("Light mode foreground = %s, expected #1a1a1a", result[15])
} else if !tt.opts.IsLight && result[15] != "#ffffff" {
t.Errorf("Dark mode foreground = %s, expected #ffffff", result[15])
}
})
}
}
func TestEnrichVSCodeTheme(t *testing.T) {
colors := GeneratePalette("#625690", PaletteOptions{IsLight: false})
baseTheme := map[string]interface{}{
"name": "Test Theme",
"type": "dark",
"colors": map[string]interface{}{
"editor.background": "#000000",
},
}
themeJSON, err := json.Marshal(baseTheme)
if err != nil {
t.Fatalf("Failed to marshal base theme: %v", err)
}
result, err := EnrichVSCodeTheme(themeJSON, colors)
if err != nil {
t.Fatalf("EnrichVSCodeTheme failed: %v", err)
}
var enriched map[string]interface{}
if err := json.Unmarshal(result, &enriched); err != nil {
t.Fatalf("Failed to unmarshal result: %v", err)
}
colorsMap, ok := enriched["colors"].(map[string]interface{})
if !ok {
t.Fatal("colors is not a map")
}
terminalColors := []string{
"terminal.ansiBlack",
"terminal.ansiRed",
"terminal.ansiGreen",
"terminal.ansiYellow",
"terminal.ansiBlue",
"terminal.ansiMagenta",
"terminal.ansiCyan",
"terminal.ansiWhite",
"terminal.ansiBrightBlack",
"terminal.ansiBrightRed",
"terminal.ansiBrightGreen",
"terminal.ansiBrightYellow",
"terminal.ansiBrightBlue",
"terminal.ansiBrightMagenta",
"terminal.ansiBrightCyan",
"terminal.ansiBrightWhite",
}
for i, key := range terminalColors {
if val, ok := colorsMap[key]; !ok {
t.Errorf("Missing terminal color: %s", key)
} else if val != colors[i] {
t.Errorf("%s = %s, expected %s", key, val, colors[i])
}
}
if colorsMap["editor.background"] != "#000000" {
t.Error("Original theme colors should be preserved")
}
}
func TestEnrichVSCodeThemeInvalidJSON(t *testing.T) {
colors := GeneratePalette("#625690", PaletteOptions{IsLight: false})
invalidJSON := []byte("{invalid json")
_, err := EnrichVSCodeTheme(invalidJSON, colors)
if err == nil {
t.Error("Expected error for invalid JSON, got nil")
}
}
func TestRoundTripConversion(t *testing.T) {
testColors := []string{"#000000", "#ffffff", "#ff0000", "#00ff00", "#0000ff", "#625690", "#808080"}
for _, hex := range testColors {
t.Run(hex, func(t *testing.T) {
rgb := HexToRGB(hex)
result := RGBToHex(rgb)
if result != hex {
t.Errorf("Round trip %s -> RGB -> %s failed", hex, result)
}
})
}
}
func TestRGBHSVRoundTrip(t *testing.T) {
testCases := []RGB{
{R: 0.0, G: 0.0, B: 0.0},
{R: 1.0, G: 1.0, B: 1.0},
{R: 1.0, G: 0.0, B: 0.0},
{R: 0.0, G: 1.0, B: 0.0},
{R: 0.0, G: 0.0, B: 1.0},
{R: 0.5, G: 0.5, B: 0.5},
{R: 0.3843137254901961, G: 0.33725490196078434, B: 0.5647058823529412},
}
for _, rgb := range testCases {
t.Run("", func(t *testing.T) {
hsv := RGBToHSV(rgb)
result := HSVToRGB(hsv)
if !floatEqual(result.R, rgb.R) || !floatEqual(result.G, rgb.G) || !floatEqual(result.B, rgb.B) {
t.Errorf("Round trip RGB->HSV->RGB failed: %v -> %v -> %v", rgb, hsv, result)
}
})
}
}
func floatEqual(a, b float64) bool {
return math.Abs(a-b) < 1e-9
}
func TestDeltaPhiStar(t *testing.T) {
tests := []struct {
name string
fg string
bg string
negativePolarity bool
minExpected float64
}{
{
name: "white on black (negative polarity)",
fg: "#ffffff",
bg: "#000000",
negativePolarity: true,
minExpected: 100.0,
},
{
name: "black on white (positive polarity)",
fg: "#000000",
bg: "#ffffff",
negativePolarity: false,
minExpected: 100.0,
},
{
name: "low contrast same color",
fg: "#808080",
bg: "#808080",
negativePolarity: false,
minExpected: -40.0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := DeltaPhiStar(tt.fg, tt.bg, tt.negativePolarity)
if result < tt.minExpected {
t.Errorf("DeltaPhiStar(%s, %s, %v) = %f, expected >= %f",
tt.fg, tt.bg, tt.negativePolarity, result, tt.minExpected)
}
})
}
}
func TestDeltaPhiStarContrast(t *testing.T) {
tests := []struct {
name string
fg string
bg string
isLightMode bool
minExpected float64
}{
{
name: "white on black (dark mode)",
fg: "#ffffff",
bg: "#000000",
isLightMode: false,
minExpected: 100.0,
},
{
name: "black on white (light mode)",
fg: "#000000",
bg: "#ffffff",
isLightMode: true,
minExpected: 100.0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := DeltaPhiStarContrast(tt.fg, tt.bg, tt.isLightMode)
if result < tt.minExpected {
t.Errorf("DeltaPhiStarContrast(%s, %s, %v) = %f, expected >= %f",
tt.fg, tt.bg, tt.isLightMode, result, tt.minExpected)
}
})
}
}
func TestEnsureContrastDPS(t *testing.T) {
tests := []struct {
name string
color string
bg string
minLc float64
isLightMode bool
}{
{
name: "already sufficient contrast dark mode",
color: "#ffffff",
bg: "#000000",
minLc: 60.0,
isLightMode: false,
},
{
name: "already sufficient contrast light mode",
color: "#000000",
bg: "#ffffff",
minLc: 60.0,
isLightMode: true,
},
{
name: "needs adjustment dark mode",
color: "#404040",
bg: "#1a1a1a",
minLc: 60.0,
isLightMode: false,
},
{
name: "needs adjustment light mode",
color: "#c0c0c0",
bg: "#f8f8f8",
minLc: 60.0,
isLightMode: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := EnsureContrastDPS(tt.color, tt.bg, tt.minLc, tt.isLightMode)
actualLc := DeltaPhiStarContrast(result, tt.bg, tt.isLightMode)
if actualLc < tt.minLc {
t.Errorf("EnsureContrastDPS(%s, %s, %f, %t) = %s with Lc %f, expected Lc >= %f",
tt.color, tt.bg, tt.minLc, tt.isLightMode, result, actualLc, tt.minLc)
}
})
}
}
func TestGeneratePaletteWithDPS(t *testing.T) {
tests := []struct {
name string
base string
opts PaletteOptions
}{
{
name: "dark theme with DPS",
base: "#625690",
opts: PaletteOptions{IsLight: false, UseDPS: true},
},
{
name: "light theme with DPS",
base: "#625690",
opts: PaletteOptions{IsLight: true, UseDPS: true},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := GeneratePalette(tt.base, tt.opts)
if len(result) != 16 {
t.Errorf("GeneratePalette returned %d colors, expected 16", len(result))
}
for i, color := range result {
if len(color) != 7 || color[0] != '#' {
t.Errorf("Color at index %d (%s) is not a valid hex color", i, color)
}
}
bgColor := result[0]
for i := 1; i < 8; i++ {
lc := DeltaPhiStarContrast(result[i], bgColor, tt.opts.IsLight)
minLc := 30.0
if lc < minLc && lc > 0 {
t.Errorf("Color %d (%s) has insufficient DPS contrast %f with background %s (expected >= %f)",
i, result[i], lc, bgColor, minLc)
}
}
})
}
}
func TestDeriveContainer(t *testing.T) {
tests := []struct {
name string
primary string
isLight bool
expected string
}{
{
name: "dark mode",
primary: "#ccbdff",
isLight: false,
expected: "#4a3e76",
},
{
name: "light mode",
primary: "#625690",
isLight: true,
expected: "#e7deff",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := DeriveContainer(tt.primary, tt.isLight)
resultRGB := HexToRGB(result)
expectedRGB := HexToRGB(tt.expected)
rDiff := math.Abs(resultRGB.R - expectedRGB.R)
gDiff := math.Abs(resultRGB.G - expectedRGB.G)
bDiff := math.Abs(resultRGB.B - expectedRGB.B)
tolerance := 0.02
if rDiff > tolerance || gDiff > tolerance || bDiff > tolerance {
t.Errorf("DeriveContainer(%s, %v) = %s, expected %s (RGB diff: R:%.4f G:%.4f B:%.4f)",
tt.primary, tt.isLight, result, tt.expected, rDiff, gDiff, bDiff)
}
})
}
}
func TestContrastAlgorithmComparison(t *testing.T) {
base := "#625690"
optsWCAG := PaletteOptions{IsLight: false, UseDPS: false}
optsDPS := PaletteOptions{IsLight: false, UseDPS: true}
paletteWCAG := GeneratePalette(base, optsWCAG)
paletteDPS := GeneratePalette(base, optsDPS)
if len(paletteWCAG) != 16 || len(paletteDPS) != 16 {
t.Fatal("Both palettes should have 16 colors")
}
if paletteWCAG[0] != paletteDPS[0] {
t.Errorf("Background colors differ: WCAG=%s, DPS=%s", paletteWCAG[0], paletteDPS[0])
}
differentCount := 0
for i := 0; i < 16; i++ {
if paletteWCAG[i] != paletteDPS[i] {
differentCount++
}
}
t.Logf("WCAG and DPS palettes differ in %d/16 colors", differentCount)
}

View File

@@ -0,0 +1,126 @@
package dank16
import (
"encoding/json"
"fmt"
"strings"
)
func GenerateJSON(colors []string) string {
colorMap := make(map[string]string)
for i, color := range colors {
colorMap[fmt.Sprintf("color%d", i)] = color
}
marshalled, _ := json.Marshal(colorMap)
return string(marshalled)
}
func GenerateKittyTheme(colors []string) string {
kittyColors := []struct {
name string
index int
}{
{"color0", 0},
{"color1", 1},
{"color2", 2},
{"color3", 3},
{"color4", 4},
{"color5", 5},
{"color6", 6},
{"color7", 7},
{"color8", 8},
{"color9", 9},
{"color10", 10},
{"color11", 11},
{"color12", 12},
{"color13", 13},
{"color14", 14},
{"color15", 15},
}
var result strings.Builder
for _, kc := range kittyColors {
fmt.Fprintf(&result, "%s %s\n", kc.name, colors[kc.index])
}
return result.String()
}
func GenerateFootTheme(colors []string) string {
footColors := []struct {
name string
index int
}{
{"regular0", 0},
{"regular1", 1},
{"regular2", 2},
{"regular3", 3},
{"regular4", 4},
{"regular5", 5},
{"regular6", 6},
{"regular7", 7},
{"bright0", 8},
{"bright1", 9},
{"bright2", 10},
{"bright3", 11},
{"bright4", 12},
{"bright5", 13},
{"bright6", 14},
{"bright7", 15},
}
var result strings.Builder
for _, fc := range footColors {
fmt.Fprintf(&result, "%s=%s\n", fc.name, strings.TrimPrefix(colors[fc.index], "#"))
}
return result.String()
}
func GenerateAlacrittyTheme(colors []string) string {
alacrittyColors := []struct {
section string
name string
index int
}{
{"normal", "black", 0},
{"normal", "red", 1},
{"normal", "green", 2},
{"normal", "yellow", 3},
{"normal", "blue", 4},
{"normal", "magenta", 5},
{"normal", "cyan", 6},
{"normal", "white", 7},
{"bright", "black", 8},
{"bright", "red", 9},
{"bright", "green", 10},
{"bright", "yellow", 11},
{"bright", "blue", 12},
{"bright", "magenta", 13},
{"bright", "cyan", 14},
{"bright", "white", 15},
}
var result strings.Builder
currentSection := ""
for _, ac := range alacrittyColors {
if ac.section != currentSection {
if currentSection != "" {
result.WriteString("\n")
}
fmt.Fprintf(&result, "[colors.%s]\n", ac.section)
currentSection = ac.section
}
fmt.Fprintf(&result, "%-7s = '%s'\n", ac.name, colors[ac.index])
}
return result.String()
}
func GenerateGhosttyTheme(colors []string) string {
var result strings.Builder
for i, color := range colors {
fmt.Fprintf(&result, "palette = %d=%s\n", i, color)
}
return result.String()
}

View File

@@ -0,0 +1,250 @@
package dank16
import (
"encoding/json"
"fmt"
)
type VSCodeTheme struct {
Schema string `json:"$schema"`
Name string `json:"name"`
Type string `json:"type"`
Colors map[string]string `json:"colors"`
TokenColors []VSCodeTokenColor `json:"tokenColors"`
SemanticHighlighting bool `json:"semanticHighlighting"`
SemanticTokenColors map[string]VSCodeTokenSetting `json:"semanticTokenColors"`
}
type VSCodeTokenColor struct {
Scope interface{} `json:"scope"`
Settings VSCodeTokenSetting `json:"settings"`
}
type VSCodeTokenSetting struct {
Foreground string `json:"foreground,omitempty"`
FontStyle string `json:"fontStyle,omitempty"`
}
func updateTokenColor(tc interface{}, scopeToColor map[string]string) {
tcMap, ok := tc.(map[string]interface{})
if !ok {
return
}
scopes, ok := tcMap["scope"].([]interface{})
if !ok {
return
}
settings, ok := tcMap["settings"].(map[string]interface{})
if !ok {
return
}
isYaml := hasScopeContaining(scopes, "yaml")
for _, scope := range scopes {
scopeStr, ok := scope.(string)
if !ok {
continue
}
if scopeStr == "string" && isYaml {
continue
}
if applyColorToScope(settings, scope, scopeToColor) {
break
}
}
}
func applyColorToScope(settings map[string]interface{}, scope interface{}, scopeToColor map[string]string) bool {
scopeStr, ok := scope.(string)
if !ok {
return false
}
newColor, exists := scopeToColor[scopeStr]
if !exists {
return false
}
settings["foreground"] = newColor
return true
}
func hasScopeContaining(scopes []interface{}, substring string) bool {
for _, scope := range scopes {
scopeStr, ok := scope.(string)
if !ok {
continue
}
for i := 0; i <= len(scopeStr)-len(substring); i++ {
if scopeStr[i:i+len(substring)] == substring {
return true
}
}
}
return false
}
func EnrichVSCodeTheme(themeData []byte, colors []string) ([]byte, error) {
var theme map[string]interface{}
if err := json.Unmarshal(themeData, &theme); err != nil {
return nil, err
}
colorsMap, ok := theme["colors"].(map[string]interface{})
if !ok {
colorsMap = make(map[string]interface{})
theme["colors"] = colorsMap
}
bg := colors[0]
isLight := false
if len(bg) == 7 && bg[0] == '#' {
r, g, b := 0, 0, 0
fmt.Sscanf(bg[1:], "%02x%02x%02x", &r, &g, &b)
luminance := (0.299*float64(r) + 0.587*float64(g) + 0.114*float64(b)) / 255.0
isLight = luminance > 0.5
}
if isLight {
theme["type"] = "light"
} else {
theme["type"] = "dark"
}
colorsMap["terminal.ansiBlack"] = colors[0]
colorsMap["terminal.ansiRed"] = colors[1]
colorsMap["terminal.ansiGreen"] = colors[2]
colorsMap["terminal.ansiYellow"] = colors[3]
colorsMap["terminal.ansiBlue"] = colors[4]
colorsMap["terminal.ansiMagenta"] = colors[5]
colorsMap["terminal.ansiCyan"] = colors[6]
colorsMap["terminal.ansiWhite"] = colors[7]
colorsMap["terminal.ansiBrightBlack"] = colors[8]
colorsMap["terminal.ansiBrightRed"] = colors[9]
colorsMap["terminal.ansiBrightGreen"] = colors[10]
colorsMap["terminal.ansiBrightYellow"] = colors[11]
colorsMap["terminal.ansiBrightBlue"] = colors[12]
colorsMap["terminal.ansiBrightMagenta"] = colors[13]
colorsMap["terminal.ansiBrightCyan"] = colors[14]
colorsMap["terminal.ansiBrightWhite"] = colors[15]
tokenColors, ok := theme["tokenColors"].([]interface{})
if ok {
scopeToColor := map[string]string{
"comment": colors[8],
"punctuation.definition.comment": colors[8],
"keyword": colors[5],
"storage.type": colors[13],
"storage.modifier": colors[5],
"variable": colors[15],
"variable.parameter": colors[7],
"meta.object-literal.key": colors[4],
"meta.property.object": colors[4],
"variable.other.property": colors[4],
"constant.other.symbol": colors[12],
"constant.numeric": colors[12],
"constant.language": colors[12],
"constant.character": colors[3],
"entity.name.type": colors[12],
"support.type": colors[13],
"entity.name.class": colors[12],
"entity.name.function": colors[2],
"support.function": colors[2],
"support.class": colors[15],
"support.variable": colors[15],
"variable.language": colors[12],
"entity.name.tag.yaml": colors[12],
"string.unquoted.plain.out.yaml": colors[15],
"string.unquoted.yaml": colors[15],
"string": colors[3],
}
for i, tc := range tokenColors {
updateTokenColor(tc, scopeToColor)
tokenColors[i] = tc
}
yamlRules := []VSCodeTokenColor{
{
Scope: "entity.name.tag.yaml",
Settings: VSCodeTokenSetting{Foreground: colors[12]},
},
{
Scope: []string{"string.unquoted.plain.out.yaml", "string.unquoted.yaml"},
Settings: VSCodeTokenSetting{Foreground: colors[15]},
},
}
for _, rule := range yamlRules {
tokenColors = append(tokenColors, rule)
}
theme["tokenColors"] = tokenColors
}
if semanticTokenColors, ok := theme["semanticTokenColors"].(map[string]interface{}); ok {
updates := map[string]string{
"variable": colors[15],
"variable.readonly": colors[12],
"property": colors[4],
"function": colors[2],
"method": colors[2],
"type": colors[12],
"class": colors[12],
"typeParameter": colors[13],
"enumMember": colors[12],
"string": colors[3],
"number": colors[12],
"comment": colors[8],
"keyword": colors[5],
"operator": colors[15],
"parameter": colors[7],
"namespace": colors[15],
}
for key, color := range updates {
if existing, ok := semanticTokenColors[key].(map[string]interface{}); ok {
existing["foreground"] = color
} else {
semanticTokenColors[key] = map[string]interface{}{
"foreground": color,
}
}
}
} else {
semanticTokenColors := make(map[string]interface{})
updates := map[string]string{
"variable": colors[7],
"variable.readonly": colors[12],
"property": colors[4],
"function": colors[2],
"method": colors[2],
"type": colors[12],
"class": colors[12],
"typeParameter": colors[13],
"enumMember": colors[12],
"string": colors[3],
"number": colors[12],
"comment": colors[8],
"keyword": colors[5],
"operator": colors[15],
"parameter": colors[7],
"namespace": colors[15],
}
for key, color := range updates {
semanticTokenColors[key] = map[string]interface{}{
"foreground": color,
}
}
theme["semanticTokenColors"] = semanticTokenColors
}
return json.MarshalIndent(theme, "", " ")
}