This commit is contained in:
@@ -3,11 +3,12 @@ Anweisung: Projektstruktur (Basis-Template) wie in „papa-kind-treff“
|
|||||||
Aktueller Projektstand
|
Aktueller Projektstand
|
||||||
- Papa-Kind-Treff nutzt im Mitgliederbereich eine Mehr-Style-Avatar-Architektur auf Basis von DiceBear.
|
- Papa-Kind-Treff nutzt im Mitgliederbereich eine Mehr-Style-Avatar-Architektur auf Basis von DiceBear.
|
||||||
- Die zentrale Avatar-Verwaltung liegt in `src/App/Avatar/AvatarManager.php`.
|
- Die zentrale Avatar-Verwaltung liegt in `src/App/Avatar/AvatarManager.php`.
|
||||||
- Der aktuell aktive Style liegt in `src/App/Avatar/Lorelei.php`.
|
- Die aktuell aktiven Styles liegen in `src/App/Avatar/Lorelei.php` und `src/App/Avatar/Croodles.php`.
|
||||||
- Die Avatar-Konfiguration liegt in `config/avatar.php`.
|
- Die Avatar-Konfiguration liegt in `config/avatar.php`.
|
||||||
- Der Avatar-Proxy liegt unter `public/api/avatar.php`.
|
- Der Avatar-Proxy liegt unter `public/api/avatar.php`.
|
||||||
- Die visuelle Avatar-Konfiguration liegt im Dashboard-Modal unter `partials/landing/account/dashboard.php` und rendert Komponenten-Vorschauen live nach.
|
- Die visuelle Avatar-Konfiguration liegt im Dashboard-Modal unter `partials/landing/account/dashboard.php` und rendert Komponenten-Vorschauen live nach.
|
||||||
- Der Generator speichert `avatar_style`, `avatar_seed` und `avatar_config_json` generisch im Profil.
|
- Der Generator speichert `avatar_style`, `avatar_seed` und `avatar_config_json` generisch im Profil.
|
||||||
|
- Der Generator zeigt pro Style auch die von DiceBear unterstützten Farbfelder inklusive Fuellung, Stopps und Winkel und speichert diese im gleichen Config-JSON.
|
||||||
- Gespeicherte Benutzer-Avatare werden lokal unter `public/assets/avatars/users/ptk-<userId>.svg` abgelegt und bei Änderungen überschrieben.
|
- Gespeicherte Benutzer-Avatare werden lokal unter `public/assets/avatars/users/ptk-<userId>.svg` abgelegt und bei Änderungen überschrieben.
|
||||||
- Der Generator besitzt einen Button `Random Profilbild`, der Seed und Varianten des aktiven Styles zufällig neu setzt.
|
- Der Generator besitzt einen Button `Random Profilbild`, der Seed und Varianten des aktiven Styles zufällig neu setzt.
|
||||||
- Beim Oeffnen des Modals werden nur die Bilder des aktiven Styles und Tabs geladen; weitere Tabs laden ihre Varianten erst beim Wechsel nach.
|
- Beim Oeffnen des Modals werden nur die Bilder des aktiven Styles und Tabs geladen; weitere Tabs laden ihre Varianten erst beim Wechsel nach.
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter. Kernbereiche sind l
|
|||||||
- MySQL/MariaDB-Schema in `schema.sql`
|
- MySQL/MariaDB-Schema in `schema.sql`
|
||||||
- Globales Frontend vor allem über `public/assets/js/app.js` und `public/assets/css/app.css`
|
- Globales Frontend vor allem über `public/assets/js/app.js` und `public/assets/css/app.css`
|
||||||
- Avatar-Manager liegt zentral unter `src/App/Avatar/AvatarManager.php`
|
- Avatar-Manager liegt zentral unter `src/App/Avatar/AvatarManager.php`
|
||||||
- Der aktive Style `Lorelei` liegt getrennt unter `src/App/Avatar/Lorelei.php`
|
- Die aktiven Styles `Lorelei` und `Croodles` liegen getrennt unter `src/App/Avatar/Lorelei.php` und `src/App/Avatar/Croodles.php`
|
||||||
- Die Avatar-Konfiguration liegt in `config/avatar.php`
|
- Die Avatar-Konfiguration liegt in `config/avatar.php`
|
||||||
|
- In `config/avatar.php` sind pro Style auch Beispielpaletten, Komponentenlisten und Beispielprofile dokumentiert
|
||||||
- Der Avatar-Endpunkt ist als direkte Datei unter `public/api/avatar.php` angebunden und laeuft bewusst ohne vollen App-Bootstrap.
|
- Der Avatar-Endpunkt ist als direkte Datei unter `public/api/avatar.php` angebunden und laeuft bewusst ohne vollen App-Bootstrap.
|
||||||
|
|
||||||
## Wichtige Produktentscheidungen
|
## Wichtige Produktentscheidungen
|
||||||
@@ -23,9 +24,9 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter. Kernbereiche sind l
|
|||||||
- Die Community ist hierarchisch aufgebaut: Kategorien -> Boards -> Threads -> Posts.
|
- Die Community ist hierarchisch aufgebaut: Kategorien -> Boards -> Threads -> Posts.
|
||||||
- Moderation ist getrennt vom normalen Community-Frontend in einem eigenen Adminbereich.
|
- Moderation ist getrennt vom normalen Community-Frontend in einem eigenen Adminbereich.
|
||||||
- Standortfunktionen sind relevant für lokale Treffen und werden künftig weiter ausgebaut.
|
- Standortfunktionen sind relevant für lokale Treffen und werden künftig weiter ausgebaut.
|
||||||
- Profilbilder werden aktuell über die Mehr-Style-Architektur serverseitig erzeugt; aktiv ist derzeit `lorelei`.
|
- Profilbilder werden aktuell über die Mehr-Style-Architektur serverseitig erzeugt; aktiv sind derzeit `lorelei` und `croodles`.
|
||||||
- Stil, Seed und Varianten werden generisch im Profil gespeichert; das fertige SVG wird lokal pro Benutzer unter `ptk-<userId>.svg` abgelegt.
|
- Stil, Seed und Varianten werden generisch im Profil gespeichert; das fertige SVG wird lokal pro Benutzer unter `ptk-<userId>.svg` abgelegt.
|
||||||
- Der Avatar-Editor bietet visuelle Komponenten-Vorschauen statt reiner Text-Dropdowns und laedt nur den aktiven Style/Tab nach.
|
- Der Avatar-Editor bietet visuelle Komponenten-Vorschauen statt reiner Text-Dropdowns, laedt nur den aktiven Style/Tab nach und zeigt pro Style die unterstützten Farb-, Fuellungs-, Stopps- und Winkelparameter an.
|
||||||
|
|
||||||
## Navigation / Konto
|
## Navigation / Konto
|
||||||
- Eingeloggte Nutzer sehen rechts ein Profil-Menü statt einzelner `Dashboard`-/`Logout`-Buttons.
|
- Eingeloggte Nutzer sehen rechts ein Profil-Menü statt einzelner `Dashboard`-/`Logout`-Buttons.
|
||||||
|
|||||||
@@ -18,9 +18,10 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter mit Fokus auf lokale
|
|||||||
- standortbasierte Sortierung für die neuesten Treffen
|
- standortbasierte Sortierung für die neuesten Treffen
|
||||||
- eigener Einstellungsbereich für Standortfreigabe, Browser-Hinweise und Consent-Verwaltung
|
- eigener Einstellungsbereich für Standortfreigabe, Browser-Hinweise und Consent-Verwaltung
|
||||||
- modernes Avatar-System mit zentraler Mehr-Style-Architektur auf Basis von DiceBear
|
- modernes Avatar-System mit zentraler Mehr-Style-Architektur auf Basis von DiceBear
|
||||||
- aktiver Avatar-Style aktuell: `lorelei`; weitere Styles können über `config/avatar.php` freigeschaltet werden
|
- aktive Avatar-Styles aktuell: `lorelei` und `croodles`; weitere Styles können über `config/avatar.php` freigeschaltet werden
|
||||||
- Profilbild-Generator mit visueller Komponenten-Auswahl, generischem Style-/Seed-/Config-Speichermodell und `Random Profilbild`
|
- Profilbild-Generator mit visueller Komponenten-Auswahl, generischem Style-/Seed-/Config-Speichermodell, Style-Wechsel und vollständigen Farb-, Füllungs-, Stopps- und Winkeloptionen pro Style
|
||||||
- jedes gespeicherte Profilbild wird als lokale Benutzer-SVG unter `public/assets/avatars/users/ptk-<userId>.svg` abgelegt
|
- jedes gespeicherte Profilbild wird als lokale Benutzer-SVG unter `public/assets/avatars/users/ptk-<userId>.svg` abgelegt
|
||||||
|
- `config/avatar.php` enthaelt jetzt pro Style auch explizite Beispielpaletten und Beispielprofile als Einbindungsreferenz
|
||||||
- Consent-Manager für Analyse und externe Dienste
|
- Consent-Manager für Analyse und externe Dienste
|
||||||
- Navigation aktuell mit `Home`, `Suche`, `Community`
|
- Navigation aktuell mit `Home`, `Suche`, `Community`
|
||||||
- eingeloggte Nutzer sehen rechts ein Profil-Menü mit Direktlinks zu `Profil`, `Kinder`, `Termine`, `Community`, `Einstellungen`
|
- eingeloggte Nutzer sehen rechts ein Profil-Menü mit Direktlinks zu `Profil`, `Kinder`, `Termine`, `Community`, `Einstellungen`
|
||||||
@@ -41,11 +42,12 @@ Papa-Kind-Treff ist eine PHP-basierte Plattform für Väter mit Fokus auf lokale
|
|||||||
|
|
||||||
Die Rechteverteilung und Community-Logik sind in `src/App/CommunityAccess.php` abgebildet.
|
Die Rechteverteilung und Community-Logik sind in `src/App/CommunityAccess.php` abgebildet.
|
||||||
Die zentrale Avatar-Verwaltung liegt in `src/App/Avatar/AvatarManager.php`.
|
Die zentrale Avatar-Verwaltung liegt in `src/App/Avatar/AvatarManager.php`.
|
||||||
Der aktuell aktive Style `Lorelei` liegt in `src/App/Avatar/Lorelei.php`.
|
Die aktuell aktiven Styles liegen in `src/App/Avatar/Lorelei.php` und `src/App/Avatar/Croodles.php`.
|
||||||
Die Style-/Storage-Konfiguration liegt in `config/avatar.php`.
|
Die Style-/Storage-Konfiguration liegt in `config/avatar.php`.
|
||||||
Der direkte Avatar-Endpunkt liegt in `public/api/avatar.php`.
|
Der direkte Avatar-Endpunkt liegt in `public/api/avatar.php`.
|
||||||
Die visuelle Avatar-Auswahl sitzt im Dashboard-Modal in `partials/landing/account/dashboard.php`.
|
Die visuelle Avatar-Auswahl sitzt im Dashboard-Modal in `partials/landing/account/dashboard.php`.
|
||||||
Beim Oeffnen des Generators werden nur die Varianten des aktuell aktiven Styles und Tabs geladen; weitere Varianten werden erst beim Wechsel nachgeladen.
|
Beim Oeffnen des Generators werden nur die Varianten des aktuell aktiven Styles und Tabs geladen; weitere Varianten werden erst beim Wechsel nachgeladen.
|
||||||
|
Farbfelder werden pro Style direkt im Generator gepflegt und zusammen mit Stil und Seed im Profil gespeichert.
|
||||||
|
|
||||||
## Consent und rechtlich relevante Integrationen
|
## Consent und rechtlich relevante Integrationen
|
||||||
Der aktuelle Code nutzt oder kann nutzen:
|
Der aktuelle Code nutzt oder kann nutzen:
|
||||||
|
|||||||
@@ -16,13 +16,220 @@ return [
|
|||||||
'default_style' => 'lorelei',
|
'default_style' => 'lorelei',
|
||||||
'enabled_styles' => [
|
'enabled_styles' => [
|
||||||
'lorelei',
|
'lorelei',
|
||||||
|
'croodles',
|
||||||
],
|
],
|
||||||
'styles' => [
|
'styles' => [
|
||||||
'lorelei' => [
|
'lorelei' => [
|
||||||
'label' => 'Lorelei',
|
'label' => 'Lorelei',
|
||||||
'class' => \App\Avatar\Lorelei::class,
|
'class' => \App\Avatar\Lorelei::class,
|
||||||
'fixed_colors' => [],
|
'fixed_colors' => [],
|
||||||
'color_controls' => [],
|
'component_controls' => [
|
||||||
|
'eyesVariant',
|
||||||
|
'eyebrowsVariant',
|
||||||
|
'mouthVariant',
|
||||||
|
'glassesVariant',
|
||||||
|
'hairVariant',
|
||||||
|
'beardVariant',
|
||||||
|
'earringsVariant',
|
||||||
|
],
|
||||||
|
'color_controls' => [
|
||||||
|
'earringsColor' => [
|
||||||
|
'label' => 'Ohrringe',
|
||||||
|
'default' => '#d6b36b',
|
||||||
|
'samples' => ['#d6b36b', '#b08d57', '#d9d9d9', '#8c6c4a'],
|
||||||
|
'allow_custom' => true,
|
||||||
|
'allow_fill' => true,
|
||||||
|
'allow_fill_stops' => true,
|
||||||
|
'allow_angle' => true,
|
||||||
|
],
|
||||||
|
'eyebrowsColor' => [
|
||||||
|
'label' => 'Augenbrauen',
|
||||||
|
'default' => '#4a3227',
|
||||||
|
'samples' => ['#4a3227', '#6a4938', '#2c221d', '#8a6651'],
|
||||||
|
'allow_custom' => true,
|
||||||
|
'allow_fill' => true,
|
||||||
|
'allow_fill_stops' => true,
|
||||||
|
'allow_angle' => true,
|
||||||
|
],
|
||||||
|
'eyesColor' => [
|
||||||
|
'label' => 'Augen',
|
||||||
|
'default' => '#2a2522',
|
||||||
|
'samples' => ['#2a2522', '#3f4f66', '#6b4d35', '#1f3a5f'],
|
||||||
|
'allow_custom' => true,
|
||||||
|
'allow_fill' => true,
|
||||||
|
'allow_fill_stops' => true,
|
||||||
|
'allow_angle' => true,
|
||||||
|
],
|
||||||
|
'frecklesColor' => [
|
||||||
|
'label' => 'Sommersprossen',
|
||||||
|
'default' => '#b67852',
|
||||||
|
'samples' => ['#b67852', '#a16645', '#c48a66', '#8f5a3e'],
|
||||||
|
'allow_custom' => true,
|
||||||
|
'allow_fill' => true,
|
||||||
|
'allow_fill_stops' => true,
|
||||||
|
'allow_angle' => true,
|
||||||
|
],
|
||||||
|
'glassesColor' => [
|
||||||
|
'label' => 'Brille',
|
||||||
|
'default' => '#26313f',
|
||||||
|
'samples' => ['#26313f', '#4d5f73', '#1b1f27', '#7a5d42'],
|
||||||
|
'allow_custom' => true,
|
||||||
|
'allow_fill' => true,
|
||||||
|
'allow_fill_stops' => true,
|
||||||
|
'allow_angle' => true,
|
||||||
|
],
|
||||||
|
'hairColor' => [
|
||||||
|
'label' => 'Haare',
|
||||||
|
'default' => '#4b3128',
|
||||||
|
'samples' => ['#4b3128', '#6f4b38', '#221916', '#b38a52'],
|
||||||
|
'allow_custom' => true,
|
||||||
|
'allow_fill' => true,
|
||||||
|
'allow_fill_stops' => true,
|
||||||
|
'allow_angle' => true,
|
||||||
|
],
|
||||||
|
'hairAccessoriesColor' => [
|
||||||
|
'label' => 'Haar-Accessoires',
|
||||||
|
'default' => '#b96f54',
|
||||||
|
'samples' => ['#b96f54', '#c98664', '#8d5a9e', '#5e8aa8'],
|
||||||
|
'allow_custom' => true,
|
||||||
|
'allow_fill' => true,
|
||||||
|
'allow_fill_stops' => true,
|
||||||
|
'allow_angle' => true,
|
||||||
|
],
|
||||||
|
'mouthColor' => [
|
||||||
|
'label' => 'Mund',
|
||||||
|
'default' => '#9f5a56',
|
||||||
|
'samples' => ['#9f5a56', '#b66b66', '#804444', '#c98a7a'],
|
||||||
|
'allow_custom' => true,
|
||||||
|
'allow_fill' => true,
|
||||||
|
'allow_fill_stops' => true,
|
||||||
|
'allow_angle' => true,
|
||||||
|
],
|
||||||
|
'noseColor' => [
|
||||||
|
'label' => 'Nase',
|
||||||
|
'default' => '#a56f58',
|
||||||
|
'samples' => ['#a56f58', '#bf8569', '#8d5f49', '#d3a17b'],
|
||||||
|
'allow_custom' => true,
|
||||||
|
'allow_fill' => true,
|
||||||
|
'allow_fill_stops' => true,
|
||||||
|
'allow_angle' => true,
|
||||||
|
],
|
||||||
|
'skinColor' => [
|
||||||
|
'label' => 'Haut',
|
||||||
|
'default' => '#e7ba9a',
|
||||||
|
'samples' => ['#f4d2ba', '#e7ba9a', '#c98f69', '#8a5638'],
|
||||||
|
'allow_custom' => true,
|
||||||
|
'allow_fill' => true,
|
||||||
|
'allow_fill_stops' => true,
|
||||||
|
'allow_angle' => true,
|
||||||
|
],
|
||||||
|
'backgroundColor' => [
|
||||||
|
'label' => 'Hintergrund',
|
||||||
|
'default' => '#f5efe5',
|
||||||
|
'samples' => ['#f5efe5', '#e9dfd1', '#d7e6ef', '#efe2d2'],
|
||||||
|
'allow_custom' => true,
|
||||||
|
'allow_fill' => true,
|
||||||
|
'allow_fill_stops' => true,
|
||||||
|
'allow_angle' => true,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'example_profile' => [
|
||||||
|
'seed' => 'pkt-demo-lorelei',
|
||||||
|
'components' => [
|
||||||
|
'eyesVariant' => 'variant08',
|
||||||
|
'eyebrowsVariant' => 'variant04',
|
||||||
|
'mouthVariant' => 'happy06',
|
||||||
|
'glassesVariant' => 'variant02',
|
||||||
|
'hairVariant' => 'variant18',
|
||||||
|
'beardVariant' => 'variant01',
|
||||||
|
'earringsVariant' => '',
|
||||||
|
],
|
||||||
|
'colors' => [
|
||||||
|
'skinColor' => '#e7ba9a',
|
||||||
|
'hairColor' => '#4b3128',
|
||||||
|
'eyesColor' => '#2a2522',
|
||||||
|
'backgroundColor' => '#f5efe5',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'croodles' => [
|
||||||
|
'label' => 'Croodles',
|
||||||
|
'class' => \App\Avatar\Croodles::class,
|
||||||
|
'fixed_colors' => [],
|
||||||
|
'component_controls' => [
|
||||||
|
'headVariant',
|
||||||
|
'eyesVariant',
|
||||||
|
'noseVariant',
|
||||||
|
'mouthVariant',
|
||||||
|
'topVariant',
|
||||||
|
'beardVariant',
|
||||||
|
'mustacheVariant',
|
||||||
|
],
|
||||||
|
'color_controls' => [
|
||||||
|
'baseColor' => [
|
||||||
|
'label' => 'Basisfarbe',
|
||||||
|
'default' => '#e7ba9a',
|
||||||
|
'samples' => ['#f4d2ba', '#e7ba9a', '#c98f69', '#8a5638'],
|
||||||
|
'allow_custom' => true,
|
||||||
|
'allow_fill' => true,
|
||||||
|
'allow_fill_stops' => true,
|
||||||
|
'allow_angle' => true,
|
||||||
|
],
|
||||||
|
'eyepatchColor' => [
|
||||||
|
'label' => 'Augenklappe',
|
||||||
|
'default' => '#283341',
|
||||||
|
'samples' => ['#283341', '#1a222d', '#4a5a6f', '#77583f'],
|
||||||
|
'allow_custom' => true,
|
||||||
|
'allow_fill' => true,
|
||||||
|
'allow_fill_stops' => true,
|
||||||
|
'allow_angle' => true,
|
||||||
|
],
|
||||||
|
'glassesColor' => [
|
||||||
|
'label' => 'Brille',
|
||||||
|
'default' => '#283341',
|
||||||
|
'samples' => ['#283341', '#4a5a6f', '#1a222d', '#74614a'],
|
||||||
|
'allow_custom' => true,
|
||||||
|
'allow_fill' => true,
|
||||||
|
'allow_fill_stops' => true,
|
||||||
|
'allow_angle' => true,
|
||||||
|
],
|
||||||
|
'topColor' => [
|
||||||
|
'label' => 'Top-Farbe',
|
||||||
|
'default' => '#4f6ea8',
|
||||||
|
'samples' => ['#4f6ea8', '#6d8fc2', '#36507a', '#6f8c55'],
|
||||||
|
'allow_custom' => true,
|
||||||
|
'allow_fill' => true,
|
||||||
|
'allow_fill_stops' => true,
|
||||||
|
'allow_angle' => true,
|
||||||
|
],
|
||||||
|
'backgroundColor' => [
|
||||||
|
'label' => 'Hintergrund',
|
||||||
|
'default' => '#f2eadf',
|
||||||
|
'samples' => ['#f2eadf', '#e6dccd', '#d9e8ef', '#efe0d0'],
|
||||||
|
'allow_custom' => true,
|
||||||
|
'allow_fill' => true,
|
||||||
|
'allow_fill_stops' => true,
|
||||||
|
'allow_angle' => true,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'example_profile' => [
|
||||||
|
'seed' => 'pkt-demo-croodles',
|
||||||
|
'components' => [
|
||||||
|
'headVariant' => 'variant03',
|
||||||
|
'eyesVariant' => 'variant06',
|
||||||
|
'noseVariant' => 'variant04',
|
||||||
|
'mouthVariant' => 'variant09',
|
||||||
|
'topVariant' => 'variant11',
|
||||||
|
'beardVariant' => 'variant02',
|
||||||
|
'mustacheVariant' => '',
|
||||||
|
],
|
||||||
|
'colors' => [
|
||||||
|
'baseColor' => '#e7ba9a',
|
||||||
|
'topColor' => '#4f6ea8',
|
||||||
|
'glassesColor' => '#283341',
|
||||||
|
'backgroundColor' => '#f2eadf',
|
||||||
|
],
|
||||||
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ extract($vm, EXTR_OVERWRITE);
|
|||||||
$avatarPlaceholder = 'data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 12 12%22%3E%3Crect width=%2212%22 height=%2212%22 fill=%22%23f7efe6%22/%3E%3C/svg%3E';
|
$avatarPlaceholder = 'data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 12 12%22%3E%3Crect width=%2212%22 height=%2212%22 fill=%22%23f7efe6%22/%3E%3C/svg%3E';
|
||||||
$avatarStyleKeys = array_keys($avatarBuilder ?? []);
|
$avatarStyleKeys = array_keys($avatarBuilder ?? []);
|
||||||
$currentAvatarStyle = (string)($profile['avatar_style'] ?? ($avatarStyleKeys[0] ?? 'lorelei'));
|
$currentAvatarStyle = (string)($profile['avatar_style'] ?? ($avatarStyleKeys[0] ?? 'lorelei'));
|
||||||
$currentAvatarComponents = $avatarBuilder[$currentAvatarStyle]['components'] ?? [];
|
|
||||||
$dashboardUserId = (int)($_SESSION['user_id'] ?? 0);
|
$dashboardUserId = (int)($_SESSION['user_id'] ?? 0);
|
||||||
$editing = isset($editEvent) && $editEvent !== null;
|
$editing = isset($editEvent) && $editEvent !== null;
|
||||||
$actionEvent = $editing ? 'event_update' : 'event_add';
|
$actionEvent = $editing ? 'event_update' : 'event_add';
|
||||||
@@ -382,6 +381,107 @@ $sectionLinks = [
|
|||||||
</section>
|
</section>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
|
<?php $styleColors = $styleMeta['colors'] ?? []; ?>
|
||||||
|
<?php if ($styleColors): ?>
|
||||||
|
<div class="account-panel" style="margin-top:18px;">
|
||||||
|
<div class="account-panel__head">
|
||||||
|
<h3>Farben</h3>
|
||||||
|
<p class="muted small">Alle aktuell für diesen DiceBear-Style unterstützten Farbfelder.</p>
|
||||||
|
</div>
|
||||||
|
<div class="account-panel__body">
|
||||||
|
<div class="form-grid">
|
||||||
|
<?php foreach ($styleColors as $colorField => $colorMeta): ?>
|
||||||
|
<?php
|
||||||
|
$colorValue = (string)($colorMeta['value'] ?? '');
|
||||||
|
$colorDefault = (string)($colorMeta['default'] ?? '#c4a484');
|
||||||
|
?>
|
||||||
|
<div class="stack gap-6">
|
||||||
|
<label class="label" for="avatarColor<?= htmlspecialchars($styleKey . $colorField, ENT_QUOTES) ?>"><?= htmlspecialchars((string)($colorMeta['label'] ?? $colorField), ENT_QUOTES) ?></label>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="avatar_config[<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>][<?= htmlspecialchars((string)$colorField, ENT_QUOTES) ?>]"
|
||||||
|
value="<?= htmlspecialchars($colorValue, ENT_QUOTES) ?>"
|
||||||
|
data-avatar-style="<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>"
|
||||||
|
data-avatar-color-input="<?= htmlspecialchars((string)$colorField, ENT_QUOTES) ?>"
|
||||||
|
>
|
||||||
|
<div class="flex gap-12 center-y" style="flex-wrap:wrap;">
|
||||||
|
<input
|
||||||
|
id="avatarColor<?= htmlspecialchars($styleKey . $colorField, ENT_QUOTES) ?>"
|
||||||
|
type="color"
|
||||||
|
value="<?= htmlspecialchars($colorValue !== '' ? $colorValue : $colorDefault, ENT_QUOTES) ?>"
|
||||||
|
data-avatar-style="<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>"
|
||||||
|
data-avatar-color-picker="<?= htmlspecialchars((string)$colorField, ENT_QUOTES) ?>"
|
||||||
|
data-avatar-color-default="<?= htmlspecialchars($colorDefault, ENT_QUOTES) ?>"
|
||||||
|
style="width:64px; height:44px; padding:4px;"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn ghost"
|
||||||
|
type="button"
|
||||||
|
data-avatar-style="<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>"
|
||||||
|
data-avatar-color-reset="<?= htmlspecialchars((string)$colorField, ENT_QUOTES) ?>"
|
||||||
|
>Standard</button>
|
||||||
|
<span class="muted small" data-avatar-color-label="<?= htmlspecialchars($styleKey . ':' . (string)$colorField, ENT_QUOTES) ?>">
|
||||||
|
<?= $colorValue !== '' ? htmlspecialchars($colorValue, ENT_QUOTES) : 'DiceBear-Standard' ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid" style="margin-top:6px;">
|
||||||
|
<div class="stack gap-6">
|
||||||
|
<label class="label" for="avatarFill<?= htmlspecialchars($styleKey . $colorField, ENT_QUOTES) ?>">Füllung</label>
|
||||||
|
<select
|
||||||
|
id="avatarFill<?= htmlspecialchars($styleKey . $colorField, ENT_QUOTES) ?>"
|
||||||
|
name="avatar_config[<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>][<?= htmlspecialchars((string)$colorField, ENT_QUOTES) ?>Fill]"
|
||||||
|
class="select"
|
||||||
|
data-avatar-style="<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>"
|
||||||
|
data-avatar-color-meta="<?= htmlspecialchars((string)$colorField, ENT_QUOTES) ?>"
|
||||||
|
data-avatar-color-meta-type="fill"
|
||||||
|
>
|
||||||
|
<option value="" <?= (($colorMeta['fill'] ?? '') === '') ? 'selected' : '' ?>>Standard</option>
|
||||||
|
<option value="solid" <?= (($colorMeta['fill'] ?? '') === 'solid') ? 'selected' : '' ?>>Solid</option>
|
||||||
|
<option value="linear" <?= (($colorMeta['fill'] ?? '') === 'linear') ? 'selected' : '' ?>>Linear</option>
|
||||||
|
<option value="radial" <?= (($colorMeta['fill'] ?? '') === 'radial') ? 'selected' : '' ?>>Radial</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="stack gap-6">
|
||||||
|
<label class="label" for="avatarStops<?= htmlspecialchars($styleKey . $colorField, ENT_QUOTES) ?>">Farb-Stopps</label>
|
||||||
|
<input
|
||||||
|
id="avatarStops<?= htmlspecialchars($styleKey . $colorField, ENT_QUOTES) ?>"
|
||||||
|
type="number"
|
||||||
|
min="2"
|
||||||
|
max="8"
|
||||||
|
step="1"
|
||||||
|
name="avatar_config[<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>][<?= htmlspecialchars((string)$colorField, ENT_QUOTES) ?>FillStops]"
|
||||||
|
class="input"
|
||||||
|
value="<?= htmlspecialchars((string)($colorMeta['fill_stops'] ?? ''), ENT_QUOTES) ?>"
|
||||||
|
placeholder="Standard"
|
||||||
|
data-avatar-style="<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>"
|
||||||
|
data-avatar-color-meta="<?= htmlspecialchars((string)$colorField, ENT_QUOTES) ?>"
|
||||||
|
data-avatar-color-meta-type="fillStops"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="stack gap-6">
|
||||||
|
<label class="label" for="avatarAngle<?= htmlspecialchars($styleKey . $colorField, ENT_QUOTES) ?>">Winkel</label>
|
||||||
|
<input
|
||||||
|
id="avatarAngle<?= htmlspecialchars($styleKey . $colorField, ENT_QUOTES) ?>"
|
||||||
|
type="number"
|
||||||
|
min="-360"
|
||||||
|
max="360"
|
||||||
|
step="1"
|
||||||
|
name="avatar_config[<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>][<?= htmlspecialchars((string)$colorField, ENT_QUOTES) ?>Angle]"
|
||||||
|
class="input"
|
||||||
|
value="<?= htmlspecialchars((string)($colorMeta['angle'] ?? ''), ENT_QUOTES) ?>"
|
||||||
|
placeholder="Standard"
|
||||||
|
data-avatar-style="<?= htmlspecialchars($styleKey, ENT_QUOTES) ?>"
|
||||||
|
data-avatar-color-meta="<?= htmlspecialchars((string)$colorField, ENT_QUOTES) ?>"
|
||||||
|
data-avatar-color-meta-type="angle"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -729,6 +729,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const avatarStyleConfigs = {
|
const avatarStyleConfigs = {
|
||||||
lorelei: {
|
lorelei: {
|
||||||
fields: ['eyesVariant', 'eyebrowsVariant', 'mouthVariant', 'glassesVariant', 'hairVariant', 'beardVariant', 'earringsVariant'],
|
fields: ['eyesVariant', 'eyebrowsVariant', 'mouthVariant', 'glassesVariant', 'hairVariant', 'beardVariant', 'earringsVariant'],
|
||||||
|
colorFields: ['earringsColor', 'eyebrowsColor', 'eyesColor', 'frecklesColor', 'glassesColor', 'hairColor', 'hairAccessoriesColor', 'mouthColor', 'noseColor', 'skinColor', 'backgroundColor'],
|
||||||
paramMap: {
|
paramMap: {
|
||||||
eyesVariant: 'eyesVariant',
|
eyesVariant: 'eyesVariant',
|
||||||
eyebrowsVariant: 'eyebrowsVariant',
|
eyebrowsVariant: 'eyebrowsVariant',
|
||||||
@@ -747,6 +748,29 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
hairVariant: 'hairProbability',
|
hairVariant: 'hairProbability',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
croodles: {
|
||||||
|
fields: ['headVariant', 'eyesVariant', 'noseVariant', 'mouthVariant', 'topVariant', 'beardVariant', 'mustacheVariant'],
|
||||||
|
colorFields: ['baseColor', 'eyepatchColor', 'glassesColor', 'topColor', 'backgroundColor'],
|
||||||
|
paramMap: {
|
||||||
|
headVariant: 'headVariant',
|
||||||
|
eyesVariant: 'eyesVariant',
|
||||||
|
noseVariant: 'noseVariant',
|
||||||
|
mouthVariant: 'mouthVariant',
|
||||||
|
topVariant: 'topVariant',
|
||||||
|
beardVariant: 'beardVariant',
|
||||||
|
mustacheVariant: 'mustacheVariant',
|
||||||
|
},
|
||||||
|
optionalProbabilityMap: {
|
||||||
|
headVariant: 'headProbability',
|
||||||
|
eyesVariant: 'eyesProbability',
|
||||||
|
noseVariant: 'noseProbability',
|
||||||
|
mouthVariant: 'mouthProbability',
|
||||||
|
topVariant: 'topProbability',
|
||||||
|
beardVariant: 'beardProbability',
|
||||||
|
mustacheVariant: 'mustacheProbability',
|
||||||
|
},
|
||||||
|
alwaysProbabilityMap: {},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCurrentAvatarStyle = (form) => {
|
const getCurrentAvatarStyle = (form) => {
|
||||||
@@ -769,6 +793,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
return input ? input.value : '';
|
return input ? input.value : '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const readAvatarColorValue = (form, style, field) => {
|
||||||
|
const panel = getAvatarStylePanel(form, style);
|
||||||
|
if (!panel) return '';
|
||||||
|
const input = panel.querySelector(`[data-avatar-color-input="${field}"]`);
|
||||||
|
return input ? input.value : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const readAvatarColorMetaValue = (form, style, field, type) => {
|
||||||
|
const panel = getAvatarStylePanel(form, style);
|
||||||
|
if (!panel) return '';
|
||||||
|
const input = panel.querySelector(`[data-avatar-color-meta="${field}"][data-avatar-color-meta-type="${type}"]`);
|
||||||
|
return input ? input.value : '';
|
||||||
|
};
|
||||||
|
|
||||||
const getAvatarStyleConfig = (style) => {
|
const getAvatarStyleConfig = (style) => {
|
||||||
if (avatarStyleConfigs[style]) return avatarStyleConfigs[style];
|
if (avatarStyleConfigs[style]) return avatarStyleConfigs[style];
|
||||||
return avatarStyleConfigs.lorelei;
|
return avatarStyleConfigs.lorelei;
|
||||||
@@ -801,6 +839,27 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (value && param) params.set(param, value);
|
if (value && param) params.set(param, value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
(styleConfig.colorFields || []).forEach((field) => {
|
||||||
|
const value = Object.prototype.hasOwnProperty.call(overrides, field)
|
||||||
|
? overrides[field]
|
||||||
|
: readAvatarColorValue(form, style, field);
|
||||||
|
if (value) {
|
||||||
|
params.set(field, value);
|
||||||
|
const fill = Object.prototype.hasOwnProperty.call(overrides, `${field}Fill`)
|
||||||
|
? overrides[`${field}Fill`]
|
||||||
|
: readAvatarColorMetaValue(form, style, field, 'fill');
|
||||||
|
const fillStops = Object.prototype.hasOwnProperty.call(overrides, `${field}FillStops`)
|
||||||
|
? overrides[`${field}FillStops`]
|
||||||
|
: readAvatarColorMetaValue(form, style, field, 'fillStops');
|
||||||
|
const angle = Object.prototype.hasOwnProperty.call(overrides, `${field}Angle`)
|
||||||
|
? overrides[`${field}Angle`]
|
||||||
|
: readAvatarColorMetaValue(form, style, field, 'angle');
|
||||||
|
if (fill) params.set(`${field}Fill`, fill);
|
||||||
|
if (fillStops) params.set(`${field}FillStops`, fillStops);
|
||||||
|
if (angle) params.set(`${field}Angle`, angle);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return params;
|
return params;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -871,6 +930,72 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
form.querySelectorAll('[data-avatar-color-picker]').forEach((picker) => {
|
||||||
|
const syncColor = () => {
|
||||||
|
const style = picker.getAttribute('data-avatar-style') || getCurrentAvatarStyle(form);
|
||||||
|
const field = picker.getAttribute('data-avatar-color-picker') || '';
|
||||||
|
const panel = getAvatarStylePanel(form, style);
|
||||||
|
if (!panel || !field) return;
|
||||||
|
const hidden = panel.querySelector(`[data-avatar-color-input="${field}"]`);
|
||||||
|
const label = panel.querySelector(`[data-avatar-color-label="${style}:${field}"]`);
|
||||||
|
if (hidden) {
|
||||||
|
hidden.value = picker.value;
|
||||||
|
}
|
||||||
|
if (label) {
|
||||||
|
label.textContent = picker.value;
|
||||||
|
}
|
||||||
|
ensureAvatarBuilderLoaded(form, style);
|
||||||
|
updateAvatarPreview(form);
|
||||||
|
updateAvatarOptionPreviews(form, style, getActiveAvatarField(form, style));
|
||||||
|
};
|
||||||
|
|
||||||
|
picker.addEventListener('input', syncColor);
|
||||||
|
picker.addEventListener('change', syncColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
form.querySelectorAll('[data-avatar-color-reset]').forEach((button) => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
const style = button.getAttribute('data-avatar-style') || getCurrentAvatarStyle(form);
|
||||||
|
const field = button.getAttribute('data-avatar-color-reset') || '';
|
||||||
|
const panel = getAvatarStylePanel(form, style);
|
||||||
|
if (!panel || !field) return;
|
||||||
|
const hidden = panel.querySelector(`[data-avatar-color-input="${field}"]`);
|
||||||
|
const picker = panel.querySelector(`[data-avatar-color-picker="${field}"]`);
|
||||||
|
const label = panel.querySelector(`[data-avatar-color-label="${style}:${field}"]`);
|
||||||
|
const fill = panel.querySelector(`[data-avatar-color-meta="${field}"][data-avatar-color-meta-type="fill"]`);
|
||||||
|
const fillStops = panel.querySelector(`[data-avatar-color-meta="${field}"][data-avatar-color-meta-type="fillStops"]`);
|
||||||
|
const angle = panel.querySelector(`[data-avatar-color-meta="${field}"][data-avatar-color-meta-type="angle"]`);
|
||||||
|
const defaultValue = picker?.getAttribute('data-avatar-color-default') || '#c4a484';
|
||||||
|
if (hidden) {
|
||||||
|
hidden.value = '';
|
||||||
|
}
|
||||||
|
if (picker) {
|
||||||
|
picker.value = defaultValue;
|
||||||
|
}
|
||||||
|
if (label) {
|
||||||
|
label.textContent = 'DiceBear-Standard';
|
||||||
|
}
|
||||||
|
if (fill) fill.value = '';
|
||||||
|
if (fillStops) fillStops.value = '';
|
||||||
|
if (angle) angle.value = '';
|
||||||
|
ensureAvatarBuilderLoaded(form, style);
|
||||||
|
updateAvatarPreview(form);
|
||||||
|
updateAvatarOptionPreviews(form, style, getActiveAvatarField(form, style));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
form.querySelectorAll('[data-avatar-color-meta]').forEach((input) => {
|
||||||
|
const syncMeta = () => {
|
||||||
|
const style = input.getAttribute('data-avatar-style') || getCurrentAvatarStyle(form);
|
||||||
|
ensureAvatarBuilderLoaded(form, style);
|
||||||
|
updateAvatarPreview(form);
|
||||||
|
updateAvatarOptionPreviews(form, style, getActiveAvatarField(form, style));
|
||||||
|
};
|
||||||
|
|
||||||
|
input.addEventListener('input', syncMeta);
|
||||||
|
input.addEventListener('change', syncMeta);
|
||||||
|
});
|
||||||
|
|
||||||
const setActiveComponentPanel = (style, field) => {
|
const setActiveComponentPanel = (style, field) => {
|
||||||
const panel = getAvatarStylePanel(form, style);
|
const panel = getAvatarStylePanel(form, style);
|
||||||
if (!panel) return;
|
if (!panel) return;
|
||||||
@@ -893,6 +1018,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
panel.querySelectorAll('[data-avatar-field]').forEach((input) => {
|
panel.querySelectorAll('[data-avatar-field]').forEach((input) => {
|
||||||
input.disabled = !active;
|
input.disabled = !active;
|
||||||
});
|
});
|
||||||
|
panel.querySelectorAll('[data-avatar-color-input], [data-avatar-color-picker], [data-avatar-color-reset]').forEach((input) => {
|
||||||
|
input.disabled = !active;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
ensureAvatarBuilderLoaded(form, style);
|
ensureAvatarBuilderLoaded(form, style);
|
||||||
setActiveComponentPanel(style, getActiveAvatarField(form, style));
|
setActiveComponentPanel(style, getActiveAvatarField(form, style));
|
||||||
|
|||||||
@@ -66,6 +66,13 @@ final class AvatarManager
|
|||||||
'fixed_colors' => [],
|
'fixed_colors' => [],
|
||||||
'color_controls' => [],
|
'color_controls' => [],
|
||||||
];
|
];
|
||||||
|
$result['croodles'] = [
|
||||||
|
'key' => 'croodles',
|
||||||
|
'label' => Croodles::label(),
|
||||||
|
'class' => Croodles::class,
|
||||||
|
'fixed_colors' => [],
|
||||||
|
'color_controls' => [],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
@@ -222,10 +229,24 @@ final class AvatarManager
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$colors = [];
|
||||||
|
foreach ($styleClass::colorDefinitions() as $field => $definition) {
|
||||||
|
$colors[$field] = [
|
||||||
|
'field' => $field,
|
||||||
|
'label' => (string)($definition['label'] ?? $field),
|
||||||
|
'value' => (string)($styleConfig[$field] ?? ''),
|
||||||
|
'default' => (string)($definition['default'] ?? '#c4a484'),
|
||||||
|
'fill' => (string)($styleConfig[$field . 'Fill'] ?? ''),
|
||||||
|
'fill_stops' => (string)($styleConfig[$field . 'FillStops'] ?? ''),
|
||||||
|
'angle' => (string)($styleConfig[$field . 'Angle'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
$styles[$meta['key']] = [
|
$styles[$meta['key']] = [
|
||||||
'key' => $meta['key'],
|
'key' => $meta['key'],
|
||||||
'label' => $meta['label'],
|
'label' => $meta['label'],
|
||||||
'components' => $components,
|
'components' => $components,
|
||||||
|
'colors' => $colors,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
259
src/App/Avatar/Croodles.php
Normal file
259
src/App/Avatar/Croodles.php
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Avatar;
|
||||||
|
|
||||||
|
final class Croodles
|
||||||
|
{
|
||||||
|
public static function key(): string
|
||||||
|
{
|
||||||
|
return 'croodles';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function label(): string
|
||||||
|
{
|
||||||
|
return 'Croodles';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function defaultSeed(): string
|
||||||
|
{
|
||||||
|
return 'papa-kind-treff';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function defaultConfig(): array
|
||||||
|
{
|
||||||
|
return array_merge([
|
||||||
|
'beardVariant' => '',
|
||||||
|
'eyesVariant' => '',
|
||||||
|
'headVariant' => '',
|
||||||
|
'mouthVariant' => '',
|
||||||
|
'mustacheVariant' => '',
|
||||||
|
'noseVariant' => '',
|
||||||
|
'topVariant' => '',
|
||||||
|
'baseColor' => '',
|
||||||
|
'eyepatchColor' => '',
|
||||||
|
'glassesColor' => '',
|
||||||
|
'topColor' => '',
|
||||||
|
'backgroundColor' => '',
|
||||||
|
], self::colorCompanionDefaults());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function options(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'beardVariant' => ['' => 'Keine'] + self::variantOptions(5),
|
||||||
|
'eyesVariant' => ['' => 'Keine'] + self::variantOptions(16),
|
||||||
|
'headVariant' => ['' => 'Keine'] + self::variantOptions(8),
|
||||||
|
'mouthVariant' => ['' => 'Keine'] + self::variantOptions(18),
|
||||||
|
'mustacheVariant' => ['' => 'Keine'] + self::variantOptions(4),
|
||||||
|
'noseVariant' => ['' => 'Keine'] + self::variantOptions(9),
|
||||||
|
'topVariant' => ['' => 'Keine'] + self::variantOptions(29),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function componentDefinitions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'headVariant' => ['label' => 'Kopf'],
|
||||||
|
'eyesVariant' => ['label' => 'Augen'],
|
||||||
|
'noseVariant' => ['label' => 'Nase'],
|
||||||
|
'mouthVariant' => ['label' => 'Mund'],
|
||||||
|
'topVariant' => ['label' => 'Top'],
|
||||||
|
'beardVariant' => ['label' => 'Bart'],
|
||||||
|
'mustacheVariant' => ['label' => 'Schnurrbart'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function colorDefinitions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'baseColor' => ['label' => 'Basisfarbe', 'default' => '#e7ba9a'],
|
||||||
|
'eyepatchColor' => ['label' => 'Augenklappe', 'default' => '#283341'],
|
||||||
|
'glassesColor' => ['label' => 'Brille', 'default' => '#283341'],
|
||||||
|
'topColor' => ['label' => 'Top-Farbe', 'default' => '#4f6ea8'],
|
||||||
|
'backgroundColor' => ['label' => 'Hintergrund', 'default' => '#f2eadf'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
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::defaultSeed();
|
||||||
|
}
|
||||||
|
|
||||||
|
return substr($seed, 0, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function normalizeConfig(array $input): array
|
||||||
|
{
|
||||||
|
$defaults = self::defaultConfig();
|
||||||
|
$normalized = [];
|
||||||
|
|
||||||
|
foreach ($defaults as $key => $defaultValue) {
|
||||||
|
if (array_key_exists($key, self::colorDefinitions())) {
|
||||||
|
$normalized[$key] = self::normalizeColor((string)($input[$key] ?? $defaultValue));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (str_ends_with($key, 'Fill')) {
|
||||||
|
$normalized[$key] = self::normalizeFill((string)($input[$key] ?? $defaultValue));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (str_ends_with($key, 'FillStops')) {
|
||||||
|
$normalized[$key] = self::normalizeFillStops($input[$key] ?? $defaultValue);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (str_ends_with($key, 'Angle')) {
|
||||||
|
$normalized[$key] = self::normalizeAngle($input[$key] ?? $defaultValue);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalized[$key] = self::normalizeVariant((string)($input[$key] ?? $defaultValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function buildQuery(string $seed, array $config): array
|
||||||
|
{
|
||||||
|
$config = self::normalizeConfig($config);
|
||||||
|
$query = ['seed' => self::normalizeSeed($seed)];
|
||||||
|
$componentFields = array_keys(self::componentDefinitions());
|
||||||
|
|
||||||
|
foreach ($componentFields as $field) {
|
||||||
|
$probability = preg_replace('/Variant$/', 'Probability', $field);
|
||||||
|
if ($config[$field] === '') {
|
||||||
|
$query[$probability] = '0';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query[$field] = $config[$field];
|
||||||
|
$query[$probability] = '100';
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (array_keys(self::colorDefinitions()) as $field) {
|
||||||
|
if ($config[$field] !== '') {
|
||||||
|
$query[$field] = $config[$field];
|
||||||
|
foreach (self::colorCompanionFields($field) as $companionField) {
|
||||||
|
if ($config[$companionField] !== '') {
|
||||||
|
$query[$companionField] = $config[$companionField];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function queryToConfig(array $query): array
|
||||||
|
{
|
||||||
|
$config = self::defaultConfig();
|
||||||
|
|
||||||
|
foreach (array_keys(self::componentDefinitions()) as $field) {
|
||||||
|
$config[$field] = self::normalizeVariant((string)($query[$field] ?? ''));
|
||||||
|
$probability = (int)($query[preg_replace('/Variant$/', 'Probability', $field)] ?? 100);
|
||||||
|
if ($probability <= 0) {
|
||||||
|
$config[$field] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (array_keys(self::colorDefinitions()) as $field) {
|
||||||
|
$config[$field] = self::normalizeColor((string)($query[$field] ?? ''));
|
||||||
|
foreach (self::colorCompanionFields($field) as $companionField) {
|
||||||
|
$config[$companionField] = (string)($query[$companionField] ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::normalizeConfig($config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function fallbackSvg(string $seed, string $label = 'Avatar'): string
|
||||||
|
{
|
||||||
|
return Lorelei::fallbackSvg($seed, $label);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function normalizeVariant(string $value): string
|
||||||
|
{
|
||||||
|
$value = strtolower(trim($value));
|
||||||
|
if ($value === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return preg_match('/^variant\d{2}$/', $value) === 1 ? $value : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function normalizeColor(string $value): string
|
||||||
|
{
|
||||||
|
$value = strtolower(trim($value));
|
||||||
|
if ($value === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return preg_match('/^#[0-9a-f]{6}$/', $value) === 1 ? $value : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function normalizeFill(string $value): string
|
||||||
|
{
|
||||||
|
$value = strtolower(trim($value));
|
||||||
|
if ($value === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return in_array($value, ['solid', 'linear', 'radial'], true) ? $value : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function normalizeFillStops(mixed $value): string
|
||||||
|
{
|
||||||
|
if ($value === '' || $value === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$number = max(2, min(8, (int)$value));
|
||||||
|
return (string)$number;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function normalizeAngle(mixed $value): string
|
||||||
|
{
|
||||||
|
if ($value === '' || $value === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$number = max(-360, min(360, (int)$value));
|
||||||
|
return (string)$number;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function colorCompanionDefaults(): array
|
||||||
|
{
|
||||||
|
$defaults = [];
|
||||||
|
foreach (array_keys(self::colorDefinitions()) as $field) {
|
||||||
|
foreach (self::colorCompanionFields($field) as $companionField) {
|
||||||
|
$defaults[$companionField] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $defaults;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function colorCompanionFields(string $field): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
$field . 'Fill',
|
||||||
|
$field . 'FillStops',
|
||||||
|
$field . 'Angle',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function variantOptions(int $count): array
|
||||||
|
{
|
||||||
|
$options = [];
|
||||||
|
for ($index = 1; $index <= $count; $index++) {
|
||||||
|
$key = sprintf('variant%02d', $index);
|
||||||
|
$options[$key] = 'Variante ' . $index;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $options;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ final class Lorelei
|
|||||||
|
|
||||||
public static function defaultConfig(): array
|
public static function defaultConfig(): array
|
||||||
{
|
{
|
||||||
return [
|
return array_merge([
|
||||||
'eyesVariant' => '',
|
'eyesVariant' => '',
|
||||||
'eyebrowsVariant' => '',
|
'eyebrowsVariant' => '',
|
||||||
'mouthVariant' => '',
|
'mouthVariant' => '',
|
||||||
@@ -30,7 +30,18 @@ final class Lorelei
|
|||||||
'hairVariant' => '',
|
'hairVariant' => '',
|
||||||
'beardVariant' => '',
|
'beardVariant' => '',
|
||||||
'earringsVariant' => '',
|
'earringsVariant' => '',
|
||||||
];
|
'earringsColor' => '',
|
||||||
|
'eyebrowsColor' => '',
|
||||||
|
'eyesColor' => '',
|
||||||
|
'frecklesColor' => '',
|
||||||
|
'glassesColor' => '',
|
||||||
|
'hairColor' => '',
|
||||||
|
'hairAccessoriesColor' => '',
|
||||||
|
'mouthColor' => '',
|
||||||
|
'noseColor' => '',
|
||||||
|
'skinColor' => '',
|
||||||
|
'backgroundColor' => '',
|
||||||
|
], self::colorCompanionDefaults());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function options(): array
|
public static function options(): array
|
||||||
@@ -59,6 +70,23 @@ final class Lorelei
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function colorDefinitions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'earringsColor' => ['label' => 'Ohrringe', 'default' => '#d6b36b'],
|
||||||
|
'eyebrowsColor' => ['label' => 'Augenbrauen', 'default' => '#4a3227'],
|
||||||
|
'eyesColor' => ['label' => 'Augen', 'default' => '#2a2522'],
|
||||||
|
'frecklesColor' => ['label' => 'Sommersprossen', 'default' => '#b67852'],
|
||||||
|
'glassesColor' => ['label' => 'Brille', 'default' => '#26313f'],
|
||||||
|
'hairColor' => ['label' => 'Haare', 'default' => '#4b3128'],
|
||||||
|
'hairAccessoriesColor' => ['label' => 'Haar-Accessoires', 'default' => '#b96f54'],
|
||||||
|
'mouthColor' => ['label' => 'Mund', 'default' => '#9f5a56'],
|
||||||
|
'noseColor' => ['label' => 'Nase', 'default' => '#a56f58'],
|
||||||
|
'skinColor' => ['label' => 'Haut', 'default' => '#e7ba9a'],
|
||||||
|
'backgroundColor' => ['label' => 'Hintergrund', 'default' => '#f5efe5'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public static function legacyColumnsFromConfig(string $seed, array $config): array
|
public static function legacyColumnsFromConfig(string $seed, array $config): array
|
||||||
{
|
{
|
||||||
$config = self::normalizeConfig($config);
|
$config = self::normalizeConfig($config);
|
||||||
@@ -107,6 +135,22 @@ final class Lorelei
|
|||||||
$normalized = [];
|
$normalized = [];
|
||||||
|
|
||||||
foreach ($defaults as $key => $defaultValue) {
|
foreach ($defaults as $key => $defaultValue) {
|
||||||
|
if (array_key_exists($key, self::colorDefinitions())) {
|
||||||
|
$normalized[$key] = self::normalizeColor((string)($input[$key] ?? $defaultValue));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (str_ends_with($key, 'Fill')) {
|
||||||
|
$normalized[$key] = self::normalizeFill((string)($input[$key] ?? $defaultValue));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (str_ends_with($key, 'FillStops')) {
|
||||||
|
$normalized[$key] = self::normalizeFillStops($input[$key] ?? $defaultValue);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (str_ends_with($key, 'Angle')) {
|
||||||
|
$normalized[$key] = self::normalizeAngle($input[$key] ?? $defaultValue);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$normalized[$key] = self::normalizeVariant((string)($input[$key] ?? $defaultValue));
|
$normalized[$key] = self::normalizeVariant((string)($input[$key] ?? $defaultValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,6 +193,17 @@ final class Lorelei
|
|||||||
$query[$params['probability']] = '100';
|
$query[$params['probability']] = '100';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (array_keys(self::colorDefinitions()) as $field) {
|
||||||
|
if ($config[$field] !== '') {
|
||||||
|
$query[$field] = $config[$field];
|
||||||
|
foreach (self::colorCompanionFields($field) as $companionField) {
|
||||||
|
if ($config[$companionField] !== '') {
|
||||||
|
$query[$companionField] = $config[$companionField];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $query;
|
return $query;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,6 +237,13 @@ final class Lorelei
|
|||||||
$config['hairVariant'] = '';
|
$config['hairVariant'] = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (array_keys(self::colorDefinitions()) as $field) {
|
||||||
|
$config[$field] = self::normalizeColor((string)($query[$field] ?? ''));
|
||||||
|
foreach (self::colorCompanionFields($field) as $companionField) {
|
||||||
|
$config[$companionField] = (string)($query[$companionField] ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return self::normalizeConfig($config);
|
return self::normalizeConfig($config);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,4 +302,65 @@ final class Lorelei
|
|||||||
|
|
||||||
return preg_match('/^(variant\d{2}|happy\d{2}|sad\d{2})$/', $value) === 1 ? $value : '';
|
return preg_match('/^(variant\d{2}|happy\d{2}|sad\d{2})$/', $value) === 1 ? $value : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static function normalizeColor(string $value): string
|
||||||
|
{
|
||||||
|
$value = strtolower(trim($value));
|
||||||
|
if ($value === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return preg_match('/^#[0-9a-f]{6}$/', $value) === 1 ? $value : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function normalizeFill(string $value): string
|
||||||
|
{
|
||||||
|
$value = strtolower(trim($value));
|
||||||
|
if ($value === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return in_array($value, ['solid', 'linear', 'radial'], true) ? $value : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function normalizeFillStops(mixed $value): string
|
||||||
|
{
|
||||||
|
if ($value === '' || $value === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$number = max(2, min(8, (int)$value));
|
||||||
|
return (string)$number;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function normalizeAngle(mixed $value): string
|
||||||
|
{
|
||||||
|
if ($value === '' || $value === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$number = max(-360, min(360, (int)$value));
|
||||||
|
return (string)$number;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function colorCompanionDefaults(): array
|
||||||
|
{
|
||||||
|
$defaults = [];
|
||||||
|
foreach (array_keys(self::colorDefinitions()) as $field) {
|
||||||
|
foreach (self::colorCompanionFields($field) as $companionField) {
|
||||||
|
$defaults[$companionField] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $defaults;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function colorCompanionFields(string $field): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
$field . 'Fill',
|
||||||
|
$field . 'FillStops',
|
||||||
|
$field . 'Angle',
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user