<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">🎨 图片底色转换工具</title>
<style>
* {
box-sizing: border-box;
}
body {
background: linear-gradient(145deg, #0f172a 0%, #1e293b 100%);
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
min-height: 100vh;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.card {
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 32px;
padding: 30px;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.6), inset 0 1px 2px rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.15);
max-width: 950px;
width: 100%;
transition: all 0.2s ease;
}
h1 {
margin: 0 0 20px 0;
font-size: 2.2rem;
font-weight: 600;
color: #f0f9ff;
text-align: center;
letter-spacing: -0.5px;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
text-shadow: 0 4px 10px rgba(0,0,0,0.3);
}
h1 span {
background: #38bdf8;
width: 14px;
height: 14px;
border-radius: 50%;
box-shadow: 0 0 20px #38bdf8;
}
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 20px;
align-items: flex-end;
justify-content: center;
background: rgba(15, 23, 42, 0.7);
border-radius: 60px;
padding: 20px 25px;
margin-bottom: 30px;
border: 1px solid rgba(148, 163, 184, 0.2);
}
.color-picker-wrap {
display: flex;
align-items: center;
gap: 12px;
background: rgba(51, 65, 85, 0.6);
padding: 8px 18px;
border-radius: 40px;
backdrop-filter: blur(4px);
}
.color-picker-wrap label {
color: #cbd5e1;
font-weight: 500;
font-size: 0.9rem;
letter-spacing: 0.3px;
}
#bgColor {
width: 60px;
height: 60px;
border-radius: 30px;
border: 2px solid #fff;
background: #ffffff;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
transition: 0.1s;
}
#bgColor:hover {
transform: scale(1.05);
}
.action-buttons {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.btn {
background: #ffffff10;
border: 1px solid #ffffff30;
padding: 12px 28px;
border-radius: 60px;
font-size: 1rem;
font-weight: 500;
color: #f8fafc;
cursor: pointer;
transition: 0.2s;
display: flex;
align-items: center;
gap: 8px;
backdrop-filter: blur(4px);
letter-spacing: 0.3px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.btn-primary {
background: #38bdf8;
border-color: #7dd3fc;
color: #0c1a2a;
font-weight: 600;
box-shadow: 0 8px 20px rgba(56, 189, 248, 0.3);
}
.btn-primary:hover {
background: #0ea5e9;
transform: translateY(-2px);
}
.btn-outline:hover {
background: #ffffff20;
border-color: #ffffff70;
}
.btn:active {
transform: scale(0.96);
}
.canvas-section {
display: flex;
flex-direction: column;
align-items: center;
}
.canvas-container {
background-color: rgba(255, 255, 255, 0.03);
border-radius: 28px;
padding: 12px;
border: 1px dashed #afc3d8;
box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.3);
margin-bottom: 20px;
width: 100%;
display: flex;
justify-content: center;
}
canvas {
width: 100%;
height: auto;
max-width: 800px;
max-height: 500px;
border-radius: 16px;
background: repeating-conic-gradient(#2a3b4e 0% 25%, #1e2a3a 0% 50%) 50% / 20px 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
display: block;
transition: box-shadow 0.2s;
}
.upload-area {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 16px;
margin-bottom: 10px;
}
.file-label {
background: #2a3b4e;
padding: 14px 24px;
border-radius: 60px;
color: #e2e8f0;
font-weight: 500;
cursor: pointer;
transition: 0.2s;
box-shadow: 0 4px 10px rgba(0,0,0,0.2);
display: inline-flex;
align-items: center;
gap: 10px;
border: 1px solid #475569;
}
.file-label:hover {
background: #3b4f66;
border-color: #64748b;
}
.file-input {
display: none;
}
.info-text {
color: #94a3b8;
text-align: center;
font-size: 0.9rem;
margin-top: 12px;
border-top: 1px solid #334155;
padding-top: 20px;
letter-spacing: 0.2px;
}
.download-btn {
background: #22c55e;
border: none;
color: #fff;
font-weight: 600;
box-shadow: 0 6px 16px #22c55e40;
}
.download-btn:hover {
background: #16a34a;
}
.filename-badge {
font-size: 0.85rem;
background: #1e293b;
padding: 6px 18px;
border-radius: 60px;
color: #a5d8ff;
display: inline-flex;
align-items: center;
gap: 6px;
max-width: 260px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.filename-badge img {
width: 16px;
height: 16px;
filter: invert(0.8);
}
.split-line {
display: flex;
align-items: center;
gap: 16px;
margin: 16px 0;
}
.split-line span {
background: #1e3a5f;
height: 1px;
flex: 1;
opacity: 0.3;
}
@media (max-width: 700px) {
.toolbar {
flex-direction: column;
align-items: stretch;
border-radius: 32px;
}
.color-picker-wrap, .action-buttons {
justify-content: center;
}
}
</style>
</head>
<body>
<div class="card">
<h1>
<span></span> 底色转换器 <span></span>
</h1>
<!-- 上传 + 工具栏 -->
<div class="toolbar">
<div class="upload-area" style="margin-bottom:0;">
<label for="fileInput" class="file-label">
📂 选择图片
</label>
<input type="file" id="fileInput" class="file-input" accept="image/*">
<span class="filename-badge" id="fileNameDisplay">📄 未选择文件</span>
</div>
<div class="color-picker-wrap">
<label for="bgColor">🎯 目标底色</label>
<input type="color" id="bgColor" value="#ffffff">
</div>
<div class="action-buttons">
<button id="convertBtn" class="btn btn-primary">✨ 转换底色</button>
<button id="downloadBtn" class="btn download-btn">⬇️ 下载图片</button>
</div>
</div>
<!-- Canvas 预览区 -->
<div class="canvas-section">
<div class="canvas-container">
<canvas id="canvas" width="800" height="500"></canvas>
</div>
<div class="info-text">
💡 点击【选择图片】载入本地图像,再选取目标背景色,点击「转换底色」<br>
背景将变为纯色(自动去除白色或浅色背景)
</div>
</div>
</div>
<script>
(function() {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const fileInput = document.getElementById('fileInput');
const fileNameDisplay = document.getElementById('fileNameDisplay');
const bgColorInput = document.getElementById('bgColor');
const convertBtn = document.getElementById('convertBtn');
const downloadBtn = document.getElementById('downloadBtn');
// 状态: 保存原始图像数据 (ImageData 或 canvas)
let originalCanvas = null; // 存储原始图片的canvas
let hasImage = false;
// 默认画一个示例图片 (渐变+圆,方便演示)
function drawPlaceholder() {
const w = canvas.width;
const h = canvas.height;
ctx.clearRect(0, 0, w, h);
// 渐变背景
const grad = ctx.createLinearGradient(0, 0, w*0.6, h);
grad.addColorStop(0, '#faa7c0');
grad.addColorStop(0.5, '#ffe0e9');
grad.addColorStop(1, '#ffffff');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, w, h);
// 画一些图形
ctx.fillStyle = '#5b8def';
ctx.shadowColor = 'rgba(0,0,0,0.3)';
ctx.shadowBlur = 10;
ctx.beginPath();
ctx.arc(w*0.6, h*0.45, 80, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = '#eb8f8f';
ctx.shadowBlur = 14;
ctx.beginPath();
ctx.arc(w*0.3, h*0.3, 55, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = '#c0a5fb';
ctx.shadowBlur = 8;
ctx.beginPath();
ctx.arc(w*0.45, h*0.7, 65, 0, Math.PI*2);
ctx.fill();
ctx.shadowBlur = 0;
// 添加文字
ctx.font = 'bold 28px "Segoe UI", sans-serif';
ctx.fillStyle = '#003153';
ctx.fillText('示例图片', 50, 90);
// 保存为原始canvas
originalCanvas = document.createElement('canvas');
originalCanvas.width = w;
originalCanvas.height = h;
const octx = originalCanvas.getContext('2d');
octx.drawImage(canvas, 0, 0);
hasImage = true;
fileNameDisplay.innerHTML = '📄 内置示例';
}
// 重新绘制为当前原始内容 (不改变)
function restoreOriginal() {
if (originalCanvas) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(originalCanvas, 0, 0);
hasImage = true;
} else {
drawPlaceholder();
}
}
// 初始画占位图
drawPlaceholder();
// ---------- 文件上传 ----------
fileInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(event) {
const img = new Image();
img.onload = function() {
// 设置canvas尺寸适配图像比例,但限制最大宽度 800,高度 500
let imgW = img.width;
let imgH = img.height;
const maxW = 800;
const maxH = 500;
// 缩放比例
let ratio = Math.min(maxW/imgW, maxH/imgH, 1);
let scaledW = Math.floor(imgW * ratio);
let scaledH = Math.floor(imgH * ratio);
canvas.width = scaledW;
canvas.height = scaledH;
ctx.clearRect(0, 0, scaledW, scaledH);
// 背景填充为白色,便于后续处理(默认展示白色底)
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, scaledW, scaledH);
ctx.drawImage(img, 0, 0, scaledW, scaledH);
// 保存原始canvas
originalCanvas = document.createElement('canvas');
originalCanvas.width = scaledW;
originalCanvas.height = scaledH;
const octx = originalCanvas.getContext('2d');
octx.drawImage(canvas, 0, 0);
hasImage = true;
// 更新显示文件名
fileNameDisplay.textContent = `📄 ${file.name}`;
// 清掉文件input内部(但保留同文件可选)
fileInput.value = '';
};
img.src = event.target.result;
};
reader.readAsDataURL(file);
});
// ---------- 转换底色核心算法 ----------
function convertBackground(targetHex) {
if (!hasImage || !originalCanvas) {
alert('请先选择图片');
return;
}
// 获取原始图像数据
const origCtx = originalCanvas.getContext('2d', { willReadFrequently: true });
const origData = origCtx.getImageData(0, 0, originalCanvas.width, originalCanvas.height);
const data = new Uint8ClampedArray(origData.data); // 复制一份进行修改
// 解析目标RGB
let targetR = parseInt(targetHex.substr(1,2), 16);
let targetG = parseInt(targetHex.substr(3,2), 16);
let targetB = parseInt(targetHex.substr(5,2), 16);
const tolerance = 165; // 背景识别容差(大于等于245的像素都视为“浅色/白色”背景区域)
const outputR = targetR;
const outputG = targetG;
const outputB = targetB;
const totalPixels = data.length / 4;
// 第一步:扫描所有像素,找出“背景像素”——即亮度很高且颜色饱和度较低的浅色像素(近似白色、灰色、很浅的彩色)
// 阈值:RGB 每个通道 > 245 或 亮度 > 245 且饱和度低,但更通用:只要三个通道都 > 200且接近无色(最大通道差<30)都视为底色
// 但是这里图片可能含有白色物体,所以只处理“浅色占大部分”的边缘情况,为了简化,我们处理纯白色或接近白色像素。
// 可以调整策略: 将 R,G,B 都大于 245 的像素判定为背景。
const isBackground = new Uint8Array(totalPixels);
for (let i = 0; i < totalPixels; i++) {
const idx = i * 4;
const r = data[idx];
const g = data[idx+1];
const b = data[idx+2];
// 背景像素: 非常亮且饱和度低的颜色 (白色、极浅灰色)
// 增加兼容:所有通道 > 240 或 (亮度>240且max-min<15)
const maxC = Math.max(r, g, b);
const minC = Math.min(r, g, b);
if (r > 240 && g > 240 && b > 240) {
isBackground[i] = 1;
} else if (maxC > 235 && (maxC - minC) < 12) {
isBackground[i] = 1;
}
// 可额外处理接近白色的背景,否则略过
}
// 第二步:将背景像素替换为目标颜色,非背景像素保持不变
for (let i = 0; i < totalPixels; i++) {
if (isBackground[i]) {
const idx = i * 4;
data[idx] = outputR;
data[idx+1] = outputG;
data[idx+2] = outputB;
data[idx+3] = 255; // 不透明
}
}
// 写入canvas
const imageData = new ImageData(data, originalCanvas.width, originalCanvas.height);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.putImageData(imageData, 0, 0);
// 注意:转换后originalCanvas不应该更新,因为要保留原图,如果再次转换会丢失原始信息。
// 这里我们保持originalCanvas不变,但canvas预览变为已转换的图像,后续点击下载导出基于canvas即可。
}
// ---------- 事件绑定 ----------
convertBtn.addEventListener('click', function() {
const hexColor = bgColorInput.value; // #ff00aa
if (!hasImage) {
alert('请先选择一张图片');
return;
}
convertBackground(hexColor);
});
// 恢复原始图片 (其实为了用户体验,可以把未修改的原始图片存到originalCanvas,每次转换仅基于originalCanvas)
// 但为了可以在转换后再点转换,我们需要基于原始originalCanvas重建,而不是基于已经被修改的canvas。
// 改进:convertBackground 每次基于 originalCanvas 的图像数据,所以不管canvas现在是什么,都不影响。
// 现在convert函数内使用origData,完美。
// 可添加“重置”按钮?为了体验,可以双击canvas重置,但不做复杂UI,临时加一个监听: 点击文件名区域重置
// 但工具已经够用,用户点击【选择图片】会自动重置。
// 下载
downloadBtn.addEventListener('click', function() {
if (!hasImage) {
alert('没有图片可下载');
return;
}
const link = document.createElement('a');
link.download = `converted_${Date.now()}.png`;
link.href = canvas.toDataURL('image/png');
link.click();
});
// 当点击选择图片后,如果读图失败或者空,就不动。
// 同时添加一个重置小功能:双击预览恢复原始图像
canvas.addEventListener('dblclick', function() {
// 恢复显示originalCanvas
if (originalCanvas) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(originalCanvas, 0, 0);
hasImage = true;
} else {
drawPlaceholder();
}
});
// 如果文件为空,保留默认示例,同时如果用户没有上传图片但是点击转换,会提示先选择图片(默认示例也是图,可以展示转换效果)
// 但hasImage默认是true,因为drawPlaceholder设置了originalCanvas,可以转换示例图片,很棒。
// 但是目前placeholder生成的是示例,已经可以测试。
// 处理窗口变化预留(无)
// 修改颜色选择器默认白色
// 初始化: 让原始画布清晰
restoreOriginal();
// 如果样例颜色改变,用户再次点击转换,会基于originalCanvas,不受之前转换影响 (因为我们每次convert都从originalCanvas取值)
// 转换后canvas更新的图像不会更新originalCanvas,因此再次点击依旧用原图。
console.log('✅ 图片底色转换脚本已就绪。');
})();
</script>
</body>
</html>
功能与操作说明
您可以轻松上传图片并一键替换其浅色背景,非常适合快速制作纯色背景的图片。

- 图片上传与预览:页面顶部提供“选择图片”按钮,支持从本地加载常见格式的图片,加载后,图片会显示在中央的预览画布上,文件名也会同步更新。
- 核心功能操作:通过颜色选择器选取您想要的目标底色,然后点击“转换底色”按钮,工具会智能识别并替换图片中的白色或浅色背景区域,其他颜色像素将保持不变。
- 结果处理与提示:转换后的图片会立即在画布上预览,您可以使用“下载图片”按钮将其保存为PNG格式,双击预览区域可以随时恢复显示原始图片,方便对比效果。