feat(avatar): add SVG, PNG and HDR avatar exports

This commit is contained in:
2026-10-18 18:48:49 +08:00
parent 61d039beba
commit 899df27427
10 changed files with 1331 additions and 324 deletions
+54
View File
@@ -0,0 +1,54 @@
"""Luminance-controlled palettes shared by SDR and HDR output."""
import math
SRGB_Y = (0.2126, 0.7152, 0.0722)
BT2020_Y = (0.2627, 0.6780, 0.0593)
TO_2020 = ((0.6274039, 0.3292830, 0.0433131),
(0.0690973, 0.9195404, 0.0113623),
(0.0163914, 0.0880133, 0.8955953))
ORIGINAL_BLUE = '#3b73fa'
GRADIENT_STEPS = 32
def linear_srgb(color):
values = (int(color[i:i + 2], 16) / 255 for i in (1, 3, 5))
return tuple(v / 12.92 if v <= 0.04045 else ((v + .055) / 1.055) ** 2.4 for v in values)
def gradient_level(position):
"""32 perceptually separated levels, with exact dark and bright endpoints."""
t = round(min(1, max(0, position)) * (GRADIENT_STEPS - 1)) / (GRADIENT_STEPS - 1)
return .005 + .995 * t ** 2.2
SDR_PEAK_Y = sum(v * w for v, w in zip(linear_srgb(ORIGINAL_BLUE), SRGB_Y))
def equal_luminance(color, target, channel_limit, *, hdr=False):
"""Scale linear RGB equally, preserving chromaticity without adding white."""
rgb = linear_srgb(color)
weights = BT2020_Y if hdr else SRGB_Y
if hdr:
rgb = tuple(sum(a * b for a, b in zip(row, rgb)) for row in TO_2020)
luminance = sum(a * b for a, b in zip(rgb, weights))
if luminance <= 0:
return (target,) * 3
rgb = tuple(v * target / luminance for v in rgb)
maximum = max(rgb)
if maximum > channel_limit + 1e-6:
raise ValueError('Requested luminance exceeds the color-preserving channel limit')
return rgb
def sdr_color(base, level=1.0):
# Scale a fixed peak color to keep chromaticity constant along the trail.
peak = equal_luminance(base, SDR_PEAK_Y, 1.0)
rgb = [max(0, min(1, v * level)) for v in peak]
rgb = [12.92 * v if v <= .0031308 else 1.055 * v ** (1 / 2.4) - .055 for v in rgb]
return '#' + ''.join(f'{round(v * 255):02x}' for v in rgb)
def validate_peak(peak):
if not math.isfinite(peak) or not 100 <= peak <= 2000:
raise ValueError('--peak-nits must be between 100 and 2000')