<!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>
批量反色与便捷管理
您可以通过选择或拖放图片,快速生成并管理负片效果,其核心操作围绕批量处理与灵活下载展开。

- 批量处理与预览:您可以选择多张图片,工具会立即为每张图片生成负片效果,并以网格形式清晰预览。
- 灵活的下载方式:您可以点击任意图片卡片单独下载其负片版本,也可以通过顶部的“批量下载 ZIP”按钮一次性打包下载所有处理后的图片。
- 直观的状态管理:界面会清晰显示已加载的图片数量,并通过进度条反馈处理或打包的进度。
优化建议: 您可以根据需要调整 preview-grid 的 grid-template-columns 属性值,repeat(auto-fill, minmax(180px, 1fr)),以改变预览卡片的大小。
版权声明:除非特别标注,否则均为本站原创文章,转载时请以链接形式注明文章出处。