微件

TierListMaker:修订间差异

来自卡厄思梦境WIKI

律Rhyme留言 | 贡献
无编辑摘要
律Rhyme留言 | 贡献
无编辑摘要
第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)
       // Use contains() to allow editing label text safely (fix S/A/B/C not editable)
       th.addEventListener('click', (e) => {
       th.addEventListener('click', (e) => {
         if (e.target.closest('.tier-label')) {
         const labelEl = th.querySelector('.tier-label');
           // allow editing text without opening palette
        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';
      // ensure id
       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 (from ask or later)
   // 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 && node.querySelectorAll('.avatar-frame').forEach(setupDraggable);
             node.querySelectorAll('.avatar-frame').forEach(setupDraggable);
           }
           }
         }
         }
第324行: 第324行:
     // Enable interactions
     // Enable interactions
     th.addEventListener('click', (e) => {
     th.addEventListener('click', (e) => {
       if (e.target.closest('.tier-label')) return;
       const labelEl = th.querySelector('.tier-label');
      if (labelEl && labelEl.contains(e.target)) return;
       openColorPalette(th, e);
       openColorPalette(th, e);
     });
     });
第341行: 第342行:
     });
     });
   }
   }
   // Export to PNG by drawing canvas (preserving backgrounds)
   // 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 thEl = table.querySelector('th.tier-th');
    const thWidth = thEl ? Math.ceil(thEl.getBoundingClientRect().width) : 120;
    // Collect rows
     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 dzRect = dz.getBoundingClientRect();
      const height = Math.max(thRect.height, dzRect.height);
       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';
      // 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 => {
         const img = av.querySelector('img');
         const img = av.querySelector('img');
第372行: 第368行:
         };
         };
       });
       });
       return { thRect, dzRect, height, label, color, textColor, dzBg, avatars };
       return {
        y: Math.round(rowRect.top - tableRect.top),
        height: Math.round(rowRect.height),
        thWidth: Math.round(thRect.width),
        label, color, textColor, dzBg, avatars
      };
     });
     });
    const totalHeight = Math.ceil(rowInfo.reduce((acc, r) => acc + r.height, 0));
     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');
     // Full page background (match body)
     // Base white background so row间隔保留为白色
     const bodyBg = getComputedStyle(document.body).backgroundColor || '#ffffff';
     ctx.fillStyle = '#ffffff';
    ctx.fillStyle = bodyBg;
     ctx.fillRect(0, 0, canvas.width, canvas.height);
     ctx.fillRect(0, 0, canvas.width, canvas.height);
     // Load all images
     // 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();
            // try anonymous to avoid taint if same-origin allows
             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); });
        if (img) imgMap.set(a.imgSrc, img);
       // Draw each row at its actual Y to preserve gaps
      });
       rowInfo.forEach(info => {
       // Draw rows
         // Header background
       let yCursor = 0;
      rows.forEach((row, idx) => {
        const info = rowInfo[idx];
         // Left header background
         ctx.fillStyle = info.color;
         ctx.fillStyle = info.color;
         ctx.fillRect(0, yCursor, thWidth, info.height);
         ctx.fillRect(0, info.y, info.thWidth, info.height);
         // Right dropzone background
         // Dropzone background
         ctx.fillStyle = info.dzBg;
         ctx.fillStyle = info.dzBg;
         ctx.fillRect(thWidth, yCursor, width - thWidth, info.height);
         ctx.fillRect(info.thWidth, info.y, width - info.thWidth, info.height);
         // Label text centered
         // 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(yCursor + info.height / 2));
         ctx.fillText(labelText, Math.floor(info.thWidth / 2), Math.floor(info.y + info.height / 2));
         // Draw avatars
         // 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 y = Math.round(a.y);
             const rowTop = info.y;
            const rowTop = yCursor;
             const rowBottom = info.y + info.height - a.h - 6;
             const rowBottom = yCursor + info.height - a.h - 6;
             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 background
             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);
            // Border
             ctx.strokeStyle = '#cccccc';
             ctx.strokeStyle = '#cccccc';
             ctx.lineWidth = 3;
             ctx.lineWidth = 3;
第453行: 第446行:
           }
           }
         });
         });
        yCursor += info.height;
       });
       });
       // Download
       // Download

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