<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">✨ 自动对比度 · 智能文本调整</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
}
body {
min-height: 100vh;
background: #0f0f1c;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem 1rem;
}
.card {
max-width: 1100px;
width: 100%;
background: rgba(255, 255, 255, 0.04);
backdrop-filter: blur(4px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 2rem;
padding: 2rem 1.8rem 2.2rem;
box-shadow: 0 18px 40px rgba(0,0,0,0.6);
}
h1 {
font-size: 1.9rem;
font-weight: 600;
letter-spacing: 1px;
color: #fff;
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 0.5rem;
border-bottom: 1px solid rgba(255,255,255,0.15);
padding-bottom: 0.8rem;
}
h1 span {
background: #7cffb0;
color: #00110a;
font-size: 0.9rem;
padding: 4px 12px;
border-radius: 40px;
font-weight: 600;
letter-spacing: 0.3px;
}
.sub {
color: #b7c0cf;
font-size: 0.95rem;
margin-top: -0.2rem;
margin-bottom: 1.8rem;
}
.grid {
display: grid;
grid-template-columns: 1.6fr 0.9fr 1.6fr;
gap: 1.5rem;
align-items: center;
}
@media (max-width: 760px) {
.grid {
grid-template-columns: 1fr;
gap: 1rem;
}
}
.preview-area {
background: #151522;
border-radius: 1.8rem;
padding: 0.8rem;
border: 1px solid #2d2d3f;
transition: all 0.1s;
}
.preview-box {
background: var(--bg-color, #3498db);
padding: 2.2rem 1.5rem;
border-radius: 1.5rem;
box-shadow: inset 0 2px 8px rgba(0,0,0,0.15);
transition: background-color 0.15s;
min-height: 200px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
border: 2px solid rgba(255,255,255,0.1);
}
.preview-title {
font-size: 2.3rem;
font-weight: 700;
line-height: 1.3;
color: var(--text-color, #fff);
text-shadow: 0 2px 6px rgba(0,0,0,0.2);
transition: color 0.15s;
word-break: break-word;
}
.preview-sub {
font-size: 1.1rem;
font-weight: 400;
margin-top: 0.65rem;
color: var(--text-color, #fff);
opacity: 0.85;
transition: color 0.15s;
}
.chip {
background: rgba(0,0,0,0.3);
backdrop-filter: blur(4px);
padding: 0.25rem 0.8rem;
border-radius: 40px;
font-size: 0.8rem;
border: 1px solid rgba(255,255,255,0.2);
display: inline-flex;
align-items: center;
gap: 6px;
}
.control-panel {
background: #1b1b2a;
padding: 1.5rem 1.3rem;
border-radius: 1.8rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
border: 1px solid #2c2c40;
}
.control-panel h3 {
color: #dde3ef;
font-weight: 500;
font-size: 1.1rem;
margin-bottom: 1.2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.color-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1.2rem;
gap: 12px;
flex-wrap: wrap;
}
.color-row label {
display: flex;
align-items: center;
gap: 8px;
color: #b3c0d6;
font-weight: 500;
font-size: 0.95rem;
}
input[type="color"] {
width: 48px;
height: 48px;
border: 1px solid rgba(255,255,255,0.3);
border-radius: 16px;
background: transparent;
cursor: pointer;
padding: 4px;
}
input[type="color"]::-webkit-color-swatch-wrapper {
padding: 0;
}
input[type="color"]::-webkit-color-swatch {
border: none;
border-radius: 12px;
}
.color-hex {
background: #00000020;
padding: 0.3rem 0.9rem;
border-radius: 40px;
font-family: 'Fira Mono', monospace;
font-size: 0.85rem;
border: 1px solid #3f3f58;
color: #e6eaf5;
}
.divider {
height: 1px;
background: linear-gradient(90deg, transparent, #3d3d5a, transparent);
margin: 1.2rem 0;
}
.auto-badge {
background: #00c2a8;
color: #00110d;
padding: 0.2rem 1rem;
border-radius: 40px;
font-size: 0.8rem;
font-weight: 600;
}
.result-info {
margin-top: 1rem;
background: #0000002c;
border-radius: 1.2rem;
padding: 0.8rem 1rem;
display: flex;
align-items: center;
justify-content: space-between;
border: 1px solid #34344b;
}
.result-item {
display: flex;
flex-direction: column;
align-items: center;
}
.result-item small {
color: #8f9ab0;
font-size: 0.7rem;
text-transform: uppercase;
}
.result-item strong {
color: #fff;
font-size: 1.3rem;
font-weight: 600;
letter-spacing: 0.5px;
}
.result-item .contrast {
background: #2c2c42;
padding: 0.2rem 0.8rem;
border-radius: 80px;
}
.toggle-manual {
background: #22223b;
border: 1px solid #3e3e5b;
border-radius: 60px;
padding: 0.25rem 0.25rem;
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 0.8rem;
}
.toggle-manual button {
flex: 1;
background: transparent;
border: none;
padding: 0.6rem 0.3rem;
border-radius: 40px;
color: #aeb8d0;
font-weight: 500;
cursor: pointer;
transition: 0.15s;
}
.toggle-manual button.active {
background: #00b896;
color: #0a1f1b;
font-weight: 600;
box-shadow: 0 2px 6px rgba(0,200,160,0.2);
}
.manual-colors {
display: flex;
gap: 1rem;
justify-content: center;
margin-top: 1rem;
}
.manual-colors input {
width: 60px;
height: 40px;
border-radius: 12px;
}
.footnote {
margin-top: 2rem;
color: #8593aa;
display: flex;
justify-content: flex-start;
gap: 18px;
flex-wrap: wrap;
font-size: 0.85rem;
border-top: 1px dashed #3a3a52;
padding-top: 1.2rem;
}
.footnote span {
background: #232338;
padding: 0.3rem 1rem;
border-radius: 50px;
display: inline-flex;
align-items: center;
gap: 6px;
}
</style>
</head>
<body>
<div class="card">
<h1>
🌈 自动对比度
<span>LUM</span>
</h1>
<div class="sub">W3C 相对亮度公式 · 自动黑/白文本</div>
<div class="grid">
<!-- 预览区 -->
<div class="preview-area">
<div class="preview-box" id="previewBox">
<div class="preview-title" id="displayText">对比文本</div>
<div class="preview-sub" id="displaySub">背景 · 自动适配</div>
<div style="margin-top: 16px; display: flex; gap: 8px; justify-content: center;">
<span class="chip" id="bgChip"></span>
<span class="chip" id="fgChip"></span>
</div>
</div>
</div>
<!-- 控制区 -->
<div class="control-panel">
<h3>
🎨 背景色
<span class="auto-badge">自动文本</span>
</h3>
<div class="color-row">
<label>
<input type="color" id="bgColorPicker" value="#3498db">
</label>
<span class="color-hex" id="bgHexDisplay">#3498DB</span>
</div>
<div class="divider"></div>
<h3 style="margin-bottom: 10px;">🧠 文本颜色模式</h3>
<div class="toggle-manual">
<button id="autoBtn" class="active" >⚡ 自动 (黑/白)</button>
<button id="manualBtn">🖌️ 手动</button>
</div>
<div id="manualControls" style="display: none;">
<div class="manual-colors">
<div>
<input type="color" id="manualFgColor" value="#ffffff" disabled>
</div>
<div>
<input type="color" id="manualBgOverride" value="#333333" disabled>
</div>
</div>
<div style="display: flex; justify-content: space-between; color: #8492a8; font-size: 0.8rem; padding: 0 0.8rem;">
<span>文字色</span>
<span>备用背景(不生效)</span>
</div>
</div>
<div class="result-info">
<div class="result-item">
<small>背景亮度</small>
<strong id="bgLuminance">0.20</strong>
</div>
<div class="result-item">
<small>文本颜色</small>
<strong id="resolvedTextColor" style="color: #fff;">#FFFFFF</strong>
</div>
<div class="result-item">
<small>对比度</small>
<strong><span id="contrastRatio" class="contrast">9.8</span></strong>
</div>
</div>
</div>
<!-- 附加信息 / 快速预设 -->
<div style="background: #161624; border-radius: 1.8rem; padding: 1.2rem; border: 1px solid #2b2b40;">
<h3 style="color: #d9e3f5; margin-bottom: 1.2rem; font-weight: 500;">🎯 预设背景</h3>
<div style="display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 1rem;">
<button class="preset-btn" style="background:#3498db" data-color="#3498db">蓝</button>
<button class="preset-btn" style="background:#e74c3c" data-color="#e74c3c">红</button>
<button class="preset-btn" style="background:#2ecc71" data-color="#2ecc71">绿</button>
<button class="preset-btn" style="background:#f1c40f" data-color="#f1c40f">黄</button>
<button class="preset-btn" style="background:#9b59b6" data-color="#9b59b6">紫</button>
<button class="preset-btn" style="background:#1abc9c" data-color="#1abc9c">青</button>
<button class="preset-btn" style="background:#000000; border:1px solid #34343f" data-color="#000000">黑</button>
<button class="preset-btn" style="background:#ffffff; color: black; border:1px solid #888;" data-color="#ffffff">白</button>
</div>
<p style="color:#a2adc2; font-size:0.9rem; line-height:1.5;">
⚡ <strong>自动对比规则</strong> · 计算背景相对亮度(Y),使用 WCAG 阈值
<span style="background:#2c2c44; padding:2px 10px; border-radius: 30px;">Y > 0.35 ✔ 文本用黑色</span>
</p>
<div style="background: #1d1d31; border-radius: 16px; padding: 0.6rem 1rem; margin-top: 0.6rem;">
<span style="color: #b7c9ea;">亮度公式: 0.2126R' + 0.7152G' + 0.0722B'</span>
</div>
</div>
</div>
<div class="footnote">
<span>📐 对比度 ≥ 4.5:1 (AA)</span>
<span>🧪 手动模式保留你的选择</span>
<span>💡 实时计算</span>
</div>
</div>
<script>
(function(){
// DOM 元素
const bgColorPicker = document.getElementById('bgColorPicker');
const bgHexDisplay = document.getElementById('bgHexDisplay');
const previewBox = document.getElementById('previewBox');
const displayText = document.getElementById('displayText');
const displaySub = document.getElementById('displaySub');
const bgChip = document.getElementById('bgChip');
const fgChip = document.getElementById('fgChip');
const bgLuminanceEl = document.getElementById('bgLuminance');
const contrastSpan = document.getElementById('contrastRatio');
const resolvedTextColorEl = document.getElementById('resolvedTextColor');
// 手动控件
const autoBtn = document.getElementById('autoBtn');
const manualBtn = document.getElementById('manualBtn');
const manualControls = document.getElementById('manualControls');
const manualFgColor = document.getElementById('manualFgColor');
const manualBgOverride = document.getElementById('manualBgOverride'); // 保留但禁用
let currentMode = 'auto'; // 'auto' 或 'manual'
let manualFg = '#ffffff'; // 手动文本色
// 预设按钮
const presetButtons = document.querySelectorAll('.preset-btn');
// ---------- 工具函数 ----------
// 将 #rgb / #rrggbb 转为 {r,g,b} 0-255
function parseHex(hex) {
if (hex.startsWith('#')) hex = hex.slice(1);
if (hex.length === 3) {
return {
r: parseInt(hex[0] + hex[0], 16),
g: parseInt(hex[1] + hex[1], 16),
b: parseInt(hex[2] + hex[2], 16)
};
}
return {
r: parseInt(hex.substr(0, 2), 16),
g: parseInt(hex.substr(2, 2), 16),
b: parseInt(hex.substr(4, 2), 16)
};
}
// 线性化通道
function linearizeChannel(c) {
let s = c / 255;
if (s <= 0.04045) {
return s / 12.92;
} else {
return Math.pow((s + 0.055) / 1.055, 2.4);
}
}
// 计算相对亮度 (YC)
function relativeLuminance(r, g, b) {
return 0.2126 * linearizeChannel(r) + 0.7152 * linearizeChannel(g) + 0.0722 * linearizeChannel(b);
}
// 计算对比度 (WCAG)
function contrastRatio(l1, l2) {
const lighter = Math.max(l1, l2);
const darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
// 转为两位hex大写
function toHex2(n) {
return n.toString(16).padStart(2, '0').toUpperCase();
}
function rgbToHex(r, g, b) {
return `#${toHex2(r)}${toHex2(g)}${toHex2(b)}`;
}
// 根据背景亮度 Y 自动选择黑色或白色 (阈值 0.35)
function autoTextColor(bgLum) {
return bgLum > 0.35 ? '#000000' : '#ffffff';
}
// 更新预览样式
function applyUpdate() {
// 获取背景色 hex
const bgHex = bgColorPicker.value;
const bgRGB = parseHex(bgHex);
const bgLum = relativeLuminance(bgRGB.r, bgRGB.g, bgRGB.b);
// 解析最终文本颜色
let textColorHex;
if (currentMode === 'auto') {
textColorHex = autoTextColor(bgLum);
} else {
// 手动模式: 使用手动文本色,并简单应用
textColorHex = manualFg;
}
// 文本色 rgb
const fgRGB = parseHex(textColorHex);
const fgLum = relativeLuminance(fgRGB.r, fgRGB.g, fgRGB.b);
const cr = contrastRatio(bgLum, fgLum);
// 更新预览变量
previewBox.style.setProperty('--bg-color', bgHex);
previewBox.style.setProperty('--text-color', textColorHex);
// 更新文本内容 (显示对比度彩蛋)
displayText.textContent = '对比文本';
const modeLabel = currentMode === 'auto' ? '⚡ 自动' : '🖌️ 手动';
displaySub.textContent = `${bgHex} · ${textColorHex}`;
// 更新 hex 显示
bgHexDisplay.textContent = bgHex.toUpperCase();
// 更新 chip
bgChip.textContent = `BG ${bgHex.toUpperCase()}`;
fgChip.textContent = `TEXT ${textColorHex.toUpperCase()}`;
// 更新数据展示
bgLuminanceEl.textContent = bgLum.toFixed(3);
contrastSpan.textContent = cr.toFixed(2);
resolvedTextColorEl.textContent = textColorHex.toUpperCase();
resolvedTextColorEl.style.color = textColorHex;
// 手动界面禁用bg override 忽略 (我们只使用手动文本色,背景没有变化)
// 控制台不需要额外操作
}
// 重置手动按钮状态
function setMode(mode) {
currentMode = mode;
if (mode === 'auto') {
autoBtn.classList.add('active');
manualBtn.classList.remove('active');
manualControls.style.display = 'none';
// 禁用手动颜色选择器
manualFgColor.disabled = true;
manualBgOverride.disabled = true;
} else {
manualBtn.classList.add('active');
autoBtn.classList.remove('active');
manualControls.style.display = 'block';
manualFgColor.disabled = false;
manualBgOverride.disabled = true; // 无效果只是摆设
// 如果当前没有手动色值,从当前文本色获取
if (!manualFg) manualFg = '#ffffff';
}
applyUpdate();
}
// 监听背景颜色变化
bgColorPicker.addEventListener('input', function(e) {
applyUpdate();
});
// 模式切换
autoBtn.addEventListener('click', function() {
setMode('auto');
});
manualBtn.addEventListener('click', function() {
setMode('manual');
});
// 手动颜色变化
manualFgColor.addEventListener('input', function(e) {
manualFg = e.target.value;
applyUpdate();
});
// 预设按钮
presetButtons.forEach(btn => {
btn.addEventListener('click', function() {
const color = this.dataset.color;
bgColorPicker.value = color;
applyUpdate();
});
});
// 手动模式下当更改背景时也为手动的颜色显示 (没有影响但保留)
// 额外: 切换预设时同时保留手动模式的颜色
// 初始化
function init() {
// 设定初始背景色
bgColorPicker.value = '#3498db';
manualFg = '#ffffff';
manualBgOverride.value = '#333333'; // 无实际作用,占位
setMode('auto');
applyUpdate();
}
init();
})();
</script>
</body>
</html>
核心机制与操作说明
您可以通过调整背景色或选择预设,直观地看到文本颜色的智能变化,其核心逻辑基于 WCAG 标准。

- 自动对比度算法:脚本使用 WCAG 规定的相对亮度公式(0.2126R‘ + 0.7152G’ + 0.0722B‘)计算背景色亮度,当背景亮度高于阈值(Y > 0.35)时,自动选用黑色文本;否则选用白色文本,以确保足够的对比度(通常达到 AA 级别)。
- 双模式切换:您可以在“自动”和“手动”模式间切换,自动模式完全由算法决定文本颜色;手动模式则允许您通过颜色选择器自由指定文本颜色,但同时会显示该手动颜色与背景的实际对比度,方便您自行判断。
- 实时反馈与预设:所有调整(背景色、模式、手动颜色)都会立即反映在预览卡片上,并同步更新背景亮度、文本颜色和对比度数值,页面右侧提供了多种预设背景色按钮,方便您快速测试不同颜色方案的效果。