图片批量双色调工具怀旧感

联启 电脑工具 16
<!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>
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }
        body {
            background: #1e1a17;
            font-family: 'Segoe UI', Roboto, system-ui, -apple-system, sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            margin: 0;
            padding: 20px;
            color: #e8ddd0;
        }
        .app-container {
            max-width: 1200px;
            width: 100%;
            background: #2c2622;
            border-radius: 32px;
            padding: 28px 30px 35px;
            box-shadow: 0 20px 40px rgba(0, 0, 0, 0.7), inset 0 1px 2px rgba(255, 215, 180, 0.08);
            border: 1px solid #4d3d32;
        }
        h1 {
            display: flex;
            align-items: center;
            gap: 12px;
            font-weight: 400;
            font-size: 1.9rem;
            letter-spacing: 1px;
            color: #dac5b0;
            border-bottom: 1px solid #4f3f34;
            padding-bottom: 16px;
            margin-bottom: 24px;
        }
        h1 span {
            background: #7a5f4c;
            color: #efe1d2;
            font-size: 0.9rem;
            font-weight: 500;
            padding: 4px 14px;
            border-radius: 40px;
            letter-spacing: 0.3px;
            margin-left: 8px;
        }
        .control-panel {
            background: #362e28;
            border-radius: 24px;
            padding: 20px 24px;
            margin-bottom: 28px;
            display: flex;
            flex-wrap: wrap;
            align-items: center;
            gap: 18px 30px;
            border: 1px solid #5a463a;
            box-shadow: inset 0 2px 3px rgba(0,0,0,0.3);
        }
        .file-upload {
            display: flex;
            align-items: center;
            gap: 12px;
            flex-wrap: wrap;
        }
        .file-upload label {
            background: #6b4f3d;
            padding: 10px 20px;
            border-radius: 40px;
            color: #f7efe9;
            font-weight: 500;
            cursor: pointer;
            transition: 0.2s;
            border: 1px solid #9a7b66;
            letter-spacing: 0.3px;
            display: inline-flex;
            align-items: center;
            gap: 8px;
        }
        .file-upload label:hover {
            background: #8a6b55;
            border-color: #be9b83;
            transform: scale(1.01);
        }
        .file-upload input[type="file"] {
            display: none;
        }
        .file-upload .file-hint {
            color: #bba693;
            font-size: 0.9rem;
        }
        .color-inputs {
            display: flex;
            flex-wrap: wrap;
            align-items: center;
            gap: 12px 24px;
            background: #271f1b;
            padding: 8px 18px 8px 14px;
            border-radius: 48px;
            border: 1px solid #5d4a3d;
        }
        .color-pair {
            display: flex;
            align-items: center;
            gap: 8px;
        }
        .color-pair label {
            font-size: 0.85rem;
            color: #d0bcab;
        }
        .color-pair input[type="color"] {
            width: 36px;
            height: 36px;
            border: 2px solid #7d6555;
            border-radius: 50%;
            background: transparent;
            cursor: pointer;
            padding: 2px;
        }
        .color-pair input[type="color"]::-webkit-color-swatch-wrapper {
            padding: 0;
        }
        .color-pair input[type="color"]::-webkit-color-swatch {
            border: none;
            border-radius: 50%;
        }
        .preset-zone {
            display: flex;
            align-items: center;
            gap: 6px;
        }
        .preset-btn {
            background: #4d3a30;
            border: none;
            color: #e5d4c4;
            padding: 5px 14px;
            border-radius: 30px;
            font-size: 0.75rem;
            font-weight: 500;
            letter-spacing: 0.4px;
            cursor: pointer;
            border: 1px solid #6f594a;
            transition: 0.15s;
        }
        .preset-btn:hover {
            background: #6e5543;
            border-color: #a6836c;
            color: #fff6ed;
        }
        .preset-btn.active-preset {
            background: #9e7a62;
            border-color: #d6b49b;
            color: #fffaf4;
            box-shadow: 0 0 0 1px #c8a68d;
        }
        .action-buttons {
            display: flex;
            flex-wrap: wrap;
            align-items: center;
            gap: 10px;
            margin-left: auto;
        }
        .action-btn {
            background: #5b473b;
            border: 1px solid #826b5a;
            color: #ede0d4;
            padding: 9px 22px;
            border-radius: 40px;
            font-weight: 500;
            font-size: 0.9rem;
            cursor: pointer;
            transition: 0.15s;
            display: inline-flex;
            align-items: center;
            gap: 6px;
        }
        .action-btn:hover {
            background: #7b6453;
            border-color: #b6967e;
            color: #fffcf7;
        }
        .action-btn:active {
            transform: scale(0.96);
        }
        .action-btn.download-btn {
            background: #6f543f;
            border-color: #b18568;
        }
        .action-btn.download-btn:hover {
            background: #9a7a61;
        }
        .action-btn.clear-btn {
            background: #3f3129;
            border-color: #5f4b3d;
        }
        .action-btn.clear-btn:hover {
            background: #5d4a3c;
        }
        .gallery {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
            gap: 18px;
            margin: 28px 0 12px;
            min-height: 120px;
        }
        .card {
            background: #1f1a16;
            border-radius: 20px;
            overflow: hidden;
            box-shadow: 0 8px 14px rgba(0,0,0,0.6);
            border: 1px solid #4f3f34;
            transition: 0.2s;
            display: flex;
            flex-direction: column;
        }
        .card:hover {
            transform: translateY(-3px);
            border-color: #8d735f;
        }
        .card canvas {
            width: 100%;
            aspect-ratio: 1/1;
            display: block;
            background: #2e2722;
            object-fit: cover;
        }
        .card .card-footer {
            padding: 10px 12px;
            background: #2b231f;
            display: flex;
            justify-content: space-between;
            align-items: center;
            font-size: 0.75rem;
            color: #b8a392;
            border-top: 1px solid #3f332b;
        }
        .card .card-footer .filename {
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
            max-width: 100px;
        }
        .card .card-footer .remove-btn {
            background: none;
            border: none;
            color: #b28d78;
            font-size: 1.2rem;
            cursor: pointer;
            padding: 0 6px;
            line-height: 1;
            transition: 0.1s;
        }
        .card .card-footer .remove-btn:hover {
            color: #f3b59a;
            transform: scale(1.2);
        }
        .empty-gallery {
            grid-column: 1 / -1;
            text-align: center;
            color: #786559;
            padding: 40px 0;
            font-size: 1.1rem;
            letter-spacing: 0.3px;
            background: #23201c;
            border-radius: 30px;
            border: 1px dashed #5c4c40;
        }
        .footer-info {
            display: flex;
            justify-content: space-between;
            flex-wrap: wrap;
            margin-top: 20px;
            color: #8d7b6d;
            font-size: 0.85rem;
            border-top: 1px solid #3f352e;
            padding-top: 18px;
        }
        .badge {
            background: #3a2e27;
            padding: 2px 14px;
            border-radius: 30px;
            color: #c7b3a2;
        }
        @media (max-width: 700px) {
            .app-container { padding: 16px; }
            .control-panel { flex-direction: column; align-items: stretch; gap: 14px; }
            .action-buttons { margin-left: 0; justify-content: flex-end; }
            .color-inputs { justify-content: center; }
        }
    </style>
</head>
<body>
<div class="app-container">
    <h1>
        🎞️ 双色调·怀旧
        <span>批量</span>
    </h1>
    <div class="control-panel">
        <div class="file-upload">
            <label for="imageInput">📁 选择图片</label>
            <input type="file" id="imageInput" accept="image/*" multiple>
            <span class="file-hint">支持多选</span>
        </div>
        <div class="color-inputs">
            <div class="color-pair">
                <label for="color1">深色</label>
                <input type="color" id="color1" value="#3b2b23">
            </div>
            <div class="color-pair">
                <label for="color2">浅色</label>
                <input type="color" id="color2" value="#e9d6c0">
            </div>
        </div>
        <div class="preset-zone">
            <span style="color:#a08b7b; font-size:0.8rem; margin-right:4px;">🎨</span>
            <button class="preset-btn" data-h="sepia" data-c1="#3b2b23" data-c2="#e9d6c0">复古棕</button>
            <button class="preset-btn" data-h="vintage" data-c1="#2c2a3b" data-c2="#cbba9a">旧报纸</button>
            <button class="preset-btn" data-h="cyan" data-c1="#153b42" data-c2="#d9cdb3">青调</button>
            <button class="preset-btn" data-h="warm" data-c1="#5a3828" data-c2="#f2dbb8">暖阳</button>
        </div>
        <div class="action-buttons">
            <button class="action-btn download-btn" id="downloadAllBtn">⬇️ 全部下载</button>
            <button class="action-btn clear-btn" id="clearBtn">✕ 清空</button>
        </div>
    </div>
    <div class="gallery" id="galleryContainer">
        <div class="empty-gallery">⬆ 选择图片,开始怀旧双色调</div>
    </div>
    <div class="footer-info">
        <span>🕯️ 双色调 · 怀旧感</span>
        <span class="badge" id="imageCount">0 张图片</span>
    </div>
</div>
<script>
    (function() {
        // DOM 元素
        const input = document.getElementById('imageInput');
        const gallery = document.getElementById('galleryContainer');
        const color1Input = document.getElementById('color1');
        const color2Input = document.getElementById('color2');
        const downloadAllBtn = document.getElementById('downloadAllBtn');
        const clearBtn = document.getElementById('clearBtn');
        const imageCountSpan = document.getElementById('imageCount');
        // 存储图片数据 { id, file, dataUrl, canvas }
        let imageItems = [];
        let idCounter = 0;
        // 预设按钮
        const presetBtns = document.querySelectorAll('.preset-btn');
        presetBtns.forEach(btn => {
            btn.addEventListener('click', function(e) {
                const c1 = this.dataset.c1;
                const c2 = this.dataset.c2;
                if (c1) color1Input.value = c1;
                if (c2) color2Input.value = c2;
                // 高亮当前预设
                presetBtns.forEach(b => b.classList.remove('active-preset'));
                this.classList.add('active-preset');
                // 重新渲染所有图片
                rerenderAll();
            });
        });
        // 颜色变化时重新渲染
        color1Input.addEventListener('input', rerenderAll);
        color2Input.addEventListener('input', rerenderAll);
        // 移除预设高亮(手动改颜色)
        color1Input.addEventListener('change', () => presetBtns.forEach(b => b.classList.remove('active-preset')));
        color2Input.addEventListener('change', () => presetBtns.forEach(b => b.classList.remove('active-preset')));
        // 清空
        clearBtn.addEventListener('click', function() {
            imageItems = [];
            renderGallery();
        });
        // 下载全部
        downloadAllBtn.addEventListener('click', function() {
            if (imageItems.length === 0) {
                alert('没有图片可以下载');
                return;
            }
            imageItems.forEach((item, index) => {
                const canvas = item.canvas;
                if (!canvas) return;
                const link = document.createElement('a');
                link.download = `duotone_${item.filename || `image_${index+1}`}.png`;
                link.href = canvas.toDataURL('image/png');
                link.click();
            });
        });
        // 文件选择
        input.addEventListener('change', function(e) {
            const files = e.target.files;
            if (!files || files.length === 0) return;
            for (let i = 0; i < files.length; i++) {
                const file = files[i];
                if (!file.type.startsWith('image/')) continue;
                const reader = new FileReader();
                reader.onload = (function(f) {
                    return function(ev) {
                        const dataUrl = ev.target.result;
                        addImageItem(dataUrl, f.name);
                    };
                })(file);
                reader.readAsDataURL(file);
            }
            // 清空 input 以便再次选择相同文件
            input.value = '';
        });
        // 添加图片项
        function addImageItem(dataUrl, filename) {
            const img = new Image();
            img.onload = function() {
                const canvas = document.createElement('canvas');
                // 保持比例,最大边长 640 (保持性能)
                let w = img.naturalWidth;
                let h = img.naturalHeight;
                const maxDim = 640;
                if (w > maxDim || h > maxDim) {
                    const ratio = Math.min(maxDim / w, maxDim / h);
                    w = Math.round(w * ratio);
                    h = Math.round(h * ratio);
                }
                canvas.width = w;
                canvas.height = h;
                const ctx = canvas.getContext('2d');
                ctx.drawImage(img, 0, 0, w, h);
                // 应用双色调
                applyDuotoneToCanvas(canvas, color1Input.value, color2Input.value);
                const item = {
                    id: idCounter++,
                    filename: filename || 'image',
                    dataUrl: dataUrl,
                    canvas: canvas,
                };
                imageItems.push(item);
                renderGallery();
            };
            img.src = dataUrl;
        }
        // 对canvas应用双色调 (保留透明区域?默认透明背景处理为白色)
        function applyDuotoneToCanvas(canvas, hex1, hex2) {
            const ctx = canvas.getContext('2d');
            const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
            const data = imageData.data;
            const c1 = hexToRgb(hex1);
            const c2 = hexToRgb(hex2);
            for (let i = 0; i < data.length; i += 4) {
                const r = data[i];
                const g = data[i + 1];
                const b = data[i + 2];
                const alpha = data[i + 3];
                // 灰度 ( luminosity )
                const gray = 0.299 * r + 0.587 * g + 0.114 * b;
                // 归一化 0-1
                const t = Math.min(1, Math.max(0, gray / 255));
                // 双色调插值
                const newR = Math.round(c1.r + (c2.r - c1.r) * t);
                const newG = Math.round(c1.g + (c2.g - c1.g) * t);
                const newB = Math.round(c1.b + (c2.b - c1.b) * t);
                data[i] = newR;
                data[i + 1] = newG;
                data[i + 2] = newB;
                // 保留原始alpha (但白色背景更好)
                if (alpha === 0) {
                    // 完全透明 -> 设置为白色 (或浅色) 避免全透明
                    data[i] = c2.r;
                    data[i+1] = c2.g;
                    data[i+2] = c2.b;
                    data[i+3] = 255;
                }
            }
            ctx.putImageData(imageData, 0, 0);
        }
        // 重新渲染全部图片 (从存储的原始canvas重新应用双色调)
        function rerenderAll() {
            const c1 = color1Input.value;
            const c2 = color2Input.value;
            imageItems.forEach(item => {
                if (!item.canvas) return;
                // 重新从原始图片绘制? 直接基于当前canvas再次应用双色调会叠加,需要从原始dataUrl重绘
                // 但是canvas已经被修改,所以我们用存储的dataUrl重新生成canvas
                const img = new Image();
                img.onload = function() {
                    const canvas = document.createElement('canvas');
                    let w = img.naturalWidth;
                    let h = img.naturalHeight;
                    const maxDim = 640;
                    if (w > maxDim || h > maxDim) {
                        const ratio = Math.min(maxDim / w, maxDim / h);
                        w = Math.round(w * ratio);
                        h = Math.round(h * ratio);
                    }
                    canvas.width = w;
                    canvas.height = h;
                    const ctx = canvas.getContext('2d');
                    ctx.drawImage(img, 0, 0, w, h);
                    // 双色调
                    applyDuotoneToCanvas(canvas, c1, c2);
                    item.canvas = canvas;
                    // 更新DOM中对应的canvas
                    const card = document.querySelector(`.card[data-id="${item.id}"]`);
                    if (card) {
                        const canvasEl = card.querySelector('canvas');
                        if (canvasEl) {
                            const ctx2 = canvasEl.getContext('2d');
                            canvasEl.width = canvas.width;
                            canvasEl.height = canvas.height;
                            ctx2.drawImage(canvas, 0, 0);
                        }
                    }
                };
                img.src = item.dataUrl;
            });
            // gallery 已经渲染,等待图片加载完成后更新 但简单处理:直接重新渲染整个gallery
            // 但为了流畅,延迟一点点重新渲染
            setTimeout(() => {
                renderGallery();
            }, 50);
        }
        // 渲染画廊
        function renderGallery() {
            if (imageItems.length === 0) {
                gallery.innerHTML = `<div class="empty-gallery">⬆ 选择图片,开始怀旧双色调</div>`;
                imageCountSpan.textContent = '0 张图片';
                return;
            }
            let html = '';
            imageItems.forEach(item => {
                const canvas = item.canvas;
                let dataUrl = '';
                if (canvas) {
                    dataUrl = canvas.toDataURL('image/png');
                }
                html += `
                    <div class="card" data-id="${item.id}">
                        <canvas width="${canvas?.width || 200}" height="${canvas?.height || 200}" style="background: #2e2722;"></canvas>
                        <div class="card-footer">
                            <span class="filename" title="${item.filename}">${item.filename}</span>
                            <button class="remove-btn" data-id="${item.id}" title="移除">✕</button>
                        </div>
                    </div>
                `;
            });
            gallery.innerHTML = html;
            // 渲染canvas图像 (因innerHTML canvas不会自动绘制)
            imageItems.forEach(item => {
                const card = gallery.querySelector(`.card[data-id="${item.id}"]`);
                if (!card) return;
                const canvasEl = card.querySelector('canvas');
                if (canvasEl && item.canvas) {
                    const ctx = canvasEl.getContext('2d');
                    canvasEl.width = item.canvas.width;
                    canvasEl.height = item.canvas.height;
                    ctx.drawImage(item.canvas, 0, 0);
                }
            });
            // 绑定移除按钮
            gallery.querySelectorAll('.remove-btn').forEach(btn => {
                btn.addEventListener('click', function(e) {
                    const id = parseInt(this.dataset.id);
                    imageItems = imageItems.filter(item => item.id !== id);
                    renderGallery();
                });
            });
            imageCountSpan.textContent = `${imageItems.length} 张图片`;
        }
        // 工具: hex to rgb
        function hexToRgb(hex) {
            let h = hex.replace('#', '');
            if (h.length === 3) h = h.split('').map(c => c + c).join('');
            const num = parseInt(h, 16);
            return {
                r: (num >> 16) & 255,
                g: (num >> 8) & 255,
                b: num & 255,
            };
        }
        // 初始化:若有预设高亮 (默认复古棕)
        document.querySelector('.preset-btn[data-h="sepia"]')?.classList.add('active-preset');
        // 默认渲染空状态
        renderGallery();
    })();
</script>
</body>
</html>

工具功能与操作流程

您可以通过简单的几步,为图片添加上世纪风格的复古色调,其核心是让您自由控制双色调的深浅颜色,并支持批量处理。

图片批量双色调工具怀旧感-第1张图片-电脑手机工具软件下载 - 免费实用工具合集 | 联启科技

  • 图片导入与批量处理:点击“选择图片”按钮,您可以从本地一次性选取多张图片,所有图片会以网格形式展示在界面中,方便您预览和管理。
  • 双色调颜色自定义:您可以通过“深色”和“浅色”颜色选择器,自由搭配双色调的两种主色,工具还预设了“复古棕”、“旧报纸”等几种怀旧风格,点击即可快速应用。
  • 效果预览与输出:每张图片的缩略图会实时显示应用双色调后的效果,您可以通过每张图片右下角的“✕”按钮移除单独图片,或使用“全部下载”按钮将所有处理后的图片保存为PNG格式。

标签: 双色调

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