+
Outfit-Farbe
+
+ $meta): ?>
+
+ >
+
+ = htmlspecialchars((string)$meta['label'], ENT_QUOTES) ?>
diff --git a/partials/structure/nav.php b/partials/structure/nav.php
index f01c696..302a2d1 100755
--- a/partials/structure/nav.php
+++ b/partials/structure/nav.php
@@ -11,7 +11,7 @@ if ($isLoggedIn) {
if ($pdo) {
$profileSettings = new \App\ProfileSettings($pdo);
$profileSettings->ensureSchema();
- $stmt = $pdo->prepare('SELECT display_name, first_name, avatar_preset, avatar_skin_tone, avatar_hair_style, avatar_hair_color, avatar_eye_color, avatar_beard_style, avatar_glasses_style, avatar_head_shape, avatar_hat_style, avatar_hat_color, avatar_outfit_color FROM user_profiles WHERE user_id = :id LIMIT 1');
+ $stmt = $pdo->prepare('SELECT display_name, first_name, avatar_preset, avatar_skin_tone, avatar_hair_style, avatar_hair_color, avatar_eye_color, avatar_beard_style, avatar_glasses_style, avatar_head_shape, avatar_hat_style, avatar_hat_color, avatar_outfit_style, avatar_outfit_color FROM user_profiles WHERE user_id = :id LIMIT 1');
$stmt->execute(['id' => (int)$_SESSION['user_id']]);
$profileRow = $stmt->fetch(PDO::FETCH_ASSOC) ?: [];
$displayName = trim((string)($profileRow['display_name'] ?? '')) ?: trim((string)($profileRow['first_name'] ?? '')) ?: 'Profil';
diff --git a/public/assets/avatars/layers/modern/beard/full.svg b/public/assets/avatars/layers/modern/beard/full.svg
new file mode 100644
index 0000000..b799da1
--- /dev/null
+++ b/public/assets/avatars/layers/modern/beard/full.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/public/assets/avatars/layers/modern/beard/mustache.svg b/public/assets/avatars/layers/modern/beard/mustache.svg
new file mode 100644
index 0000000..535a8ae
--- /dev/null
+++ b/public/assets/avatars/layers/modern/beard/mustache.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/public/assets/avatars/layers/modern/beard/short.svg b/public/assets/avatars/layers/modern/beard/short.svg
new file mode 100644
index 0000000..00c77f2
--- /dev/null
+++ b/public/assets/avatars/layers/modern/beard/short.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/public/assets/avatars/layers/modern/features/default.svg b/public/assets/avatars/layers/modern/features/default.svg
new file mode 100644
index 0000000..6648a42
--- /dev/null
+++ b/public/assets/avatars/layers/modern/features/default.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/assets/avatars/layers/modern/glasses/round.svg b/public/assets/avatars/layers/modern/glasses/round.svg
new file mode 100644
index 0000000..fc3b976
--- /dev/null
+++ b/public/assets/avatars/layers/modern/glasses/round.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/public/assets/avatars/layers/modern/glasses/square.svg b/public/assets/avatars/layers/modern/glasses/square.svg
new file mode 100644
index 0000000..014d8a1
--- /dev/null
+++ b/public/assets/avatars/layers/modern/glasses/square.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/public/assets/avatars/layers/modern/hair/curly.svg b/public/assets/avatars/layers/modern/hair/curly.svg
new file mode 100644
index 0000000..059de85
--- /dev/null
+++ b/public/assets/avatars/layers/modern/hair/curly.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/public/assets/avatars/layers/modern/hair/fade.svg b/public/assets/avatars/layers/modern/hair/fade.svg
new file mode 100644
index 0000000..2aaffa8
--- /dev/null
+++ b/public/assets/avatars/layers/modern/hair/fade.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/public/assets/avatars/layers/modern/hair/parted.svg b/public/assets/avatars/layers/modern/hair/parted.svg
new file mode 100644
index 0000000..b98134d
--- /dev/null
+++ b/public/assets/avatars/layers/modern/hair/parted.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/public/assets/avatars/layers/modern/hair/short.svg b/public/assets/avatars/layers/modern/hair/short.svg
new file mode 100644
index 0000000..0f7f774
--- /dev/null
+++ b/public/assets/avatars/layers/modern/hair/short.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/public/assets/avatars/layers/modern/hair/waves.svg b/public/assets/avatars/layers/modern/hair/waves.svg
new file mode 100644
index 0000000..ea2d423
--- /dev/null
+++ b/public/assets/avatars/layers/modern/hair/waves.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/public/assets/avatars/layers/modern/hat/beanie.svg b/public/assets/avatars/layers/modern/hat/beanie.svg
new file mode 100644
index 0000000..d5ec582
--- /dev/null
+++ b/public/assets/avatars/layers/modern/hat/beanie.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/public/assets/avatars/layers/modern/hat/cap.svg b/public/assets/avatars/layers/modern/hat/cap.svg
new file mode 100644
index 0000000..aab2d9a
--- /dev/null
+++ b/public/assets/avatars/layers/modern/hat/cap.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/public/assets/avatars/layers/modern/head/oval.svg b/public/assets/avatars/layers/modern/head/oval.svg
new file mode 100644
index 0000000..0d19968
--- /dev/null
+++ b/public/assets/avatars/layers/modern/head/oval.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/public/assets/avatars/layers/modern/head/round.svg b/public/assets/avatars/layers/modern/head/round.svg
new file mode 100644
index 0000000..353b967
--- /dev/null
+++ b/public/assets/avatars/layers/modern/head/round.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/public/assets/avatars/layers/modern/head/square.svg b/public/assets/avatars/layers/modern/head/square.svg
new file mode 100644
index 0000000..9bc08b5
--- /dev/null
+++ b/public/assets/avatars/layers/modern/head/square.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/public/assets/avatars/layers/modern/outfit/crew.svg b/public/assets/avatars/layers/modern/outfit/crew.svg
new file mode 100644
index 0000000..fccbace
--- /dev/null
+++ b/public/assets/avatars/layers/modern/outfit/crew.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/public/assets/avatars/layers/modern/outfit/hoodie.svg b/public/assets/avatars/layers/modern/outfit/hoodie.svg
new file mode 100644
index 0000000..8e691f8
--- /dev/null
+++ b/public/assets/avatars/layers/modern/outfit/hoodie.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/public/assets/avatars/layers/modern/outfit/polo.svg b/public/assets/avatars/layers/modern/outfit/polo.svg
new file mode 100644
index 0000000..084f1d6
--- /dev/null
+++ b/public/assets/avatars/layers/modern/outfit/polo.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/public/assets/avatars/layers/modern/outfit/tee.svg b/public/assets/avatars/layers/modern/outfit/tee.svg
new file mode 100644
index 0000000..3918e7d
--- /dev/null
+++ b/public/assets/avatars/layers/modern/outfit/tee.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/public/assets/css/app.css b/public/assets/css/app.css
index 36730e2..f2e82a2 100755
--- a/public/assets/css/app.css
+++ b/public/assets/css/app.css
@@ -114,84 +114,22 @@ body {
.step { text-align: left; }
.step__icon { width: 34px; height:34px; border-radius: 10px; background: var(--color-accent); color: #fff; display:flex; align-items:center; justify-content:center; font-weight:700; margin-bottom: 8px; }
-.pkt-avatar { --avatar-size: 52px; --avatar-skin: #efc1a3; --avatar-hair: #5b392b; --avatar-eye: #5f4431; --avatar-hat: #38475f; --avatar-outfit: #5f7387; position: relative; display: inline-block; width: var(--avatar-size); height: var(--avatar-size); border-radius: 20px; overflow: hidden; background: #f7efe6; border: 1px solid rgba(74, 52, 38, 0.12); box-shadow: 0 10px 24px rgba(39, 28, 21, 0.08); }
+.pkt-avatar { --avatar-size: 52px; --avatar-skin: #efc1a3; --avatar-hair: #5b392b; --avatar-eye: #5f4431; --avatar-hat: #38475f; --avatar-outfit: #5f7387; position: relative; display: inline-block; width: var(--avatar-size); height: var(--avatar-size); border-radius: 20px; overflow: hidden; background: radial-gradient(circle at 50% 22%, #fffdfa 0%, #f7efe6 62%, #ecdac6 100%); border: 1px solid rgba(74, 52, 38, 0.12); box-shadow: 0 10px 24px rgba(39, 28, 21, 0.08); isolation: isolate; }
.pkt-avatar--nav { --avatar-size: 34px; border-radius: 999px; }
.pkt-avatar--forum-list { --avatar-size: 42px; border-radius: 14px; }
.pkt-avatar--forum { --avatar-size: 52px; border-radius: 16px; }
.pkt-avatar--xl { --avatar-size: 120px; border-radius: 28px; }
.pkt-avatar--xxl { --avatar-size: 168px; border-radius: 36px; }
-.pkt-avatar__img { display: block; width: 100%; height: 100%; object-fit: cover; object-position: center top; }
-.pkt-avatar__svg { display: block; width: 100%; height: 100%; }
-.pkt-avatar__bg { fill: url(#none); }
-.pkt-avatar__bg { fill: #f7efe6; }
-.pkt-avatar__shadow { fill: rgba(70, 52, 40, 0.10); }
-.pkt-avatar__outfit { fill: color-mix(in srgb, var(--avatar-outfit) 88%, black 12%); }
-.pkt-avatar__outfit-detail { fill: none; stroke: rgba(255,255,255,0.45); stroke-width: 3.5; stroke-linecap: round; }
-.pkt-avatar__neck,
-.pkt-avatar__ear,
-.pkt-avatar__face { fill: var(--avatar-skin); }
-.pkt-avatar__face-shade { fill: rgba(88, 58, 35, 0.10); }
-.pkt-avatar__cheek { fill: rgba(204, 112, 100, 0.12); }
-.pkt-avatar__hair-base,
-.pkt-avatar__hair-short,
-.pkt-avatar__hair-fade,
-.pkt-avatar__hair-parted,
-.pkt-avatar__hair-curly,
-.pkt-avatar__hair-waves,
-.pkt-avatar__beard-mustache,
-.pkt-avatar__beard-short,
-.pkt-avatar__beard-full,
-.pkt-avatar__brow { fill: var(--avatar-hair); stroke: none; }
-.pkt-avatar__hair-base { opacity: .15; }
-.pkt-avatar__hair-short,
-.pkt-avatar__hair-fade,
-.pkt-avatar__hair-parted,
-.pkt-avatar__hair-curly,
-.pkt-avatar__hair-waves { display: none; }
-.pkt-avatar--hair-short .pkt-avatar__hair-short,
-.pkt-avatar--hair-fade .pkt-avatar__hair-fade,
-.pkt-avatar--hair-parted .pkt-avatar__hair-parted,
-.pkt-avatar--hair-curly .pkt-avatar__hair-curly,
-.pkt-avatar--hair-waves .pkt-avatar__hair-waves { display: block; }
-.pkt-avatar--hair-bald .pkt-avatar__hair-base,
-.pkt-avatar--hair-bald .pkt-avatar__hair-short,
-.pkt-avatar--hair-bald .pkt-avatar__hair-fade,
-.pkt-avatar--hair-bald .pkt-avatar__hair-parted,
-.pkt-avatar--hair-bald .pkt-avatar__hair-curly,
-.pkt-avatar--hair-bald .pkt-avatar__hair-waves { display: none; }
-.pkt-avatar__hat-beanie,
-.pkt-avatar__hat-beanie-rib,
-.pkt-avatar__hat-cap,
-.pkt-avatar__hat-cap-brim { display: none; fill: var(--avatar-hat); }
-.pkt-avatar__hat-beanie-rib,
-.pkt-avatar__hat-cap-brim { fill: color-mix(in srgb, var(--avatar-hat) 86%, black 14%); }
-.pkt-avatar--hat-beanie .pkt-avatar__hat-beanie,
-.pkt-avatar--hat-beanie .pkt-avatar__hat-beanie-rib,
-.pkt-avatar--hat-cap .pkt-avatar__hat-cap,
-.pkt-avatar--hat-cap .pkt-avatar__hat-cap-brim { display: block; }
-.pkt-avatar__brow { fill: none; stroke: color-mix(in srgb, var(--avatar-hair) 92%, black 8%); stroke-width: 4; stroke-linecap: round; opacity: .9; }
-.pkt-avatar__eye-shell { fill: #fff; }
-.pkt-avatar__eye { fill: var(--avatar-eye); }
-.pkt-avatar__eye-highlight { fill: rgba(255,255,255,0.9); }
-.pkt-avatar__nose { fill: none; stroke: rgba(113, 76, 49, 0.18); stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; }
-.pkt-avatar__mouth { fill: none; stroke: rgba(102, 66, 42, 0.7); stroke-width: 3; stroke-linecap: round; }
-.pkt-avatar__beard-mustache,
-.pkt-avatar__beard-short,
-.pkt-avatar__beard-full { display: none; }
-.pkt-avatar--beard-mustache .pkt-avatar__beard-mustache,
-.pkt-avatar--beard-short .pkt-avatar__beard-short,
-.pkt-avatar--beard-full .pkt-avatar__beard-full { display: block; }
-.pkt-avatar__glasses-round-left,
-.pkt-avatar__glasses-round-right,
-.pkt-avatar__glasses-square-left,
-.pkt-avatar__glasses-square-right,
-.pkt-avatar__glasses-bridge { display: none; fill: none; stroke: rgba(41, 41, 41, 0.78); stroke-width: 2.8; stroke-linecap: round; }
-.pkt-avatar--glasses-round .pkt-avatar__glasses-round-left,
-.pkt-avatar--glasses-round .pkt-avatar__glasses-round-right,
-.pkt-avatar--glasses-round .pkt-avatar__glasses-bridge { display: block; }
-.pkt-avatar--glasses-square .pkt-avatar__glasses-square-left,
-.pkt-avatar--glasses-square .pkt-avatar__glasses-square-right,
-.pkt-avatar--glasses-square .pkt-avatar__glasses-bridge { display: block; }
+.pkt-avatar--layered::before { content: ""; position: absolute; inset: auto 8% -8% 8%; height: 28%; border-radius: 50%; background: radial-gradient(circle at center, rgba(74, 52, 38, 0.16), transparent 70%); filter: blur(10px); z-index: 0; pointer-events: none; }
+.pkt-avatar__layer { position: absolute; inset: 0; display: block; z-index: 1; pointer-events: none; }
+.pkt-avatar__layer--outfit { z-index: 1; }
+.pkt-avatar__layer--head { z-index: 2; }
+.pkt-avatar__layer--hair { z-index: 3; }
+.pkt-avatar__layer--hat { z-index: 4; }
+.pkt-avatar__layer--features { z-index: 5; }
+.pkt-avatar__layer--beard { z-index: 6; }
+.pkt-avatar__layer--glasses { z-index: 7; }
+.pkt-avatar__layer svg { display: block; width: 100%; height: 100%; }
.faq details { border:1px solid var(--color-border); border-radius: var(--radius-sm); padding: 10px 12px; background: #fff; }
.faq summary { cursor:pointer; font-weight: 600; }
@@ -311,18 +249,11 @@ body {
.avatar-builder__layout { display: grid; grid-template-columns: 240px minmax(0, 1fr); gap: 24px; align-items: start; }
.avatar-builder__preview { position: sticky; top: 0; display: grid; justify-items: center; gap: 12px; padding: 18px; background: linear-gradient(180deg, #fffdfa, #f8f4ec); border: 1px solid var(--color-border); border-radius: 18px; }
.avatar-builder__controls { display: grid; gap: 18px; }
-.avatar-preset-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(132px, 1fr)); gap: 12px; }
-.avatar-preset-card { display: grid; gap: 8px; align-content: start; padding: 10px; border: 1px solid var(--color-border); border-radius: 16px; background: #fff; cursor: pointer; transition: border-color .15s ease, box-shadow .15s ease, transform .15s ease; }
-.avatar-preset-card:hover { border-color: var(--color-primary); box-shadow: 0 10px 20px rgba(41, 31, 22, 0.08); transform: translateY(-1px); }
-.avatar-preset-card input { position: absolute; opacity: 0; pointer-events: none; }
-.avatar-preset-card__thumb { display: block; aspect-ratio: 1; border-radius: 14px; overflow: hidden; background: linear-gradient(180deg, #f7efe6, #f0e4d7); border: 1px solid rgba(74, 52, 38, 0.08); }
-.avatar-preset-card__thumb img { width: 100%; height: 100%; object-fit: cover; object-position: center top; display: block; }
-.avatar-preset-card__label { font-size: 13px; font-weight: 700; color: var(--color-text); }
-.avatar-preset-card:has(input:checked) { border-color: var(--color-primary); box-shadow: 0 0 0 3px rgba(52,72,90,0.14); }
.avatar-swatch-group { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 10px; }
.avatar-swatch { display: grid; grid-template-columns: 20px 18px minmax(0, 1fr); gap: 10px; align-items: center; padding: 10px 12px; border: 1px solid var(--color-border); border-radius: 14px; background: #fff; cursor: pointer; }
.avatar-swatch__color { width: 18px; height: 18px; border-radius: 999px; background: var(--swatch-color); border: 1px solid rgba(0,0,0,0.12); }
.avatar-swatch input { margin: 0; }
+.avatar-swatch:has(input:checked) { border-color: var(--color-primary); box-shadow: 0 0 0 3px rgba(52,72,90,0.14); }
.settings-help { display: grid; gap: 14px; }
.settings-help details { border: 1px solid var(--color-border); border-radius: 12px; padding: 12px 14px; background: #fff; }
.settings-help summary { cursor: pointer; font-weight: 700; }
diff --git a/public/assets/js/app.js b/public/assets/js/app.js
index 215b5f6..71ba718 100755
--- a/public/assets/js/app.js
+++ b/public/assets/js/app.js
@@ -732,22 +732,13 @@ document.addEventListener('DOMContentLoaded', () => {
'pkt-avatar--beard-',
'pkt-avatar--glasses-',
'pkt-avatar--hat-',
+ 'pkt-avatar--outfit-',
];
const updateAvatarPreview = (form) => {
const preview = form.querySelector('[data-avatar-preview] .pkt-avatar');
if (!preview) return;
- const presetChoice = form.querySelector('[data-avatar-preset-option]:checked');
- if (presetChoice) {
- const img = preview.querySelector('.pkt-avatar__img');
- const src = presetChoice.getAttribute('data-avatar-src') || '';
- if (img && src) {
- img.src = src;
- }
- return;
- }
-
const readValue = (field) => {
const checked = form.querySelector(`[data-avatar-field="${field}"]:checked`);
if (checked) return checked.value;
@@ -770,6 +761,7 @@ document.addEventListener('DOMContentLoaded', () => {
preview.classList.add(`pkt-avatar--beard-${readValue('avatar_beard_style')}`);
preview.classList.add(`pkt-avatar--glasses-${readValue('avatar_glasses_style')}`);
preview.classList.add(`pkt-avatar--hat-${readValue('avatar_hat_style')}`);
+ preview.classList.add(`pkt-avatar--outfit-${readValue('avatar_outfit_style')}`);
preview.style.setProperty('--avatar-skin', readColor('avatar_skin_tone'));
preview.style.setProperty('--avatar-hair', readColor('avatar_hair_color'));
preview.style.setProperty('--avatar-eye', readColor('avatar_eye_color'));
@@ -779,7 +771,7 @@ document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('[data-avatar-builder]').forEach((form) => {
updateAvatarPreview(form);
- form.querySelectorAll('[data-avatar-field], [data-avatar-preset-option]').forEach((field) => {
+ form.querySelectorAll('[data-avatar-field]').forEach((field) => {
field.addEventListener('change', () => updateAvatarPreview(form));
field.addEventListener('input', () => updateAvatarPreview(form));
});
diff --git a/schema.sql b/schema.sql
index 4a9068c..94aa157 100755
--- a/schema.sql
+++ b/schema.sql
@@ -40,6 +40,7 @@ CREATE TABLE user_profiles (
avatar_head_shape VARCHAR(24) NOT NULL DEFAULT 'round',
avatar_hat_style VARCHAR(24) NOT NULL DEFAULT 'none',
avatar_hat_color VARCHAR(24) NOT NULL DEFAULT 'navy',
+ avatar_outfit_style VARCHAR(24) NOT NULL DEFAULT 'tee',
avatar_outfit_color VARCHAR(24) NOT NULL DEFAULT 'slate',
contact_phone VARBINARY(512) NULL,
contact_email VARBINARY(512) NULL,
diff --git a/src/App/AccountPages.php b/src/App/AccountPages.php
index 52cba09..3670e96 100755
--- a/src/App/AccountPages.php
+++ b/src/App/AccountPages.php
@@ -348,12 +348,13 @@ final class AccountPages
'avatar_head_shape' => 'round',
'avatar_hat_style' => 'none',
'avatar_hat_color' => 'navy',
+ 'avatar_outfit_style' => 'tee',
'avatar_outfit_color' => 'slate',
];
if ($profileSettings) {
$profileSettings->ensureSchema();
}
- $stmt = $pdo?->prepare('SELECT u.email, u.status, p.display_name, p.first_name, p.last_name, p.zip, p.city, p.profession, p.languages, p.about, p.contact_phone, p.location_tracking_preference, p.avatar_preset, p.avatar_skin_tone, p.avatar_hair_style, p.avatar_hair_color, p.avatar_eye_color, p.avatar_beard_style, p.avatar_glasses_style, p.avatar_head_shape, p.avatar_hat_style, p.avatar_hat_color, p.avatar_outfit_color FROM users u LEFT JOIN user_profiles p ON p.user_id = u.id WHERE u.id = :id LIMIT 1');
+ $stmt = $pdo?->prepare('SELECT u.email, u.status, p.display_name, p.first_name, p.last_name, p.zip, p.city, p.profession, p.languages, p.about, p.contact_phone, p.location_tracking_preference, p.avatar_preset, p.avatar_skin_tone, p.avatar_hair_style, p.avatar_hair_color, p.avatar_eye_color, p.avatar_beard_style, p.avatar_glasses_style, p.avatar_head_shape, p.avatar_hat_style, p.avatar_hat_color, p.avatar_outfit_style, p.avatar_outfit_color FROM users u LEFT JOIN user_profiles p ON p.user_id = u.id WHERE u.id = :id LIMIT 1');
$stmt?->execute(['id' => $userId]);
$row = $stmt?->fetch(\PDO::FETCH_ASSOC);
if ($row) {
@@ -420,7 +421,6 @@ final class AccountPages
$section = 'profile';
}
$avatarOptions = Avatar::options();
- $avatarPresetOptions = Avatar::presetOptions();
return compact(
'flash',
@@ -438,7 +438,6 @@ final class AccountPages
'communityCanApply',
'communityRestrictions',
'avatarOptions',
- 'avatarPresetOptions',
'section',
'allowedSections'
);
diff --git a/src/App/Avatar.php b/src/App/Avatar.php
index d3581fb..6b2dae9 100644
--- a/src/App/Avatar.php
+++ b/src/App/Avatar.php
@@ -10,14 +10,15 @@ final class Avatar
return [
'avatar_preset' => 'modern_01',
'avatar_skin_tone' => 'warm',
- 'avatar_hair_style' => 'short',
+ 'avatar_hair_style' => 'parted',
'avatar_hair_color' => 'brown',
'avatar_eye_color' => 'brown',
- 'avatar_beard_style' => 'none',
+ 'avatar_beard_style' => 'short',
'avatar_glasses_style' => 'none',
- 'avatar_head_shape' => 'round',
+ 'avatar_head_shape' => 'oval',
'avatar_hat_style' => 'none',
'avatar_hat_color' => 'navy',
+ 'avatar_outfit_style' => 'tee',
'avatar_outfit_color' => 'slate',
];
}
@@ -25,34 +26,33 @@ final class Avatar
public static function options(): array
{
return [
- 'avatar_preset' => self::presetOptions(),
'avatar_skin_tone' => [
- 'fair' => ['label' => 'Hell', 'color' => '#f5d7c2'],
- 'warm' => ['label' => 'Warm', 'color' => '#efc1a3'],
- 'golden' => ['label' => 'Gold', 'color' => '#dca57f'],
- 'deep' => ['label' => 'Tief', 'color' => '#a86d49'],
- 'rich' => ['label' => 'Kräftig', 'color' => '#7d4d31'],
+ 'fair' => ['label' => 'Hell', 'color' => '#f6d7c1'],
+ 'warm' => ['label' => 'Warm', 'color' => '#eec0a0'],
+ 'golden' => ['label' => 'Gold', 'color' => '#d7a179'],
+ 'deep' => ['label' => 'Tief', 'color' => '#9c633e'],
+ 'rich' => ['label' => 'Kräftig', 'color' => '#6f432b'],
],
'avatar_hair_style' => [
+ 'parted' => 'Seitenscheitel',
'short' => 'Kurz',
'fade' => 'Fade',
- 'parted' => 'Seitlich',
'curly' => 'Lockig',
- 'waves' => 'Wellen',
+ 'waves' => 'Wellig',
'bald' => 'Kahl',
],
'avatar_hair_color' => [
- 'black' => ['label' => 'Schwarz', 'color' => '#1d1b1b'],
- 'brown' => ['label' => 'Braun', 'color' => '#5b392b'],
- 'blonde' => ['label' => 'Blond', 'color' => '#d7b15f'],
- 'red' => ['label' => 'Rot', 'color' => '#9e4f35'],
- 'gray' => ['label' => 'Grau', 'color' => '#767676'],
+ 'black' => ['label' => 'Schwarz', 'color' => '#191919'],
+ 'brown' => ['label' => 'Braun', 'color' => '#65442f'],
+ 'blonde' => ['label' => 'Blond', 'color' => '#d3a053'],
+ 'red' => ['label' => 'Rot', 'color' => '#9f5a34'],
+ 'gray' => ['label' => 'Grau', 'color' => '#8f8c87'],
],
'avatar_eye_color' => [
- 'brown' => ['label' => 'Braun', 'color' => '#5f4431'],
- 'hazel' => ['label' => 'Hasel', 'color' => '#7f6845'],
- 'green' => ['label' => 'Grün', 'color' => '#54704e'],
- 'blue' => ['label' => 'Blau', 'color' => '#4d6e92'],
+ 'brown' => ['label' => 'Braun', 'color' => '#5e4330'],
+ 'hazel' => ['label' => 'Hasel', 'color' => '#7c6848'],
+ 'green' => ['label' => 'Grün', 'color' => '#4f6c52'],
+ 'blue' => ['label' => 'Blau', 'color' => '#507196'],
],
'avatar_beard_style' => [
'none' => 'Ohne',
@@ -76,11 +76,17 @@ final class Avatar
'cap' => 'Cap',
],
'avatar_hat_color' => [
- 'navy' => ['label' => 'Navy', 'color' => '#38475f'],
- 'charcoal' => ['label' => 'Anthrazit', 'color' => '#3c3f45'],
- 'forest' => ['label' => 'Waldgrün', 'color' => '#48614d'],
- 'brick' => ['label' => 'Ziegel', 'color' => '#975244'],
- 'sand' => ['label' => 'Sand', 'color' => '#b79e79'],
+ 'navy' => ['label' => 'Navy', 'color' => '#42526a'],
+ 'charcoal' => ['label' => 'Anthrazit', 'color' => '#43464d'],
+ 'forest' => ['label' => 'Waldgrün', 'color' => '#51634f'],
+ 'brick' => ['label' => 'Ziegel', 'color' => '#9a5a47'],
+ 'sand' => ['label' => 'Sand', 'color' => '#baa078'],
+ ],
+ 'avatar_outfit_style' => [
+ 'tee' => 'T-Shirt',
+ 'hoodie' => 'Hoodie',
+ 'polo' => 'Polo',
+ 'crew' => 'Pullover',
],
'avatar_outfit_color' => [
'slate' => ['label' => 'Schiefer', 'color' => '#5f7387'],
@@ -92,29 +98,6 @@ final class Avatar
];
}
- public static function presetOptions(): array
- {
- $options = [];
- for ($i = 1; $i <= 30; $i++) {
- $id = sprintf('modern_%02d', $i);
- $options[$id] = [
- 'label' => 'Modern ' . str_pad((string)$i, 2, '0', STR_PAD_LEFT),
- 'src' => self::presetPublicPath($id),
- ];
- }
- return $options;
- }
-
- public static function presetPublicPath(string $preset): string
- {
- return '/assets/avatars/presets/' . rawurlencode($preset) . '.png';
- }
-
- public static function presetFilePath(string $preset): string
- {
- return dirname(__DIR__, 2) . '/public/assets/avatars/presets/' . $preset . '.png';
- }
-
public static function selectColumns(string $alias = 'p'): string
{
$columns = array_keys(self::defaults());
@@ -132,7 +115,7 @@ final class Avatar
foreach ($defaults as $key => $defaultValue) {
$value = (string)($input[$key] ?? $defaultValue);
- if (!array_key_exists($value, $options[$key])) {
+ if (isset($options[$key]) && !array_key_exists($value, $options[$key])) {
$value = $defaultValue;
}
$normalized[$key] = $value;
@@ -143,117 +126,70 @@ final class Avatar
public static function render(array $row, string $name = 'Mitglied', string $size = 'md'): string
{
- static $instanceCounter = 0;
- $instanceCounter++;
$avatar = self::normalize($row);
$options = self::options();
- $preset = (string)($avatar['avatar_preset'] ?? '');
-
- if ($preset !== '' && is_file(self::presetFilePath($preset))) {
- $classes = [
- 'pkt-avatar',
- 'pkt-avatar--' . preg_replace('/[^a-z0-9\-]/', '', strtolower($size)),
- 'pkt-avatar--preset',
- ];
-
- return sprintf(
- '
',
- htmlspecialchars(implode(' ', $classes), ENT_QUOTES),
- htmlspecialchars('Avatar von ' . $name, ENT_QUOTES),
- htmlspecialchars(self::presetPublicPath($preset), ENT_QUOTES)
- );
- }
-
- $skin = $options['avatar_skin_tone'][$avatar['avatar_skin_tone']]['color'];
- $hair = $options['avatar_hair_color'][$avatar['avatar_hair_color']]['color'];
- $eyes = $options['avatar_eye_color'][$avatar['avatar_eye_color']]['color'];
- $hat = $options['avatar_hat_color'][$avatar['avatar_hat_color']]['color'];
- $outfit = $options['avatar_outfit_color'][$avatar['avatar_outfit_color']]['color'];
$classes = [
'pkt-avatar',
'pkt-avatar--' . preg_replace('/[^a-z0-9\-]/', '', strtolower($size)),
+ 'pkt-avatar--layered',
'pkt-avatar--head-' . $avatar['avatar_head_shape'],
'pkt-avatar--hair-' . $avatar['avatar_hair_style'],
'pkt-avatar--beard-' . $avatar['avatar_beard_style'],
'pkt-avatar--glasses-' . $avatar['avatar_glasses_style'],
'pkt-avatar--hat-' . $avatar['avatar_hat_style'],
+ 'pkt-avatar--outfit-' . $avatar['avatar_outfit_style'],
];
$style = sprintf(
'--avatar-skin:%s;--avatar-hair:%s;--avatar-eye:%s;--avatar-hat:%s;--avatar-outfit:%s;',
- $skin,
- $hair,
- $eyes,
- $hat,
- $outfit
+ $options['avatar_skin_tone'][$avatar['avatar_skin_tone']]['color'],
+ $options['avatar_hair_color'][$avatar['avatar_hair_color']]['color'],
+ $options['avatar_eye_color'][$avatar['avatar_eye_color']]['color'],
+ $options['avatar_hat_color'][$avatar['avatar_hat_color']]['color'],
+ $options['avatar_outfit_color'][$avatar['avatar_outfit_color']]['color']
);
- $label = htmlspecialchars('Avatar von ' . $name, ENT_QUOTES);
- $clipId = 'pktAvatarClip' . $instanceCounter;
+ $layers = [
+ self::renderLayer('outfit', $avatar['avatar_outfit_style']),
+ self::renderLayer('head', $avatar['avatar_head_shape']),
+ self::renderLayer('hair', $avatar['avatar_hair_style']),
+ self::renderLayer('hat', $avatar['avatar_hat_style']),
+ self::renderLayer('features', 'default'),
+ self::renderLayer('beard', $avatar['avatar_beard_style']),
+ self::renderLayer('glasses', $avatar['avatar_glasses_style']),
+ ];
return sprintf(
- '
' .
- '' .
- '' .
- ' ' .
- ' ' .
- '' .
- ' ' .
- ' ' .
- ' ' .
- ' ' .
- ' ' .
- ' ' .
- ' ' .
- ' ' .
- ' ' .
- ' ' .
- ' ' .
- '' .
- ' ' .
- ' ' .
- ' ' .
- ' ' .
- ' ' .
- ' ' .
- ' ' .
- '' .
- ' ' .
- ' ' .
- ' ' .
- ' ' .
- ' ' .
- ' ' .
- ' ' .
- ' ' .
- ' ' .
- ' ' .
- ' ' .
- ' ' .
- ' ' .
- ' ' .
- ' ' .
- '' .
- ' ' .
- ' ' .
- ' ' .
- ' ' .
- '' .
- ' ' .
- ' ' .
- ' ' .
- ' ' .
- ' ' .
- ' ' .
- ' ' .
- ' ' .
- ' ',
+ '
%s ',
htmlspecialchars(implode(' ', $classes), ENT_QUOTES),
htmlspecialchars($style, ENT_QUOTES),
- $label,
- htmlspecialchars($clipId, ENT_QUOTES),
- htmlspecialchars($clipId, ENT_QUOTES)
+ htmlspecialchars('Avatar von ' . $name, ENT_QUOTES),
+ implode('', array_filter($layers))
);
}
+
+ private static function renderLayer(string $category, string $variant): string
+ {
+ if ($variant === 'none') {
+ return '';
+ }
+
+ $file = self::layerFilePath($category, $variant);
+ if (!is_file($file)) {
+ return '';
+ }
+
+ $svg = file_get_contents($file);
+ if ($svg === false) {
+ return '';
+ }
+
+ return '
' . $svg . ' ';
+ }
+
+ public static function layerFilePath(string $category, string $variant): string
+ {
+ return dirname(__DIR__, 2) . '/public/assets/avatars/layers/modern/' . $category . '/' . $variant . '.svg';
+ }
}
diff --git a/src/App/ProfileSettings.php b/src/App/ProfileSettings.php
index fe65f5c..0a2874e 100644
--- a/src/App/ProfileSettings.php
+++ b/src/App/ProfileSettings.php
@@ -39,6 +39,7 @@ final class ProfileSettings
'avatar_head_shape' => "ALTER TABLE user_profiles ADD COLUMN avatar_head_shape VARCHAR(24) NOT NULL DEFAULT 'round' AFTER avatar_glasses_style",
'avatar_hat_style' => "ALTER TABLE user_profiles ADD COLUMN avatar_hat_style VARCHAR(24) NOT NULL DEFAULT 'none' AFTER avatar_head_shape",
'avatar_hat_color' => "ALTER TABLE user_profiles ADD COLUMN avatar_hat_color VARCHAR(24) NOT NULL DEFAULT 'navy' AFTER avatar_hat_style",
+ 'avatar_outfit_style' => "ALTER TABLE user_profiles ADD COLUMN avatar_outfit_style VARCHAR(24) NOT NULL DEFAULT 'tee' AFTER avatar_hat_color",
'avatar_outfit_color' => "ALTER TABLE user_profiles ADD COLUMN avatar_outfit_color VARCHAR(24) NOT NULL DEFAULT 'slate' AFTER avatar_hat_color",
];
foreach ($avatarColumns as $column => $sql) {
@@ -86,27 +87,11 @@ final class ProfileSettings
}
$this->ensureSchema();
- $preset = (string)($avatarConfig['avatar_preset'] ?? '');
- if ($preset !== '') {
- $options = Avatar::presetOptions();
- $preset = array_key_exists($preset, $options) ? $preset : 'modern_01';
- $stmt = $this->pdo->prepare(
- 'UPDATE user_profiles
- SET avatar_preset = :preset,
- updated_at = NOW()
- WHERE user_id = :id'
- );
- $stmt->execute([
- 'preset' => $preset,
- 'id' => $userId,
- ]);
- return;
- }
-
$avatar = Avatar::normalize($avatarConfig);
$stmt = $this->pdo->prepare(
'UPDATE user_profiles
- SET avatar_skin_tone = :skin,
+ SET avatar_preset = :preset,
+ avatar_skin_tone = :skin,
avatar_hair_style = :hairStyle,
avatar_hair_color = :hairColor,
avatar_eye_color = :eyeColor,
@@ -115,11 +100,13 @@ final class ProfileSettings
avatar_head_shape = :headShape,
avatar_hat_style = :hatStyle,
avatar_hat_color = :hatColor,
+ avatar_outfit_style = :outfitStyle,
avatar_outfit_color = :outfitColor,
updated_at = NOW()
WHERE user_id = :id'
);
$stmt->execute([
+ 'preset' => $avatar['avatar_preset'],
'skin' => $avatar['avatar_skin_tone'],
'hairStyle' => $avatar['avatar_hair_style'],
'hairColor' => $avatar['avatar_hair_color'],
@@ -129,6 +116,7 @@ final class ProfileSettings
'headShape' => $avatar['avatar_head_shape'],
'hatStyle' => $avatar['avatar_hat_style'],
'hatColor' => $avatar['avatar_hat_color'],
+ 'outfitStyle' => $avatar['avatar_outfit_style'],
'outfitColor' => $avatar['avatar_outfit_color'],
'id' => $userId,
]);