image-dither / index.html
pjayofficial's picture
Add 2 files
c4d604b verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>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;
}
</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">Pixel Art Dithering Editor</h1>
<p class="text-gray-600 max-w-2xl mx-auto">Transform your images with customizable dithering effects. Upload an image, adjust the settings, and download your pixel-perfect creation.</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>
<!-- Download Button -->
<button id="downloadBtn" class="w-full 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-download mr-2"></i> Download Image
</button>
</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>
</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;
// 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 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();
});
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');
// 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, pixelatedHeight);
// Draw back to original size
ctx.clearRect(0, 0, width, height);
ctx.imageSmoothingEnabled = false;
ctx.drawImage(tempCanvas, 0, 0, pixelatedWidth, pixelatedHeight, 0, 0, width, height);
// Get the pixelated data
const pixelatedData = ctx.getImageData(0, 0, width, height).data;
// Copy back to original data array
for (let i = 0; i < data.length; i++) {
data[i] = pixelatedData[i];
}
}
function hexToRgb(hex) {
// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
hex = hex.replace(shorthandRegex, function(m, r, g, b) {
return r + r + g + g + b + b;
});
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : { r: 0, g: 0, b: 0 };
}
function downloadImage() {
if (!originalImage) {
alert('Please upload an image first!');
return;
}
const link = document.createElement('a');
link.download = 'dithered-image.png';
link.href = canvas.toDataURL('image/png');
link.click();
}
function showLoading() {
loadingOverlay.classList.remove('hidden');
}
function hideLoading() {
loadingOverlay.classList.add('hidden');
}
function applyPreset(preset) {
switch (preset) {
case 'retro':
currentAlgorithm = 'floydSteinberg';
currentColors = ['#000000', '#ffffff'];
threshold = 128;
intensity = 60;
pixelSize = 4;
break;
case 'newsprint':
currentAlgorithm = 'bayer';
currentColors = ['#000000', '#ffffff'];
threshold = 100;
intensity = 70;
pixelSize = 1;
break;
case 'poster':
currentAlgorithm = 'atkinson';
currentColors = ['#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff', '#00ffff'];
threshold = 128;
intensity = 80;
pixelSize = 2;
break;
case 'sketch':
currentAlgorithm = 'random';
currentColors = ['#000000', '#ffffff'];
threshold = 150;
intensity = 40;
pixelSize = 1;
break;
case 'xray':
currentAlgorithm = 'threshold';
currentColors = ['#000000', '#ffffff'];
threshold = 200;
intensity = 90;
pixelSize = 1;
break;
case 'vintage':
currentAlgorithm = 'floydSteinberg';
currentColors = ['#654321', '#8b0000', '#ff8c00', '#ffd700', '#ffffff'];
threshold = 100;
intensity = 50;
pixelSize = 3;
break;
}
// Update UI to match preset
algorithmBtns.forEach(btn => {
btn.classList.remove('active');
if (btn.dataset.algorithm === currentAlgorithm) {
btn.classList.add('active');
}
});
thresholdSlider.value = threshold;
intensitySlider.value = intensity;
pixelSizeSlider.value = pixelSize;
applyDithering();
}
// Load a sample image on startup
const sampleImage = new Image();
sampleImage.onload = function() {
originalImage = sampleImage;
resetCanvas();
applyDithering();
};
sampleImage.src = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4MDAiIGhlaWdodD0iNjAwIiB2aWV3Qm94PSIwIDAgODAwIDYwMCI+PHJlY3Qgd2lkdGg9IjgwMCIgaGVpZ2h0PSI2MDAiIGZpbGw9IiNmNWY1ZjUiLz48Y2lyY2xlIGN4PSI0MDAiIGN5PSIzMDAiIHI9IjI1MCIgZmlsbD0iI2Q0ZDVkNCIvPjxjaXJjbGUgY3g9IjMwMCIgY3k9IjIwMCIgcj0iNTAiIGZpbGw9IiNhYWEiLz48Y2lyY2xlIGN4PSI1MDAiIGN5PSIyMDAiIHI9IjUwIiBmaWxsPSIjYWFhIi8+PGNpcmNsZSBjeD0iMzUwIiBjeT0iMjUwIiByPSIxMDAiIGZpbGw9IiNhYWEiLz48Y2lyY2xlIGN4PSI0NTAiIGN5PSIyNTAiIHI9IjEwMCIgZmlsbD0iI2FhYSIvPjxwYXRoIGQ9Ik0zMDAgNDAwIHEgMTAwIDEwMCAyMDAgMCIgc3Ryb2tlPSIjYWFhIiBzdHJva2Utd2lkdGg9IjEwIiBmaWxsPSJub25lIi8+PC9zdmc+';
});
</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=pjayofficial/image-dither" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body>
</html>