TierListMaker:修订间差异
来自卡厄思梦境WIKI
无编辑摘要 |
无编辑摘要 |
||
| 第180行: | 第180行: | ||
const color = th.getAttribute('data-color') || '#888888'; | const color = th.getAttribute('data-color') || '#888888'; | ||
applyThColor(th, color); | applyThColor(th, color); | ||
// | // Use contains() to allow editing label text safely (fix S/A/B/C not editable) | ||
th.addEventListener('click', (e) => { | th.addEventListener('click', (e) => { | ||
const labelEl = th.querySelector('.tier-label'); | |||
// allow editing | if (labelEl && labelEl.contains(e.target)) { | ||
// Clicking inside the editable label: allow text editing | |||
return; | return; | ||
} | } | ||
| 第246行: | 第247行: | ||
el.classList.add('dragging'); | el.classList.add('dragging'); | ||
e.dataTransfer.effectAllowed = 'move'; | e.dataTransfer.effectAllowed = 'move'; | ||
if (!el.dataset.id) { | if (!el.dataset.id) { | ||
const img = el.querySelector('img'); | const img = el.querySelector('img'); | ||
| 第282行: | 第282行: | ||
setupDropzone(pool); | setupDropzone(pool); | ||
} | } | ||
// Observe dynamically added avatars | // Observe dynamically added avatars | ||
const poolObserver = new MutationObserver((mutations) => { | const poolObserver = new MutationObserver((mutations) => { | ||
mutations.forEach((m) => { | mutations.forEach((m) => { | ||
| 第289行: | 第289行: | ||
if (node.classList && node.classList.contains('avatar-frame')) { | if (node.classList && node.classList.contains('avatar-frame')) { | ||
setupDraggable(node); | setupDraggable(node); | ||
} else { | } else if (node.querySelectorAll) { | ||
node.querySelectorAll('.avatar-frame').forEach(setupDraggable); | |||
} | } | ||
} | } | ||
| 第324行: | 第324行: | ||
// Enable interactions | // Enable interactions | ||
th.addEventListener('click', (e) => { | th.addEventListener('click', (e) => { | ||
const labelEl = th.querySelector('.tier-label'); | |||
if (labelEl && labelEl.contains(e.target)) return; | |||
openColorPalette(th, e); | openColorPalette(th, e); | ||
}); | }); | ||
| 第341行: | 第342行: | ||
}); | }); | ||
} | } | ||
// Export to PNG | // Export to PNG (preserve row gaps and backgrounds) | ||
function exportPNG() { | function exportPNG() { | ||
const table = document.getElementById('tierlist-table'); | const table = document.getElementById('tierlist-table'); | ||
const tableRect = table.getBoundingClientRect(); | const tableRect = table.getBoundingClientRect(); | ||
const rows = Array.from(table.querySelectorAll('tr')); | const rows = Array.from(table.querySelectorAll('tr')); | ||
const rowInfo = rows.map(row => { | const rowInfo = rows.map(row => { | ||
const rowRect = row.getBoundingClientRect(); | |||
const th = row.querySelector('th.tier-th'); | const th = row.querySelector('th.tier-th'); | ||
const dz = row.querySelector('.tier-dropzone'); | const dz = row.querySelector('.tier-dropzone'); | ||
const thRect = th.getBoundingClientRect(); | const thRect = th.getBoundingClientRect(); | ||
const label = th.querySelector('.tier-label')?.textContent || ''; | const label = th.querySelector('.tier-label')?.textContent || ''; | ||
const color = th.getAttribute('data-color') || '#888'; | const color = th.getAttribute('data-color') || '#888'; | ||
const textColor = idealTextColor(color); | const textColor = idealTextColor(color); | ||
const dzBg = getComputedStyle(dz).backgroundColor || '#ffffff'; | const dzBg = getComputedStyle(dz).backgroundColor || '#ffffff'; | ||
const avatars = Array.from(dz.querySelectorAll('.avatar-frame')).map(av => { | const avatars = Array.from(dz.querySelectorAll('.avatar-frame')).map(av => { | ||
const img = av.querySelector('img'); | const img = av.querySelector('img'); | ||
| 第372行: | 第368行: | ||
}; | }; | ||
}); | }); | ||
return { | return { | ||
y: Math.round(rowRect.top - tableRect.top), | |||
height: Math.round(rowRect.height), | |||
thWidth: Math.round(thRect.width), | |||
label, color, textColor, dzBg, avatars | |||
}; | |||
}); | }); | ||
const width = Math.ceil(tableRect.width); | const width = Math.ceil(tableRect.width); | ||
const totalHeight = Math.ceil((rows.at(-1)?.getBoundingClientRect().bottom || tableRect.bottom) - tableRect.top); | |||
const canvas = document.createElement('canvas'); | const canvas = document.createElement('canvas'); | ||
canvas.width = width; | canvas.width = width; | ||
canvas.height = totalHeight; | canvas.height = totalHeight; | ||
const ctx = canvas.getContext('2d'); | const ctx = canvas.getContext('2d'); | ||
// | // Base white background so row间隔保留为白色 | ||
ctx.fillStyle = '#ffffff'; | |||
ctx.fillRect(0, 0, canvas.width, canvas.height); | ctx.fillRect(0, 0, canvas.width, canvas.height); | ||
// Load | // Load images | ||
const loadTasks = []; | const loadTasks = []; | ||
rowInfo.forEach(r => { | rowInfo.forEach(r => { | ||
| 第391行: | 第391行: | ||
loadTasks.push(new Promise((resolve) => { | loadTasks.push(new Promise((resolve) => { | ||
const img = new Image(); | const img = new Image(); | ||
img.crossOrigin = 'anonymous'; | img.crossOrigin = 'anonymous'; | ||
img.onload = () => resolve({ a, img }); | img.onload = () => resolve({ a, img }); | ||
| 第402行: | 第401行: | ||
Promise.all(loadTasks).then(results => { | Promise.all(loadTasks).then(results => { | ||
const imgMap = new Map(); | const imgMap = new Map(); | ||
results.forEach(({ a, img }) => { | results.forEach(({ a, img }) => { if (img) imgMap.set(a.imgSrc, img); }); | ||
// Draw each row at its actual Y to preserve gaps | |||
rowInfo.forEach(info => { | |||
// Draw | // Header background | ||
// | |||
ctx.fillStyle = info.color; | ctx.fillStyle = info.color; | ||
ctx.fillRect(0, | ctx.fillRect(0, info.y, info.thWidth, info.height); | ||
// | // Dropzone background | ||
ctx.fillStyle = info.dzBg; | ctx.fillStyle = info.dzBg; | ||
ctx.fillRect(thWidth, | ctx.fillRect(info.thWidth, info.y, width - info.thWidth, info.height); | ||
// Label text | // Label text | ||
ctx.fillStyle = info.textColor; | ctx.fillStyle = info.textColor; | ||
ctx.font = 'bold 20px sans-serif'; | ctx.font = 'bold 20px sans-serif'; | ||
| 第421行: | 第416行: | ||
ctx.textBaseline = 'middle'; | ctx.textBaseline = 'middle'; | ||
const labelText = (info.label || '').trim(); | const labelText = (info.label || '').trim(); | ||
ctx.fillText(labelText, Math.floor(thWidth / 2), Math.floor( | ctx.fillText(labelText, Math.floor(info.thWidth / 2), Math.floor(info.y + info.height / 2)); | ||
// | // Avatars | ||
info.avatars.forEach(a => { | info.avatars.forEach(a => { | ||
const img = imgMap.get(a.imgSrc); | const img = imgMap.get(a.imgSrc); | ||
if (img) { | if (img) { | ||
const x = Math.max(thWidth + 6, Math.round(a.x)); | const x = Math.max(info.thWidth + 6, Math.round(a.x)); | ||
const | const rowTop = info.y; | ||
const rowBottom = info.y + info.height - a.h - 6; | |||
const rowBottom = | const yDraw = Math.min(Math.max(Math.round(a.y), rowTop + 6), rowBottom); | ||
const yDraw = Math.min(Math.max(y, rowTop + 6), rowBottom); | // Frame | ||
// Frame | |||
ctx.fillStyle = '#f5f5f5'; | ctx.fillStyle = '#f5f5f5'; | ||
ctx.fillRect(x - 3, yDraw - 3, a.w + 6, a.h + 6); | ctx.fillRect(x - 3, yDraw - 3, a.w + 6, a.h + 6); | ||
ctx.strokeStyle = '#cccccc'; | ctx.strokeStyle = '#cccccc'; | ||
ctx.lineWidth = 3; | ctx.lineWidth = 3; | ||
| 第453行: | 第446行: | ||
} | } | ||
}); | }); | ||
}); | }); | ||
// Download | // Download | ||