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:
453
core/internal/dank16/dank16.go
Normal file
453
core/internal/dank16/dank16.go
Normal 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
|
||||
}
|
||||
727
core/internal/dank16/dank16_test.go
Normal file
727
core/internal/dank16/dank16_test.go
Normal 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)
|
||||
}
|
||||
126
core/internal/dank16/terminals.go
Normal file
126
core/internal/dank16/terminals.go
Normal 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()
|
||||
}
|
||||
250
core/internal/dank16/vscode.go
Normal file
250
core/internal/dank16/vscode.go
Normal 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, "", " ")
|
||||
}
|
||||
Reference in New Issue
Block a user