TierListMaker:修订间差异
来自卡厄思梦境WIKI
无编辑摘要 |
无编辑摘要 |
||
| 第1行: | 第1行: | ||
<includeonly> | <includeonly> | ||
<style> | <style> | ||
.tierlist-controls { | .tierlist-controls { | ||
display: flex; | display: flex; | ||
gap: 8px; | gap: 8px; | ||
margin: 8px 0 12px 0; | |||
margin | |||
} | } | ||
. | .btn { | ||
display: inline-block; | display: inline-block; | ||
padding: 6px 12px; | padding: 6px 12px; | ||
background: # | background: #f0f0f0; | ||
color: # | border: 1px solid #ccc; | ||
color: #333; | |||
border-radius: 4px; | border-radius: 4px; | ||
cursor: pointer; | cursor: pointer; | ||
user-select: none; | user-select: none; | ||
} | } | ||
. | .btn:hover { background: #e6e6e6; } | ||
. | .btn.primary { background: #4a8cf6; color: #fff; border-color: #3b78de; } | ||
.btn.primary:hover { background: #3b78de; } | |||
.tierlist-table { | .tierlist-table { | ||
width: 100%; | width: 100%; | ||
table-layout: fixed; | table-layout: fixed; | ||
} | } | ||
. | .tier-row .tier-head { | ||
position: relative; | |||
width: 120px; | width: 120px; | ||
color: #fff; | |||
text-align: center; | text-align: center; | ||
font-size: 18px; | font-size: 18px; | ||
font-weight: bold; | font-weight: bold; | ||
padding: 8px; | |||
white-space: nowrap; | |||
border: 1px solid #ccc; | |||
background: #555; | |||
} | |||
.tier-row .tier-cell { | |||
border: 1px solid #ccc; | |||
padding: 6px; | |||
} | |||
.tier-tools { | |||
position: absolute; | |||
right: 6px; | |||
top: 6px; | |||
display: flex; | |||
gap: 6px; | |||
} | |||
.color-toggle { | |||
width: 18px; | |||
height: 18px; | |||
border-radius: 3px; | |||
border: 1px solid rgba(0,0,0,0.2); | |||
cursor: pointer; | |||
background: rgba(255,255,255,0.6); | |||
} | } | ||
. | .delete-row { | ||
padding: | font-size: 12px; | ||
padding: 2px 6px; | |||
border-radius: 3px; | |||
background: rgba(0,0,0,0.1); | |||
cursor: pointer; | |||
} | } | ||
.delete-row:hover { background: rgba(0,0,0,0.2); } | |||
.tier-dropzone { | .tier-dropzone { | ||
min-height: | min-height: 112px; | ||
display: flex; | display: flex; | ||
flex-wrap: wrap; | flex-wrap: wrap; | ||
gap: 6px; | |||
align-items: flex-start; | align-items: flex-start; | ||
} | } | ||
.tier-dropzone. | .tier-dropzone.pool { | ||
border | border: 2px dashed #ccc; | ||
padding: 8px; | |||
border-radius: 6px; | border-radius: 6px; | ||
background: #fafafa; | |||
background: # | |||
} | } | ||
.pool- | .pool-wrapper { margin-top: 12px; } | ||
.pool-header { | |||
font-weight: bold; | font-weight: bold; | ||
margin-bottom: 6px; | margin-bottom: 6px; | ||
} | } | ||
.avatar-frame { | .avatar-frame { | ||
| 第84行: | 第87行: | ||
border-radius: 5px; | border-radius: 5px; | ||
overflow: hidden; | overflow: hidden; | ||
box-shadow: 0 2px 5px rgba(0,0,0,0.1); | box-shadow: 0 2px 5px rgba(0,0,0,0.1); | ||
background: #f5f5f5; | background: #f5f5f5; | ||
transition: transform 0. | transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; | ||
cursor: | cursor: move; | ||
} | } | ||
.avatar-frame.dragging { opacity: 0. | .avatar-frame.dragging { | ||
opacity: 0.7; | |||
transform: scale(1. | transform: scale(1.03); | ||
border-color: #4a8cf6; | |||
z-index: 10; | z-index: 10; | ||
} | } | ||
| 第100行: | 第103行: | ||
height: 100px; | height: 100px; | ||
object-fit: cover; | object-fit: cover; | ||
pointer-events: none; | |||
-webkit-user-drag: none; | |||
} | } | ||
.avatar-name { | .avatar-name { | ||
| 第118行: | 第123行: | ||
.color-palette { | .color-palette { | ||
position: absolute; | position: absolute; | ||
display: none; | |||
gap: 6px; | |||
flex-wrap: wrap; | |||
width: 210px; | |||
padding: 8px; | |||
background: #fff; | background: #fff; | ||
border: 1px solid #ccc; | border: 1px solid #ccc; | ||
box-shadow: 0 2px 8px rgba(0,0,0,0.15); | |||
border-radius: 6px; | border-radius: 6px; | ||
z-index: 9999; | |||
} | } | ||
.color-swatch { | .color-swatch { | ||
width: | width: 24px; height: 24px; | ||
border-radius: 4px; | border-radius: 4px; | ||
border: 1px solid | border: 1px solid rgba(0,0,0,0.2); | ||
cursor: pointer; | cursor: pointer; | ||
} | } | ||
.color- | .color-swatch:hover { outline: 2px solid rgba(0,0,0,0.2); } | ||
.tier-label { | |||
display: inline-block; | |||
padding: 2px 6px; | |||
border-radius: 3px; | |||
background: rgba(255,255,255,0.15); | |||
} | |||
.tier-label[contenteditable="true"] { | |||
outline: none; | |||
cursor: text; | |||
} | } | ||
. | |||
.exporting .tier-dropzone.pool { | |||
background: | border: 0; | ||
background: transparent; | |||
} | } | ||
</style> | </style> | ||
<script> | <script> | ||
(function(){ | (function() { | ||
function ready(fn) { | |||
function | if (document.readyState !== 'loading') fn(); | ||
if (! | else document.addEventListener('DOMContentLoaded', fn); | ||
} | } | ||
function | function contrastColor(hex) { | ||
hex = hex.replace('#',''); | |||
if (hex.length === 3) hex = hex.split('').map(c => c + c).join(''); | |||
var r = parseInt(hex.substr(0,2), 16); | |||
var g = parseInt(hex.substr(2,2), 16); | |||
var b = parseInt(hex.substr(4,2), 16); | |||
var yiq = ((r*299)+(g*587)+(b*114))/1000; | |||
return yiq >= 128 ? '#000' : '#fff'; | |||
} | } | ||
function | function getDragAfterElement(container, y) { | ||
const | const elements = [...container.querySelectorAll('.avatar-frame:not(.dragging)')]; | ||
return elements.reduce((closest, child) => { | |||
const | const box = child.getBoundingClientRect(); | ||
const offset = y - box.top - box.height/2; | |||
if (offset < 0 && offset > closest.offset) { | |||
return { offset: offset, element: child }; | |||
} else { | |||
return closest; | |||
} | |||
}, { offset: Number.NEGATIVE_INFINITY, element: null }).element; | |||
} | |||
}); | |||
} | } | ||
function makeDraggable(avatar) { | |||
avatar.setAttribute('draggable', 'true'); | |||
avatar.classList.add('draggable-avatar'); | |||
avatar.addEventListener('dragstart', function(e) { | |||
avatar.classList.add('dragging'); | |||
e.dataTransfer.setData('text/plain', avatar.dataset.id || avatar.querySelector('.avatar-name')?.textContent || ''); | |||
// allow move effect | |||
function | e.dataTransfer.effectAllowed = 'move'; | ||
}); | }); | ||
avatar.addEventListener('dragend', function() { | |||
avatar.classList.remove('dragging'); | |||
}); | }); | ||
} | } | ||
function | function initAvatars(root) { | ||
const avatars = root.querySelectorAll('.avatar-frame'); | |||
const | avatars.forEach(function(av) { | ||
// Ensure data attributes exist | |||
if (!av.dataset.id) { | |||
const nameEl = av.querySelector('.avatar-name'); | |||
av.dataset.name = nameEl ? nameEl.textContent.trim() : ''; | |||
if (! | |||
const | |||
} | } | ||
makeDraggable(av); | |||
}); | }); | ||
} | } | ||
function | function initDropzone(zone) { | ||
zone.addEventListener('dragover', (e) | zone.addEventListener('dragover', function(e) { | ||
e.preventDefault(); | |||
zone. | const afterElement = getDragAfterElement(zone, e.clientY); | ||
const dragging = document.querySelector('.avatar-frame.dragging'); | |||
if (!dragging) return; | |||
if (afterElement == null) { | |||
zone.appendChild(dragging); | |||
} else { | |||
zone.insertBefore(dragging, afterElement); | |||
} | |||
}); | }); | ||
zone.addEventListener('drop', function(e) { | |||
zone.addEventListener('drop', (e) | |||
e.preventDefault(); | e.preventDefault(); | ||
const dragging = document.querySelector('.avatar-frame.dragging'); | |||
if (! | if (dragging && dragging.parentNode !== zone) { | ||
zone.appendChild(dragging); | |||
} | |||
}); | }); | ||
} | } | ||
function | function buildEditableHead(th) { | ||
document. | const currentText = (th.childNodes[0] && th.childNodes[0].nodeType === 3) | ||
document. | ? th.childNodes[0].nodeValue.trim() | ||
const pool = document.getElementById(' | : th.textContent.trim(); | ||
const tools = th.querySelector('.tier-tools'); | |||
th.innerHTML = ''; | |||
const label = document.createElement('div'); | |||
label.className = 'tier-label'; | |||
label.setAttribute('contenteditable', 'true'); | |||
label.textContent = currentText || '未命名'; | |||
const toolsWrap = tools || (function() { | |||
const t = document.createElement('div'); | |||
t.className = 'tier-tools'; | |||
const color = document.createElement('div'); | |||
color.className = 'color-toggle'; | |||
const del = document.createElement('div'); | |||
del.className = 'delete-row'; | |||
del.textContent = '删除'; | |||
t.appendChild(color); t.appendChild(del); | |||
return t; | |||
})(); | |||
th.appendChild(label); | |||
th.appendChild(toolsWrap); | |||
// initial colors | |||
const initialBg = th.getAttribute('data-initial-bg') || 'gray'; | |||
const fg = contrastColor(initialBg); | |||
th.style.background = initialBg; | |||
th.style.color = fg; | |||
// color palette | |||
attachColorPalette(toolsWrap.querySelector('.color-toggle'), th); | |||
// delete handler | |||
const delBtn = toolsWrap.querySelector('.delete-row'); | |||
delBtn.addEventListener('click', function() { | |||
const tr = th.closest('tr'); | |||
const dropzone = tr.querySelector('.tier-dropzone'); | |||
const pool = document.getElementById('character-pool'); | |||
// move items back to pool | |||
Array.from(dropzone.querySelectorAll('.avatar-frame')).forEach(function(av) { | |||
pool.appendChild(av); | |||
}); | |||
tr.parentNode.removeChild(tr); | |||
}); | |||
} | } | ||
function attachColorPalette(toggle, th) { | |||
const palette = document.createElement('div'); | |||
palette.className = 'color-palette'; | |||
const colors = [ | |||
'#e53935','#d81b60','#8e24aa','#5e35b1','#3949ab','#1e88e5','#039be5','#00acc1', | |||
'#00897b','#43a047','#7cb342','#c0ca33','#fdd835','#fb8c00','#f4511e','#6d4c41', | |||
'#546e7a','#9e9e9e','#000000','#ffffff' | |||
]; | |||
colors.forEach(function(c) { | |||
const sw = document.createElement('div'); | |||
sw.className = 'color-swatch'; | |||
sw.style.background = c; | |||
sw.addEventListener('click', function(e) { | |||
th.style.background = c; | |||
th.style.color = contrastColor(c); | |||
palette.style.display = 'none'; | |||
}); | }); | ||
palette.appendChild(sw); | |||
}); | }); | ||
document.body.appendChild(palette); | |||
function placePalette() { | |||
const rect = toggle.getBoundingClientRect(); | |||
palette.style.left = (window.scrollX + rect.left) + 'px'; | |||
palette.style.top = (window.scrollY + rect.bottom + 6) + 'px'; | |||
} | } | ||
toggle.addEventListener('click', function() { | |||
if (palette.style.display === 'block') { | |||
palette.style.display = 'none'; | |||
} else { | |||
placePalette(); | |||
palette.style.display = 'block'; | |||
} | |||
}); | |||
document.addEventListener('click', function(e) { | |||
if (e.target === toggle || palette.contains(e.target)) return; | |||
palette.style.display = 'none'; | |||
}); | |||
window.addEventListener('scroll', function() { | |||
if (palette.style.display === 'block') placePalette(); | |||
}); | |||
window.addEventListener('resize', function() { | |||
if (palette.style.display === 'block') placePalette(); | |||
}); | |||
} | } | ||
function addNewRow() { | |||
function | const tbody = document.querySelector('#tierlist-table tbody'); | ||
const | |||
const tr = document.createElement('tr'); | const tr = document.createElement('tr'); | ||
tr.className = 'tier-row'; | |||
const th = document.createElement('th'); | const th = document.createElement('th'); | ||
th.className = 'tier- | th.className = 'tier-head'; | ||
th.setAttribute('data-initial-bg', '#8888ff'); | |||
th.setAttribute('data-initial-fg', 'white'); | |||
th.textContent = '新行'; | |||
const td = document.createElement('td'); | const td = document.createElement('td'); | ||
const | td.className = 'tier-cell'; | ||
const dz = document.createElement('div'); | |||
dz.className = 'tier-dropzone'; | |||
td.appendChild( | dz.setAttribute('data-tier', 'CUSTOM'); | ||
td.appendChild(dz); | |||
tr.appendChild(th); | tr.appendChild(th); | ||
tr.appendChild(td); | tr.appendChild(td); | ||
tbody.appendChild(tr); | |||
initDropzone(dz); | |||
buildEditableHead(th); | |||
} | } | ||
function ensureHtml2Canvas(cb) { | |||
function | if (window.html2canvas) { cb(); return; } | ||
var s = document.createElement('script'); | |||
s.src = 'https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js'; | |||
s.onload = cb; | |||
s.onerror = function() { | |||
}); | alert('加载截图库失败,请检查网络或跨域策略。'); | ||
}; | |||
document.body.appendChild(s); | |||
} | } | ||
function savePNG() { | |||
function | |||
const table = document.getElementById('tierlist-table'); | const table = document.getElementById('tierlist-table'); | ||
document.getElementById('tierlist-maker').classList.add('exporting'); | |||
ensureHtml2Canvas(function() { | |||
// Use higher scale for sharper image | |||
html2canvas(table, {backgroundColor: null, scale: 2}).then(function(canvas) { | |||
const link = document.createElement('a'); | |||
link.href = canvas.toDataURL('image/png'); | |||
link.download = 'tierlist.png'; | |||
document.body.appendChild(link); | |||
link.click(); | |||
document.body.removeChild(link); | |||
document.getElementById('tierlist-maker').classList.remove('exporting'); | |||
}).catch(function() { | |||
document.getElementById('tierlist-maker').classList.remove('exporting'); | |||
alert('保存PNG失败。'); | |||
}); | }); | ||
}); | }); | ||
} | |||
ready(function() { | |||
// Initialize table heads editable and with tools | |||
document.querySelectorAll('#tierlist-table .tier-head').forEach(buildEditableHead); | |||
// Initialize dropzones | |||
document.querySelectorAll('.tier-dropzone').forEach(initDropzone); | |||
// | // Initialize avatars in pool | ||
initAvatars(document.getElementById('character-pool')); | |||
// Controls | |||
// | document.getElementById('add-row').addEventListener('click', addNewRow); | ||
const | document.getElementById('save-png').addEventListener('click', savePNG); | ||
// If avatars are loaded later (SMW), observe mutations to init new avatars | |||
const pool = document.getElementById('character-pool'); | |||
if ( | const obs = new MutationObserver(function(muts) { | ||
muts.forEach(function(m) { | |||
if (m.addedNodes && m.addedNodes.length) { | |||
m.addedNodes.forEach(function(n) { | |||
if (n.nodeType === 1) { | |||
if (n.classList.contains('avatar-frame')) makeDraggable(n); | |||
// Also init any nested avatar frames | |||
} | n.querySelectorAll && n.querySelectorAll('.avatar-frame').forEach(makeDraggable); | ||
} | |||
}); | |||
} | } | ||
}); | }); | ||
}); | }); | ||
obs.observe(pool, { childList: true, subtree: true }); | |||
}); | |||
} | |||
})(); | })(); | ||
</script> | </script> | ||
</includeonly> | </includeonly> | ||