This commit is contained in:
156
src/App/Avatar/Lorelei.php
Normal file
156
src/App/Avatar/Lorelei.php
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user