微件

TierListMaker:修订间差异

来自卡厄思梦境WIKI

律Rhyme留言 | 贡献
无编辑摘要
律Rhyme留言 | 贡献
无编辑摘要
第1行: 第1行:
<includeonly>
<includeonly>
<style>
<style>
.tierlist-wrapper {
  max-width: 1200px;
  margin: 0 auto;
  padding: 8px;
}
.tierlist-controls {
.tierlist-controls {
   display: flex;
   display: flex;
   gap: 8px;
   gap: 8px;
  align-items: center;
   margin: 8px 0 12px 0;
  flex-wrap: wrap;
   margin-bottom: 8px;
}
}
.control-btn {
.btn {
   display: inline-block;
   display: inline-block;
   padding: 6px 12px;
   padding: 6px 12px;
   background: #3498db;
   background: #f0f0f0;
   color: #fff;
  border: 1px solid #ccc;
   color: #333;
   border-radius: 4px;
   border-radius: 4px;
   cursor: pointer;
   cursor: pointer;
   user-select: none;
   user-select: none;
}
}
.control-btn:hover { background: #2980b9; }
.btn:hover { background: #e6e6e6; }
.control-tips { color: #666; font-size: 12px; }
.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;
}
}
.tierlist-table th.tier-th {
.tier-row .tier-head {
  position: relative;
   width: 120px;
   width: 120px;
  color: #fff;
   text-align: center;
   text-align: center;
  vertical-align: middle;
  color: #fff;
  position: relative;
  padding: 0;
}
.tier-label {
  padding: 6px 8px;
   font-size: 18px;
   font-size: 18px;
   font-weight: bold;
   font-weight: bold;
   line-height: 1.2;
   padding: 8px;
   outline: none;
  white-space: nowrap;
   cursor: text;
  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);
}
}
.tierlist-table td {
.delete-row {
   padding: 4px;
  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: 116px;
   min-height: 112px;
  border: 2px dashed #ddd;
  border-radius: 6px;
  background: #fafafa;
   display: flex;
   display: flex;
   flex-wrap: wrap;
   flex-wrap: wrap;
  gap: 6px;
   align-items: flex-start;
   align-items: flex-start;
  gap: 6px;
  padding: 6px;
}
}
.tier-dropzone.over {
.tier-dropzone.pool {
   border-color: #3498db;
   border: 2px dashed #ccc;
  background: #f0f8ff;
   padding: 8px;
}
.char-pool {
  margin-top: 10px;
   border: 2px solid #ddd;
   border-radius: 6px;
   border-radius: 6px;
  padding: 6px;
   background: #fafafa;
   background: #fff;
}
}
.pool-title {
.pool-wrapper { margin-top: 12px; }
.pool-header {
   font-weight: bold;
   font-weight: bold;
   margin-bottom: 6px;
   margin-bottom: 6px;
}
.pool-dropzone.over {
  border-color: #3498db;
  background: #f0f8ff;
}
}
.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.3s ease, box-shadow 0.3s ease;
   transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
   cursor: grab;
   cursor: move;
}
}
.avatar-frame.dragging { opacity: 0.6; }
.avatar-frame.dragging {
.avatar-frame:hover {
  opacity: 0.7;
   transform: scale(1.05);
   transform: scale(1.03);
   box-shadow: 0 4px 8px rgba(0,0,0,0.2);
   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;
   z-index: 99999;
   display: none;
  gap: 6px;
  flex-wrap: wrap;
  width: 210px;
  padding: 8px;
   background: #fff;
   background: #fff;
   border: 1px solid #ccc;
   border: 1px solid #ccc;
   padding: 6px;
   box-shadow: 0 2px 8px rgba(0,0,0,0.15);
   border-radius: 6px;
   border-radius: 6px;
   box-shadow: 0 4px 10px rgba(0,0,0,0.15);
   z-index: 9999;
  display: none;
}
.color-palette .colors {
  display: grid;
  grid-template-columns: repeat(6, 20px);
  gap: 6px;
}
}
.color-swatch {
.color-swatch {
   width: 20px;
   width: 24px; height: 24px;
  height: 20px;
   border-radius: 4px;
   border-radius: 4px;
   border: 1px solid #ccc;
   border: 1px solid rgba(0,0,0,0.2);
   cursor: pointer;
   cursor: pointer;
}
}
.color-palette .actions {
.color-swatch:hover { outline: 2px solid rgba(0,0,0,0.2); }
   margin-top: 6px;
.tier-label {
   display: flex;
   display: inline-block;
   gap: 8px;
  padding: 2px 6px;
   border-radius: 3px;
  background: rgba(255,255,255,0.15);
}
.tier-label[contenteditable="true"] {
  outline: none;
   cursor: text;
}
}
.palette-close {
 
   padding: 4px 8px;
.exporting .tier-dropzone.pool {
   background: #eee;
   border: 0;
  border-radius: 4px;
   background: transparent;
  cursor: pointer;
}
}
</style>
</style>


<script>
<script>
(function(){
(function() {
  // Utility: ideal text color for background
   function ready(fn) {
   function idealTextColor(hex) {
     if (document.readyState !== 'loading') fn();
     if (!hex) return '#fff';
     else document.addEventListener('DOMContentLoaded', fn);
    const c = hex.replace('#','');
    let r, g, b;
    if (c.length === 3) {
      r = parseInt(c[0] + c[0], 16);
      g = parseInt(c[1] + c[1], 16);
      b = parseInt(c[2] + c[2], 16);
     } else {
      r = parseInt(c.substring(0,2), 16);
      g = parseInt(c.substring(2,4), 16);
      b = parseInt(c.substring(4,6), 16);
    }
    const luminance = (0.2126*r + 0.7152*g + 0.0722*b) / 255;
    return luminance > 0.55 ? '#000' : '#fff';
   }
   }
   function applyThColor(th, color) {
   function contrastColor(hex) {
     th.setAttribute('data-color', color);
     hex = hex.replace('#','');
     th.style.backgroundColor = color;
     if (hex.length === 3) hex = hex.split('').map(c => c + c).join('');
     th.style.color = idealTextColor(color);
     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 setupThColors() {
   function getDragAfterElement(container, y) {
     const ths = document.querySelectorAll('#tierlist-table th.tier-th');
     const elements = [...container.querySelectorAll('.avatar-frame:not(.dragging)')];
     ths.forEach(th => {
     return elements.reduce((closest, child) => {
       const color = th.getAttribute('data-color') || '#888888';
       const box = child.getBoundingClientRect();
       applyThColor(th, color);
       const offset = y - box.top - box.height/2;
      // Use contains() to allow editing label text safely (fix S/A/B/C not editable)
      if (offset < 0 && offset > closest.offset) {
      th.addEventListener('click', (e) => {
        return { offset: offset, element: child };
        const labelEl = th.querySelector('.tier-label');
      } else {
        if (labelEl && labelEl.contains(e.target)) {
         return closest;
          // Clicking inside the editable label: allow text editing
       }
          return;
     }, { offset: Number.NEGATIVE_INFINITY, element: null }).element;
        }
         openColorPalette(th, e);
       });
     });
   }
   }
  // Color palette
   function makeDraggable(avatar) {
  const paletteColors = [
     avatar.setAttribute('draggable', 'true');
    '#e74c3c','#f1963b','#ffef03','#c2d402','#2ecc71','#3498db',
     avatar.classList.add('draggable-avatar');
    '#9b59b6','#34495e','#7f8c8d','#ffffff','#000000','#f39c12',
     avatar.addEventListener('dragstart', function(e) {
    '#1abc9c','#8e44ad','#d35400','#27ae60','#2980b9','#bdc3c7'
       avatar.classList.add('dragging');
  ];
       e.dataTransfer.setData('text/plain', avatar.dataset.id || avatar.querySelector('.avatar-name')?.textContent || '');
  let paletteEl = null;
      // allow move effect
   function buildPalette() {
      e.dataTransfer.effectAllowed = 'move';
     paletteEl = document.createElement('div');
     paletteEl.className = 'color-palette';
    const colorsWrap = document.createElement('div');
     colorsWrap.className = 'colors';
    paletteColors.forEach(col => {
       const s = document.createElement('div');
       s.className = 'color-swatch';
      s.style.backgroundColor = col;
      s.addEventListener('click', () => {
        if (paletteEl._targetTh) {
          applyThColor(paletteEl._targetTh, col);
        }
        paletteEl.style.display = 'none';
      });
      colorsWrap.appendChild(s);
     });
     });
     const actions = document.createElement('div');
     avatar.addEventListener('dragend', function() {
    actions.className = 'actions';
      avatar.classList.remove('dragging');
    const closeBtn = document.createElement('div');
    closeBtn.className = 'palette-close';
    closeBtn.textContent = '关闭';
    closeBtn.addEventListener('click', () => paletteEl.style.display = 'none');
    actions.appendChild(closeBtn);
    paletteEl.appendChild(colorsWrap);
    paletteEl.appendChild(actions);
    document.body.appendChild(paletteEl);
    document.addEventListener('click', (e) => {
      if (!paletteEl.contains(e.target)) paletteEl.style.display = 'none';
     });
     });
   }
   }
   function openColorPalette(th, evt) {
   function initAvatars(root) {
    if (!paletteEl) buildPalette();
     const avatars = root.querySelectorAll('.avatar-frame');
     const rect = th.getBoundingClientRect();
     avatars.forEach(function(av) {
    paletteEl.style.left = (window.scrollX + rect.left + 10) + 'px';
       // Ensure data attributes exist
    paletteEl.style.top = (window.scrollY + rect.bottom + 8) + 'px';
       if (!av.dataset.id) {
    paletteEl.style.display = 'block';
         const nameEl = av.querySelector('.avatar-name');
    paletteEl._targetTh = th;
         av.dataset.name = nameEl ? nameEl.textContent.trim() : '';
  }
  // Drag & Drop
  let draggedEl = null;
  function setupDraggable(el) {
    if (!el || el._draggableSetup) return;
    el._draggableSetup = true;
     el.setAttribute('draggable', 'true');
    el.addEventListener('dragstart', (e) => {
       draggedEl = el;
      el.classList.add('dragging');
      e.dataTransfer.effectAllowed = 'move';
       if (!el.dataset.id) {
         const img = el.querySelector('img');
         if (img && img.src) {
          const m = img.src.match(/face_character_([^./?]+)\.png/i);
          if (m) el.dataset.id = m[1];
        }
       }
       }
    });
       makeDraggable(av);
    el.addEventListener('dragend', () => {
       el.classList.remove('dragging');
      draggedEl = null;
     });
     });
   }
   }
   function setupDropzone(zone) {
   function initDropzone(zone) {
     zone.addEventListener('dragover', (e) => {
     zone.addEventListener('dragover', function(e) {
       if (draggedEl) e.preventDefault();
       e.preventDefault();
       zone.classList.add('over');
       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('dragleave', () => {
     zone.addEventListener('drop', function(e) {
      zone.classList.remove('over');
    });
     zone.addEventListener('drop', (e) => {
       e.preventDefault();
       e.preventDefault();
       zone.classList.remove('over');
       const dragging = document.querySelector('.avatar-frame.dragging');
       if (!draggedEl) return;
       if (dragging && dragging.parentNode !== zone) {
      zone.appendChild(draggedEl);
        zone.appendChild(dragging);
       draggedEl.style.pointerEvents = 'auto';
       }
     });
     });
   }
   }
   function initDragAndDrop() {
   function buildEditableHead(th) {
     document.querySelectorAll('.avatar-frame').forEach(setupDraggable);
     const currentText = (th.childNodes[0] && th.childNodes[0].nodeType === 3)
     document.querySelectorAll('.tier-dropzone').forEach(setupDropzone);
      ? th.childNodes[0].nodeValue.trim()
     const pool = document.getElementById('char-pool');
      : th.textContent.trim();
    setupDropzone(pool);
    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);
    });
   }
   }
   // Observe dynamically added avatars
   function attachColorPalette(toggle, th) {
  const poolObserver = new MutationObserver((mutations) => {
    const palette = document.createElement('div');
     mutations.forEach((m) => {
    palette.className = 'color-palette';
       m.addedNodes.forEach((node) => {
    const colors = [
        if (node.nodeType === 1) {
      '#e53935','#d81b60','#8e24aa','#5e35b1','#3949ab','#1e88e5','#039be5','#00acc1',
          if (node.classList && node.classList.contains('avatar-frame')) {
      '#00897b','#43a047','#7cb342','#c0ca33','#fdd835','#fb8c00','#f4511e','#6d4c41',
            setupDraggable(node);
      '#546e7a','#9e9e9e','#000000','#ffffff'
          } else if (node.querySelectorAll) {
    ];
            node.querySelectorAll('.avatar-frame').forEach(setupDraggable);
     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 startObservers() {
    function placePalette() {
    const pool = document.getElementById('char-pool');
      const rect = toggle.getBoundingClientRect();
    if (pool) {
      palette.style.left = (window.scrollX + rect.left) + 'px';
       poolObserver.observe(pool, { childList: true, subtree: true });
       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();
    });
   }
   }
  // Add row
   function addNewRow() {
   function addRow() {
     const tbody = document.querySelector('#tierlist-table tbody');
     const table = document.getElementById('tierlist-table');
     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';
     th.className = 'tier-head';
     applyThColor(th, '#7f8c8d');
     th.setAttribute('data-initial-bg', '#8888ff');
    const label = document.createElement('div');
     th.setAttribute('data-initial-fg', 'white');
    label.className = 'tier-label';
     th.textContent = '新行';
     label.setAttribute('contenteditable', 'true');
     label.textContent = 'NEW';
    th.appendChild(label);
     const td = document.createElement('td');
     const td = document.createElement('td');
     const dropzone = document.createElement('div');
    td.className = 'tier-cell';
     dropzone.className = 'tier-dropzone';
     const dz = document.createElement('div');
     dropzone.setAttribute('data-tier', label.textContent || 'NEW');
     dz.className = 'tier-dropzone';
     td.appendChild(dropzone);
     dz.setAttribute('data-tier', 'CUSTOM');
     td.appendChild(dz);
     tr.appendChild(th);
     tr.appendChild(th);
     tr.appendChild(td);
     tr.appendChild(td);
     table.tBodies[0].appendChild(tr);
     tbody.appendChild(tr);
     // Enable interactions
     initDropzone(dz);
    th.addEventListener('click', (e) => {
    buildEditableHead(th);
      const labelEl = th.querySelector('.tier-label');
      if (labelEl && labelEl.contains(e.target)) return;
      openColorPalette(th, e);
    });
    setupDropzone(dropzone);
    // Keep data-tier synced with label edits
    label.addEventListener('input', () => {
      dropzone.setAttribute('data-tier', label.textContent.trim());
    });
   }
   }
  // Reset: move all avatars back to pool
   function ensureHtml2Canvas(cb) {
   function resetTierList() {
     if (window.html2canvas) { cb(); return; }
     const pool = document.getElementById('char-pool');
     var s = document.createElement('script');
     if (!pool) return;
     s.src = 'https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js';
     document.querySelectorAll('#tierlist-table .tier-dropzone .avatar-frame').forEach(av => {
    s.onload = cb;
       pool.appendChild(av);
    s.onerror = function() {
     });
       alert('加载截图库失败,请检查网络或跨域策略。');
     };
    document.body.appendChild(s);
   }
   }
  // Export to PNG (preserve row gaps and backgrounds)
   function savePNG() {
   function exportPNG() {
     const table = document.getElementById('tierlist-table');
     const table = document.getElementById('tierlist-table');
     const tableRect = table.getBoundingClientRect();
     document.getElementById('tierlist-maker').classList.add('exporting');
    const rows = Array.from(table.querySelectorAll('tr'));
     ensureHtml2Canvas(function() {
     const rowInfo = rows.map(row => {
       // Use higher scale for sharper image
       const rowRect = row.getBoundingClientRect();
      html2canvas(table, {backgroundColor: null, scale: 2}).then(function(canvas) {
      const th = row.querySelector('th.tier-th');
        const link = document.createElement('a');
      const dz = row.querySelector('.tier-dropzone');
        link.href = canvas.toDataURL('image/png');
      const thRect = th.getBoundingClientRect();
        link.download = 'tierlist.png';
      const label = th.querySelector('.tier-label')?.textContent || '';
        document.body.appendChild(link);
      const color = th.getAttribute('data-color') || '#888';
        link.click();
      const textColor = idealTextColor(color);
        document.body.removeChild(link);
      const dzBg = getComputedStyle(dz).backgroundColor || '#ffffff';
        document.getElementById('tierlist-maker').classList.remove('exporting');
      const avatars = Array.from(dz.querySelectorAll('.avatar-frame')).map(av => {
      }).catch(function() {
        const img = av.querySelector('img');
         document.getElementById('tierlist-maker').classList.remove('exporting');
        const rect = av.getBoundingClientRect();
         alert('保存PNG失败。');
         return {
          imgSrc: img ? img.src : null,
          x: rect.left - tableRect.left,
          y: rect.top - tableRect.top,
          w: img ? img.width : 100,
          h: img ? img.height : 100,
          name: av.querySelector('.avatar-name')?.textContent || ''
         };
       });
       });
      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 totalHeight = Math.ceil((rows.at(-1)?.getBoundingClientRect().bottom || tableRect.bottom) - tableRect.top);
  ready(function() {
     const canvas = document.createElement('canvas');
    // Initialize table heads editable and with tools
    canvas.width = width;
     document.querySelectorAll('#tierlist-table .tier-head').forEach(buildEditableHead);
     canvas.height = totalHeight;
     // Initialize dropzones
     const ctx = canvas.getContext('2d');
    document.querySelectorAll('.tier-dropzone').forEach(initDropzone);
     // Base white background so row间隔保留为白色
     // Initialize avatars in pool
     ctx.fillStyle = '#ffffff';
     initAvatars(document.getElementById('character-pool'));
     ctx.fillRect(0, 0, canvas.width, canvas.height);
     // Controls
     // Load images
     document.getElementById('add-row').addEventListener('click', addNewRow);
     const loadTasks = [];
     document.getElementById('save-png').addEventListener('click', savePNG);
     rowInfo.forEach(r => {
     // If avatars are loaded later (SMW), observe mutations to init new avatars
       r.avatars.forEach(a => {
     const pool = document.getElementById('character-pool');
         if (a.imgSrc) {
     const obs = new MutationObserver(function(muts) {
           loadTasks.push(new Promise((resolve) => {
       muts.forEach(function(m) {
             const img = new Image();
         if (m.addedNodes && m.addedNodes.length) {
            img.crossOrigin = 'anonymous';
           m.addedNodes.forEach(function(n) {
            img.onload = () => resolve({ a, img });
             if (n.nodeType === 1) {
            img.onerror = () => resolve({ a, img: null });
              if (n.classList.contains('avatar-frame')) makeDraggable(n);
             img.src = a.imgSrc;
              // Also init any nested avatar frames
           }));
              n.querySelectorAll && n.querySelectorAll('.avatar-frame').forEach(makeDraggable);
             }
           });
         }
         }
       });
       });
     });
     });
     Promise.all(loadTasks).then(results => {
     obs.observe(pool, { childList: true, subtree: true });
      const imgMap = new Map();
   });
      results.forEach(({ a, img }) => { if (img) imgMap.set(a.imgSrc, img); });
      // Draw each row at its actual Y to preserve gaps
      rowInfo.forEach(info => {
        // Header background
        ctx.fillStyle = info.color;
        ctx.fillRect(0, info.y, info.thWidth, info.height);
        // Dropzone background
        ctx.fillStyle = info.dzBg;
        ctx.fillRect(info.thWidth, info.y, width - info.thWidth, info.height);
        // Label text
        ctx.fillStyle = info.textColor;
        ctx.font = 'bold 20px sans-serif';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        const labelText = (info.label || '').trim();
        ctx.fillText(labelText, Math.floor(info.thWidth / 2), Math.floor(info.y + info.height / 2));
        // Avatars
        info.avatars.forEach(a => {
          const img = imgMap.get(a.imgSrc);
          if (img) {
            const x = Math.max(info.thWidth + 6, Math.round(a.x));
            const rowTop = info.y;
            const rowBottom = info.y + info.height - a.h - 6;
            const yDraw = Math.min(Math.max(Math.round(a.y), rowTop + 6), rowBottom);
            // Frame
            ctx.fillStyle = '#f5f5f5';
            ctx.fillRect(x - 3, yDraw - 3, a.w + 6, a.h + 6);
            ctx.strokeStyle = '#cccccc';
            ctx.lineWidth = 3;
            ctx.strokeRect(x - 3, yDraw - 3, a.w + 6, a.h + 6);
            // Image
            ctx.drawImage(img, x, yDraw, a.w, a.h);
            // Name bar
            if (a.name) {
              ctx.fillStyle = 'rgba(0,0,0,0.6)';
              ctx.fillRect(x, yDraw + a.h - 18, a.w, 18);
              ctx.fillStyle = '#ffffff';
              ctx.font = 'bold 12px sans-serif';
              ctx.textAlign = 'left';
              ctx.textBaseline = 'middle';
              const nameText = a.name.length > 18 ? a.name.slice(0, 17) + '…' : a.name;
              ctx.fillText(nameText, x + 6, yDraw + a.h - 9);
            }
          }
        });
      });
      // Download
      try {
        const url = canvas.toDataURL('image/png');
        const a = document.createElement('a');
        a.href = url;
        a.download = 'TierList.png';
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
      } catch (e) {
        const url = canvas.toDataURL('image/png');
        window.open(url, '_blank');
      }
    });
  }
  function init() {
    setupThColors();
    initDragAndDrop();
    startObservers();
    // Keep data-tier in sync when user edits labels on existing rows
    document.querySelectorAll('#tierlist-table tr').forEach(tr => {
      const label = tr.querySelector('.tier-label');
      const dz = tr.querySelector('.tier-dropzone');
      if (label && dz) {
        label.addEventListener('input', () => {
          dz.setAttribute('data-tier', label.textContent.trim());
        });
      }
    });
    // Controls
    const addRowBtn = document.getElementById('add-row');
    const saveBtn = document.getElementById('save-png');
    const resetBtn = document.getElementById('reset-all');
    addRowBtn && addRowBtn.addEventListener('click', addRow);
    saveBtn && saveBtn.addEventListener('click', exportPNG);
    resetBtn && resetBtn.addEventListener('click', resetTierList);
   }
  if (document.readyState === 'complete' || document.readyState === 'interactive') {
    setTimeout(init, 0);
  } else {
    document.addEventListener('DOMContentLoaded', init);
  }
})();
})();
</script>
</script>
</includeonly>
</includeonly>

2025年10月20日 (一) 22:54的版本