mirror of
https://github.com/janishutz/MusicPlayerV2.git
synced 2025-11-25 13:04:23 +00:00
color palette extractor
This commit is contained in:
@@ -43,6 +43,10 @@ app.get( '/showcase.js', ( req, res ) => {
|
|||||||
res.sendFile( path.join( __dirname + '/client/showcase.js' ) );
|
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 ) => {
|
app.get( '/showcase.css', ( req, res ) => {
|
||||||
res.sendFile( path.join( __dirname + '/client/showcase.css' ) );
|
res.sendFile( path.join( __dirname + '/client/showcase.css' ) );
|
||||||
} );
|
} );
|
||||||
|
|||||||
145
frontend/src/client/colorPaletteExtractor.js
Normal file
145
frontend/src/client/colorPaletteExtractor.js
Normal file
@@ -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 );
|
||||||
|
} );
|
||||||
|
};
|
||||||
@@ -171,3 +171,21 @@ body {
|
|||||||
font-size: 5vw !important;
|
font-size: 5vw !important;
|
||||||
user-select: none;
|
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;
|
||||||
|
}
|
||||||
@@ -11,8 +11,14 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="content" id="app">
|
<div class="content" id="app">
|
||||||
<h1>Ok</h1>
|
<div v-if="hasLoaded" style="width: 100%">
|
||||||
<div class="song-list-wrapper" v-if="hasLoaded">
|
<div class="current-song">
|
||||||
|
<span class="material-symbols-outlined fancy-view-song-art" v-if="!playingSong.hasCoverArt">music_note</span>
|
||||||
|
<img v-else :src="'/getSongCover?filename=' + playingSong.filename" class="fancy-view-song-art">
|
||||||
|
<h1>{{ playingSong.title }}</h1>
|
||||||
|
<p>{{ playingSong.artist }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="song-list-wrapper">
|
||||||
<div v-for="song in songQueue" class="song-list">
|
<div v-for="song in songQueue" class="song-list">
|
||||||
<span class="material-symbols-outlined song-image" v-if="!song.hasCoverArt && ( playingSong.filename !== song.filename || isPlaying )">music_note</span>
|
<span class="material-symbols-outlined song-image" v-if="!song.hasCoverArt && ( playingSong.filename !== song.filename || isPlaying )">music_note</span>
|
||||||
<img v-else-if="song.hasCoverArt && ( playingSong.filename !== song.filename || isPlaying )" :src="'/getSongCover?filename=' + song.filename" class="song-image">
|
<img v-else-if="song.hasCoverArt && ( playingSong.filename !== song.filename || isPlaying )" :src="'/getSongCover?filename=' + song.filename" class="song-image">
|
||||||
@@ -31,12 +37,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- <img :src="" alt=""> -->
|
<!-- <img :src="" alt=""> -->
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<h1>Loading...</h1>
|
<h1>Loading...</h1>
|
||||||
</div>
|
</div>
|
||||||
|
<canvas id="canvas"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
|
||||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||||
|
<script src="/colorPaletteExtractor.js"></script>
|
||||||
<script src="/showcase.js"></script>
|
<script src="/showcase.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -10,6 +10,7 @@ createApp( {
|
|||||||
isPlaying: false,
|
isPlaying: false,
|
||||||
pos: 0,
|
pos: 0,
|
||||||
queuePos: 0,
|
queuePos: 0,
|
||||||
|
colourPalette: [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -40,6 +41,11 @@ createApp( {
|
|||||||
this.songs = data.data.songQueue ?? [];
|
this.songs = data.data.songQueue ?? [];
|
||||||
this.pos = data.data.pos ?? 0;
|
this.pos = data.data.pos ?? 0;
|
||||||
this.queuePos = data.data.queuePos ?? 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' ) {
|
} else if ( data.type === 'pos' ) {
|
||||||
this.pos = data.data;
|
this.pos = data.data;
|
||||||
} else if ( data.type === 'isPlaying' ) {
|
} else if ( data.type === 'isPlaying' ) {
|
||||||
@@ -48,6 +54,11 @@ createApp( {
|
|||||||
this.songs = data.data;
|
this.songs = data.data;
|
||||||
} else if ( data.type === 'playingSong' ) {
|
} else if ( data.type === 'playingSong' ) {
|
||||||
this.playingSong = data.data;
|
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' ) {
|
} else if ( data.type === 'queuePos' ) {
|
||||||
this.queuePos = data.data;
|
this.queuePos = data.data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,7 @@
|
|||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
object-position: center;
|
object-position: center;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
font-size: 40vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
|
|||||||
Reference in New Issue
Block a user