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

This commit is contained in:
2026-07-28 02:03:01 +02:00
parent 0ac69fdfd5
commit aa86001760
18 changed files with 827 additions and 385 deletions

View File

@@ -726,51 +726,73 @@ document.addEventListener('DOMContentLoaded', () => {
});
document.addEventListener('pkt:open-consent', openConsentModal);
const readAvatarValue = (form, field) => {
const checked = form.querySelector(`[data-avatar-field="${field}"]:checked`);
const avatarStyleConfigs = {
lorelei: {
fields: ['eyesVariant', 'eyebrowsVariant', 'mouthVariant', 'glassesVariant', 'hairVariant', 'beardVariant', 'earringsVariant'],
paramMap: {
eyesVariant: 'eyesVariant',
eyebrowsVariant: 'eyebrowsVariant',
mouthVariant: 'mouthVariant',
glassesVariant: 'glassesVariant',
hairVariant: 'hairVariant',
beardVariant: 'beardVariant',
earringsVariant: 'earringsVariant',
},
optionalProbabilityMap: {
glassesVariant: 'glassesProbability',
beardVariant: 'beardProbability',
earringsVariant: 'earringsProbability',
},
alwaysProbabilityMap: {
hairVariant: 'hairProbability',
},
},
};
const getCurrentAvatarStyle = (form) => {
const input = form.querySelector('[data-avatar-style-select]');
const value = input?.value || '';
if (value) return value;
return form.querySelector('[data-avatar-style-panel]')?.getAttribute('data-avatar-style-panel') || 'lorelei';
};
const getAvatarSeedInput = (form) => form.querySelector('[data-avatar-seed]');
const getAvatarStylePanel = (form, style) => form.querySelector(`[data-avatar-style-panel="${style}"]`);
const readAvatarValue = (form, style, field) => {
const panel = getAvatarStylePanel(form, style);
if (!panel) return '';
const checked = panel.querySelector(`[data-avatar-field="${field}"]:checked`);
if (checked) return checked.value;
const input = form.querySelector(`[data-avatar-field="${field}"]`);
const input = panel.querySelector(`[data-avatar-field="${field}"]`);
return input ? input.value : '';
};
const avatarComponentFieldMap = {
avatar_lorelei_eyes_variant: 'eyesVariant',
avatar_lorelei_eyebrows_variant: 'eyebrowsVariant',
avatar_lorelei_mouth_variant: 'mouthVariant',
avatar_lorelei_glasses_variant: 'glassesVariant',
avatar_lorelei_hair_variant: 'hairVariant',
avatar_lorelei_beard_variant: 'beardVariant',
avatar_lorelei_earrings_variant: 'earringsVariant',
const getAvatarStyleConfig = (style) => {
if (avatarStyleConfigs[style]) return avatarStyleConfigs[style];
return avatarStyleConfigs.lorelei;
};
const avatarComponentFields = Object.keys(avatarComponentFieldMap);
const avatarEndpoint = '/api/avatar.php';
const avatarOptionalProbabilityMap = {
avatar_lorelei_glasses_variant: 'glassesProbability',
avatar_lorelei_beard_variant: 'beardProbability',
avatar_lorelei_earrings_variant: 'earringsProbability',
};
const buildAvatarParams = (form, overrides = {}) => {
const seed = overrides.avatar_preset || readAvatarValue(form, 'avatar_preset');
const buildAvatarParams = (form, style = getCurrentAvatarStyle(form), overrides = {}) => {
const styleConfig = getAvatarStyleConfig(style);
const seed = overrides.avatar_seed || getAvatarSeedInput(form)?.value || '';
if (!seed) return null;
const params = new URLSearchParams({ seed });
const hairValue = Object.prototype.hasOwnProperty.call(overrides, 'avatar_lorelei_hair_variant')
? overrides.avatar_lorelei_hair_variant
: readAvatarValue(form, 'avatar_lorelei_hair_variant');
params.set('hairProbability', '100');
if (hairValue) {
params.set('hairVariant', hairValue);
}
avatarComponentFields.forEach((field) => {
if (field === 'avatar_lorelei_hair_variant') return;
const params = new URLSearchParams({ style, seed });
styleConfig.fields.forEach((field) => {
const value = Object.prototype.hasOwnProperty.call(overrides, field)
? overrides[field]
: readAvatarValue(form, field);
const param = avatarComponentFieldMap[field];
const probabilityParam = avatarOptionalProbabilityMap[field];
: readAvatarValue(form, style, field);
const param = styleConfig.paramMap[field];
const probabilityParam = styleConfig.optionalProbabilityMap[field];
const alwaysProbabilityParam = styleConfig.alwaysProbabilityMap[field];
if (alwaysProbabilityParam) {
params.set(alwaysProbabilityParam, value ? '100' : '0');
}
if (probabilityParam) {
params.set(probabilityParam, value ? '100' : '0');
@@ -788,7 +810,8 @@ document.addEventListener('DOMContentLoaded', () => {
const image = preview.querySelector('.pkt-avatar__img');
if (!image) return;
const params = buildAvatarParams(form);
const style = getCurrentAvatarStyle(form);
const params = buildAvatarParams(form, style);
if (!params) return;
const url = `${avatarEndpoint}?${params.toString()}`;
@@ -796,20 +819,24 @@ document.addEventListener('DOMContentLoaded', () => {
image.src = url;
};
const getActiveAvatarField = (form) => {
const activeTab = form.querySelector('[data-avatar-component-tab].is-active');
return activeTab?.getAttribute('data-avatar-component-tab') || 'avatar_lorelei_eyes_variant';
const getActiveAvatarField = (form, style) => {
const panel = getAvatarStylePanel(form, style);
if (!panel) return '';
const activeTab = panel.querySelector('[data-avatar-component-tab].is-active');
return activeTab?.getAttribute('data-avatar-component-tab') || panel.querySelector('[data-avatar-component-tab]')?.getAttribute('data-avatar-component-tab') || '';
};
const updateAvatarOptionPreviews = (form, activeField = null) => {
const updateAvatarOptionPreviews = (form, style, activeField = null) => {
const panel = getAvatarStylePanel(form, style);
if (!panel) return;
const selector = activeField
? `[data-avatar-component-panel="${activeField}"] [data-avatar-option-thumb]`
: '[data-avatar-option-thumb]';
form.querySelectorAll(selector).forEach((image) => {
panel.querySelectorAll(selector).forEach((image) => {
const field = image.getAttribute('data-avatar-option-field') || '';
const value = image.getAttribute('data-avatar-option-value') || '';
const params = buildAvatarParams(form, { [field]: value });
const params = buildAvatarParams(form, style, { [field]: value });
if (!params) return;
const src = `${avatarEndpoint}?${params.toString()}`;
@@ -819,52 +846,83 @@ document.addEventListener('DOMContentLoaded', () => {
});
};
const ensureAvatarBuilderLoaded = (form) => {
if (form.dataset.avatarInitialized === 'true') return;
form.dataset.avatarInitialized = 'true';
const ensureAvatarBuilderLoaded = (form, style = getCurrentAvatarStyle(form)) => {
const loadedStyles = new Set((form.dataset.avatarInitializedStyles || '').split(',').filter(Boolean));
if (loadedStyles.has(style)) return;
loadedStyles.add(style);
form.dataset.avatarInitializedStyles = Array.from(loadedStyles).join(',');
updateAvatarPreview(form);
updateAvatarOptionPreviews(form, getActiveAvatarField(form));
updateAvatarOptionPreviews(form, style, getActiveAvatarField(form, style));
};
document.querySelectorAll('[data-avatar-builder]').forEach((form) => {
form.querySelectorAll('[data-avatar-field]').forEach((field) => {
field.addEventListener('change', () => {
ensureAvatarBuilderLoaded(form);
const style = field.getAttribute('data-avatar-style') || getCurrentAvatarStyle(form);
ensureAvatarBuilderLoaded(form, style);
updateAvatarPreview(form);
updateAvatarOptionPreviews(form, getActiveAvatarField(form));
updateAvatarOptionPreviews(form, style, getActiveAvatarField(form, style));
});
field.addEventListener('input', () => {
ensureAvatarBuilderLoaded(form);
const style = field.getAttribute('data-avatar-style') || getCurrentAvatarStyle(form);
ensureAvatarBuilderLoaded(form, style);
updateAvatarPreview(form);
updateAvatarOptionPreviews(form, getActiveAvatarField(form));
updateAvatarOptionPreviews(form, style, getActiveAvatarField(form, style));
});
});
const setActiveComponentPanel = (field) => {
form.querySelectorAll('[data-avatar-component-tab]').forEach((button) => {
const setActiveComponentPanel = (style, field) => {
const panel = getAvatarStylePanel(form, style);
if (!panel) return;
panel.querySelectorAll('[data-avatar-component-tab]').forEach((button) => {
const active = button.getAttribute('data-avatar-component-tab') === field;
button.classList.toggle('is-active', active);
button.setAttribute('aria-selected', active ? 'true' : 'false');
});
form.querySelectorAll('[data-avatar-component-panel]').forEach((panel) => {
panel.classList.toggle('is-active', panel.getAttribute('data-avatar-component-panel') === field);
panel.querySelectorAll('[data-avatar-component-panel]').forEach((componentPanel) => {
componentPanel.classList.toggle('is-active', componentPanel.getAttribute('data-avatar-component-panel') === field);
});
updateAvatarOptionPreviews(form, field);
updateAvatarOptionPreviews(form, style, field);
};
const setActiveStyle = (style) => {
form.querySelectorAll('[data-avatar-style-panel]').forEach((panel) => {
const active = panel.getAttribute('data-avatar-style-panel') === style;
panel.hidden = !active;
panel.classList.toggle('is-active', active);
panel.querySelectorAll('[data-avatar-field]').forEach((input) => {
input.disabled = !active;
});
});
ensureAvatarBuilderLoaded(form, style);
setActiveComponentPanel(style, getActiveAvatarField(form, style));
updateAvatarPreview(form);
};
form.querySelector('[data-avatar-style-select]')?.addEventListener('change', (event) => {
const nextStyle = event.currentTarget?.value || getCurrentAvatarStyle(form);
setActiveStyle(nextStyle);
});
form.querySelectorAll('[data-avatar-component-tab]').forEach((button) => {
button.addEventListener('click', () => {
const style = button.getAttribute('data-avatar-style') || getCurrentAvatarStyle(form);
const field = button.getAttribute('data-avatar-component-tab');
if (field) {
ensureAvatarBuilderLoaded(form);
setActiveComponentPanel(field);
ensureAvatarBuilderLoaded(form, style);
setActiveComponentPanel(style, field);
}
});
});
avatarComponentFields.forEach((field) => {
form.querySelectorAll(`[data-avatar-field="${field}"]`).forEach((input) => {
input.addEventListener('change', () => setActiveComponentPanel(field));
Object.values(avatarStyleConfigs).forEach((styleConfig) => {
styleConfig.fields.forEach((fieldName) => {
form.querySelectorAll(`[data-avatar-field="${fieldName}"]`).forEach((input) => {
input.addEventListener('change', () => {
const style = input.getAttribute('data-avatar-style') || getCurrentAvatarStyle(form);
setActiveComponentPanel(style, fieldName);
});
});
});
});
@@ -891,29 +949,36 @@ document.addEventListener('DOMContentLoaded', () => {
};
form.querySelector('[data-avatar-random]')?.addEventListener('click', () => {
const seedField = form.querySelector('[data-avatar-field="avatar_preset"]');
const style = getCurrentAvatarStyle(form);
const styleConfig = getAvatarStyleConfig(style);
const seedField = getAvatarSeedInput(form);
if (!(seedField instanceof HTMLInputElement)) return;
seedField.value = buildSeed();
avatarComponentFields.forEach((field) => {
const options = Array.from(form.querySelectorAll(`[data-avatar-field="${field}"]`));
styleConfig.fields.forEach((field) => {
const panel = getAvatarStylePanel(form, style);
if (!panel) return;
const options = Array.from(panel.querySelectorAll(`[data-avatar-field="${field}"]`));
if (!options.length) return;
const randomOption = options[Math.floor(Math.random() * options.length)];
if (!(randomOption instanceof HTMLInputElement)) return;
randomOption.checked = true;
});
ensureAvatarBuilderLoaded(form);
ensureAvatarBuilderLoaded(form, style);
updateAvatarPreview(form);
updateAvatarOptionPreviews(form, getActiveAvatarField(form));
updateAvatarOptionPreviews(form, style, getActiveAvatarField(form, style));
});
setActiveStyle(getCurrentAvatarStyle(form));
});
document.querySelectorAll('[data-modal-open="modalAvatar"]').forEach((button) => {
button.addEventListener('click', () => {
const form = document.querySelector('#modalAvatar [data-avatar-builder]');
if (!form) return;
window.setTimeout(() => ensureAvatarBuilderLoaded(form), 20);
const style = getCurrentAvatarStyle(form);
window.setTimeout(() => ensureAvatarBuilderLoaded(form, style), 20);
});
});