This commit is contained in:
@@ -3,11 +3,12 @@ Anweisung: Projektstruktur (Basis-Template) wie in „papa-kind-treff“
|
||||
Aktueller Projektstand
|
||||
- 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`.
|
||||
- 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`.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
@@ -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`
|
||||
- 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`
|
||||
- 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`
|
||||
- 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.
|
||||
|
||||
## 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.
|
||||
- Moderation ist getrennt vom normalen Community-Frontend in einem eigenen Adminbereich.
|
||||
- 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.
|
||||
- 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
|
||||
- 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
|
||||
- eigener Einstellungsbereich für Standortfreigabe, Browser-Hinweise und Consent-Verwaltung
|
||||
- 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
|
||||
- Profilbild-Generator mit visueller Komponenten-Auswahl, generischem Style-/Seed-/Config-Speichermodell und `Random Profilbild`
|
||||
- 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, 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
|
||||
- `config/avatar.php` enthaelt jetzt pro Style auch explizite Beispielpaletten und Beispielprofile als Einbindungsreferenz
|
||||
- Consent-Manager für Analyse und externe Dienste
|
||||
- Navigation aktuell mit `Home`, `Suche`, `Community`
|
||||
- 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 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`.
|
||||
Der direkte Avatar-Endpunkt liegt in `public/api/avatar.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.
|
||||
Farbfelder werden pro Style direkt im Generator gepflegt und zusammen mit Stil und Seed im Profil gespeichert.
|
||||
|
||||
## Consent und rechtlich relevante Integrationen
|
||||
Der aktuelle Code nutzt oder kann nutzen:
|
||||
|
||||
@@ -16,13 +16,220 @@ return [
|
||||
'default_style' => 'lorelei',
|
||||
'enabled_styles' => [
|
||||
'lorelei',
|
||||
'croodles',
|
||||
],
|
||||
'styles' => [
|
||||
'lorelei' => [
|
||||
'label' => 'Lorelei',
|
||||
'class' => \App\Avatar\Lorelei::class,
|
||||
'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';
|
||||
$avatarStyleKeys = array_keys($avatarBuilder ?? []);
|
||||
$currentAvatarStyle = (string)($profile['avatar_style'] ?? ($avatarStyleKeys[0] ?? 'lorelei'));
|
||||
$currentAvatarComponents = $avatarBuilder[$currentAvatarStyle]['components'] ?? [];
|
||||
$dashboardUserId = (int)($_SESSION['user_id'] ?? 0);
|
||||
$editing = isset($editEvent) && $editEvent !== null;
|
||||
$actionEvent = $editing ? 'event_update' : 'event_add';
|
||||
@@ -382,6 +381,107 @@ $sectionLinks = [
|
||||
</section>
|
||||
<?php endforeach; ?>
|
||||
</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>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
@@ -729,6 +729,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const avatarStyleConfigs = {
|
||||
lorelei: {
|
||||
fields: ['eyesVariant', 'eyebrowsVariant', 'mouthVariant', 'glassesVariant', 'hairVariant', 'beardVariant', 'earringsVariant'],
|
||||
colorFields: ['earringsColor', 'eyebrowsColor', 'eyesColor', 'frecklesColor', 'glassesColor', 'hairColor', 'hairAccessoriesColor', 'mouthColor', 'noseColor', 'skinColor', 'backgroundColor'],
|
||||
paramMap: {
|
||||
eyesVariant: 'eyesVariant',
|
||||
eyebrowsVariant: 'eyebrowsVariant',
|
||||
@@ -747,6 +748,29 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
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) => {
|
||||
@@ -769,6 +793,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
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) => {
|
||||
if (avatarStyleConfigs[style]) return avatarStyleConfigs[style];
|
||||
return avatarStyleConfigs.lorelei;
|
||||
@@ -801,6 +839,27 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -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 panel = getAvatarStylePanel(form, style);
|
||||
if (!panel) return;
|
||||
@@ -893,6 +1018,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
panel.querySelectorAll('[data-avatar-field]').forEach((input) => {
|
||||
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);
|
||||
setActiveComponentPanel(style, getActiveAvatarField(form, style));
|
||||
|
||||
@@ -66,6 +66,13 @@ final class AvatarManager
|
||||
'fixed_colors' => [],
|
||||
'color_controls' => [],
|
||||
];
|
||||
$result['croodles'] = [
|
||||
'key' => 'croodles',
|
||||
'label' => Croodles::label(),
|
||||
'class' => Croodles::class,
|
||||
'fixed_colors' => [],
|
||||
'color_controls' => [],
|
||||
];
|
||||
}
|
||||
|
||||
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']] = [
|
||||
'key' => $meta['key'],
|
||||
'label' => $meta['label'],
|
||||
'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
|
||||
{
|
||||
return [
|
||||
return array_merge([
|
||||
'eyesVariant' => '',
|
||||
'eyebrowsVariant' => '',
|
||||
'mouthVariant' => '',
|
||||
@@ -30,7 +30,18 @@ final class Lorelei
|
||||
'hairVariant' => '',
|
||||
'beardVariant' => '',
|
||||
'earringsVariant' => '',
|
||||
];
|
||||
'earringsColor' => '',
|
||||
'eyebrowsColor' => '',
|
||||
'eyesColor' => '',
|
||||
'frecklesColor' => '',
|
||||
'glassesColor' => '',
|
||||
'hairColor' => '',
|
||||
'hairAccessoriesColor' => '',
|
||||
'mouthColor' => '',
|
||||
'noseColor' => '',
|
||||
'skinColor' => '',
|
||||
'backgroundColor' => '',
|
||||
], self::colorCompanionDefaults());
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
$config = self::normalizeConfig($config);
|
||||
@@ -107,6 +135,22 @@ final class Lorelei
|
||||
$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));
|
||||
}
|
||||
|
||||
@@ -149,6 +193,17 @@ final class Lorelei
|
||||
$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;
|
||||
}
|
||||
|
||||
@@ -182,6 +237,13 @@ final class Lorelei
|
||||
$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);
|
||||
}
|
||||
|
||||
@@ -240,4 +302,65 @@ final class Lorelei
|
||||
|
||||
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