图片批量反色工具负片效果

联启 电脑工具 14
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">图片批量反色 · 负片效果</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    body {
      font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
      background: #1e1e2a;
      color: #eaeef0;
      min-height: 100vh;
      display: flex;
      justify-content: center;
      align-items: center;
      padding: 20px;
    }
    .card {
      max-width: 1100px;
      width: 100%;
      background: #2c2c3e;
      backdrop-filter: blur(2px);
      border-radius: 32px;
      padding: 28px 24px 32px;
      box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.04);
      transition: all 0.2s;
    }
    h1 {
      font-weight: 550;
      font-size: 1.9rem;
      letter-spacing: -0.3px;
      display: flex;
      align-items: center;
      gap: 10px;
      flex-wrap: wrap;
      margin-bottom: 12px;
    }
    h1 small {
      font-size: 0.9rem;
      font-weight: 400;
      opacity: 0.7;
      margin-left: 6px;
    }
    .sub {
      opacity: 0.75;
      margin-bottom: 24px;
      font-size: 0.95rem;
      border-left: 3px solid #6c8cff;
      padding-left: 14px;
    }
    .toolbar {
      background: #222236;
      border-radius: 28px;
      padding: 16px 20px;
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      gap: 16px 20px;
      margin-bottom: 28px;
      border: 1px solid #3b3b54;
    }
    .file-zone {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      gap: 12px 16px;
      flex: 2;
    }
    .button {
      background: #3f3f5c;
      border: none;
      color: #f0f4ff;
      padding: 10px 20px;
      border-radius: 60px;
      font-size: 0.95rem;
      font-weight: 500;
      display: inline-flex;
      align-items: center;
      gap: 8px;
      cursor: pointer;
      transition: 0.2s;
      border: 1px solid #56567a;
      box-shadow: 0 4px 8px rgba(0,0,0,0.2);
      line-height: 1;
    }
    .button-primary {
      background: #4f6bff;
      border-color: #7c93ff;
      color: white;
      box-shadow: 0 6px 12px rgba(79, 107, 255, 0.25);
    }
    .button-primary:hover {
      background: #5f78ff;
      transform: scale(1.02);
      border-color: #9aadff;
    }
    .button-primary:active {
      transform: scale(0.97);
    }
    .button-outline {
      background: transparent;
      border: 1px solid #6f6f96;
      box-shadow: none;
    }
    .button-outline:hover {
      background: #3a3a52;
    }
    .button:disabled {
      opacity: 0.4;
      pointer-events: none;
      filter: grayscale(0.5);
    }
    .badge {
      background: #1f1f30;
      border-radius: 60px;
      padding: 8px 16px;
      font-size: 0.9rem;
      color: #bcc4e0;
      border: 1px solid #3f3f5a;
    }
    .preview-grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
      gap: 16px;
      margin: 20px 0 16px;
      min-height: 80px;
    }
    .image-card {
      background: #1b1b2b;
      border-radius: 20px;
      overflow: hidden;
      box-shadow: 0 6px 12px rgba(0,0,0,0.5);
      border: 1px solid #35354e;
      transition: transform 0.15s, border-color 0.2s;
      display: flex;
      flex-direction: column;
    }
    .image-card:hover {
      transform: translateY(-4px);
      border-color: #6d8aff;
    }
    .image-card canvas {
      display: block;
      width: 100%;
      aspect-ratio: 1 / 1;
      object-fit: cover;
      background: #111;
      image-rendering: auto;
    }
    .image-card .card-footer {
      padding: 10px 12px 12px;
      background: #232338;
      display: flex;
      justify-content: space-between;
      align-items: center;
      font-size: 0.8rem;
      flex-wrap: wrap;
      gap: 4px;
    }
    .image-card .file-name {
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
      max-width: 100px;
      color: #b5c2f0;
    }
    .image-card .file-size {
      opacity: 0.6;
      font-size: 0.7rem;
    }
    .placeholder {
      grid-column: 1 / -1;
      background: #222236;
      border-radius: 28px;
      padding: 40px 20px;
      text-align: center;
      color: #9aa0c0;
      border: 1px dashed #4f5270;
      font-size: 0.95rem;
    }
    .placeholder svg {
      opacity: 0.3;
      margin-bottom: 6px;
    }
    .action-row {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      gap: 16px;
      margin-top: 18px;
      border-top: 1px solid #303049;
      padding-top: 22px;
    }
    .progress-bar {
      flex: 1;
      background: #1a1a2c;
      border-radius: 40px;
      height: 8px;
      min-width: 100px;
      overflow: hidden;
    }
    .progress-fill {
      height: 100%;
      width: 0%;
      background: linear-gradient(90deg, #6c8cff, #a86eff);
      border-radius: 40px;
      transition: width 0.3s ease;
    }
    .download-all {
      margin-left: auto;
    }
    @media (max-width: 600px) {
      .card { padding: 18px 14px; }
      h1 { font-size: 1.5rem; }
      .toolbar { flex-direction: column; align-items: stretch; }
      .file-zone { justify-content: center; }
      .action-row { flex-direction: column; align-items: stretch; }
      .download-all { margin-left: 0; }
    }
  </style>
</head>
<body>
<div class="card">
  <h1>
    🎞️ 负片反色器
    <small>批量 · 浏览器本地</small>
  </h1>
  <div class="sub">上传图片即时生成负片效果,支持批量下载 ZIP / 单张保存</div>
  <div class="toolbar">
    <div class="file-zone">
      <label class="button button-primary" for="fileInput">
        ⬆️ 选择图片
      </label>
      <input type="file" id="fileInput" accept="image/*" multiple style="display: none">
      <span class="badge" id="fileCount">0 张</span>
      <button class="button button-outline" id="clearBtn">🗑️ 清空</button>
    </div>
  </div>
  <!-- 预览网格 -->
  <div class="preview-grid" id="previewGrid">
    <div class="placeholder">
      <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2"><rect x="3" y="3" width="18" height="18" rx="3"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg>
      <p>拖放或点击“选择图片”<br>支持 JPG / PNG / WebP</p>
    </div>
  </div>
  <!-- 操作栏 -->
  <div class="action-row">
    <div class="progress-bar" id="progressWrap" style="display: none;">
      <div class="progress-fill" id="progressFill"></div>
    </div>
    <button class="button button-primary download-all" id="downloadAllBtn" disabled>
      📦 批量下载 ZIP
    </button>
    <span style="font-size:0.8rem; opacity:0.5;">点击卡片可单独保存</span>
  </div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<script>
  (function() {
    const fileInput = document.getElementById('fileInput');
    const previewGrid = document.getElementById('previewGrid');
    const fileCount = document.getElementById('fileCount');
    const clearBtn = document.getElementById('clearBtn');
    const downloadAllBtn = document.getElementById('downloadAllBtn');
    const progressWrap = document.getElementById('progressWrap');
    const progressFill = document.getElementById('progressFill');
    // 存储所有图片处理结果 { id, fileName, fileSize, originalCanvas, invertedCanvas, blobURL? }
    let images = [];
    let idCounter = 0;
    // ---------- 工具: 反色 (负片) ----------
    function applyInvert(canvas) {
      const ctx = canvas.getContext('2d', { willReadFrequently: true });
      const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
      const data = imageData.data;
      for (let i = 0; i < data.length; i += 4) {
        data[i] = 255 - data[i];       // R
        data[i + 1] = 255 - data[i + 1]; // G
        data[i + 2] = 255 - data[i + 2]; // B
        // alpha 保持不变
      }
      ctx.putImageData(imageData, 0, 0);
      return canvas;
    }
    // ---------- 将图片绘制到 canvas (正方形或原比例? 保持原比例展示) ----------
    function loadImageToCanvas(file) {
      return new Promise((resolve, reject) => {
        const img = new Image();
        const url = URL.createObjectURL(file);
        img.onload = () => {
          const canvas = document.createElement('canvas');
          canvas.width = img.naturalWidth || img.width;
          canvas.height = img.naturalHeight || img.height;
          const ctx = canvas.getContext('2d');
          ctx.drawImage(img, 0, 0);
          URL.revokeObjectURL(url);
          resolve(canvas);
        };
        img.onerror = () => {
          URL.revokeObjectURL(url);
          reject(new Error('图片加载失败'));
        };
        img.src = url;
      });
    }
    // ---------- 渲染预览卡片 (显示反色后) ----------
    function renderPreviews() {
      if (images.length === 0) {
        previewGrid.innerHTML = `
          <div class="placeholder">
            <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2"><rect x="3" y="3" width="18" height="18" rx="3"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg>
            <p>拖放或点击“选择图片”<br>支持 JPG / PNG / WebP</p>
          </div>
        `;
        fileCount.textContent = '0 张';
        downloadAllBtn.disabled = true;
        return;
      }
      // 更新计数
      fileCount.textContent = `${images.length} 张`;
      downloadAllBtn.disabled = false;
      let html = '';
      for (const item of images) {
        // 使用 invertedCanvas 生成 dataURL 展示
        const dataUrl = item.invertedCanvas.toDataURL('image/png');
        const name = item.fileName.length > 12 ? item.fileName.slice(0, 10) + '…' : item.fileName;
        const sizeStr = item.fileSize < 1024 
          ? `${item.fileSize} B` 
          : `${(item.fileSize / 1024).toFixed(1)} KB`;
        html += `
          <div class="image-card" data-id="${item.id}" title="点击下载此图片 (负片)">
            <canvas data-id="${item.id}" width="${item.invertedCanvas.width}" height="${item.invertedCanvas.height}" style="aspect-ratio: ${item.invertedCanvas.width} / ${item.invertedCanvas.height}; object-fit: contain; background: #111;"></canvas>
            <div class="card-footer">
              <span class="file-name">📸 ${name}</span>
              <span class="file-size">${sizeStr}</span>
            </div>
          </div>
        `;
      }
      previewGrid.innerHTML = html;
      // 将 invertedCanvas 绘制到对应 canvas 上 (保证缩放清晰)
      for (const item of images) {
        const canvasEl = previewGrid.querySelector(`canvas[data-id="${item.id}"]`);
        if (canvasEl) {
          const ctx = canvasEl.getContext('2d');
          // 保持宽高比展示
          const dpr = window.devicePixelRatio || 1;
          const rect = canvasEl.getBoundingClientRect();
          // 但 canvas 元素宽高由 css 控制, 我们需要设置 canvas 宽高为图片原始尺寸?为了显示清晰,设置与 invertedCanvas 相同尺寸
          canvasEl.width = item.invertedCanvas.width;
          canvasEl.height = item.invertedCanvas.height;
          ctx.clearRect(0, 0, canvasEl.width, canvasEl.height);
          ctx.drawImage(item.invertedCanvas, 0, 0);
        }
      }
      // 为每个卡片绑定单击下载
      document.querySelectorAll('.image-card').forEach(card => {
        card.addEventListener('click', function(e) {
          // 防止点 footer 内部冒泡混乱
          const id = parseInt(this.dataset.id, 10);
          const item = images.find(img => img.id === id);
          if (item) {
            downloadSingle(item);
          }
        });
      });
    }
    // ---------- 下载单张 (负片) ----------
    function downloadSingle(item) {
      const link = document.createElement('a');
      // 导出为 PNG (保留透明度)
      link.download = `inverted_${item.fileName.replace(/\.[^.]+$/, '')}.png`;
      link.href = item.invertedCanvas.toDataURL('image/png');
      link.click();
    }
    // ---------- 处理新文件 ----------
    async function processFiles(files) {
      if (!files || files.length === 0) return;
      // 显示进度条
      progressWrap.style.display = 'block';
      progressFill.style.width = '0%';
      const total = files.length;
      let completed = 0;
      const validFiles = Array.from(files).filter(f => f.type.startsWith('image/'));
      if (validFiles.length === 0) {
        progressWrap.style.display = 'none';
        return;
      }
      for (const file of validFiles) {
        try {
          const originalCanvas = await loadImageToCanvas(file);
          // 克隆原图用于反色 (避免修改原图引用)
          const invertedCanvas = document.createElement('canvas');
          invertedCanvas.width = originalCanvas.width;
          invertedCanvas.height = originalCanvas.height;
          const ctx = invertedCanvas.getContext('2d');
          ctx.drawImage(originalCanvas, 0, 0);
          applyInvert(invertedCanvas);
          images.push({
            id: ++idCounter,
            fileName: file.name,
            fileSize: file.size,
            originalCanvas: originalCanvas,
            invertedCanvas: invertedCanvas,
          });
        } catch (err) {
          console.warn('跳过文件:', file.name, err);
        }
        completed++;
        const percent = Math.min(100, Math.round((completed / total) * 100));
        progressFill.style.width = percent + '%';
      }
      // 延迟一点点让进度条动画完成
      setTimeout(() => {
        progressWrap.style.display = 'none';
        progressFill.style.width = '0%';
      }, 600);
      renderPreviews();
    }
    // ---------- 批量下载 ZIP ----------
    async function downloadAllAsZip() {
      if (images.length === 0) return;
      const zip = new JSZip();
      const folder = zip.folder('负片效果');
      // 显示进度
      progressWrap.style.display = 'block';
      progressFill.style.width = '0%';
      const total = images.length;
      for (let i = 0; i < images.length; i++) {
        const item = images[i];
        const baseName = item.fileName.replace(/\.[^.]+$/, '') || 'image';
        const fileName = `inverted_${baseName}.png`;
        // 将 canvas 转为 blob
        const blob = await new Promise(resolve => item.invertedCanvas.toBlob(resolve, 'image/png'));
        if (blob) {
          folder.file(fileName, blob);
        }
        progressFill.style.width = `${Math.round(((i + 1) / total) * 100)}%`;
      }
      const zipBlob = await zip.generateAsync({ type: 'blob' }, (metadata) => {
        // 压缩进度
        const percent = Math.round(metadata.percent);
        progressFill.style.width = percent + '%';
      });
      const link = document.createElement('a');
      link.href = URL.createObjectURL(zipBlob);
      link.download = `负片批量_${Date.now()}.zip`;
      link.click();
      URL.revokeObjectURL(link.href);
      setTimeout(() => {
        progressWrap.style.display = 'none';
        progressFill.style.width = '0%';
      }, 500);
    }
    // ---------- 清空 ----------
    function clearAll() {
      images = [];
      renderPreviews();
      // 重置文件输入
      fileInput.value = '';
    }
    // ---------- 事件绑定 ----------
    fileInput.addEventListener('change', (e) => {
      const files = e.target.files;
      if (files && files.length > 0) {
        processFiles(files);
      }
      // 清空 input 以便重复选择相同文件
      fileInput.value = '';
    });
    // 支持拖放 (简易)
    const cardElement = document.querySelector('.card');
    cardElement.addEventListener('dragover', (e) => {
      e.preventDefault();
      e.stopPropagation();
      cardElement.style.borderColor = '#6c8cff';
    });
    cardElement.addEventListener('dragleave', (e) => {
      e.preventDefault();
      e.stopPropagation();
      cardElement.style.borderColor = 'transparent';
    });
    cardElement.addEventListener('drop', (e) => {
      e.preventDefault();
      e.stopPropagation();
      cardElement.style.borderColor = 'transparent';
      const files = e.dataTransfer.files;
      if (files && files.length > 0) {
        processFiles(files);
      }
    });
    clearBtn.addEventListener('click', clearAll);
    downloadAllBtn.addEventListener('click', downloadAllAsZip);
    // 初始化渲染
    renderPreviews();
  })();
</script>
</body>
</html>

批量反色与便捷管理

您可以通过选择或拖放图片,快速生成并管理负片效果,其核心操作围绕批量处理与灵活下载展开。

图片批量反色工具负片效果-第1张图片-电脑手机工具软件下载 - 免费实用工具合集 | 联启科技

  • 批量处理与预览:您可以选择多张图片,工具会立即为每张图片生成负片效果,并以网格形式清晰预览。
  • 灵活的下载方式:您可以点击任意图片卡片单独下载其负片版本,也可以通过顶部的“批量下载 ZIP”按钮一次性打包下载所有处理后的图片。
  • 直观的状态管理:界面会清晰显示已加载的图片数量,并通过进度条反馈处理或打包的进度。

优化建议: 您可以根据需要调整 preview-gridgrid-template-columns 属性值,repeat(auto-fill, minmax(180px, 1fr)),以改变预览卡片的大小。

标签: 图片批量反色 负片效果

抱歉,评论功能暂时关闭!