我速度太快了,它突然穿出来的,我甚至不知道什么颜色的,伤口火辣辣的 13 个帖子 - 9 位参与者 阅读完整话题
证件照换底(支持自定义颜色代码) <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>证件照换底 - 离线隐私处理</title> <style> *{margin:0;padding:0;box-sizing:border-box;} body{ font-family: -apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif; background:#f0f2f5;min-height:100vh;color:#1a1a2e;line-height:1.6; } .header{ background:#fff;border-bottom:1px solid #e0e0e0;padding:14px 0; position:sticky;top:0;z-index:100;box-shadow:0 1px 3px rgba(0,0,0,0.06); } .header-inner{max-width:1200px;margin:0 auto;padding:0 24px;display:flex;align-items:center;justify-content:space-between;} .logo{display:flex;align-items:center;gap:10px;font-size:1.25rem;font-weight:700;} .logo-icon{width:38px;height:38px;border-radius:11px;background:linear-gradient(135deg,#4f6ef7,#7b8ff7);display:flex;align-items:center;justify-content:center;font-size:1.3rem;color:#fff;} .privacy-badge{display:flex;align-items:center;gap:6px;font-size:0.8rem;color:#27ae60;background:#eafaf1;padding:5px 12px;border-radius:20px;font-weight:600;} .privacy-dot{width:7px;height:7px;border-radius:50%;background:#27ae60;animation:pulse 2s infinite;} @keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}} .main-container{max-width:1200px;margin:0 auto;padding:24px;display:grid;grid-template-columns:1fr 1fr;gap:24px;} @media(max-width:900px){.main-container{grid-template-columns:1fr;max-width:520px;}} .card{background:#fff;border-radius:16px;padding:22px;box-shadow:0 4px 24px rgba(0,0,0,0.08);border:1px solid #e0e0e0;} .card-title{font-size:1rem;font-weight:700;margin-bottom:16px;} .upload-zone{border:2px dashed #d0d5e0;border-radius:10px;padding:36px 20px;text-align:center;cursor:pointer;background:#fafbfc;position:relative;min-height:180px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;transition:0.25s;} .upload-zone:hover,.upload-zone.drag-over{border-color:#4f6ef7;background:#eef1ff;} .upload-zone.has-image{border-style:solid;border-color:#e0e0e0;padding:6px;min-height:auto;} .upload-icon{font-size:2.8rem;opacity:0.5;} .upload-text{font-size:0.9rem;color:#555;} .upload-hint{font-size:0.75rem;color:#999;} .upload-zone input[type="file"]{position:absolute;inset:0;opacity:0;cursor:pointer;} .preview-image{width:100%;max-height:350px;object-fit:contain;display:block;border-radius:5px;} .image-container{position:relative;display:block;width:100%;} .image-actions-overlay{position:absolute;top:8px;right:8px;z-index:5;} .btn-icon-sm{width:32px;height:32px;border-radius:50%;border:none;background:rgba(0,0,0,0.5);color:#fff;cursor:pointer;font-size:0.85rem;display:flex;align-items:center;justify-content:center;} .btn{display:inline-flex;align-items:center;justify-content:center;gap:5px;padding:9px 18px;border-radius:25px;font-size:0.88rem;font-weight:600;cursor:pointer;border:none;transition:0.25s;white-space:nowrap;} .btn:active{transform:scale(0.96);} .btn-primary{background:#4f6ef7;color:#fff;} .btn-primary:hover{background:#3b54db;} .btn-outline{background:#fff;color:#4f6ef7;border:2px solid #4f6ef7;} .btn-outline:hover{background:#eef1ff;} .btn-success{background:#27ae60;color:#fff;} .btn-success:hover{opacity:0.9;} .btn-lg{padding:11px 26px;font-size:0.95rem;border-radius:28px;} .btn:disabled{opacity:0.5;cursor:not-allowed;pointer-events:none;} .btn-block{width:100%;} .section-label{font-size:0.82rem;font-weight:600;margin:14px 0 8px;} .color-presets{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:8px;} .color-preset{width:40px;height:40px;border-radius:50%;cursor:pointer;border:3px solid transparent;transition:0.25s;box-shadow:0 1px 3px rgba(0,0,0,0.1);position:relative;} .color-preset:hover{transform:scale(1.12);} .color-preset.active{border-color:#1a1a2e;box-shadow:0 0 0 4px rgba(0,0,0,0.08);} .color-preset.active::after{content:'✓';position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:bold;font-size:0.9rem;text-shadow:0 1px 2px rgba(0,0,0,0.5);} .custom-color-section{background:#f8f9fb;border-radius:12px;padding:12px 14px;margin-bottom:4px;border:1px solid #e8eaef;} .custom-color-row{display:flex;gap:10px;align-items:center;flex-wrap:wrap;} .custom-color-row input[type="color"]{width:40px;height:40px;border-radius:50%;border:2px solid #e0e0e0;cursor:pointer;padding:2px;flex-shrink:0;} .custom-color-row input[type="color"]::-webkit-color-swatch-wrapper{padding:0;} .custom-color-row input[type="color"]::-webkit-color-swatch{border-radius:50%;border:none;} .hex-input-wrapper{position:relative;flex:1;min-width:110px;} .hex-input-wrapper .hash{position:absolute;left:10px;top:50%;transform:translateY(-50%);color:#999;font-weight:600;font-size:0.85rem;pointer-events:none;} .hex-input{width:100%;padding:8px 10px 8px 24px;border:2px solid #e0e5ec;border-radius:10px;font-size:0.85rem;font-family:monospace;font-weight:600;color:#1a1a2e;background:#fff;outline:none;transition:0.25s;} .hex-input:focus{border-color:#4f6ef7;box-shadow:0 0 0 3px rgba(79,110,247,0.1);} .hex-input.valid{border-color:#27ae60;background:#f8fdf8;} .hex-input.invalid{border-color:#e74c3c;background:#fef8f8;} .color-preview-dot{width:30px;height:30px;border-radius:50%;border:2px solid #ddd;flex-shrink:0;box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);} .control-group{margin-bottom:12px;} .control-label{display:flex;justify-content:space-between;align-items:center;font-size:0.82rem;font-weight:600;margin-bottom:5px;} .control-value{font-size:0.75rem;color:#555;background:#f5f6f8;padding:2px 10px;border-radius:12px;} input[type="range"]{-webkit-appearance:none;width:100%;height:5px;border-radius:3px;background:#e0e5ec;outline:none;cursor:pointer;} input[type="range"]::-webkit-slider-thumb{-webkit-appearance:none;width:20px;height:20px;border-radius:50%;background:#4f6ef7;cursor:pointer;} .size-presets{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px;} .size-preset{padding:10px 12px;border-radius:8px;border:2px solid #e0e0e0;cursor:pointer;text-align:center;font-size:0.82rem;font-weight:600;transition:0.25s;background:#fafbfc;} .size-preset:hover{border-color:#4f6ef7;background:#eef1ff;} .size-preset.active{border-color:#4f6ef7;background:#eef1ff;color:#4f6ef7;} .size-preset .size-dims{font-size:0.7rem;color:#555;font-weight:400;} .result-container{text-align:center;} .result-image{max-width:100%;max-height:350px;border-radius:10px;box-shadow:0 4px 24px rgba(0,0,0,0.08);border:1px solid #e0e0e0;} .result-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:220px;color:#bbb;gap:10px;} .result-placeholder .icon{font-size:3.5rem;} .action-row{display:flex;gap:10px;flex-wrap:wrap;margin-top:14px;} .toast{position:fixed;bottom:30px;left:50%;transform:translateX(-50%) translateY(100px);background:#1a1a2e;color:#fff;padding:10px 22px;border-radius:25px;font-size:0.85rem;font-weight:600;z-index:999;opacity:0;transition:all 0.35s;pointer-events:none;box-shadow:0 8px 30px rgba(0,0,0,0.25);} .toast.show{opacity:1;transform:translateX(-50%) translateY(0);} .toast.success{background:#27ae60;} .toast.error{background:#e74c3c;} .spinner{display:inline-block;width:16px;height:16px;border:2px solid rgba(255,255,255,0.3);border-top-color:#fff;border-radius:50%;animation:spin 0.7s linear infinite;} @keyframes spin{to{transform:rotate(360deg);}} .footer{text-align:center;padding:18px;font-size:0.75rem;color:#aaa;} </style> </head> <body> <header class="header"> <div class="header-inner"> <div class="logo"><div class="logo-icon">📸</div><span>证件照换底</span></div> <div class="privacy-badge"><span class="privacy-dot"></span>离线处理 · 隐私安全</div> </div> </header> <div class="main-container"> <!-- ========== 左侧面板 ========== --> <div class="card"> <div class="card-title">⚙️ 上传与设置</div> <div class="upload-zone" id="uploadZone"> <span class="upload-icon">📷</span> <span class="upload-text">点击或拖拽上传证件照</span> <span class="upload-hint">支持 JPG / PNG / WebP</span> <input type="file" id="fileInput" accept="image/*"> </div> <div class="section-label">🎨 背景颜色(实时预览)</div> <div class="color-presets" id="colorPresets"> <div class="color-preset active" data-color="#FF0000" style="background:#FF0000;" title="红色"></div> <div class="color-preset" data-color="#FFFFFF" style="background:#FFFFFF;" title="白色"></div> <div class="color-preset" data-color="#438EDB" style="background:#438EDB;" title="蓝色"></div> <div class="color-preset" data-color="#1AAD19" style="background:#1AAD19;" title="绿色"></div> <div class="color-preset" data-color="#808080" style="background:#808080;" title="灰色"></div> <div class="color-preset" data-color="#003399" style="background:#003399;" title="深蓝"></div> </div> <div class="custom-color-section"> <div style="font-size:0.78rem;font-weight:600;margin-bottom:8px;color:#555;">🎯 自定义颜色</div> <div class="custom-color-row"> <input type="color" id="customColorPicker" value="#FF0000" title="取色器"> <div class="hex-input-wrapper"> <span class="hash">#</span> <input type="text" class="hex-input valid" id="hexInput" value="FF0000" maxlength="6" placeholder="输入6位HEX色值"> </div> <div class="color-preview-dot" id="colorPreviewDot" style="background:#FF0000;" title="当前颜色"></div> </div> </div> <div class="control-group" style="margin-top:14px;"> <div class="control-label"><span>🔍 容差范围</span><span class="control-value" id="toleranceVal">50</span></div> <input type="range" id="toleranceSlider" min="10" max="120" value="50" step="1"> </div> <div class="control-group"> <div class="control-label"><span>✨ 边缘羽化</span><span class="control-value" id="featherVal">1</span></div> <input type="range" id="featherSlider" min="0" max="5" value="1" step="0.5"> </div> <div class="control-group"> <div class="control-label"><span>☀️ 亮度</span><span class="control-value" id="brightnessVal">0</span></div> <input type="range" id="brightnessSlider" min="-30" max="30" value="0" step="1"> </div> <div class="section-label">📐 裁剪尺寸</div> <div class="size-presets" id="sizePresets"> <div class="size-preset active" data-width="295" data-height="413" data-name="一寸"><div>一寸</div><div class="size-dims">25×35mm</div></div> <div class="size-preset" data-width="413" data-height="579" data-name="二寸"><div>二寸</div><div class="size-dims">35×49mm</div></div> <div class="size-preset" data-width="260" data-height="378" data-name="小一寸"><div>小一寸</div><div class="size-dims">22×32mm</div></div> <div class="size-preset" data-width="390" data-height="567" data-name="小二寸"><div>小二寸</div><div class="size-dims">33×48mm</div></div> </div> <div class="action-row"> <button class="btn btn-primary btn-lg" id="processBtn" disabled>🎯 开始换底</button> <button class="btn btn-outline" id="resetBtn">🔄 重置</button> </div> </div> <!-- ========== 右侧面板 ========== --> <div class="card"> <div class="card-title">🖼️ 结果预览</div> <div class="result-container"> <div class="result-placeholder" id="resultPlaceholder"> <span class="icon">🖼️</span> <span>处理后的照片将显示在这里</span> </div> <img class="result-image" id="resultImage" style="display:none;" alt="处理结果"> </div> <div class="action-row"> <button class="btn btn-success btn-block" id="downloadBtn" disabled>💾 下载照片</button> </div> </div> </div> <div class="toast" id="toast"></div> <div class="footer">🔒 所有图片处理均在浏览器本地完成,不会上传到任何服务器</div> <script> (function() { // ==================== DOM引用 ==================== var uploadZone = document.getElementById('uploadZone'); var fileInput = document.getElementById('fileInput'); var processBtn = document.getElementById('processBtn'); var resetBtn = document.getElementById('resetBtn'); var downloadBtn = document.getElementById('downloadBtn'); var resultImage = document.getElementById('resultImage'); var resultPH = document.getElementById('resultPlaceholder'); var toastEl = document.getElementById('toast'); var colorPicker = document.getElementById('customColorPicker'); var hexInput = document.getElementById('hexInput'); var colorDot = document.getElementById('colorPreviewDot'); var tolSlider = document.getElementById('toleranceSlider'); var tolVal = document.getElementById('toleranceVal'); var feaSlider = document.getElementById('featherSlider'); var feaVal = document.getElementById('featherVal'); var briSlider = document.getElementById('brightnessSlider'); var briVal = document.getElementById('brightnessVal'); // ==================== 状态 ==================== var originalImage = null; var resultDataURL = null; var currentColor = '#FF0000'; var currentSize = { w: 295, h: 413, name: '一寸' }; var toastTimer = null; var autoTimer = null; var cachedMask = null; // ==================== Toast ==================== function toast(msg, type) { if (toastTimer) clearTimeout(toastTimer); toastEl.textContent = msg; toastEl.className = 'toast ' + (type || '') + ' show'; toastTimer = setTimeout(function() { toastEl.classList.remove('show'); }, 2000); } // ==================== 颜色工具 ==================== function hex3to6(s) { s = s.toUpperCase(); if (s.length === 3) { return s[0]+s[0]+s[1]+s[1]+s[2]+s[2]; } return s; } function isValidHex(s) { return /^[0-9a-fA-F]{3}$/.test(s) || /^[0-9a-fA-F]{6}$/.test(s); } function hexToRgb(hex) { hex = hex.replace('#', ''); if (hex.length === 3) hex = hex3to6(hex); return { r: parseInt(hex.substr(0,2), 16), g: parseInt(hex.substr(2,2), 16), b: parseInt(hex.substr(4,2), 16) }; } // 同步所有颜色UI function applyColor(hex6, source) { hex6 = hex6.toUpperCase(); if (!/^[0-9a-fA-F]{6}$/.test(hex6)) return; currentColor = '#' + hex6; // 取色器 if (source !== 'picker') { colorPicker.value = currentColor; } // 输入框 if (source !== 'input') { hexInput.value = hex6; hexInput.className = 'hex-input valid'; } // 预设圆点 if (source !== 'preset') { var all = document.querySelectorAll('.color-preset'); for (var i = 0; i < all.length; i++) { all[i].classList.remove('active'); } var m = document.querySelector('.color-preset[data-color="#' + hex6 + '"]'); if (m) m.classList.add('active'); } // 预览圆点 colorDot.style.background = currentColor; // 实时更新结果 scheduleAuto(); } // ==================== 颜色事件 ==================== document.getElementById('colorPresets').addEventListener('click', function(e) { var p = e.target.closest('.color-preset'); if (!p) return; var hex6 = p.getAttribute('data-color').replace('#', ''); var all = document.querySelectorAll('.color-preset'); for (var i = 0; i < all.length; i++) all[i].classList.remove('active'); p.classList.add('active'); applyColor(hex6, 'preset'); }); colorPicker.addEventListener('input', function() { var hex = colorPicker.value.replace('#', ''); if (isValidHex(hex)) { applyColor(hex3to6(hex), 'picker'); } }); hexInput.addEventListener('input', function() { var raw = hexInput.value.replace(/[^0-9a-fA-F]/g, '').slice(0, 6); hexInput.value = raw; if (raw.length === 6 && /^[0-9a-fA-F]{6}$/.test(raw)) { hexInput.className = 'hex-input valid'; applyColor(raw, 'input'); } else if (raw.length === 3 && /^[0-9a-fA-F]{3}$/.test(raw)) { hexInput.className = 'hex-input valid'; applyColor(hex3to6(raw), 'input'); } else if (raw.length === 0) { hexInput.className = 'hex-input'; } else if (/^[0-9a-fA-F]+$/.test(raw)) { hexInput.className = 'hex-input'; } else { hexInput.className = 'hex-input invalid'; } }); hexInput.addEventListener('blur', function() { var raw = hexInput.value; if (isValidHex(raw)) { var h6 = hex3to6(raw); hexInput.value = h6; hexInput.className = 'hex-input valid'; applyColor(h6, 'input'); } }); hexInput.addEventListener('paste', function(e) { e.preventDefault(); var t = (e.clipboardData || window.clipboardData).getData('text'); t = t.replace('#', '').replace(/[^0-9a-fA-F]/g, '').slice(0, 6); hexInput.value = t; if (isValidHex(t)) { hexInput.className = 'hex-input valid'; applyColor(hex3to6(t), 'input'); } }); // ==================== 尺寸 ==================== document.getElementById('sizePresets').addEventListener('click', function(e) { var p = e.target.closest('.size-preset'); if (!p) return; var all = document.querySelectorAll('.size-preset'); for (var i = 0; i < all.length; i++) all[i].classList.remove('active'); p.classList.add('active'); currentSize = { w: parseInt(p.getAttribute('data-width')), h: parseInt(p.getAttribute('data-height')), name: p.getAttribute('data-name') }; scheduleAuto(); }); // ==================== 滑块 ==================== tolSlider.addEventListener('input', function() { tolVal.textContent = tolSlider.value; cachedMask = null; scheduleAuto(); }); feaSlider.addEventListener('input', function() { feaVal.textContent = feaSlider.value; scheduleAuto(); }); briSlider.addEventListener('input', function() { briVal.textContent = briSlider.value; scheduleAuto(); }); // ==================== 防抖 ==================== function scheduleAuto() { if (!originalImage) return; if (autoTimer) clearTimeout(autoTimer); autoTimer = setTimeout(function() { doProcess(true); }, 200); } // ==================== 上传 ==================== uploadZone.addEventListener('click', function(e) { if (e.target.tagName === 'BUTTON' || e.target.tagName === 'INPUT') return; fileInput.click(); }); fileInput.addEventListener('change', function(e) { if (e.target.files && e.target.files[0]) { loadFile(e.target.files[0]); } }); uploadZone.addEventListener('dragover', function(e) { e.preventDefault(); uploadZone.classList.add('drag-over'); }); uploadZone.addEventListener('dragleave', function() { uploadZone.classList.remove('drag-over'); }); uploadZone.addEventListener('drop', function(e) { e.preventDefault(); uploadZone.classList.remove('drag-over'); if (e.dataTransfer.files && e.dataTransfer.files[0]) { loadFile(e.dataTransfer.files[0]); } }); function loadFile(file) { if (!file.type.match(/image\//)) { toast('请上传图片文件', 'error'); return; } var reader = new FileReader(); reader.onload = function(ev) { var img = new Image(); img.onload = function() { originalImage = img; cachedMask = null; showPreview(img); processBtn.disabled = false; toast('图片加载成功', 'success'); doProcess(false); }; img.onerror = function() { toast('图片加载失败', 'error'); }; img.src = ev.target.result; }; reader.readAsDataURL(file); } function showPreview(img) { // 清除旧预览 var old = uploadZone.querySelector('.image-container'); if (old) old.remove(); var iconEl = uploadZone.querySelector('.upload-icon'); var textEl = uploadZone.querySelector('.upload-text'); var hintEl = uploadZone.querySelector('.upload-hint'); uploadZone.classList.add('has-image'); if (iconEl) iconEl.style.display = 'none'; if (textEl) textEl.style.display = 'none'; if (hintEl) hintEl.style.display = 'none'; var container = document.createElement('div'); container.className = 'image-container'; var pImg = document.createElement('img'); pImg.className = 'preview-image'; pImg.src = img.src; pImg.alt = '原始照片'; var overlay = document.createElement('div'); overlay.className = 'image-actions-overlay'; var delBtn = document.createElement('button'); delBtn.className = 'btn-icon-sm'; delBtn.innerHTML = '✕'; delBtn.title = '移除图片'; delBtn.addEventListener('click', function(ev) { ev.stopPropagation(); resetAll(); }); overlay.appendChild(delBtn); container.appendChild(pImg); container.appendChild(overlay); uploadZone.appendChild(container); } // ==================== 核心算法 ==================== function doProcess(silent) { if (!originalImage) return; var tolerance = parseInt(tolSlider.value); var featherR = parseFloat(feaSlider.value); var brightness = parseInt(briSlider.value); var target = hexToRgb(currentColor); var srcW = originalImage.naturalWidth; var srcH = originalImage.naturalHeight; // 画到canvas var canvas = document.createElement('canvas'); canvas.width = srcW; canvas.height = srcH; var ctx = canvas.getContext('2d'); ctx.drawImage(originalImage, 0, 0); var imgData = ctx.getImageData(0, 0, srcW, srcH); var data = imgData.data; var w = srcW; var h = srcH; // 步骤1:建立遮罩(仅首次或容差改变时) if (cachedMask === null) { // 采样边缘 var samples = []; var step = Math.max(1, Math.floor(Math.min(w, h) / 30)); var x, y; for (x = 0; x < w; x += step) { samples.push(getPixel(data, w, x, 0)); samples.push(getPixel(data, w, x, h - 1)); } for (y = 0; y < h; y += step) { samples.push(getPixel(data, w, 0, y)); samples.push(getPixel(data, w, w - 1, y)); } // 中位数 samples.sort(function(a, b) { return (a.r + a.g + a.b) - (b.r + b.g + b.b); }); var mid = samples[Math.floor(samples.length / 2)]; var bgR = mid.r; var bgG = mid.g; var bgB = mid.b; // 颜色距离 var mask = new Uint8Array(w * h); var tolSq = tolerance * tolerance; var i; for (i = 0; i < w * h; i++) { var pi = i * 4; var dr = data[pi] - bgR; var dg = data[pi + 1] - bgG; var db = data[pi + 2] - bgB; if (dr * dr + dg * dg + db * db <= tolSq) { mask[i] = 1; } } // Flood fill 从边缘连通 var visited = new Uint8Array(w * h); var queue = []; var head = 0; var x2, y2; for (x2 = 0; x2 < w; x2++) { if (mask[x2] === 1 && !visited[x2]) { visited[x2] = 1; queue.push(x2); } var bIdx = (h - 1) * w + x2; if (mask[bIdx] === 1 && !visited[bIdx]) { visited[bIdx] = 1; queue.push(bIdx); } } for (y2 = 0; y2 < h; y2++) { var lIdx = y2 * w; if (mask[lIdx] === 1 && !visited[lIdx]) { visited[lIdx] = 1; queue.push(lIdx); } var rIdx = y2 * w + (w - 1); if (mask[rIdx] === 1 && !visited[rIdx]) { visited[rIdx] = 1; queue.push(rIdx); } } while (head < queue.length) { var qIdx = queue[head++]; var qx = qIdx % w; var qy = Math.floor(qIdx / w); var nb = []; if (qx > 0) nb.push(qIdx - 1); if (qx < w - 1) nb.push(qIdx + 1); if (qy > 0) nb.push(qIdx - w); if (qy < h - 1) nb.push(qIdx + w); for (var ni = 0; ni < nb.length; ni++) { var nIdx = nb[ni]; if (mask[nIdx] === 1 && !visited[nIdx]) { visited[nIdx] = 1; queue.push(nIdx); } } } cachedMask = visited; } // 步骤2:从原始图重新着色 var origCanvas = document.createElement('canvas'); origCanvas.width = w; origCanvas.height = h; var origCtx = origCanvas.getContext('2d'); origCtx.drawImage(originalImage, 0, 0); var origData = origCtx.getImageData(0, 0, w, h).data; var work = new Uint8ClampedArray(origData.length); var k; for (k = 0; k < origData.length; k++) { work[k] = origData[k]; } var featherPx = Math.round(featherR); var j; for (j = 0; j < work.length; j += 4) { var idx = j / 4; if (cachedMask[idx] === 1) { var alpha = 1; if (featherPx > 0) { alpha = getFeather(cachedMask, idx, w, h, featherPx); } work[j] = Math.round(work[j] * (1 - alpha) + target.r * alpha); work[j + 1] = Math.round(work[j + 1] * (1 - alpha) + target.g * alpha); work[j + 2] = Math.round(work[j + 2] * (1 - alpha) + target.b * alpha); } } // 亮度 if (brightness !== 0) { var bj; for (bj = 0; bj < work.length; bj += 4) { work[bj] = clamp(work[bj] + brightness, 0, 255); work[bj + 1] = clamp(work[bj + 1] + brightness, 0, 255); work[bj + 2] = clamp(work[bj + 2] + brightness, 0, 255); } } var resultID = new ImageData(work, w, h); var workCanvas = document.createElement('canvas'); workCanvas.width = w; workCanvas.height = h; var workCtx = workCanvas.getContext('2d'); workCtx.putImageData(resultID, 0, 0); // 裁剪 var finalCanvas = cropCanvas(workCanvas, currentSize.w, currentSize.h); resultDataURL = finalCanvas.toDataURL('image/png'); resultImage.src = resultDataURL; resultImage.style.display = 'block'; resultPH.style.display = 'none'; downloadBtn.disabled = false; if (!silent) { toast('换底完成!', 'success'); } } function getPixel(data, w, x, y) { var i = (y * w + x) * 4; return { r: data[i], g: data[i + 1], b: data[i + 2] }; } function getFeather(mask, idx, w, h, radius) { var y = Math.floor(idx / w); var x = idx % w; var fg = 0; var total = 0; var dy, dx; for (dy = -radius; dy <= radius; dy++) { for (dx = -radius; dx <= radius; dx++) { var nx = x + dx; var ny = y + dy; if (nx >= 0 && nx < w && ny >= 0 && ny < h) { total++; if (mask[ny * w + nx] === 0) fg++; } } } if (total === 0) return 1; var ratio = fg / total; if (ratio > 0.6) return 0; if (ratio > 0.3) return 0.5; return 1; } function cropCanvas(srcCanvas, tw, th) { var sw = srcCanvas.width; var sh = srcCanvas.height; var sa = sw / sh; var ta = tw / th; var cw, ch, ox, oy; if (sa > ta) { ch = sh; cw = Math.round(sh * ta); ox = Math.round((sw - cw) / 2); oy = 0; } else { cw = sw; ch = Math.round(sw / ta); ox = 0; oy = Math.round((sh - ch) / 2); } var fc = document.createElement('canvas'); fc.width = tw; fc.height = th; var fctx = fc.getContext('2d'); fctx.imageSmoothingEnabled = true; fctx.imageSmoothingQuality = 'high'; fctx.drawImage(srcCanvas, ox, oy, cw, ch, 0, 0, tw, th); return fc; } function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); } // ==================== 按钮 ==================== processBtn.addEventListener('click', function() { processBtn.disabled = true; processBtn.innerHTML = '<span class="spinner"></span> 处理中...'; cachedMask = null; setTimeout(function() { try { doProcess(false); } catch(err) { console.error(err); toast('处理出错: ' + err.message, 'error'); } processBtn.disabled = false; processBtn.innerHTML = '🎯 开始换底'; }, 30); }); resetBtn.addEventListener('click', resetAll); downloadBtn.addEventListener('click', function() { if (!resultDataURL) return; var a = document.createElement('a'); a.download = '证件照_' + currentSize.name + '_' + Date.now() + '.png'; a.href = resultDataURL; document.body.appendChild(a); a.click(); document.body.removeChild(a); toast('下载中', 'success'); }); function resetAll() { originalImage = null; resultDataURL = null; cachedMask = null; if (autoTimer) clearTimeout(autoTimer); resultImage.style.display = 'none'; resultImage.src = ''; resultPH.style.display = ''; downloadBtn.disabled = true; processBtn.disabled = true; processBtn.innerHTML = '🎯 开始换底'; uploadZone.classList.remove('has-image'); var old = uploadZone.querySelector('.image-container'); if (old) old.remove(); var iconEl = uploadZone.querySelector('.upload-icon'); var textEl = uploadZone.querySelector('.upload-text'); var hintEl = uploadZone.querySelector('.upload-hint'); if (iconEl) iconEl.style.display = ''; if (textEl) textEl.style.display = ''; if (hintEl) hintEl.style.display = ''; fileInput.value = ''; tolSlider.value = 50; tolVal.textContent = '50'; feaSlider.value = 1; feaVal.textContent = '1'; briSlider.value = 0; briVal.textContent = '0'; currentColor = '#FF0000'; colorPicker.value = '#FF0000'; hexInput.value = 'FF0000'; hexInput.className = 'hex-input valid'; colorDot.style.background = '#FF0000'; var all = document.querySelectorAll('.color-preset'); for (var i = 0; i < all.length; i++) all[i].classList.remove('active'); var red = document.querySelector('.color-preset[data-color="#FF0000"]'); if (red) red.classList.add('active'); var allS = document.querySelectorAll('.size-preset'); for (var j = 0; j < allS.length; j++) allS[j].classList.remove('active'); var yc = document.querySelector('.size-preset[data-name="一寸"]'); if (yc) yc.classList.add('active'); currentSize = { w: 295, h: 413, name: '一寸' }; } // ==================== 快捷键 ==================== document.addEventListener('keydown', function(e) { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); if (!processBtn.disabled) processBtn.click(); } if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault(); if (!downloadBtn.disabled) downloadBtn.click(); } }); console.log('证件照换底工具已就绪 - 离线处理 - 实时预览'); })(); </script> </body> </html> 2 个帖子 - 1 位参与者 阅读完整话题
IT之家 6 月 8 日消息,中兴 U15S 随身 WiFi 今日开售,有黑白两种颜色可选, 售价 179 元 。 京东 中兴(ZTE)U15S 随身 wifi 179 元 直达链接 这款产品搭载中兴自研双核芯片 V3E-A53,内置双卡、支持双网任意切换,支持 Wi-Fi 6;采用“G2 曲面手感设计”,正面配备一块 1.44 英寸触摸屏面板,可显示二维码、流量等信息;重约 237g。 这款产品内置 10000mAh 电芯,支持 18W 快充,支持 QC / PD / PPS 等协议,同时支持电源直供电模式。 IT之家附这款产品详细参数如下: 京东 618 无门槛红包 面额至高 26618 元,每天抽 3 次: 点此抽红包 淘宝 618 无门槛红包 面额至高 26888 元,每天抽 1 次: 点此抽红包
就两个问题: 在"轻松农场"里用"精炼室"精炼各种颜色的卡牌,有没有日限额啥的(比如一天限制精炼多少次之类的限制)。 在"集换卡片"里用"重铸工坊"重铸各种颜色的卡牌,有没有日限额啥的(比如一天限制重铸多少次之类的) 有没有有经验的佬懂的, 问题的目的:我在玩里面的"集换卡片"游戏,现在每天650抽满抽,想法是先通过自然满抽,抽多少算多少,前期先不通过这两种方式精炼和重铸,但最后留出比如5天时间,来集中精炼和重铸,很怕到那时突然来个日限啥的,翻车了,那就完蛋蛋了 1 个帖子 - 1 位参与者 阅读完整话题
这grok是彻底要废了,都削成啥了,审核又变严了,还搞了个你出图被阻止多了几次就锁你,然后就变成一次就只能出一张.不能搞颜色了谁还用你啊没点数 1 个帖子 - 1 位参与者 阅读完整话题
IT之家 6 月 6 日消息,消息源 @KaroulSahil 昨日(6 月 5 日)在 X 平台分享了一段视频, 展示了白色和深蓝色(接近黑色)版本苹果 iPhone 折叠手机(上市后预估名为 iPhone Ultra)。 IT之家此前报道,针对 iPhone Fold,苹果公司内部正研发两种颜色,其一是经典的银白色,其二是深靛蓝色,比较接近 iPhone 17 Pro 的深蓝色。 供应链分析师郭明錤曾警告称,苹果早期会面临生产良率和产能爬坡问题,导致这款手机的供不应求状况至少持续到 2026 年底。 他还指出,外界提到的 1500 万-2000 万台销量预测,更可能是 2-3 年产品生命周期的累计需求。
一张疑似苹果首款折叠屏 iPhone——“iPhone Ultra”的真机模型照片近日在微博流出,被认为首次清晰展示了这款新机的其中一种配色。据悉,苹果预计将在今年晚些时候正式发布这款折叠屏 iPhone,而其配色策略将明显区别于目前常规 iPhone 产品线。 此次图片由爆料人士冰宇宙发布,画面中设备采用白色机身,被认为是已进入早期量产阶段机型的模型版本。尽管具体细节仍有保留,但另一位爆料者 Instant Digital 此前已表示,白色目前是唯一“可以被确认”的量产配色。 多方消息显示,iPhone Ultra 整体仅会提供两种颜色,另一种尚未曝光的配色被认为接近 iPhone 17 Pro 的“深蓝”调 indigo 版本。来自供应链的说法称,与 iPhone 18 Pro 系列相比,折叠屏 iPhone 的颜色选择会更少,不会出现鲜艳、跳跃的高饱和色调。 彭博社记者 Mark Gurman 的报道同样指向类似判断:苹果计划有意“回避有趣的颜色”,回归更传统的深空灰 / 黑色与银色 / 白色路线。这种做法与当年 2017 年首发的 iPhone X 颇为相似,当时该机型也仅提供银色和深空灰两种选择。 业内分析认为,有限的配色与折叠屏 iPhone 较低的产量预期密切相关。分析师郭明錤此前警告称,制造端的技术与良率挑战,可能会让折叠屏 iPhone 的供应紧张情况持续到至少 2026 年底,增加配色只会进一步提高生产复杂度和成本。 在供应量有限、定价被外界普遍认为将超过 2000 美元的背景下,苹果在首发阶段并不急于通过多样化配色来刺激需求。相关报道指出,这一价位的潜在用户更看重形态、做工和功能等差异,而不会将颜色作为核心购买因素。 按照目前爆料节奏,iPhone Ultra 预计将与 iPhone 18 Pro、iPhone 18 Pro Max 一同在今年 9 月发布。随着量产推进,关于其最终配色和设计细节的更多消息,预计还将陆续浮出水面。 查看评论
IT之家 5 月 30 日消息,消息源 @SonnyDickson 昨日(5 月 29 日)在 X 平台发布推文,分享了一组机模照片, 展示了苹果 iPhone 18 四种配色,涵盖黑色、银色、樱桃色和浅蓝色。 消息源表示,在苹果 iPhone 17 系列主打星宇橙后,苹果 iPhone 18 系列主打颜色就是樱桃色,在 Pantone 色系中,颜色编号为 6076,是一种非常柔和的颜色,色调类似于酒红色。IT之家附上相关图片如下: 延伸阅读 根据此前报道,苹果 iPhone 18 Pro 系列将接替 iPhone 17 Pro 系列的“星宇橙”配色,主打颜色被命名为“深樱桃色”(Dark Cherry)。 除了樱桃色,消息称苹果还在为 iPhone 18 Pro 系列测试浅蓝、深灰及银色版本。其中浅蓝色被描述为比较接近 iPhone 17 的青雾蓝色,是一款低饱和度、带灰调的雅致色调。 彭博社记者马克 · 古尔曼曾透露苹果正在为 iPhone 18 Pro 系列测试全新“深红色”配色,这得益于 iPhone 17 Pro 系列引入的铝合金一体成型机身提供了更灵活的着色空间。如果深红色最终入选,这将是自 2022 年 iPhone 14 之后,苹果再次推出红色系 iPhone,也是红色首次出现在“Pro”级别机型上。 在硬件设计方面,iPhone 18 Pro 系列将基于现有形态精细化打磨,新机将采用更小的灵动岛设计,有望缩小后置摄像头玻璃与模组凸起之间的间隙,降低玻璃与金属边框间的色差,提升机身背面的整体感。 影像方面,消息称 iPhone 18 Pro 系列背部摄像头布局基本不变,但为了容纳 48MP 可变光圈,后摄模组区域可能略微加厚。
预算 4000 ,这个价格是不是买不到太好的 5k 显示器。 颜色正常,不偏色就行。
预算 4000 ,这个价格是不是买不到太好的 5k 显示器。 颜色正常,不偏色就行。
预算 4000 ,这个价格是不是买不到太好的 5k 显示器。 颜色正常,不偏色就行。
预算 4000 ,这个价格是不是买不到太好的 5k 显示器。 颜色正常,不偏色就行。
预算 4000 ,这个价格是不是买不到太好的 5k 显示器。 颜色正常,不偏色就行。
让他改个按钮颜色,把我其他页面弄坏了,让他修,然后一堆wait! wait! wait!看得我好烦,然后把我windsurf一天的额度用光了,用了我一半周额度 最后还没修好,现在用codex擦擦屁股 11 个帖子 - 11 位参与者 阅读完整话题
不知道从哪个版本开始,我VSCode的终端界面超级奇怪,输入的字符总是莫名其妙给我加了黑底色,就像是渲染出了 Bug 一样。改主题、重装都没用。心态炸了,各位大佬知道怎么回事吗? 1 个帖子 - 1 位参与者 阅读完整话题
IT之家 5 月 20 日消息,小米汽车副总裁李肖爽今日晒出了 YU7 GT 外观颜色、内饰风格以及轮毂样式。 IT之家注意到, 小米 YU7 GT 拥有 5 款外观 ,包括车厘子红、火山灰、曜石黑、珍珠白、钛金属色;2 种内饰风格,包括豪华运动和豪华舒适; 全系采用 21 英寸轮毂 ,包括低风阻幻刃轮毂、五辐双层锻造轮毂。 根据官方介绍,小米 YU7 GT 的定位其实是一款适合长途旅行的跑车级 SUV。关于该车为何要上纽北赛道,雷军在昨日的答网友问里解释称,纽北不仅仅是赛道,其实也是全球最佳的高性能车试炼场。GT 跑纽北不仅仅是为了赛道成绩,更多是为了验证 GT 在复杂路况和复杂天气情况下的机械素质,提高操控的稳定性。 小米创办人、董事长兼 CEO 雷军今日发布视频,回应了小米 YU7 GT 相关问题。 他表示,这款车为时代精英设计,价格肯定会有点小贵 。
不需要整个标题文字都换色,那样显得很乱,但是那个灰色图钉可不可以换一个更亮眼的颜色? 2 个帖子 - 2 位参与者 阅读完整话题
IT之家 5 月 17 日消息, 小米 YU7 汽车全新配色「火山灰」官图今天公布 (虽然官方微博称是 YU7 新色,但实际预热海报显示为 YU7 GT),灵感来自晨雾下的火山地貌。 据小米汽车透露,小米 YU7 全新颜色「火山灰」实车已陆续进店。同时, 小米 YU7 还新增了「霞光紫」配色 。据介绍,目前 YU7 GT 仅做静态展示, 内饰体验将在 5 月底上市发布后开放 。 小米汽车官方还表示, 更多颜色也在陆续进店 。需要注意的是,由于运输时间差异,准确到店时间需咨询门店。IT之家附 361 家门店分布情况如下(因门店信息过多导致阅读体验不佳,想了解具体门店信息可 点此前往 小米汽车官方文章查看详情): 北京:共 17 家 上海:共 16 家 天津:共 6 家 重庆:共 7 家 广东:共 52 家 四川:共 16 家 浙江:共 49 家 江苏:共 43 家 湖北:共 15 家 河南:共 14 家 山东:共 17 家 湖南:共 9 家 陕西:共 4 家 安徽:共 15 家 福建:共 14 家 河北:共 11 家 辽宁:共 6 家 吉林:共 4 家 黑龙江:共 2 家 云南:共 6 家 江西:共 9 家 内蒙古:共 2 家 山西:共 3 家 新疆:共 3 家 贵州:共 5 家 广西:共 6 家 海南:共 4 家 宁夏:共 2 家 甘肃:共 3 家 青海:共 1 家
类别悬浮占用的区域太大了,其实只要下面一办就行。还有颜色的视觉效果看起来很累没有旧主题这么轻松,可能是覆盖率太高 2 个帖子 - 2 位参与者 阅读完整话题
标题: 做了一个在线颜色匹配小游戏 Toon Tone ,想听听大家对玩法和 UI 的建议 正文: 最近在做一个很轻量的网页小游戏,名字暂时叫 Toon Tone 。 最开始的想法是做一个“根据卡通角色某个部位颜色进行匹配”的游戏,比如记住角色帽子、衣服、眼睛的颜色,然后用 HSB 滑块复原。但实际开发时发现角色方案比想象中复杂很多: 如果用 SVG 直接画角色,AI / 代码生成出来的角色很容易很丑; 如果用 PNG 分层做角色,需要 base 、mask 、shading 几层严格对齐; 可变色部位如果对不齐,Canvas 叠加效果会很奇怪; 角色素材制作成本远高于游戏逻辑本身。 所以第一版我决定先把玩法简化成 色卡匹配游戏 : 玩家看到一个目标颜色,然后通过: Hue Saturation Brightness 三个滑块去尽量匹配目标色。每局 10 轮,每轮根据颜色接近程度给分。 目前第一版的目标是: 先验证“调色匹配”这个玩法本身是否有趣; 把核心逻辑、计分、移动端交互做好; 如果用户反馈不错,再考虑加每日挑战、分享结果、排行榜,甚至重新尝试角色版本。 技术上暂时没做得很复杂: 纯前端实现; 游戏直接放在首页 hero 区,不单独做 /play 页面; 用 HSB / RGB 转换做实时颜色预览; 用 RGB 距离计算分数; 桌面端左右布局,移动端上下布局; 后续可能再换成更接近人眼感知的色差算法。 我自己感觉这个方向比一开始的角色版更容易落地,也更适合作为 MVP 。 目前想听听大家的建议: 这种颜色匹配小游戏,你会觉得有一点可玩性吗? 目标色一直显示比较好,还是显示几秒后隐藏做成记忆挑战更好? 10 轮一局会不会太长? 计分方式需要更专业吗,比如用 CIEDE2000 ,而不是 RGB 距离? 这种小游戏更适合做成 Daily Challenge ,还是无限练习模式? 如果有做过类似小游戏、颜色工具、Canvas 交互或者前端小游戏的朋友,也欢迎给点建议。 网站地址: https://toontone.work/ 感谢大家~