mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-05 21:15:38 -05:00
454 lines
13 KiB
Go
454 lines
13 KiB
Go
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
|
|
}
|