微件

TierListMaker:修订间差异

来自卡厄思梦境WIKI

律Rhyme留言 | 贡献
无编辑摘要
律Rhyme留言 | 贡献
无编辑摘要
 
(未显示同一用户的9个中间版本)
第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: 0px;
}
.tier-tools {
  position: absolute;
  right: 6px;
  top: 6px;
  display: flex;
  gap: 6px;
}
/* 导出时隐藏工具按钮 */
.exporting .tier-tools {
  display: none !important;
}
.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: 4px;
   padding: 6px;
}
}
.tier-dropzone.over {
.tier-dropzone.pool {
   border-color: #3498db;
  border: 2px dashed #ccc;
   background: #f0f8ff;
  padding: 8px;
   border-radius: 6px;
   background: #fafafa;
}
}
.char-pool {
/* 导出时隐藏角色池 */
   margin-top: 10px;
.exporting .pool-wrapper {
  border: 2px solid #ddd;
   display: none !important;
  border-radius: 6px;
  padding: 6px;
  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行: 第96行:
   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行: 第112行:
   height: 100px;
   height: 100px;
   object-fit: cover;
   object-fit: cover;
  pointer-events: none;
  -webkit-user-drag: none;
}
}
.avatar-name {
.avatar-name {
第118行: 第132行:
.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);
}
}
.palette-close {
.tier-label[contenteditable="true"] {
   padding: 4px 8px;
   outline: none;
  background: #eee;
   cursor: text;
  border-radius: 4px;
   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();
     const c = hex.replace('#','');
    else document.addEventListener('DOMContentLoaded', fn);
    let r, g, b;
  }
     if (c.length === 3) {
   function contrastColor(hex) {
      r = parseInt(c[0] + c[0], 16);
     hex = (hex || '').replace('#','');
      g = parseInt(c[1] + c[1], 16);
     if (hex.length === 3) hex = hex.split('').map(c => c + c).join('');
      b = parseInt(c[2] + c[2], 16);
    if (!/^[0-9a-fA-F]{6}$/.test(hex)) return '#fff';
     } else {
     var r = parseInt(hex.substr(0,2), 16);
      r = parseInt(c.substring(0,2), 16);
    var g = parseInt(hex.substr(2,2), 16);
      g = parseInt(c.substring(2,4), 16);
    var b = parseInt(hex.substr(4,2), 16);
      b = parseInt(c.substring(4,6), 16);
     var yiq = ((r*299)+(g*587)+(b*114))/1000;
     }
     return yiq >= 128 ? '#000' : '#fff';
    // relative luminance
  }
    const luminance = (0.2126*r + 0.7152*g + 0.0722*b) / 255;
  function getDragAfterElement(container, y) {
     return luminance > 0.55 ? '#000' : '#fff';
    const elements = [...container.querySelectorAll('.avatar-frame:not(.dragging)')];
    return elements.reduce((closest, child) => {
      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 applyThColor(th, color) {
   function ensureCrossOrigin(img) {
     th.setAttribute('data-color', color);
     try {
    th.style.backgroundColor = color;
      if (!img) return;
    th.style.color = idealTextColor(color);
      if (!img.crossOrigin) img.crossOrigin = 'anonymous';
      if (!img.referrerPolicy) img.referrerPolicy = 'no-referrer';
    } catch(e) {}
   }
   }
   function setupThColors() {
   function makeDraggable(avatar) {
     const ths = document.querySelectorAll('#tierlist-table th.tier-th');
     if (!avatar || avatar._draggableInit) return;
     ths.forEach(th => {
    avatar._draggableInit = true;
       const color = th.getAttribute('data-color') || '#888888';
    avatar.setAttribute('draggable', 'true');
       applyThColor(th, color);
    avatar.classList.add('draggable-avatar');
      th.addEventListener('click', (e) => {
     avatar.querySelectorAll('img').forEach(ensureCrossOrigin);
        openColorPalette(th, e);
 
       });
    avatar.addEventListener('dragstart', function(e) {
       avatar.classList.add('dragging');
      avatar._prevParent = avatar.parentNode;
      e.dataTransfer.setData('text/plain', avatar.dataset.id || avatar.querySelector('.avatar-name')?.textContent || '');
       e.dataTransfer.effectAllowed = 'move';
    });
    avatar.addEventListener('dragend', function() {
      avatar.classList.remove('dragging');
       cleanupEmptyWrapper(avatar._prevParent);
      avatar._prevParent = null;
     });
     });
   }
   }
  // Color palette
   function initAvatars(root) {
  const paletteColors = [
     if (!root) return;
    '#e74c3c','#f1963b','#ffef03','#c2d402','#2ecc71','#3498db',
     const avatars = root.querySelectorAll('.avatar-frame');
    '#9b59b6','#34495e','#7f8c8d','#ffffff','#000000','#f39c12',
     avatars.forEach(function(av) {
    '#1abc9c','#8e44ad','#d35400','#27ae60','#2980b9','#bdc3c7'
       if (!av.dataset.id) {
  ];
         const nameEl = av.querySelector('.avatar-name');
  let paletteEl = null;
        av.dataset.name = nameEl ? nameEl.textContent.trim() : '';
   function buildPalette() {
      }
     paletteEl = document.createElement('div');
       makeDraggable(av);
    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');
    actions.className = 'actions';
    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 cleanupEmptyWrapper(node) {
     if (!paletteEl) buildPalette();
     if (!node || node.nodeType !== 1) return;
     const rect = th.getBoundingClientRect();
    if (node.classList && node.classList.contains('tier-dropzone')) return;
     paletteEl.style.left = (window.scrollX + rect.left + 10) + 'px';
     const hasElementChild = node.querySelector('.avatar-frame');
     paletteEl.style.top = (window.scrollY + rect.bottom + 8) + 'px';
     const onlyWhitespace = !node.textContent || node.textContent.trim().length === 0;
    paletteEl.style.display = 'block';
     if (!hasElementChild && node.childElementCount === 0 && onlyWhitespace) {
     paletteEl._targetTh = th;
      node.parentNode && node.parentNode.removeChild(node);
     }
   }
   }
  // Drag & Drop
   function cleanupEmptyPlaceholdersIn(container) {
  let draggedEl = null;
     if (!container) return;
   function setupDraggable(el) {
     Array.from(container.children).forEach(function(child) {
     if (!el || el._draggableSetup) return;
       if (child.nodeType !== 1) return;
     el._draggableSetup = true;
       if (child.classList.contains('avatar-frame')) return;
    el.setAttribute('draggable', 'true');
       const hasAvatarInside = !!child.querySelector('.avatar-frame');
    el.addEventListener('dragstart', (e) => {
       const onlyWhitespace = !child.textContent || child.textContent.trim().length === 0;
       draggedEl = el;
      if (!hasAvatarInside && child.childElementCount === 0 && onlyWhitespace) {
       el.classList.add('dragging');
        child.remove();
       e.dataTransfer.effectAllowed = 'move';
       // ensure id
      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];
        }
       }
       }
    });
    el.addEventListener('dragend', () => {
      el.classList.remove('dragging');
      draggedEl = null;
     });
     });
   }
   }
   function setupDropzone(zone) {
   function initDropzone(zone) {
     zone.addEventListener('dragover', (e) => {
    if (!zone || zone._dropzoneInit) return;
       if (draggedEl) e.preventDefault();
    zone._dropzoneInit = true;
       zone.classList.add('over');
 
     zone.addEventListener('dragover', function(e) {
       e.preventDefault();
       e.dataTransfer.dropEffect = 'move';
      const afterElement = getDragAfterElement(zone, e.clientY);
      const dragging = document.querySelector('.avatar-frame.dragging');
      if (!dragging) return;
      if (afterElement == null) {
        if (dragging.parentNode !== zone) zone.appendChild(dragging);
      } else {
        if (afterElement !== dragging) zone.insertBefore(dragging, afterElement);
      }
     });
     });
    zone.addEventListener('dragleave', () => {
 
      zone.classList.remove('over');
     zone.addEventListener('drop', function(e) {
    });
     zone.addEventListener('drop', (e) => {
       e.preventDefault();
       e.preventDefault();
       zone.classList.remove('over');
       e.stopPropagation();
       if (!draggedEl) return;
      const dragging = document.querySelector('.avatar-frame.dragging');
      // move element
       if (dragging) {
      zone.appendChild(draggedEl);
        if (dragging.parentNode !== zone) zone.appendChild(dragging);
       // normalize styling when moved
        cleanupEmptyWrapper(dragging._prevParent);
       draggedEl.style.pointerEvents = 'auto';
        dragging._prevParent = null;
       }
       if (zone.classList.contains('pool')) {
        cleanupEmptyPlaceholdersIn(zone);
      }
     });
     });
   }
   }
   function initDragAndDrop() {
   function buildEditableHead(th) {
     // make existing avatars draggable
     const currentText = (th.childNodes[0] && th.childNodes[0].nodeType === 3)
     document.querySelectorAll('.avatar-frame').forEach(setupDraggable);
      ? th.childNodes[0].nodeValue.trim()
     // dropzones
      : th.textContent.trim();
     document.querySelectorAll('.tier-dropzone').forEach(setupDropzone);
    const tools = th.querySelector('.tier-tools');
     const pool = document.getElementById('char-pool');
     th.innerHTML = '';
    setupDropzone(pool);
    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';
      color.title = '更改颜色';
      const del = document.createElement('div');
      del.className = 'delete-row';
      del.textContent = '删除';
      del.title = '删除该行';
      t.appendChild(color); t.appendChild(del);
      return t;
    })();
    th.appendChild(label);
    th.appendChild(toolsWrap);
    const initialBg = th.getAttribute('data-initial-bg') || '#555';
    th.style.background = initialBg;
    th.style.color = contrastColor(initialBg);
    attachColorPalette(toolsWrap.querySelector('.color-toggle'), th);
     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');
      Array.from(dropzone.querySelectorAll('.avatar-frame')).forEach(function(av) {
        pool.appendChild(av);
      });
      tr.parentNode.removeChild(tr);
    });
   }
   }
   // Observe dynamically added avatars (from ask or later)
   function attachColorPalette(toggle, th) {
  const poolObserver = new MutationObserver((mutations) => {
    if (!toggle) return;
     mutations.forEach((m) => {
    const palette = document.createElement('div');
       m.addedNodes.forEach((node) => {
    palette.className = 'color-palette';
        if (node.nodeType === 1) {
    const colors = [
          if (node.classList && node.classList.contains('avatar-frame')) {
      '#e53935','#d81b60','#8e24aa','#5e35b1','#3949ab','#1e88e5','#039be5','#00acc1',
            setupDraggable(node);
      '#00897b','#43a047','#7cb342','#c0ca33','#fdd835','#fb8c00','#f4511e','#6d4c41',
          } else {
      '#546e7a','#9e9e9e','#000000','#ffffff'
            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() {
        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(e) {
      e.stopPropagation();
      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') || document.querySelector('#tierlist-table');
     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.textContent = '新行';
    label.className = 'tier-label';
    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) => openColorPalette(th, e));
     buildEditableHead(th);
     setupDropzone(dropzone);
  }
     // Keep data-tier synced with label edits
  function ensureHtml2Canvas(cb) {
     label.addEventListener('input', () => {
    if (window.html2canvas) { cb(); return; }
       dropzone.setAttribute('data-tier', label.textContent.trim());
     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);
   }
   }
  // Export to PNG by drawing canvas (no external libs)
   function downloadCanvas(canvas) {
   function exportPNG() {
     function done(blobOrUrl) {
    const table = document.getElementById('tierlist-table');
    const tableRect = table.getBoundingClientRect();
    const thWidth = table.querySelector('th.tier-th')?.getBoundingClientRect().width || 120;
    // Collect rows
    const rows = Array.from(table.querySelectorAll('tr'));
    const rowInfo = rows.map(row => {
      const th = row.querySelector('th.tier-th');
      const dz = row.querySelector('.tier-dropzone');
      const thRect = th.getBoundingClientRect();
      const dzRect = dz.getBoundingClientRect();
      const height = Math.max(thRect.height, dzRect.height);
      const label = th.querySelector('.tier-label')?.textContent || '';
      const color = th.getAttribute('data-color') || '#888';
      const textColor = idealTextColor(color);
      // collect avatars in this dropzone
      const avatars = Array.from(dz.querySelectorAll('.avatar-frame')).map(av => {
        const img = av.querySelector('img');
        const rect = av.getBoundingClientRect();
        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 { thRect, dzRect, height, label, color, textColor, avatars };
    });
    const totalHeight = rowInfo.reduce((acc, r) => acc + r.height, 0);
    const width = tableRect.width;
    const canvas = document.createElement('canvas');
    canvas.width = Math.ceil(width);
    canvas.height = Math.ceil(totalHeight);
    const ctx = canvas.getContext('2d');
    // Background
    ctx.fillStyle = '#ffffff';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    // Load all images
    const loadTasks = [];
    rowInfo.forEach(r => {
      r.avatars.forEach(a => {
        if (a.imgSrc) {
          loadTasks.push(new Promise((resolve) => {
            const img = new Image();
            img.onload = () => resolve({ a, img });
            img.onerror = () => resolve({ a, img: null });
            img.src = a.imgSrc;
          }));
        }
      });
     });
    Promise.all(loadTasks).then(results => {
      const imgMap = new Map();
      results.forEach(({ a, img }) => {
        if (img) imgMap.set(a.imgSrc, img);
      });
      // Draw rows
      let yCursor = 0;
      rows.forEach((row, idx) => {
        const info = rowInfo[idx];
        // Left header
        ctx.fillStyle = info.color;
        ctx.fillRect(0, yCursor, thWidth, info.height);
        // Label text centered
        ctx.fillStyle = info.textColor;
        ctx.font = 'bold 20px sans-serif';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.fillText(info.label, thWidth / 2, yCursor + info.height / 2);
        // Draw avatars (absolute positions relative to table)
        info.avatars.forEach(a => {
          const img = imgMap.get(a.imgSrc);
          if (img) {
            const x = Math.max(thWidth + 6, Math.round(a.x));
            const y = Math.round(a.y);
            // If rect outside current row vertical span, clamp within this row
            const rowTop = yCursor;
            const rowBottom = yCursor + info.height - a.h - 6;
            const yDraw = Math.min(Math.max(y, rowTop + 6), rowBottom);
            // Frame background
            ctx.fillStyle = '#f5f5f5';
            ctx.fillRect(x - 3, yDraw - 3, a.w + 6, a.h + 6);
            // Border
            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);
            }
          }
        });
        yCursor += info.height;
      });
      // Download
       try {
       try {
         const url = canvas.toDataURL('image/png');
         const link = document.createElement('a');
         const a = document.createElement('a');
         const isBlob = blobOrUrl instanceof Blob;
         a.href = url;
        const url = isBlob ? (URL.createObjectURL(blobOrUrl)) : blobOrUrl;
         a.download = 'TierList.png';
         link.href = url;
         document.body.appendChild(a);
         link.download = 'tierlist.png';
         a.click();
         document.body.appendChild(link);
         document.body.removeChild(a);
         link.click();
         document.body.removeChild(link);
        if (isBlob) setTimeout(() => URL.revokeObjectURL(url), 0);
       } catch (e) {
       } catch (e) {
        // Fallback: open in new tab
         const dataUrl = canvas.toDataURL('image/png');
         const url = canvas.toDataURL('image/png');
         window.open(dataUrl, '_blank');
         window.open(url, '_blank');
       }
       }
     });
     }
    if (canvas.toBlob) {
      canvas.toBlob(function(blob) {
        if (blob) done(blob);
        else done(canvas.toDataURL('image/png'));
      }, 'image/png');
    } else {
      done(canvas.toDataURL('image/png'));
    }
  }
  function prepareImagesForExport(container) {
    if (!container) return;
    container.querySelectorAll('img').forEach(ensureCrossOrigin);
   }
   }
   function init() {
   function savePNG() {
     setupThColors();
     const maker = document.getElementById('tierlist-maker');
     initDragAndDrop();
     const table = document.getElementById('tierlist-table');
     startObservers();
     if (!table) {
     // Keep data-tier in sync when user edits labels on existing rows
      alert('未找到要导出的表格。');
     document.querySelectorAll('#tierlist-table tr').forEach(tr => {
      return;
       const label = tr.querySelector('.tier-label');
    }
      const dz = tr.querySelector('.tier-dropzone');
   
      if (label && dz) {
     // 添加导出样式类
         label.addEventListener('input', () => {
     maker.classList.add('exporting');
           dz.setAttribute('data-tier', label.textContent.trim());
    prepareImagesForExport(table);
   
    ensureHtml2Canvas(function() {
       // 稍微延迟以确保CSS生效
      setTimeout(function() {
        html2canvas(table, {
          backgroundColor: '#ffffff',
          scale: 2,
          useCORS: true,
          allowTaint: false,
          imageTimeout: 15000
        }).then(function(canvas) {
          downloadCanvas(canvas);
        }).catch(function(err) {
          console.error('html2canvas 失败', err);
          alert('保存PNG失败:' + (err && err.message ? err.message : '未知错误'));
         }).finally(function() {
           maker.classList.remove('exporting');
         });
         });
       }
       }, 100);
     });
     });
    // Controls
    const addRowBtn = document.getElementById('add-row');
    const saveBtn = document.getElementById('save-png');
    addRowBtn && addRowBtn.addEventListener('click', addRow);
    saveBtn && saveBtn.addEventListener('click', exportPNG);
   }
   }
   if (document.readyState === 'complete' || document.readyState === 'interactive') {
   function resetAll() {
    setTimeout(init, 0);
    const pool = document.getElementById('character-pool');
  } else {
    if (!pool) return;
     document.addEventListener('DOMContentLoaded', init);
    const rows = document.querySelectorAll('#tierlist-table .tier-dropzone');
    rows.forEach(function(zone) {
      if (zone.classList.contains('pool')) return;
      Array.from(zone.querySelectorAll('.avatar-frame')).forEach(function(av) {
        pool.appendChild(av);
      });
      cleanupEmptyPlaceholdersIn(zone);
    });
     cleanupEmptyPlaceholdersIn(pool);
   }
   }
  ready(function() {
    document.querySelectorAll('#tierlist-table .tier-head').forEach(buildEditableHead);
    document.querySelectorAll('.tier-dropzone').forEach(initDropzone);
    initAvatars(document.getElementById('character-pool'));
    var addBtn = document.getElementById('add-row');
    if (addBtn) addBtn.addEventListener('click', addNewRow);
    var saveBtn = document.getElementById('save-png');
    if (saveBtn) saveBtn.addEventListener('click', savePNG);
    (function insertResetButton() {
      const controls = document.querySelector('#tierlist-maker .tierlist-controls');
      if (!controls || document.getElementById('reset-roles')) return;
      const resetBtn = document.createElement('div');
      resetBtn.id = 'reset-roles';
      resetBtn.className = 'btn';
      resetBtn.textContent = '重置';
      controls.insertBefore(resetBtn, controls.querySelector('#save-png') || null);
      resetBtn.addEventListener('click', resetAll);
    })();
    const pool = document.getElementById('character-pool');
    if (pool) {
      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);
                }
                n.querySelectorAll && n.querySelectorAll('.avatar-frame').forEach(makeDraggable);
                if (pool.contains(n)) cleanupEmptyPlaceholdersIn(pool);
              }
            });
          }
        });
      });
      obs.observe(pool, { childList: true, subtree: true });
    }
  });
})();
})();
</script>
</script>
</includeonly>
</includeonly>

2025年10月29日 (三) 14:50的最新版本