Theme: add faithful (alternative)

This commit is contained in:
Lysec
2026-02-03 01:01:37 +01:00
parent 96351db6cf
commit 9da1cbd080
22 changed files with 184 additions and 105 deletions
+106 -1
View File
@@ -195,6 +195,28 @@ def _hue_to_family(hue: float) -> int:
return 5 # PURPLE
def _hue_to_family_alternative(hue: float) -> int:
"""
Alternative hue mapping for Alternate mode.
Splits Emerald/Mint from Teal/Cyan to prioritize the latter.
"""
if hue >= 330 or hue < 30:
return 0 # RED
elif hue < 60:
return 1 # ORANGE
elif hue < 105:
return 2 # YELLOW
elif hue < 155:
return 3 # GREEN
elif hue < 185:
return 4 # EMERALD/MINT
elif hue < 215:
return 5 # TEAL/CYAN
elif hue < 270:
return 6 # BLUE
else:
return 7 # PURPLE
def _score_colors_count(
colors_with_counts: list[tuple[RGB, int]],
) -> list[tuple[Color, float]]:
@@ -262,6 +284,84 @@ def _score_colors_count(
return result_colors
def _score_colors_count_alt(
colors_with_counts: list[tuple[RGB, int]],
) -> list[tuple[Color, float]]:
"""
Score colors prioritizing a 'Subject' color.
Logic:
1. Identify the most dominant family by area (usually the background).
2. Skip it if possible.
3. From the remaining families, pick the one with most vibrant colors.
NOTE: We give a slight priority boost to TEAL/CYAN (5) to catch
specific character subjects.
"""
MIN_CHROMA = 10.0
hue_families: dict[int, list[tuple[Color, float, float, int]]] = {}
for rgb, count in colors_with_counts:
color = Color.from_rgb(rgb)
try:
hct = color.to_hct()
if hct.chroma >= MIN_CHROMA:
family = _hue_to_family_alternative(hct.hue)
if family not in hue_families:
hue_families[family] = []
hue_families[family].append((color, hct.hue, hct.chroma, count))
except (ValueError, ZeroDivisionError):
pass
if not hue_families:
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])
if len(result) > 1:
return [result[1]] + [result[0]] + result[2:]
return result
area_rank = []
for f, colors in hue_families.items():
area_rank.append((f, sum(c[3] for c in colors)))
area_rank.sort(key=lambda x: -x[1])
remaining_families = [f for f, _ in area_rank]
dominant_family = remaining_families[0]
if len(remaining_families) > 1:
remaining_families = remaining_families[1:]
vibrancy_rank = []
for f in remaining_families:
mx_chroma = max(c[2] for c in hue_families[f])
# Significant boost for Teal (5) vs Emerald (4) to ensure subject focus
# This helps target "Teal" characters even if Emerald backgrounds are vibrant.
weight = mx_chroma * (2.0 if f == 5 else 1.0)
vibrancy_rank.append((f, weight))
vibrancy_rank.sort(key=lambda x: -x[1])
best_family = vibrancy_rank[0][0]
result_families = [best_family]
if dominant_family != best_family:
result_families.append(dominant_family)
for f in [fr[0] for fr in area_rank]:
if f not in result_families:
result_families.append(f)
result_colors = []
for i, family in enumerate(result_families):
family_colors = hue_families[family]
family_colors.sort(key=lambda x: (-x[3], -x[2]))
for color, _, chroma, count in family_colors:
score = (len(result_families) - i) * 1000000 + count * 1000 + chroma
result_colors.append((color, score))
result_colors.sort(key=lambda x: -x[1])
return result_colors
def _score_colors_muted(
colors_with_counts: list[tuple[RGB, int]],
) -> list[tuple[Color, float]]:
@@ -434,6 +534,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)
- "count-alt": area-dominant, picks SECOND most prominent family
- "muted": like count but without chroma filtering (monochrome wallpapers)
Returns:
@@ -451,7 +552,7 @@ 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 == "count":
elif scoring in ("count", "count-alt"):
# Faithful mode: many clusters to capture color diversity, no pre-filtering
# Scoring will filter to colorful colors and pick by count
cluster_count = 48
@@ -494,6 +595,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 == "count-alt":
# Use representative colors with count scoring, but 2nd family (faithful-alt mode)
colors_for_scoring = [(c[1], c[2]) for c in clusters]
scored = _score_colors_count_alt(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]
+1 -1
View File
@@ -860,7 +860,7 @@ def generate_theme(
"""
# 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 scheme_type in ("vibrant", "faithful", "faithful-alt"):
if mode == "dark":
return generate_normal_dark(palette)
return generate_normal_light(palette)
@@ -12,6 +12,7 @@ Supported scheme types:
- monochrome: Pure grayscale M3 scheme (chroma = 0, only error has color)
- vibrant: Prioritizes the most saturated colors regardless of area coverage
- faithful: Prioritizes dominant colors by area, what you see is what you get
- faithful-alt: Like faithful, but prioritizes the 2nd most prominent color family
- muted: Preserves hue but caps saturation low (for monochrome/monotonal wallpapers)
Usage:
@@ -83,7 +84,7 @@ Examples:
# Scheme type selection
parser.add_argument(
'--scheme-type',
choices=['tonal-spot', 'content', 'fruit-salad', 'rainbow', 'monochrome', 'vibrant', 'faithful', 'muted'],
choices=['tonal-spot', 'content', 'fruit-salad', 'rainbow', 'monochrome', 'vibrant', 'faithful', 'faithful-alt', 'muted'],
default='tonal-spot',
help='Color scheme type (default: tonal-spot)'
)
@@ -275,6 +276,9 @@ 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 == "faithful-alt":
# K-means with count-alt scoring - picks 2nd dominant family
palette = extract_palette(pixels, k=5, scoring="count-alt")
elif scheme_type == "muted":
# K-means with muted scoring - accepts low/zero chroma colors
# For monochrome/monotonal wallpapers where dominant color has low saturation