avatar generator
All checks were successful
Deploy / deploy (push) Successful in 1m39s

This commit is contained in:
2026-07-27 01:36:24 +02:00
parent e274e07f45
commit 708830fe0a
11 changed files with 26 additions and 18 deletions

156
src/App/Avatar/Lorelei.php Normal file
View File

@@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
namespace App\Avatar;
final class Lorelei
{
public static function defaults(): array
{
return [
'avatar_preset' => 'papa-kind-treff',
'avatar_skin_tone' => 'warm',
'avatar_hair_style' => 'short',
'avatar_hair_color' => 'brown',
'avatar_eye_color' => 'brown',
'avatar_beard_style' => 'none',
'avatar_glasses_style' => 'none',
'avatar_head_shape' => 'round',
'avatar_hat_style' => 'none',
'avatar_hat_color' => 'navy',
'avatar_outfit_style' => 'tee',
'avatar_outfit_color' => 'slate',
];
}
public static function options(): array
{
return [];
}
public static function selectColumns(string $alias = 'p'): string
{
$columns = array_keys(self::defaults());
return implode(', ', array_map(
static fn(string $column): string => sprintf('%s.%s', $alias, $column),
$columns
));
}
public static function normalize(array $input): array
{
$defaults = self::defaults();
$normalized = [];
foreach ($defaults as $key => $defaultValue) {
$value = (string)($input[$key] ?? $defaultValue);
$normalized[$key] = $key === 'avatar_preset'
? self::normalizeSeed($value)
: $value;
}
return $normalized;
}
public static function normalizeSeed(string $seed): string
{
$seed = strtolower(trim($seed));
$seed = preg_replace('/[^a-z0-9]+/', '-', $seed) ?: '';
$seed = trim($seed, '-');
if ($seed === '') {
$seed = self::defaults()['avatar_preset'];
}
return substr($seed, 0, 64);
}
public static function seedChoices(string $selectedSeed, int $count = 24): array
{
$selectedSeed = self::normalizeSeed($selectedSeed);
$prefixes = [
'abend', 'ahorn', 'anker', 'atlas', 'berg', 'blick', 'brise', 'echo',
'eiche', 'fjord', 'fluss', 'fokus', 'hafen', 'herz', 'horizont', 'insel',
'kiesel', 'kompass', 'kraft', 'linie', 'licht', 'lotse', 'mond', 'morgen',
'nord', 'pfad', 'quelle', 'rauch', 'runde', 'sand', 'sommer', 'stein',
'sturm', 'tal', 'ufer', 'wald', 'welle', 'wind', 'winkel', 'zeit',
];
$suffixes = [
'alpha', 'atlas', 'balu', 'bravo', 'caspar', 'dario', 'emil', 'felix',
'fiete', 'finn', 'gerrit', 'hanno', 'ilan', 'joris', 'kian', 'leon',
'linus', 'maik', 'malo', 'marlon', 'mats', 'mika', 'milan', 'noel',
'ole', 'oskar', 'pepe', 'quinn', 'remo', 'sam', 'taro', 'timo',
'veit', 'vito', 'yaro', 'yuri', 'zeno', 'zuri',
];
$choices = [[
'seed' => $selectedSeed,
'label' => 'Aktuelle Auswahl',
'image' => self::dicebearUrl($selectedSeed),
]];
$seen = [$selectedSeed => true];
$base = abs((int) crc32($selectedSeed));
$index = 0;
while (count($choices) < $count) {
$prefix = $prefixes[($base + ($index * 7)) % count($prefixes)];
$suffix = $suffixes[($base + ($index * 11)) % count($suffixes)];
$candidate = self::normalizeSeed($prefix . '-' . $suffix . '-' . (($base + $index) % 997));
$index++;
if (isset($seen[$candidate])) {
continue;
}
$seen[$candidate] = true;
$choices[] = [
'seed' => $candidate,
'label' => 'Variante ' . count($choices),
'image' => self::dicebearUrl($candidate),
];
}
return $choices;
}
public static function dicebearUrl(string $seed): string
{
return '/api/avatar?seed=' . rawurlencode(self::normalizeSeed($seed));
}
public static function render(array $row, string $name = 'Mitglied', string $size = 'md'): string
{
$avatar = self::normalize($row);
$src = self::dicebearUrl($avatar['avatar_preset']);
return sprintf(
'<span class="%s" role="img" aria-label="%s"><img class="pkt-avatar__img" src="%s" alt=""></span>',
htmlspecialchars(implode(' ', [
'pkt-avatar',
'pkt-avatar--' . preg_replace('/[^a-z0-9\-]/', '', strtolower($size)),
'pkt-avatar--preset',
]), ENT_QUOTES),
htmlspecialchars('Avatar von ' . $name, ENT_QUOTES),
htmlspecialchars($src, ENT_QUOTES)
);
}
public static function fallbackSvg(string $seed, string $label = 'Avatar'): string
{
$seed = self::normalizeSeed($seed);
$initials = strtoupper(substr(preg_replace('/[^a-z]/', '', $seed) ?: 'pk', 0, 2));
$palette = ['#f5efe5', '#d9c7b3', '#8f6f54', '#37485a', '#c88f5b'];
$hash = abs((int) crc32($seed));
$bg = $palette[$hash % count($palette)];
$fg = $hash % 2 === 0 ? '#243142' : '#ffffff';
return sprintf(
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-label="%s"><rect width="128" height="128" rx="28" fill="%s"/><circle cx="64" cy="50" r="24" fill="rgba(255,255,255,0.26)"/><path d="M30 110c4-18 18-30 34-30s30 12 34 30" fill="rgba(255,255,255,0.2)"/><text x="64" y="76" text-anchor="middle" font-family="Arial, sans-serif" font-size="28" font-weight="700" fill="%s">%s</text></svg>',
htmlspecialchars($label, ENT_QUOTES),
htmlspecialchars($bg, ENT_QUOTES),
htmlspecialchars($fg, ENT_QUOTES),
htmlspecialchars($initials, ENT_QUOTES)
);
}
}