Untitled

 avatar
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