template-processor: added new "faithful" mode + fixed contrast of container vs on_container

This commit is contained in:
Lemmy
2026-01-20 18:27:57 -05:00
parent 0fee13919f
commit e967030cec
6 changed files with 52 additions and 24 deletions
+1
View File
@@ -454,6 +454,7 @@
"upload": "Upload",
"version": "Version",
"vibrant": "Vibrant",
"faithful": "Faithful",
"visualizer": "Visualizer",
"volume": "Volume",
"volumes": "Volumes",
@@ -234,6 +234,10 @@ ColumnLayout {
"key": "vibrant",
"name": I18n.tr("common.vibrant")
},
{
"key": "faithful",
"name": I18n.tr("common.faithful")
},
]
currentKey: Settings.data.colorSchemes.generationMethod
onSelected: key => {
+19 -6
View File
@@ -317,8 +317,10 @@ def extract_palette(
Args:
pixels: List of RGB tuples
k: Number of colors to extract
scoring: Scoring method - "population" (matugen-like, representative colors)
or "chroma" (vibrant, most colorful colors)
scoring: Scoring method:
- "population": matugen-like, representative colors (M3 schemes)
- "chroma": vibrant, chroma-prioritized with centroid averaging
- "chroma-representative": chroma-prioritized with actual pixels (faithful)
Returns:
List of Color objects, sorted by score
@@ -335,9 +337,15 @@ def extract_palette(
# Don't pre-filter for population scoring - let the Score algorithm filter
# This matches matugen which quantizes all pixels, then filters in scoring
filtered = sampled
elif scoring == "chroma-representative":
# Faithful mode: more clusters, no pre-filtering
# This picks actual dominant colors from the image without averaging
cluster_count = 48
filtered = sampled # No colorfulness filter - let scoring handle it
else:
# Vibrant mode: fewer clusters with colorfulness pre-filter
cluster_count = k
# For chroma scoring, filter to colorful pixels
# Filter to colorful pixels for smoother averaged results
filtered = []
for p in sampled:
try:
@@ -354,12 +362,17 @@ def extract_palette(
clusters = kmeans_cluster(filtered, k=cluster_count)
# Score colors based on method
# Vibrant (chroma): use centroid colors (averaged, smoother - original behavior)
# M3 (population): use representative colors (actual pixels - matugen behavior)
# - chroma: centroid colors (averaged, smoother - vibrant mode)
# - chroma-representative: representative pixels with chroma scoring (faithful mode)
# - population: representative colors with Material scoring (M3 schemes)
if scoring == "chroma":
# Use centroid colors for vibrant mode
# Use centroid colors for vibrant mode (smoother, blended)
colors_for_scoring = [(c[0], c[2]) for c in clusters]
scored = _score_colors_chroma(colors_for_scoring)
elif scoring == "chroma-representative":
# Use representative colors with chroma scoring (faithful mode)
colors_for_scoring = [(c[1], c[2]) for c in clusters]
scored = _score_colors_chroma(colors_for_scoring)
else:
# Use representative colors for M3 schemes
colors_for_scoring = [(c[1], c[2]) for c in clusters]
+15 -12
View File
@@ -21,7 +21,7 @@ from .palette import find_error_color
# Type aliases
ThemeMode = Literal["dark", "light"]
SchemeType = Literal["tonal-spot", "fruit-salad", "rainbow", "vibrant"]
SchemeType = Literal["tonal-spot", "fruit-salad", "rainbow", "vibrant", "faithful"]
# Map scheme type strings to classes
SCHEME_CLASSES = {
@@ -168,13 +168,14 @@ def generate_normal_dark(palette: list[Color]) -> dict[str, str]:
on_error = ensure_contrast(dark_fg, error, 7.0)
# "On" colors for containers - light text on dark containers, tinted with respective color
on_primary_container = ensure_contrast(Color.from_hsl(primary_h, primary_s, 0.90), primary_container, 4.5)
# Explicitly prefer_light=True since containers in dark mode are dark
on_primary_container = ensure_contrast(Color.from_hsl(primary_h, primary_s, 0.90), primary_container, 4.5, prefer_light=True)
sec_h, sec_s, _ = secondary.to_hsl()
on_secondary_container = ensure_contrast(Color.from_hsl(sec_h, sec_s, 0.90), secondary_container, 4.5)
on_secondary_container = ensure_contrast(Color.from_hsl(sec_h, sec_s, 0.90), secondary_container, 4.5, prefer_light=True)
ter_h, ter_s, _ = tertiary.to_hsl()
on_tertiary_container = ensure_contrast(Color.from_hsl(ter_h, ter_s, 0.90), tertiary_container, 4.5)
on_tertiary_container = ensure_contrast(Color.from_hsl(ter_h, ter_s, 0.90), tertiary_container, 4.5, prefer_light=True)
err_h, err_s, _ = error.to_hsl()
on_error_container = ensure_contrast(Color.from_hsl(err_h, err_s, 0.90), error_container, 4.5)
on_error_container = ensure_contrast(Color.from_hsl(err_h, err_s, 0.90), error_container, 4.5, prefer_light=True)
# Shadow and scrim
shadow = surface
@@ -360,14 +361,15 @@ def generate_normal_light(palette: list[Color]) -> dict[str, str]:
on_error = ensure_contrast(light_fg, error, 7.0)
# "On" colors for containers - dark text on light containers, tinted with respective color
# Explicitly prefer_light=False since containers in light mode are light
primary_h, primary_s, _ = primary.to_hsl()
on_primary_container = ensure_contrast(Color.from_hsl(primary_h, primary_s, 0.15), primary_container, 4.5)
on_primary_container = ensure_contrast(Color.from_hsl(primary_h, primary_s, 0.15), primary_container, 4.5, prefer_light=False)
sec_h, sec_s, _ = secondary.to_hsl()
on_secondary_container = ensure_contrast(Color.from_hsl(sec_h, sec_s, 0.15), secondary_container, 4.5)
on_secondary_container = ensure_contrast(Color.from_hsl(sec_h, sec_s, 0.15), secondary_container, 4.5, prefer_light=False)
ter_h, ter_s, _ = tertiary.to_hsl()
on_tertiary_container = ensure_contrast(Color.from_hsl(ter_h, ter_s, 0.15), tertiary_container, 4.5)
on_tertiary_container = ensure_contrast(Color.from_hsl(ter_h, ter_s, 0.15), tertiary_container, 4.5, prefer_light=False)
err_h, err_s, _ = error.to_hsl()
on_error_container = ensure_contrast(Color.from_hsl(err_h, err_s, 0.15), error_container, 4.5)
on_error_container = ensure_contrast(Color.from_hsl(err_h, err_s, 0.15), error_container, 4.5, prefer_light=False)
# Fixed colors - high-chroma accents consistent across light/dark
# In light mode: darker versions of accent colors
@@ -482,13 +484,14 @@ def generate_theme(
Args:
palette: List of extracted colors
mode: "dark" or "light"
scheme_type: One of "tonal-spot", "fruit-salad", "rainbow", "vibrant"
scheme_type: One of "tonal-spot", "fruit-salad", "rainbow", "vibrant", "faithful"
Returns:
Dictionary of color token names to hex values
"""
# Handle vibrant mode separately (uses legacy generate_normal_* functions)
if scheme_type == "vibrant":
# Handle vibrant/faithful modes (use generate_normal_* functions)
# Both use same theme generation, but different color extraction (handled in palette.py)
if scheme_type in ("vibrant", "faithful"):
if mode == "dark":
return generate_normal_dark(palette)
return generate_normal_light(palette)
@@ -8,7 +8,8 @@ Supported scheme types:
- tonal-spot: Default Android 12-13 Material You scheme (recommended)
- fruit-salad: Bold/playful with -50° hue rotation
- rainbow: Chromatic accents with grayscale neutrals
- vibrant: Preserves wallpaper colors directly
- vibrant: Colorful with smooth blended colors
- faithful: Colorful with actual wallpaper pixels
Usage:
python3 template-processor.py IMAGE_OR_JSON [OPTIONS]
@@ -73,7 +74,7 @@ Examples:
# Scheme type selection
parser.add_argument(
'--scheme-type',
choices=['tonal-spot', 'fruit-salad', 'rainbow', 'vibrant'],
choices=['tonal-spot', 'fruit-salad', 'rainbow', 'vibrant', 'faithful'],
default='tonal-spot',
help='Color scheme type (default: tonal-spot)'
)
@@ -256,10 +257,16 @@ def main() -> int:
scheme_type = "tonal-spot"
# Extract palette with appropriate scoring method
# vibrant mode uses chroma-based scoring (picks most colorful colors)
# M3 schemes use population-based scoring (picks most representative colors)
# - vibrant: chroma scoring with centroid averaging (smooth blended colors)
# - faithful: chroma scoring with representative pixels (actual wallpaper colors)
# - M3 schemes: population scoring (most representative colors)
k = 5
scoring = "chroma" if scheme_type == "vibrant" else "population"
if scheme_type == "vibrant":
scoring = "chroma"
elif scheme_type == "faithful":
scoring = "chroma-representative"
else:
scoring = "population"
palette = extract_palette(pixels, k=k, scoring=scoring)
if not palette:
+1 -1
View File
@@ -255,7 +255,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", "fruit-salad", "rainbow", "vibrant"];
const validTypes = ["tonal-spot", "fruit-salad", "rainbow", "vibrant", "faithful"];
return validTypes.includes(method) ? method : "tonal-spot";
}