@@ -10,6 +10,9 @@ import (
1010// colors. Slots 8 and 9-15 are left empty — pass through finalizePalette to fill them.
1111// Mode transforms call this directly when they intend to overwrite bg/fg/ANSI in OKLCH,
1212// avoiding wasted slot 8/9-15 generation that the full chromatic pipeline would do.
13+ //
14+ // Slot 1-6 hues are taken verbatim from the image's best matches (no synthesis at
15+ // canonical sRGB primary hues). NormalizeBrightness handles AA contrast downstream.
1316func extractChromaticHues (dominantColors []string , lightMode bool ) [16 ]string {
1417 topCount := 12
1518 if len (dominantColors ) < topCount {
@@ -34,40 +37,36 @@ func extractChromaticHues(dominantColors []string, lightMode bool) [16]string {
3437
3538 assignments := FindOptimalAnsiAssignment (dominantColors , usedIndices , lightMode )
3639
37- var matchedColors []string
38- synthesizedSlots := [6 ]bool {}
3940 for i := 0 ; i < 6 ; i ++ {
4041 assignment := assignments [i ]
41- if assignment != nil && assignment .Score < SynthesisScoreThreshold {
42- lch := color .HexToOKLCH (dominantColors [assignment .PoolIndex ])
43- if lch .C >= MinChromaForAnsiMatch {
44- palette [i + 1 ] = dominantColors [assignment .PoolIndex ]
45- matchedColors = append (matchedColors , palette [i + 1 ])
46- usedIndices [assignment .PoolIndex ] = true
47- continue
48- }
49- }
50- synthesizedSlots [i ] = true
51- }
52-
53- // Stagger synthesized slots so they don't all share the same lightness — without
54- // stagger, multiple synthesized slots end up at avgL with only hue differing,
55- // which is hard to distinguish at low chroma.
56- synthStagger := [6 ]float64 {- 0.06 , + 0.02 , + 0.07 , - 0.04 , - 0.02 , + 0.04 }
57- for i := 0 ; i < 6 ; i ++ {
58- if ! synthesizedSlots [i ] {
42+ if assignment != nil {
43+ palette [i + 1 ] = dominantColors [assignment .PoolIndex ]
44+ usedIndices [assignment .PoolIndex ] = true
5945 continue
6046 }
61- base := SynthesizeAnsiColor (OKLCHAnsiHues [i ], matchedColors )
62- lch := color .HexToOKLCH (base )
63- lch .L = math .Max (0.30 , math .Min (0.85 , lch .L + synthStagger [i ]))
64- palette [i + 1 ] = color .OKLCHToHex (lch )
65- matchedColors = append (matchedColors , palette [i + 1 ])
47+ // Pool exhausted — the optimal-assignment loop ran out of unused colors.
48+ // Fall back to the next available pool entry rather than synthesizing a
49+ // canonical-hue color that isn't in the image.
50+ palette [i + 1 ] = nextUnusedColor (dominantColors , usedIndices )
6651 }
6752
6853 return palette
6954}
7055
56+ // nextUnusedColor returns the first dominant color not yet claimed by another slot,
57+ // marking it used. Returns the first pool entry as a last resort when the pool is
58+ // fully consumed (extremely degenerate — len(dominantColors) is bounded ≥ 8 by
59+ // ExtractColors).
60+ func nextUnusedColor (dominantColors []string , usedIndices map [int ]bool ) string {
61+ for i , c := range dominantColors {
62+ if ! usedIndices [i ] {
63+ usedIndices [i ] = true
64+ return c
65+ }
66+ }
67+ return dominantColors [0 ]
68+ }
69+
7170// synthesizeBgIfTooMid replaces a mid-lightness image bg with a synthesized OKLCH
7271// color at a sane bg lightness, preserving hue. Without this, images with no truly
7372// dark/light pixels (e.g. a sunset photo or a Nord-themed wallpaper) produce muddy
@@ -87,8 +86,11 @@ func synthesizeBgIfTooMid(bgColor string, lightMode bool) string {
8786}
8887
8988// GenerateChromaticPalette: vibrant chromatic palette from image-derived hues.
90- // OKLCH-based optimal assignment for slots 1-6, contrast-aware bg/fg, synthesized
91- // missing hues. finalizePalette derives slots 8/9-15 and enforces AA contrast.
89+ // OKLCH-based optimal assignment for slots 1-6, contrast-aware bg/fg. Slots 1-6
90+ // always come from the image — the pipeline does not synthesize canonical-hue
91+ // fallbacks, so wallpapers without a strong red/green/etc. produce palettes
92+ // faithful to the source rather than to ANSI conventions.
93+ // finalizePalette derives slots 8/9-15 and enforces AA contrast.
9294func GenerateChromaticPalette (dominantColors []string , lightMode bool ) [16 ]string {
9395 palette := extractChromaticHues (dominantColors , lightMode )
9496 finalizePalette (& palette )
0 commit comments