pjayofficial commited on
Commit
c4d604b
·
verified ·
1 Parent(s): 201f1cd

Add 2 files

Browse files
Files changed (2) hide show
  1. README.md +7 -5
  2. index.html +721 -19
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
- title: Image Dither
3
- emoji: 🌖
4
- colorFrom: green
5
- colorTo: purple
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: image-dither
3
+ emoji: 🐳
4
+ colorFrom: gray
5
+ colorTo: blue
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite
10
  ---
11
 
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
index.html CHANGED
@@ -1,19 +1,721 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Image Dithering Editor</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <style>
10
+ .canvas-container {
11
+ position: relative;
12
+ margin: 0 auto;
13
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
14
+ border-radius: 0.5rem;
15
+ overflow: hidden;
16
+ }
17
+ canvas {
18
+ display: block;
19
+ max-width: 100%;
20
+ height: auto;
21
+ }
22
+ .loading-overlay {
23
+ position: absolute;
24
+ top: 0;
25
+ left: 0;
26
+ width: 100%;
27
+ height: 100%;
28
+ background: rgba(0, 0, 0, 0.7);
29
+ display: flex;
30
+ justify-content: center;
31
+ align-items: center;
32
+ color: white;
33
+ font-size: 1.5rem;
34
+ z-index: 10;
35
+ border-radius: 0.5rem;
36
+ }
37
+ .range-slider {
38
+ -webkit-appearance: none;
39
+ width: 100%;
40
+ height: 8px;
41
+ border-radius: 4px;
42
+ background: #e2e8f0;
43
+ outline: none;
44
+ }
45
+ .range-slider::-webkit-slider-thumb {
46
+ -webkit-appearance: none;
47
+ appearance: none;
48
+ width: 18px;
49
+ height: 18px;
50
+ border-radius: 50%;
51
+ background: #4f46e5;
52
+ cursor: pointer;
53
+ transition: all 0.2s;
54
+ }
55
+ .range-slider::-webkit-slider-thumb:hover {
56
+ transform: scale(1.2);
57
+ background: #6366f1;
58
+ }
59
+ .range-slider::-moz-range-thumb {
60
+ width: 18px;
61
+ height: 18px;
62
+ border-radius: 50%;
63
+ background: #4f46e5;
64
+ cursor: pointer;
65
+ transition: all 0.2s;
66
+ }
67
+ .range-slider::-moz-range-thumb:hover {
68
+ transform: scale(1.2);
69
+ background: #6366f1;
70
+ }
71
+ .algorithm-btn.active {
72
+ background-color: #4f46e5;
73
+ color: white;
74
+ }
75
+ .dropzone {
76
+ border: 2px dashed #cbd5e1;
77
+ transition: all 0.3s;
78
+ }
79
+ .dropzone:hover, .dropzone.dragover {
80
+ border-color: #4f46e5;
81
+ background-color: #f8fafc;
82
+ }
83
+ </style>
84
+ </head>
85
+ <body class="bg-gray-50 min-h-screen">
86
+ <div class="container mx-auto px-4 py-8">
87
+ <header class="text-center mb-8">
88
+ <h1 class="text-4xl font-bold text-gray-800 mb-2">Pixel Art Dithering Editor</h1>
89
+ <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>
90
+ </header>
91
+
92
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
93
+ <!-- Left Panel - Controls -->
94
+ <div class="bg-white p-6 rounded-xl shadow-md">
95
+ <h2 class="text-xl font-semibold text-gray-800 mb-4">Controls</h2>
96
+
97
+ <!-- Image Upload -->
98
+ <div class="mb-6">
99
+ <label class="block text-sm font-medium text-gray-700 mb-2">Upload Image</label>
100
+ <div id="dropzone" class="dropzone rounded-lg p-6 text-center cursor-pointer">
101
+ <input type="file" id="imageInput" accept="image/*" class="hidden">
102
+ <div class="flex flex-col items-center justify-center">
103
+ <i class="fas fa-cloud-upload-alt text-4xl text-gray-400 mb-2"></i>
104
+ <p class="text-gray-500">Drag & drop an image or click to browse</p>
105
+ <p class="text-xs text-gray-400 mt-1">Supports JPG, PNG, GIF</p>
106
+ </div>
107
+ </div>
108
+ </div>
109
+
110
+ <!-- Dithering Algorithm -->
111
+ <div class="mb-6">
112
+ <label class="block text-sm font-medium text-gray-700 mb-2">Dithering Algorithm</label>
113
+ <div class="grid grid-cols-2 gap-2">
114
+ <button data-algorithm="none" class="algorithm-btn py-2 px-3 rounded-md bg-gray-100 hover:bg-gray-200 transition">None</button>
115
+ <button data-algorithm="floydSteinberg" class="algorithm-btn py-2 px-3 rounded-md bg-gray-100 hover:bg-gray-200 transition">Floyd-Steinberg</button>
116
+ <button data-algorithm="atkinson" class="algorithm-btn py-2 px-3 rounded-md bg-gray-100 hover:bg-gray-200 transition">Atkinson</button>
117
+ <button data-algorithm="bayer" class="algorithm-btn py-2 px-3 rounded-md bg-gray-100 hover:bg-gray-200 transition">Bayer</button>
118
+ <button data-algorithm="random" class="algorithm-btn py-2 px-3 rounded-md bg-gray-100 hover:bg-gray-200 transition">Random</button>
119
+ <button data-algorithm="threshold" class="algorithm-btn py-2 px-3 rounded-md bg-gray-100 hover:bg-gray-200 transition">Threshold</button>
120
+ </div>
121
+ </div>
122
+
123
+ <!-- Color Palette -->
124
+ <div class="mb-6">
125
+ <label class="block text-sm font-medium text-gray-700 mb-2">Color Palette</label>
126
+ <div class="grid grid-cols-5 gap-2 mb-3">
127
+ <div class="color-option h-8 rounded cursor-pointer border border-gray-300" style="background-color: #000000;" data-colors="1"></div>
128
+ <div class="color-option h-8 rounded cursor-pointer border border-gray-300" style="background-color: #ffffff;" data-colors="2"></div>
129
+ <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>
130
+ <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>
131
+ <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>
132
+ </div>
133
+ <div class="grid grid-cols-4 gap-2">
134
+ <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>
135
+ <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>
136
+ <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>
137
+ <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>
138
+ </div>
139
+ </div>
140
+
141
+ <!-- Customization Sliders -->
142
+ <div class="mb-4">
143
+ <label for="thresholdSlider" class="block text-sm font-medium text-gray-700 mb-1">Threshold</label>
144
+ <input type="range" id="thresholdSlider" min="0" max="255" value="128" class="range-slider">
145
+ <div class="flex justify-between text-xs text-gray-500">
146
+ <span>0</span>
147
+ <span>128</span>
148
+ <span>255</span>
149
+ </div>
150
+ </div>
151
+
152
+ <div class="mb-4">
153
+ <label for="intensitySlider" class="block text-sm font-medium text-gray-700 mb-1">Dither Intensity</label>
154
+ <input type="range" id="intensitySlider" min="0" max="100" value="50" class="range-slider">
155
+ <div class="flex justify-between text-xs text-gray-500">
156
+ <span>Low</span>
157
+ <span>Medium</span>
158
+ <span>High</span>
159
+ </div>
160
+ </div>
161
+
162
+ <div class="mb-4">
163
+ <label for="pixelSizeSlider" class="block text-sm font-medium text-gray-700 mb-1">Pixel Size</label>
164
+ <input type="range" id="pixelSizeSlider" min="1" max="20" value="1" class="range-slider">
165
+ <div class="flex justify-between text-xs text-gray-500">
166
+ <span>1px</span>
167
+ <span>10px</span>
168
+ <span>20px</span>
169
+ </div>
170
+ </div>
171
+
172
+ <!-- Download Button -->
173
+ <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">
174
+ <i class="fas fa-download mr-2"></i> Download Image
175
+ </button>
176
+ </div>
177
+
178
+ <!-- Middle Panel - Canvas -->
179
+ <div class="lg:col-span-2">
180
+ <div class="canvas-container bg-white p-4 rounded-xl shadow-md">
181
+ <div id="loadingOverlay" class="loading-overlay hidden">
182
+ <div class="text-center">
183
+ <i class="fas fa-spinner fa-spin mb-2"></i>
184
+ <p>Processing image...</p>
185
+ </div>
186
+ </div>
187
+ <canvas id="canvas"></canvas>
188
+ </div>
189
+
190
+ <!-- Presets -->
191
+ <div class="mt-6 bg-white p-6 rounded-xl shadow-md">
192
+ <h2 class="text-xl font-semibold text-gray-800 mb-4">Quick Presets</h2>
193
+ <div class="grid grid-cols-2 md:grid-cols-3 gap-3">
194
+ <button data-preset="retro" class="preset-btn py-2 px-3 rounded-md bg-gray-100 hover:bg-gray-200 transition flex items-center">
195
+ <i class="fas fa-gamepad mr-2 text-purple-500"></i> Retro Game
196
+ </button>
197
+ <button data-preset="newsprint" class="preset-btn py-2 px-3 rounded-md bg-gray-100 hover:bg-gray-200 transition flex items-center">
198
+ <i class="fas fa-newspaper mr-2 text-gray-500"></i> Newsprint
199
+ </button>
200
+ <button data-preset="poster" class="preset-btn py-2 px-3 rounded-md bg-gray-100 hover:bg-gray-200 transition flex items-center">
201
+ <i class="fas fa-palette mr-2 text-red-500"></i> Color Poster
202
+ </button>
203
+ <button data-preset="sketch" class="preset-btn py-2 px-3 rounded-md bg-gray-100 hover:bg-gray-200 transition flex items-center">
204
+ <i class="fas fa-pencil-alt mr-2 text-blue-500"></i> Pencil Sketch
205
+ </button>
206
+ <button data-preset="xray" class="preset-btn py-2 px-3 rounded-md bg-gray-100 hover:bg-gray-200 transition flex items-center">
207
+ <i class="fas fa-x-ray mr-2 text-green-500"></i> X-Ray
208
+ </button>
209
+ <button data-preset="vintage" class="preset-btn py-2 px-3 rounded-md bg-gray-100 hover:bg-gray-200 transition flex items-center">
210
+ <i class="fas fa-camera-retro mr-2 text-yellow-600"></i> Vintage
211
+ </button>
212
+ </div>
213
+ </div>
214
+ </div>
215
+ </div>
216
+ </div>
217
+
218
+ <script>
219
+ document.addEventListener('DOMContentLoaded', function() {
220
+ // Canvas setup
221
+ const canvas = document.getElementById('canvas');
222
+ const ctx = canvas.getContext('2d');
223
+ const loadingOverlay = document.getElementById('loadingOverlay');
224
+
225
+ // State variables
226
+ let originalImage = null;
227
+ let currentAlgorithm = 'floydSteinberg';
228
+ let currentColors = ['#000000', '#ffffff'];
229
+ let threshold = 128;
230
+ let intensity = 50;
231
+ let pixelSize = 1;
232
+
233
+ // UI Elements
234
+ const algorithmBtns = document.querySelectorAll('.algorithm-btn');
235
+ const colorOptions = document.querySelectorAll('.color-option');
236
+ const thresholdSlider = document.getElementById('thresholdSlider');
237
+ const intensitySlider = document.getElementById('intensitySlider');
238
+ const pixelSizeSlider = document.getElementById('pixelSizeSlider');
239
+ const downloadBtn = document.getElementById('downloadBtn');
240
+ const imageInput = document.getElementById('imageInput');
241
+ const dropzone = document.getElementById('dropzone');
242
+ const presetBtns = document.querySelectorAll('.preset-btn');
243
+
244
+ // Event Listeners
245
+ imageInput.addEventListener('change', handleImageUpload);
246
+ dropzone.addEventListener('click', () => imageInput.click());
247
+ dropzone.addEventListener('dragover', handleDragOver);
248
+ dropzone.addEventListener('dragleave', handleDragLeave);
249
+ dropzone.addEventListener('drop', handleDrop);
250
+
251
+ algorithmBtns.forEach(btn => {
252
+ btn.addEventListener('click', () => {
253
+ algorithmBtns.forEach(b => b.classList.remove('active'));
254
+ btn.classList.add('active');
255
+ currentAlgorithm = btn.dataset.algorithm;
256
+ applyDithering();
257
+ });
258
+ });
259
+
260
+ colorOptions.forEach(option => {
261
+ option.addEventListener('click', () => {
262
+ const colorCount = parseInt(option.dataset.colors);
263
+ if (colorCount === 1) {
264
+ currentColors = [option.style.backgroundColor];
265
+ } else if (colorCount === 2 && option.style.background.includes('gradient')) {
266
+ currentColors = ['#000000', '#ffffff'];
267
+ } else if (colorCount > 2) {
268
+ // Extract colors from gradient (simplified)
269
+ const gradientColors = option.style.background.match(/#[0-9a-f]{3,6}/gi);
270
+ currentColors = gradientColors || ['#000000', '#ffffff'];
271
+ }
272
+ applyDithering();
273
+ });
274
+ });
275
+
276
+ thresholdSlider.addEventListener('input', () => {
277
+ threshold = parseInt(thresholdSlider.value);
278
+ applyDithering();
279
+ });
280
+
281
+ intensitySlider.addEventListener('input', () => {
282
+ intensity = parseInt(intensitySlider.value);
283
+ applyDithering();
284
+ });
285
+
286
+ pixelSizeSlider.addEventListener('input', () => {
287
+ pixelSize = parseInt(pixelSizeSlider.value);
288
+ applyDithering();
289
+ });
290
+
291
+ downloadBtn.addEventListener('click', downloadImage);
292
+
293
+ presetBtns.forEach(btn => {
294
+ btn.addEventListener('click', () => {
295
+ const preset = btn.dataset.preset;
296
+ applyPreset(preset);
297
+ });
298
+ });
299
+
300
+ // Initialize with Floyd-Steinberg active
301
+ document.querySelector('[data-algorithm="floydSteinberg"]').classList.add('active');
302
+
303
+ // Functions
304
+ function handleImageUpload(e) {
305
+ const file = e.target.files[0];
306
+ if (!file) return;
307
+
308
+ const reader = new FileReader();
309
+ reader.onload = function(event) {
310
+ const img = new Image();
311
+ img.onload = function() {
312
+ originalImage = img;
313
+ resetCanvas();
314
+ applyDithering();
315
+ };
316
+ img.src = event.target.result;
317
+ };
318
+ reader.readAsDataURL(file);
319
+ }
320
+
321
+ function handleDragOver(e) {
322
+ e.preventDefault();
323
+ e.stopPropagation();
324
+ dropzone.classList.add('dragover');
325
+ }
326
+
327
+ function handleDragLeave(e) {
328
+ e.preventDefault();
329
+ e.stopPropagation();
330
+ dropzone.classList.remove('dragover');
331
+ }
332
+
333
+ function handleDrop(e) {
334
+ e.preventDefault();
335
+ e.stopPropagation();
336
+ dropzone.classList.remove('dragover');
337
+
338
+ const file = e.dataTransfer.files[0];
339
+ if (!file.type.match('image.*')) return;
340
+
341
+ const reader = new FileReader();
342
+ reader.onload = function(event) {
343
+ const img = new Image();
344
+ img.onload = function() {
345
+ originalImage = img;
346
+ resetCanvas();
347
+ applyDithering();
348
+ };
349
+ img.src = event.target.result;
350
+ };
351
+ reader.readAsDataURL(file);
352
+ }
353
+
354
+ function resetCanvas() {
355
+ if (!originalImage) return;
356
+
357
+ // Set canvas dimensions (max 800px width/height while maintaining aspect ratio)
358
+ const maxSize = 800;
359
+ let width = originalImage.width;
360
+ let height = originalImage.height;
361
+
362
+ if (width > height && width > maxSize) {
363
+ height = Math.round((maxSize / width) * height);
364
+ width = maxSize;
365
+ } else if (height > maxSize) {
366
+ width = Math.round((maxSize / height) * width);
367
+ height = maxSize;
368
+ }
369
+
370
+ canvas.width = width;
371
+ canvas.height = height;
372
+
373
+ // Draw original image
374
+ ctx.drawImage(originalImage, 0, 0, width, height);
375
+ }
376
+
377
+ function applyDithering() {
378
+ if (!originalImage) return;
379
+
380
+ showLoading();
381
+
382
+ // Use setTimeout to allow UI to update before heavy processing
383
+ setTimeout(() => {
384
+ try {
385
+ // Create image data
386
+ const width = canvas.width;
387
+ const height = canvas.height;
388
+
389
+ // Get original image data
390
+ ctx.drawImage(originalImage, 0, 0, width, height);
391
+ const imageData = ctx.getImageData(0, 0, width, height);
392
+ const data = imageData.data;
393
+
394
+ // Convert to grayscale first
395
+ for (let i = 0; i < data.length; i += 4) {
396
+ const r = data[i];
397
+ const g = data[i + 1];
398
+ const b = data[i + 2];
399
+ const gray = 0.299 * r + 0.587 * g + 0.114 * b;
400
+ data[i] = data[i + 1] = data[i + 2] = gray;
401
+ }
402
+
403
+ // Apply selected dithering algorithm
404
+ switch (currentAlgorithm) {
405
+ case 'floydSteinberg':
406
+ floydSteinbergDither(data, width, height);
407
+ break;
408
+ case 'atkinson':
409
+ atkinsonDither(data, width, height);
410
+ break;
411
+ case 'bayer':
412
+ bayerDither(data, width, height);
413
+ break;
414
+ case 'random':
415
+ randomDither(data, width, height);
416
+ break;
417
+ case 'threshold':
418
+ thresholdDither(data, width, height);
419
+ break;
420
+ default:
421
+ // No dithering
422
+ break;
423
+ }
424
+
425
+ // Apply color palette
426
+ applyColorPalette(data, currentColors);
427
+
428
+ // Apply pixelation if needed
429
+ if (pixelSize > 1) {
430
+ pixelateImage(data, width, height, pixelSize);
431
+ }
432
+
433
+ // Put the modified data back
434
+ ctx.putImageData(imageData, 0, 0);
435
+
436
+ } catch (error) {
437
+ console.error('Error applying dithering:', error);
438
+ } finally {
439
+ hideLoading();
440
+ }
441
+ }, 100);
442
+ }
443
+
444
+ function floydSteinbergDither(data, width, height) {
445
+ const adjustedThreshold = threshold * (intensity / 50);
446
+
447
+ for (let y = 0; y < height; y++) {
448
+ for (let x = 0; x < width; x++) {
449
+ const idx = (y * width + x) * 4;
450
+ const oldPixel = data[idx];
451
+ const newPixel = oldPixel < adjustedThreshold ? 0 : 255;
452
+ data[idx] = data[idx + 1] = data[idx + 2] = newPixel;
453
+
454
+ const quantError = oldPixel - newPixel;
455
+
456
+ // Distribute error to neighboring pixels
457
+ if (x + 1 < width) {
458
+ const idxRight = idx + 4;
459
+ data[idxRight] += quantError * 7 / 16;
460
+ }
461
+ if (x > 0 && y + 1 < height) {
462
+ const idxDownLeft = idx + width * 4 - 4;
463
+ data[idxDownLeft] += quantError * 3 / 16;
464
+ }
465
+ if (y + 1 < height) {
466
+ const idxDown = idx + width * 4;
467
+ data[idxDown] += quantError * 5 / 16;
468
+ }
469
+ if (x + 1 < width && y + 1 < height) {
470
+ const idxDownRight = idx + width * 4 + 4;
471
+ data[idxDownRight] += quantError * 1 / 16;
472
+ }
473
+ }
474
+ }
475
+ }
476
+
477
+ function atkinsonDither(data, width, height) {
478
+ const adjustedThreshold = threshold * (intensity / 50);
479
+
480
+ for (let y = 0; y < height; y++) {
481
+ for (let x = 0; x < width; x++) {
482
+ const idx = (y * width + x) * 4;
483
+ const oldPixel = data[idx];
484
+ const newPixel = oldPixel < adjustedThreshold ? 0 : 255;
485
+ data[idx] = data[idx + 1] = data[idx + 2] = newPixel;
486
+
487
+ const quantError = oldPixel - newPixel;
488
+ const errorPart = quantError / 8;
489
+
490
+ // Distribute error to neighboring pixels (Atkinson's algorithm)
491
+ if (x + 1 < width) {
492
+ const idxRight = idx + 4;
493
+ data[idxRight] += errorPart;
494
+ }
495
+ if (x + 2 < width) {
496
+ const idxRight2 = idx + 8;
497
+ data[idxRight2] += errorPart;
498
+ }
499
+ if (x > 0 && y + 1 < height) {
500
+ const idxDownLeft = idx + width * 4 - 4;
501
+ data[idxDownLeft] += errorPart;
502
+ }
503
+ if (y + 1 < height) {
504
+ const idxDown = idx + width * 4;
505
+ data[idxDown] += errorPart;
506
+ }
507
+ if (x + 1 < width && y + 1 < height) {
508
+ const idxDownRight = idx + width * 4 + 4;
509
+ data[idxDownRight] += errorPart;
510
+ }
511
+ if (y + 2 < height) {
512
+ const idxDown2 = idx + width * 8;
513
+ data[idxDown2] += errorPart;
514
+ }
515
+ }
516
+ }
517
+ }
518
+
519
+ function bayerDither(data, width, height) {
520
+ // 4x4 Bayer matrix
521
+ const bayerMatrix = [
522
+ [ 1, 9, 3, 11 ],
523
+ [ 13, 5, 15, 7 ],
524
+ [ 4, 12, 2, 10 ],
525
+ [ 16, 8, 14, 6 ]
526
+ ];
527
+
528
+ const adjustedThreshold = threshold * (intensity / 50) * (16 / 255);
529
+
530
+ for (let y = 0; y < height; y++) {
531
+ for (let x = 0; x < width; x++) {
532
+ const idx = (y * width + x) * 4;
533
+ const gray = data[idx];
534
+ const thresholdValue = bayerMatrix[y % 4][x % 4] * adjustedThreshold;
535
+
536
+ data[idx] = data[idx + 1] = data[idx + 2] = gray > thresholdValue ? 255 : 0;
537
+ }
538
+ }
539
+ }
540
+
541
+ function randomDither(data, width, height) {
542
+ const adjustedThreshold = threshold * (intensity / 50);
543
+
544
+ for (let i = 0; i < data.length; i += 4) {
545
+ const gray = data[i];
546
+ const randomThreshold = adjustedThreshold + (Math.random() * 50 - 25);
547
+ data[i] = data[i + 1] = data[i + 2] = gray < randomThreshold ? 0 : 255;
548
+ }
549
+ }
550
+
551
+ function thresholdDither(data, width, height) {
552
+ const adjustedThreshold = threshold * (intensity / 50);
553
+
554
+ for (let i = 0; i < data.length; i += 4) {
555
+ const gray = data[i];
556
+ data[i] = data[i + 1] = data[i + 2] = gray < adjustedThreshold ? 0 : 255;
557
+ }
558
+ }
559
+
560
+ function applyColorPalette(data, colors) {
561
+ if (colors.length === 1) return; // No color change needed
562
+
563
+ // Convert hex colors to RGB
564
+ const rgbColors = colors.map(hexToRgb);
565
+
566
+ for (let i = 0; i < data.length; i += 4) {
567
+ const gray = data[i];
568
+
569
+ // Find closest color in palette based on grayscale value
570
+ const colorIndex = Math.floor((gray / 255) * (rgbColors.length - 1));
571
+ const color = rgbColors[colorIndex];
572
+
573
+ data[i] = color.r;
574
+ data[i + 1] = color.g;
575
+ data[i + 2] = color.b;
576
+ }
577
+ }
578
+
579
+ function pixelateImage(data, width, height, pixelSize) {
580
+ if (pixelSize <= 1) return;
581
+
582
+ // Create a temporary canvas for pixelation
583
+ const tempCanvas = document.createElement('canvas');
584
+ tempCanvas.width = width;
585
+ tempCanvas.height = height;
586
+ const tempCtx = tempCanvas.getContext('2d');
587
+
588
+ // Put the current image data to temp canvas
589
+ const tempImageData = new ImageData(new Uint8ClampedArray(data), width, height);
590
+ tempCtx.putImageData(tempImageData, 0, 0);
591
+
592
+ // Calculate new dimensions
593
+ const pixelatedWidth = Math.ceil(width / pixelSize);
594
+ const pixelatedHeight = Math.ceil(height / pixelSize);
595
+
596
+ // Draw small version
597
+ tempCtx.imageSmoothingEnabled = false;
598
+ tempCtx.drawImage(tempCanvas, 0, 0, pixelatedWidth, pixelatedHeight);
599
+
600
+ // Draw back to original size
601
+ ctx.clearRect(0, 0, width, height);
602
+ ctx.imageSmoothingEnabled = false;
603
+ ctx.drawImage(tempCanvas, 0, 0, pixelatedWidth, pixelatedHeight, 0, 0, width, height);
604
+
605
+ // Get the pixelated data
606
+ const pixelatedData = ctx.getImageData(0, 0, width, height).data;
607
+
608
+ // Copy back to original data array
609
+ for (let i = 0; i < data.length; i++) {
610
+ data[i] = pixelatedData[i];
611
+ }
612
+ }
613
+
614
+ function hexToRgb(hex) {
615
+ // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
616
+ const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
617
+ hex = hex.replace(shorthandRegex, function(m, r, g, b) {
618
+ return r + r + g + g + b + b;
619
+ });
620
+
621
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
622
+ return result ? {
623
+ r: parseInt(result[1], 16),
624
+ g: parseInt(result[2], 16),
625
+ b: parseInt(result[3], 16)
626
+ } : { r: 0, g: 0, b: 0 };
627
+ }
628
+
629
+ function downloadImage() {
630
+ if (!originalImage) {
631
+ alert('Please upload an image first!');
632
+ return;
633
+ }
634
+
635
+ const link = document.createElement('a');
636
+ link.download = 'dithered-image.png';
637
+ link.href = canvas.toDataURL('image/png');
638
+ link.click();
639
+ }
640
+
641
+ function showLoading() {
642
+ loadingOverlay.classList.remove('hidden');
643
+ }
644
+
645
+ function hideLoading() {
646
+ loadingOverlay.classList.add('hidden');
647
+ }
648
+
649
+ function applyPreset(preset) {
650
+ switch (preset) {
651
+ case 'retro':
652
+ currentAlgorithm = 'floydSteinberg';
653
+ currentColors = ['#000000', '#ffffff'];
654
+ threshold = 128;
655
+ intensity = 60;
656
+ pixelSize = 4;
657
+ break;
658
+ case 'newsprint':
659
+ currentAlgorithm = 'bayer';
660
+ currentColors = ['#000000', '#ffffff'];
661
+ threshold = 100;
662
+ intensity = 70;
663
+ pixelSize = 1;
664
+ break;
665
+ case 'poster':
666
+ currentAlgorithm = 'atkinson';
667
+ currentColors = ['#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff', '#00ffff'];
668
+ threshold = 128;
669
+ intensity = 80;
670
+ pixelSize = 2;
671
+ break;
672
+ case 'sketch':
673
+ currentAlgorithm = 'random';
674
+ currentColors = ['#000000', '#ffffff'];
675
+ threshold = 150;
676
+ intensity = 40;
677
+ pixelSize = 1;
678
+ break;
679
+ case 'xray':
680
+ currentAlgorithm = 'threshold';
681
+ currentColors = ['#000000', '#ffffff'];
682
+ threshold = 200;
683
+ intensity = 90;
684
+ pixelSize = 1;
685
+ break;
686
+ case 'vintage':
687
+ currentAlgorithm = 'floydSteinberg';
688
+ currentColors = ['#654321', '#8b0000', '#ff8c00', '#ffd700', '#ffffff'];
689
+ threshold = 100;
690
+ intensity = 50;
691
+ pixelSize = 3;
692
+ break;
693
+ }
694
+
695
+ // Update UI to match preset
696
+ algorithmBtns.forEach(btn => {
697
+ btn.classList.remove('active');
698
+ if (btn.dataset.algorithm === currentAlgorithm) {
699
+ btn.classList.add('active');
700
+ }
701
+ });
702
+
703
+ thresholdSlider.value = threshold;
704
+ intensitySlider.value = intensity;
705
+ pixelSizeSlider.value = pixelSize;
706
+
707
+ applyDithering();
708
+ }
709
+
710
+ // Load a sample image on startup
711
+ const sampleImage = new Image();
712
+ sampleImage.onload = function() {
713
+ originalImage = sampleImage;
714
+ resetCanvas();
715
+ applyDithering();
716
+ };
717
+ sampleImage.src = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4MDAiIGhlaWdodD0iNjAwIiB2aWV3Qm94PSIwIDAgODAwIDYwMCI+PHJlY3Qgd2lkdGg9IjgwMCIgaGVpZ2h0PSI2MDAiIGZpbGw9IiNmNWY1ZjUiLz48Y2lyY2xlIGN4PSI0MDAiIGN5PSIzMDAiIHI9IjI1MCIgZmlsbD0iI2Q0ZDVkNCIvPjxjaXJjbGUgY3g9IjMwMCIgY3k9IjIwMCIgcj0iNTAiIGZpbGw9IiNhYWEiLz48Y2lyY2xlIGN4PSI1MDAiIGN5PSIyMDAiIHI9IjUwIiBmaWxsPSIjYWFhIi8+PGNpcmNsZSBjeD0iMzUwIiBjeT0iMjUwIiByPSIxMDAiIGZpbGw9IiNhYWEiLz48Y2lyY2xlIGN4PSI0NTAiIGN5PSIyNTAiIHI9IjEwMCIgZmlsbD0iI2FhYSIvPjxwYXRoIGQ9Ik0zMDAgNDAwIHEgMTAwIDEwMCAyMDAgMCIgc3Ryb2tlPSIjYWFhIiBzdHJva2Utd2lkdGg9IjEwIiBmaWxsPSJub25lIi8+PC9zdmc+';
718
+ });
719
+ </script>
720
+ <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>
721
+ </html>