颜色选择器edit icon

作者:
邓朝元
Fork(复制)
下载
嵌入
BUG反馈
index.html
现在支持上传本地图片了!
index.html
            
            <!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>

        
编辑器加载中
预览
控制台