-
-
Save dyedd/9c87ddfc3429f5658ee021346883e0a6 to your computer and use it in GitHub Desktop.
智能吸色抠图工具
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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> | |
| :root { | |
| --bg-color: #121212; | |
| --card-bg: #1e1e1e; | |
| --text-color: #e0e0e0; | |
| --accent-color: #8b5cf6; | |
| /* 紫色系 */ | |
| --accent-hover: #7c3aed; | |
| --border-color: #333; | |
| } | |
| body { | |
| background-color: var(--bg-color); | |
| color: var(--text-color); | |
| font-family: "Segoe UI", Roboto, Helvetica, Arial, sans-serif; | |
| margin: 0; | |
| padding: 20px; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| min-height: 100vh; | |
| } | |
| h1 { | |
| margin-bottom: 20px; | |
| font-weight: 300; | |
| letter-spacing: 2px; | |
| background: linear-gradient(90deg, #a78bfa, #3b82f6); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .container { | |
| background-color: var(--card-bg); | |
| padding: 25px; | |
| border-radius: 16px; | |
| box-shadow: 0 10px 40px rgba(0, 0, 0, 0.6); | |
| width: 95%; | |
| max-width: 900px; | |
| border: 1px solid var(--border-color); | |
| } | |
| .controls { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); | |
| gap: 20px; | |
| margin-bottom: 20px; | |
| padding-bottom: 20px; | |
| border-bottom: 1px solid var(--border-color); | |
| } | |
| .control-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| } | |
| label { | |
| font-size: 0.9em; | |
| color: #bbb; | |
| font-weight: 500; | |
| } | |
| /* 通用按钮样式 */ | |
| .btn { | |
| background-color: #333; | |
| color: white; | |
| border: 1px solid #444; | |
| padding: 10px 16px; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-size: 0.95em; | |
| transition: all 0.2s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| } | |
| .btn:hover:not(:disabled) { | |
| border-color: var(--accent-color); | |
| background-color: #2a2a2a; | |
| } | |
| .btn-primary { | |
| background-color: var(--accent-color); | |
| border: none; | |
| } | |
| .btn-primary:hover:not(:disabled) { | |
| background-color: var(--accent-hover); | |
| } | |
| .btn:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| /* 吸管工具专用样式 */ | |
| .picker-active { | |
| background-color: #f59e0b !important; | |
| color: #000 !important; | |
| font-weight: bold; | |
| animation: pulse 1.5s infinite; | |
| } | |
| @keyframes pulse { | |
| 0% { | |
| box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.4); | |
| } | |
| 70% { | |
| box-shadow: 0 0 0 10px rgba(245, 158, 11, 0); | |
| } | |
| 100% { | |
| box-shadow: 0 0 0 0 rgba(245, 158, 11, 0); | |
| } | |
| } | |
| /* 颜色预览块 */ | |
| .color-preview { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| background: #252525; | |
| padding: 8px; | |
| border-radius: 8px; | |
| } | |
| .color-box { | |
| width: 30px; | |
| height: 30px; | |
| border-radius: 6px; | |
| border: 2px solid #555; | |
| background-color: #000000; | |
| /* 默认黑色 */ | |
| box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); | |
| } | |
| .color-val { | |
| font-family: monospace; | |
| color: #888; | |
| font-size: 0.9em; | |
| } | |
| /* 滑块样式 */ | |
| input[type="range"] { | |
| width: 100%; | |
| accent-color: var(--accent-color); | |
| cursor: pointer; | |
| } | |
| select { | |
| padding: 10px; | |
| border-radius: 8px; | |
| background: #252525; | |
| color: white; | |
| border: 1px solid #444; | |
| outline: none; | |
| width: 100%; | |
| } | |
| /* 预览区域 */ | |
| .preview-area { | |
| position: relative; | |
| min-height: 400px; | |
| background-image: | |
| linear-gradient(45deg, #2a2a2a 25%, transparent 25%), | |
| linear-gradient(-45deg, #2a2a2a 25%, transparent 25%), | |
| linear-gradient(45deg, transparent 75%, #2a2a2a 75%), | |
| linear-gradient(-45deg, transparent 75%, #2a2a2a 75%); | |
| background-size: 20px 20px; | |
| background-position: 0 0, 0 10px, 10px -10px, -10px 0px; | |
| border-radius: 12px; | |
| overflow: hidden; | |
| border: 2px solid #333; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| canvas { | |
| max-width: 100%; | |
| max-height: 600px; | |
| object-fit: contain; | |
| cursor: default; | |
| } | |
| /* 当处于吸管模式时的光标 */ | |
| canvas.cursor-crosshair { | |
| cursor: crosshair; | |
| } | |
| .placeholder { | |
| color: #555; | |
| text-align: center; | |
| } | |
| input[type="file"] { | |
| display: none; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>CHROMA KEY TOOL</h1> | |
| <div class="container"> | |
| <div class="controls"> | |
| <div class="control-group"> | |
| <label>1. 上传图片</label> | |
| <label for="upload" class="btn"> | |
| 📂 选择图片 | |
| </label> | |
| <input type="file" id="upload" accept="image/*" /> | |
| </div> | |
| <div class="control-group"> | |
| <label>2. 吸取背景色</label> | |
| <button id="picker-btn" class="btn" disabled> | |
| 🖌️ 点击开始吸色 | |
| </button> | |
| <div class="color-preview"> | |
| <div id="color-box" class="color-box"></div> | |
| <span id="color-val" class="color-val">RGB(0,0,0)</span> | |
| </div> | |
| </div> | |
| <div class="control-group"> | |
| <label>3. 颜色容差 (<span id="thresh-val">30</span>)</label> | |
| <input type="range" id="threshold" min="0" max="200" value="30" /> | |
| <div style="font-size: 0.8em; color: #666; margin-top:5px;">数值越大,去除的颜色范围越广</div> | |
| </div> | |
| <div class="control-group"> | |
| <label>4. 导出</label> | |
| <select id="format"> | |
| <option value="image/png">PNG (透明背景)</option> | |
| <option value="image/webp">WebP (推荐)</option> | |
| <option value="image/jpeg">JPEG (白色背景)</option> | |
| </select> | |
| <button id="download-btn" class="btn btn-primary" disabled>⬇️ 保存图片</button> | |
| </div> | |
| </div> | |
| <div class="preview-area"> | |
| <p id="placeholder" class="placeholder">请上传图片<br>然后点击“吸色”选择要去除的背景</p> | |
| <canvas id="canvas"></canvas> | |
| </div> | |
| </div> | |
| <script> | |
| // DOM 元素 | |
| const uploadInput = document.getElementById("upload"); | |
| const canvas = document.getElementById("canvas"); | |
| const ctx = canvas.getContext("2d", { willReadFrequently: true }); | |
| const pickerBtn = document.getElementById("picker-btn"); | |
| const colorBox = document.getElementById("color-box"); | |
| const colorValText = document.getElementById("color-val"); | |
| const thresholdInput = document.getElementById("threshold"); | |
| const threshValDisplay = document.getElementById("thresh-val"); | |
| const downloadBtn = document.getElementById("download-btn"); | |
| const placeholder = document.getElementById("placeholder"); | |
| const formatSelect = document.getElementById("format"); | |
| // 状态变量 | |
| let originalImage = null; // 存储原始图片对象 | |
| let isImageLoaded = false; | |
| let isPickingMode = false; // 是否处于吸色模式 | |
| // 目标颜色 (默认黑色) | |
| let targetColor = { r: 0, g: 0, b: 0 }; | |
| // 1. 图片上传处理 | |
| uploadInput.addEventListener("change", (e) => { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = (event) => { | |
| originalImage = new Image(); | |
| originalImage.onload = () => { | |
| initCanvas(); | |
| }; | |
| originalImage.src = event.target.result; | |
| }; | |
| reader.readAsDataURL(file); | |
| }); | |
| function initCanvas() { | |
| isImageLoaded = true; | |
| placeholder.style.display = "none"; | |
| canvas.style.display = "block"; | |
| pickerBtn.disabled = false; | |
| downloadBtn.disabled = false; | |
| // 设置 Canvas 尺寸 | |
| canvas.width = originalImage.width; | |
| canvas.height = originalImage.height; | |
| // 默认先处理一次(去除黑色) | |
| processImage(); | |
| } | |
| // 2. 吸管工具逻辑 | |
| pickerBtn.addEventListener("click", () => { | |
| if (!isImageLoaded) return; | |
| isPickingMode = !isPickingMode; // 切换状态 | |
| if (isPickingMode) { | |
| // 开启吸色模式 | |
| pickerBtn.classList.add("picker-active"); | |
| pickerBtn.innerText = "点击图片选取颜色..."; | |
| canvas.classList.add("cursor-crosshair"); | |
| // 重要:吸色时必须显示原图,否则如果是透明的就吸不到了 | |
| ctx.drawImage(originalImage, 0, 0); | |
| } else { | |
| stopPicking(); | |
| } | |
| }); | |
| function stopPicking() { | |
| isPickingMode = false; | |
| pickerBtn.classList.remove("picker-active"); | |
| pickerBtn.innerText = "🖌️ 点击开始吸色"; | |
| canvas.classList.remove("cursor-crosshair"); | |
| processImage(); // 恢复处理效果 | |
| } | |
| // 3. Canvas 点击事件 (用于吸取颜色) | |
| canvas.addEventListener("click", (e) => { | |
| if (!isPickingMode || !isImageLoaded) return; | |
| // 获取点击坐标 | |
| const rect = canvas.getBoundingClientRect(); | |
| // 计算鼠标在 Canvas 内部的实际坐标 (处理缩放) | |
| const scaleX = canvas.width / rect.width; | |
| const scaleY = canvas.height / rect.height; | |
| const x = (e.clientX - rect.left) * scaleX; | |
| const y = (e.clientY - rect.top) * scaleY; | |
| // 获取该点像素 | |
| const pixel = ctx.getImageData(x, y, 1, 1).data; | |
| // 更新目标颜色 | |
| targetColor = { r: pixel[0], g: pixel[1], b: pixel[2] }; | |
| // 更新 UI 显示 | |
| updateColorUI(); | |
| // 自动结束吸色模式并应用抠图 | |
| stopPicking(); | |
| }); | |
| function updateColorUI() { | |
| const rgbStr = `rgb(${targetColor.r}, ${targetColor.g}, ${targetColor.b})`; | |
| colorBox.style.backgroundColor = rgbStr; | |
| colorValText.innerText = `RGB(${targetColor.r},${targetColor.g},${targetColor.b})`; | |
| } | |
| // 4. 滑块拖动监听 | |
| thresholdInput.addEventListener("input", function () { | |
| threshValDisplay.innerText = this.value; | |
| if (isImageLoaded && !isPickingMode) { | |
| processImage(); | |
| } | |
| }); | |
| // 5. 核心图像处理算法 | |
| function processImage() { | |
| if (!isImageLoaded) return; | |
| // 始终从原图开始绘制,确保无损处理 | |
| ctx.drawImage(originalImage, 0, 0); | |
| const frame = ctx.getImageData(0, 0, canvas.width, canvas.height); | |
| const data = frame.data; | |
| const threshold = parseInt(thresholdInput.value); | |
| // 优化:计算阈值的平方,避免在循环中开根号,提高性能 | |
| const thresholdSq = threshold * threshold; | |
| // 遍历所有像素 | |
| for (let i = 0; i < data.length; i += 4) { | |
| const r = data[i]; | |
| const g = data[i + 1]; | |
| const b = data[i + 2]; | |
| // 计算当前像素与目标颜色的距离 (欧几里得距离的平方) | |
| // 距离 = (r1-r2)^2 + (g1-g2)^2 + (b1-b2)^2 | |
| const distSq = | |
| (r - targetColor.r) ** 2 + | |
| (g - targetColor.g) ** 2 + | |
| (b - targetColor.b) ** 2; | |
| // 如果距离小于阈值(说明颜色很接近),则变透明 | |
| // 注意:我们在 RGB 空间计算距离,最大距离约为 441.6 (sqrt(255^2*3)) | |
| // 简单的阈值对比效果通常足够好 | |
| if (distSq <= thresholdSq * 3) { | |
| // 乘以3是为了让滑块手感更线性,因为 RGB 是三个通道 | |
| data[i + 3] = 0; // Alpha设为0 | |
| } | |
| } | |
| ctx.putImageData(frame, 0, 0); | |
| } | |
| // 6. 下载功能 | |
| downloadBtn.addEventListener("click", () => { | |
| if (!isImageLoaded) return; | |
| const link = document.createElement("a"); | |
| const format = formatSelect.value; | |
| const ext = format === 'image/jpeg' ? 'jpg' : (format === 'image/webp' ? 'webp' : 'png'); | |
| link.download = `processed_${new Date().getTime()}.${ext}`; | |
| link.href = canvas.toDataURL(format); | |
| link.click(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
好用。非常感谢