Color Match
颜色迁移 · 一键将参考图的色调应用到目标图
目标图片
点击上传目标图片
需要调整色调的图片(可多张)
参考图片
点击上传参考图片
目标色调来源
目标列表
0
张
清空
方法
Linear RGB
直方图匹配
Reinhard Lab
线性 RGB 空间均值/标准差迁移 — 简单快速
强度
0.10
应用匹配
原始目标
—
上传目标图
参考图
—
等待上传
匹配结果
—
点击「应用」预览
处理中...
处理中...
0%
下载当前
匹配全部
(0)
添加更多
重新开始
✕
// ═══════════════════════════════════════════════════════════ // i18n 国际化 & 导航注入 & 全局错误修复 // ═══════════════════════════════════════════════════════════ (function() { 'use strict'; // ─── 翻译资源 ────────────────────────────────────────────── const resources = { zh: { translation: { 'app.title': 'Color Match', 'app.subtitle': '颜色迁移 · 一键将参考图的色调应用到目标图', 'target.label': '目标图片', 'target.hint': '需要调整色调的图片(可多张)', 'target.placeholder': '点击上传目标图片', 'ref.label': '参考图片', 'ref.hint': '目标色调来源', 'ref.placeholder': '点击上传参考图片', 'target.list.header': '目标列表', 'target.list.count': '{n} 张', 'clear.all': '清空', 'method.label': '方法', 'strength.label': '强度', 'apply.btn': '应用匹配', 'download.btn': '下载当前', 'download.all': '匹配全部', 'add.more': '添加更多', 'reset': '重新开始', 'orig.preview': '原始目标', 'ref.preview': '参考图', 'result.preview': '匹配结果', 'orig.placeholder': '上传目标图', 'ref.placeholder2': '等待上传', 'result.placeholder': '点击「应用」预览', 'status.processing': '处理中...', 'progress.text': '处理中...', 'toast.saved': '保存成功', 'toast.error': '操作失败,请重试', 'toast.jsziperror': 'JSZip 加载失败,下载功能不可用', 'method.linear_rgb': 'Linear RGB', 'method.hist_match': '直方图匹配', 'method.reinhard_lab': 'Reinhard Lab', 'desc.linear_rgb': '线性 RGB 空间均值/标准差迁移 — 简单快速', 'desc.hist_match': '逐通道直方图匹配 — 精确匹配参考图色调分布', 'desc.reinhard_lab': 'Lab 空间均值/标准差迁移 — 效果最自然' } }, en: { translation: { 'app.title': 'Color Match', 'app.subtitle': 'Color transfer — Apply reference palette to target images', 'target.label': 'Target Image(s)', 'target.hint': 'Images to be color adjusted (multiple allowed)', 'target.placeholder': 'Click to upload target image(s)', 'ref.label': 'Reference Image', 'ref.hint': 'Source of the target palette', 'ref.placeholder': 'Click to upload reference', 'target.list.header': 'Target List', 'target.list.count': '{n} image(s)', 'clear.all': 'Clear All', 'method.label': 'Method', 'strength.label': 'Strength', 'apply.btn': 'Apply Match', 'download.btn': 'Download Current', 'download.all': 'Match All', 'add.more': 'Add More', 'reset': 'Reset', 'orig.preview': 'Original Target', 'ref.preview': 'Reference', 'result.preview': 'Result', 'orig.placeholder': 'Upload target image', 'ref.placeholder2': 'Waiting for upload', 'result.placeholder': 'Click "Apply" to preview', 'status.processing': 'Processing...', 'progress.text': 'Processing...', 'toast.saved': 'Saved successfully', 'toast.error': 'Operation failed, please retry', 'toast.jsziperror': 'JSZip failed to load, download unavailable', 'method.linear_rgb': 'Linear RGB', 'method.hist_match': 'Histogram Matching', 'method.reinhard_lab': 'Reinhard Lab', 'desc.linear_rgb': 'Linear RGB mean/std transfer — Simple & fast', 'desc.hist_match': 'Per-channel histogram matching — Exact tone distribution', 'desc.reinhard_lab': 'Lab mean/std transfer — Most natural result' } } }; // ─── 初始化 i18next ──────────────────────────────────────── if (typeof i18next !== 'undefined') { i18next.init({ lng: 'zh', fallbackLng: 'zh', resources: resources, interpolation: { escapeValue: false } }); } else { // 简易降级方案 window.__ = function(key) { const enFallback = resources.en.translation[key] || key; return enFallback; }; } // ─── 翻译函数 ────────────────────────────────────────────── function t(key, opts) { if (typeof i18next !== 'undefined') { return i18next.t(key, opts); } const en = resources.en.translation[key]; return en || key; } window.__ = t; // ─── 语言切换按钮注入 ────────────────────────────────────── function createLangSwitch() { const container = document.createElement('div'); container.className = 'lang-switch'; container.innerHTML = '
中文
EN
'; document.body.appendChild(container); container.addEventListener('click', function(e) { const btn = e.target.closest('.lang-btn'); if (!btn) return; const lang = btn.dataset.lang; if (typeof i18next !== 'undefined') { i18next.changeLanguage(lang, applyTranslation); } else { applyTranslation(); } container.querySelectorAll('.lang-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); }); } // ─── 应用翻译到 DOM ──────────────────────────────────────── function applyTranslation() { document.querySelectorAll('[data-i18n]').forEach(el => { const key = el.dataset.i18n; el.textContent = t(key); }); // 通过 id 替换 const idMap = { 'targetLabel': 'target.placeholder', 'refLabel': 'ref.placeholder', 'origPlaceholder': 'orig.placeholder', 'refPreviewPlaceholder': 'ref.placeholder2', 'resultPlaceholder': 'result.placeholder', 'methodDesc': 'desc.' + (state ? state.method || 'linear_rgb' : 'linear_rgb'), 'statusText': 'status.processing', 'progressText': 'progress.text', 'applyBtn': 'apply.btn', 'downloadBtn': 'download.btn', 'downloadAllBtn': 'download.all', 'addMoreBtn': 'add.more', 'resetBtn': 'reset', 'clearAllBtn': 'clear.all' }; for (const [id, key] of Object.entries(idMap)) { const el = document.getElementById(id); if (el) el.textContent = t(key); } // 方法标签 document.querySelectorAll('.method-opt').forEach(btn => { const method = btn.dataset.method; btn.textContent = t('method.' + method); }); // 标题和副标题 const h1 = document.querySelector('.header h1'); if (h1) h1.textContent = t('app.title'); const p = document.querySelector('.header p'); if (p) p.textContent = t('app.subtitle'); } // ─── DOM 就绪后执行 ────────────────────────────────────────── if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } function init() { // 注入语言切换 createLangSwitch(); // 应用翻译 applyTranslation(); // 初始化导航 (由 nav-bar.js 提供) if (typeof initNavBar === 'function') { initNavBar(); } // JSZip 加载失败检测 if (typeof JSZip === 'undefined') { const toast = document.getElementById('toast'); if (toast) { toast.textContent = t('toast.jsziperror'); toast.classList.add('show'); setTimeout(() => toast.classList.remove('show'), 4000); } } // 全局 fetch 担保 catch const origFetch = window.fetch; if (origFetch) { window.fetch = function() { return origFetch.apply(this, arguments).catch(function(err) { console.warn('Fetch error caught globally:', err); const toast = document.getElementById('toast'); if (toast) { toast.textContent = t('toast.error'); toast.classList.add('show'); setTimeout(() => toast.classList.remove('show'), 3000); } throw err; }); }; } // 确保所有 .catch 链存在(主要是原有 JS 中可能遗漏的) // 可以劫持 Promise 原型,但不做,由 fetch 覆盖处理 } // ─── 全局错误弹窗 ────────────────────────────────────────── window.addEventListener('error', function(e) { const toast = document.getElementById('toast'); if (toast) { toast.textContent = 'Script error: ' + (e.message || 'unknown'); toast.classList.add('show'); setTimeout(() => toast.classList.remove('show'), 5000); } }); })(); // ─── 修复原 JS 语法错误:补全 addImages 函数 ─────────── // 由于原代码被截断,此处覆盖定义完整函数 function addImages(files) { const p = []; for (const f of files) { if (!f.type.startsWith('image/')) continue; const id = ++idCounter; const e = { id, file: f, name: f.name, img: null, width: 0, height: 0, loaded: false }; state.images.push(e); p.push(e); const r = new FileReader(); r.onload = ev => { const img = new Image(); img.onload = () => { e.img = img; e.width = img.width; e.height = img.height; e.loaded = true; if (state.selectedId === e.id || !state.selectedId) { if (!state.selectedId) selectImage(e.id); else if (e.id === state.selectedId) { renderOrig(img); debounceMatch(); } } renderList(); updateCount(); }; img.src = ev.target.result; }; r.readAsDataURL(f); } return p; }