From e4670a15c50c4feac6b9b6343d0b79ea85ed7de9 Mon Sep 17 00:00:00 2001 From: Lily <56913305+Memez4Life7@users.noreply.github.com> Date: Sat, 9 Nov 2024 20:56:54 +0100 Subject: [PATCH 1/4] option for dynamic or static accent colors - dynamically change the accent colors based on the background image - added an option to select a custom static accent color --- app.css | 20 +++- color.ini | 6 +- hazy.js | 269 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 291 insertions(+), 4 deletions(-) diff --git a/app.css b/app.css index d81bd24..8d188dc 100644 --- a/app.css +++ b/app.css @@ -1,5 +1,4 @@ :root { - --backdrop: rgba(0, 0, 0, 0.3); --button-border: 8px; } @@ -1938,3 +1937,22 @@ POPUP MODAL .main-playlistEditDetailsModal-textElement:focus { background-color: rgba(var(--spice-rgb-selected-row), 0.1); } + +/*------------------- + NOW PLAYING COLOR FIX + -------------------*/ +.main-trackList-playingIcon { -webkit-mask-image: url("data:image/svg+xml,%3Csvg id='playing-icon' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 24'%3E%3Cdefs%3E%3Cstyle%3E %23playing-icon %7B fill: %2320BC54; %7D @keyframes play %7B 0%25 %7Btransform: scaleY(1);%7D 3.3%25 %7Btransform: scaleY(0.9583);%7D 6.6%25 %7Btransform: scaleY(0.9166);%7D 9.9%25 %7Btransform: scaleY(0.8333);%7D 13.3%25 %7Btransform: scaleY(0.7083);%7D 16.6%25 %7Btransform: scaleY(0.5416);%7D 19.9%25 %7Btransform: scaleY(0.4166);%7D 23.3%25 %7Btransform: scaleY(0.25);%7D 26.6%25 %7Btransform: scaleY(0.1666);%7D 29.9%25 %7Btransform: scaleY(0.125);%7D 33.3%25 %7Btransform: scaleY(0.125);%7D 36.6%25 %7Btransform: scaleY(0.1666);%7D 39.9%25 %7Btransform: scaleY(0.1666);%7D 43.3%25 %7Btransform: scaleY(0.2083);%7D 46.6%25 %7Btransform: scaleY(0.2916);%7D 49.9%25 %7Btransform: scaleY(0.375);%7D 53.3%25 %7Btransform: scaleY(0.5);%7D 56.6%25 %7Btransform: scaleY(0.5833);%7D 59.9%25 %7Btransform: scaleY(0.625);%7D 63.3%25 %7Btransform: scaleY(0.6666);%7D 66.6%25 %7Btransform: scaleY(0.6666);%7D 69.9%25 %7Btransform: scaleY(0.6666);%7D 73.3%25 %7Btransform: scaleY(0.6666);%7D 76.6%25 %7Btransform: scaleY(0.7083);%7D 79.9%25 %7Btransform: scaleY(0.75);%7D 83.3%25 %7Btransform: scaleY(0.8333);%7D 86.6%25 %7Btransform: scaleY(0.875);%7D 89.9%25 %7Btransform: scaleY(0.9166);%7D 93.3%25 %7Btransform: scaleY(0.9583);%7D 96.6%25 %7Btransform: scaleY(1);%7D %7D %23bar1 %7B transform-origin: bottom; animation: play 0.9s -0.51s infinite; %7D %23bar2 %7B transform-origin: bottom; animation: play 0.9s infinite; %7D %23bar3 %7B transform-origin: bottom; animation: play 0.9s -0.15s infinite; %7D %23bar4 %7B transform-origin: bottom; animation: play 0.9s -0.75s infinite; %7D %3C/style%3E%3C/defs%3E%3Ctitle%3Eplaying-icon%3C/title%3E%3Crect id='bar1' class='cls-1' width='4' height='24'/%3E%3Crect id='bar2' class='cls-1' x='6' width='4' height='24'/%3E%3Crect id='bar3' class='cls-1' x='12' width='4' height='24'/%3E%3Crect id='bar4' class='cls-1' x='18' width='4' height='24'/%3E%3C/svg%3E"); background: var(--spice-button); content-visibility: hidden; -webkit-mask-repeat: no-repeat; } + +/*------------------- + COLOR PICKER + -------------------*/ +input[type="color"] { + border: none; + background-color: black; + overflow: hidden; + cursor: pointer; + border-radius: 4px; + border: none; + padding: 2px; + vertical-align: middle; +} \ No newline at end of file diff --git a/color.ini b/color.ini index 84d3b37..6452b42 100644 --- a/color.ini +++ b/color.ini @@ -3,6 +3,6 @@ main = 0A0A0A subtext = F0F0F0 shadow = 000000 text = FFFFFF -button = 30bf63 -button-active = 30bf63 -accent = 30bf63 \ No newline at end of file +button = FFC0EA +button-active = FFC0EA +accent = FFC0EA \ No newline at end of file diff --git a/hazy.js b/hazy.js index 7975101..9e6d468 100644 --- a/hazy.js +++ b/hazy.js @@ -91,8 +91,227 @@ loopOptions("/"); updateLyricsPageProperties(); + + //custom code added by lily + if (!config.useCustomColor) { + let imageUrl; + if (!config.useCurrSongAsHome) { + imageUrl = Spicetify.Player.data.item.metadata.image_url.replace("spotify:image:", "https://i.scdn.co/image/"); + } else { + const defImage = "https://i.imgur.com/Wl2D0h0.png"; + imageUrl = localStorage.getItem("hazy:startupBg") || defImage; + } + + changeAccentColors(imageUrl); + } else { + let color = localStorage.getItem("CustomColor") || "#FFC0EA"; + document.querySelector(':root').style.setProperty('--spice-button', color); + document.querySelector(':root').style.setProperty('--spice-button-active', color); + document.querySelector(':root').style.setProperty('--spice-accent', color); + } + } + + //functions added by lily + //changes the accent colors to the most prominent color + //in the background image when the background is changed + //------------------------------------- + + function changeAccentColors(imageUrl) { + getMostProminentColor(imageUrl, function (color) { + document.querySelector(':root').style.setProperty('--spice-button', color); + document.querySelector(':root').style.setProperty('--spice-button-active', color); + document.querySelector(':root').style.setProperty('--spice-accent', color); + }); + } + + function getMostProminentColor(imageUrl, callback) { + const img = new Image(); + img.crossOrigin = "Anonymous"; // allows CORS-enabled images + + img.onload = function () { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + canvas.width = img.width; + canvas.height = img.height; + ctx.drawImage(img, 0, 0); + + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height).data; + let rgbList = buildRgb(imageData); + + // attempt with filters + let hexColor = findColor(rgbList); + + // retry without filters if no color is found + if (!hexColor) { + hexColor = findColor(rgbList, true); + } + + callback(hexColor); + }; + + img.onerror = function () { + console.error("Image load error"); + callback(null); + }; + + img.src = imageUrl; + } + + //gets the most prominent color in a list of rgb values + function findColor(rgbList, skipFilters = false) { + const colorCount = {}; + let maxColor = ''; + let maxCount = 0; + + for (let i = 0; i < rgbList.length; i++) { + if (!skipFilters && (isTooDark(rgbList[i]) || isTooCloseToWhite(rgbList[i]))) { + continue; + } + + const color = `${rgbList[i].r},${rgbList[i].g},${rgbList[i].b}`; + colorCount[color] = (colorCount[color] || 0) + 1; + + if (colorCount[color] > maxCount) { + maxColor = color; + maxCount = colorCount[color]; + } + } + + if (maxColor) { + const [r, g, b] = maxColor.split(',').map(Number); + return rgbToHex(r, g, b); + } else { + return null; // no color found + } + } + + //quantizes a list of rgb values (currently unused) + 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), + ]; + }; + + // returns what color channel has the biggest difference (currently unused) + 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"; + } + }; + + //creates a list of rgb values from image data + 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; + }; + + //converts RGB to Hex + function rgbToHex(r, g, b) { + return "#" + [r, g, b].map(x => x.toString(16).padStart(2, '0')).join(''); + } + + //checks if a color is too dark + function isTooDark(rgb) { + const brightness = 0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b; + const threshold = 100; // adjust this value to control the "darkness" threshold + return brightness < threshold; + } + + //checks if a color is too close to white + function isTooCloseToWhite(rgb) { + const threshold = 200; + return rgb.r > threshold && rgb.g > threshold && rgb.b > threshold; } + //------------------------------------- + Spicetify.Player.addEventListener("songchange", onSongChange); onSongChange(); windowControls(); @@ -418,6 +637,14 @@ config.useCurrSongAsHome = JSON.parse( localStorage.getItem("UseCustomBackground") ); + + //save the selected custom color + //to the config (added by lily) + //------------------------- + config.useCustomColor = JSON.parse( + localStorage.getItem("UseCustomColor") + ); + //------------------------- } parseOptions(); @@ -732,6 +959,32 @@ createOption("UseCustomBackground", "Custom background:", false); setValue("blurAmount", "contAmount", "satuAmount", "brightAmount", " "); + //additional settings (added by lily) + //------------------- + + //color label + const colorLabel = document.createElement("label"); + colorLabel.id = "color-label"; + colorLabel.htmlFor = "color"; + colorLabel.textContent = "Color:"; + colorLabel.style.textAlign = "right"; + colorLabel.style.marginRight = "10px"; + colorLabel.style.fontSize = "0.875rem"; + optionList.append(colorLabel); + + //color picker + const colorInput = document.createElement("input"); + colorInput.type = "color"; + colorInput.id = "color-input"; + colorInput.value = localStorage.getItem("CustomColor") || "#FFC0EA"; + colorInput.style.border = "none"; + optionList.append(colorInput); + + //color toggle + createOption("UseCustomColor", "Custom color:", false); + + //------------------- + content.append(optionList); content.append(valueList); @@ -762,10 +1015,26 @@ // update changed bg image startImage = srcInput.value || content.querySelector("img").src; localStorage.setItem("hazy:startupBg", startImage); + + //save the selected custom color (added by lily) + //------------------------- + let colorElem = document.getElementById("color-input"); + localStorage.setItem("CustomColor", colorElem.value); + //------------------------- + onSongChange(); // save options to local storage for (const option of [...optionList.children]) { + + //ignore the color changing options + //as they are handled differently (added by lily) + //------------------------- + if (option.id == "color-input" || option.id == "color-label") { + continue; + } + //------------------------- + localStorage.setItem( option.getAttribute("name"), option.querySelector(".toggle").classList.contains("enabled") From 65dde31bf2bdbc50091e46baa21fe764561494ac Mon Sep 17 00:00:00 2001 From: Lily <56913305+Memez4Life7@users.noreply.github.com> Date: Sat, 9 Nov 2024 21:11:41 +0100 Subject: [PATCH 2/4] forgot to commit this --- app.css | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app.css b/app.css index 8d188dc..36c0cbc 100644 --- a/app.css +++ b/app.css @@ -1,5 +1,16 @@ :root { - --button-border: 8px; + --backdrop: rgba(0, 0, 0, 0.3); + --button-border: 8px; + + /* root variables from color.ini + --spice-main: 0A0A0A; + --spice-subtext: F0F0F0; + --spice-shadow: 000000; + --spice-text: FFFFFF; + --spice-button: FFC0EA; + --spice-button-active: FFC0EA; + --spice-accent: FFC0EA; + */ } svg { From d2292ee50bd0ed32713a08686a4c4da580d7b177 Mon Sep 17 00:00:00 2001 From: Lily <56913305+Memez4Life7@users.noreply.github.com> Date: Sat, 9 Nov 2024 21:16:50 +0100 Subject: [PATCH 3/4] cleanup for pull request --- color.ini | 6 +-- hazy.js | 107 ------------------------------------------------------ 2 files changed, 3 insertions(+), 110 deletions(-) diff --git a/color.ini b/color.ini index 6452b42..84d3b37 100644 --- a/color.ini +++ b/color.ini @@ -3,6 +3,6 @@ main = 0A0A0A subtext = F0F0F0 shadow = 000000 text = FFFFFF -button = FFC0EA -button-active = FFC0EA -accent = FFC0EA \ No newline at end of file +button = 30bf63 +button-active = 30bf63 +accent = 30bf63 \ No newline at end of file diff --git a/hazy.js b/hazy.js index 9e6d468..e41fbcf 100644 --- a/hazy.js +++ b/hazy.js @@ -185,113 +185,6 @@ } } - //quantizes a list of rgb values (currently unused) - 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), - ]; - }; - - // returns what color channel has the biggest difference (currently unused) - 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"; - } - }; - - //creates a list of rgb values from image data - 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; - }; - //converts RGB to Hex function rgbToHex(r, g, b) { return "#" + [r, g, b].map(x => x.toString(16).padStart(2, '0')).join(''); From 97465b0aac135b8e2c402ac726cad3110b55987d Mon Sep 17 00:00:00 2001 From: Lily <56913305+Memez4Life7@users.noreply.github.com> Date: Sat, 9 Nov 2024 21:18:52 +0100 Subject: [PATCH 4/4] messed up again --- hazy.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/hazy.js b/hazy.js index e41fbcf..da3557e 100644 --- a/hazy.js +++ b/hazy.js @@ -185,6 +185,24 @@ } } + //creates a list of rgb values from image data + 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; + }; + //converts RGB to Hex function rgbToHex(r, g, b) { return "#" + [r, g, b].map(x => x.toString(16).padStart(2, '0')).join('');