创建一个设计师专用的色彩生成工具。核心功

2026-01-08
创建一个设计师专用的色彩生成工具。核心功能是基于色彩理论(互补色、三元色、类比色、分裂互补色)自动生成和谐的五色配色方案。界面中央应为一个交互式色轮,用户拖动主色点时,其他关联色点应根据选定的和声规则自动移动。下方展示生成的色卡,支持一键复制 HEX/RGB/HSL 代码,并提供“视觉无障碍测试”预览(模拟色盲视角)。UI 设计应极致优雅,采用纯白背景,突出色彩本身的鲜艳与纯净。
思考过程
这是一个非常专业的设计工具需求,需要创建一个优雅的色彩生成器。让我分析一下核心需求: 1. **色彩理论规则**: - 互补色 (Complementary): 180度对角 - 三元色 (Triadic): 120度等分 - 类比色 (Analogous): 30度邻近 - 分裂互补色 (Split-complementary): 150度和210度 2. **交互式色轮**: - 中央色轮 - 可拖动的主色点 - 关联色点自动移动 3. **色卡展示**: - 五色配色方案 - HEX/RGB/HSL格式 - 一键复制功能 4. **视觉无障碍测试**: - 色盲模拟(红绿色盲、蓝黄色盲等) 5. **UI设计**: - 纯白背景 - 极致优雅 - 突出色彩 让我开始编写代码: ```html <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>ChromaHarmony - 专业色彩生成工具</title> <style> /* 样式代码 */ </style> </head> <body> <!-- HTML内容 --> <script> // JavaScript代码 </script> </body> </html> ``` 现在需要实现SVG色轮绘制、拖拽交互、HSL色彩计算、色彩和谐规则应用、色盲模拟滤镜、复制功能以及优雅的动画效果。
# ChromaHarmony - 专业色彩生成工具 这是一个功能完整、视觉优雅的设计师专用色彩工具,支持多种色彩和谐规则和无障碍测试。 ```html <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>ChromaHarmony - 专业色彩生成工具</title> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet"> <style> * { margin: 0; padding: 0; box-sizing: border-box; } :root { --bg: #ffffff; --text: #1a1a2e; --text-light: #6b7280; --border: #e5e7eb; --shadow: rgba(0, 0, 0, 0.08); } body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; overflow-x: hidden; } /* Header */ .header { padding: 2rem 3rem; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border); } .logo { display: flex; align-items: center; gap: 0.75rem; } .logo-icon { width: 40px; height: 40px; background: conic-gradient( #ff6b6b, #feca57, #48dbfb, #ff9ff3, #ff6b6b ); border-radius: 10px; animation: logoSpin 10s linear infinite; } @keyframes logoSpin { to { filter: hue-rotate(360deg); } } .logo h1 { font-size: 1.5rem; font-weight: 600; letter-spacing: -0.5px; } .logo span { color: var(--text-light); font-weight: 400; } /* Main Layout */ .main-container { display: grid; grid-template-columns: 1fr 400px; gap: 0; min-height: calc(100vh - 85px); } /* Color Wheel Section */ .wheel-section { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 3rem; position: relative; } .harmony-selector { display: flex; gap: 0.5rem; margin-bottom: 2.5rem; background: #f8f9fa; padding: 6px; border-radius: 12px; } .harmony-btn { padding: 0.75rem 1.25rem; border: none; background: transparent; color: var(--text-light); font-size: 0.875rem; font-weight: 500; cursor: pointer; border-radius: 8px; transition: all 0.3s ease; font-family: inherit; } .harmony-btn:hover { color: var(--text); } .harmony-btn.active { background: white; color: var(--text); box-shadow: 0 2px 8px var(--shadow); } /* Color Wheel */ .wheel-container { position: relative; width: 420px; height: 420px; } .color-wheel { width: 100%; height: 100%; border-radius: 50%; background: conic-gradient( hsl(0, 100%, 50%), hsl(30, 100%, 50%), hsl(60, 100%, 50%), hsl(90, 100%, 50%), hsl(120, 100%, 50%), hsl(150, 100%, 50%), hsl(180, 100%, 50%), hsl(210, 100%, 50%), hsl(240, 100%, 50%), hsl(270, 100%, 50%), hsl(300, 100%, 50%), hsl(330, 100%, 50%), hsl(360, 100%, 50%) ); position: relative; box-shadow: 0 0 0 1px rgba(0,0,0,0.05), 0 20px 60px rgba(0,0,0,0.1); } .color-wheel::before { content: ''; position: absolute; inset: 0; border-radius: 50%; background: radial-gradient( circle at center, white 0%, white 25%, transparent 70% ); } .color-wheel::after { content: ''; position: absolute; inset: 35%; border-radius: 50%; background: white; box-shadow: inset 0 0 20px rgba(0,0,0,0.05), 0 0 30px rgba(255,255,255,0.8); } /* Color Points */ .color-point { position: absolute; width: 32px; height: 32px; border-radius: 50%; transform: translate(-50%, -50%); cursor: grab; border: 4px solid white; box-shadow: 0 4px 12px rgba(0,0,0,0.2), 0 0 0 1px rgba(0,0,0,0.05); transition: transform 0.15s ease, box-shadow 0.15s ease; z-index: 10; } .color-point:hover { transform: translate(-50%, -50%) scale(1.15); box-shadow: 0 6px 20px rgba(0,0,0,0.25), 0 0 0 1px rgba(0,0,0,0.05); } .color-point.dragging { cursor: grabbing; transform: translate(-50%, -50%) scale(1.2); z-index: 20; } .color-point.primary { width: 44px; height: 44px; border-width: 5px; z-index: 15; } .color-point::after { content: attr(data-label); position: absolute; top: 110%; left: 50%; transform: translateX(-50%); font-size: 0.7rem; font-weight: 600; color: var(--text); white-space: nowrap; opacity: 0; transition: opacity 0.2s; } .color-point:hover::after { opacity: 1; } /* Connection Lines */ .connection-lines { position: absolute; inset: 0; pointer-events: none; } .connection-lines line { stroke: rgba(0,0,0,0.1); stroke-width: 2; stroke-dasharray: 6 4; } /* Sidebar Panel */ .sidebar { background: #fafafa; border-left: 1px solid var(--border); padding: 2rem; display: flex; flex-direction: column; gap: 2rem; overflow-y: auto; } .section-title { font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 1px; color: var(--text-light); margin-bottom: 1rem; } /* Color Cards */ .color-cards { display: flex; flex-direction: column; gap: 0.75rem; } .color-card { background: white; border-radius: 16px; overflow: hidden; box-shadow: 0 2px 12px var(--shadow); transition: transform 0.2s, box-shadow 0.2s; } .color-card:hover { transform: translateY(-2px); box-shadow: 0 8px 24px var(--shadow); } .color-card-preview { height: 60px; position: relative; } .color-card-label { position: absolute; top: 8px; left: 12px; font-size: 0.65rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; padding: 4px 8px; border-radius: 4px; background: rgba(255,255,255,0.9); } .color-card-info { padding: 1rem; } .color-values { display: flex; flex-direction: column; gap: 0.5rem; } .color-value-row { display: flex; justify-content: space-between; align-items: center; } .color-format { font-size: 0.7rem; font-weight: 600; color: var(--text-light); width: 32px; } .color-code { font-family: 'SF Mono', 'Fira Code', monospace; font-size: 0.8rem; color: var(--text); flex: 1; padding: 0 0.5rem; } .copy-btn { background: none; border: none; padding: 6px; cursor: pointer; opacity: 0.5; transition: opacity 0.2s, transform 0.2s; border-radius: 6px; } .copy-btn:hover { opacity: 1; background: #f0f0f0; } .copy-btn:active { transform: scale(0.9); } .copy-btn svg { width: 16px; height: 16px; } /* Accessibility Section */ .accessibility-section { margin-top: auto; } .vision-modes { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.5rem; } .vision-btn { padding: 0.75rem; border: 2px solid var(--border); background: white; border-radius: 10px; font-size: 0.75rem; font-weight: 500; cursor: pointer; transition: all 0.2s; font-family: inherit; color: var(--text); } .vision-btn:hover { border-color: var(--text); } .vision-btn.active { border-color: var(--text); background: var(--text); color: white; } /* Accessibility Preview */ .preview-strip { display: flex; height: 40px; border-radius: 10px; overflow: hidden; margin-top: 1rem; box-shadow: 0 2px 8px var(--shadow); } .preview-strip-color { flex: 1; transition: background-color 0.3s; } /* Export Button */ .export-btn { width: 100%; padding: 1rem; background: var(--text); color: white; border: none; border-radius: 12px; font-size: 0.875rem; font-weight: 600; cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; font-family: inherit; display: flex; align-items: center; justify-content: center; gap: 0.5rem; } .export-btn:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(0,0,0,0.2); } /* Toast Notification */ .toast { position: fixed; bottom: 2rem; left: 50%; transform: translateX(-50%) translateY(100px); background: var(--text); color: white; padding: 1rem 1.5rem; border-radius: 12px; font-size: 0.875rem; font-weight: 500; opacity: 0; transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); z-index: 1000; display: flex; align-items: center; gap: 0.5rem; } .toast.show { opacity: 1; transform: translateX(-50%) translateY(0); } .toast svg { width: 18px; height: 18px; } /* Saturation/Lightness Sliders */ .slider-section { display: flex; flex-direction: column; gap: 1rem; } .slider-group { display: flex; flex-direction: column; gap: 0.5rem; } .slider-label { display: flex; justify-content: space-between; font-size: 0.8rem; color: var(--text-light); } .slider-label span:last-child { font-weight: 600; color: var(--text); } .slider { -webkit-appearance: none; width: 100%; height: 8px; border-radius: 4px; outline: none; cursor: pointer; } .slider::-webkit-slider-thumb { -webkit-appearance: none; width: 20px; height: 20px; border-radius: 50%; background: white; border: 3px solid var(--text); cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,0.15); } #saturationSlider { background: linear-gradient(to right, #ccc, hsl(0, 100%, 50%)); } #lightnessSlider { background: linear-gradient(to right, #000, hsl(0, 100%, 50%), #fff); } /* Harmony Info */ .harmony-info { background: white; border-radius: 12px; padding: 1rem; margin-bottom: 1rem; } .harmony-info h3 { font-size: 0.9rem; font-weight: 600; margin-bottom: 0.5rem; } .harmony-info p { font-size: 0.75rem; color: var(--text-light); line-height: 1.5; } /* Color Blindness Simulation Filter */ .color-blind-filter { position: fixed; top: 0; left: 0; width: 0; height: 0; overflow: hidden; } /* Animations */ @keyframes pulse { 0%, 100% { transform: translate(-50%, -50%) scale(1); } 50% { transform: translate(-50%, -50%) scale(1.1); } } .color-point.pulse { animation: pulse 0.5s ease; } /* Responsive */ @media (max-width: 900px) { .main-container { grid-template-columns: 1fr; } .wheel-section { padding: 2rem 1rem; } .wheel-container { width: 320px; height: 320px; } .sidebar { border-left: none; border-top: 1px solid var(--border); } .harmony-selector { flex-wrap: wrap; justify-content: center; } } </style> </head> <body> <!-- SVG Filters for Color Blindness Simulation --> <svg class="color-blind-filter"> <defs> <filter id="protanopia"> <feColorMatrix type="matrix" values=" 0.567, 0.433, 0, 0, 0 0.558, 0.442, 0, 0, 0 0, 0.242, 0.758, 0, 0 0, 0, 0, 1, 0 "/> </filter> <filter id="deuteranopia"> <feColorMatrix type="matrix" values=" 0.625, 0.375, 0, 0, 0 0.7, 0.3, 0, 0, 0 0, 0.3, 0.7, 0, 0 0, 0, 0, 1, 0 "/> </filter> <filter id="tritanopia"> <feColorMatrix type="matrix" values=" 0.95, 0.05, 0, 0, 0 0, 0.433, 0.567, 0, 0 0, 0.475, 0.525, 0, 0 0, 0, 0, 1, 0 "/> </filter> <filter id="achromatopsia"> <feColorMatrix type="matrix" values=" 0.299, 0.587, 0.114, 0, 0 0.299, 0.587, 0.114, 0, 0 0.299, 0.587, 0.114, 0, 0 0, 0, 0, 1, 0 "/> </filter> </defs> </svg> <header class="header"> <div class="logo"> <div class="logo-icon"></div> <h1>Chroma<span>Harmony</span></h1> </div> <div class="header-actions"> </div> </header> <main class="main-container"> <section class="wheel-section"> <div class="harmony-selector"> <button class="harmony-btn active" data-harmony="complementary">互补色</button> <button class="harmony-btn" data-harmony="triadic">三元色</button> <button class="harmony-btn" data-harmony="analogous">类比色</button> <button class="harmony-btn" data-harmony="split-complementary">分裂互补</button> <button class="harmony-btn" data-harmony="tetradic">四元色</button> </div> <div class="wheel-container"> <svg class="connection-lines" viewBox="0 0 420 420"> <g id="connectionGroup"></g> </svg> <div class="color-wheel" id="colorWheel"></div> <div class="color-point primary" id="primaryPoint" data-label="主色"></div> <div class="color-point" id="point1" data-label="辅色1"></div> <div class="color-point" id="point2" data-label="辅色2"></div> <div class="color-point" id="point3" data-label="辅色3"></div> <div class="color-point" id="point4" data-label="辅色4"></div> </div> </section> <aside class="sidebar"> <div class="harmony-info" id="harmonyInfo"> <h3>互补色 Complementary</h3> <p>在色轮上相隔180°的两种颜色,形成最强烈的对比,适合需要高对比度和视觉冲击力的设计。</p> </div> <div class="slider-section"> <div class="section-title">调整参数</div> <div class="slider-group"> <div class="slider-label"> <span>饱和度</span> <span id="saturationValue">80%</span> </div> <input type="range" class="slider" id="saturationSlider" min="20" max="100" value="80"> </div> <div class="slider-group"> <div class="slider-label"> <span>明度</span> <span id="lightnessValue">55%</span> </div> <input type="range" class="slider" id="lightnessSlider" min="25" max="75" value="55"> </div> </div> <div class="palette-section"> <div class="section-title">配色方案</div> <div class="color-cards" id="colorCards"></div> </div> <div class="accessibility-section"> <div class="section-title">视觉无障碍测试</div> <div class="vision-modes"> <button class="vision-btn active" data-mode="normal">正常视觉</button> <button class="vision-btn" data-mode="protanopia">红色盲</button> <button class="vision-btn" data-mode="deuteranopia">绿色盲</button> <button class="vision-btn" data-mode="tritanopia">蓝色盲</button> </div> <div class="preview-strip" id="previewStrip"></div> </div> <button class="export-btn" id="exportBtn"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/> <polyline points="7 10 12 15 17 10"/> <line x1="12" y1="15" x2="12" y2="3"/> </svg> 导出配色方案 </button> </aside> </main> <div class="toast" id="toast"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <polyline points="20 6 9 17 4 12"/> </svg> <span id="toastMessage">已复制到剪贴板</span> </div> <script> // ===== State Management ===== const state = { harmony: 'complementary', primaryHue: 0, saturation: 80, lightness: 55, visionMode: 'normal', colors: [] }; // ===== Harmony Definitions ===== const harmonies = { complementary: { name: '互补色 Complementary', description: '在色轮上相隔180°的两种颜色,形成最强烈的对比,适合需要高对比度和视觉冲击力的设计。', angles: [0, 180, 150, 210, 30] }, triadic: { name: '三元色 Triadic', description: '三种颜色在色轮上均匀分布,相隔120°,提供丰富的色彩组合同时保持平衡。', angles: [0, 120, 240, 60, 180] }, analogous: { name: '类比色 Analogous', description: '相邻的颜色组合,通常跨越30-60°,创造和谐、舒适的视觉效果。', angles: [0, 30, 60, -30, -60] }, 'split-complementary': { name: '分裂互补 Split-complementary', description: '使用主色和其互补色两侧的颜色,既有对比又不过于强烈。', angles: [0, 150, 210, 120, 240] }, tetradic: { name: '四元色 Tetradic', description: '四种颜色形成矩形关系,提供丰富的色彩选择,适合复杂的设计项目。', angles: [0, 90, 180, 270, 45] } }; // ===== Color Labels ===== const colorLabels = ['Primary', 'Secondary', 'Tertiary', 'Accent 1', 'Accent 2']; // ===== DOM Elements ===== const elements = { wheel: document.getElementById('colorWheel'), wheelContainer: document.querySelector('.wheel-container'), primaryPoint: document.getElementById('primaryPoint'), points: [ document.getElementById('primaryPoint'), document.getElementById('point1'), document.getElementById('point2'), document.getElementById('point3'), document.getElementById('point4') ], connectionGroup: document.getElementById('connectionGroup'), harmonyBtns: document.querySelectorAll('.harmony-btn'), colorCards: document.getElementById('colorCards'), saturationSlider: document.getElementById('saturationSlider'), lightnessSlider: document.getElementById('lightnessSlider'), saturationValue: document.getElementById('saturationValue'), lightnessValue: document.getElementById('lightnessValue'), visionBtns: document.querySelectorAll('.vision-btn'), previewStrip: document.getElementById('previewStrip'), harmonyInfo: document.getElementById('harmonyInfo'), toast: document.getElementById('toast'), toastMessage: document.getElementById('toastMessage'), exportBtn: document.getElementById('exportBtn') }; // ===== Color Conversion Utilities ===== function hslToRgb(h, s, l) { s /= 100; l /= 100; const c = (1 - Math.abs(2 * l - 1)) * s; const x = c * (1 - Math.abs((h / 60) % 2 - 1)); const m = l - c / 2; let r = 0, g = 0, b = 0; if (0 <= h && h < 60) { r = c; g = x; b = 0; } else if (60 <= h && h < 120) { r = x; g = c; b = 0; } else if (120 <= h && h < 180) { r = 0; g = c; b = x; } else if (180 <= h && h < 240) { r = 0; g = x; b = c; } else if (240 <= h && h < 300) { r = x; g = 0; b = c; } else { r = c; g = 0; b = x; } return { r: Math.round((r + m) * 255), g: Math.round((g + m) * 255), b: Math.round((b + m) * 255) }; } function rgbToHex(r, g, b) { return '#' + [r, g, b].map(x => x.toString(16).padStart(2, '0')).join('').toUpperCase(); } function hslToHex(h, s, l) { const rgb = hslToRgb(h, s, l); return rgbToHex(rgb.r, rgb.g, rgb.b); } function getContrastColor(h, s, l) { const rgb = hslToRgb(h, s, l); const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255; return luminance > 0.5 ? '#1a1a2e' : '#ffffff'; } function normalizeHue(hue) { return ((hue % 360) + 360) % 360; } // ===== Color Blindness Simulation ===== function simulateColorBlindness(r, g, b, type) { const matrices = { normal: [[1, 0, 0], [0, 1, 0], [0, 0, 1]], protanopia: [[0.567, 0.433, 0], [0.558, 0.442, 0], [0, 0.242, 0.758]], deuteranopia: [[0.625, 0.375, 0], [0.7, 0.3, 0], [0, 0.3, 0.7]], tritanopia: [[0.95, 0.05, 0], [0, 0.433, 0.567], [0, 0.475, 0.525]] }; const m = matrices[type] || matrices.normal; return { r: Math.round(m[0][0] * r + m[0][1] * g + m[0][2] * b), g: Math.round(m[1][0] * r + m[1][1] * g + m[1][2] * b), b: Math.round(m[2][0] * r + m[2][1] * g + m[2][2] * b) }; } // ===== Position Calculations ===== function getPointPosition(hue, radius = 160) { const centerX = 210; const centerY = 210; const angle = (hue - 90) * (Math.PI / 180); return { x: centerX + radius * Math.cos(angle), y: centerY + radius * Math.sin(angle) }; } function getHueFromPosition(x, y) { const centerX = 210; const centerY = 210; const angle = Math.atan2(y - centerY, x - centerX); let hue = angle * (180 / Math.PI) + 90; return normalizeHue(hue); } // ===== Update Functions ===== function updateColors() { const harmony = harmonies[state.harmony]; state.colors = harmony.angles.map((angle, index) => { const hue = normalizeHue(state.primaryHue + angle); const satAdjust = index === 0 ? 0 : (index % 2 === 0 ? -5 : 5); const lightAdjust = index === 0 ? 0 : (index % 2 === 0 ? 5 : -5); return { hue, saturation: Math.min(100, Math.max(20, state.saturation + satAdjust)), lightness: Math.min(75, Math.max(25, state.lightness + lightAdjust)), label: colorLabels[index] }; }); } function updatePoints() { elements.points.forEach((point, index) => { if (!state.colors[index]) return; const color = state.colors[index]; const pos = getPointPosition(color.hue, index === 0 ? 165 : 155); point.style.left = pos.x + 'px'; point.style.top = pos.y + 'px'; point.style.backgroundColor = hslToHex(color.hue, color.saturation, color.lightness); }); } function updateConnectionLines() { const group = elements.connectionGroup; group.innerHTML = ''; const positions = state.colors.map((color, index) => getPointPosition(color.hue, index === 0 ? 165 : 155) ); // Connect all points to primary for (let i = 1; i < positions.length; i++) { const line = document.createElementNS('http://www.w3.org/2000/svg', 'line'); line.setAttribute('x1', positions[0].x); line.setAttribute('y1', positions[0].y); line.setAttribute('x2', positions[i].x); line.setAttribute('y2', positions[i].y); group.appendChild(line); } } function updateColorCards() { elements.colorCards.innerHTML = state.colors.map((color, index) => { const hex = hslToHex(color.hue, color.saturation, color.lightness); const rgb = hslToRgb(color.hue, color.saturation, color.lightness); const contrastColor = getContrastColor(color.hue, color.saturation, color.lightness); return ` <div class="color-card"> <div class="color-card-preview" style="background-color: ${hex}"> <span class="color-card-label" style="color: ${contrastColor === '#ffffff' ? '#333' : '#333'}">${color.label}</span> </div> <div class="color-card-info"> <div class="color-values"> <div class="color-value-row"> <span class="color-format">HEX</span> <span class="color-code">${hex}</span> <button class="copy-btn" data-copy="${hex}"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <rect x="9" y="9" width="13" height="13" rx="2" ry="2"/> <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/> </svg> </button> </div> <div class="color-value-row"> <span class="color-format">RGB</span> <span class="color-code">${rgb.r}, ${rgb.g}, ${rgb.b}</span> <button class="copy-btn" data-copy="rgb(${rgb.r}, ${rgb.g}, ${rgb.b})"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <rect x="9" y="9" width="13" height="13" rx="2" ry="2"/> <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/> </svg> </button> </div> <div class="color-value-row"> <span class="color-format">HSL</span> <span class="color-code">${Math.round(color.hue)}°, ${color.saturation}%, ${color.lightness}%</span> <button class="copy-btn" data-copy="hsl(${Math.round(color.hue)}, ${color.saturation}%, ${color.lightness}%)"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <rect x="9" y="9" width="13" height="13" rx="2" ry="2"/> <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/> </svg> </button> </div> </div> </div> </div> `; }).join(''); // Add copy event listeners document.querySelectorAll('.copy-btn').forEach(btn => { btn.addEventListener('click', (e) => { const text = btn.dataset.copy; navigator.clipboard.writeText(text).then(() => { showToast(`已复制: ${text}`); }); }); }); } function updatePreviewStrip() { elements.previewStrip.innerHTML = state.colors.map(color => { const rgb = hslToRgb(color.hue, color.saturation, color.lightness); let displayRgb = rgb; if (state.visionMode !== 'normal') { displayRgb = simulateColorBlindness(rgb.r, rgb.g, rgb.b, state.visionMode); } return `<div class="preview-strip-color" style="background-color: rgb(${displayRgb.r}, ${displayRgb.g}, ${displayRgb.b})"></div>`; }).join(''); } function updateHarmonyInfo() { const harmony = harmonies[state.harmony]; elements.harmonyInfo.innerHTML = ` <h3>${harmony.name}</h3> <p>${harmony.description}</p> `; } function updateSliderBackgrounds() { const hue = state.primaryHue; elements.saturationSlider.style.background = `linear-gradient(to right, hsl(${hue}, 20%, ${state.lightness}%), hsl(${hue}, 100%, ${state.lightness}%))`; elements.lightnessSlider.style.background = `linear-gradient(to right, hsl(${hue}, ${state.saturation}%, 20%), hsl(${hue}, ${state.saturation}%, 50%), hsl(${hue}, ${state.saturation}%, 80%))`; } function updateAll() { updateColors(); updatePoints(); updateConnectionLines(); updateColorCards(); updatePreviewStrip(); updateSliderBackgrounds(); } // ===== Toast Notification ===== function showToast(message) { elements.toastMessage.textContent = message; elements.toast.classList.add('show'); setTimeout(() => { elements.toast.classList.remove('show'); }, 2000); } // ===== Drag Handling ===== let isDragging = false; let dragTarget = null; function handleDragStart(e) { if (e.target.classList.contains('color-point')) { isDragging = true; dragTarget = e.target; dragTarget.classList.add('dragging'); e.preventDefault(); } } function handleDragMove(e) { if (!isDragging || !dragTarget) return; const rect = elements.wheelContainer.getBoundingClientRect(); const x = (e.clientX || e.touches[0].clientX) - rect.left; const y = (e.clientY || e.touches[0].clientY) - rect.top; // Calculate distance from center const centerX = 210; const centerY = 210; const distance = Math.sqrt(Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2)); // Only update if within wheel bounds if (distance > 50 && distance < 200) { state.primaryHue = getHueFromPosition(x, y); updateAll(); } } function handleDragEnd() { if (dragTarget) { dragTarget.classList.remove('dragging'); } isDragging = false; dragTarget = null; } // ===== Event Listeners ===== // Drag events elements.wheelContainer.addEventListener('mousedown', handleDragStart); document.addEventListener('mousemove', handleDragMove); document.addEventListener('mouseup', handleDragEnd); // Touch events elements.wheelContainer.addEventListener('touchstart', handleDragStart, { passive: false }); document.addEventListener('touchmove', handleDragMove, { passive: false }); document.addEventListener('touchend', handleDragEnd); // Click on wheel elements.wheel.addEventListener('click', (e) => { const rect = elements.wheelContainer.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; const centerX = 210; const centerY = 210; const distance = Math.sqrt(Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2)); if (distance > 50 && distance < 200) { state.primaryHue = getHueFromPosition(x, y); elements.primaryPoint.classList.add('pulse'); setTimeout(() => elements.primaryPoint.classList.remove('pulse'), 500); updateAll(); } }); // Harmony buttons elements.harmonyBtns.forEach(btn => { btn.addEventListener('click', () => { elements.harmonyBtns.forEach(b => b.classList.remove('active')); btn.classList.add('active'); state.harmony = btn.dataset.harmony; updateHarmonyInfo(); updateAll(); }); }); // Sliders elements.saturationSlider.addEventListener('input', (e) => { state.saturation = parseInt(e.target.value); elements.saturationValue.textContent = state.saturation + '%'; updateAll(); }); elements.lightnessSlider.addEventListener('input', (e) => { state.lightness = parseInt(e.target.value); elements.lightnessValue.textContent = state.lightness + '%'; updateAll(); }); // Vision mode buttons elements.visionBtns.forEach(btn => { btn.addEventListener('click', () => { elements.visionBtns.forEach(b => b.classList.remove('active')); btn.classList.add('active'); state.visionMode = btn.dataset.mode; updatePreviewStrip(); }); }); // Export button elements.exportBtn.addEventListener('click', () => { const exportData = { harmony: state.harmony, colors: state.colors.map(color => ({ label: color.label, hex: hslToHex(color.hue, color.saturation, color.lightness), rgb: hslToRgb(color.hue, color.saturation, color.lightness), hsl: { h: Math.round(color.hue), s: color.saturation, l: color.lightness } })) }; // Create CSS variables string let cssVars = ':root {\n'; exportData.colors.forEach((color, i) => { cssVars += ` --color-${i + 1}: ${color.hex};\n`; }); cssVars += '}'; // Create SCSS variables let scssVars = ''; exportData.colors.forEach((color, i) => { scssVars += `$color-${color.label.toLowerCase().replace(' ', '-')}: ${color.hex};\n`; }); // Copy CSS to clipboard navigator.clipboard.writeText(cssVars + '\n\n' + scssVars).then(() => { showToast('配色方案已导出为 CSS/SCSS 变量!'); }); }); // ===== Keyboard Support ===== document.addEventListener('keydown', (e) => { if (e.key === 'ArrowLeft') { state.primaryHue = normalizeHue(state.primaryHue - 5); updateAll(); } else if (e.key === 'ArrowRight') { state.primaryHue = normalizeHue(state.primaryHue + 5); updateAll(); } }); // ===== Initialize ===== function init() { state.primaryHue = 220; // Start with a nice blue updateHarmonyInfo(); updateAll(); } init(); </script> </body> </html> ``` ## ✨ 功能亮点 ### 🎨 五种色彩和谐规则 | 规则 | 描述 | |------|------| | **互补色** | 180° 对角,最强对比 | | **三元色** | 120° 等分,丰富平衡 | | **类比色** | 30° 邻近,和谐舒适 | | **分裂互补** | 150°/210°,柔和对比 | | **四元色** | 90° 矩形,复杂多彩 | ### 🔧 核心功能 1. **交互式色轮** - 拖动或点击选取主色,关联色自动跟随 2. **实时参数调整** - 饱和度/明度滑块精细控制 3. **一键复制** - 支持 HEX/RGB/HSL 三种格式 4. **视觉无障碍测试** - 红绿蓝三种色盲模拟 5. **一键导出** - 生成 CSS/SCSS 变量代码 ### 🎯 设计特色 - 纯白极简背景,让色彩成为主角 - 精细的阴影和圆角,营造高级质感 - 流畅的动画过渡效果 - 完整的响应式适配