pic-generate / index.html
ccookie's picture
Add 3 files
5109da1 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图片色卡提取工具</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.color-card {
transition: all 0.3s ease;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.color-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
}
.color-value {
font-family: 'Courier New', monospace;
cursor: pointer;
}
.copied-message {
opacity: 0;
transition: opacity 0.3s ease;
}
.show-copied {
opacity: 1;
}
.color-preview {
height: 120px;
border-top-left-radius: 0.5rem;
border-top-right-radius: 0.5rem;
}
#imagePreview {
max-height: 400px;
max-width: 100%;
border-radius: 0.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
#dropZone {
border: 2px dashed #cbd5e0;
transition: all 0.3s ease;
}
#dropZone.drag-over {
border-color: #4299e1;
background-color: #ebf8ff;
}
.progress-bar {
height: 6px;
background-color: #e2e8f0;
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background-color: #4299e1;
transition: width 0.3s ease;
}
</style>
</head>
<body class="bg-gray-100 min-h-screen">
<div class="container mx-auto px-4 py-8">
<header class="mb-8 text-center">
<h1 class="text-4xl font-bold text-gray-800 mb-2">图片色卡提取工具</h1>
<p class="text-gray-600 max-w-2xl mx-auto">上传图片自动提取主要颜色,生成美观的色卡</p>
</header>
<div class="bg-white rounded-xl shadow-md p-6 mb-8">
<div id="dropZone" class="flex flex-col items-center justify-center p-8 rounded-lg mb-4">
<i class="fas fa-cloud-upload-alt text-4xl text-blue-500 mb-3"></i>
<p class="text-gray-700 mb-2">拖放图片到此处或</p>
<label for="imageUpload" class="cursor-pointer bg-blue-500 hover:bg-blue-600 text-white px-6 py-2 rounded-lg font-medium transition-colors inline-block">
<i class="fas fa-folder-open mr-2"></i>选择图片
</label>
<input type="file" id="imageUpload" accept="image/*" class="hidden">
<p class="text-gray-500 text-sm mt-3">支持 JPG, PNG, GIF 等格式</p>
</div>
<div class="flex items-center mb-4 hidden" id="progressContainer">
<div class="progress-bar flex-1 mr-2">
<div class="progress-fill" id="progressFill" style="width: 0%"></div>
</div>
<span class="text-sm text-gray-600" id="progressText">0%</span>
</div>
<div class="flex flex-col md:flex-row gap-6">
<div class="flex-1">
<h3 class="text-lg font-semibold mb-3 text-gray-800">图片预览</h3>
<div class="flex items-center justify-center bg-gray-100 rounded-lg p-4 min-h-48">
<img id="imagePreview" class="hidden" alt="图片预览">
<p id="noImageText" class="text-gray-500">图片将显示在这里</p>
</div>
</div>
<div class="flex-1">
<h3 class="text-lg font-semibold mb-3 text-gray-800">提取设置</h3>
<div class="space-y-4">
<div>
<label for="colorCount" class="block text-sm font-medium text-gray-700 mb-1">提取颜色数量</label>
<select id="colorCount" class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="5">5种</option>
<option value="8" selected>8种</option>
<option value="12">12种</option>
<option value="16">16种</option>
</select>
</div>
<div>
<label for="algorithm" class="block text-sm font-medium text-gray-700 mb-1">提取算法</label>
<select id="algorithm" class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="kmeans">K-means聚类</option>
<option value="medianCut" selected>中值切割</option>
<option value="simple">简单采样</option>
</select>
</div>
<div class="flex items-center">
<input type="checkbox" id="ignoreWhite" class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
<label for="ignoreWhite" class="ml-2 block text-sm text-gray-700">忽略白色/接近白色</label>
</div>
<div class="flex items-center">
<input type="checkbox" id="ignoreBlack" class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
<label for="ignoreBlack" class="ml-2 block text-sm text-gray-700">忽略黑色/接近黑色</label>
</div>
<button id="extractBtn" class="w-full bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-lg font-medium transition-colors flex items-center justify-center disabled:opacity-50" disabled>
<i class="fas fa-palette mr-2"></i>提取颜色
</button>
</div>
</div>
</div>
</div>
<div id="resultsSection" class="hidden">
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-bold text-gray-800">提取的颜色</h2>
<div class="flex gap-2">
<button id="copyAllBtn" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg flex items-center transition-colors">
<i class="fas fa-copy mr-2"></i>复制全部
</button>
<button id="exportBtn" class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-lg flex items-center transition-colors">
<i class="fas fa-file-export mr-2"></i>导出色卡
</button>
</div>
</div>
<div id="colorGrid" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-6">
<!-- 颜色卡片将在这里生成 -->
</div>
</div>
<div id="toast" class="fixed bottom-4 right-4 bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg hidden">
颜色已复制到剪贴板!
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const dropZone = document.getElementById('dropZone');
const imageUpload = document.getElementById('imageUpload');
const imagePreview = document.getElementById('imagePreview');
const noImageText = document.getElementById('noImageText');
const extractBtn = document.getElementById('extractBtn');
const colorGrid = document.getElementById('colorGrid');
const resultsSection = document.getElementById('resultsSection');
const colorCount = document.getElementById('colorCount');
const algorithm = document.getElementById('algorithm');
const ignoreWhite = document.getElementById('ignoreWhite');
const ignoreBlack = document.getElementById('ignoreBlack');
const copyAllBtn = document.getElementById('copyAllBtn');
const exportBtn = document.getElementById('exportBtn');
const toast = document.getElementById('toast');
const progressContainer = document.getElementById('progressContainer');
const progressFill = document.getElementById('progressFill');
const progressText = document.getElementById('progressText');
let uploadedImage = null;
let extractedColors = [];
// 拖放功能
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
dropZone.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, unhighlight, false);
});
function highlight() {
dropZone.classList.add('drag-over');
}
function unhighlight() {
dropZone.classList.remove('drag-over');
}
dropZone.addEventListener('drop', handleDrop, false);
function handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
if (files.length > 0 && files[0].type.match('image.*')) {
handleImageUpload(files[0]);
}
}
// 文件上传处理
imageUpload.addEventListener('change', function() {
if (this.files && this.files[0]) {
handleImageUpload(this.files[0]);
}
});
function handleImageUpload(file) {
const reader = new FileReader();
reader.onload = function(e) {
imagePreview.src = e.target.result;
imagePreview.classList.remove('hidden');
noImageText.classList.add('hidden');
// 创建新的Image对象以便后续处理
uploadedImage = new Image();
uploadedImage.onload = function() {
extractBtn.disabled = false;
};
uploadedImage.src = e.target.result;
};
reader.readAsDataURL(file);
}
// 提取颜色按钮点击事件
extractBtn.addEventListener('click', function() {
if (!uploadedImage) return;
const count = parseInt(colorCount.value);
const algo = algorithm.value;
const ignoreW = ignoreWhite.checked;
const ignoreB = ignoreBlack.checked;
progressContainer.classList.remove('hidden');
progressFill.style.width = '0%';
progressText.textContent = '0%';
// 使用setTimeout让UI有时间更新
setTimeout(() => {
extractedColors = extractColorsFromImage(uploadedImage, count, algo, ignoreW, ignoreB);
displayColorCards(extractedColors);
resultsSection.classList.remove('hidden');
progressContainer.classList.add('hidden');
}, 100);
});
// 从图片中提取颜色的函数
function extractColorsFromImage(image, colorCount, algorithm, ignoreWhite, ignoreBlack) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const width = Math.min(image.width, 200); // 缩小尺寸以提高性能
const height = Math.min(image.height, 200);
canvas.width = width;
canvas.height = height;
// 绘制图片到canvas
ctx.drawImage(image, 0, 0, width, height);
// 获取像素数据
const imageData = ctx.getImageData(0, 0, width, height).data;
const pixels = [];
// 收集所有像素颜色
for (let i = 0; i < imageData.length; i += 4) {
const r = imageData[i];
const g = imageData[i + 1];
const b = imageData[i + 2];
const a = imageData[i + 3];
// 忽略透明度低的像素
if (a < 128) continue;
// 检查是否忽略白色/黑色
if (ignoreWhite && isNearWhite(r, g, b)) continue;
if (ignoreBlack && isNearBlack(r, g, b)) continue;
pixels.push([r, g, b]);
// 更新进度
if (i % 400 === 0) {
const progress = Math.min(100, Math.floor((i / imageData.length) * 100));
progressFill.style.width = `${progress}%`;
progressText.textContent = `${progress}%`;
}
}
// 根据选择的算法提取主要颜色
let colors;
switch (algorithm) {
case 'kmeans':
colors = kMeansClustering(pixels, colorCount);
break;
case 'medianCut':
colors = medianCut(pixels, colorCount);
break;
case 'simple':
colors = simpleSampling(pixels, colorCount);
break;
default:
colors = medianCut(pixels, colorCount);
}
// 转换为十六进制并命名
return colors.map(color => {
const hex = rgbToHex(color[0], color[1], color[2]);
return {
name: `Color ${hex}`,
value: hex
};
});
}
// 辅助函数:判断是否接近白色
function isNearWhite(r, g, b, threshold = 220) {
return r > threshold && g > threshold && b > threshold;
}
// 辅助函数:判断是否接近黑色
function isNearBlack(r, g, b, threshold = 30) {
return r < threshold && g < threshold && b < threshold;
}
// RGB转十六进制
function rgbToHex(r, g, b) {
return '#' + [r, g, b].map(x => {
const hex = x.toString(16);
return hex.length === 1 ? '0' + hex : hex;
}).join('');
}
// 简单采样算法
function simpleSampling(pixels, count) {
const step = Math.floor(pixels.length / count);
const colors = [];
for (let i = 0; i < pixels.length && colors.length < count; i += step) {
colors.push(pixels[i]);
}
return colors;
}
// 中值切割算法
function medianCut(pixels, count) {
let buckets = [pixels];
while (buckets.length < count) {
const newBuckets = [];
for (const bucket of buckets) {
if (bucket.length === 0) continue;
// 找到颜色范围最大的通道
let rMin = 255, rMax = 0;
let gMin = 255, gMax = 0;
let bMin = 255, bMax = 0;
for (const [r, g, b] of bucket) {
rMin = Math.min(rMin, r);
rMax = Math.max(rMax, r);
gMin = Math.min(gMin, g);
gMax = Math.max(gMax, g);
bMin = Math.min(bMin, b);
bMax = Math.max(bMax, b);
}
const rRange = rMax - rMin;
const gRange = gMax - gMin;
const bRange = bMax - bMin;
const maxRange = Math.max(rRange, gRange, bRange);
const channel = maxRange === rRange ? 0 : (maxRange === gRange ? 1 : 2);
// 按选定通道排序
bucket.sort((a, b) => a[channel] - b[channel]);
// 找到中值并分割
const medianIndex = Math.floor(bucket.length / 2);
newBuckets.push(bucket.slice(0, medianIndex));
newBuckets.push(bucket.slice(medianIndex));
}
buckets = newBuckets;
if (buckets.length >= count) break;
}
// 计算每个桶的平均颜色
return buckets.map(bucket => {
if (bucket.length === 0) return [0, 0, 0];
let rSum = 0, gSum = 0, bSum = 0;
for (const [r, g, b] of bucket) {
rSum += r;
gSum += g;
bSum += b;
}
return [
Math.round(rSum / bucket.length),
Math.round(gSum / bucket.length),
Math.round(bSum / bucket.length)
];
});
}
// K-means聚类算法
function kMeansClustering(pixels, k, maxIterations = 10) {
// 随机选择初始中心点
let centroids = [];
const step = Math.floor(pixels.length / k);
for (let i = 0; i < k; i++) {
centroids.push(pixels[i * step]);
}
let clusters;
for (let iter = 0; iter < maxIterations; iter++) {
// 分配每个点到最近的中心点
clusters = Array(k).fill().map(() => []);
for (const pixel of pixels) {
let minDist = Infinity;
let closestCentroid = 0;
for (let i = 0; i < centroids.length; i++) {
const dist = colorDistance(pixel, centroids[i]);
if (dist < minDist) {
minDist = dist;
closestCentroid = i;
}
}
clusters[closestCentroid].push(pixel);
}
// 计算新的中心点
const newCentroids = [];
for (const cluster of clusters) {
if (cluster.length === 0) {
// 如果集群为空,保留旧的中心点
newCentroids.push(centroids[newCentroids.length]);
continue;
}
let rSum = 0, gSum = 0, bSum = 0;
for (const [r, g, b] of cluster) {
rSum += r;
gSum += g;
bSum += b;
}
newCentroids.push([
Math.round(rSum / cluster.length),
Math.round(gSum / cluster.length),
Math.round(bSum / cluster.length)
]);
}
// 检查是否收敛
let converged = true;
for (let i = 0; i < centroids.length; i++) {
if (colorDistance(centroids[i], newCentroids[i]) > 5) {
converged = false;
break;
}
}
centroids = newCentroids;
if (converged) break;
}
return centroids;
}
// 计算两个颜色之间的距离
function colorDistance(c1, c2) {
const dr = c1[0] - c2[0];
const dg = c1[1] - c2[1];
const db = c1[2] - c2[2];
return Math.sqrt(dr * dr + dg * dg + db * db);
}
// 显示颜色卡片
function displayColorCards(colors) {
colorGrid.innerHTML = '';
colors.forEach(color => {
const colorCard = document.createElement('div');
colorCard.className = 'color-card bg-white rounded-lg overflow-hidden';
// 计算文本颜色基于背景亮度
const hex = color.value.replace('#', '');
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
const textColor = brightness > 128 ? 'text-gray-900' : 'text-white';
colorCard.innerHTML = `
<div class="color-preview" style="background-color: ${color.value};"></div>
<div class="p-4">
<h3 class="font-semibold text-lg mb-1">${color.name}</h3>
<div class="flex items-center justify-between mt-2">
<span class="color-value ${textColor} bg-black bg-opacity-20 px-2 py-1 rounded" data-value="${color.value}">${color.value}</span>
<div class="copied-message text-sm text-gray-600">已复制!</div>
</div>
</div>
`;
colorGrid.appendChild(colorCard);
});
// 添加点击事件到所有颜色值
document.querySelectorAll('.color-value').forEach(el => {
el.addEventListener('click', function() {
const colorValue = this.getAttribute('data-value');
navigator.clipboard.writeText(colorValue).then(() => {
// 显示通知
toast.classList.remove('hidden');
setTimeout(() => {
toast.classList.add('hidden');
}, 2000);
// 显示特定卡片上的复制消息
const copiedMsg = this.nextElementSibling;
copiedMsg.classList.add('show-copied');
setTimeout(() => {
copiedMsg.classList.remove('show-copied');
}, 1000);
});
});
});
}
// 复制全部按钮
copyAllBtn.addEventListener('click', function() {
const allColors = extractedColors.map(c => c.value).join('\n');
navigator.clipboard.writeText(allColors).then(() => {
toast.textContent = '所有颜色已复制到剪贴板!';
toast.classList.remove('hidden');
setTimeout(() => {
toast.classList.add('hidden');
}, 2000);
});
});
// 导出色卡按钮
exportBtn.addEventListener('click', function() {
if (!extractedColors.length) return;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const cardWidth = 200;
const cardHeight = 150;
const margin = 10;
// 计算画布大小
const cols = Math.min(4, extractedColors.length);
const rows = Math.ceil(extractedColors.length / cols);
canvas.width = cols * (cardWidth + margin) + margin;
canvas.height = rows * (cardHeight + margin) + margin + 40; // 额外空间用于标题
// 绘制背景
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 添加标题
ctx.fillStyle = '#333333';
ctx.font = 'bold 20px Arial';
ctx.textAlign = 'center';
ctx.fillText('提取的颜色色卡', canvas.width / 2, 30);
// 绘制颜色卡片
ctx.font = '14px Arial';
ctx.textAlign = 'left';
for (let i = 0; i < extractedColors.length; i++) {
const color = extractedColors[i];
const col = i % cols;
const row = Math.floor(i / cols);
const x = margin + col * (cardWidth + margin);
const y = 40 + margin + row * (cardHeight + margin);
// 绘制颜色块
ctx.fillStyle = color.value;
ctx.fillRect(x, y, cardWidth, cardHeight * 0.7);
// 绘制信息区域
ctx.fillStyle = '#f8f8f8';
ctx.fillRect(x, y + cardHeight * 0.7, cardWidth, cardHeight * 0.3);
// 绘制颜色信息
ctx.fillStyle = '#333333';
ctx.fillText(color.name, x + 10, y + cardHeight * 0.7 + 25);
ctx.fillText(color.value, x + 10, y + cardHeight * 0.7 + 45);
}
// 导出为PNG
const link = document.createElement('a');
link.download = 'color-palette.png';
link.href = canvas.toDataURL('image/png');
link.click();
});
});
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=ccookie/pic-generate" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>