diff --git a/frontend/src/app.js b/frontend/src/app.js index 2b1c2fb..7d57654 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -43,6 +43,10 @@ app.get( '/showcase.js', ( req, res ) => { res.sendFile( path.join( __dirname + '/client/showcase.js' ) ); } ); +app.get( '/colorPaletteExtractor.js', ( req, res ) => { + res.sendFile( path.join( __dirname + '/client/colorPaletteExtractor.js' ) ); +} ); + app.get( '/showcase.css', ( req, res ) => { res.sendFile( path.join( __dirname + '/client/showcase.css' ) ); } ); diff --git a/frontend/src/client/colorPaletteExtractor.js b/frontend/src/client/colorPaletteExtractor.js new file mode 100644 index 0000000..cd9d2f7 --- /dev/null +++ b/frontend/src/client/colorPaletteExtractor.js @@ -0,0 +1,145 @@ +// https://github.com/zygisS22/color-palette-extraction/blob/master/index.js + + +const buildRgb = ( imageData ) => { + const rgbValues = []; + // note that we are loopin every 4! + // for every Red, Green, Blue and Alpha + for (let i = 0; i < imageData.length; i += 4) { + const rgb = { + r: imageData[ i ], + g: imageData[ i + 1 ], + b: imageData[ i + 2 ], + }; + + rgbValues.push(rgb); + } + + return rgbValues; +}; + +// returns what color channel has the biggest difference +const findBiggestColorRange = (rgbValues) => { + /** + * Min is initialized to the maximum value posible + * from there we procced to find the minimum value for that color channel + * + * Max is initialized to the minimum value posible + * from there we procced to fin the maximum value for that color channel + */ + let rMin = Number.MAX_VALUE; + let gMin = Number.MAX_VALUE; + let bMin = Number.MAX_VALUE; + + let rMax = Number.MIN_VALUE; + let gMax = Number.MIN_VALUE; + let bMax = Number.MIN_VALUE; + + rgbValues.forEach((pixel) => { + rMin = Math.min(rMin, pixel.r); + gMin = Math.min(gMin, pixel.g); + bMin = Math.min(bMin, pixel.b); + + rMax = Math.max(rMax, pixel.r); + gMax = Math.max(gMax, pixel.g); + bMax = Math.max(bMax, pixel.b); + }); + + const rRange = rMax - rMin; + const gRange = gMax - gMin; + const bRange = bMax - bMin; + + // determine which color has the biggest difference + const biggestRange = Math.max(rRange, gRange, bRange); + if (biggestRange === rRange) { + return 'r'; + } else if (biggestRange === gRange) { + return 'g'; + } else { + return 'b'; + } +}; + +/** + * Median cut implementation + * can be found here -> https://en.wikipedia.org/wiki/Median_cut + */ +const quantization = ( rgbValues, depth ) => { + const MAX_DEPTH = 4; + + // Base case + if ( depth === MAX_DEPTH || rgbValues.length === 0 ) { + const color = rgbValues.reduce( + ( prev, curr ) => { + prev.r += curr.r; + prev.g += curr.g; + prev.b += curr.b; + + return prev; + }, + { + r: 0, + g: 0, + b: 0, + } + ); + + color.r = Math.round( color.r / rgbValues.length ); + color.g = Math.round( color.g / rgbValues.length ); + color.b = Math.round( color.b / rgbValues.length ); + + return [color]; + } + + /** + * Recursively do the following: + * 1. Find the pixel channel (red,green or blue) with biggest difference/range + * 2. Order by this channel + * 3. Divide in half the rgb colors list + * 4. Repeat process again, until desired depth or base case + */ + const componentToSortBy = findBiggestColorRange( rgbValues ); + rgbValues.sort( ( p1, p2 ) => { + return p1[ componentToSortBy ] - p2[ componentToSortBy ]; + } ); + + const mid = rgbValues.length / 2; + return [ + ...quantization( rgbValues.slice( 0, mid ), depth + 1 ), + ...quantization( rgbValues.slice( mid + 1 ), depth + 1 ), + ]; +}; + +const getColourPalette = ( imageURL ) => { + return new Promise( ( resolve, reject ) => { + + // Set the canvas size to be the same as of the uploaded image + let image = new Image(); + image.src = imageURL; + const canvas = document.getElementById( 'canvas' ); + setTimeout( () => { + canvas.width = image.width ?? 500; + canvas.height = image.height ?? 500; + const ctx = canvas.getContext( '2d' ); + ctx.drawImage(image, 0, 0); + + /** + * getImageData returns an array full of RGBA values + * each pixel consists of four values: the red value of the colour, the green, the blue and the alpha + * (transparency). For array value consistency reasons, + * the alpha is not from 0 to 1 like it is in the RGBA of CSS, but from 0 to 255. + */ + const imageData = ctx.getImageData( 0, 0, canvas.width, canvas.height ); + + // Convert the image data to RGB values so its much simpler + const rgbArray = buildRgb( imageData.data ); + + /** + * Color quantization + * A process that reduces the number of colors used in an image + * while trying to visually maintin the original image as much as possible + */ + resolve( quantization( rgbArray, 0 ) ); + }, 1000 ); + } ); +}; \ No newline at end of file diff --git a/frontend/src/client/showcase.css b/frontend/src/client/showcase.css index d6d9772..b3fe7a5 100644 --- a/frontend/src/client/showcase.css +++ b/frontend/src/client/showcase.css @@ -170,4 +170,22 @@ body { object-position: center; font-size: 5vw !important; user-select: none; +} + +.current-song { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + height: 55vh; + width: 100%; +} + +.fancy-view-song-art { + height: 30vh; + width: 30vh; + object-fit: cover; + object-position: center; + margin-bottom: 20px; + font-size: 30vh !important; } \ No newline at end of file diff --git a/frontend/src/client/showcase.html b/frontend/src/client/showcase.html index 97b09b9..be80008 100644 --- a/frontend/src/client/showcase.html +++ b/frontend/src/client/showcase.html @@ -11,32 +11,41 @@
-

Ok

-
-
- music_note - -
-
-
-
-
+
+
+ music_note + +

{{ playingSong.title }}

+

{{ playingSong.artist }}

+
+
+
+ music_note + +
+
+
+
+
+
+
+ pause +
+

{{ song.title }}

+

{{ song.artist }}

- pause -
-

{{ song.title }}

-

{{ song.artist }}

-
+
-

Loading...

+
+ \ No newline at end of file diff --git a/frontend/src/client/showcase.js b/frontend/src/client/showcase.js index 7e7d6c2..47553a5 100644 --- a/frontend/src/client/showcase.js +++ b/frontend/src/client/showcase.js @@ -10,6 +10,7 @@ createApp( { isPlaying: false, pos: 0, queuePos: 0, + colourPalette: [], }; }, computed: { @@ -40,6 +41,11 @@ createApp( { this.songs = data.data.songQueue ?? []; this.pos = data.data.pos ?? 0; this.queuePos = data.data.queuePos ?? 0; + getColourPalette( '/getSongCover?filename=' + data.data.playingSong.filename ).then( palette => { + this.colourPalette = palette; + } ).catch( () => { + this.colourPalette = [ { 'r': 255, 'g': 0, 'b': 0 }, { 'r': 0, 'g': 255, 'b': 0 }, { 'r': 0, 'g': 0, 'b': 255 } ] + } ); } else if ( data.type === 'pos' ) { this.pos = data.data; } else if ( data.type === 'isPlaying' ) { @@ -48,6 +54,11 @@ createApp( { this.songs = data.data; } else if ( data.type === 'playingSong' ) { this.playingSong = data.data; + getColourPalette( '/getSongCover?filename=' + data.data.filename ).then( palette => { + this.colourPalette = palette; + } ).catch( () => { + this.colourPalette = [ { 'r': 255, 'g': 0, 'b': 0 }, { 'r': 0, 'g': 255, 'b': 0 }, { 'r': 0, 'g': 0, 'b': 255 } ] + } ); } else if ( data.type === 'queuePos' ) { this.queuePos = data.data; } diff --git a/frontend/src/components/fancyView.vue b/frontend/src/components/fancyView.vue index 986b574..773e041 100644 --- a/frontend/src/components/fancyView.vue +++ b/frontend/src/components/fancyView.vue @@ -80,6 +80,7 @@ object-fit: cover; object-position: center; margin-bottom: 20px; + font-size: 40vh; } .controls {