617 lines
27 KiB
Python
617 lines
27 KiB
Python
import math
|
|
from typing import Optional, List
|
|
|
|
class AvatarGenerator:
|
|
DEFAULT_COLORS = ["#000000", "#8f1414", "#e50e0e", "#f3450f", "#fcac03"]
|
|
DEFAULT_SIZE = 80
|
|
DEFAULT_VARIANT = 'marble'
|
|
VALID_VARIANTS = {'marble', 'beam', 'pixel', 'sunset', 'ring', 'bauhaus'}
|
|
|
|
def __init__(self, default_colors: Optional[List[str]] = None, default_size: Optional[int] = None, default_variant: Optional[str] = None):
|
|
self.colors = default_colors if default_colors else self.DEFAULT_COLORS
|
|
self.size = default_size if default_size else self.DEFAULT_SIZE
|
|
self.variant = default_variant if default_variant else self.DEFAULT_VARIANT
|
|
|
|
self.AVATAR_GENERATORS = {
|
|
'marble': self._generate_marble_avatar,
|
|
'beam': self._generate_beam_avatar,
|
|
'pixel': self._generate_pixel_avatar,
|
|
'sunset': self._generate_sunset_avatar,
|
|
'ring': self._generate_ring_avatar,
|
|
'bauhaus': self._generate_bauhaus_avatar,
|
|
}
|
|
|
|
@staticmethod
|
|
def _hash_code(name: str) -> int:
|
|
"""Generate hash from name string - exact port of hashCode function"""
|
|
try:
|
|
if not name or not isinstance(name, str):
|
|
# Consider raising a TypeError or returning a default hash for invalid input
|
|
return 12345 # Fallback for invalid input
|
|
hash_val = 0
|
|
for char in name:
|
|
hash_val = (hash_val << 5) - hash_val + ord(char)
|
|
hash_val &= hash_val # Convert to 32bit integer
|
|
return abs(hash_val)
|
|
except Exception:
|
|
return 12345 # fallback hash value
|
|
|
|
@staticmethod
|
|
def _get_modulus(num: int, max_val: int) -> int:
|
|
"""Get modulus"""
|
|
return num % max_val
|
|
|
|
@staticmethod
|
|
def _get_digit(number: int, ntn: int) -> int:
|
|
"""Get digit at position"""
|
|
try:
|
|
if not isinstance(number, int) or not isinstance(ntn, int):
|
|
return 0 # Fallback for invalid input type
|
|
return int((number / (10 ** ntn)) % 10)
|
|
except (ZeroDivisionError, ValueError, OverflowError):
|
|
return 0
|
|
|
|
@staticmethod
|
|
def _get_boolean(number: int, ntn: int) -> bool:
|
|
"""Get boolean from digit"""
|
|
return not ((AvatarGenerator._get_digit(number, ntn)) % 2)
|
|
|
|
@staticmethod
|
|
def _get_angle(x: float, y: float) -> float:
|
|
"""Get angle from coordinates"""
|
|
return math.atan2(y, x) * 180 / math.pi
|
|
|
|
@staticmethod
|
|
def _get_unit(number: int, range_val: int, index: int = None) -> int:
|
|
"""Get unit value with optional negative"""
|
|
try:
|
|
if not isinstance(number, int) or not isinstance(range_val, int) or range_val == 0:
|
|
return 0 # Fallback for invalid input
|
|
value = number % range_val
|
|
if index and ((AvatarGenerator._get_digit(number, index) % 2) == 0):
|
|
return -value
|
|
return value
|
|
except (ZeroDivisionError, ValueError, TypeError):
|
|
return 0
|
|
|
|
@staticmethod
|
|
def _get_random_color(number: int, colors: List[str], range_val: int) -> str:
|
|
"""Get random color from palette"""
|
|
try:
|
|
if not colors or range_val == 0:
|
|
return AvatarGenerator.DEFAULT_COLORS[0] # Fallback
|
|
return colors[number % range_val]
|
|
except (IndexError, TypeError, ZeroDivisionError):
|
|
return AvatarGenerator.DEFAULT_COLORS[0]
|
|
|
|
@staticmethod
|
|
def _get_contrast(hexcolor: str) -> str:
|
|
"""Get contrasting color (black or white)"""
|
|
try:
|
|
if not hexcolor or not isinstance(hexcolor, str):
|
|
return '#000000' # Fallback
|
|
|
|
# Remove leading # if present
|
|
if hexcolor.startswith('#'):
|
|
hexcolor = hexcolor[1:]
|
|
|
|
# Clean up any remaining quotes or whitespace
|
|
hexcolor = hexcolor.strip().replace('"', '').replace("'", "")
|
|
|
|
# Ensure we have exactly 6 hex characters
|
|
if len(hexcolor) != 6:
|
|
return '#000000' # Fallback
|
|
|
|
# Validate hex characters
|
|
try:
|
|
int(hexcolor, 16)
|
|
except ValueError:
|
|
return '#000000' # Fallback
|
|
|
|
# Convert to RGB
|
|
r = int(hexcolor[0:2], 16)
|
|
g = int(hexcolor[2:4], 16)
|
|
b = int(hexcolor[4:6], 16)
|
|
|
|
# Get YIQ ratio
|
|
yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000
|
|
|
|
# Check contrast
|
|
return '#000000' if yiq >= 128 else '#FFFFFF'
|
|
except Exception:
|
|
return '#000000'
|
|
|
|
@staticmethod
|
|
def normalize_colors(colors_param: Optional[str]) -> List[str]:
|
|
"""Normalize color palette from query parameter"""
|
|
try:
|
|
if not colors_param:
|
|
return AvatarGenerator.DEFAULT_COLORS
|
|
|
|
# Clean up URL encoding
|
|
cleaned = colors_param.replace('%22', '"').replace('%20', ' ').replace('%5B', '[').replace('%5D', ']').strip()
|
|
|
|
# Extract colors from different formats
|
|
color_list = AvatarGenerator._extract_color_strings(cleaned)
|
|
|
|
# Validate and normalize colors
|
|
return AvatarGenerator._validate_color_list(color_list)
|
|
|
|
except Exception:
|
|
return AvatarGenerator.DEFAULT_COLORS
|
|
|
|
@staticmethod
|
|
def _extract_color_strings(cleaned_colors: str) -> List[str]:
|
|
"""Extract color strings from cleaned input"""
|
|
# Handle array format: ["#xxxxxx", "#xxxxxx", ...]
|
|
if cleaned_colors.startswith('[') and cleaned_colors.endswith(']'):
|
|
inner_content = cleaned_colors[1:-1].strip()
|
|
if not inner_content:
|
|
return []
|
|
return [part.strip().replace('"', '').replace("'", '') for part in inner_content.split(',')]
|
|
|
|
# Handle comma-separated format: #xxxxxx, #xxxxxx, ...
|
|
return [part.strip().replace('"', '').replace("'", '') for part in cleaned_colors.split(',')]
|
|
|
|
@staticmethod
|
|
def _validate_color_list(color_list: List[str]) -> List[str]:
|
|
"""Validate and normalize hex colors"""
|
|
normalized_colors = []
|
|
|
|
for color in color_list:
|
|
color = color.strip()
|
|
if not color:
|
|
continue
|
|
if not color.startswith('#'):
|
|
color = '#' + color
|
|
if AvatarGenerator._is_valid_hex_color(color):
|
|
normalized_colors.append(color)
|
|
|
|
return normalized_colors if normalized_colors else AvatarGenerator.DEFAULT_COLORS
|
|
|
|
@staticmethod
|
|
def _is_valid_hex_color(color: str) -> bool:
|
|
"""Check if string is valid hex color"""
|
|
return len(color) == 7 and all(c in '0123456789ABCDEFabcdef' for c in color[1:])
|
|
|
|
def _generate_marble_avatar(self, name: str, colors: List[str], size: int, square: bool, title: bool) -> str:
|
|
"""Generate marble variant avatar"""
|
|
ELEMENTS = 3
|
|
SIZE = 80 # Intrinsic size of the SVG design
|
|
|
|
def generate_colors_marble(name: str, current_colors: List[str]):
|
|
num_from_name = self._hash_code(name)
|
|
range_val = len(current_colors) if current_colors else len(self.DEFAULT_COLORS)
|
|
|
|
elements_properties = []
|
|
for i in range(ELEMENTS):
|
|
elements_properties.append({
|
|
'color': self._get_random_color(num_from_name + i, current_colors, range_val),
|
|
'translateX': self._get_unit(num_from_name * (i + 1), SIZE // 10, 1),
|
|
'translateY': self._get_unit(num_from_name * (i + 1), SIZE // 10, 2),
|
|
'scale': 1.2 + self._get_unit(num_from_name * (i + 1), SIZE // 20) / 10,
|
|
'rotate': self._get_unit(num_from_name * (i + 1), 360, 1)
|
|
})
|
|
|
|
return elements_properties
|
|
|
|
properties = generate_colors_marble(name, colors)
|
|
mask_id = f"mask_{self._hash_code(name)}"
|
|
filter_id = f"filter_{self._hash_code(name)}"
|
|
|
|
# Use the passed 'size' for width/height, internal 'SIZE' for viewBox
|
|
svg = f'''<svg viewBox="0 0 {SIZE} {SIZE}" fill="none" role="img" xmlns="http://www.w3.org/2000/svg" width="{size}" height="{size}">'''
|
|
|
|
if title:
|
|
svg += f'<title>{name}</title>'
|
|
|
|
svg += f'''
|
|
<mask id="{mask_id}" maskUnits="userSpaceOnUse" x="0" y="0" width="{SIZE}" height="{SIZE}">
|
|
<rect width="{SIZE}" height="{SIZE}" {"" if square else f'rx="{SIZE * 2}"'} fill="#FFFFFF" />
|
|
</mask>
|
|
<g mask="url(#{mask_id})">
|
|
<rect width="{SIZE}" height="{SIZE}" fill="{properties[0]['color']}" />
|
|
<path
|
|
filter="url(#{filter_id})"
|
|
d="M32.414 59.35L50.376 70.5H72.5v-71H33.728L26.5 13.381l19.057 27.08L32.414 59.35z"
|
|
fill="{properties[1]['color']}"
|
|
transform="translate({properties[1]['translateX']} {properties[1]['translateY']}) rotate({properties[1]['rotate']} {SIZE // 2} {SIZE // 2}) scale({properties[1]['scale']})"
|
|
/>
|
|
<path
|
|
filter="url(#{filter_id})"
|
|
style="mix-blend-mode: overlay;"
|
|
d="M22.216 24L0 46.75l14.108 38.129L78 86l-3.081-59.276-22.378 4.005 12.972 20.186-23.35 27.395L22.215 24z"
|
|
fill="{properties[2]['color']}"
|
|
transform="translate({properties[2]['translateX']} {properties[2]['translateY']}) rotate({properties[2]['rotate']} {SIZE // 2} {SIZE // 2}) scale({properties[2]['scale']})"
|
|
/>
|
|
</g>
|
|
<defs>
|
|
<filter
|
|
id="{filter_id}"
|
|
filterUnits="userSpaceOnUse"
|
|
colorInterpolationFilters="sRGB"
|
|
>
|
|
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
|
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
|
<feGaussianBlur stdDeviation="7" result="effect1_foregroundBlur" />
|
|
</filter>
|
|
</defs>
|
|
</svg>'''
|
|
|
|
return svg
|
|
|
|
def _generate_beam_avatar(self, name: str, colors: List[str], size: int, square: bool, title: bool) -> str:
|
|
"""Generate beam variant avatar"""
|
|
SIZE = 36 # Intrinsic size of the SVG design
|
|
|
|
def generate_data_beam(name: str, current_colors: List[str]):
|
|
num_from_name = self._hash_code(name)
|
|
range_val = len(current_colors) if current_colors else len(self.DEFAULT_COLORS)
|
|
wrapper_color = self._get_random_color(num_from_name, current_colors, range_val)
|
|
pre_translate_x = self._get_unit(num_from_name, 10, 1)
|
|
wrapper_translate_x = pre_translate_x + SIZE // 9 if pre_translate_x < 5 else pre_translate_x
|
|
pre_translate_y = self._get_unit(num_from_name, 10, 2)
|
|
wrapper_translate_y = pre_translate_y + SIZE // 9 if pre_translate_y < 5 else pre_translate_y
|
|
|
|
data = {
|
|
'wrapperColor': wrapper_color,
|
|
'faceColor': self._get_contrast(wrapper_color),
|
|
'backgroundColor': self._get_random_color(num_from_name + 13, current_colors, range_val),
|
|
'wrapperTranslateX': wrapper_translate_x,
|
|
'wrapperTranslateY': wrapper_translate_y,
|
|
'wrapperRotate': self._get_unit(num_from_name, 360),
|
|
'wrapperScale': 1 + self._get_unit(num_from_name, SIZE // 12) / 10,
|
|
'isMouthOpen': self._get_boolean(num_from_name, 2),
|
|
'isCircle': self._get_boolean(num_from_name, 1),
|
|
'eyeSpread': self._get_unit(num_from_name, 5),
|
|
'mouthSpread': self._get_unit(num_from_name, 3),
|
|
'faceRotate': self._get_unit(num_from_name, 10, 3),
|
|
'faceTranslateX': wrapper_translate_x // 2 if wrapper_translate_x > SIZE // 6 else self._get_unit(num_from_name, 8, 1),
|
|
'faceTranslateY': wrapper_translate_y // 2 if wrapper_translate_y > SIZE // 6 else self._get_unit(num_from_name, 7, 2),
|
|
}
|
|
|
|
return data
|
|
|
|
data = generate_data_beam(name, colors)
|
|
mask_id = f"mask_{self._hash_code(name)}"
|
|
|
|
svg = f'''<svg viewBox="0 0 {SIZE} {SIZE}" fill="none" role="img" xmlns="http://www.w3.org/2000/svg" width="{size}" height="{size}">'''
|
|
|
|
if title:
|
|
svg += f'<title>{name}</title>'
|
|
|
|
rx_value = SIZE if data['isCircle'] else SIZE // 6
|
|
|
|
svg += f'''
|
|
<mask id="{mask_id}" maskUnits="userSpaceOnUse" x="0" y="0" width="{SIZE}" height="{SIZE}">
|
|
<rect width="{SIZE}" height="{SIZE}" {"" if square else f'rx="{SIZE * 2}"'} fill="#FFFFFF" />
|
|
</mask>
|
|
<g mask="url(#{mask_id})">
|
|
<rect width="{SIZE}" height="{SIZE}" fill="{data['backgroundColor']}" />
|
|
<rect
|
|
x="0"
|
|
y="0"
|
|
width="{SIZE}"
|
|
height="{SIZE}"
|
|
transform="translate({data['wrapperTranslateX']} {data['wrapperTranslateY']}) rotate({data['wrapperRotate']} {SIZE // 2} {SIZE // 2}) scale({data['wrapperScale']})"
|
|
fill="{data['wrapperColor']}"
|
|
rx="{rx_value}"
|
|
/>
|
|
<g transform="translate({data['faceTranslateX']} {data['faceTranslateY']}) rotate({data['faceRotate']} {SIZE // 2} {SIZE // 2})">'''
|
|
|
|
if data['isMouthOpen']:
|
|
svg += f'''
|
|
<path
|
|
d="M15 {19 + data['mouthSpread']}c2 1 4 1 6 0"
|
|
stroke="{data['faceColor']}"
|
|
fill="none"
|
|
strokeLinecap="round"
|
|
/>'''
|
|
else:
|
|
svg += f'''
|
|
<path
|
|
d="M13,{19 + data['mouthSpread']} a1,0.75 0 0,0 10,0"
|
|
fill="{data['faceColor']}"
|
|
/>'''
|
|
|
|
svg += f'''
|
|
<rect
|
|
x="{14 - data['eyeSpread']}"
|
|
y="14"
|
|
width="1.5"
|
|
height="2"
|
|
rx="1"
|
|
stroke="none"
|
|
fill="{data['faceColor']}"
|
|
/>
|
|
<rect
|
|
x="{20 + data['eyeSpread']}"
|
|
y="14"
|
|
width="1.5"
|
|
height="2"
|
|
rx="1"
|
|
stroke="none"
|
|
fill="{data['faceColor']}"
|
|
/>
|
|
</g>
|
|
</g>
|
|
</svg>'''
|
|
|
|
return svg
|
|
|
|
def _generate_pixel_avatar(self, name: str, colors: List[str], size: int, square: bool, title: bool) -> str:
|
|
"""Generate pixel variant avatar"""
|
|
ELEMENTS = 64 # 8x8 grid
|
|
SIZE = 80 # Intrinsic SVG size
|
|
|
|
def generate_colors_pixel(name: str, current_colors: List[str]):
|
|
num_from_name = self._hash_code(name)
|
|
# Use current_colors if provided, otherwise fallback to instance default, then class default
|
|
final_colors = current_colors if current_colors else self.colors
|
|
range_val = len(final_colors)
|
|
|
|
color_list = []
|
|
for i in range(ELEMENTS):
|
|
color_list.append(self._get_random_color(num_from_name + i, final_colors, range_val))
|
|
return color_list
|
|
|
|
pixel_colors = generate_colors_pixel(name, colors)
|
|
mask_id = f"mask_{self._hash_code(name)}"
|
|
|
|
svg = f'''<svg viewBox="0 0 {SIZE} {SIZE}" fill="none" role="img" xmlns="http://www.w3.org/2000/svg" width="{size}" height="{size}">'''
|
|
|
|
if title:
|
|
svg += f'<title>{name}</title>'
|
|
|
|
svg += f'''
|
|
<mask id="{mask_id}" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="{SIZE}" height="{SIZE}">
|
|
<rect width="{SIZE}" height="{SIZE}" {"" if square else f'rx="{SIZE * 2}"'} fill="#FFFFFF" />
|
|
</mask>
|
|
<g mask="url(#{mask_id})">'''
|
|
|
|
# Generate 8x8 grid of pixels
|
|
idx = 0
|
|
pixel_size = SIZE // 8 # Each pixel is 10x10 if SIZE is 80
|
|
for row in range(8):
|
|
for col in range(8):
|
|
svg += f'''
|
|
<rect x="{col * pixel_size}" y="{row * pixel_size}" width="{pixel_size}" height="{pixel_size}" fill="{pixel_colors[idx]}" />'''
|
|
idx +=1
|
|
|
|
svg += '''
|
|
</g>
|
|
</svg>'''
|
|
|
|
return svg
|
|
|
|
def _generate_sunset_avatar(self, name: str, colors: List[str], size: int, square: bool, title: bool) -> str:
|
|
"""Generate sunset variant avatar"""
|
|
ELEMENTS = 4 # For 4 gradient stops
|
|
SIZE = 80 # Intrinsic SVG size
|
|
|
|
def generate_colors_sunset(name: str, current_colors: List[str]):
|
|
num_from_name = self._hash_code(name)
|
|
# Use current_colors if provided, otherwise fallback to instance default, then class default
|
|
final_colors = current_colors if current_colors else self.colors
|
|
# Ensure we have at least 4 colors for sunset, repeat if necessary
|
|
if len(final_colors) < ELEMENTS:
|
|
final_colors = (final_colors * (ELEMENTS // len(final_colors) + 1))[:ELEMENTS]
|
|
|
|
range_val = len(final_colors)
|
|
|
|
color_list = []
|
|
for i in range(ELEMENTS):
|
|
color_list.append(self._get_random_color(num_from_name + i, final_colors, range_val))
|
|
return color_list
|
|
|
|
sunset_colors = generate_colors_sunset(name, colors)
|
|
# name_without_space = name.replace(' ', '').replace('-', '').replace('_', '') # Not used
|
|
hash_val = abs(self._hash_code(name))
|
|
mask_id = f"mask_{hash_val}"
|
|
gradient_id_1 = f"gradient_paint0_linear_{hash_val}"
|
|
gradient_id_2 = f"gradient_paint1_linear_{hash_val}"
|
|
|
|
rx_value = '' if square else f'rx="{SIZE * 2}"'
|
|
|
|
svg = f'''<svg viewBox="0 0 {SIZE} {SIZE}" fill="none" role="img" xmlns="http://www.w3.org/2000/svg" width="{size}" height="{size}">'''
|
|
|
|
if title:
|
|
svg += f'<title>{name}</title>'
|
|
|
|
svg += f'''
|
|
<defs>
|
|
<linearGradient
|
|
id="{gradient_id_1}"
|
|
x1="{SIZE // 2}"
|
|
y1="0"
|
|
x2="{SIZE // 2}"
|
|
y2="{SIZE // 2}"
|
|
gradientUnits="userSpaceOnUse"
|
|
>
|
|
<stop stop-color="{sunset_colors[0]}" />
|
|
<stop offset="1" stop-color="{sunset_colors[1]}" />
|
|
</linearGradient>
|
|
<linearGradient
|
|
id="{gradient_id_2}"
|
|
x1="{SIZE // 2}"
|
|
y1="{SIZE // 2}"
|
|
x2="{SIZE // 2}"
|
|
y2="{SIZE}"
|
|
gradientUnits="userSpaceOnUse"
|
|
>
|
|
<stop stop-color="{sunset_colors[2]}" />
|
|
<stop offset="1" stop-color="{sunset_colors[3]}" />
|
|
</linearGradient>
|
|
</defs>
|
|
<mask id="{mask_id}" maskUnits="userSpaceOnUse" x="0" y="0" width="{SIZE}" height="{SIZE}">
|
|
<rect width="{SIZE}" height="{SIZE}" {rx_value} fill="#FFFFFF" />
|
|
</mask>
|
|
<g mask="url(#{mask_id})">
|
|
<path fill="url(#{gradient_id_1})" d="M0 0h{SIZE}v{SIZE//2}H0z" />
|
|
<path fill="url(#{gradient_id_2})" d="M0 {SIZE//2}h{SIZE}v{SIZE//2}H0z" />
|
|
</g>
|
|
</svg>'''
|
|
|
|
return svg
|
|
|
|
def _generate_ring_avatar(self, name: str, colors: List[str], size: int, square: bool, title: bool) -> str:
|
|
"""Generate ring variant avatar"""
|
|
SIZE = 90 # Intrinsic SVG size
|
|
COLORS_NEEDED = 9 # Number of colors used in the SVG paths
|
|
|
|
def generate_colors_ring(name: str, current_colors: List[str]):
|
|
num_from_name = self._hash_code(name)
|
|
# Use current_colors if provided, otherwise fallback to instance default, then class default
|
|
final_colors = current_colors if current_colors else self.colors
|
|
# Ensure we have enough colors, repeat if necessary
|
|
if len(final_colors) < COLORS_NEEDED:
|
|
final_colors = (final_colors * (COLORS_NEEDED // len(final_colors) + 1))[:COLORS_NEEDED]
|
|
|
|
range_val = len(final_colors)
|
|
|
|
ring_palette = []
|
|
for i in range(COLORS_NEEDED):
|
|
ring_palette.append(self._get_random_color(num_from_name + i, final_colors, range_val))
|
|
return ring_palette
|
|
|
|
ring_colors = generate_colors_ring(name, colors)
|
|
mask_id = f"mask_{self._hash_code(name)}"
|
|
|
|
svg = f'''<svg viewBox="0 0 {SIZE} {SIZE}" fill="none" role="img" xmlns="http://www.w3.org/2000/svg" width="{size}" height="{size}">'''
|
|
|
|
if title:
|
|
svg += f'<title>{name}</title>'
|
|
|
|
svg += f'''
|
|
<mask id="{mask_id}" maskUnits="userSpaceOnUse" x="0" y="0" width="{SIZE}" height="{SIZE}">
|
|
<rect width="{SIZE}" height="{SIZE}" {"" if square else f'rx="{SIZE * 2}"'} fill="#FFFFFF" />
|
|
</mask>
|
|
<g mask="url(#{mask_id})">
|
|
<path d="M0 0h{SIZE}v{SIZE//2}H0z" fill="{ring_colors[0]}" />
|
|
<path d="M0 {SIZE//2}h{SIZE}v{SIZE//2}H0z" fill="{ring_colors[1]}" />
|
|
<path d="M{SIZE-7} {SIZE//2}a{SIZE//2-2} {SIZE//2-2} 0 00-{SIZE-14} 0h{SIZE-14}z" fill="{ring_colors[2]}" />
|
|
<path d="M{SIZE-7} {SIZE//2}a{SIZE//2-2} {SIZE//2-2} 0 01-{SIZE-14} 0h{SIZE-14}z" fill="{ring_colors[3]}" />
|
|
<path d="M{SIZE-13} {SIZE//2}a{SIZE//2-8} {SIZE//2-8} 0 10-{SIZE-26} 0h{SIZE-26}z" fill="{ring_colors[4]}" />
|
|
<path d="M{SIZE-13} {SIZE//2}a{SIZE//2-8} {SIZE//2-8} 0 11-{SIZE-26} 0h{SIZE-26}z" fill="{ring_colors[5]}" />
|
|
<path d="M{SIZE-19} {SIZE//2}a{SIZE//2-14} {SIZE//2-14} 0 00-{SIZE-38} 0h{SIZE-38}z" fill="{ring_colors[6]}" />
|
|
<path d="M{SIZE-19} {SIZE//2}a{SIZE//2-14} {SIZE//2-14} 0 01-{SIZE-38} 0h{SIZE-38}z" fill="{ring_colors[7]}" />
|
|
<circle cx="{SIZE//2}" cy="{SIZE//2}" r="{SIZE//2-22}" fill="{ring_colors[8]}" />
|
|
</g>
|
|
</svg>'''
|
|
|
|
return svg
|
|
|
|
def _generate_bauhaus_avatar(self, name: str, colors: List[str], size: int, square: bool, title: bool) -> str:
|
|
"""Generate bauhaus variant avatar"""
|
|
ELEMENTS = 4 # Number of geometric elements
|
|
SIZE = 80 # Intrinsic SVG size
|
|
|
|
def generate_colors_bauhaus(name: str, current_colors: List[str]):
|
|
num_from_name = self._hash_code(name)
|
|
# Use current_colors if provided, otherwise fallback to instance default, then class default
|
|
final_colors = current_colors if current_colors else self.colors
|
|
range_val = len(final_colors)
|
|
|
|
properties = []
|
|
for i in range(ELEMENTS):
|
|
properties.append({
|
|
'color': self._get_random_color(num_from_name + i, final_colors, range_val),
|
|
'translateX': self._get_unit(num_from_name * (i + 1), SIZE // 2 - (i + 17), 1),
|
|
'translateY': self._get_unit(num_from_name * (i + 1), SIZE // 2 - (i + 17), 2),
|
|
'rotate': self._get_unit(num_from_name * (i + 1), 360),
|
|
'isSquare': self._get_boolean(num_from_name, 2)
|
|
})
|
|
return properties
|
|
|
|
properties = generate_colors_bauhaus(name, colors)
|
|
mask_id = f"mask_{self._hash_code(name)}"
|
|
|
|
svg = f'''<svg viewBox="0 0 {SIZE} {SIZE}" fill="none" role="img" xmlns="http://www.w3.org/2000/svg" width="{size}" height="{size}">'''
|
|
|
|
if title:
|
|
svg += f'<title>{name}</title>'
|
|
|
|
svg += f'''
|
|
<mask id="{mask_id}" maskUnits="userSpaceOnUse" x="0" y="0" width="{SIZE}" height="{SIZE}">
|
|
<rect width="{SIZE}" height="{SIZE}" {"" if square else f'rx="{SIZE * 2}"'} fill="#FFFFFF" />
|
|
</mask>
|
|
<g mask="url(#{mask_id})">
|
|
<rect width="{SIZE}" height="{SIZE}" fill="{properties[0]['color']}" />
|
|
<rect
|
|
x="{(SIZE - 60) // 2}"
|
|
y="{(SIZE - 20) // 2}"
|
|
width="{SIZE}"
|
|
height="{SIZE if properties[1]['isSquare'] else SIZE // 8}"
|
|
fill="{properties[1]['color']}"
|
|
transform="translate({properties[1]['translateX']} {properties[1]['translateY']}) rotate({properties[1]['rotate']} {SIZE // 2} {SIZE // 2})"
|
|
/>
|
|
<circle
|
|
cx="{SIZE // 2}"
|
|
cy="{SIZE // 2}"
|
|
fill="{properties[2]['color']}"
|
|
r="{SIZE // 5}"
|
|
transform="translate({properties[2]['translateX']} {properties[2]['translateY']})"
|
|
/>
|
|
<line
|
|
x1="0"
|
|
y1="{SIZE // 2}"
|
|
x2="{SIZE}"
|
|
y2="{SIZE // 2}"
|
|
strokeWidth="2"
|
|
stroke="{properties[3]['color']}"
|
|
transform="translate({properties[3]['translateX']} {properties[3]['translateY']}) rotate({properties[3]['rotate']} {SIZE // 2} {SIZE // 2})"
|
|
/>
|
|
</g>
|
|
</svg>'''
|
|
|
|
return svg
|
|
|
|
def generate_avatar(self,
|
|
name: str,
|
|
variant: Optional[str] = None,
|
|
colors: Optional[List[str]] = None, # Allow passing colors as list of strings
|
|
colors_param: Optional[str] = None, # Allow passing colors as a string parameter
|
|
size: Optional[int] = None,
|
|
square: bool = False,
|
|
title: bool = True) -> str:
|
|
"""
|
|
Generate an SVG avatar.
|
|
|
|
Args:
|
|
name: The name to generate the avatar for.
|
|
variant: The avatar variant (e.g., 'marble', 'beam'). Uses instance default if None.
|
|
colors: A list of hex color strings. Overrides colors_param if provided.
|
|
colors_param: A string representation of colors (e.g., "['#FF0000', '#00FF00']" or "#FF0000,#00FF00").
|
|
Used if 'colors' list is not provided.
|
|
size: The desired size of the avatar in pixels. Uses instance default if None.
|
|
square: If True, generates a square avatar. Otherwise, a circular one.
|
|
title: If True, includes a <title> tag with the name in the SVG.
|
|
|
|
Returns:
|
|
An SVG string representing the avatar.
|
|
"""
|
|
current_variant = variant if variant and variant in self.VALID_VARIANTS else self.variant
|
|
current_size = size if size else self.size
|
|
|
|
# Determine colors: prioritize direct list, then string param, then instance default
|
|
if colors:
|
|
# Validate and normalize if a list is directly passed
|
|
final_colors = self._validate_color_list(colors)
|
|
elif colors_param:
|
|
final_colors = self.normalize_colors(colors_param)
|
|
else:
|
|
final_colors = self.colors # Use instance default colors
|
|
|
|
if not final_colors: # Ensure there's always a fallback
|
|
final_colors = self.DEFAULT_COLORS
|
|
|
|
|
|
generator_func = self.AVATAR_GENERATORS.get(current_variant)
|
|
|
|
if generator_func:
|
|
return generator_func(name, final_colors, current_size, square, title)
|
|
else:
|
|
# Fallback to default variant if the chosen one is somehow invalid after checks
|
|
default_gen_func = self.AVATAR_GENERATORS.get(self.DEFAULT_VARIANT)
|
|
return default_gen_func(name, final_colors, current_size, square, title)
|