mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
template-processor: added new "faithful" mode + fixed contrast of container vs on_container
This commit is contained in:
@@ -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 => {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user