Untitled
unknown
plain_text
6 months ago
17 kB
3
Indexable
<!DOCTYPE html> <html> <head> <title>Pixel Art Engine Pro</title> <style> body { padding: 20px; font-family: Arial, sans-serif; background-color: #f0f0f0; } .container { display: flex; gap: 20px; max-width: 1200px; margin: 0 auto; } .controls { width: 300px; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .canvases { display: flex; gap: 20px; } canvas { border: 1px solid #ccc; image-rendering: pixelated; image-rendering: crisp-edges; background: #fff; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .control-group { margin-bottom: 20px; } label { display: block; margin-bottom: 5px; font-weight: bold; color: #333; } input[type="range"] { width: 100%; margin: 10px 0; } .value-display { float: right; color: #666; } button { width: 100%; padding: 10px; margin: 5px 0; border: none; border-radius: 4px; background: #4CAF50; color: white; cursor: pointer; transition: background 0.3s; } button:hover { background: #45a049; } </style> </head> <body> <div class="container"> <div class="controls"> <div class="control-group"> <label>Pixel Size: <span class="value-display" id="pixelSizeValue">8</span> </label> <input type="range" id="pixelSize" min="2" max="32" value="8"> </div> <div class="control-group"> <label>Color Count: <span class="value-display" id="colorCountValue">16</span> </label> <input type="range" id="colorCount" min="2" max="32" value="16"> </div> <div class="control-group"> <label>Edge Sharpness: <span class="value-display" id="edgeSharpnessValue">70</span> </label> <input type="range" id="edgeSharpness" min="0" max="100" value="70"> </div> <div class="control-group"> <label>Detail Level: <span class="value-display" id="detailLevelValue">50</span> </label> <input type="range" id="detailLevel" min="0" max="100" value="50"> </div> <div class="control-group"> <label>Contrast: <span class="value-display" id="contrastValue">50</span> </label> <input type="range" id="contrast" min="0" max="100" value="50"> </div> <div class="control-group"> <label>Image Upload:</label> <input type="file" id="imageInput" accept="image/*"> </div> <div class="control-group"> <button id="processButton">Process Image</button> <button id="downloadButton">Download Result</button> </div> </div> <div class="canvases"> <canvas id="sourceCanvas" width="512" height="512"></canvas> <canvas id="pixelCanvas" width="512" height="512"></canvas> </div> </div> <script> class PixelArtEngine { constructor() { this.sourceCanvas = document.getElementById('sourceCanvas'); this.pixelCanvas = document.getElementById('pixelCanvas'); this.sourceCtx = this.sourceCanvas.getContext('2d'); this.pixelCtx = this.pixelCanvas.getContext('2d'); this.setupEventListeners(); } setupEventListeners() { document.getElementById('imageInput').addEventListener('change', (e) => { this.loadImage(e.target.files[0]); }); document.getElementById('processButton').addEventListener('click', () => { this.processImage(); }); document.getElementById('downloadButton').addEventListener('click', () => { this.downloadResult(); }); ['pixelSize', 'colorCount', 'edgeSharpness', 'detailLevel', 'contrast'].forEach(id => { const slider = document.getElementById(id); const valueDisplay = document.getElementById(`${id}Value`); slider.addEventListener('input', (e) => { valueDisplay.textContent = e.target.value; if (this.sourceCanvas.width > 0) { this.processImage(); } }); }); } loadImage(file) { if (!file) return; const reader = new FileReader(); reader.onload = (e) => { const img = new Image(); img.onload = () => { const scale = Math.min(512 / img.width, 512 / img.height); const width = Math.floor(img.width * scale); const height = Math.floor(img.height * scale); const x = Math.floor((512 - width) / 2); const y = Math.floor((512 - height) / 2); this.sourceCtx.fillStyle = '#FFFFFF'; this.sourceCtx.fillRect(0, 0, 512, 512); this.sourceCtx.drawImage(img, x, y, width, height); this.processImage(); }; img.src = e.target.result; }; reader.readAsDataURL(file); } async processImage() { const pixelSize = parseInt(document.getElementById('pixelSize').value); const colorCount = parseInt(document.getElementById('colorCount').value); const edgeSharpness = parseInt(document.getElementById('edgeSharpness').value) / 100; const detailLevel = parseInt(document.getElementById('detailLevel').value) / 100; const contrast = parseInt(document.getElementById('contrast').value) / 100; const sourceData = this.sourceCtx.getImageData(0, 0, 512, 512); // Enhanced color processing const processedColors = await this.processColors(sourceData, colorCount, contrast); // Improved edge detection const edges = this.detectEdges(processedColors, edgeSharpness); // Advanced pixelation const pixelated = this.pixelate(processedColors, edges, pixelSize, detailLevel); this.pixelCtx.putImageData(pixelated, 0, 0); } async processColors(imageData, colorCount, contrast) { const pixels = []; const data = imageData.data; // Collect pixels and apply contrast for (let i = 0; i < data.length; i += 4) { const r = this.adjustContrast(data[i], contrast); const g = this.adjustContrast(data[i + 1], contrast); const b = this.adjustContrast(data[i + 2], contrast); pixels.push([r, g, b]); } // Apply k-means clustering const palette = await this.kMeans(pixels, colorCount); // Map pixels to palette colors const result = new ImageData(imageData.width, imageData.height); for (let i = 0; i < data.length; i += 4) { const pixel = [data[i], data[i + 1], data[i + 2]]; const newColor = this.findNearestColor(pixel, palette); result.data[i] = newColor[0]; result.data[i + 1] = newColor[1]; result.data[i + 2] = newColor[2]; result.data[i + 3] = 255; } return result; } adjustContrast(value, contrast) { return Math.min(255, Math.max(0, Math.round( ((value / 255 - 0.5) * (contrast + 1) + 0.5) * 255 ))); } async kMeans(pixels, k) { const colorMap = new Map(); pixels.forEach(pixel => { const key = pixel.join(','); colorMap.set(key, (colorMap.get(key) || 0) + 1); }); const uniqueColors = Array.from(colorMap.entries()) .sort((a, b) => b[1] - a[1]) .map(([color]) => color.split(',').map(Number)); // Initialize centroids with most frequent and distinct colors const centroids = [uniqueColors[0]]; while (centroids.length < k && uniqueColors.length > centroids.length) { let maxDistance = -1; let farthestColor = null; for (const color of uniqueColors) { if (centroids.some(c => this.arraysEqual(c, color))) continue; let minDistance = Math.min(...centroids.map(c => this.colorDistance(color, c))); const frequency = colorMap.get(color.join(',')); const weightedDistance = minDistance * Math.log1p(frequency); if (weightedDistance > maxDistance) { maxDistance = weightedDistance; farthestColor = color; } } if (farthestColor) centroids.push(farthestColor); } // Refine centroids let changed = true; let iterations = 0; while (changed && iterations < 50) { changed = false; const clusters = Array(k).fill().map(() => []); uniqueColors.forEach(color => { const frequency = colorMap.get(color.join(',')); const closestCentroidIndex = this.findClosestCentroidIndex(color, centroids); for (let i = 0; i < frequency; i++) { clusters[closestCentroidIndex].push(color); } }); for (let i = 0; i < k; i++) { if (clusters[i].length > 0) { const newCentroid = this.calculateCentroid(clusters[i]); if (!this.arraysEqual(newCentroid, centroids[i])) { centroids[i] = newCentroid; changed = true; } } } iterations++; } return this.optimizePalette(centroids); } optimizePalette(colors) { // Ensure black and white are in the palette const hasBlack = colors.some(c => c.every(v => v < 32)); const hasWhite = colors.some(c => c.every(v => v > 223)); if (!hasBlack) colors[colors.length - 1] = [0, 0, 0]; if (!hasWhite) colors[colors.length - 2] = [255, 255, 255]; // Sort by luminance return colors.sort((a, b) => { const lumA = (a[0] * 299 + a[1] * 587 + a[2] * 114) / 1000; const lumB = (b[0] * 299 + b[1] * 587 + b[2] * 114) / 1000; return lumA - lumB; }); } pixelate(imageData, edges, pixelSize, detailLevel) { const result = new ImageData(imageData.width, imageData.height); const data = imageData.data; const width = imageData.width; for (let y = 0; y < imageData.height; y += pixelSize) { for (let x = 0; x < width; x += pixelSize) { const blockColors = []; const edgeColors = []; // Collect colors in current block for (let py = 0; py < pixelSize && y + py < imageData.height; py++) { for (let px = 0; px < pixelSize && x + px < width; px++) { const idx = ((y + py) * width + (x + px)) * 4; const color = [data[idx], data[idx + 1], data[idx + 2]]; if (edges[(y + py) * width + (x + px)] > 128) { edgeColors.push(color); } blockColors.push(color); } } // Determine final color for block const finalColor = edgeColors.length > 0 ? this.getMostSignificantColor(edgeColors, blockColors, detailLevel) : this.getDominantColor(blockColors); // Fill block with final color for (let py = 0; py < pixelSize && y + py < imageData.height; py++) { for (let px = 0; px < pixelSize && x + px < width; px++) { const idx = ((y + py) * width + (x + px)) * 4; result.data[idx] = finalColor[0]; result.data[idx + 1] = finalColor[1]; result.data[idx + 2] = finalColor[2]; result.data[idx + 3] = 255; } } } } return result; } detectEdges(imageData, threshold) { const data = imageData.data; const width = imageData.width; const height = imageData.height; const edges = new Uint8Array(width * height); for (let y = 1; y < height - 1; y++) { for (let x = 1; x < width - 1; x++) { let gx = 0, gy = 0; for (let ky = -1; ky <= 1; ky++) { for (let kx = -1; kx <= 1; kx++) { const idx = ((y + ky) * width + (x + kx)) * 4; const val = (data[idx] + data[idx + 1] + data[idx + 2]) / 3; gx += val * this.sobelX(kx + 1, ky + 1); gy += val * this.sobelY(kx + 1, ky + 1); } } const magnitude = Math.sqrt(gx * gx + gy * gy) * threshold; edges[y * width + x] = magnitude > 128 ? 255 : 0; } } return edges; } getMostSignificantColor(edgeColors, allColors, detailLevel) { // Blend between edge colors and dominant colors based on detail level const edgeColor = this.getDominantColor(edgeColors); const dominantColor = this.getDominantColor(allColors); return [ Math.round(edgeColor[0] * detailLevel + dominantColor[0] * (1 - detailLevel)), Math.round(edgeColor[1] * detailLevel + dominantColor[1] * (1 - detailLevel)), Math.round(edgeColor[2] * detailLevel + dominantColor[2] * (1 - detailLevel)) ]; } // Utility functions findClosestCentroidIndex(color, centroids) { let minDist = Infinity; let index = 0; centroids.forEach((centroid, i) => { const dist = this.colorDistance(color, centroid); if (dist < minDist) { minDist = dist; index = i; } }); return index; } calculateCentroid(cluster) { const sum = [0, 0, 0]; cluster.forEach(color => { sum[0] += color[0]; sum[1] += color[1]; sum[2] += color[2]; }); return sum.map(v => Math.round(v / cluster.length)); } colorDistance(c1, c2) { const rmean = (c1[0] + c2[0]) / 2; const r = c1[0] - c2[0]; const g = c1[1] - c2[1]; const b = c1[2] - c2[2]; return Math.sqrt((2 + rmean/256) * r*r + 4 * g*g + (2 + (255-rmean)/256) * b*b); } getDominantColor(colors) { const colorMap = new Map(); colors.forEach(color => { const key = color.join(','); colorMap.set(key, (colorMap.get(key) || 0) + 1); }); let maxCount = 0; let dominant = colors[0]; for (const [key, count] of colorMap.entries()) { if (count > maxCount) { maxCount = count; dominant = key.split(',').map(Number); } } return dominant; } findNearestColor(color, palette) { return palette.reduce((nearest, current) => { const currentDist = this.colorDistance(color, current); const nearestDist = this.colorDistance(color, nearest); return currentDist < nearestDist ? current : nearest; }, palette[0]); } arraysEqual(a, b) { return a.length === b.length && a.every((v, i) => v === b[i]); } sobelX(x, y) { return [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]][y][x]; } sobelY(x, y) { return [[-1, -2, -1], [0, 0, 0], [1, 2, 1]][y][x]; } downloadResult() { const link = document.createElement('a'); link.download = 'pixel-art.png'; link.href = this.pixelCanvas.toDataURL(); link.click(); } } // Initialize the engine when the page loads window.addEventListener('load', () => { new PixelArtEngine(); }); </script> </body> </html>
Editor is loading...
Leave a Comment