Skip to content

Instantly share code, notes, and snippets.

@dyedd
Last active June 16, 2026 10:16
Show Gist options
  • Select an option

  • Save dyedd/9c87ddfc3429f5658ee021346883e0a6 to your computer and use it in GitHub Desktop.

Select an option

Save dyedd/9c87ddfc3429f5658ee021346883e0a6 to your computer and use it in GitHub Desktop.
智能吸色抠图工具
<!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>
@YANG301

YANG301 commented Dec 26, 2025

Copy link
Copy Markdown

好用。非常感谢

@carter003

Copy link
Copy Markdown

好用!非常感谢!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment