mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-23 19:54:02 -04:00
dank16: use the real primary_container and fix light-mode derivation
fixes #3405 fixes #3406 ref #3408 port 1.6
This commit is contained in:
@@ -79,11 +79,11 @@ func runDank16(cmd *cobra.Command, args []string) {
|
||||
primaryLight = primaryColor
|
||||
}
|
||||
variantOpts := dank16.VariantOptions{
|
||||
PrimaryDark: primaryDark,
|
||||
PrimaryLight: primaryLight,
|
||||
Background: background,
|
||||
UseDPS: contrastAlgo == "dps",
|
||||
IsLightMode: isLight,
|
||||
PrimaryDark: primaryDark,
|
||||
PrimaryLight: primaryLight,
|
||||
BackgroundDark: background,
|
||||
UseDPS: contrastAlgo == "dps",
|
||||
IsLightMode: isLight,
|
||||
}
|
||||
variantColors := dank16.GenerateVariantPalette(variantOpts)
|
||||
fmt.Print(dank16.GenerateVariantJSON(variantColors))
|
||||
|
||||
@@ -455,6 +455,7 @@ func EnsureContrastDPSBidirectional(hexColor, hexBg string, minLc float64, isLig
|
||||
type PaletteOptions struct {
|
||||
IsLight bool
|
||||
Background string
|
||||
Container string
|
||||
UseDPS bool
|
||||
}
|
||||
|
||||
@@ -520,8 +521,18 @@ func DeriveContainer(primary string, isLight bool) string {
|
||||
return RGBToHex(HSVToRGB(HSV{H: hsv.H, S: containerS, V: containerV}))
|
||||
}
|
||||
|
||||
// DeriveContainer only approximates the Material tone pairing, so a caller that
|
||||
// has the real primary_container should pass it: color5 and color6 are the
|
||||
// primary/container pair and their contrast with each other is the invariant.
|
||||
func resolveContainer(primaryColor string, opts PaletteOptions) string {
|
||||
if opts.Container != "" {
|
||||
return opts.Container
|
||||
}
|
||||
return DeriveContainer(primaryColor, opts.IsLight)
|
||||
}
|
||||
|
||||
func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
||||
baseColor := DeriveContainer(primaryColor, opts.IsLight)
|
||||
baseColor := resolveContainer(primaryColor, opts)
|
||||
|
||||
rgb := HexToRGB(baseColor)
|
||||
hsv := RGBToHSV(rgb)
|
||||
@@ -531,13 +542,20 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
||||
|
||||
var palette Palette
|
||||
|
||||
var normalTextTarget, secondaryTarget float64
|
||||
if opts.UseDPS {
|
||||
// Delta Phi Star is not symmetric across polarity: on a light background the
|
||||
// same Lc buys far less real contrast, so the light targets are the numbers
|
||||
// that land on the same measured WCAG ratios the dark ones already deliver.
|
||||
var normalTextTarget, accentTarget float64
|
||||
switch {
|
||||
case opts.UseDPS && opts.IsLight:
|
||||
normalTextTarget = 70.0
|
||||
accentTarget = 58.0
|
||||
case opts.UseDPS:
|
||||
normalTextTarget = 40.0
|
||||
secondaryTarget = 35.0
|
||||
} else {
|
||||
accentTarget = 24.5
|
||||
default:
|
||||
normalTextTarget = 4.5
|
||||
secondaryTarget = 3.0
|
||||
accentTarget = 2.1
|
||||
}
|
||||
|
||||
var bgColor string
|
||||
@@ -557,8 +575,6 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
||||
greenH := blendHue(0.33, ph.H, 0.10)
|
||||
yellowH := blendHue(0.14, ph.H, 0.04)
|
||||
|
||||
accentTarget := secondaryTarget * 0.7
|
||||
|
||||
if opts.IsLight {
|
||||
redS := math.Min(baseSat*1.2, 1.0)
|
||||
redV := baseVal * 0.95
|
||||
@@ -570,15 +586,13 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
||||
|
||||
yellowS := math.Min(baseSat*1.5, 1.0)
|
||||
yellowV := math.Min(baseVal*1.2, 1.0)
|
||||
palette.Color3 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: yellowH, S: yellowS, V: yellowV})), bgColor, accentTarget, opts))
|
||||
palette.Color3 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: yellowH, S: yellowS, V: yellowV})), bgColor, normalTextTarget, opts))
|
||||
|
||||
blueS := math.Min(ph.S*1.05, 1.0)
|
||||
blueV := math.Min(ph.V*1.05, 1.0)
|
||||
palette.Color4 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: ph.H, S: blueS, V: blueV})), bgColor, normalTextTarget, opts))
|
||||
|
||||
// Color5 matches primary_container exactly (light container in light mode)
|
||||
container5 := DeriveContainer(primaryColor, true)
|
||||
palette.Color5 = NewColorInfo(container5)
|
||||
palette.Color5 = NewColorInfo(baseColor)
|
||||
|
||||
palette.Color6 = NewColorInfo(primaryColor)
|
||||
|
||||
@@ -604,8 +618,9 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
||||
brightBlueV := math.Min(ph.V*1.15, 1.0)
|
||||
palette.Color12 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: ph.H, S: brightBlueS, V: brightBlueV})), bgColor, accentTarget, opts))
|
||||
|
||||
lightContainer := DeriveContainer(primaryColor, true)
|
||||
palette.Color13 = NewColorInfo(lightContainer)
|
||||
color13S := ph.S * 0.7
|
||||
color13V := math.Min(ph.V*1.3, 1.0)
|
||||
palette.Color13 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: ph.H, S: color13S, V: color13V})), bgColor, accentTarget, opts))
|
||||
|
||||
brightCyanS := ph.S * 0.5
|
||||
brightCyanV := math.Min(ph.V*1.3, 1.0)
|
||||
@@ -632,9 +647,7 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
||||
blueV := ph.V * 0.95
|
||||
palette.Color4 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: ph.H, S: blueS, V: blueV})), bgColor, normalTextTarget, opts))
|
||||
|
||||
// Color5 matches primary_container exactly (dark container in dark mode)
|
||||
darkContainer := DeriveContainer(primaryColor, false)
|
||||
palette.Color5 = NewColorInfo(darkContainer)
|
||||
palette.Color5 = NewColorInfo(baseColor)
|
||||
|
||||
palette.Color6 = NewColorInfo(primaryColor)
|
||||
|
||||
@@ -681,11 +694,14 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
||||
}
|
||||
|
||||
type VariantOptions struct {
|
||||
PrimaryDark string
|
||||
PrimaryLight string
|
||||
Background string
|
||||
UseDPS bool
|
||||
IsLightMode bool
|
||||
PrimaryDark string
|
||||
PrimaryLight string
|
||||
BackgroundDark string
|
||||
BackgroundLight string
|
||||
ContainerDark string
|
||||
ContainerLight string
|
||||
UseDPS bool
|
||||
IsLightMode bool
|
||||
}
|
||||
|
||||
func mergeColorInfo(dark, light ColorInfo, isLightMode bool) VariantColorInfo {
|
||||
@@ -702,8 +718,8 @@ func mergeColorInfo(dark, light ColorInfo, isLightMode bool) VariantColorInfo {
|
||||
}
|
||||
|
||||
func GenerateVariantPalette(opts VariantOptions) VariantPalette {
|
||||
darkOpts := PaletteOptions{IsLight: false, Background: opts.Background, UseDPS: opts.UseDPS}
|
||||
lightOpts := PaletteOptions{IsLight: true, Background: opts.Background, UseDPS: opts.UseDPS}
|
||||
darkOpts := PaletteOptions{IsLight: false, Background: opts.BackgroundDark, Container: opts.ContainerDark, UseDPS: opts.UseDPS}
|
||||
lightOpts := PaletteOptions{IsLight: true, Background: opts.BackgroundLight, Container: opts.ContainerLight, UseDPS: opts.UseDPS}
|
||||
|
||||
dark := GeneratePalette(opts.PrimaryDark, darkOpts)
|
||||
light := GeneratePalette(opts.PrimaryLight, lightOpts)
|
||||
|
||||
@@ -701,7 +701,8 @@ func TestGeneratePaletteWithDPS(t *testing.T) {
|
||||
|
||||
bgColor := result.Color0.Hex
|
||||
for i := 1; i < 8; i++ {
|
||||
// Skip Color5 (container) and Color6 (exact primary) - intentionally not contrast-adjusted
|
||||
// Color5 is the container and Color6 the exact primary: they pair
|
||||
// with each other, not with the background
|
||||
if i == 5 || i == 6 {
|
||||
continue
|
||||
}
|
||||
@@ -862,3 +863,112 @@ func TestGeneratePaletteLightColor8StaysLight(t *testing.T) {
|
||||
palette.Color8.Hex, palette.Color0.Hex, bgRatio)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratePaletteLightYellowMeetsNormalTextContrast(t *testing.T) {
|
||||
primaries := []string{"#63568f", "#7d5800", "#116d2b", "#1c5070", "#8b3f68"}
|
||||
|
||||
for _, primary := range primaries {
|
||||
t.Run(primary, func(t *testing.T) {
|
||||
palette := GeneratePalette(primary, PaletteOptions{IsLight: true, Background: "#f5fafc", UseDPS: true})
|
||||
|
||||
ratio := ContrastRatio(palette.Color3.Hex, palette.Color0.Hex)
|
||||
if ratio < 4.5 {
|
||||
t.Errorf("light Color3 %s vs bg %s ratio %.2f, expected >= 4.5 (yellow must clear the normal-text target)",
|
||||
palette.Color3.Hex, palette.Color0.Hex, ratio)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateVariantPaletteUsesPerVariantBackground(t *testing.T) {
|
||||
const darkBg = "#0f1417"
|
||||
const lightBg = "#f5fafc"
|
||||
|
||||
variants := GenerateVariantPalette(VariantOptions{
|
||||
PrimaryDark: "#8ecff2",
|
||||
PrimaryLight: "#1c5070",
|
||||
BackgroundDark: darkBg,
|
||||
BackgroundLight: lightBg,
|
||||
UseDPS: true,
|
||||
})
|
||||
|
||||
if variants.Color0.Dark.Hex != darkBg {
|
||||
t.Errorf("dark Color0 = %s, expected the dark background %s", variants.Color0.Dark.Hex, darkBg)
|
||||
}
|
||||
if variants.Color0.Light.Hex != lightBg {
|
||||
t.Errorf("light Color0 = %s, expected the light background %s", variants.Color0.Light.Hex, lightBg)
|
||||
}
|
||||
|
||||
lightDim := variants.Color8.Light.Hex
|
||||
if lstar := getLstar(lightDim); lstar < 60.0 {
|
||||
t.Errorf("light Color8 %s has L* %.2f, expected >= 60 (derived from the dark surface it collapses to near-black)",
|
||||
lightDim, lstar)
|
||||
}
|
||||
if ratio := ContrastRatio(lightDim, lightBg); ratio > 3.0 {
|
||||
t.Errorf("light Color8 %s vs bg %s ratio %.2f, expected <= 3.0 (bright black stays dim)", lightDim, lightBg, ratio)
|
||||
}
|
||||
|
||||
darkDim := variants.Color8.Dark.Hex
|
||||
if ratio := ContrastRatio(darkDim, darkBg); ratio > 3.0 {
|
||||
t.Errorf("dark Color8 %s vs bg %s ratio %.2f, expected <= 3.0", darkDim, darkBg, ratio)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratePaletteColor5PairsWithColor6(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
primary string
|
||||
container string
|
||||
bg string
|
||||
isLight bool
|
||||
}{
|
||||
{"violet dark", "#cdbdff", "#4b3e76", "#141318", false},
|
||||
{"violet light", "#63568f", "#e8deff", "#fdf7ff", true},
|
||||
{"green dark", "#9ed49d", "#1f5027", "#101510", false},
|
||||
{"green light", "#38693c", "#b9f0b8", "#f7fbf2", true},
|
||||
{"amber light", "#805610", "#ffddb3", "#fff8f4", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
palette := GeneratePalette(tt.primary, PaletteOptions{
|
||||
IsLight: tt.isLight,
|
||||
Background: tt.bg,
|
||||
Container: tt.container,
|
||||
UseDPS: true,
|
||||
})
|
||||
|
||||
if palette.Color5.Hex != tt.container {
|
||||
t.Errorf("Color5 = %s, expected the supplied container %s", palette.Color5.Hex, tt.container)
|
||||
}
|
||||
if palette.Color6.Hex != tt.primary {
|
||||
t.Errorf("Color6 = %s, expected the primary %s", palette.Color6.Hex, tt.primary)
|
||||
}
|
||||
|
||||
pair := ContrastRatio(palette.Color5.Hex, palette.Color6.Hex)
|
||||
if pair < 4.5 {
|
||||
t.Errorf("Color5 %s vs Color6 %s ratio %.2f, expected >= 4.5 (the container/primary pair carries the prompt themes that use ANSI 5 on 6)",
|
||||
palette.Color5.Hex, palette.Color6.Hex, pair)
|
||||
}
|
||||
|
||||
if palette.Color5.Hex == palette.Color13.Hex {
|
||||
t.Errorf("Color5 and Color13 are both %s, expected a distinct bright variant", palette.Color5.Hex)
|
||||
}
|
||||
if sep := ContrastRatio(palette.Color5.Hex, palette.Color13.Hex); sep < 2.0 {
|
||||
t.Errorf("Color5 %s vs Color13 %s ratio %.2f, expected >= 2.0 (normal and bright must be tellable apart)",
|
||||
palette.Color5.Hex, palette.Color13.Hex, sep)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratePaletteFallsBackToDerivedContainer(t *testing.T) {
|
||||
const primary = "#cdbdff"
|
||||
|
||||
palette := GeneratePalette(primary, PaletteOptions{Background: "#141318", UseDPS: true})
|
||||
|
||||
want := DeriveContainer(primary, false)
|
||||
if palette.Color5.Hex != want {
|
||||
t.Errorf("Color5 = %s with no container supplied, expected the derived %s", palette.Color5.Hex, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,7 +343,7 @@ func buildOnce(opts *Options) (bool, error) {
|
||||
|
||||
oldColors, _ := os.ReadFile(opts.ColorsOutput())
|
||||
|
||||
var primaryDark, primaryLight, surface string
|
||||
var primaryDark, primaryLight, surfaceDark, surfaceLight, containerDark, containerLight string
|
||||
var dank16JSON string
|
||||
var importArgs []string
|
||||
var sourceImage string
|
||||
@@ -373,7 +373,10 @@ func buildOnce(opts *Options) (bool, error) {
|
||||
log.Info("Using stock/custom theme colors with matugen base")
|
||||
primaryDark = extractNestedColor(opts.StockColors, "primary", "dark")
|
||||
primaryLight = extractNestedColor(opts.StockColors, "primary", "light")
|
||||
surface = extractNestedColor(opts.StockColors, "surface", "dark")
|
||||
surfaceDark = extractNestedColor(opts.StockColors, "surface", "dark")
|
||||
surfaceLight = extractNestedColor(opts.StockColors, "surface", "light")
|
||||
containerDark = extractNestedColor(opts.StockColors, "primary_container", "dark")
|
||||
containerLight = extractNestedColor(opts.StockColors, "primary_container", "light")
|
||||
|
||||
if primaryDark == "" {
|
||||
return false, fmt.Errorf("failed to extract primary dark from stock colors")
|
||||
@@ -382,7 +385,7 @@ func buildOnce(opts *Options) (bool, error) {
|
||||
primaryLight = primaryDark
|
||||
}
|
||||
|
||||
dank16JSON = generateDank16Variants(primaryDark, primaryLight, surface, opts.Mode)
|
||||
dank16JSON = generateDank16Variants(primaryDark, primaryLight, surfaceDark, surfaceLight, containerDark, containerLight, opts.Mode)
|
||||
importData := fmt.Sprintf(`{"colors": %s, "dank16": %s}`, opts.StockColors, dank16JSON)
|
||||
importArgs = []string{"--import-json-string", importData}
|
||||
|
||||
@@ -403,7 +406,10 @@ func buildOnce(opts *Options) (bool, error) {
|
||||
|
||||
primaryDark = extractMatugenColor(matJSON, "primary", "dark")
|
||||
primaryLight = extractMatugenColor(matJSON, "primary", "light")
|
||||
surface = extractMatugenColor(matJSON, "surface", "dark")
|
||||
surfaceDark = extractMatugenColor(matJSON, "surface", "dark")
|
||||
surfaceLight = extractMatugenColor(matJSON, "surface", "light")
|
||||
containerDark = extractMatugenColor(matJSON, "primary_container", "dark")
|
||||
containerLight = extractMatugenColor(matJSON, "primary_container", "light")
|
||||
|
||||
if primaryDark == "" {
|
||||
return false, fmt.Errorf("failed to extract primary color")
|
||||
@@ -412,7 +418,7 @@ func buildOnce(opts *Options) (bool, error) {
|
||||
primaryLight = primaryDark
|
||||
}
|
||||
|
||||
dank16JSON = generateDank16Variants(primaryDark, primaryLight, surface, opts.Mode)
|
||||
dank16JSON = generateDank16Variants(primaryDark, primaryLight, surfaceDark, surfaceLight, containerDark, containerLight, opts.Mode)
|
||||
importArgs = []string{"--import-json-string", buildImportData(dank16JSON, sourceImage)}
|
||||
|
||||
log.Infof("Running matugen %s with dank16 injection", opts.Kind)
|
||||
@@ -1125,13 +1131,16 @@ func extractNestedColor(jsonStr, colorName, variant string) string {
|
||||
return color
|
||||
}
|
||||
|
||||
func generateDank16Variants(primaryDark, primaryLight, surface string, mode ColorMode) string {
|
||||
func generateDank16Variants(primaryDark, primaryLight, surfaceDark, surfaceLight, containerDark, containerLight string, mode ColorMode) string {
|
||||
variantOpts := dank16.VariantOptions{
|
||||
PrimaryDark: primaryDark,
|
||||
PrimaryLight: primaryLight,
|
||||
Background: surface,
|
||||
UseDPS: true,
|
||||
IsLightMode: mode == ColorModeLight,
|
||||
PrimaryDark: primaryDark,
|
||||
PrimaryLight: primaryLight,
|
||||
BackgroundDark: surfaceDark,
|
||||
BackgroundLight: surfaceLight,
|
||||
ContainerDark: containerDark,
|
||||
ContainerLight: containerLight,
|
||||
UseDPS: true,
|
||||
IsLightMode: mode == ColorModeLight,
|
||||
}
|
||||
variantColors := dank16.GenerateVariantPalette(variantOpts)
|
||||
return dank16.GenerateVariantJSON(variantColors)
|
||||
|
||||
Reference in New Issue
Block a user