sadasdad / index.html
pjayofficial's picture
Add 2 files
07a27e6 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Animated Image Dithering Editor</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>
.canvas-container {
position: relative;
margin: 0 auto;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
border-radius: 0.5rem;
overflow: hidden;
}
canvas {
display: block;
max-width: 100%;
height: auto;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
color: white;
font-size: 1.5rem;
z-index: 10;
border-radius: 0.5rem;
}
.range-slider {
-webkit-appearance: none;
width: 100%;
height: 8px;
border-radius: 4px;
background: #e2e8f0;
outline: none;
}
.range-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: #4f46e5;
cursor: pointer;
transition: all 0.2s;
}
.range-slider::-webkit-slider-thumb:hover {
transform: scale(1.2);
background: #6366f1;
}
.range-slider::-moz-range-thumb {
width: 18px;
height: 18px;
border-radius: 50%;
background: #4f46e5;
cursor: pointer;
transition: all 0.2s;
}
.range-slider::-moz-range-thumb:hover {
transform: scale(1.2);
background: #6366f1;
}
.algorithm-btn.active {
background-color: #4f46e5;
color: white;
}
.dropzone {
border: 2px dashed #cbd5e1;
transition: all 0.3s;
}
.dropzone:hover, .dropzone.dragover {
border-color: #4f46e5;
background-color: #f8fafc;
}
.animation-preview {
width: 100%;
height: 60px;
background: repeating-linear-gradient(
45deg,
#f8fafc,
#f8fafc 10px,
#e2e8f0 10px,
#e2e8f0 20px
);
border-radius: 0.5rem;
margin-top: 0.5rem;
overflow: hidden;
position: relative;
}
.animation-indicator {
position: absolute;
top: 0;
left: 0;
width: 20%;
height: 100%;
background-color: rgba(79, 70, 229, 0.3);
border-radius: 0.5rem;
transition: left 0.5s ease;
}
@keyframes pulse {
0% { opacity: 0.5; }
50% { opacity: 1; }
100% { opacity: 0.5; }
}
.pulse {
animation: pulse 2s infinite;
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<div class="container mx-auto px-4 py-8">
<header class="text-center mb-8">
<h1 class="text-4xl font-bold text-gray-800 mb-2">Animated Pixel Art Editor</h1>
<p class="text-gray-600 max-w-2xl mx-auto">Transform your images with customizable dithering and animation effects. Create dynamic pixel art with ease.</p>
</header>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Left Panel - Controls -->
<div class="bg-white p-6 rounded-xl shadow-md">
<h2 class="text-xl font-semibold text-gray-800 mb-4">Controls</h2>
<!-- Image Upload -->
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">Upload Image</label>
<div id="dropzone" class="dropzone rounded-lg p-6 text-center cursor-pointer">
<input type="file" id="imageInput" accept="image/*" class="hidden">
<div class="flex flex-col items-center justify-center">
<i class="fas fa-cloud-upload-alt text-4xl text-gray-400 mb-2"></i>
<p class="text-gray-500">Drag & drop an image or click to browse</p>
<p class="text-xs text-gray-400 mt-1">Supports JPG, PNG, GIF</p>
</div>
</div>
</div>
<!-- Dithering Algorithm -->
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">Dithering Algorithm</label>
<div class="grid grid-cols-2 gap-2">
<button data-algorithm="none" class="algorithm-btn py-2 px-3 rounded-md bg-gray-100 hover:bg-gray-200 transition">None</button>
<button data-algorithm="floydSteinberg" class="algorithm-btn py-2 px-3 rounded-md bg-gray-100 hover:bg-gray-200 transition">Floyd-Steinberg</button>
<button data-algorithm="atkinson" class="algorithm-btn py-2 px-3 rounded-md bg-gray-100 hover:bg-gray-200 transition">Atkinson</button>
<button data-algorithm="bayer" class="algorithm-btn py-2 px-3 rounded-md bg-gray-100 hover:bg-gray-200 transition">Bayer</button>
<button data-algorithm="random" class="algorithm-btn py-2 px-3 rounded-md bg-gray-100 hover:bg-gray-200 transition">Random</button>
<button data-algorithm="threshold" class="algorithm-btn py-2 px-3 rounded-md bg-gray-100 hover:bg-gray-200 transition">Threshold</button>
</div>
</div>
<!-- Color Palette -->
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">Color Palette</label>
<div class="grid grid-cols-5 gap-2 mb-3">
<div class="color-option h-8 rounded cursor-pointer border border-gray-300" style="background-color: #000000;" data-colors="1"></div>
<div class="color-option h-8 rounded cursor-pointer border border-gray-300" style="background-color: #ffffff;" data-colors="2"></div>
<div class="color-option h-8 rounded cursor-pointer border border-gray-300" style="background: linear-gradient(to right, #000000, #ffffff);" data-colors="2"></div>
<div class="color-option h-8 rounded cursor-pointer border border-gray-300" style="background: linear-gradient(to right, #000000, #808080, #ffffff);" data-colors="3"></div>
<div class="color-option h-8 rounded cursor-pointer border border-gray-300" style="background: linear-gradient(to right, #000000, #555555, #aaaaaa, #ffffff);" data-colors="4"></div>
</div>
<div class="grid grid-cols-4 gap-2">
<div class="color-option h-8 rounded cursor-pointer border border-gray-300" style="background: linear-gradient(to right, #ff0000, #00ff00, #0000ff);" data-colors="3"></div>
<div class="color-option h-8 rounded cursor-pointer border border-gray-300" style="background: linear-gradient(to right, #ff0000, #ffff00, #00ff00, #00ffff, #0000ff, #ff00ff);" data-colors="6"></div>
<div class="color-option h-8 rounded cursor-pointer border border-gray-300" style="background: linear-gradient(to right, #e6194b, #3cb44b, #ffe119, #4363d8, #f58231, #911eb4);" data-colors="6"></div>
<div class="color-option h-8 rounded cursor-pointer border border-gray-300" style="background: linear-gradient(to right, #000000, #654321, #8b0000, #ff8c00, #ffd700, #ffffff);" data-colors="6"></div>
</div>
</div>
<!-- Customization Sliders -->
<div class="mb-4">
<label for="thresholdSlider" class="block text-sm font-medium text-gray-700 mb-1">Threshold</label>
<input type="range" id="thresholdSlider" min="0" max="255" value="128" class="range-slider">
<div class="flex justify-between text-xs text-gray-500">
<span>0</span>
<span>128</span>
<span>255</span>
</div>
</div>
<div class="mb-4">
<label for="intensitySlider" class="block text-sm font-medium text-gray-700 mb-1">Dither Intensity</label>
<input type="range" id="intensitySlider" min="0" max="100" value="50" class="range-slider">
<div class="flex justify-between text-xs text-gray-500">
<span>Low</span>
<span>Medium</span>
<span>High</span>
</div>
</div>
<div class="mb-4">
<label for="pixelSizeSlider" class="block text-sm font-medium text-gray-700 mb-1">Pixel Size</label>
<input type="range" id="pixelSizeSlider" min="1" max="20" value="1" class="range-slider">
<div class="flex justify-between text-xs text-gray-500">
<span>1px</span>
<span>10px</span>
<span>20px</span>
</div>
</div>
<!-- Animation Controls -->
<div class="mb-6 border-t pt-4">
<h3 class="text-lg font-medium text-gray-800 mb-3 flex items-center">
<i class="fas fa-play-circle mr-2 text-indigo-600"></i> Animation Effects
</h3>
<div class="mb-4">
<label for="animationType" class="block text-sm font-medium text-gray-700 mb-1">Animation Type</label>
<select id="animationType" class="w-full p-2 border border-gray-300 rounded-md focus:ring-indigo-500 focus:border-indigo-500">
<option value="none">None</option>
<option value="thresholdPulse">Threshold Pulse</option>
<option value="colorCycle">Color Cycle</option>
<option value="pixelShift">Pixel Shift</option>
<option value="scanlines">Scanlines</option>
</select>
</div>
<div class="mb-4">
<label for="animationSpeed" class="block text-sm font-medium text-gray-700 mb-1">Animation Speed</label>
<input type="range" id="animationSpeed" min="1" max="10" value="5" class="range-slider">
<div class="flex justify-between text-xs text-gray-500">
<span>Slow</span>
<span>Medium</span>
<span>Fast</span>
</div>
</div>
<div class="mb-4">
<label for="animationIntensity" class="block text-sm font-medium text-gray-700 mb-1">Animation Intensity</label>
<input type="range" id="animationIntensity" min="1" max="100" value="50" class="range-slider">
<div class="flex justify-between text-xs text-gray-500">
<span>Subtle</span>
<span>Medium</span>
<span>Strong</span>
</div>
</div>
<div class="animation-preview">
<div id="animationIndicator" class="animation-indicator"></div>
</div>
</div>
<!-- Action Buttons -->
<div class="grid grid-cols-2 gap-3">
<button id="previewBtn" class="py-3 px-4 bg-indigo-600 hover:bg-indigo-700 text-white font-medium rounded-md transition flex items-center justify-center">
<i class="fas fa-play mr-2"></i> Preview
</button>
<button id="downloadBtn" class="py-3 px-4 bg-green-600 hover:bg-green-700 text-white font-medium rounded-md transition flex items-center justify-center">
<i class="fas fa-download mr-2"></i> Download
</button>
</div>
</div>
<!-- Middle Panel - Canvas -->
<div class="lg:col-span-2">
<div class="canvas-container bg-white p-4 rounded-xl shadow-md">
<div id="loadingOverlay" class="loading-overlay hidden">
<div class="text-center">
<i class="fas fa-spinner fa-spin mb-2"></i>
<p>Processing image...</p>
</div>
</div>
<canvas id="canvas"></canvas>
</div>
<!-- Presets -->
<div class="mt-6 bg-white p-6 rounded-xl shadow-md">
<h2 class="text-xl font-semibold text-gray-800 mb-4">Quick Presets</h2>
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
<button data-preset="retro" class="preset-btn py-2 px-3 rounded-md bg-gray-100 hover:bg-gray-200 transition flex items-center">
<i class="fas fa-gamepad mr-2 text-purple-500"></i> Retro Game
</button>
<button data-preset="newsprint" class="preset-btn py-2 px-3 rounded-md bg-gray-100 hover:bg-gray-200 transition flex items-center">
<i class="fas fa-newspaper mr-2 text-gray-500"></i> Newsprint
</button>
<button data-preset="poster" class="preset-btn py-2 px-3 rounded-md bg-gray-100 hover:bg-gray-200 transition flex items-center">
<i class="fas fa-palette mr-2 text-red-500"></i> Color Poster
</button>
<button data-preset="sketch" class="preset-btn py-2 px-3 rounded-md bg-gray-100 hover:bg-gray-200 transition flex items-center">
<i class="fas fa-pencil-alt mr-2 text-blue-500"></i> Pencil Sketch
</button>
<button data-preset="xray" class="preset-btn py-2 px-3 rounded-md bg-gray-100 hover:bg-gray-200 transition flex items-center">
<i class="fas fa-x-ray mr-2 text-green-500"></i> X-Ray
</button>
<button data-preset="vintage" class="preset-btn py-2 px-3 rounded-md bg-gray-100 hover:bg-gray-200 transition flex items-center">
<i class="fas fa-camera-retro mr-2 text-yellow-600"></i> Vintage
</button>
<button data-preset="glitch" class="preset-btn py-2 px-3 rounded-md bg-gray-100 hover:bg-gray-200 transition flex items-center">
<i class="fas fa-bolt mr-2 text-pink-500"></i> Glitch Effect
</button>
<button data-preset="cyberpunk" class="preset-btn py-2 px-3 rounded-md bg-gray-100 hover:bg-gray-200 transition flex items-center">
<i class="fas fa-robot mr-2 text-cyan-500"></i> Cyberpunk
</button>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Canvas setup
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const loadingOverlay = document.getElementById('loadingOverlay');
// State variables
let originalImage = null;
let currentAlgorithm = 'floydSteinberg';
let currentColors = ['#000000', '#ffffff'];
let threshold = 128;
let intensity = 50;
let pixelSize = 1;
let animationType = 'none';
let animationSpeed = 5;
let animationIntensity = 50;
let animationInterval = null;
let animationFrame = 0;
let isAnimating = false;
// UI Elements
const algorithmBtns = document.querySelectorAll('.algorithm-btn');
const colorOptions = document.querySelectorAll('.color-option');
const thresholdSlider = document.getElementById('thresholdSlider');
const intensitySlider = document.getElementById('intensitySlider');
const pixelSizeSlider = document.getElementById('pixelSizeSlider');
const animationTypeSelect = document.getElementById('animationType');
const animationSpeedSlider = document.getElementById('animationSpeed');
const animationIntensitySlider = document.getElementById('animationIntensity');
const animationIndicator = document.getElementById('animationIndicator');
const previewBtn = document.getElementById('previewBtn');
const downloadBtn = document.getElementById('downloadBtn');
const imageInput = document.getElementById('imageInput');
const dropzone = document.getElementById('dropzone');
const presetBtns = document.querySelectorAll('.preset-btn');
// Event Listeners
imageInput.addEventListener('change', handleImageUpload);
dropzone.addEventListener('click', () => imageInput.click());
dropzone.addEventListener('dragover', handleDragOver);
dropzone.addEventListener('dragleave', handleDragLeave);
dropzone.addEventListener('drop', handleDrop);
algorithmBtns.forEach(btn => {
btn.addEventListener('click', () => {
algorithmBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentAlgorithm = btn.dataset.algorithm;
applyDithering();
});
});
colorOptions.forEach(option => {
option.addEventListener('click', () => {
const colorCount = parseInt(option.dataset.colors);
if (colorCount === 1) {
currentColors = [option.style.backgroundColor];
} else if (colorCount === 2 && option.style.background.includes('gradient')) {
currentColors = ['#000000', '#ffffff'];
} else if (colorCount > 2) {
// Extract colors from gradient (simplified)
const gradientColors = option.style.background.match(/#[0-9a-f]{3,6}/gi);
currentColors = gradientColors || ['#000000', '#ffffff'];
}
applyDithering();
});
});
thresholdSlider.addEventListener('input', () => {
threshold = parseInt(thresholdSlider.value);
applyDithering();
});
intensitySlider.addEventListener('input', () => {
intensity = parseInt(intensitySlider.value);
applyDithering();
});
pixelSizeSlider.addEventListener('input', () => {
pixelSize = parseInt(pixelSizeSlider.value);
applyDithering();
});
animationTypeSelect.addEventListener('change', () => {
animationType = animationTypeSelect.value;
updateAnimationPreview();
if (isAnimating) {
stopAnimation();
startAnimation();
}
});
animationSpeedSlider.addEventListener('input', () => {
animationSpeed = parseInt(animationSpeedSlider.value);
updateAnimationPreview();
if (isAnimating) {
stopAnimation();
startAnimation();
}
});
animationIntensitySlider.addEventListener('input', () => {
animationIntensity = parseInt(animationIntensitySlider.value);
if (isAnimating) {
stopAnimation();
startAnimation();
}
});
previewBtn.addEventListener('click', toggleAnimation);
downloadBtn.addEventListener('click', downloadImage);
presetBtns.forEach(btn => {
btn.addEventListener('click', () => {
const preset = btn.dataset.preset;
applyPreset(preset);
});
});
// Initialize with Floyd-Steinberg active
document.querySelector('[data-algorithm="floydSteinberg"]').classList.add('active');
// Animation preview indicator
function updateAnimationPreview() {
if (animationType === 'none') {
animationIndicator.style.display = 'none';
return;
}
animationIndicator.style.display = 'block';
const speed = (11 - animationSpeed) * 100; // Invert speed for timing
let animationName;
switch (animationType) {
case 'thresholdPulse':
animationName = 'pulse';
break;
case 'colorCycle':
animationName = 'color-cycle';
break;
case 'pixelShift':
animationName = 'pixel-shift';
break;
case 'scanlines':
animationName = 'scanlines';
break;
default:
animationName = 'move';
}
// Update indicator position based on animation type
if (animationType === 'thresholdPulse') {
animationIndicator.style.width = '100%';
animationIndicator.style.left = '0';
animationIndicator.style.animation = `pulse ${speed}ms infinite`;
} else {
animationIndicator.style.width = '20%';
animationIndicator.style.animation = 'none';
animationIndicator.style.transition = `left ${speed}ms linear`;
animationIndicator.style.left = '0';
setTimeout(() => {
animationIndicator.style.left = '80%';
}, 10);
const interval = setInterval(() => {
animationIndicator.style.left = '0';
setTimeout(() => {
animationIndicator.style.left = '80%';
}, 10);
}, speed);
// Store interval ID to clear later
animationIndicator.dataset.interval = interval;
}
}
// Clear animation preview when changing types
animationTypeSelect.addEventListener('focus', () => {
const interval = animationIndicator.dataset.interval;
if (interval) clearInterval(interval);
});
// Functions
function handleImageUpload(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(event) {
const img = new Image();
img.onload = function() {
originalImage = img;
resetCanvas();
applyDithering();
};
img.src = event.target.result;
};
reader.readAsDataURL(file);
}
function handleDragOver(e) {
e.preventDefault();
e.stopPropagation();
dropzone.classList.add('dragover');
}
function handleDragLeave(e) {
e.preventDefault();
e.stopPropagation();
dropzone.classList.remove('dragover');
}
function handleDrop(e) {
e.preventDefault();
e.stopPropagation();
dropzone.classList.remove('dragover');
const file = e.dataTransfer.files[0];
if (!file.type.match('image.*')) return;
const reader = new FileReader();
reader.onload = function(event) {
const img = new Image();
img.onload = function() {
originalImage = img;
resetCanvas();
applyDithering();
};
img.src = event.target.result;
};
reader.readAsDataURL(file);
}
function resetCanvas() {
if (!originalImage) return;
// Set canvas dimensions (max 800px width/height while maintaining aspect ratio)
const maxSize = 800;
let width = originalImage.width;
let height = originalImage.height;
if (width > height && width > maxSize) {
height = Math.round((maxSize / width) * height);
width = maxSize;
} else if (height > maxSize) {
width = Math.round((maxSize / height) * width);
height = maxSize;
}
canvas.width = width;
canvas.height = height;
// Draw original image
ctx.drawImage(originalImage, 0, 0, width, height);
}
function applyDithering() {
if (!originalImage) return;
showLoading();
// Use setTimeout to allow UI to update before heavy processing
setTimeout(() => {
try {
// Create image data
const width = canvas.width;
const height = canvas.height;
// Get original image data
ctx.drawImage(originalImage, 0, 0, width, height);
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
// Convert to grayscale first
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
data[i] = data[i + 1] = data[i + 2] = gray;
}
// Apply selected dithering algorithm
switch (currentAlgorithm) {
case 'floydSteinberg':
floydSteinbergDither(data, width, height);
break;
case 'atkinson':
atkinsonDither(data, width, height);
break;
case 'bayer':
bayerDither(data, width, height);
break;
case 'random':
randomDither(data, width, height);
break;
case 'threshold':
thresholdDither(data, width, height);
break;
default:
// No dithering
break;
}
// Apply color palette
applyColorPalette(data, currentColors);
// Apply pixelation if needed
if (pixelSize > 1) {
pixelateImage(data, width, height, pixelSize);
}
// Put the modified data back
ctx.putImageData(imageData, 0, 0);
} catch (error) {
console.error('Error applying dithering:', error);
} finally {
hideLoading();
}
}, 100);
}
function floydSteinbergDither(data, width, height) {
const adjustedThreshold = threshold * (intensity / 50);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const idx = (y * width + x) * 4;
const oldPixel = data[idx];
const newPixel = oldPixel < adjustedThreshold ? 0 : 255;
data[idx] = data[idx + 1] = data[idx + 2] = newPixel;
const quantError = oldPixel - newPixel;
// Distribute error to neighboring pixels
if (x + 1 < width) {
const idxRight = idx + 4;
data[idxRight] += quantError * 7 / 16;
}
if (x > 0 && y + 1 < height) {
const idxDownLeft = idx + width * 4 - 4;
data[idxDownLeft] += quantError * 3 / 16;
}
if (y + 1 < height) {
const idxDown = idx + width * 4;
data[idxDown] += quantError * 5 / 16;
}
if (x + 1 < width && y + 1 < height) {
const idxDownRight = idx + width * 4 + 4;
data[idxDownRight] += quantError * 1 / 16;
}
}
}
}
function atkinsonDither(data, width, height) {
const adjustedThreshold = threshold * (intensity / 50);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const idx = (y * width + x) * 4;
const oldPixel = data[idx];
const newPixel = oldPixel < adjustedThreshold ? 0 : 255;
data[idx] = data[idx + 1] = data[idx + 2] = newPixel;
const quantError = oldPixel - newPixel;
const errorPart = quantError / 8;
// Distribute error to neighboring pixels (Atkinson's algorithm)
if (x + 1 < width) {
const idxRight = idx + 4;
data[idxRight] += errorPart;
}
if (x + 2 < width) {
const idxRight2 = idx + 8;
data[idxRight2] += errorPart;
}
if (x > 0 && y + 1 < height) {
const idxDownLeft = idx + width * 4 - 4;
data[idxDownLeft] += errorPart;
}
if (y + 1 < height) {
const idxDown = idx + width * 4;
data[idxDown] += errorPart;
}
if (x + 1 < width && y + 1 < height) {
const idxDownRight = idx + width * 4 + 4;
data[idxDownRight] += errorPart;
}
if (y + 2 < height) {
const idxDown2 = idx + width * 8;
data[idxDown2] += errorPart;
}
}
}
}
function bayerDither(data, width, height) {
// 4x4 Bayer matrix
const bayerMatrix = [
[ 1, 9, 3, 11 ],
[ 13, 5, 15, 7 ],
[ 4, 12, 2, 10 ],
[ 16, 8, 14, 6 ]
];
const adjustedThreshold = threshold * (intensity / 50) * (16 / 255);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const idx = (y * width + x) * 4;
const gray = data[idx];
const thresholdValue = bayerMatrix[y % 4][x % 4] * adjustedThreshold;
data[idx] = data[idx + 1] = data[idx + 2] = gray > thresholdValue ? 255 : 0;
}
}
}
function randomDither(data, width, height) {
const adjustedThreshold = threshold * (intensity / 50);
for (let i = 0; i < data.length; i += 4) {
const gray = data[i];
const randomThreshold = adjustedThreshold + (Math.random() * 50 - 25);
data[i] = data[i + 1] = data[i + 2] = gray < randomThreshold ? 0 : 255;
}
}
function thresholdDither(data, width, height) {
const adjustedThreshold = threshold * (intensity / 50);
for (let i = 0; i < data.length; i += 4) {
const gray = data[i];
data[i] = data[i + 1] = data[i + 2] = gray < adjustedThreshold ? 0 : 255;
}
}
function applyColorPalette(data, colors) {
if (colors.length === 1) return; // No color change needed
// Convert hex colors to RGB
const rgbColors = colors.map(hexToRgb);
for (let i = 0; i < data.length; i += 4) {
const gray = data[i];
// Find closest color in palette based on grayscale value
const colorIndex = Math.floor((gray / 255) * (rgbColors.length - 1));
const color = rgbColors[colorIndex];
data[i] = color.r;
data[i + 1] = color.g;
data[i + 2] = color.b;
}
}
function pixelateImage(data, width, height, pixelSize) {
if (pixelSize <= 1) return;
// Create a temporary canvas for pixelation
const tempCanvas = document.createElement('canvas');
tempCanvas.width = width;
tempCanvas.height = height;
const tempCtx = tempCanvas.getContext('2d');
// Put the current image data to temp canvas
const tempImageData = new ImageData(new Uint8ClampedArray(data), width, height);
tempCtx.putImageData(tempImageData, 0, 0);
// Calculate new dimensions
const pixelatedWidth = Math.ceil(width / pixelSize);
const pixelatedHeight = Math.ceil(height / pixelSize);
// Draw small version
tempCtx.imageSmoothingEnabled = false;
tempCtx.drawImage(tempCanvas, 0, 0, pixelatedWidth, pixelated
</html>