BIOME Formating

This commit is contained in:
2025-09-09 17:59:47 +10:00
parent 8178699d81
commit 99661096d5
28 changed files with 3781 additions and 3054 deletions
+9 -10
View File
@@ -1,12 +1,11 @@
{
"name": "@meowarex/colorama-lyrics",
"description": "Customize lyrics colors: single, gradient & auto from cover art",
"author": {
"name": "meowarex",
"url": "https://github.com/meowarex",
"avatarUrl": "https://avatars.githubusercontent.com/u/90243579"
},
"main": "./src/index.ts",
"type": "module"
"name": "@meowarex/colorama-lyrics",
"description": "Customize lyrics colors: single, gradient & auto from cover art",
"author": {
"name": "meowarex",
"url": "https://github.com/meowarex",
"avatarUrl": "https://avatars.githubusercontent.com/u/90243579"
},
"main": "./src/index.ts",
"type": "module"
}
File diff suppressed because it is too large Load Diff
+178 -164
View File
@@ -13,181 +13,195 @@ new StyleTag("ColoramaLyrics", unloads, styles);
// Simple dominant color extraction from current cover art
async function getCoverArtElement(): Promise<HTMLImageElement | null> {
const img = document.querySelector('figure[class*="_albumImage"] > div > div > div > img') as HTMLImageElement | null;
if (img) return img;
const video = document.querySelector('figure[class*="_albumImage"] > div > div > div > video') as HTMLVideoElement | null;
if (video) {
const poster = video.getAttribute("poster");
if (!poster) return null;
const tempImg = new Image();
tempImg.crossOrigin = "anonymous";
tempImg.src = poster;
await new Promise<void>((resolve) => {
tempImg.onload = () => resolve();
tempImg.onerror = () => resolve();
});
return tempImg as unknown as HTMLImageElement;
}
return null;
const img = document.querySelector(
'figure[class*="_albumImage"] > div > div > div > img',
) as HTMLImageElement | null;
if (img) return img;
const video = document.querySelector(
'figure[class*="_albumImage"] > div > div > div > video',
) as HTMLVideoElement | null;
if (video) {
const poster = video.getAttribute("poster");
if (!poster) return null;
const tempImg = new Image();
tempImg.crossOrigin = "anonymous";
tempImg.src = poster;
await new Promise<void>((resolve) => {
tempImg.onload = () => resolve();
tempImg.onerror = () => resolve();
});
return tempImg as unknown as HTMLImageElement;
}
return null;
}
function getDominantColorsFromImage(img: HTMLImageElement, count: number = 2): string[] {
try {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) return ["#ffffff", "#88aaff"]; // fallback
const w = 64;
const h = 64;
canvas.width = w;
canvas.height = h;
ctx.drawImage(img, 0, 0, w, h);
const data = ctx.getImageData(0, 0, w, h).data;
function getDominantColorsFromImage(
img: HTMLImageElement,
count: number = 2,
): string[] {
try {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
if (!ctx) return ["#ffffff", "#88aaff"]; // fallback
const w = 64;
const h = 64;
canvas.width = w;
canvas.height = h;
ctx.drawImage(img, 0, 0, w, h);
const data = ctx.getImageData(0, 0, w, h).data;
// Simple k-means-ish binning into 16 buckets per channel
const buckets = new Map<string, number>();
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const key = `${Math.round(r/16)},${Math.round(g/16)},${Math.round(b/16)}`;
buckets.set(key, (buckets.get(key) ?? 0) + 1);
}
const sorted = [...buckets.entries()].sort((a, b) => b[1] - a[1]);
const picked = sorted.slice(0, Math.max(1, count)).map(([key]) => {
const [r, g, b] = key.split(',').map(v => parseInt(v, 10) * 16);
return `#${[r, g, b].map(v => Math.max(0, Math.min(255, v)).toString(16).padStart(2, '0')).join('')}`;
});
return picked;
} catch {
return ["#ffffff", "#88aaff"]; // fallback
}
// Simple k-means-ish binning into 16 buckets per channel
const buckets = new Map<string, number>();
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const key = `${Math.round(r / 16)},${Math.round(g / 16)},${Math.round(b / 16)}`;
buckets.set(key, (buckets.get(key) ?? 0) + 1);
}
const sorted = [...buckets.entries()].sort((a, b) => b[1] - a[1]);
const picked = sorted.slice(0, Math.max(1, count)).map(([key]) => {
const [r, g, b] = key.split(",").map((v) => parseInt(v, 10) * 16);
return `#${[r, g, b].map((v) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0")).join("")}`;
});
return picked;
} catch {
return ["#ffffff", "#88aaff"]; // fallback
}
}
// build rgba() from hex + alpha percentage
function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
let v = hex.trim();
if (!v.startsWith('#')) v = `#${v}`;
if (/^#([0-9a-fA-F]{3})$/.test(v)) {
const r = parseInt(v[1] + v[1], 16);
const g = parseInt(v[2] + v[2], 16);
const b = parseInt(v[3] + v[3], 16);
return { r, g, b };
}
if (/^#([0-9a-fA-F]{6})$/.test(v)) {
const r = parseInt(v.slice(1, 3), 16);
const g = parseInt(v.slice(3, 5), 16);
const b = parseInt(v.slice(5, 7), 16);
return { r, g, b };
}
// 8-digit hex expects #AARRGGBB. Indices 1-3 are the alpha byte (ignored here),
// so r/g/b are extracted from v.slice(3,5), v.slice(5,7), v.slice(7,9) respectively.
if (/^#([0-9a-fA-F]{8})$/.test(v)) {
const r = parseInt(v.slice(3, 5), 16);
const g = parseInt(v.slice(5, 7), 16);
const b = parseInt(v.slice(7, 9), 16);
return { r, g, b };
}
return null;
let v = hex.trim();
if (!v.startsWith("#")) v = `#${v}`;
if (/^#([0-9a-fA-F]{3})$/.test(v)) {
const r = parseInt(v[1] + v[1], 16);
const g = parseInt(v[2] + v[2], 16);
const b = parseInt(v[3] + v[3], 16);
return { r, g, b };
}
if (/^#([0-9a-fA-F]{6})$/.test(v)) {
const r = parseInt(v.slice(1, 3), 16);
const g = parseInt(v.slice(3, 5), 16);
const b = parseInt(v.slice(5, 7), 16);
return { r, g, b };
}
// 8-digit hex expects #AARRGGBB. Indices 1-3 are the alpha byte (ignored here),
// so r/g/b are extracted from v.slice(3,5), v.slice(5,7), v.slice(7,9) respectively.
if (/^#([0-9a-fA-F]{8})$/.test(v)) {
const r = parseInt(v.slice(3, 5), 16);
const g = parseInt(v.slice(5, 7), 16);
const b = parseInt(v.slice(7, 9), 16);
return { r, g, b };
}
return null;
}
function rgbaFromHexAndAlpha(hex: string, alphaPercent: number | undefined): string {
const rgb = hexToRgb(hex);
const a = Math.max(0.05, Math.min(100, alphaPercent ?? 100)) / 100;
if (!rgb) return `rgba(255,255,255,${a})`;
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${a})`;
function rgbaFromHexAndAlpha(
hex: string,
alphaPercent: number | undefined,
): string {
const rgb = hexToRgb(hex);
const a = Math.max(0.05, Math.min(100, alphaPercent ?? 100)) / 100;
if (!rgb) return `rgba(255,255,255,${a})`;
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${a})`;
}
function applySingleColor(color: string) {
const alpha = (settings as any).singleAlpha ?? 100;
const rgba = rgbaFromHexAndAlpha(color, alpha);
document.documentElement.style.setProperty('--cl-lyrics-color', rgba);
document.documentElement.style.setProperty('--cl-glow1', rgba);
document.documentElement.style.setProperty('--cl-glow2', rgba);
document.documentElement.style.removeProperty('--cl-grad-start');
document.documentElement.style.removeProperty('--cl-grad-end');
document.documentElement.style.removeProperty('--cl-grad-angle');
document.body.classList.remove('colorama-gradient');
document.body.classList.add('colorama-single');
const alpha = (settings as any).singleAlpha ?? 100;
const rgba = rgbaFromHexAndAlpha(color, alpha);
document.documentElement.style.setProperty("--cl-lyrics-color", rgba);
document.documentElement.style.setProperty("--cl-glow1", rgba);
document.documentElement.style.setProperty("--cl-glow2", rgba);
document.documentElement.style.removeProperty("--cl-grad-start");
document.documentElement.style.removeProperty("--cl-grad-end");
document.documentElement.style.removeProperty("--cl-grad-angle");
document.body.classList.remove("colorama-gradient");
document.body.classList.add("colorama-single");
}
function applyGradient(start: string, end: string, angle: number) {
const startAlpha = (settings as any).gradientStartAlpha ?? 100;
const endAlpha = (settings as any).gradientEndAlpha ?? 100;
const startRgba = rgbaFromHexAndAlpha(start, startAlpha);
const endRgba = rgbaFromHexAndAlpha(end, endAlpha);
document.documentElement.style.setProperty('--cl-grad-start', startRgba);
document.documentElement.style.setProperty('--cl-grad-end', endRgba);
document.documentElement.style.setProperty('--cl-grad-angle', `${angle}deg`);
document.documentElement.style.setProperty('--cl-glow1', startRgba);
document.documentElement.style.setProperty('--cl-glow2', endRgba);
document.body.classList.remove('colorama-single');
document.body.classList.add('colorama-gradient');
const startAlpha = (settings as any).gradientStartAlpha ?? 100;
const endAlpha = (settings as any).gradientEndAlpha ?? 100;
const startRgba = rgbaFromHexAndAlpha(start, startAlpha);
const endRgba = rgbaFromHexAndAlpha(end, endAlpha);
document.documentElement.style.setProperty("--cl-grad-start", startRgba);
document.documentElement.style.setProperty("--cl-grad-end", endRgba);
document.documentElement.style.setProperty("--cl-grad-angle", `${angle}deg`);
document.documentElement.style.setProperty("--cl-glow1", startRgba);
document.documentElement.style.setProperty("--cl-glow2", endRgba);
document.body.classList.remove("colorama-single");
document.body.classList.add("colorama-gradient");
}
function resetModeClasses(): void {
document.body.classList.remove('colorama-single', 'colorama-gradient');
document.body.classList.remove("colorama-single", "colorama-gradient");
}
async function applyCoverColors(gradient: boolean) {
const img = await getCoverArtElement();
if (!img) return;
const colors = getDominantColorsFromImage(img, gradient ? 2 : 1);
if (gradient) {
const start = colors[0] ?? settings.gradientStart;
const end = colors[1] ?? settings.gradientEnd;
applyGradient(start, end, settings.gradientAngle);
} else {
const color = colors[0] ?? settings.singleColor;
applySingleColor(color);
}
const img = await getCoverArtElement();
if (!img) return;
const colors = getDominantColorsFromImage(img, gradient ? 2 : 1);
if (gradient) {
const start = colors[0] ?? settings.gradientStart;
const end = colors[1] ?? settings.gradientEnd;
applyGradient(start, end, settings.gradientAngle);
} else {
const color = colors[0] ?? settings.singleColor;
applySingleColor(color);
}
}
function applyColoramaLyrics(): void {
if (!settings.enabled) {
document.body.classList.remove('colorama-single', 'colorama-gradient');
return;
}
if (!settings.enabled) {
document.body.classList.remove("colorama-single", "colorama-gradient");
return;
}
// Toggle only-active-line mode class
if (settings.excludeInactive) {
document.body.classList.add('colorama-only-active');
} else {
document.body.classList.remove('colorama-only-active');
}
resetModeClasses();
switch (settings.mode) {
case "single":
applySingleColor(settings.singleColor);
break;
case "gradient-experimental":
applyGradient(settings.gradientStart, settings.gradientEnd, settings.gradientAngle);
break;
case "cover":
applyCoverColors(false);
break;
case "cover-gradient":
applyCoverColors(true);
break;
}
// Toggle only-active-line mode class
if (settings.excludeInactive) {
document.body.classList.add("colorama-only-active");
} else {
document.body.classList.remove("colorama-only-active");
}
resetModeClasses();
switch (settings.mode) {
case "single":
applySingleColor(settings.singleColor);
break;
case "gradient-experimental":
applyGradient(
settings.gradientStart,
settings.gradientEnd,
settings.gradientAngle,
);
break;
case "cover":
applyCoverColors(false);
break;
case "cover-gradient":
applyCoverColors(true);
break;
}
}
(window as any).applyColoramaLyrics = applyColoramaLyrics;
// Re-apply on track changes (for auto modes)
function observeTrackChanges(): void {
let lastTrackId: string | null = null;
const check = () => {
const currentTrackId = PlayState.playbackContext?.actualProductId;
if (currentTrackId && currentTrackId !== lastTrackId) {
lastTrackId = currentTrackId;
if (settings.mode === 'cover' || settings.mode === 'cover-gradient') {
setTimeout(() => applyColoramaLyrics(), 200);
}
}
};
const interval = setInterval(check, 500);
unloads.add(() => clearInterval(interval));
check();
let lastTrackId: string | null = null;
const check = () => {
const currentTrackId = PlayState.playbackContext?.actualProductId;
if (currentTrackId && currentTrackId !== lastTrackId) {
lastTrackId = currentTrackId;
if (settings.mode === "cover" || settings.mode === "cover-gradient") {
setTimeout(() => applyColoramaLyrics(), 200);
}
}
};
const interval = setInterval(check, 500);
unloads.add(() => clearInterval(interval));
check();
}
// Initial apply and observers
@@ -196,26 +210,26 @@ observeTrackChanges();
// for some reason, re-apply after Radiant updates its styles/backgrounds
function hookRadiantUpdates(): void {
const w = window as any;
const wrap = (name: string) => {
const fn = w[name];
if (typeof fn === 'function' && !fn.__coloramaPatched) {
const orig = fn.bind(w);
const patched = (...args: unknown[]) => {
const result = orig(...args);
try { applyColoramaLyrics(); } catch {}
return result;
};
(patched as any).__coloramaPatched = true;
w[name] = patched;
}
};
wrap('updateRadiantLyricsStyles');
wrap('updateRadiantLyricsNowPlayingBackground');
wrap('updateRadiantLyricsGlobalBackground');
wrap('updateRadiantLyricsTextGlow');
const w = window as any;
const wrap = (name: string) => {
const fn = w[name];
if (typeof fn === "function" && !fn.__coloramaPatched) {
const orig = fn.bind(w);
const patched = (...args: unknown[]) => {
const result = orig(...args);
try {
applyColoramaLyrics();
} catch {}
return result;
};
(patched as any).__coloramaPatched = true;
w[name] = patched;
}
};
wrap("updateRadiantLyricsStyles");
wrap("updateRadiantLyricsNowPlayingBackground");
wrap("updateRadiantLyricsGlobalBackground");
wrap("updateRadiantLyricsTextGlow");
}
setTimeout(() => hookRadiantUpdates(), 0);
+95 -51
View File
@@ -1,94 +1,138 @@
/* Variables used by Colorama Lyrics */
:root {
--cl-lyrics-color: #ffffff;
--cl-grad-start: #ffffff;
--cl-grad-end: #88aaff;
--cl-grad-angle: 0deg;
--cl-glow1: #ffffff;
--cl-glow2: #ffffff;
--cl-lyrics-color: #ffffff;
--cl-grad-start: #ffffff;
--cl-grad-end: #88aaff;
--cl-grad-angle: 0deg;
--cl-glow1: #ffffff;
--cl-glow2: #ffffff;
}
/* Apply solid color to lyrics text */
.colorama-single [class*="_lyricsText"] > div > span,
.colorama-single [class*="_lyricsText"] > div > span[data-current="true"],
.colorama-single [class^="_lyricsContainer"] > div > div > span,
.colorama-single [class^="_lyricsContainer"] > div > div > span[data-current="true"] {
color: var(--cl-lyrics-color) !important;
background: none !important;
-webkit-background-clip: initial !important;
background-clip: initial !important;
-webkit-text-fill-color: initial !important;
.colorama-single
[class^="_lyricsContainer"]
> div
> div
> span[data-current="true"] {
color: var(--cl-lyrics-color) !important;
background: none !important;
-webkit-background-clip: initial !important;
background-clip: initial !important;
-webkit-text-fill-color: initial !important;
}
/* Apply gradient to lyrics text */
.colorama-gradient [class*="_lyricsText"] > div > span,
.colorama-gradient [class*="_lyricsText"] > div > span[data-current="true"],
.colorama-gradient [class^="_lyricsContainer"] > div > div > span,
.colorama-gradient [class^="_lyricsContainer"] > div > div > span[data-current="true"] {
background: linear-gradient(var(--cl-grad-angle), var(--cl-grad-start), var(--cl-grad-end)) !important;
-webkit-background-clip: text !important;
background-clip: text !important;
color: transparent !important;
-webkit-text-fill-color: transparent !important;
.colorama-gradient
[class^="_lyricsContainer"]
> div
> div
> span[data-current="true"] {
background: linear-gradient(
var(--cl-grad-angle),
var(--cl-grad-start),
var(--cl-grad-end)
) !important;
-webkit-background-clip: text !important;
background-clip: text !important;
color: transparent !important;
-webkit-text-fill-color: transparent !important;
}
/* Only-active: apply container class only on the active line via JS */
/* Slight emphasis on current line (uniform to single mode) */
.colorama-gradient [class*="_lyricsText"] > div > span[data-current="true"],
.colorama-gradient [class^="_lyricsContainer"] > div > div > span[data-current="true"] {
filter: brightness(1.1) !important;
.colorama-gradient
[class^="_lyricsContainer"]
> div
> div
> span[data-current="true"] {
filter: brightness(1.1) !important;
}
/* Keep song title color unchanged; its glow is controlled in Radiant CSS */
/* Color Radiant glow shadows using Colorama colors (respect RL sizes) */
.colorama-single [class*="_lyricsText"] > div > span[data-current="true"],
.colorama-single [class^="_lyricsContainer"] > div > div > span[data-current="true"],
.colorama-single
[class^="_lyricsContainer"]
> div
> div
> span[data-current="true"],
.colorama-gradient [class*="_lyricsText"] > div > span[data-current="true"],
.colorama-gradient [class^="_lyricsContainer"] > div > div > span[data-current="true"],
.colorama-gradient [class^="_lyricsContainer"] > div > div > span[data-current="true"] {
text-shadow: 0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #ffffff), 0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #ffffff) !important;
.colorama-gradient
[class^="_lyricsContainer"]
> div
> div
> span[data-current="true"],
.colorama-gradient
[class^="_lyricsContainer"]
> div
> div
> span[data-current="true"] {
text-shadow:
0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #ffffff),
0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #ffffff) !important;
}
/* Hover: force glow color to match Colorama settings for inactive lines */
.colorama-single [class*="_lyricsText"] > div > span:hover,
.colorama-single [class^="_lyricsContainer"] > div > div > span:hover {
color: var(--cl-lyrics-color) !important;
text-shadow: 0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #ffffff), 0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #ffffff) !important;
color: var(--cl-lyrics-color) !important;
text-shadow:
0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #ffffff),
0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #ffffff) !important;
}
.colorama-gradient [class*="_lyricsText"] > div > span:hover,
.colorama-gradient [class^="_lyricsContainer"] > div > div > span:hover {
background: linear-gradient(var(--cl-grad-angle), var(--cl-grad-start), var(--cl-grad-end)) !important;
-webkit-background-clip: text !important;
background-clip: text !important;
color: transparent !important;
-webkit-text-fill-color: transparent !important;
/* Do not increase glow strength on hover for gradients */
background: linear-gradient(
var(--cl-grad-angle),
var(--cl-grad-start),
var(--cl-grad-end)
) !important;
-webkit-background-clip: text !important;
background-clip: text !important;
color: transparent !important;
-webkit-text-fill-color: transparent !important;
/* Do not increase glow strength on hover for gradients */
}
/* Only color active line mode */
body.colorama-only-active.colorama-single [class*="_lyricsText"] > div > span:not([data-current="true"]),
body.colorama-only-active.colorama-gradient [class*="_lyricsText"] > div > span:not([data-current="true"]) {
/* Match Radiant inactive styling */
color: rgba(128, 128, 128, 0.4) !important;
background: none !important;
-webkit-background-clip: initial !important;
background-clip: initial !important;
-webkit-text-fill-color: initial !important;
text-shadow: initial !important;
body.colorama-only-active.colorama-single [class*="_lyricsText"]
> div
> span:not([data-current="true"]),
body.colorama-only-active.colorama-gradient
[class*="_lyricsText"]
> div
> span:not([data-current="true"]) {
/* Match Radiant inactive styling */
color: rgba(128, 128, 128, 0.4) !important;
background: none !important;
-webkit-background-clip: initial !important;
background-clip: initial !important;
-webkit-text-fill-color: initial !important;
text-shadow: initial !important;
}
/* In only-active mode, keep TIDAL defaults even on hover for inactive lines */
body.colorama-only-active.colorama-single [class*="_lyricsText"] > div > span:not([data-current="true"]):hover,
body.colorama-only-active.colorama-gradient [class*="_lyricsText"] > div > span:not([data-current="true"]):hover {
color: lightgray !important;
background: none !important;
-webkit-background-clip: initial !important;
background-clip: initial !important;
-webkit-text-fill-color: initial !important;
text-shadow: initial !important;
body.colorama-only-active.colorama-single [class*="_lyricsText"]
> div
> span:not([data-current="true"]):hover,
body.colorama-only-active.colorama-gradient
[class*="_lyricsText"]
> div
> span:not([data-current="true"]):hover {
color: lightgray !important;
background: none !important;
-webkit-background-clip: initial !important;
background-clip: initial !important;
-webkit-text-fill-color: initial !important;
text-shadow: initial !important;
}