<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>专业颜色选择器 · Color Toolkit</title>
<style>
:root {
--bg: #0f172a;
--panel: #0b1120;
--panel-soft: #020617;
--border: rgba(148, 163, 184, 0.4);
--accent: #38bdf8;
--accent-soft: rgba(56, 189, 248, 0.16);
--text-main: #e5e7eb;
--text-soft: #9ca3af;
--radius-lg: 16px;
--radius-md: 10px;
--shadow-soft: 0 18px 40px rgba(15, 23, 42, 0.8);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
sans-serif;
background: radial-gradient(circle at top, #1e293b 0, #020617 50%, #000 100%);
color: var(--text-main);
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.app {
width: 100%;
max-width: 1120px;
background: radial-gradient(circle at top left, #0b1120 0, #020617 55%);
border-radius: 24px;
padding: 20px 22px 22px;
box-shadow: var(--shadow-soft);
border: 1px solid rgba(148, 163, 184, 0.35);
position: relative;
overflow: hidden;
}
.app::before {
content: "";
position: absolute;
inset: -40%;
background:
radial-gradient(circle at 0 0, rgba(56, 189, 248, 0.16), transparent 55%),
radial-gradient(circle at 100% 0, rgba(129, 140, 248, 0.15), transparent 55%);
opacity: 0.9;
pointer-events: none;
z-index: -1;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
margin-bottom: 16px;
padding: 4px 2px 8px;
}
header .title {
display: flex;
flex-direction: column;
gap: 4px;
}
header h1 {
margin: 0;
font-size: 1.4rem;
letter-spacing: 0.03em;
display: flex;
align-items: center;
gap: 6px;
}
header h1 span.icon {
font-size: 1.1rem;
}
header .subtitle {
font-size: 0.78rem;
color: var(--text-soft);
}
.badge {
font-size: 0.72rem;
padding: 4px 8px;
border-radius: 999px;
border: 1px solid rgba(148, 163, 184, 0.6);
background: rgba(15, 23, 42, 0.8);
display: inline-flex;
align-items: center;
gap: 4px;
white-space: nowrap;
}
.badge-dot {
width: 6px;
height: 6px;
border-radius: 999px;
background: #22c55e;
box-shadow: 0 0 0 4px rgba(34, 197, 94, 0.2);
}
.layout {
display: grid;
grid-template-columns: minmax(0, 1.6fr) minmax(0, 1.2fr);
gap: 16px;
}
@media (max-width: 768px) {
.layout {
grid-template-columns: minmax(0, 1fr);
}
.app {
padding: 16px;
border-radius: 18px;
}
}
.panel {
background: radial-gradient(circle at top, var(--panel) 0, #020617 80%);
border-radius: 18px;
padding: 12px 12px 12px;
border: 1px solid rgba(148, 163, 184, 0.35);
backdrop-filter: blur(18px);
}
.panel-main {
display: flex;
flex-direction: column;
gap: 14px;
}
.panel-info {
display: flex;
flex-direction: column;
gap: 10px;
}
.panel h2 {
font-size: 0.9rem;
margin: 0 0 6px;
color: #e5e7eb;
display: flex;
align-items: center;
gap: 6px;
}
.panel h2::before {
content: "";
width: 6px;
height: 6px;
border-radius: 999px;
background: var(--accent);
box-shadow: 0 0 0 4px rgba(56, 189, 248, 0.18);
}
.preview-area {
background: radial-gradient(circle at top, #020617 0, #020617 50%);
border-radius: var(--radius-lg);
border: 1px solid rgba(148, 163, 184, 0.45);
padding: 10px;
display: flex;
flex-direction: column;
gap: 8px;
}
.color-preview {
border-radius: 14px;
height: 210px;
position: relative;
overflow: hidden;
display: flex;
align-items: center;
padding: 16px 18px;
box-shadow: inset 0 0 0 1px rgba(15, 23, 42, 0.3);
transition: background-color 0.18s ease-out;
}
.preview-text {
position: relative;
z-index: 1;
max-width: 100%;
display: flex;
flex-direction: column;
gap: 8px;
}
.preview-title {
font-weight: 600;
font-size: 0.9rem;
letter-spacing: 0.06em;
text-transform: uppercase;
opacity: 0.95;
}
.preview-body {
font-size: 0.86rem;
line-height: 1.4;
opacity: 0.92;
}
.preview-meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.75rem;
color: var(--text-soft);
padding: 0 2px 2px;
gap: 6px;
flex-wrap: wrap;
}
.contrast-info {
opacity: 0.9;
}
.tag-row {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.tag {
font-size: 0.72rem;
padding: 2px 6px;
border-radius: 999px;
border: 1px solid rgba(148, 163, 184, 0.45);
background: rgba(15, 23, 42, 0.9);
}
.picker-row {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
padding: 6px 8px;
border-radius: var(--radius-md);
background: linear-gradient(90deg, rgba(15, 23, 42, 0.9), rgba(15, 23, 42, 0.4));
border: 1px solid rgba(148, 163, 184, 0.45);
}
.field-label {
font-size: 0.78rem;
color: var(--text-soft);
min-width: 72px;
}
.color-input {
-webkit-appearance: none;
appearance: none;
width: 60px;
height: 34px;
border-radius: 10px;
border: 1px solid rgba(148, 163, 184, 0.6);
background: transparent;
cursor: pointer;
padding: 0;
}
.color-input::-webkit-color-swatch {
border-radius: 8px;
border: none;
}
.color-input::-moz-color-swatch {
border-radius: 8px;
border: none;
}
.btn {
border-radius: 999px;
border: 1px solid rgba(148, 163, 184, 0.7);
background: radial-gradient(circle at top, rgba(15, 23, 42, 0.9) 0, #020617 70%);
color: var(--text-main);
padding: 6px 10px;
font-size: 0.78rem;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 4px;
white-space: nowrap;
transition: transform 0.1s ease, box-shadow 0.1s ease, background 0.1s ease,
border-color 0.1s ease;
}
.btn:hover {
transform: translateY(-1px);
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.7);
border-color: rgba(248, 250, 252, 0.9);
}
.btn:active {
transform: translateY(0);
box-shadow: none;
}
.btn-ghost {
background: transparent;
border-style: dashed;
border-color: rgba(148, 163, 184, 0.6);
}
.btn-small {
padding: 4px 8px;
font-size: 0.72rem;
}
.slider-group {
display: flex;
flex-direction: column;
gap: 6px;
border-radius: var(--radius-md);
padding: 8px 10px;
background: radial-gradient(circle at top left, #020617 0, #020617 80%);
border: 1px solid rgba(148, 163, 184, 0.45);
}
.slider-row {
display: grid;
grid-template-columns: 26px minmax(0, 1fr) 40px;
align-items: center;
gap: 8px;
font-size: 0.78rem;
}
.slider-row label {
color: var(--text-soft);
}
input[type="range"] {
width: 100%;
-webkit-appearance: none;
appearance: none;
height: 4px;
border-radius: 999px;
background: rgba(15, 23, 42, 0.9);
outline: none;
cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 14px;
height: 14px;
border-radius: 999px;
background: var(--accent);
border: 2px solid #0b1120;
box-shadow: 0 0 0 4px rgba(56, 189, 248, 0.28);
}
input[type="range"]::-moz-range-thumb {
width: 14px;
height: 14px;
border-radius: 999px;
background: var(--accent);
border: 2px solid #0b1120;
box-shadow: 0 0 0 4px rgba(56, 189, 248, 0.28);
}
input[type="range"]::-moz-range-track {
height: 4px;
border-radius: 999px;
background: rgba(15, 23, 42, 0.9);
}
.slider-value {
text-align: right;
font-variant-numeric: tabular-nums;
color: var(--text-soft);
}
.field {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 4px;
}
.field > label {
font-size: 0.78rem;
color: var(--text-soft);
}
.field-row {
display: flex;
gap: 6px;
align-items: center;
}
.field-row-stack {
align-items: flex-start;
}
input[type="text"] {
width: 100%;
border-radius: 999px;
border: 1px solid rgba(148, 163, 184, 0.6);
background: radial-gradient(circle at top, rgba(15, 23, 42, 0.9), #020617);
color: var(--text-main);
padding: 6px 10px;
font-size: 0.8rem;
outline: none;
}
input[type="text"]::placeholder {
color: rgba(148, 163, 184, 0.7);
}
input[type="text"]:focus {
border-color: var(--accent);
box-shadow: 0 0 0 1px rgba(56, 189, 248, 0.6);
}
textarea {
width: 100%;
min-height: 60px;
border-radius: 12px;
border: 1px solid rgba(148, 163, 184, 0.6);
background: radial-gradient(circle at top, rgba(15, 23, 42, 0.9), #020617);
color: var(--text-main);
padding: 6px 10px;
font-size: 0.78rem;
outline: none;
resize: vertical;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
line-height: 1.4;
}
textarea::placeholder {
color: rgba(148, 163, 184, 0.7);
}
textarea:focus {
border-color: var(--accent);
box-shadow: 0 0 0 1px rgba(56, 189, 248, 0.6);
}
.palette-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 4px;
}
.palette {
margin-top: 4px;
padding: 6px 8px;
border-radius: var(--radius-md);
background: radial-gradient(circle at top, var(--panel-soft), #020617);
border: 1px dashed rgba(148, 163, 184, 0.5);
display: flex;
flex-direction: column;
gap: 4px;
}
.palette-label {
font-size: 0.76rem;
color: var(--text-soft);
}
.swatch-list {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 2px;
}
.swatch {
width: 22px;
height: 22px;
border-radius: 8px;
border: 1px solid rgba(15, 23, 42, 0.8);
cursor: pointer;
outline: none;
padding: 0;
background-clip: padding-box;
box-shadow: 0 4px 10px rgba(15, 23, 42, 0.9);
transition: transform 0.08s ease, box-shadow 0.08s ease, border-color 0.08s ease;
}
.swatch:hover {
transform: translateY(-1px) scale(1.03);
box-shadow: 0 7px 16px rgba(15, 23, 42, 0.95);
border-color: rgba(248, 250, 252, 0.9);
}
.swatch:active {
transform: translateY(0) scale(0.97);
box-shadow: 0 3px 8px rgba(15, 23, 42, 0.8);
}
.theme-preview {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 4px;
}
.theme-card {
flex: 1 1 142px;
border-radius: 14px;
padding: 8px 10px;
font-size: 0.76rem;
border: 1px solid rgba(148, 163, 184, 0.45);
display: flex;
flex-direction: column;
gap: 6px;
}
.theme-light {
background: #f9fafb;
color: #111827;
}
.theme-dark {
background: #020617;
color: #e5e7eb;
}
.theme-title {
font-weight: 600;
font-size: 0.78rem;
}
.theme-body {
display: flex;
gap: 6px;
align-items: center;
flex-wrap: wrap;
}
.theme-chip {
padding: 2px 8px;
border-radius: 999px;
border: 1px solid rgba(148, 163, 184, 0.6);
font-size: 0.72rem;
background: rgba(148, 163, 184, 0.16);
}
.theme-btn {
border-radius: 999px;
padding: 3px 10px;
font-size: 0.74rem;
border: none;
cursor: default;
font-weight: 500;
}
footer {
margin-top: 12px;
padding-top: 8px;
border-top: 1px solid rgba(30, 64, 175, 0.5);
font-size: 0.72rem;
color: var(--text-soft);
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 6px;
}
.footer-right {
opacity: 0.8;
}
</style>
</head>
<body>
<div class="app">
<header>
<div class="title">
<h1><span class="icon">🎨</span>专业颜色选择器 · Color Toolkit</h1>
<div class="subtitle">
HEX / RGB / HSL 联动 · 滑块精调 · 收藏与历史 · 对比度提示 · 主题预览 & Token 导出
</div>
</div>
<div class="badge">
<span class="badge-dot"></span>
Design Ready
</div>
</header>
<main class="layout">
<!-- 左侧:预览 + 滑块 -->
<section class="panel panel-main">
<div class="preview-area">
<div id="colorPreview" class="color-preview">
<div id="previewText" class="preview-text">
<div class="preview-title">Preview Text / 示例文本</div>
<div class="preview-body">
ABC 123 — 字体对比预览<br />
中文测试:这是一段示例文字。<br />
按照当前背景色自动调整文字颜色。
</div>
</div>
</div>
<div class="preview-meta">
<div id="contrastInfo" class="contrast-info">对比度:计算中…</div>
<div class="tag-row">
<span class="tag">大色块预览</span>
<span class="tag">自动推荐浅/深色文字</span>
</div>
</div>
</div>
<div class="picker-row">
<span class="field-label">基础选择器:</span>
<input type="color" id="colorPicker" class="color-input" value="#ff6600" />
<button id="randomBtn" class="btn">随机颜色 🎲</button>
<button id="resetBtn" class="btn btn-ghost">重置默认</button>
</div>
<div class="slider-group">
<div class="slider-row">
<label for="rRange">R</label>
<input type="range" id="rRange" min="0" max="255" value="255" />
<div class="slider-value" id="rValue">255</div>
</div>
<div class="slider-row">
<label for="gRange">G</label>
<input type="range" id="gRange" min="0" max="255" value="102" />
<div class="slider-value" id="gValue">102</div>
</div>
<div class="slider-row">
<label for="bRange">B</label>
<input type="range" id="bRange" min="0" max="255" value="0" />
<div class="slider-value" id="bValue">0</div>
</div>
<div class="slider-row">
<label for="aRange">A</label>
<input type="range" id="aRange" min="0" max="100" value="100" />
<div class="slider-value" id="aValue">100%</div>
</div>
</div>
</section>
<!-- 右侧:数值输入 + 收藏 & 历史 + 预览 + 导出 -->
<section class="panel panel-info">
<h2>颜色数值</h2>
<div class="field">
<label for="hexInput">HEX(十六进制)</label>
<div class="field-row">
<input id="hexInput" type="text" value="#FF6600" placeholder="#RRGGBB" />
<button class="btn btn-small" id="copyHex">复制</button>
</div>
</div>
<div class="field">
<label for="rgbInput">RGB</label>
<div class="field-row">
<input
id="rgbInput"
type="text"
value="255, 102, 0"
placeholder="例如:255, 102, 0 或 rgb(255,102,0)"
/>
<button class="btn btn-small" id="copyRgb">复制</button>
</div>
</div>
<div class="field">
<label for="hslInput">HSL</label>
<input
id="hslInput"
type="text"
value="24, 100%, 50%"
placeholder="例如:24, 100%, 50% 或 hsl(24,100%,50%)"
/>
</div>
<h2>颜色收藏 & 历史</h2>
<div class="palette-actions">
<button class="btn btn-small" id="saveColor">保存当前颜色</button>
<button class="btn btn-small btn-ghost" id="clearHistory">清空历史</button>
</div>
<div class="palette">
<div class="palette-label">收藏:</div>
<div id="savedColors" class="swatch-list"></div>
</div>
<div class="palette">
<div class="palette-label">最近使用:</div>
<div id="historyColors" class="swatch-list"></div>
</div>
<h2>主题预览</h2>
<div class="theme-preview">
<div class="theme-card theme-light">
<div class="theme-title">浅色主题</div>
<div class="theme-body">
<div class="theme-chip">标签</div>
<button class="theme-btn">主按钮</button>
</div>
</div>
<div class="theme-card theme-dark">
<div class="theme-title">深色主题</div>
<div class="theme-body">
<div class="theme-chip">Tag</div>
<button class="theme-btn">Primary</button>
</div>
</div>
</div>
<h2>导出颜色变量</h2>
<div class="field">
<label>CSS 变量(:root)</label>
<div class="field-row field-row-stack">
<textarea id="cssExport" readonly></textarea>
<button class="btn btn-small" id="copyCssExport">复制</button>
</div>
</div>
<div class="field">
<label>Design Token JSON</label>
<div class="field-row field-row-stack">
<textarea id="jsonExport" readonly></textarea>
<button class="btn btn-small" id="copyJsonExport">复制</button>
</div>
</div>
</section>
</main>
<footer>
<div>适合前端 / 设计同学作为基础调色与导出工具使用</div>
<div class="footer-right">由 GPT-5.1 Thinking 生成 · 可自由修改/商用</div>
</footer>
</div>
<script>
const state = {
r: 255,
g: 102,
b: 0,
a: 1,
};
const colorPreview = document.getElementById("colorPreview");
const previewText = document.getElementById("previewText");
const contrastInfo = document.getElementById("contrastInfo");
const colorPicker = document.getElementById("colorPicker");
const rRange = document.getElementById("rRange");
const gRange = document.getElementById("gRange");
const bRange = document.getElementById("bRange");
const aRange = document.getElementById("aRange");
const rValue = document.getElementById("rValue");
const gValue = document.getElementById("gValue");
const bValue = document.getElementById("bValue");
const aValue = document.getElementById("aValue");
const hexInput = document.getElementById("hexInput");
const rgbInput = document.getElementById("rgbInput");
const hslInput = document.getElementById("hslInput");
const randomBtn = document.getElementById("randomBtn");
const resetBtn = document.getElementById("resetBtn");
const copyHexBtn = document.getElementById("copyHex");
const copyRgbBtn = document.getElementById("copyRgb");
const saveColorBtn = document.getElementById("saveColor");
const clearHistoryBtn = document.getElementById("clearHistory");
const savedColors = document.getElementById("savedColors");
const historyColors = document.getElementById("historyColors");
const themeLightBtn = document.querySelector(".theme-light .theme-btn");
const themeDarkBtn = document.querySelector(".theme-dark .theme-btn");
const themeLightChip = document.querySelector(".theme-light .theme-chip");
const themeDarkChip = document.querySelector(".theme-dark .theme-chip");
const cssExport = document.getElementById("cssExport");
const jsonExport = document.getElementById("jsonExport");
const copyCssExportBtn = document.getElementById("copyCssExport");
const copyJsonExportBtn = document.getElementById("copyJsonExport");
function clamp(min, max, value) {
return Math.min(max, Math.max(min, value));
}
function rgbToHex(r, g, b) {
const toHex = (v) => v.toString(16).padStart(2, "0");
return "#" + toHex(r) + toHex(g) + toHex(b);
}
function hexToRgb(hex) {
const normalized = normalizeHex(hex);
if (!normalized) return null;
const value = parseInt(normalized.slice(1), 16);
return {
r: (value >> 16) & 255,
g: (value >> 8) & 255,
b: value & 255,
};
}
function rgbToHsl(r, g, b) {
r /= 255;
g /= 255;
b /= 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h, s;
const l = (max + min) / 2;
if (max === min) {
h = s = 0;
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
default:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
return {
h: Math.round(h * 360),
s: Math.round(s * 100),
l: Math.round(l * 100),
};
}
function hslToRgb(h, s, l) {
h /= 360;
s /= 100;
l /= 100;
let r, g, b;
if (s === 0) {
r = g = b = l;
} else {
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255),
};
}
function normalizeHex(input) {
let v = (input || "").trim();
if (!v) return null;
if (!v.startsWith("#")) v = "#" + v;
if (!/^#[0-9a-fA-F]{6}$/.test(v)) return null;
return v.toUpperCase();
}
function getCurrentHex() {
return rgbToHex(state.r, state.g, state.b).toUpperCase();
}
function applyHex(hex) {
const rgb = hexToRgb(hex);
if (!rgb) return;
state.r = rgb.r;
state.g = rgb.g;
state.b = rgb.b;
syncFromState();
}
function updateContrastInfo() {
const r = state.r / 255;
const g = state.g / 255;
const b = state.b / 255;
const toLinear = (c) =>
c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
const R = toLinear(r);
const G = toLinear(g);
const B = toLinear(b);
const L = 0.2126 * R + 0.7152 * G + 0.0722 * B;
const Lwhite = 1;
const Lblack = 0;
const ratioWhite = (Lwhite + 0.05) / (L + 0.05);
const ratioBlack = (L + 0.05) / (Lblack + 0.05);
const useBlack = ratioBlack >= ratioWhite;
const recommendedColor = useBlack ? "#000000" : "#FFFFFF";
previewText.style.color = recommendedColor;
contrastInfo.textContent =
"与黑色对比度 " +
ratioBlack.toFixed(2) +
" : 1, 与白色对比度 " +
ratioWhite.toFixed(2) +
" : 1,推荐使用" +
(useBlack ? "深色文字。" : "浅色文字。");
}
function updateThemePreview() {
const hex = getCurrentHex();
const rgb = "rgb(" + state.r + "," + state.g + "," + state.b + ")";
const rgbaSoftLight =
"rgba(" + state.r + "," + state.g + "," + state.b + ",0.14)";
const rgbaSoftDark =
"rgba(" + state.r + "," + state.g + "," + state.b + ",0.25)";
if (themeLightBtn) {
themeLightBtn.style.backgroundColor = hex;
themeLightBtn.style.color = "#ffffff";
}
if (themeDarkBtn) {
themeDarkBtn.style.backgroundColor = hex;
themeDarkBtn.style.color = "#0f172a";
}
if (themeLightChip) {
themeLightChip.style.borderColor =
"rgba(" + state.r + "," + state.g + "," + state.b + ",0.35)";
themeLightChip.style.color = rgb;
themeLightChip.style.backgroundColor = rgbaSoftLight;
}
if (themeDarkChip) {
themeDarkChip.style.borderColor =
"rgba(" + state.r + "," + state.g + "," + state.b + ",0.5)";
themeDarkChip.style.color = rgb;
themeDarkChip.style.backgroundColor = rgbaSoftDark;
}
}
function updateExportSnippets() {
const hex = getCurrentHex();
const hsl = rgbToHsl(state.r, state.g, state.b);
const rgbString = state.r + ", " + state.g + ", " + state.b;
if (cssExport) {
cssExport.value =
":root {\n" +
" --color-primary: " +
hex +
";\n" +
" --color-primary-rgb: " +
rgbString +
";\n" +
" --color-primary-h: " +
hsl.h +
";\n" +
" --color-primary-s: " +
hsl.s +
"%;\n" +
" --color-primary-l: " +
hsl.l +
"%;\n" +
"}";
}
if (jsonExport) {
const token = {
primary: hex,
primaryRgb: rgbString,
primaryHsl: hsl.h + ", " + hsl.s + "%, " + hsl.l + "%",
};
jsonExport.value = JSON.stringify(token, null, 2);
}
}
function syncFromState() {
colorPreview.style.backgroundColor =
"rgba(" + state.r + "," + state.g + "," + state.b + "," + state.a + ")";
const hex = getCurrentHex();
colorPicker.value = hex;
hexInput.value = hex;
rgbInput.value = state.r + ", " + state.g + ", " + state.b;
const hsl = rgbToHsl(state.r, state.g, state.b);
hslInput.value = hsl.h + ", " + hsl.s + "%, " + hsl.l + "%";
rRange.value = state.r;
gRange.value = state.g;
bRange.value = state.b;
aRange.value = Math.round(state.a * 100);
rValue.textContent = state.r;
gValue.textContent = state.g;
bValue.textContent = state.b;
aValue.textContent = Math.round(state.a * 100) + "%";
updateContrastInfo();
updateThemePreview();
updateExportSnippets();
}
function pushSwatch(container, hex, maxCount) {
const upper = hex.toUpperCase();
const first = container.firstElementChild;
if (first && first.dataset.hex === upper) return;
Array.from(container.children).forEach((el) => {
if (el.dataset.hex === upper) {
container.removeChild(el);
}
});
const btn = document.createElement("button");
btn.className = "swatch";
btn.style.backgroundColor = upper;
btn.dataset.hex = upper;
btn.title = upper;
btn.addEventListener("click", () => {
applyHex(upper);
addToHistory(upper);
});
container.prepend(btn);
while (container.children.length > maxCount) {
container.removeChild(container.lastElementChild);
}
}
function addToHistory(hex) {
pushSwatch(historyColors, hex, 12);
}
function addToSaved(hex) {
pushSwatch(savedColors, hex, 24);
}
function softFlashButton(btn) {
if (!btn) return;
const original = btn.textContent;
btn.textContent = "已复制 ✔";
btn.disabled = true;
setTimeout(() => {
btn.textContent = original;
btn.disabled = false;
}, 900);
}
function copyText(text, btn) {
if (!text) return;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).catch(() => {});
} else {
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed";
textarea.style.opacity = "0";
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
try {
document.execCommand("copy");
} catch (e) {}
document.body.removeChild(textarea);
}
softFlashButton(btn);
}
function initEvents() {
colorPicker.addEventListener("input", () => {
applyHex(colorPicker.value);
addToHistory(getCurrentHex());
});
rRange.addEventListener("input", () => {
state.r = clamp(0, 255, parseInt(rRange.value, 10) || 0);
syncFromState();
});
gRange.addEventListener("input", () => {
state.g = clamp(0, 255, parseInt(gRange.value, 10) || 0);
syncFromState();
});
bRange.addEventListener("input", () => {
state.b = clamp(0, 255, parseInt(bRange.value, 10) || 0);
syncFromState();
});
aRange.addEventListener("input", () => {
state.a = clamp(0, 100, parseInt(aRange.value, 10) || 0) / 100;
syncFromState();
});
rRange.addEventListener("change", () => addToHistory(getCurrentHex()));
gRange.addEventListener("change", () => addToHistory(getCurrentHex()));
bRange.addEventListener("change", () => addToHistory(getCurrentHex()));
aRange.addEventListener("change", () => addToHistory(getCurrentHex()));
hexInput.addEventListener("change", () => {
const normalized = normalizeHex(hexInput.value);
if (!normalized) {
alert("HEX 格式应为 #RRGGBB");
hexInput.value = getCurrentHex();
return;
}
applyHex(normalized);
addToHistory(getCurrentHex());
});
rgbInput.addEventListener("change", () => {
const nums = (rgbInput.value || "").match(/\d+/g);
if (!nums || nums.length < 3) {
alert("RGB 输入格式不正确,示例:255, 102, 0");
rgbInput.value = state.r + ", " + state.g + ", " + state.b;
return;
}
state.r = clamp(0, 255, parseInt(nums[0], 10) || 0);
state.g = clamp(0, 255, parseInt(nums[1], 10) || 0);
state.b = clamp(0, 255, parseInt(nums[2], 10) || 0);
syncFromState();
addToHistory(getCurrentHex());
});
hslInput.addEventListener("change", () => {
const nums = (hslInput.value || "").match(/-?\d+(\.\d+)?/g);
if (!nums || nums.length < 3) {
alert("HSL 输入格式不正确,示例:24, 100%, 50%");
const hsl = rgbToHsl(state.r, state.g, state.b);
hslInput.value = hsl.h + ", " + hsl.s + "%, " + hsl.l + "%";
return;
}
const h = ((parseFloat(nums[0]) % 360) + 360) % 360;
const s = clamp(0, 100, parseFloat(nums[1]) || 0);
const l = clamp(0, 100, parseFloat(nums[2]) || 0);
const rgb = hslToRgb(h, s, l);
state.r = rgb.r;
state.g = rgb.g;
state.b = rgb.b;
syncFromState();
addToHistory(getCurrentHex());
});
randomBtn.addEventListener("click", () => {
state.r = Math.floor(Math.random() * 256);
state.g = Math.floor(Math.random() * 256);
state.b = Math.floor(Math.random() * 256);
state.a = 1;
syncFromState();
addToHistory(getCurrentHex());
});
resetBtn.addEventListener("click", () => {
state.r = 255;
state.g = 102;
state.b = 0;
state.a = 1;
syncFromState();
addToHistory(getCurrentHex());
});
copyHexBtn.addEventListener("click", () => {
copyText(getCurrentHex(), copyHexBtn);
});
copyRgbBtn.addEventListener("click", () => {
const text = state.r + ", " + state.g + ", " + state.b;
copyText(text, copyRgbBtn);
});
saveColorBtn.addEventListener("click", () => {
addToSaved(getCurrentHex());
});
clearHistoryBtn.addEventListener("click", () => {
historyColors.innerHTML = "";
});
if (copyCssExportBtn) {
copyCssExportBtn.addEventListener("click", () => {
copyText(cssExport.value, copyCssExportBtn);
});
}
if (copyJsonExportBtn) {
copyJsonExportBtn.addEventListener("click", () => {
copyText(jsonExport.value, copyJsonExportBtn);
});
}
}
function init() {
syncFromState();
addToHistory(getCurrentHex());
initEvents();
}
init();
</script>
</body>
</html>
index.html
index.html