Skip to content

Commit 1191401

Browse files
committed
Add dithering functionality, to improve images with gradients
1 parent 0f74345 commit 1191401

File tree

3 files changed

+144
-42
lines changed

3 files changed

+144
-42
lines changed

assets/convert.js

Lines changed: 77 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ window.addEventListener("paste", (e) => {
1111
// Prevent default drag behaviors
1212
["dragenter", "dragover", "dragleave", "drop"].forEach((e) => {
1313
document.body.addEventListener(e, preventDefaults, false);
14+
imageLoader.style.height = "12vh";
1415
});
1516

1617
// Handle dropped files
@@ -49,6 +50,7 @@ var colour_palette_count = 0;
4950
var menuVisible = false;
5051
var list_of_themes;
5152
var themes_keys;
53+
var dithering = false;
5254

5355
// Fetch data from themes.json
5456
fetch("./assets/themes.json")
@@ -61,6 +63,7 @@ fetch("./assets/themes.json")
6163

6264
// Loads image onto canvas
6365
function handleImage(source) {
66+
imageLoader.style.height = "1vh";
6467
ogimage = source;
6568
var reader = new FileReader();
6669

@@ -166,66 +169,107 @@ function createCustomPalette() {
166169
displayPalette();
167170
}
168171

172+
function dither() {
173+
dithering = !dithering;
174+
document.getElementById("dither-checkbox").checked = dithering;
175+
}
176+
169177
function initialize() {
170178
if (theme.length == 0) {
171179
scrollTheme();
172180
}
173181
if (customMenu.style.display === "block") {
174182
createCustomPalette();
175183
}
176-
loadingScreen.style.opacity = "100";
177184
loadingScreen.style.visibility = "visible";
178-
loadingScreen.style.transition = "0s";
185+
loadingScreen.style.opacity = "100";
186+
loadingScreen.style.display = "block";
179187
setTimeout(function () {
180188
convertImage();
181-
loadingScreen.style.transition = "0.5s";
182189
loadingScreen.style.opacity = "0";
183190
loadingScreen.style.visibility = "hidden";
184-
}, 0);
191+
}, 100);
192+
}
193+
194+
function nearestColour(targetColour) {
195+
let minDistance = Infinity;
196+
let closestColor = theme[0];
197+
198+
for (let i = 0; i < theme.length; i += 3) {
199+
let color = [theme[i], theme[i + 1], theme[i + 2]];
200+
// Euclidean distance in RGB space
201+
let distance = Math.sqrt(
202+
Math.pow(targetColour[0] - color[0], 2) +
203+
Math.pow(targetColour[1] - color[1], 2) +
204+
Math.pow(targetColour[2] - color[2], 2)
205+
);
206+
207+
if (distance < minDistance) {
208+
minDistance = distance;
209+
closestColor = color;
210+
}
211+
}
212+
return closestColor;
185213
}
186214

187215
/*
188216
This is the function that processes the image.
189217
It works by scanning every pixel and finding the nearest colour.
190218
After finding the nearest colour, it uses that data to reconstruct the image.
219+
If we are dithering, we calculate the quantization error and distribute it using the Sierra Lite kernel.
191220
*/
192221
function convertImage() {
193222
downloadButton.style.visibility = "hidden";
194223
resetButton.style.visibility = "hidden";
195224
// Assigning variables
196225
var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
197226
var pixels = imageData.data;
198-
var numPixels = pixels.length;
199-
var lens = [];
200-
var minimum = 0;
201-
var x = 0;
202-
203-
// For every pixel in the image
204-
for (var i = 0; i < numPixels; i += 4) {
205-
minimum = 0;
206-
// For the amount of colours there are in the theme
207-
for (var j = 0; j < theme.length; j += 3) {
208-
// 3d distance formula
209-
lens[x] = Math.sqrt(
210-
Math.pow(pixels[i] - theme[j], 2) +
211-
Math.pow(pixels[i + 1] - theme[j + 1], 2) +
212-
Math.pow(pixels[i + 2] - theme[j + 2], 2)
213-
);
214-
x += 1;
215-
}
216-
x = 0;
217-
// Sort to find the smallest value (closest distance)
218-
for (var k = 1; k < lens.length; k++) {
219-
if (lens[k] < lens[minimum]) {
220-
minimum = k;
221-
}
222-
}
223227

224-
// Assign the R,G, and B values based on the smallest value
225-
for (var k = 0; k < 3; k++) {
226-
pixels[i + k] = theme[minimum * 3 + k];
228+
for (let y = 0; y < canvas.height; y++) {
229+
for (let x = 0; x < canvas.width; x++) {
230+
const index = (y * canvas.width + x) * 4;
231+
232+
// Get the original pixel colour
233+
let oldPixel = [
234+
pixels[index],
235+
pixels[index + 1],
236+
pixels[index + 2],
237+
];
238+
239+
// Find the closest color from the palette
240+
let newPixel = nearestColour(oldPixel);
241+
242+
// Replace the pixel with the new colour
243+
pixels[index] = newPixel[0];
244+
pixels[index + 1] = newPixel[1];
245+
pixels[index + 2] = newPixel[2];
246+
247+
if (dithering) {
248+
// Calculate quantization error
249+
let quantError = [
250+
oldPixel[0] - newPixel[0],
251+
oldPixel[1] - newPixel[1],
252+
oldPixel[2] - newPixel[2],
253+
];
254+
255+
// Distribute the error using Sierra Lite kernel
256+
if (x + 1 < canvas.width) {
257+
const rightIndex = index + 4;
258+
pixels[rightIndex] += (quantError[0] * 1) / 2;
259+
pixels[rightIndex + 1] += (quantError[1] * 1) / 2;
260+
pixels[rightIndex + 2] += (quantError[2] * 1) / 2;
261+
}
262+
263+
if (y + 1 < canvas.height) {
264+
const belowIndex = index + canvas.width * 4;
265+
pixels[belowIndex] += (quantError[0] * 1) / 2;
266+
pixels[belowIndex + 1] += (quantError[1] * 1) / 2;
267+
pixels[belowIndex + 2] += (quantError[2] * 1) / 2;
268+
}
269+
}
227270
}
228271
}
272+
229273
// Reconstruct the image and make the download/reset buttons visible
230274
ctx.putImageData(imageData, 0, 0);
231275
downloadButton.style.visibility = "visible";
@@ -281,4 +325,4 @@ document.addEventListener("click", function (event) {
281325
createCustomPalette();
282326
});
283327
}
284-
});
328+
});

assets/style.css

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ h1 {
2121
word-spacing: 7px;
2222
font-weight: 100;
2323
display: inline-table;
24+
margin: 0.3em 0;
2425
}
2526

2627
a {
@@ -66,7 +67,7 @@ button {
6667
}
6768

6869
button:hover {
69-
background-color: #222;
70+
background-color: var(--darkest-grey);
7071
}
7172

7273
.main-button {
@@ -80,6 +81,49 @@ button:hover {
8081
visibility: hidden;
8182
}
8283

84+
.dither-options {
85+
margin: auto;
86+
padding: 0.5em;
87+
text-align: center;
88+
width: fit-content;
89+
user-select: none;
90+
}
91+
92+
input[type="checkbox"] {
93+
display: none;
94+
--webkit-appearance: none;
95+
width: 1.1em;
96+
height: 1.1em;
97+
margin: 0.5em;
98+
cursor: pointer;
99+
font-size: 1em;
100+
display: inline;
101+
}
102+
103+
input[type="checkbox"]:checked {
104+
accent-color: var(--blue-grey);
105+
}
106+
107+
#dither-checkbox-label {
108+
font-size: 1em;
109+
}
110+
111+
.tooltip-text {
112+
display: none;
113+
position: absolute;
114+
background-color: var(--dark-grey);
115+
color: var(--white);
116+
border-radius: 5px;
117+
padding: 0.5em;
118+
z-index: 1;
119+
font-size: 0.8em;
120+
left: 43vw;
121+
}
122+
123+
.dither-options:hover .tooltip-text {
124+
display: block;
125+
}
126+
83127
#drop-zone {
84128
background-color: var(--darkest-grey);
85129
text-align: center;
@@ -106,10 +150,15 @@ button:hover {
106150
background-color: var(--dark-grey);
107151
border-radius: 3px;
108152
color: var(--white);
153+
border: 2px solid var(--light-grey);
109154
margin: 0.5em;
110155
cursor: pointer;
111156
}
112157

158+
#image-loader::file-selector-button:hover {
159+
background-color: var(--darkest-grey);
160+
}
161+
113162
#image-canvas {
114163
border: 3px solid var(--light-grey);
115164
max-height: 50vh;
@@ -140,7 +189,7 @@ button:hover {
140189
display: inline-block;
141190
height: 1em;
142191
width: 1em;
143-
border: 1px solid #aaa;
192+
border: 1px solid var(--light-grey);
144193
}
145194

146195
.options-container {
@@ -205,6 +254,7 @@ input[type="color"] {
205254
text-align: center;
206255
font-size: xx-large;
207256
background-color: rgba(30, 30, 30, 0.8);
257+
transition: 0.5s;
208258
}
209259

210260
#loading-screen p {
@@ -224,6 +274,10 @@ footer a {
224274
}
225275

226276
@media screen and (max-width: 800px) {
277+
h1 {
278+
font-size: 1.5em;
279+
}
280+
227281
header {
228282
margin-left: 0vw;
229283
margin-right: 0vw;
@@ -247,8 +301,8 @@ footer a {
247301
}
248302

249303
#image-canvas {
250-
max-height: 40vh;
251-
min-height: 20vh;
304+
width: 84vw;
305+
min-height: 10vw;
252306
}
253307

254308
.download-buttons {
@@ -268,4 +322,8 @@ footer a {
268322
height: 0.8em;
269323
width: 0.8em;
270324
}
325+
326+
#custom-menu {
327+
width: auto;
328+
}
271329
}

index.html

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ <h1>Wallpaper Theme Converter</h1>
1616
<body>
1717
<div id="loading-screen">
1818
<p>Loading...</p>
19-
2019
</div>
2120
<hr />
2221
<br/>
@@ -76,16 +75,17 @@ <h3>Select a theme</h3>
7675
<button id="opt-down" onclick="scrollTheme(1)"></button>
7776
<button id="custom-theme-button" onclick="openCustomMenu()"> + Custom Theme </button>
7877
</div>
79-
80-
<br/>
81-
78+
<div class="dither-options" onclick="dither()">
79+
<input type="checkbox" id="dither-checkbox" />
80+
<label id="dither-checkbox-label" for="checkbox">More Accurate Colours</label>
81+
<span class="tooltip-text">Adds dithering to reduce colour-banding</span>
82+
</div>
8283
<div id="custom-menu">
8384
<button onclick="addColour()">+</button>
8485
<button onclick="removeColour()">-</button>
8586
<div id="colours">
8687
</div>
8788
</div>
88-
8989
<button class="main-button" id="convert" type="button" onclick="initialize()">Convert</button>
9090
</div>
9191

0 commit comments

Comments
 (0)