Update badge defaults and add nickname customization
Changes price-tag defaults to priceY=9 and priceSize=60% (was 0/100) per real-world feedback — the band sits a bit lower and is more compact by default. Adds three nickname controls mirroring the price- tag ones: text size slider (60-160%), edge-offset slider (0-30% inset from the circle edge, default 14%), and a color row with six preset swatches plus a native color picker for custom colors. The legacy white/black radio is gone; existing items with "white"/"black" values are migrated to hex on first render. Shadow color now auto-flips based on text brightness so any color stays readable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -639,12 +639,12 @@ https://live.staticflickr.com/65535/12345678901_abcdef1234_b.jpg"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Положение по вертикали: <span id="badge-price-y-value">0</span></label>
|
||||
<input type="range" id="badge-price-y" min="-50" max="20" step="1" value="0">
|
||||
<label>Положение по вертикали: <span id="badge-price-y-value">9</span></label>
|
||||
<input type="range" id="badge-price-y" min="-50" max="20" step="1" value="9">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Размер плашки: <span id="badge-price-size-value">100%</span></label>
|
||||
<input type="range" id="badge-price-size" min="60" max="160" step="5" value="100">
|
||||
<label>Размер плашки: <span id="badge-price-size-value">60%</span></label>
|
||||
<input type="range" id="badge-price-size" min="60" max="160" step="5" value="60">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Цвет ленты:</label>
|
||||
@@ -685,17 +685,24 @@ https://live.staticflickr.com/65535/12345678901_abcdef1234_b.jpg"></textarea>
|
||||
<input type="text" id="badge-nickname-value" maxlength="40" placeholder="Никнейм">
|
||||
<span class="hint">По умолчанию — из настроек.</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Размер текста: <span id="badge-nick-size-value">100%</span></label>
|
||||
<input type="range" id="badge-nick-size" min="60" max="160" step="5" value="100">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Отступ от края: <span id="badge-nick-edge-value">14</span></label>
|
||||
<input type="range" id="badge-nick-edge" min="0" max="30" step="1" value="14">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Цвет текста:</label>
|
||||
<div class="badge-radio-row">
|
||||
<label class="radio-label">
|
||||
<input type="radio" name="badge-nick-color" value="white" checked>
|
||||
<span>Белый (с тенью)</span>
|
||||
</label>
|
||||
<label class="radio-label">
|
||||
<input type="radio" name="badge-nick-color" value="black">
|
||||
<span>Чёрный</span>
|
||||
</label>
|
||||
<div class="badge-color-row" data-target="nick">
|
||||
<button type="button" class="badge-color-swatch" data-color="#FFFFFF" style="background:#FFFFFF;border-color:#ccc" title="Белый"></button>
|
||||
<button type="button" class="badge-color-swatch" data-color="#000000" style="background:#000000" title="Чёрный"></button>
|
||||
<button type="button" class="badge-color-swatch" data-color="#FFCC00" style="background:#FFCC00" title="Жёлтый"></button>
|
||||
<button type="button" class="badge-color-swatch" data-color="#FF3B30" style="background:#FF3B30" title="Красный"></button>
|
||||
<button type="button" class="badge-color-swatch" data-color="#34C759" style="background:#34C759" title="Зелёный"></button>
|
||||
<button type="button" class="badge-color-swatch" data-color="#007AFF" style="background:#007AFF" title="Синий"></button>
|
||||
<input type="color" id="badge-nick-color" value="#FFFFFF" title="Свой цвет текста">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4260,6 +4260,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
nickEnabled: document.getElementById('badge-nickname-enabled'),
|
||||
nickOptions: document.getElementById('badge-nickname-options'),
|
||||
nickValue: document.getElementById('badge-nickname-value'),
|
||||
nickColor: document.getElementById('badge-nick-color'),
|
||||
nickSize: document.getElementById('badge-nick-size'),
|
||||
nickSizeValue: document.getElementById('badge-nick-size-value'),
|
||||
nickEdge: document.getElementById('badge-nick-edge'),
|
||||
nickEdgeValue: document.getElementById('badge-nick-edge-value'),
|
||||
btnDownload: document.getElementById('btn-badge-download'),
|
||||
btnRemove: document.getElementById('btn-badge-remove'),
|
||||
batchActions: document.getElementById('badge-batch-actions'),
|
||||
@@ -4372,12 +4377,32 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
c.restore();
|
||||
}
|
||||
|
||||
function drawNicknameArc(c, size, text, color, position) {
|
||||
function hexBrightness(hex) {
|
||||
const c = (hex || '').replace('#', '');
|
||||
if (c.length !== 6) return 255;
|
||||
const r = parseInt(c.substr(0, 2), 16);
|
||||
const g = parseInt(c.substr(2, 2), 16);
|
||||
const b = parseInt(c.substr(4, 2), 16);
|
||||
return (r * 299 + g * 587 + b * 114) / 1000;
|
||||
}
|
||||
|
||||
// Map legacy "white"/"black" values to hex so old items still render.
|
||||
function normalizeNickColor(v) {
|
||||
if (!v) return '#FFFFFF';
|
||||
if (v === 'white') return '#FFFFFF';
|
||||
if (v === 'black') return '#000000';
|
||||
return v;
|
||||
}
|
||||
|
||||
function drawNicknameArc(c, size, text, item, position) {
|
||||
const color = normalizeNickColor(item.nickColor);
|
||||
const sizePct = item.nickSize || 100;
|
||||
const edgePct = (item.nickEdge != null) ? item.nickEdge : 14;
|
||||
const cx = size / 2;
|
||||
const cy = size / 2;
|
||||
const R = size / 2;
|
||||
const textRadius = R * 0.86;
|
||||
const fontSize = Math.max(11, size * 0.075);
|
||||
const textRadius = R * Math.max(0.55, Math.min(0.99, 1 - edgePct / 100));
|
||||
const fontSize = Math.max(8, size * 0.075 * (sizePct / 100));
|
||||
c.save();
|
||||
c.font = `700 ${fontSize}px ${FONT_STACK}`;
|
||||
c.textAlign = 'center';
|
||||
@@ -4400,6 +4425,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
rotationOffset = -Math.PI / 2;
|
||||
}
|
||||
|
||||
// Pick a contrasting shadow so the text is readable against any background.
|
||||
const isDarkText = hexBrightness(color) < 130;
|
||||
const shadowColor = isDarkText ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.75)';
|
||||
const shadowBlur = size * (isDarkText ? 0.012 : 0.016);
|
||||
|
||||
let cumulative = 0;
|
||||
for (let i = 0; i < chars.length; i++) {
|
||||
const w = widths[i];
|
||||
@@ -4410,16 +4440,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
c.save();
|
||||
c.translate(x, y);
|
||||
c.rotate(a + rotationOffset);
|
||||
if (color === 'black') {
|
||||
c.shadowColor = 'rgba(255,255,255,0.7)';
|
||||
c.shadowBlur = size * 0.012;
|
||||
c.fillStyle = '#000';
|
||||
} else {
|
||||
c.shadowColor = 'rgba(0,0,0,0.75)';
|
||||
c.shadowBlur = size * 0.016;
|
||||
c.shadowOffsetY = size * 0.003;
|
||||
c.fillStyle = '#fff';
|
||||
}
|
||||
c.shadowColor = shadowColor;
|
||||
c.shadowBlur = shadowBlur;
|
||||
if (!isDarkText) c.shadowOffsetY = size * 0.003;
|
||||
c.fillStyle = color;
|
||||
c.fillText(chars[i], 0, 0);
|
||||
c.restore();
|
||||
}
|
||||
@@ -4473,7 +4497,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const nick = (item.nickname || '').trim();
|
||||
if (nick) {
|
||||
// Auto-position: if price occupies the bottom, draw nickname as a top arc.
|
||||
drawNicknameArc(c, size, nick, item.nickColor || 'white', hasPrice ? 'top' : 'bottom');
|
||||
drawNicknameArc(c, size, nick, item, hasPrice ? 'top' : 'bottom');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4530,13 +4554,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
showPrice: false,
|
||||
priceValue: '',
|
||||
priceStyle: 'arc',
|
||||
priceY: 0, // -50..+20: vertical offset from default position
|
||||
priceSize: 100, // 60..160 (% scale of band height)
|
||||
priceY: 9, // -50..+20: vertical offset from default position
|
||||
priceSize: 60, // 60..160 (% scale of band height)
|
||||
priceBg: '#FFCC00', // ribbon color
|
||||
priceFg: '#000000', // digits color
|
||||
showNickname: false,
|
||||
nickname: '',
|
||||
nickColor: 'white',
|
||||
nickColor: '#FFFFFF', // text color (hex)
|
||||
nickSize: 100, // 60..160 (% scale of font)
|
||||
nickEdge: 14, // 0..30: percent inset from circle edge (0 = at edge)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4593,9 +4619,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
el.nickEnabled.checked = item.showNickname;
|
||||
el.nickOptions.classList.toggle('hidden', !item.showNickname);
|
||||
el.nickValue.value = item.nickname || badgeState.defaultNickname || '';
|
||||
document.querySelectorAll('input[name="badge-nick-color"]').forEach(r => {
|
||||
r.checked = r.value === item.nickColor;
|
||||
});
|
||||
const nickHex = normalizeNickColor(item.nickColor);
|
||||
item.nickColor = nickHex;
|
||||
el.nickColor.value = nickHex;
|
||||
syncSwatches('nick', nickHex);
|
||||
el.nickSize.value = item.nickSize;
|
||||
el.nickSizeValue.textContent = item.nickSize + '%';
|
||||
el.nickEdge.value = item.nickEdge;
|
||||
el.nickEdgeValue.textContent = item.nickEdge;
|
||||
renderItemsGrid();
|
||||
renderPreview();
|
||||
}
|
||||
@@ -4873,6 +4904,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
} else if (target === 'fg') {
|
||||
item.priceFg = color;
|
||||
el.priceFg.value = color;
|
||||
} else if (target === 'nick') {
|
||||
item.nickColor = color;
|
||||
el.nickColor.value = color;
|
||||
}
|
||||
syncSwatches(target, color);
|
||||
renderPreview();
|
||||
@@ -4899,15 +4933,33 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
renderPreview();
|
||||
});
|
||||
|
||||
document.querySelectorAll('input[name="badge-nick-color"]').forEach(r => {
|
||||
r.addEventListener('change', () => {
|
||||
const item = currentItem();
|
||||
if (!item) return;
|
||||
if (r.checked) {
|
||||
item.nickColor = r.value;
|
||||
renderPreview();
|
||||
}
|
||||
});
|
||||
// Nickname color (hex input + swatches handled together with price swatches)
|
||||
el.nickColor.addEventListener('input', () => {
|
||||
const item = currentItem();
|
||||
if (!item) return;
|
||||
item.nickColor = el.nickColor.value;
|
||||
syncSwatches('nick', item.nickColor);
|
||||
renderPreview();
|
||||
});
|
||||
|
||||
// Nickname size
|
||||
el.nickSize.addEventListener('input', () => {
|
||||
const item = currentItem();
|
||||
if (!item) return;
|
||||
const v = parseInt(el.nickSize.value, 10) || 100;
|
||||
item.nickSize = v;
|
||||
el.nickSizeValue.textContent = v + '%';
|
||||
renderPreview();
|
||||
});
|
||||
|
||||
// Nickname edge offset
|
||||
el.nickEdge.addEventListener('input', () => {
|
||||
const item = currentItem();
|
||||
if (!item) return;
|
||||
const v = parseInt(el.nickEdge.value, 10) || 0;
|
||||
item.nickEdge = v;
|
||||
el.nickEdgeValue.textContent = v;
|
||||
renderPreview();
|
||||
});
|
||||
|
||||
el.btnRemove.addEventListener('click', () => {
|
||||
|
||||
Reference in New Issue
Block a user