<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">🌅 图片渐变工具 · 渐变色生成器</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background: #0d1117;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
padding: 1.5rem;
margin: 0;
}
.card {
max-width: 860px;
width: 100%;
background: #161b22;
border-radius: 2.5rem;
padding: 2rem 2rem 2.5rem;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.03);
transition: box-shadow 0.2s;
}
h1 {
font-weight: 500;
font-size: 1.9rem;
letter-spacing: -0.02em;
color: #f0f6fc;
display: flex;
align-items: center;
gap: 0.4rem;
margin-bottom: 1.2rem;
}
h1 small {
font-size: 1rem;
font-weight: 400;
color: #8b949e;
margin-left: 0.4rem;
}
/* 渐变画布 + 叠加指示 */
.preview-area {
position: relative;
border-radius: 28px;
background: #0d1117;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.05);
margin-bottom: 2rem;
overflow: hidden;
}
#gradientCanvas {
display: block;
width: 100%;
height: auto;
aspect-ratio: 16 / 9;
border-radius: 28px;
background: #1e1e1e;
transition: box-shadow 0.1s;
cursor: crosshair;
}
/* 控制面板 —— 两列灵活布局 */
.control-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.8rem 1.2rem;
background: #1c2128;
border-radius: 28px;
padding: 1.8rem 1.8rem 2rem;
border: 1px solid #2d333b;
}
@media (max-width: 600px) {
.card { padding: 1.2rem; }
.control-grid { grid-template-columns: 1fr; gap: 1.4rem; }
}
.color-block {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.color-block label {
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.02em;
font-weight: 500;
color: #b1bac4;
display: flex;
justify-content: space-between;
}
.color-row {
display: flex;
align-items: center;
gap: 0.7rem;
background: #0d1117;
border-radius: 60px;
padding: 0.3rem 0.3rem 0.3rem 0.8rem;
border: 1px solid #2d333b;
}
.color-row input[type="color"] {
-webkit-appearance: none;
appearance: none;
width: 42px;
height: 42px;
border: none;
background: transparent;
cursor: pointer;
border-radius: 50%;
padding: 0;
flex-shrink: 0;
}
.color-row input[type="color"]::-webkit-color-swatch-wrapper {
padding: 2px;
}
.color-row input[type="color"]::-webkit-color-swatch {
border: 1px solid #3a434e;
border-radius: 50%;
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
}
.color-row input[type="color"]::-moz-color-swatch {
border: 1px solid #3a434e;
border-radius: 50%;
}
.color-row .hex-value {
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 0.9rem;
color: #e6edf3;
background: transparent;
border: none;
padding: 0.2rem 0;
width: 85px;
letter-spacing: 0.2px;
}
.color-row .hex-value:focus {
outline: none;
color: #ffffff;
}
.angle-slider-group {
grid-column: 1 / -1;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem 1rem;
}
.angle-slider-group label {
display: flex;
align-items: center;
gap: 0.6rem;
color: #b1bac4;
font-weight: 500;
font-size: 0.9rem;
min-width: 65px;
}
.angle-slider-group input[type="range"] {
flex: 2;
min-width: 120px;
height: 6px;
background: #2d333b;
border-radius: 10px;
-webkit-appearance: none;
appearance: none;
}
.angle-slider-group input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: #58a6ff;
border: 2px solid #0d1117;
box-shadow: 0 2px 8px rgba(0,0,0,0.5);
cursor: pointer;
transition: 0.08s;
}
.angle-slider-group input[type="range"]::-moz-range-thumb {
width: 18px;
height: 18px;
border-radius: 50%;
background: #58a6ff;
border: 2px solid #0d1117;
cursor: pointer;
}
.angle-value-badge {
background: #0d1117;
border-radius: 40px;
padding: 0.2rem 1rem;
font-size: 0.9rem;
font-weight: 500;
color: #c9d1d9;
border: 1px solid #2d333b;
min-width: 60px;
text-align: center;
}
.action-buttons {
grid-column: 1 / -1;
display: flex;
flex-wrap: wrap;
gap: 0.8rem;
justify-content: flex-end;
margin-top: 0.2rem;
}
.btn {
background: #21262d;
border: 1px solid #30363d;
color: #c9d1d9;
padding: 0.5rem 1.2rem;
border-radius: 40px;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
display: inline-flex;
align-items: center;
gap: 0.4rem;
}
.btn-primary {
background: #1f6feb;
border-color: #1f6feb;
color: #ffffff;
}
.btn-primary:hover {
background: #388bfd;
border-color: #388bfd;
box-shadow: 0 4px 12px rgba(31, 111, 235, 0.3);
}
.btn:hover {
background: #2d333b;
border-color: #444c56;
}
.btn-primary:active {
transform: scale(0.96);
}
.footer-note {
margin-top: 0.8rem;
color: #484f58;
font-size: 0.7rem;
text-align: right;
letter-spacing: 0.2px;
}
.footer-note span {
color: #6e7681;
}
</style>
</head>
<body>
<div class="card">
<h1>
🎨 渐变工坊
<small>线性 · 实时</small>
</h1>
<div class="preview-area">
<canvas id="gradientCanvas" width="1600" height="900"></canvas>
</div>
<div class="control-grid">
<!-- 颜色 A -->
<div class="color-block">
<label>颜色 起点 <span id="hexLabelA" style="color: #e6edf3;">#ff6b6b</span></label>
<div class="color-row">
<input type="color" id="colorPickerA" value="#ff6b6b">
<input type="text" class="hex-value" id="hexInputA" value="#ff6b6b" maxlength="7">
</div>
</div>
<!-- 颜色 B -->
<div class="color-block">
<label>颜色 终点 <span id="hexLabelB" style="color: #e6edf3;">#4ecdc4</span></label>
<div class="color-row">
<input type="color" id="colorPickerB" value="#4ecdc4">
<input type="text" class="hex-value" id="hexInputB" value="#4ecdc4" maxlength="7">
</div>
</div>
<!-- 角度滑条 -->
<div class="angle-slider-group">
<label>
<span>↗ 角度</span>
</label>
<input type="range" id="angleSlider" min="0" max="360" value="135" step="0.5">
<span class="angle-value-badge" id="angleDisplay">135°</span>
</div>
<!-- 操作按钮 -->
<div class="action-buttons">
<button class="btn" id="randomBtn">✨ 随机渐变</button>
<button class="btn btn-primary" id="downloadBtn">⬇ 下载PNG</button>
</div>
</div>
<div class="footer-note">
<span>拖动滑块 / 取色器 — 即时渲染</span>
</div>
</div>
<script>
(function() {
// DOM 元素
const canvas = document.getElementById('gradientCanvas');
const ctx = canvas.getContext('2d', { willReadFrequently: false });
const colorPickerA = document.getElementById('colorPickerA');
const colorPickerB = document.getElementById('colorPickerB');
const hexInputA = document.getElementById('hexInputA');
const hexInputB = document.getElementById('hexInputB');
const hexLabelA = document.getElementById('hexLabelA');
const hexLabelB = document.getElementById('hexLabelB');
const angleSlider = document.getElementById('angleSlider');
const angleDisplay = document.getElementById('angleDisplay');
const randomBtn = document.getElementById('randomBtn');
const downloadBtn = document.getElementById('downloadBtn');
// 固定尺寸 (16:9)
const W = 1600, H = 900;
canvas.width = W;
canvas.height = H;
// ----- 工具函数:颜色十六进制格式化 -----
function hexFromColorInput(input) {
let val = input.value.trim();
if (val.startsWith('#')) val = val.slice(1);
// 如果输入不完整,补全或忽略 (仅渲染时会修正)
if (!/^[0-9a-fA-F]{6}$/.test(val)) {
// 尝试补全为6位, 若不能则返回原picker值
if (val.length === 3) {
val = val.split('').map(c => c + c).join('');
} else {
return input.value;
}
}
return '#' + val.toLowerCase();
}
// 同步 取色器 <=> 输入框 + 标签
function syncColorA(fromPicker = true) {
let hex;
if (fromPicker) {
hex = colorPickerA.value;
} else {
let raw = hexInputA.value.trim();
if (!raw.startsWith('#')) raw = '#' + raw;
if (/^#[0-9a-fA-F]{6}$/.test(raw)) {
hex = raw.toLowerCase();
} else {
// 非法则回退到picker
hex = colorPickerA.value;
}
}
colorPickerA.value = hex;
hexInputA.value = hex;
hexLabelA.textContent = hex;
}
function syncColorB(fromPicker = true) {
let hex;
if (fromPicker) {
hex = colorPickerB.value;
} else {
let raw = hexInputB.value.trim();
if (!raw.startsWith('#')) raw = '#' + raw;
if (/^#[0-9a-fA-F]{6}$/.test(raw)) {
hex = raw.toLowerCase();
} else {
hex = colorPickerB.value;
}
}
colorPickerB.value = hex;
hexInputB.value = hex;
hexLabelB.textContent = hex;
}
// ----- 核心:绘制渐变 -----
function drawGradient() {
const colorA = colorPickerA.value;
const colorB = colorPickerB.value;
const angleDeg = parseFloat(angleSlider.value) || 135;
const angleRad = (angleDeg - 90) * (Math.PI / 180); // 0deg = 从左到右 (对应CSS linear-gradient 0deg)
// 计算方向向量 (单位圆)
const cosA = Math.cos(angleRad);
const sinA = Math.sin(angleRad);
// 渐变起始点与结束点: 以对角线方向延伸覆盖画布
// 使用 “最长对角线” 方法:通过中心沿方向向量延伸至边界
const cx = W / 2, cy = H / 2;
// 计算延伸比例 (取绝对值保证覆盖)
const absCos = Math.abs(cosA);
const absSin = Math.abs(sinA);
let scale = 1;
if (absCos > 0.0001 || absSin > 0.0001) {
// 到左右边界的距离 / cos, 到上下边界的距离 / sin,取较大者
const distX = (W / 2) / (absCos || 0.0001);
const distY = (H / 2) / (absSin || 0.0001);
scale = Math.max(distX, distY);
} else {
// 角度为0? 退化为水平
scale = W/2;
}
const x0 = cx - cosA * scale;
const y0 = cy - sinA * scale;
const x1 = cx + cosA * scale;
const y1 = cy + sinA * scale;
// 创建渐变
const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
gradient.addColorStop(0, colorA);
gradient.addColorStop(1, colorB);
// 清除 & 绘制
ctx.clearRect(0, 0, W, H);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, W, H);
// 更新角度显示
angleDisplay.textContent = Math.round(angleDeg) + '°';
}
// ----- 随机颜色 & 角度 -----
function randomHex() {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
function applyRandom() {
const randA = randomHex();
const randB = randomHex();
colorPickerA.value = randA;
colorPickerB.value = randB;
hexInputA.value = randA;
hexInputB.value = randB;
hexLabelA.textContent = randA;
hexLabelB.textContent = randB;
// 随机角度 (0~360)
const randAngle = Math.round(Math.random() * 360);
angleSlider.value = randAngle;
angleDisplay.textContent = randAngle + '°';
drawGradient();
}
// ----- 下载图片 -----
function downloadPNG() {
const link = document.createElement('a');
link.download = `gradient_${Date.now()}.png`;
link.href = canvas.toDataURL('image/png');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
// ----- 事件绑定 -----
// 颜色A 取色器
colorPickerA.addEventListener('input', function(e) {
syncColorA(true);
drawGradient();
});
// 颜色A 输入框
hexInputA.addEventListener('input', function() {
let val = this.value.trim();
if (!val.startsWith('#')) val = '#' + val;
if (/^#[0-9a-fA-F]{6}$/.test(val)) {
colorPickerA.value = val.toLowerCase();
hexLabelA.textContent = val.toLowerCase();
drawGradient();
} else {
// 不合法时只显示标签同步,不重绘 (避免闪烁)
hexLabelA.textContent = val || '——';
}
});
hexInputA.addEventListener('blur', function() {
// 失陪时强制校正
syncColorA(false);
drawGradient();
});
// 颜色B
colorPickerB.addEventListener('input', function() {
syncColorB(true);
drawGradient();
});
hexInputB.addEventListener('input', function() {
let val = this.value.trim();
if (!val.startsWith('#')) val = '#' + val;
if (/^#[0-9a-fA-F]{6}$/.test(val)) {
colorPickerB.value = val.toLowerCase();
hexLabelB.textContent = val.toLowerCase();
drawGradient();
} else {
hexLabelB.textContent = val || '——';
}
});
hexInputB.addEventListener('blur', function() {
syncColorB(false);
drawGradient();
});
// 角度滑块
angleSlider.addEventListener('input', function() {
angleDisplay.textContent = Math.round(this.value) + '°';
drawGradient();
});
// 随机按钮
randomBtn.addEventListener('click', applyRandom);
// 下载按钮
downloadBtn.addEventListener('click', downloadPNG);
// 额外:支持回车确认输入框
hexInputA.addEventListener('keydown', (e) => { if (e.key === 'Enter') { hexInputA.blur(); } });
hexInputB.addEventListener('keydown', (e) => { if (e.key === 'Enter') { hexInputB.blur(); } });
// 初始化绘制
syncColorA(true);
syncColorB(true);
drawGradient();
// 展示随机渐变作为开场 (也展示色彩)
setTimeout(() => {
applyRandom();
}, 100);
})();
</script>
</body>
</html>
渐变工具的核心功能
您可以通过颜色选择、角度调整和随机生成来创建独特的线性渐变,并支持一键下载。

- 颜色控制:您可以通过取色器或直接输入十六进制色值来精准设置渐变的起点和终点颜色。
- 角度调节:拖动滑块可以改变渐变的方向(角度),从水平、垂直到任意对角线方向均可实现。
- 快捷操作:点击“随机渐变”按钮可以快速获得灵感,点击“下载PNG”按钮则能将当前画布上的渐变保存为图片。
标签: 渐变色
版权声明:除非特别标注,否则均为本站原创文章,转载时请以链接形式注明文章出处。