mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
template-processor: added a new "muted" scheme, very desaturated and monotonal
This commit is contained in:
@@ -354,6 +354,7 @@
|
||||
"clear": "Löschen",
|
||||
"clipboard": "Zwischenablage",
|
||||
"close": "Schließen",
|
||||
"color-muted": "Stummgeschaltet",
|
||||
"colors": "Farben",
|
||||
"command": "Befehl",
|
||||
"connect": "Verbinden",
|
||||
|
||||
@@ -354,6 +354,7 @@
|
||||
"clear": "Clear",
|
||||
"clipboard": "Clipboard",
|
||||
"close": "Close",
|
||||
"color-muted": "Muted",
|
||||
"colors": "Colors",
|
||||
"command": "Command",
|
||||
"connect": "Connect",
|
||||
|
||||
@@ -354,6 +354,7 @@
|
||||
"clear": "Borrar",
|
||||
"clipboard": "Portapapeles",
|
||||
"close": "Cerrar",
|
||||
"color-muted": "Silenciado",
|
||||
"colors": "Colores",
|
||||
"command": "Comando",
|
||||
"connect": "Conectar",
|
||||
|
||||
@@ -354,6 +354,7 @@
|
||||
"clear": "Effacer",
|
||||
"clipboard": "Presse-papiers",
|
||||
"close": "Fermer",
|
||||
"color-muted": "Muet",
|
||||
"colors": "Couleurs",
|
||||
"command": "Commande",
|
||||
"connect": "Connecter",
|
||||
|
||||
@@ -354,6 +354,7 @@
|
||||
"clear": "Törlés",
|
||||
"clipboard": "Vágólap",
|
||||
"close": "Bezárás",
|
||||
"color-muted": "Némítva",
|
||||
"colors": "Színek",
|
||||
"command": "Parancs",
|
||||
"connect": "Csatlakozás",
|
||||
|
||||
@@ -354,6 +354,7 @@
|
||||
"clear": "クリア",
|
||||
"clipboard": "クリップボード",
|
||||
"close": "閉じる",
|
||||
"color-muted": "ミュート",
|
||||
"colors": "色",
|
||||
"command": "コマンド",
|
||||
"connect": "接続",
|
||||
|
||||
@@ -354,6 +354,7 @@
|
||||
"clear": "Paqqij bike",
|
||||
"clipboard": "Klîpbir",
|
||||
"close": "Bigire",
|
||||
"color-muted": "Bêdengkirî",
|
||||
"colors": "Rengan",
|
||||
"command": "Ferman",
|
||||
"connect": "Girêdan",
|
||||
|
||||
@@ -354,6 +354,7 @@
|
||||
"clear": "Wissen",
|
||||
"clipboard": "Klembord",
|
||||
"close": "Sluiten",
|
||||
"color-muted": "Gedempt",
|
||||
"colors": "Kleuren",
|
||||
"command": "Commando",
|
||||
"connect": "Verbinden",
|
||||
|
||||
@@ -354,6 +354,7 @@
|
||||
"clear": "Wyczyść",
|
||||
"clipboard": "Schowek",
|
||||
"close": "Zamknij",
|
||||
"color-muted": "Wyciszony",
|
||||
"colors": "Kolory",
|
||||
"command": "Komenda",
|
||||
"connect": "Połącz",
|
||||
|
||||
@@ -354,6 +354,7 @@
|
||||
"clear": "Limpar",
|
||||
"clipboard": "Área de transferência",
|
||||
"close": "Fechar",
|
||||
"color-muted": "Silenciado",
|
||||
"colors": "Cores",
|
||||
"command": "Comando",
|
||||
"connect": "Conectar",
|
||||
|
||||
@@ -354,6 +354,7 @@
|
||||
"clear": "Очистить",
|
||||
"clipboard": "Буфер обмена",
|
||||
"close": "Закрыть",
|
||||
"color-muted": "Выключено",
|
||||
"colors": "Цвета",
|
||||
"command": "Команда",
|
||||
"connect": "Соединить",
|
||||
|
||||
@@ -354,6 +354,7 @@
|
||||
"clear": "Temizle",
|
||||
"clipboard": "Panoya",
|
||||
"close": "Kapat",
|
||||
"color-muted": "Sessiz",
|
||||
"colors": "Renkler",
|
||||
"command": "Komut",
|
||||
"connect": "Bağlan",
|
||||
|
||||
@@ -354,6 +354,7 @@
|
||||
"clear": "Очистити",
|
||||
"clipboard": "Буфер обміну",
|
||||
"close": "Закрити",
|
||||
"color-muted": "Вимкнено звук",
|
||||
"colors": "Кольори",
|
||||
"command": "Команда",
|
||||
"connect": "Підключити",
|
||||
|
||||
@@ -354,6 +354,7 @@
|
||||
"clear": "清除",
|
||||
"clipboard": "剪贴板",
|
||||
"close": "关闭",
|
||||
"color-muted": "已静音",
|
||||
"colors": "颜色",
|
||||
"command": "命令",
|
||||
"connect": "连接",
|
||||
|
||||
@@ -350,6 +350,7 @@
|
||||
"clear": "清空",
|
||||
"clipboard": "剪貼簿",
|
||||
"close": "關閉",
|
||||
"color-muted": "已靜音",
|
||||
"colors": "顏色",
|
||||
"command": "指令",
|
||||
"connect": "連接",
|
||||
|
||||
@@ -272,6 +272,10 @@ ColumnLayout {
|
||||
"key": "faithful",
|
||||
"name": I18n.tr("common.faithful")
|
||||
},
|
||||
{
|
||||
"key": "muted",
|
||||
"name": I18n.tr("common.color-muted")
|
||||
},
|
||||
]
|
||||
currentKey: Settings.data.colorSchemes.generationMethod
|
||||
onSelected: key => {
|
||||
|
||||
@@ -12,6 +12,7 @@ Scheme types:
|
||||
- M3 schemes (tonal-spot, fruit-salad, rainbow, content): Compared with matugen
|
||||
- vibrant: Prioritizes the most saturated colors regardless of area
|
||||
- faithful: Prioritizes dominant colors by area coverage
|
||||
- muted: Preserves hue but caps saturation low (for monochrome wallpapers)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
@@ -113,20 +114,21 @@ def run_matugen(image_path: Path, scheme: str) -> dict | None:
|
||||
return None
|
||||
|
||||
|
||||
def analyze_vibrant_faithful(image_path: Path) -> None:
|
||||
"""Analyze vibrant and faithful mode outputs."""
|
||||
def analyze_vibrant_faithful_muted(image_path: Path) -> None:
|
||||
"""Analyze vibrant, faithful, and muted mode outputs."""
|
||||
print("\n" + "=" * 78)
|
||||
print("VIBRANT vs FAITHFUL COMPARISON")
|
||||
print("VIBRANT vs FAITHFUL vs MUTED COMPARISON")
|
||||
print("=" * 78)
|
||||
print()
|
||||
print("Vibrant: Prioritizes the most saturated colors regardless of area")
|
||||
print("Faithful: Prioritizes dominant colors by area coverage")
|
||||
print("Muted: Preserves hue but caps saturation low (monochrome wallpapers)")
|
||||
print()
|
||||
print("-" * 78)
|
||||
print(f"{'Mode':<12} {'Color':<12} {'Hex':<10} {'Hue':>8} {'Chroma':>8} {'Name':<10}")
|
||||
print("-" * 78)
|
||||
|
||||
for scheme in ["vibrant", "faithful"]:
|
||||
for scheme in ["vibrant", "faithful", "muted"]:
|
||||
colors = run_our_processor(image_path, scheme)
|
||||
if not colors:
|
||||
print(f"{scheme}: Failed to get colors")
|
||||
@@ -149,26 +151,36 @@ def analyze_vibrant_faithful(image_path: Path) -> None:
|
||||
# Summary comparison
|
||||
vibrant = run_our_processor(image_path, "vibrant")
|
||||
faithful = run_our_processor(image_path, "faithful")
|
||||
muted = run_our_processor(image_path, "muted")
|
||||
|
||||
if vibrant and faithful:
|
||||
if vibrant and faithful and muted:
|
||||
print()
|
||||
print("Summary:")
|
||||
v_hct = get_hct(vibrant.get("primary", "#000000"))
|
||||
f_hct = get_hct(faithful.get("primary", "#000000"))
|
||||
m_hct = get_hct(muted.get("primary", "#000000"))
|
||||
|
||||
v_name = hue_to_name(v_hct.hue)
|
||||
f_name = hue_to_name(f_hct.hue)
|
||||
m_name = hue_to_name(m_hct.hue)
|
||||
|
||||
diff = hue_diff(v_hct.hue, f_hct.hue)
|
||||
vf_diff = hue_diff(v_hct.hue, f_hct.hue)
|
||||
|
||||
print(f" Vibrant primary: {vibrant.get('primary')} ({v_name}, hue {v_hct.hue:.0f}°, chroma {v_hct.chroma:.1f})")
|
||||
print(f" Faithful primary: {faithful.get('primary')} ({f_name}, hue {f_hct.hue:.0f}°, chroma {f_hct.chroma:.1f})")
|
||||
print(f" Hue difference: {diff:.1f}°")
|
||||
print(f" Muted primary: {muted.get('primary')} ({m_name}, hue {m_hct.hue:.0f}°, chroma {m_hct.chroma:.1f})")
|
||||
print(f" V-F hue diff: {vf_diff:.1f}°")
|
||||
|
||||
if diff > 60:
|
||||
print(f" → Modes picked DIFFERENT color families ({v_name} vs {f_name})")
|
||||
if vf_diff > 60:
|
||||
print(f" → Vibrant/Faithful picked DIFFERENT color families ({v_name} vs {f_name})")
|
||||
else:
|
||||
print(f" → Modes picked SIMILAR colors")
|
||||
print(f" → Vibrant/Faithful picked SIMILAR colors")
|
||||
|
||||
# Note the muted chroma reduction
|
||||
if m_hct.chroma < 20:
|
||||
print(f" → Muted successfully reduced chroma to {m_hct.chroma:.1f}")
|
||||
else:
|
||||
print(f" → Muted chroma still moderately high ({m_hct.chroma:.1f})")
|
||||
|
||||
|
||||
def compare_m3_schemes(image_path: Path, has_matugen: bool) -> None:
|
||||
@@ -293,8 +305,8 @@ def main() -> int:
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
print("Note: matugen not found, skipping M3 comparison")
|
||||
|
||||
# Always show vibrant vs faithful first (most useful)
|
||||
analyze_vibrant_faithful(args.wallpaper)
|
||||
# Always show vibrant vs faithful vs muted first (most useful)
|
||||
analyze_vibrant_faithful_muted(args.wallpaper)
|
||||
|
||||
# Then show M3 schemes
|
||||
compare_m3_schemes(args.wallpaper, has_matugen)
|
||||
|
||||
@@ -268,6 +268,31 @@ def _score_colors_count(
|
||||
return result_colors
|
||||
|
||||
|
||||
def _score_colors_muted(
|
||||
colors_with_counts: list[tuple[RGB, int]],
|
||||
) -> list[tuple[Color, float]]:
|
||||
"""
|
||||
Score colors for muted mode - pure pixel count without chroma filtering.
|
||||
|
||||
Unlike count scoring which filters to chroma >= 10, this accepts all colors
|
||||
including grayscale. Designed for monochrome/monotonal wallpapers where
|
||||
the dominant color may have very low or zero saturation.
|
||||
|
||||
Args:
|
||||
colors_with_counts: List of (RGB, count) tuples from clustering
|
||||
|
||||
Returns:
|
||||
List of (Color, score) tuples, sorted by count descending
|
||||
"""
|
||||
result = []
|
||||
for rgb, count in colors_with_counts:
|
||||
color = Color.from_rgb(rgb)
|
||||
result.append((color, float(count)))
|
||||
|
||||
result.sort(key=lambda x: -x[1])
|
||||
return result
|
||||
|
||||
|
||||
def _score_colors_population(
|
||||
colors_with_counts: list[tuple[RGB, int]],
|
||||
total_pixels: int
|
||||
@@ -415,6 +440,7 @@ def extract_palette(
|
||||
- "population": matugen-like, representative colors (M3 schemes)
|
||||
- "chroma": vibrant, chroma-prioritized with centroid averaging
|
||||
- "count": area-dominant, picks by pixel count (faithful mode)
|
||||
- "muted": like count but without chroma filtering (monochrome wallpapers)
|
||||
|
||||
Returns:
|
||||
List of Color objects, sorted by score
|
||||
@@ -436,6 +462,11 @@ def extract_palette(
|
||||
# Scoring will filter to colorful colors and pick by count
|
||||
cluster_count = 48
|
||||
filtered = sampled
|
||||
elif scoring == "muted":
|
||||
# Muted mode: similar to count but accepts low-chroma colors
|
||||
# For monochrome/monotonal wallpapers
|
||||
cluster_count = 24
|
||||
filtered = sampled
|
||||
else:
|
||||
# Vibrant mode: more clusters to capture high-chroma colors that might
|
||||
# otherwise get averaged away, with colorfulness pre-filter
|
||||
@@ -459,6 +490,7 @@ def extract_palette(
|
||||
# Score colors based on method
|
||||
# - chroma: centroid colors (averaged, smoother - vibrant mode)
|
||||
# - count: representative pixels by area dominance (faithful mode)
|
||||
# - muted: like count but accepts low/zero chroma (monochrome wallpapers)
|
||||
# - population: representative colors with Material scoring (M3 schemes)
|
||||
if scoring == "chroma":
|
||||
# Use centroid colors for vibrant mode (smoother, blended)
|
||||
@@ -468,6 +500,10 @@ def extract_palette(
|
||||
# Use representative colors with count scoring (faithful mode)
|
||||
colors_for_scoring = [(c[1], c[2]) for c in clusters]
|
||||
scored = _score_colors_count(colors_for_scoring)
|
||||
elif scoring == "muted":
|
||||
# Use representative colors with muted scoring (no chroma filter)
|
||||
colors_for_scoring = [(c[1], c[2]) for c in clusters]
|
||||
scored = _score_colors_muted(colors_for_scoring)
|
||||
else:
|
||||
# Use representative colors for M3 schemes
|
||||
colors_for_scoring = [(c[1], c[2]) for c in clusters]
|
||||
|
||||
@@ -11,6 +11,7 @@ Supported scheme types:
|
||||
- rainbow: Chromatic accents with grayscale neutrals
|
||||
- vibrant: Prioritizes the most saturated colors regardless of area
|
||||
- faithful: Prioritizes dominant colors by area coverage
|
||||
- muted: Preserves hue but caps saturation low (for monochrome wallpapers)
|
||||
"""
|
||||
|
||||
from typing import Literal
|
||||
@@ -22,7 +23,7 @@ from .palette import find_error_color
|
||||
|
||||
# Type aliases
|
||||
ThemeMode = Literal["dark", "light"]
|
||||
SchemeType = Literal["tonal-spot", "fruit-salad", "rainbow", "content", "vibrant", "faithful"]
|
||||
SchemeType = Literal["tonal-spot", "fruit-salad", "rainbow", "content", "vibrant", "faithful", "muted"]
|
||||
|
||||
# Map scheme type strings to classes
|
||||
SCHEME_CLASSES = {
|
||||
@@ -30,7 +31,7 @@ SCHEME_CLASSES = {
|
||||
"fruit-salad": SchemeFruitSalad,
|
||||
"rainbow": SchemeRainbow,
|
||||
"content": SchemeContent,
|
||||
# "vibrant" and "faithful" uses generate_normal_* functions, not a scheme class
|
||||
# "vibrant", "faithful", and "muted" use generate_*_* functions, not a scheme class
|
||||
}
|
||||
|
||||
|
||||
@@ -484,6 +485,362 @@ def generate_normal_light(palette: list[Color]) -> dict[str, str]:
|
||||
}
|
||||
|
||||
|
||||
def generate_muted_dark(palette: list[Color]) -> dict[str, str]:
|
||||
"""
|
||||
Generate muted dark theme from palette.
|
||||
|
||||
Designed for monochrome/monotonal wallpapers - preserves the dominant hue
|
||||
but caps saturation to very low values for a subtle, understated look.
|
||||
Outputs same keys as Material for compatibility.
|
||||
"""
|
||||
# Use primary color's hue but with very low saturation
|
||||
primary = palette[0] if palette else Color(128, 128, 128)
|
||||
primary_h, primary_s, primary_l = primary.to_hsl()
|
||||
|
||||
# Derive secondary and tertiary with subtle hue shifts (monochromatic feel)
|
||||
# Much smaller shifts than normal mode since we want cohesion
|
||||
secondary = shift_hue(primary, 15)
|
||||
tertiary = shift_hue(primary, 30)
|
||||
quaternary = shift_hue(primary, 180)
|
||||
error = find_error_color(palette)
|
||||
|
||||
# Cap saturation low - this is the key difference from normal mode
|
||||
MUTED_SAT_PRIMARY = 0.15
|
||||
MUTED_SAT_SECONDARY = 0.12
|
||||
MUTED_SAT_TERTIARY = 0.10
|
||||
MUTED_SAT_SURFACE = 0.08
|
||||
|
||||
h, s, l = primary.to_hsl()
|
||||
primary_adjusted = Color.from_hsl(h, min(s, MUTED_SAT_PRIMARY), max(l, 0.65))
|
||||
|
||||
h, s, l = secondary.to_hsl()
|
||||
secondary_adjusted = Color.from_hsl(h, min(s, MUTED_SAT_SECONDARY), max(l, 0.60))
|
||||
|
||||
h, s, l = tertiary.to_hsl()
|
||||
tertiary_adjusted = Color.from_hsl(h, min(s, MUTED_SAT_TERTIARY), max(l, 0.60))
|
||||
|
||||
# Container colors - darker, slightly saturated versions
|
||||
def make_container_dark(base: Color) -> Color:
|
||||
h, s, l = base.to_hsl()
|
||||
return Color.from_hsl(h, min(s + 0.05, MUTED_SAT_PRIMARY), max(l - 0.35, 0.15))
|
||||
|
||||
primary_container = make_container_dark(primary_adjusted)
|
||||
secondary_container = make_container_dark(secondary_adjusted)
|
||||
tertiary_container = make_container_dark(tertiary_adjusted)
|
||||
error_container = make_container_dark(error)
|
||||
|
||||
# Surface: very low saturation, preserving hue for subtle tint
|
||||
surface_hue = primary_h
|
||||
base_surface = Color.from_hsl(surface_hue, MUTED_SAT_SURFACE, 0.5)
|
||||
|
||||
surface = adjust_surface(base_surface, MUTED_SAT_SURFACE, 0.12)
|
||||
surface_variant = adjust_surface(base_surface, MUTED_SAT_SURFACE, 0.16)
|
||||
|
||||
# Surface containers - progressive lightness with minimal saturation
|
||||
surface_container_lowest = adjust_surface(base_surface, MUTED_SAT_SURFACE, 0.06)
|
||||
surface_container_low = adjust_surface(base_surface, MUTED_SAT_SURFACE, 0.10)
|
||||
surface_container = adjust_surface(base_surface, MUTED_SAT_SURFACE, 0.20)
|
||||
surface_container_high = adjust_surface(base_surface, MUTED_SAT_SURFACE, 0.18)
|
||||
surface_container_highest = adjust_surface(base_surface, MUTED_SAT_SURFACE, 0.22)
|
||||
|
||||
# Text colors - near-neutral with slight hue tint
|
||||
base_on_surface = Color.from_hsl(primary_h, 0.03, 0.95)
|
||||
on_surface = ensure_contrast(base_on_surface, surface, 4.5)
|
||||
|
||||
base_on_surface_variant = Color.from_hsl(primary_h, 0.03, 0.80)
|
||||
on_surface_variant = ensure_contrast(base_on_surface_variant, surface_variant, 4.5)
|
||||
|
||||
outline = ensure_contrast(Color.from_hsl(primary_h, 0.05, 0.30), surface, 3.0)
|
||||
outline_variant = ensure_contrast(Color.from_hsl(primary_h, 0.05, 0.40), surface, 3.0)
|
||||
|
||||
# Contrasting foregrounds
|
||||
dark_fg = Color.from_hsl(primary_h, 0.10, 0.12)
|
||||
on_primary = ensure_contrast(dark_fg, primary_adjusted, 7.0)
|
||||
on_secondary = ensure_contrast(dark_fg, secondary_adjusted, 7.0)
|
||||
on_tertiary = ensure_contrast(dark_fg, tertiary_adjusted, 7.0)
|
||||
on_error = ensure_contrast(dark_fg, error, 7.0)
|
||||
|
||||
# "On" colors for containers
|
||||
on_primary_container = ensure_contrast(Color.from_hsl(primary_h, 0.05, 0.90), primary_container, 4.5, prefer_light=True)
|
||||
sec_h, _, _ = secondary.to_hsl()
|
||||
on_secondary_container = ensure_contrast(Color.from_hsl(sec_h, 0.05, 0.90), secondary_container, 4.5, prefer_light=True)
|
||||
ter_h, _, _ = tertiary.to_hsl()
|
||||
on_tertiary_container = ensure_contrast(Color.from_hsl(ter_h, 0.05, 0.90), tertiary_container, 4.5, prefer_light=True)
|
||||
err_h, _, _ = error.to_hsl()
|
||||
on_error_container = ensure_contrast(Color.from_hsl(err_h, 0.05, 0.90), error_container, 4.5, prefer_light=True)
|
||||
|
||||
# Shadow and scrim
|
||||
shadow = surface
|
||||
scrim = Color(0, 0, 0)
|
||||
|
||||
# Inverse colors
|
||||
inverse_surface = Color.from_hsl(primary_h, 0.05, 0.90)
|
||||
inverse_on_surface = Color.from_hsl(primary_h, 0.03, 0.15)
|
||||
inverse_primary = Color.from_hsl(primary_h, min(primary_s * 0.5, MUTED_SAT_PRIMARY), 0.40)
|
||||
|
||||
# Background aliases
|
||||
background = surface
|
||||
on_background = on_surface
|
||||
|
||||
# Fixed colors - still muted
|
||||
def make_fixed_dark(base: Color) -> tuple[Color, Color]:
|
||||
h, s, _ = base.to_hsl()
|
||||
fixed = Color.from_hsl(h, min(s, MUTED_SAT_PRIMARY), 0.85)
|
||||
fixed_dim = Color.from_hsl(h, min(s, MUTED_SAT_PRIMARY), 0.75)
|
||||
return fixed, fixed_dim
|
||||
|
||||
primary_fixed, primary_fixed_dim = make_fixed_dark(primary_adjusted)
|
||||
secondary_fixed, secondary_fixed_dim = make_fixed_dark(secondary_adjusted)
|
||||
tertiary_fixed, tertiary_fixed_dim = make_fixed_dark(tertiary_adjusted)
|
||||
|
||||
# "On" colors for fixed
|
||||
on_primary_fixed = ensure_contrast(Color.from_hsl(primary_h, 0.05, 0.15), primary_fixed, 4.5)
|
||||
on_primary_fixed_variant = ensure_contrast(Color.from_hsl(primary_h, 0.05, 0.20), primary_fixed_dim, 4.5)
|
||||
on_secondary_fixed = ensure_contrast(Color.from_hsl(sec_h, 0.05, 0.15), secondary_fixed, 4.5)
|
||||
on_secondary_fixed_variant = ensure_contrast(Color.from_hsl(sec_h, 0.05, 0.20), secondary_fixed_dim, 4.5)
|
||||
on_tertiary_fixed = ensure_contrast(Color.from_hsl(ter_h, 0.05, 0.15), tertiary_fixed, 4.5)
|
||||
on_tertiary_fixed_variant = ensure_contrast(Color.from_hsl(ter_h, 0.05, 0.20), tertiary_fixed_dim, 4.5)
|
||||
|
||||
# Surface dim and bright
|
||||
surface_dim = adjust_surface(base_surface, MUTED_SAT_SURFACE, 0.08)
|
||||
surface_bright = adjust_surface(base_surface, MUTED_SAT_SURFACE, 0.24)
|
||||
|
||||
return {
|
||||
# Primary
|
||||
"primary": primary_adjusted.to_hex(),
|
||||
"on_primary": on_primary.to_hex(),
|
||||
"primary_container": primary_container.to_hex(),
|
||||
"on_primary_container": on_primary_container.to_hex(),
|
||||
"primary_fixed": primary_fixed.to_hex(),
|
||||
"primary_fixed_dim": primary_fixed_dim.to_hex(),
|
||||
"on_primary_fixed": on_primary_fixed.to_hex(),
|
||||
"on_primary_fixed_variant": on_primary_fixed_variant.to_hex(),
|
||||
# Secondary
|
||||
"secondary": secondary_adjusted.to_hex(),
|
||||
"on_secondary": on_secondary.to_hex(),
|
||||
"secondary_container": secondary_container.to_hex(),
|
||||
"on_secondary_container": on_secondary_container.to_hex(),
|
||||
"secondary_fixed": secondary_fixed.to_hex(),
|
||||
"secondary_fixed_dim": secondary_fixed_dim.to_hex(),
|
||||
"on_secondary_fixed": on_secondary_fixed.to_hex(),
|
||||
"on_secondary_fixed_variant": on_secondary_fixed_variant.to_hex(),
|
||||
# Tertiary
|
||||
"tertiary": tertiary_adjusted.to_hex(),
|
||||
"on_tertiary": on_tertiary.to_hex(),
|
||||
"tertiary_container": tertiary_container.to_hex(),
|
||||
"on_tertiary_container": on_tertiary_container.to_hex(),
|
||||
"tertiary_fixed": tertiary_fixed.to_hex(),
|
||||
"tertiary_fixed_dim": tertiary_fixed_dim.to_hex(),
|
||||
"on_tertiary_fixed": on_tertiary_fixed.to_hex(),
|
||||
"on_tertiary_fixed_variant": on_tertiary_fixed_variant.to_hex(),
|
||||
# Error
|
||||
"error": error.to_hex(),
|
||||
"on_error": on_error.to_hex(),
|
||||
"error_container": error_container.to_hex(),
|
||||
"on_error_container": on_error_container.to_hex(),
|
||||
# Surface
|
||||
"surface": surface.to_hex(),
|
||||
"on_surface": on_surface.to_hex(),
|
||||
"surface_variant": surface_variant.to_hex(),
|
||||
"on_surface_variant": on_surface_variant.to_hex(),
|
||||
"surface_dim": surface_dim.to_hex(),
|
||||
"surface_bright": surface_bright.to_hex(),
|
||||
# Surface containers
|
||||
"surface_container_lowest": surface_container_lowest.to_hex(),
|
||||
"surface_container_low": surface_container_low.to_hex(),
|
||||
"surface_container": surface_container.to_hex(),
|
||||
"surface_container_high": surface_container_high.to_hex(),
|
||||
"surface_container_highest": surface_container_highest.to_hex(),
|
||||
# Outline and other
|
||||
"outline": outline.to_hex(),
|
||||
"outline_variant": outline_variant.to_hex(),
|
||||
"shadow": shadow.to_hex(),
|
||||
"scrim": scrim.to_hex(),
|
||||
# Inverse
|
||||
"inverse_surface": inverse_surface.to_hex(),
|
||||
"inverse_on_surface": inverse_on_surface.to_hex(),
|
||||
"inverse_primary": inverse_primary.to_hex(),
|
||||
# Background
|
||||
"background": background.to_hex(),
|
||||
"on_background": on_background.to_hex(),
|
||||
}
|
||||
|
||||
|
||||
def generate_muted_light(palette: list[Color]) -> dict[str, str]:
|
||||
"""
|
||||
Generate muted light theme from palette.
|
||||
|
||||
Designed for monochrome/monotonal wallpapers - preserves the dominant hue
|
||||
but caps saturation to very low values for a subtle, understated look.
|
||||
Outputs same keys as Material for compatibility.
|
||||
"""
|
||||
primary = palette[0] if palette else Color(128, 128, 128)
|
||||
primary_h, primary_s, _ = primary.to_hsl()
|
||||
|
||||
# Derive secondary and tertiary with subtle hue shifts
|
||||
secondary = shift_hue(primary, 15)
|
||||
tertiary = shift_hue(primary, 30)
|
||||
quaternary = shift_hue(primary, 180)
|
||||
error = find_error_color(palette)
|
||||
|
||||
# Cap saturation low
|
||||
MUTED_SAT_PRIMARY = 0.15
|
||||
MUTED_SAT_SECONDARY = 0.12
|
||||
MUTED_SAT_TERTIARY = 0.10
|
||||
MUTED_SAT_SURFACE = 0.08
|
||||
|
||||
h, s, l = primary.to_hsl()
|
||||
primary_adjusted = Color.from_hsl(h, min(s, MUTED_SAT_PRIMARY), min(l, 0.45))
|
||||
|
||||
h, s, l = secondary.to_hsl()
|
||||
secondary_adjusted = Color.from_hsl(h, min(s, MUTED_SAT_SECONDARY), min(l, 0.40))
|
||||
|
||||
h, s, l = tertiary.to_hsl()
|
||||
tertiary_adjusted = Color.from_hsl(h, min(s, MUTED_SAT_TERTIARY), min(l, 0.35))
|
||||
|
||||
# Container colors - lighter, less saturated
|
||||
def make_container_light(base: Color) -> Color:
|
||||
h, s, l = base.to_hsl()
|
||||
return Color.from_hsl(h, max(s - 0.05, 0.05), min(l + 0.35, 0.85))
|
||||
|
||||
primary_container = make_container_light(primary_adjusted)
|
||||
secondary_container = make_container_light(secondary_adjusted)
|
||||
tertiary_container = make_container_light(tertiary_adjusted)
|
||||
error_container = make_container_light(error)
|
||||
|
||||
# Surface: very low saturation, preserving hue for subtle tint
|
||||
surface = adjust_surface(primary, MUTED_SAT_SURFACE, 0.90)
|
||||
surface_variant = adjust_surface(primary, MUTED_SAT_SURFACE, 0.78)
|
||||
|
||||
# Surface containers - progressive darkening with minimal saturation
|
||||
surface_container_lowest = adjust_surface(primary, MUTED_SAT_SURFACE, 0.96)
|
||||
surface_container_low = adjust_surface(primary, MUTED_SAT_SURFACE, 0.92)
|
||||
surface_container = adjust_surface(primary, MUTED_SAT_SURFACE, 0.86)
|
||||
surface_container_high = adjust_surface(primary, MUTED_SAT_SURFACE, 0.84)
|
||||
surface_container_highest = adjust_surface(primary, MUTED_SAT_SURFACE, 0.80)
|
||||
|
||||
# Text colors - near-neutral with slight hue tint
|
||||
base_on_surface = Color.from_hsl(primary_h, 0.03, 0.10)
|
||||
on_surface = ensure_contrast(base_on_surface, surface, 4.5)
|
||||
|
||||
base_on_surface_variant = Color.from_hsl(primary_h, 0.03, 0.90)
|
||||
on_surface_variant = ensure_contrast(base_on_surface_variant, surface_variant, 4.5)
|
||||
|
||||
# Contrasting foregrounds
|
||||
light_fg = Color.from_hsl(primary_h, 0.05, 0.98)
|
||||
on_primary = ensure_contrast(light_fg, primary_adjusted, 7.0)
|
||||
on_secondary = ensure_contrast(light_fg, secondary_adjusted, 7.0)
|
||||
on_tertiary = ensure_contrast(light_fg, tertiary_adjusted, 7.0)
|
||||
on_error = ensure_contrast(light_fg, error, 7.0)
|
||||
|
||||
# "On" colors for containers
|
||||
on_primary_container = ensure_contrast(Color.from_hsl(primary_h, 0.05, 0.15), primary_container, 4.5, prefer_light=False)
|
||||
sec_h, _, _ = secondary.to_hsl()
|
||||
on_secondary_container = ensure_contrast(Color.from_hsl(sec_h, 0.05, 0.15), secondary_container, 4.5, prefer_light=False)
|
||||
ter_h, _, _ = tertiary.to_hsl()
|
||||
on_tertiary_container = ensure_contrast(Color.from_hsl(ter_h, 0.05, 0.15), tertiary_container, 4.5, prefer_light=False)
|
||||
err_h, _, _ = error.to_hsl()
|
||||
on_error_container = ensure_contrast(Color.from_hsl(err_h, 0.05, 0.15), error_container, 4.5, prefer_light=False)
|
||||
|
||||
# Fixed colors - still muted
|
||||
def make_fixed_light(base: Color) -> tuple[Color, Color]:
|
||||
h, s, _ = base.to_hsl()
|
||||
fixed = Color.from_hsl(h, min(s, MUTED_SAT_PRIMARY), 0.40)
|
||||
fixed_dim = Color.from_hsl(h, min(s, MUTED_SAT_PRIMARY), 0.30)
|
||||
return fixed, fixed_dim
|
||||
|
||||
primary_fixed, primary_fixed_dim = make_fixed_light(primary_adjusted)
|
||||
secondary_fixed, secondary_fixed_dim = make_fixed_light(secondary_adjusted)
|
||||
tertiary_fixed, tertiary_fixed_dim = make_fixed_light(tertiary_adjusted)
|
||||
|
||||
# "On" colors for fixed
|
||||
on_primary_fixed = ensure_contrast(Color.from_hsl(primary_h, 0.05, 0.90), primary_fixed, 4.5)
|
||||
on_primary_fixed_variant = ensure_contrast(Color.from_hsl(primary_h, 0.05, 0.85), primary_fixed_dim, 4.5)
|
||||
on_secondary_fixed = ensure_contrast(Color.from_hsl(sec_h, 0.05, 0.90), secondary_fixed, 4.5)
|
||||
on_secondary_fixed_variant = ensure_contrast(Color.from_hsl(sec_h, 0.05, 0.85), secondary_fixed_dim, 4.5)
|
||||
on_tertiary_fixed = ensure_contrast(Color.from_hsl(ter_h, 0.05, 0.90), tertiary_fixed, 4.5)
|
||||
on_tertiary_fixed_variant = ensure_contrast(Color.from_hsl(ter_h, 0.05, 0.85), tertiary_fixed_dim, 4.5)
|
||||
|
||||
# Surface dim and bright
|
||||
surface_dim = adjust_surface(primary, MUTED_SAT_SURFACE, 0.82)
|
||||
surface_bright = adjust_surface(primary, MUTED_SAT_SURFACE, 0.95)
|
||||
|
||||
# Outline
|
||||
outline = ensure_contrast(Color.from_hsl(primary_h, 0.05, 0.65), surface, 3.0)
|
||||
outline_variant = ensure_contrast(Color.from_hsl(primary_h, 0.05, 0.75), surface, 3.0)
|
||||
shadow = Color.from_hsl(primary_h, 0.05, 0.80)
|
||||
scrim = Color(0, 0, 0)
|
||||
|
||||
# Inverse colors
|
||||
inverse_surface = Color.from_hsl(primary_h, 0.05, 0.15)
|
||||
inverse_on_surface = Color.from_hsl(primary_h, 0.03, 0.90)
|
||||
inverse_primary = Color.from_hsl(primary_h, min(primary_s * 0.5, MUTED_SAT_PRIMARY), 0.70)
|
||||
|
||||
# Background aliases
|
||||
background = surface
|
||||
on_background = on_surface
|
||||
|
||||
return {
|
||||
# Primary
|
||||
"primary": primary_adjusted.to_hex(),
|
||||
"on_primary": on_primary.to_hex(),
|
||||
"primary_container": primary_container.to_hex(),
|
||||
"on_primary_container": on_primary_container.to_hex(),
|
||||
"primary_fixed": primary_fixed.to_hex(),
|
||||
"primary_fixed_dim": primary_fixed_dim.to_hex(),
|
||||
"on_primary_fixed": on_primary_fixed.to_hex(),
|
||||
"on_primary_fixed_variant": on_primary_fixed_variant.to_hex(),
|
||||
# Secondary
|
||||
"secondary": secondary_adjusted.to_hex(),
|
||||
"on_secondary": on_secondary.to_hex(),
|
||||
"secondary_container": secondary_container.to_hex(),
|
||||
"on_secondary_container": on_secondary_container.to_hex(),
|
||||
"secondary_fixed": secondary_fixed.to_hex(),
|
||||
"secondary_fixed_dim": secondary_fixed_dim.to_hex(),
|
||||
"on_secondary_fixed": on_secondary_fixed.to_hex(),
|
||||
"on_secondary_fixed_variant": on_secondary_fixed_variant.to_hex(),
|
||||
# Tertiary
|
||||
"tertiary": tertiary_adjusted.to_hex(),
|
||||
"on_tertiary": on_tertiary.to_hex(),
|
||||
"tertiary_container": tertiary_container.to_hex(),
|
||||
"on_tertiary_container": on_tertiary_container.to_hex(),
|
||||
"tertiary_fixed": tertiary_fixed.to_hex(),
|
||||
"tertiary_fixed_dim": tertiary_fixed_dim.to_hex(),
|
||||
"on_tertiary_fixed": on_tertiary_fixed.to_hex(),
|
||||
"on_tertiary_fixed_variant": on_tertiary_fixed_variant.to_hex(),
|
||||
# Error
|
||||
"error": error.to_hex(),
|
||||
"on_error": on_error.to_hex(),
|
||||
"error_container": error_container.to_hex(),
|
||||
"on_error_container": on_error_container.to_hex(),
|
||||
# Surface
|
||||
"surface": surface.to_hex(),
|
||||
"on_surface": on_surface.to_hex(),
|
||||
"surface_variant": surface_variant.to_hex(),
|
||||
"on_surface_variant": on_surface_variant.to_hex(),
|
||||
"surface_dim": surface_dim.to_hex(),
|
||||
"surface_bright": surface_bright.to_hex(),
|
||||
# Surface containers
|
||||
"surface_container_lowest": surface_container_lowest.to_hex(),
|
||||
"surface_container_low": surface_container_low.to_hex(),
|
||||
"surface_container": surface_container.to_hex(),
|
||||
"surface_container_high": surface_container_high.to_hex(),
|
||||
"surface_container_highest": surface_container_highest.to_hex(),
|
||||
# Outline and other
|
||||
"outline": outline.to_hex(),
|
||||
"outline_variant": outline_variant.to_hex(),
|
||||
"shadow": shadow.to_hex(),
|
||||
"scrim": scrim.to_hex(),
|
||||
# Inverse
|
||||
"inverse_surface": inverse_surface.to_hex(),
|
||||
"inverse_on_surface": inverse_on_surface.to_hex(),
|
||||
"inverse_primary": inverse_primary.to_hex(),
|
||||
# Background
|
||||
"background": background.to_hex(),
|
||||
"on_background": on_background.to_hex(),
|
||||
}
|
||||
|
||||
|
||||
def generate_theme(
|
||||
palette: list[Color],
|
||||
mode: ThemeMode,
|
||||
@@ -495,7 +852,7 @@ def generate_theme(
|
||||
Args:
|
||||
palette: List of extracted colors
|
||||
mode: "dark" or "light"
|
||||
scheme_type: One of "tonal-spot", "fruit-salad", "rainbow", "vibrant", "faithful"
|
||||
scheme_type: One of "tonal-spot", "fruit-salad", "rainbow", "vibrant", "faithful", "muted"
|
||||
|
||||
Returns:
|
||||
Dictionary of color token names to hex values
|
||||
@@ -507,6 +864,12 @@ def generate_theme(
|
||||
return generate_normal_dark(palette)
|
||||
return generate_normal_light(palette)
|
||||
|
||||
# Handle muted mode (low saturation, monochrome wallpapers)
|
||||
if scheme_type == "muted":
|
||||
if mode == "dark":
|
||||
return generate_muted_dark(palette)
|
||||
return generate_muted_light(palette)
|
||||
|
||||
# All other schemes use Material Design 3 generation
|
||||
if mode == "dark":
|
||||
return generate_material_dark(palette, scheme_type)
|
||||
|
||||
@@ -11,12 +11,13 @@ Supported scheme types:
|
||||
- rainbow: Chromatic accents with grayscale neutrals
|
||||
- vibrant: Prioritizes the most saturated colors regardless of area coverage
|
||||
- faithful: Prioritizes dominant colors by area, what you see is what you get
|
||||
- muted: Preserves hue but caps saturation low (for monochrome/monotonal wallpapers)
|
||||
|
||||
Usage:
|
||||
python3 template-processor.py IMAGE_OR_JSON [OPTIONS]
|
||||
|
||||
Options:
|
||||
--scheme-type Scheme type: tonal-spot (default), content, fruit-salad, rainbow, vibrant, faithful
|
||||
--scheme-type Scheme type: tonal-spot (default), content, fruit-salad, rainbow, vibrant, faithful, muted
|
||||
--dark Generate dark theme only
|
||||
--light Generate light theme only
|
||||
--both Generate both themes (default)
|
||||
@@ -81,7 +82,7 @@ Examples:
|
||||
# Scheme type selection
|
||||
parser.add_argument(
|
||||
'--scheme-type',
|
||||
choices=['tonal-spot', 'content', 'fruit-salad', 'rainbow', 'vibrant', 'faithful'],
|
||||
choices=['tonal-spot', 'content', 'fruit-salad', 'rainbow', 'vibrant', 'faithful', 'muted'],
|
||||
default='tonal-spot',
|
||||
help='Color scheme type (default: tonal-spot)'
|
||||
)
|
||||
@@ -265,6 +266,7 @@ def main() -> int:
|
||||
# This matches matugen's color extraction exactly
|
||||
# - vibrant: Use k-means clustering for colorful/blended colors
|
||||
# - faithful: Use Wu quantizer for primary (dominant by area), k-means for accents
|
||||
# - muted: Like count but without chroma filtering (for monochrome wallpapers)
|
||||
if scheme_type == "vibrant":
|
||||
# K-means with chroma scoring for vibrant, blended colors
|
||||
palette = extract_palette(pixels, k=5, scoring="chroma")
|
||||
@@ -272,6 +274,10 @@ def main() -> int:
|
||||
# K-means with count scoring - picks dominant color by area coverage
|
||||
# This ensures primary reflects what you actually see in the image
|
||||
palette = extract_palette(pixels, k=5, scoring="count")
|
||||
elif scheme_type == "muted":
|
||||
# K-means with muted scoring - accepts low/zero chroma colors
|
||||
# For monochrome/monotonal wallpapers where dominant color has low saturation
|
||||
palette = extract_palette(pixels, k=5, scoring="muted")
|
||||
else:
|
||||
# Wu quantizer + Score algorithm (matches matugen)
|
||||
source_argb = extract_source_color(pixels)
|
||||
|
||||
@@ -279,7 +279,7 @@ Singleton {
|
||||
// Get scheme type, defaulting to tonal-spot if not a recognized value
|
||||
function getSchemeType() {
|
||||
const method = Settings.data.colorSchemes.generationMethod;
|
||||
const validTypes = ["tonal-spot", "content", "fruit-salad", "rainbow", "vibrant", "faithful"];
|
||||
const validTypes = ["tonal-spot", "content", "fruit-salad", "rainbow", "vibrant", "faithful", "muted"];
|
||||
return validTypes.includes(method) ? method : "tonal-spot";
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user