@@ -72,6 +72,37 @@ func detectMonochromeTint(colors []string) (hue float64, hasTint bool, tintStren
7272 return avgHue , true , concentration * avgChroma
7373}
7474
75+ // meanDominantChroma returns the mean OKLCH chroma across the dominant colors,
76+ // a proxy for how colorful the source image is. Used by the tinted-mono path
77+ // to scale synthesized ANSI chroma toward the image's actual saturation.
78+ func meanDominantChroma (colors []string ) float64 {
79+ if len (colors ) == 0 {
80+ return 0
81+ }
82+ var sum float64
83+ for _ , c := range colors {
84+ sum += color .HexToOKLCH (c ).C
85+ }
86+ return sum / float64 (len (colors ))
87+ }
88+
89+ func mean6 (v [6 ]float64 ) float64 {
90+ var sum float64
91+ for _ , x := range v {
92+ sum += x
93+ }
94+ return sum / 6
95+ }
96+
97+ // monoChromaFactor maps the source image's mean chroma to a [floor, 1.0] scale
98+ // applied to the canonical mono ANSI chroma arrays. refMean is the mean of those
99+ // arrays, so a source colorful enough to reach it yields factor 1.0 (unchanged
100+ // vivid output); grayer sources scale down toward MonoChromaFactorFloor.
101+ func monoChromaFactor (dominantColors []string , refMean float64 ) float64 {
102+ srcChroma := meanDominantChroma (dominantColors )
103+ return clampF (srcChroma * MonoChromaFidelityGain / refMean , MonoChromaFactorFloor , 1.0 )
104+ }
105+
75106// applyTint applies tint influence to an OKLCH hue based on the image's
76107// dominant tone, at the strength used by the auto-detected monochrome path.
77108func applyTint (ansiHue , tintHue float64 , hasTint bool ) float64 {
@@ -230,8 +261,13 @@ func generateTintedMonochromaticPalette(dominantColors []string, lightMode bool,
230261 palette [7 ] = color .OKLabToHex (fgLab )
231262 }
232263
233- // ANSI 1-6: canonical hues pulled strongly toward the base hue.
264+ // ANSI 1-6: canonical hues pulled strongly toward the base hue. Chroma is
265+ // scaled toward the source image's actual saturation (chromaFactor) so a
266+ // near-grayscale image produces a muted palette instead of a fixed rainbow;
267+ // strongly-tinted sources keep the full vivid levels (factor 1.0).
234268 chromaLevels := [6 ]float64 {0.09 , 0.11 , 0.13 , 0.10 , 0.12 , 0.14 }
269+ brightChromaLevels := [6 ]float64 {0.11 , 0.14 , 0.16 , 0.12 , 0.15 , 0.17 }
270+ chromaFactor := monoChromaFactor (dominantColors , mean6 (chromaLevels ))
235271 lightnessOffsets := [6 ]float64 {- 0.08 , - 0.02 , + 0.04 , + 0.10 , - 0.05 , + 0.07 }
236272 lightnessBase := 0.62
237273 if lightMode {
@@ -241,12 +277,11 @@ func generateTintedMonochromaticPalette(dominantColors []string, lightMode bool,
241277 for i := 0 ; i < 6 ; i ++ {
242278 lightness := math .Max (0.30 , math .Min (0.85 , lightnessBase + lightnessOffsets [i ]))
243279 hue := applyTintStrength (OKLCHAnsiHues [i ], baseHue , true , MonochromaticTintStrength )
244- palette [i + 1 ] = color .OKLCHToHex (color.OKLCH {L : lightness , C : chromaLevels [i ], H : hue })
280+ palette [i + 1 ] = color .OKLCHToHex (color.OKLCH {L : lightness , C : chromaLevels [i ] * chromaFactor , H : hue })
245281 }
246282
247283 palette [8 ] = generateCommentColor (palette [0 ])
248284
249- brightChromaLevels := [6 ]float64 {0.11 , 0.14 , 0.16 , 0.12 , 0.15 , 0.17 }
250285 for i := 0 ; i < 6 ; i ++ {
251286 baseLightness := lightnessBase + lightnessOffsets [i ]
252287 adjustment := 0.10
@@ -255,7 +290,7 @@ func generateTintedMonochromaticPalette(dominantColors []string, lightMode bool,
255290 }
256291 lightness := math .Max (0.20 , math .Min (0.92 , baseLightness + adjustment ))
257292 hue := applyTintStrength (OKLCHAnsiHues [i ], baseHue , true , MonochromaticTintStrength )
258- palette [i + 9 ] = color .OKLCHToHex (color.OKLCH {L : lightness , C : brightChromaLevels [i ], H : hue })
293+ palette [i + 9 ] = color .OKLCHToHex (color.OKLCH {L : lightness , C : brightChromaLevels [i ] * chromaFactor , H : hue })
259294 }
260295
261296 if lightMode {
0 commit comments