@@ -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;
4950var menuVisible = false ;
5051var list_of_themes ;
5152var themes_keys ;
53+ var dithering = false ;
5254
5355// Fetch data from themes.json
5456fetch ( "./assets/themes.json" )
@@ -61,6 +63,7 @@ fetch("./assets/themes.json")
6163
6264// Loads image onto canvas
6365function 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+
169177function 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/*
188216This is the function that processes the image.
189217It works by scanning every pixel and finding the nearest colour.
190218After 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*/
192221function 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+ } ) ;
0 commit comments