<!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)),以改变预览卡片的大小。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播