这是一个非常实用的图像处理需求!我来帮你创建一个HTML页面,实现图片的批量反色(负片效果)处理工具。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">图片批量反色工具 · 负片效果</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
background: #f0f2f5;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 2rem 1rem;
color: #1a1a2e;
}
.container {
max-width: 1100px;
width: 100%;
background: white;
border-radius: 24px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.08), 0 8px 16px rgba(0, 0, 0, 0.06);
padding: 2rem 2rem 2.5rem;
transition: all 0.2s;
}
h1 {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
font-size: 1.8rem;
letter-spacing: -0.01em;
margin-bottom: 0.25rem;
color: #0b0b1e;
}
.subtitle {
color: #5b5b7a;
margin-bottom: 1.8rem;
font-size: 1rem;
border-left: 4px solid #6c5ce7;
padding-left: 1rem;
background: #f8f7ff;
border-radius: 0 8px 8px 0;
line-height: 1.4;
}
.upload-area {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 2px dashed #c2c7e0;
border-radius: 20px;
padding: 2.5rem 1rem;
background: #fafbff;
transition: border 0.2s, background 0.2s;
cursor: pointer;
}
.upload-area:hover {
border-color: #6c5ce7;
background: #f4f2ff;
}
.upload-area.dragover {
border-color: #6c5ce7;
background: #eeebff;
box-shadow: 0 0 0 4px rgba(108, 92, 231, 0.15);
}
.upload-icon {
font-size: 3.2rem;
line-height: 1;
margin-bottom: 0.6rem;
opacity: 0.7;
}
.upload-text {
font-weight: 500;
font-size: 1.2rem;
color: #2d2d44;
}
.upload-hint {
font-size: 0.9rem;
color: #7a7a9a;
margin-top: 0.3rem;
}
input[type="file"] {
display: none;
}
.action-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 16px;
margin: 24px 0 20px;
}
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 0.6rem 1.6rem;
border-radius: 60px;
border: none;
font-weight: 500;
font-size: 0.95rem;
background: #fff;
color: #1a1a2e;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
border: 1px solid #d9dceb;
transition: all 0.15s;
cursor: pointer;
}
.btn-primary {
background: #6c5ce7;
border: 1px solid #6c5ce7;
color: white;
box-shadow: 0 4px 10px rgba(108, 92, 231, 0.25);
}
.btn-primary:hover {
background: #5a4bd1;
box-shadow: 0 6px 14px rgba(108, 92, 231, 0.3);
transform: translateY(-1px);
}
.btn-primary:active {
transform: scale(0.96);
}
.btn-secondary {
background: #f0f2f8;
border-color: #d9dceb;
}
.btn-secondary:hover {
background: #e5e7f0;
}
.btn-danger {
background: #fff0f0;
border-color: #ffc2c2;
color: #b33a3a;
}
.btn-danger:hover {
background: #ffe0e0;
border-color: #ff9f9f;
}
.stats {
margin-left: auto;
font-size: 0.9rem;
color: #5b5b7a;
background: #f4f5fc;
padding: 0.4rem 1.2rem;
border-radius: 40px;
white-space: nowrap;
}
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 20px;
margin: 20px 0 10px;
}
.card {
background: #ffffff;
border-radius: 18px;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.04);
border: 1px solid #eef0f8;
overflow: hidden;
transition: transform 0.1s;
display: flex;
flex-direction: column;
}
.card:hover {
transform: translateY(-3px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.06);
}
.card-image {
width: 100%;
aspect-ratio: 1 / 1;
object-fit: cover;
display: block;
background: #f2f2f8;
border-bottom: 1px solid #eff1f9;
}
.card-info {
padding: 10px 12px 12px;
font-size: 0.8rem;
display: flex;
justify-content: space-between;
align-items: center;
background: white;
}
.card-filename {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 110px;
color: #2d2d44;
font-weight: 450;
}
.card-download {
background: transparent;
border: none;
color: #6c5ce7;
font-weight: 500;
font-size: 0.75rem;
padding: 4px 12px;
border-radius: 30px;
background: #f0eeff;
cursor: pointer;
transition: 0.1s;
white-space: nowrap;
}
.card-download:hover {
background: #6c5ce7;
color: white;
}
.empty-message {
grid-column: 1 / -1;
text-align: center;
padding: 3rem 1rem;
color: #8989ae;
background: #f8f9ff;
border-radius: 24px;
font-size: 1rem;
}
.footer-note {
margin-top: 24px;
font-size: 0.85rem;
color: #8b8bae;
text-align: center;
border-top: 1px solid #edf0f8;
padding-top: 20px;
display: flex;
justify-content: center;
gap: 20px;
flex-wrap: wrap;
}
@media (max-width: 600px) {
.container {
padding: 1.2rem;
}
.action-bar {
gap: 12px;
}
.stats {
margin-left: 0;
width: 100%;
text-align: center;
}
.gallery {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 14px;
}
h1 {
font-size: 1.5rem;
}
}
/* 自定义滚动条 */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-thumb {
background: #c2c7e0;
border-radius: 12px;
}
</style>
</head>
<body>
<div class="container">
<h1>🎞️ 批量反色 · 负片</h1>
<div class="subtitle">拖拽或选择图片 —— 自动生成反色负片效果,支持批量下载</div>
<!-- 上传区域 -->
<div class="upload-area" id="dropZone">
<div class="upload-icon">🖼️</div>
<div class="upload-text">点击或拖放图片到这里</div>
<div class="upload-hint">支持 JPG / PNG / WebP · 批量处理</div>
</div>
<input type="file" id="fileInput" multiple accept="image/png,image/jpeg,image/webp,image/bmp,image/gif,image/avif">
<!-- 操作栏 -->
<div class="action-bar">
<button class="btn btn-primary" id="processBtn">✨ 全部反色</button>
<button class="btn btn-secondary" id="downloadAllBtn">📦 下载全部</button>
<button class="btn btn-danger" id="clearBtn">🗑️ 清空</button>
<span class="stats" id="statsDisplay">0 张图片</span>
</div>
<!-- 图片网格 -->
<div class="gallery" id="gallery">
<div class="empty-message">📸 添加图片后,将在此显示原始与反色预览</div>
</div>
<div class="footer-note">
<span>⚡ 反色 = 255 - 像素值 (RGB)</span>
<span>💡 点击单张下载按钮保存负片</span>
</div>
</div>
<script>
(function() {
// ---- DOM refs ----
const fileInput = document.getElementById('fileInput');
const dropZone = document.getElementById('dropZone');
const gallery = document.getElementById('gallery');
const processBtn = document.getElementById('processBtn');
const downloadAllBtn = document.getElementById('downloadAllBtn');
const clearBtn = document.getElementById('clearBtn');
const statsDisplay = document.getElementById('statsDisplay');
// ---- 数据存储 ----
// 每个条目: { id, file, originalDataUrl, invertedDataUrl (初始null) }
let imageItems = [];
let nextId = 0;
// ---- 辅助函数 ----
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / 1048576).toFixed(1) + ' MB';
}
function createCard(item) {
const card = document.createElement('div');
card.className = 'card';
card.dataset.id = item.id;
// 图片显示:优先显示反色后的,若没有则显示原始
const img = document.createElement('img');
img.className = 'card-image';
img.alt = 'preview';
// 使用 invertedDataUrl 或 originalDataUrl
img.src = item.invertedDataUrl || item.originalDataUrl;
const info = document.createElement('div');
info.className = 'card-info';
const filenameSpan = document.createElement('span');
filenameSpan.className = 'card-filename';
// 截断文件名
let name = item.file.name;
if (name.length > 18) {
name = name.substring(0, 12) + '…' + name.substring(name.lastIndexOf('.'));
}
filenameSpan.textContent = name;
const downloadBtn = document.createElement('button');
downloadBtn.className = 'card-download';
downloadBtn.textContent = '⬇ 负片';
downloadBtn.addEventListener('click', (e) => {
e.stopPropagation();
downloadSingleImage(item);
});
info.appendChild(filenameSpan);
info.appendChild(downloadBtn);
card.appendChild(img);
card.appendChild(info);
// 点击卡片预览大图? 简单起见:点击卡片触发下载单张
card.addEventListener('click', () => {
downloadSingleImage(item);
});
return card;
}
// 下载单张 (总是下载反色版本,若没有则下载原图)
function downloadSingleImage(item) {
const dataUrl = item.invertedDataUrl || item.originalDataUrl;
if (!dataUrl) return;
const link = document.createElement('a');
link.href = dataUrl;
// 添加负片后缀
const baseName = item.file.name.replace(/\.[^.]+$/, '');
const ext = item.file.name.includes('.') ? item.file.name.substring(item.file.name.lastIndexOf('.')) : '.png';
link.download = `${baseName}_负片${ext}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
// 更新 gallery 渲染
function renderGallery() {
gallery.innerHTML = '';
if (imageItems.length === 0) {
gallery.innerHTML = '<div class="empty-message">📸 添加图片后,将在此显示原始与反色预览</div>';
statsDisplay.textContent = '0 张图片';
return;
}
imageItems.forEach(item => {
const card = createCard(item);
gallery.appendChild(card);
});
statsDisplay.textContent = `${imageItems.length} 张图片`;
}
// ---- 核心反色函数 (使用Canvas) ----
function invertImage(item) {
return new Promise((resolve) => {
const img = new Image();
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);
// 获取像素数据
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
// 反色: 255 - 值 (保留Alpha)
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
// data[i+3] 不变 (Alpha)
}
ctx.putImageData(imageData, 0, 0);
// 保存反色结果 (用 PNG 保证质量)
const invertedDataUrl = canvas.toDataURL('image/png');
item.invertedDataUrl = invertedDataUrl;
resolve(item);
};
img.onerror = () => {
// 如果加载失败,保持原样,标记反色为null
item.invertedDataUrl = null;
resolve(item);
};
img.src = item.originalDataUrl;
});
}
// 批量反色
async function batchInvert() {
if (imageItems.length === 0) return;
// 显示处理中状态 (简单禁用按钮)
processBtn.disabled = true;
processBtn.textContent = '⏳ 处理中...';
// 只处理还没有 invertedDataUrl 的(或者强制全部刷新? 这里全部重新生成保证一致性)
const promises = imageItems.map(item => invertImage(item));
await Promise.all(promises);
// 更新UI
renderGallery();
processBtn.disabled = false;
processBtn.textContent = '✨ 全部反色';
}
// ---- 添加文件 ----
function addFiles(files) {
if (!files || files.length === 0) return;
const validFiles = Array.from(files).filter(file => file.type.startsWith('image/'));
if (validFiles.length === 0) {
alert('请选择图片文件 (JPG, PNG, WebP 等)');
return;
}
const newItems = [];
validFiles.forEach(file => {
const reader = new FileReader();
reader.onload = (e) => {
const originalDataUrl = e.target.result;
const item = {
id: nextId++,
file: file,
originalDataUrl: originalDataUrl,
invertedDataUrl: null, // 初始无反色
};
imageItems.push(item);
newItems.push(item);
// 如果是最后一张完成,渲染
if (newItems.length === validFiles.length) {
// 自动为新增图片执行反色 (也可手动触发)
// 为了更直观,自动反色
const autoInvert = async () => {
const invertPromises = newItems.map(item => invertImage(item));
await Promise.all(invertPromises);
renderGallery();
};
autoInvert();
}
};
reader.readAsDataURL(file);
});
// 清空 input 以便再次选择同一文件
fileInput.value = '';
}
// ---- 清空 ----
function clearAll() {
if (imageItems.length === 0) return;
if (confirm('确定清空所有图片吗?')) {
imageItems = [];
nextId = 0;
renderGallery();
}
}
// ---- 下载全部 (ZIP) ----
async function downloadAllAsZip() {
if (imageItems.length === 0) {
alert('没有图片可下载');
return;
}
// 动态加载 JSZip
let JSZip;
try {
const module = await import('https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js');
JSZip = module.default;
} catch (e) {
// fallback: 使用 script 加载
await new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js';
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
JSZip = window.JSZip;
}
const zip = new JSZip();
// 确保所有图片都有反色结果 (如果没有则先生成)
const needInvert = imageItems.filter(item => !item.invertedDataUrl);
if (needInvert.length > 0) {
processBtn.disabled = true;
processBtn.textContent = '⏳ 预处理...';
await Promise.all(needInvert.map(item => invertImage(item)));
processBtn.disabled = false;
processBtn.textContent = '✨ 全部反色';
renderGallery();
}
// 添加文件到 zip
imageItems.forEach(item => {
const dataUrl = item.invertedDataUrl || item.originalDataUrl;
if (!dataUrl) return;
// 将 dataURL 转为 blob
const byteString = atob(dataUrl.split(',')[1]);
const mimeString = dataUrl.split(',')[0].split(':')[1].split(';')[0];
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
const blob = new Blob([ab], { type: mimeString });
const baseName = item.file.name.replace(/\.[^.]+$/, '');
const ext = '.png'; // 统一用 png 保持质量
zip.file(`${baseName}_负片${ext}`, blob);
});
const content = await zip.generateAsync({ type: 'blob' });
const link = document.createElement('a');
link.href = URL.createObjectURL(content);
link.download = `负片批量_${Date.now()}.zip`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(link.href);
}
// ---- 事件绑定 ----
// 点击上传区
dropZone.addEventListener('click', () => fileInput.click());
// 文件选择
fileInput.addEventListener('change', (e) => {
addFiles(e.target.files);
});
// 拖拽上传
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('dragover');
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('dragover');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('dragover');
const files = e.dataTransfer.files;
if (files.length > 0) {
addFiles(files);
}
});
// 按钮
processBtn.addEventListener('click', batchInvert);
clearBtn.addEventListener('click', clearAll);
downloadAllBtn.addEventListener('click', downloadAllAsZip);
// 额外:拖拽到页面其他位置阻止默认行为
document.addEventListener('dragover', (e) => e.preventDefault());
document.addEventListener('drop', (e) => e.preventDefault());
// 初始渲染
renderGallery();
})();
</script>
</body>
</html>
批量图片反色工具
您可以通过拖拽或点击上传多张图片,一键生成负片效果,并支持批量下载。
- 图片上传与预览:您可以通过点击上传区域或直接拖拽图片文件到指定区域,工具会自动读取并显示图片,支持的图片格式包括 JPG、PNG、WebP 等常见格式。
- 核心反色功能:点击“全部反色”按钮,工具会使用 Canvas 技术对所有图片的像素值进行
255 - 原始值的计算,快速生成标准的负片效果,每张图片的处理是独立的,您可以随时对新增图片单独应用此功能。 - 灵活的下载选项:您可以点击单张图片下方的“负片”按钮或直接点击图片本身,下载该图片的反色版本,如需一次获取所有处理后的图片,点击“下载全部”按钮,工具会将所有负片图片打包成一个 ZIP 文件,方便您保存和分享。
标签: 负片效果
版权声明:除非特别标注,否则均为本站原创文章,转载时请以链接形式注明文章出处。