TierListMaker:修订间差异
来自卡厄思梦境WIKI
无编辑摘要 |
无编辑摘要 |
||
| 第155行: | 第155行: | ||
// Utility: ideal text color for background | // Utility: ideal text color for background | ||
function idealTextColor(hex) { | function idealTextColor(hex) { | ||
if (!hex) return '#fff'; | |||
const c = hex.replace('#',''); | const c = hex.replace('#',''); | ||
let r, g, b; | let r, g, b; | ||
| 第166行: | 第167行: | ||
b = parseInt(c.substring(4,6), 16); | b = parseInt(c.substring(4,6), 16); | ||
} | } | ||
const luminance = (0.2126*r + 0.7152*g + 0.0722*b) / 255; | const luminance = (0.2126*r + 0.7152*g + 0.0722*b) / 255; | ||
return luminance > 0.55 ? '#000' : '#fff'; | return luminance > 0.55 ? '#000' : '#fff'; | ||
| 第180行: | 第180行: | ||
const color = th.getAttribute('data-color') || '#888888'; | const color = th.getAttribute('data-color') || '#888888'; | ||
applyThColor(th, color); | applyThColor(th, color); | ||
// Open palette when clicking header area (but not the editable label) | |||
th.addEventListener('click', (e) => { | th.addEventListener('click', (e) => { | ||
if (e.target.closest('.tier-label')) { | |||
// allow editing text without opening palette | |||
return; | |||
} | |||
openColorPalette(th, e); | openColorPalette(th, e); | ||
}); | }); | ||
| 第267行: | 第272行: | ||
zone.classList.remove('over'); | zone.classList.remove('over'); | ||
if (!draggedEl) return; | if (!draggedEl) return; | ||
zone.appendChild(draggedEl); | zone.appendChild(draggedEl); | ||
draggedEl.style.pointerEvents = 'auto'; | draggedEl.style.pointerEvents = 'auto'; | ||
}); | }); | ||
} | } | ||
function initDragAndDrop() { | function initDragAndDrop() { | ||
document.querySelectorAll('.avatar-frame').forEach(setupDraggable); | document.querySelectorAll('.avatar-frame').forEach(setupDraggable); | ||
document.querySelectorAll('.tier-dropzone').forEach(setupDropzone); | document.querySelectorAll('.tier-dropzone').forEach(setupDropzone); | ||
const pool = document.getElementById('char-pool'); | const pool = document.getElementById('char-pool'); | ||
| 第322行: | 第323行: | ||
table.tBodies[0].appendChild(tr); | table.tBodies[0].appendChild(tr); | ||
// Enable interactions | // Enable interactions | ||
th.addEventListener('click', (e) => openColorPalette(th, e)); | th.addEventListener('click', (e) => { | ||
if (e.target.closest('.tier-label')) return; | |||
openColorPalette(th, e); | |||
}); | |||
setupDropzone(dropzone); | setupDropzone(dropzone); | ||
// Keep data-tier synced with label edits | // Keep data-tier synced with label edits | ||
| 第329行: | 第333行: | ||
}); | }); | ||
} | } | ||
// Export to PNG by drawing canvas ( | // Reset: move all avatars back to pool | ||
function resetTierList() { | |||
const pool = document.getElementById('char-pool'); | |||
if (!pool) return; | |||
document.querySelectorAll('#tierlist-table .tier-dropzone .avatar-frame').forEach(av => { | |||
pool.appendChild(av); | |||
}); | |||
} | |||
// Export to PNG by drawing canvas (preserving 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 | const thEl = table.querySelector('th.tier-th'); | ||
const thWidth = thEl ? Math.ceil(thEl.getBoundingClientRect().width) : 120; | |||
// Collect rows | // Collect rows | ||
const rows = Array.from(table.querySelectorAll('tr')); | const rows = Array.from(table.querySelectorAll('tr')); | ||
| 第345行: | 第358行: | ||
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'; | |||
// collect avatars in this dropzone | // collect avatars in this dropzone | ||
const avatars = Array.from(dz.querySelectorAll('.avatar-frame')).map(av => { | const avatars = Array.from(dz.querySelectorAll('.avatar-frame')).map(av => { | ||
| 第358行: | 第372行: | ||
}; | }; | ||
}); | }); | ||
return { thRect, dzRect, height, label, color, textColor, avatars }; | return { thRect, dzRect, height, label, color, textColor, dzBg, avatars }; | ||
}); | }); | ||
const totalHeight = rowInfo.reduce((acc, r) => acc + r.height, 0); | const totalHeight = Math.ceil(rowInfo.reduce((acc, r) => acc + r.height, 0)); | ||
const width = tableRect.width; | const width = Math.ceil(tableRect.width); | ||
const canvas = document.createElement('canvas'); | const canvas = document.createElement('canvas'); | ||
canvas.width = | canvas.width = width; | ||
canvas.height = | canvas.height = totalHeight; | ||
const ctx = canvas.getContext('2d'); | const ctx = canvas.getContext('2d'); | ||
// | // Full page background (match body) | ||
const bodyBg = getComputedStyle(document.body).backgroundColor || '#ffffff'; | |||
ctx.fillStyle = bodyBg; | |||
ctx.fillRect(0, 0, canvas.width, canvas.height); | ctx.fillRect(0, 0, canvas.width, canvas.height); | ||
// Load all images | // Load all images | ||
| 第376行: | 第391行: | ||
loadTasks.push(new Promise((resolve) => { | loadTasks.push(new Promise((resolve) => { | ||
const img = new Image(); | const img = new Image(); | ||
// try anonymous to avoid taint if same-origin allows | |||
img.crossOrigin = 'anonymous'; | |||
img.onload = () => resolve({ a, img }); | img.onload = () => resolve({ a, img }); | ||
img.onerror = () => resolve({ a, img: null }); | img.onerror = () => resolve({ a, img: null }); | ||
| 第392行: | 第409行: | ||
rows.forEach((row, idx) => { | rows.forEach((row, idx) => { | ||
const info = rowInfo[idx]; | const info = rowInfo[idx]; | ||
// Left header | // Left header background | ||
ctx.fillStyle = info.color; | ctx.fillStyle = info.color; | ||
ctx.fillRect(0, yCursor, thWidth, info.height); | ctx.fillRect(0, yCursor, thWidth, info.height); | ||
// Right dropzone background | |||
ctx.fillStyle = info.dzBg; | |||
ctx.fillRect(thWidth, yCursor, width - thWidth, info.height); | |||
// Label text centered | // Label text centered | ||
ctx.fillStyle = info.textColor; | ctx.fillStyle = info.textColor; | ||
| 第400行: | 第420行: | ||
ctx.textAlign = 'center'; | ctx.textAlign = 'center'; | ||
ctx.textBaseline = 'middle'; | ctx.textBaseline = 'middle'; | ||
ctx.fillText( | const labelText = (info.label || '').trim(); | ||
// Draw avatars | ctx.fillText(labelText, Math.floor(thWidth / 2), Math.floor(yCursor + info.height / 2)); | ||
// Draw avatars | |||
info.avatars.forEach(a => { | info.avatars.forEach(a => { | ||
const img = imgMap.get(a.imgSrc); | const img = imgMap.get(a.imgSrc); | ||
| 第407行: | 第428行: | ||
const x = Math.max(thWidth + 6, Math.round(a.x)); | const x = Math.max(thWidth + 6, Math.round(a.x)); | ||
const y = Math.round(a.y); | const y = Math.round(a.y); | ||
const rowTop = yCursor; | const rowTop = yCursor; | ||
const rowBottom = yCursor + info.height - a.h - 6; | const rowBottom = yCursor + info.height - a.h - 6; | ||
| 第445行: | 第465行: | ||
document.body.removeChild(a); | document.body.removeChild(a); | ||
} catch (e) { | } catch (e) { | ||
const url = canvas.toDataURL('image/png'); | const url = canvas.toDataURL('image/png'); | ||
window.open(url, '_blank'); | window.open(url, '_blank'); | ||
| 第468行: | 第487行: | ||
const addRowBtn = document.getElementById('add-row'); | const addRowBtn = document.getElementById('add-row'); | ||
const saveBtn = document.getElementById('save-png'); | const saveBtn = document.getElementById('save-png'); | ||
const resetBtn = document.getElementById('reset-all'); | |||
addRowBtn && addRowBtn.addEventListener('click', addRow); | addRowBtn && addRowBtn.addEventListener('click', addRow); | ||
saveBtn && saveBtn.addEventListener('click', exportPNG); | saveBtn && saveBtn.addEventListener('click', exportPNG); | ||
resetBtn && resetBtn.addEventListener('click', resetTierList); | |||
} | } | ||
if (document.readyState === 'complete' || document.readyState === 'interactive') { | if (document.readyState === 'complete' || document.readyState === 'interactive') { | ||