Merge pull request #113 from JulienMaille/colorthief

Colorthief
This commit is contained in:
Erik 2021-11-17 21:26:51 +01:00 committed by GitHub
commit f6aa2a5c7d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 141 additions and 66 deletions

View file

@ -1,5 +1,5 @@
Added: Added:
- `Report Bugs` and `Changelog` buttons to `Settings > About` - `Report Bugs`, `Request Feature` and `Changelog` buttons to `Settings > About`
- Markdown parsing for settings descriptions - Markdown parsing for settings descriptions
- Option to have a button to open the settings next to your profile picture - Option to have a button to open the settings next to your profile picture
@ -17,4 +17,5 @@ Improved:
- Checkbox / Switch input styles are now more in line with other input styles - Checkbox / Switch input styles are now more in line with other input styles
- Available updates are now shown as a clickable button next to your user icon instead of having to open the user menu - Available updates are now shown as a clickable button next to your user icon instead of having to open the user menu
- The "offline" icon is now handled by dribbblish and fits in with the other info icons - The "offline" icon is now handled by dribbblish and fits in with the other info icons
- Hovering over the release date in the album info now shows the full date - Hovering over the release date in the album info now shows the full date
- Better color extraction from the cover image

View file

@ -18,11 +18,12 @@
}, },
"dependencies": { "dependencies": {
"chroma-js": "^2.1.2", "chroma-js": "^2.1.2",
"colorthief": "^2.3.2",
"jquery": "^3.6.0", "jquery": "^3.6.0",
"markdown-it": "^12.2.0", "markdown-it": "^12.2.0",
"markdown-it-attrs": "^4.1.0", "markdown-it-attrs": "^4.1.0",
"markdown-it-bracketed-spans": "^1.0.1", "markdown-it-bracketed-spans": "^1.0.1",
"moment": "^2.29.1", "moment": "^2.29.1",
"node-vibrant": "3.1.4" "node-vibrant": "^3.1.6"
} }
} }

View file

@ -1,7 +1,7 @@
import $ from "jquery"; import $ from "jquery";
import MarkdownIt from "markdown-it"; import MarkdownIt from "markdown-it";
import MarkdownItAttrs from "markdown-it-attrs"; import MarkdownItAttrs from "markdown-it-attrs";
import markdownItBracketedSpans from "markdown-it-bracketed-spans"; import MarkdownItBracketedSpans from "markdown-it-bracketed-spans";
import svgUndo from "svg/undo"; import svgUndo from "svg/undo";
@ -82,8 +82,8 @@ export default class ConfigMenu {
linkify: true, linkify: true,
typographer: true typographer: true
}); });
this.#md.use(MarkdownItBracketedSpans);
this.#md.use(MarkdownItAttrs); this.#md.use(MarkdownItAttrs);
this.#md.use(markdownItBracketedSpans);
const container = document.createElement("div"); const container = document.createElement("div");
container.id = "dribbblish-config"; container.id = "dribbblish-config";

View file

@ -33,3 +33,7 @@ export function copyToClipboard(text) {
export function capitalizeFirstLetter(string) { export function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1); return string.charAt(0).toUpperCase() + string.slice(1);
} }
export function getClosestToNum(arr, num) {
return arr.reduce((prev, curr) => (Math.abs(curr - num) < Math.abs(prev - num) ? curr : prev));
}

View file

@ -1,9 +1,10 @@
import * as Vibrant from "node-vibrant"; import * as Vibrant from "node-vibrant";
import ColorThief from "colorthief";
import chroma from "chroma-js"; import chroma from "chroma-js";
import $ from "jquery"; import $ from "jquery";
import moment from "moment"; import moment from "moment";
import { waitForElement, copyToClipboard, capitalizeFirstLetter } from "./Util"; import { waitForElement, copyToClipboard, capitalizeFirstLetter, getClosestToNum } from "./Util";
import ConfigMenu from "./ConfigMenu"; import ConfigMenu from "./ConfigMenu";
import Info from "./Info"; import Info from "./Info";
@ -16,6 +17,7 @@ const Dribbblish = {
config: new ConfigMenu(), config: new ConfigMenu(),
info: new Info() info: new Info()
}; };
const colorThief = new ColorThief();
// To expose to external scripts // To expose to external scripts
window.Dribbblish = Dribbblish; window.Dribbblish = Dribbblish;
@ -387,7 +389,7 @@ waitForElement(["#main"], () => {
type: "button", type: "button",
key: "aboutDribbblishBugs", key: "aboutDribbblishBugs",
name: "Report Bugs", name: "Report Bugs",
description: "Open new issue on GitHub", description: "Open new issue on GitHub to report a bug",
data: "Create Report", data: "Create Report",
onChange: () => { onChange: () => {
const reportBody = ` const reportBody = `
@ -442,6 +444,22 @@ waitForElement(["#main"], () => {
} }
}); });
Dribbblish.config.register({
area: "About",
type: "button",
key: "aboutDribbblishFeature",
name: "Request Feature",
description: "Open new issue on GitHub to request a feature",
data: "Request Feature",
onChange: () => {
const reportURL = new URL("https://github.com/JulienMaille/dribbblish-dynamic-theme/issues/new");
reportURL.searchParams.set("labels", "enhancement");
reportURL.searchParams.set("template", "feature_request.md");
window.open(reportURL.toString(), "_blank");
}
});
Dribbblish.config.register({ Dribbblish.config.register({
area: "About", area: "About",
type: "button", type: "button",
@ -460,9 +478,7 @@ async function getAlbumRelease(uri) {
} }
function isLight(hex) { function isLight(hex) {
var [r, g, b] = chroma(hex).rgb().map(Number); return chroma(hex).luminance() > 0.5;
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
return brightness > 128;
} }
// From: https://stackoverflow.com/a/13763063/12126879 // From: https://stackoverflow.com/a/13763063/12126879
@ -492,10 +508,8 @@ function getImageLightness(img) {
return brightness; return brightness;
} }
// parse to hex beacuse "--spice-sidebar" is `rgb()` // parse to hex because "--spice-sidebar" is `rgb()`
let textColor = chroma($("html").css("--spice-text")).hex();
let textColorBg = chroma($("html").css("--spice-main")).hex(); let textColorBg = chroma($("html").css("--spice-main")).hex();
let sidebarColor = chroma($("html").css("--spice-sidebar")).hex();
function setRootColor(name, color) { function setRootColor(name, color) {
$("html").css(`--spice-${name}`, chroma(color).hex()); $("html").css(`--spice-${name}`, chroma(color).hex());
@ -514,10 +528,10 @@ function toggleDark(setDark) {
setRootColor("subtext", setDark ? "#EAEAEA" : "#3D3D3D"); setRootColor("subtext", setDark ? "#EAEAEA" : "#3D3D3D");
setRootColor("notification", setDark ? "#303030" : "#DDDDDD"); setRootColor("notification", setDark ? "#303030" : "#DDDDDD");
updateColors(textColor, sidebarColor, false); updateColors(false);
} }
function checkDarkLightMode(colors) { function checkDarkLightMode() {
const theme = Dribbblish.config.get("theme"); const theme = Dribbblish.config.get("theme");
if (theme == "time") { if (theme == "time") {
const start = 60 * parseInt(Dribbblish.config.get("darkModeOnTime").split(":")[0]) + parseInt(Dribbblish.config.get("darkModeOnTime").split(":")[1]); const start = 60 * parseInt(Dribbblish.config.get("darkModeOnTime").split(":")[0]) + parseInt(Dribbblish.config.get("darkModeOnTime").split(":")[1]);
@ -530,22 +544,31 @@ function checkDarkLightMode(colors) {
if (end < start) dark = start <= time || time < end; if (end < start) dark = start <= time || time < end;
else dark = start <= time && time < end; else dark = start <= time && time < end;
toggleDark(dark); toggleDark(dark);
} else if (theme == "color") {
if (colors && colors.length > 0) toggleDark(isLight(colors[0]));
} }
} }
// Run every Minute to check time and set dark / light mode // Run every Minute to check time and set dark / light mode
setInterval(checkDarkLightMode, 60000); setInterval(checkDarkLightMode, 60000);
Dribbblish.config.register({ Dribbblish.config.register({
area: "Theme", area: "Theme",
type: "checkbox", type: "select",
key: "dynamicColors", key: "colorSelectionAlgorithm",
name: "Dynamic", name: "Color Selection Algorithm",
description: "If the Theme's Color should be extracted from Albumart", description: `
defaultValue: true, [Algorithm of selecting colors from the albumart]
onChange: (val) => updateColors(), - **Colorthief [(see)](https://lokeshdhakar.com/projects/color-thief/):** Gets more fitting colors
showChildren: (val) => !val, - **Vibrant [(see)](https://jariz.github.io/vibrant.js/):** Gets more vibrant colors *(was the default up to v3.1.1)*
- **Static:** Select a static color to be used
{.muted}
`,
data: { colorthief: "Colorthief", vibrant: "Vibrant", static: "Static" },
defaultValue: "colorthief",
onChange: () => updateColors(),
showChildren: (val) => {
if (val == "static") return ["colorOverride"];
return ["colorSelectionMode"];
},
children: [ children: [
{ {
type: "color", type: "color",
@ -554,7 +577,53 @@ Dribbblish.config.register({
description: "The Color of the Theme", description: "The Color of the Theme",
defaultValue: "#1ed760", defaultValue: "#1ed760",
fireInitialChange: false, fireInitialChange: false,
onChange: (val) => updateColors() onChange: () => updateColors()
},
{
area: "Theme",
type: "select",
key: "colorSelectionMode",
name: "Color Selection Mode",
description: `
Method of selecting colors from the albumart
- **Default:** Choose closest matching{.muted}
- **Luminance:** Choose matching current theme (lighter/darker){.muted}
`,
data: { default: "Default", luminance: "Luminance" },
defaultValue: "default",
onChange: () => updateColors(),
showChildren: (val) => {
if (val == "dynamicLuminance") return ["lightModeLuminance", "darkModeLuminance"];
return false;
},
children: [
{
type: "number",
key: "lightModeLuminance",
name: "Desired Light Mode Luminance",
description: `
Set desired luminance in light mode.
*the selected color will be the one who's luminance is closest to the desired luminance*{.muted}
`,
defaultValue: 0.6,
data: { min: 0, max: 1, step: 0.05 },
fireInitialChange: false,
onChange: () => updateColors()
},
{
type: "number",
key: "darkModeLuminance",
name: "Desired Dark Mode Luminance",
description: `
Set desired luminance in dark mode.
*the selected color will be the one who's luminance is closest to the desired luminance*{.muted}
`,
defaultValue: 0.2,
data: { min: 0, max: 1, step: 0.05 },
fireInitialChange: false,
onChange: () => updateColors()
}
]
} }
] ]
}); });
@ -562,7 +631,7 @@ Dribbblish.config.register({
Dribbblish.config.register({ Dribbblish.config.register({
area: "Theme", area: "Theme",
type: "select", type: "select",
data: { dark: "Dark", light: "Light", time: "Based on Time", color: "Based on Color" }, data: { dark: "Dark", light: "Light", time: "Based on Time" },
key: "theme", key: "theme",
name: "Theme", name: "Theme",
description: "Select Dark / Bright mode", description: "Select Dark / Bright mode",
@ -582,9 +651,6 @@ Dribbblish.config.register({
case "time": case "time":
checkDarkLightMode(); checkDarkLightMode();
break; break;
case "color":
checkDarkLightMode();
break;
} }
}, },
children: [ children: [
@ -609,27 +675,16 @@ Dribbblish.config.register({
] ]
}); });
var currentColor; function updateColors(checkDarkMode = true, sideColHex) {
var currentSideColor; if (sideColHex == undefined) return registerCoverListener();
function updateColors(textColHex, sideColHex, checkDarkMode = true) {
if (textColHex && sideColHex) {
currentColor = textColHex;
currentSideColor = sideColHex;
} else {
if (!(currentColor && currentSideColor)) return; // If `updateColors()` is called early these vars are undefined and would break
textColHex = currentColor;
sideColHex = currentSideColor;
}
if (!Dribbblish.config.get("dynamicColors")) {
const col = Dribbblish.config.get("colorOverride");
textColHex = col;
sideColHex = col;
}
let isLightBg = isLight(textColorBg); let isLightBg = isLight(textColorBg);
if (isLightBg) textColHex = chroma(textColHex).darken(0.15).hex(); // vibrant color is always too bright for white bg mode let textColHex = sideColHex;
if (isLightBg && chroma(textColHex).luminance() > 0.2) {
textColHex = chroma(textColHex).luminance(0.2).hex();
} else if (!isLightBg && chroma(textColHex).luminance() < 0.1) {
textColHex = chroma(textColHex).luminance(0.1).hex();
}
let darkColHex = chroma(textColHex) let darkColHex = chroma(textColHex)
.brighten(isLightBg ? 0.12 : -0.2) .brighten(isLightBg ? 0.12 : -0.2)
@ -675,8 +730,6 @@ async function songchange() {
let bgImage = Spicetify.Player.data.track.metadata.image_url; let bgImage = Spicetify.Player.data.track.metadata.image_url;
if (bgImage === undefined) { if (bgImage === undefined) {
bgImage = "/images/tracklist-row-song-fallback.svg"; bgImage = "/images/tracklist-row-song-fallback.svg";
textColor = "#509bf5";
updateColors(textColor, textColor);
} }
if (album_uri !== undefined && !album_uri.includes("spotify:show")) { if (album_uri !== undefined && !album_uri.includes("spotify:show")) {
@ -719,25 +772,41 @@ async function pickCoverColor(img) {
$("html").css("--image-brightness", getImageLightness(img) / 255); $("html").css("--image-brightness", getImageLightness(img) / 255);
var swatches = await new Promise((resolve, reject) => new Vibrant(img, 5).getPalette().then(resolve).catch(reject)); let color = "#509bf5";
var lightCols = ["Vibrant", "DarkVibrant", "Muted", "LightVibrant"]; if (img.complete) {
var darkCols = ["Vibrant", "LightVibrant", "Muted", "DarkVibrant"]; const colorSelectionAlgorithm = Dribbblish.config.get("colorSelectionAlgorithm");
const colorSelectionMode = Dribbblish.config.get("colorSelectionMode");
let palette = {};
var mainCols = isLight(textColorBg) ? lightCols : darkCols; if (colorSelectionAlgorithm == "colorthief") {
textColor = "#509bf5"; palette = Object.fromEntries([colorThief.getColor(img), ...colorThief.getPalette(img, 24, 5)].map((c) => chroma(c)).map((c) => [c.luminance(), c]));
for (var col in mainCols) } else if (colorSelectionAlgorithm == "vibrant") {
if (swatches[mainCols[col]]) { const swatches = await new Promise((resolve, reject) => new Vibrant(img, 5).getPalette().then(resolve).catch(reject));
textColor = swatches[mainCols[col]].getHex(); for (var col of ["Vibrant", "DarkVibrant", "Muted", "LightVibrant"]) {
break; if (swatches[col]) {
const c = chroma(swatches[col].getHex());
palette[c.luminance()] = c;
}
}
} else if (colorSelectionAlgorithm == "static") {
palette[1] = chroma(Dribbblish.config.get("colorOverride"));
} }
sidebarColor = "#509bf5"; if (colorSelectionMode == "default") {
for (var col in lightCols) color = Object.values(palette)[0];
if (swatches[lightCols[col]]) { for (const col of Object.values(palette)) {
sidebarColor = swatches[lightCols[col]].getHex(); if (col.luminance() > 0.05 && col.luminance() < 0.9) {
break; color = col.hex();
break;
}
}
} else if (colorSelectionMode == "luminance") {
const wantedLuminance = $("html").css("--is_light") == "1" ? Dribbblish.config.get("lightModeLuminance") : Dribbblish.config.get("darkModeLuminance");
color = palette[getClosestToNum(Object.keys(palette), wantedLuminance)].hex();
} }
updateColors(textColor, sidebarColor); }
updateColors(false, color);
} }
var coverListener; var coverListener;

View file

@ -6,6 +6,6 @@
} }
.muted { .muted {
opacity: 0.5; color: spiceColor("subtext", 0.5);
} }
} }