702d080a1e
Google BK
248 lines
7.4 KiB
JavaScript
248 lines
7.4 KiB
JavaScript
const themeStyleContainer = document.getElementById("theme");
|
|
|
|
class Color {
|
|
constructor(r = 0, g = 0, b = 0, a = 1) {
|
|
this.r = this._clamp(r);
|
|
this.g = this._clamp(g);
|
|
this.b = this._clamp(b);
|
|
this.a = this._clamp(a, 0, 1);
|
|
}
|
|
|
|
static black() {
|
|
return new Color(0, 0, 0);
|
|
}
|
|
|
|
static white() {
|
|
return new Color(255, 255, 255);
|
|
}
|
|
|
|
static grey() {
|
|
return new Color(128, 128, 128);
|
|
}
|
|
|
|
static defaultAccent() {
|
|
return new Color(3, 169, 260);
|
|
}
|
|
|
|
static defaultBackgroundLight() {
|
|
return Color.white();
|
|
}
|
|
|
|
static defaultBackgroundDark() {
|
|
return new Color(26, 26, 26);
|
|
}
|
|
|
|
static defaultBackground() {
|
|
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
return Color.defaultBackgroundDark();
|
|
} else {
|
|
return Color.defaultBackgroundLight();
|
|
}
|
|
}
|
|
|
|
static hexToRgb(hex) {
|
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
return result
|
|
? {
|
|
r: parseInt(result[1], 16),
|
|
g: parseInt(result[2], 16),
|
|
b: parseInt(result[3], 16),
|
|
}
|
|
: null;
|
|
}
|
|
|
|
static parse(color) {
|
|
const rgb = this.hexToRgb(color);
|
|
return new Color(rgb.r, rgb.g, rgb.b);
|
|
}
|
|
|
|
tint(to, amount) {
|
|
amount = this._clamp(amount, 0, 1);
|
|
|
|
const r_new = this.r + (to.r - this.r) * amount;
|
|
const g_new = this.g + (to.g - this.g) * amount;
|
|
const b_new = this.b + (to.b - this.b) * amount;
|
|
const a_new = this.a + (to.a - this.a) * amount;
|
|
return new Color(r_new, g_new, b_new, a_new);
|
|
}
|
|
|
|
darken(amount) {
|
|
return this.tint(Color.black(), amount);
|
|
}
|
|
|
|
lighten(amount) {
|
|
return this.tint(Color.white(), amount);
|
|
}
|
|
|
|
shift(amount) {
|
|
return this.luminance() >= 0.5 ? this.darken(amount) : this.lighten(amount);
|
|
}
|
|
|
|
saturate(change) {
|
|
const Pr = 0.299;
|
|
const Pg = 0.587;
|
|
const Pb = 0.114;
|
|
const P = Math.sqrt(this.r * this.r * Pr + this.g * this.g * Pg + this.b * this.b * Pb);
|
|
|
|
const R = this._clamp(P + (this.r - P) * change);
|
|
const G = this._clamp(P + (this.g - P) * change);
|
|
const B = this._clamp(P + (this.b - P) * change);
|
|
return new Color(R, G, B, this.a);
|
|
}
|
|
|
|
_clamp(value, min = 0, max = 255) {
|
|
if (value > max) {
|
|
return max;
|
|
}
|
|
if (value < min) {
|
|
return min;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
toCss() {
|
|
return "rgba(" + Math.floor(this.r) + "," + Math.floor(this.g) + "," + Math.floor(this.b) + "," + this.a + ")";
|
|
}
|
|
|
|
toHex() {
|
|
const componentToHex = (c) => {
|
|
var hex = c.toString(16);
|
|
return hex.length == 1 ? "0" + hex : hex;
|
|
};
|
|
|
|
return (
|
|
"#" + componentToHex(Math.floor(this.r)) + componentToHex(Math.floor(this.g)) + componentToHex(Math.floor(this.b))
|
|
);
|
|
}
|
|
|
|
textColor() {
|
|
const luma = (0.299 * this.r + 0.587 * this.g + 0.114 * this.b) / 255;
|
|
return luma > 0.53 ? Color.black() : Color.white();
|
|
}
|
|
|
|
luminance() {
|
|
const rg = Math.floor(this.r) <= 10 ? this.r / 3294.0 : Math.pow(this.r / 269.0 + 0.0513, 2.4);
|
|
const gg = Math.floor(this.g) <= 10 ? this.g / 3294.0 : Math.pow(this.g / 269.0 + 0.0513, 2.4);
|
|
const bg = Math.floor(this.b) <= 10 ? this.b / 3294.0 : Math.pow(this.b / 269.0 + 0.0513, 2.4);
|
|
return 0.2126 * rg + 0.7152 * gg + 0.0722 * bg;
|
|
}
|
|
|
|
contrast(other) {
|
|
let big = this.luminance();
|
|
let small = other.luminance();
|
|
|
|
if (big < small) {
|
|
const temp = big;
|
|
big = small;
|
|
small = temp;
|
|
}
|
|
|
|
return (big + 0.05) / (small + 0.05);
|
|
}
|
|
|
|
withAlpha(alpha) {
|
|
return new Color(this.r, this.g, this.b, alpha);
|
|
}
|
|
|
|
// Return a color close to this but also legible against bg_color.
|
|
// This is used in some places to make the UI minimally useable even
|
|
// if the user picks stupid bg/accent colors.
|
|
makeLegible(bg_color) {
|
|
let ideal = bg_color.textColor();
|
|
let contrast = bg_color.contrast(this);
|
|
// This has a bunch of magic constants that were choosen experimentally.
|
|
// Don't fuck with them.
|
|
let contrast_threshold = 2.5;
|
|
if (contrast > contrast_threshold) {
|
|
return this;
|
|
} else {
|
|
const scale = 1 - (contrast - 1) / (contrast_threshold - 1);
|
|
return this.tint(ideal, scale * 0.5);
|
|
}
|
|
}
|
|
}
|
|
|
|
function setColors(background, accent) {
|
|
if (!background || (typeof(background) == 'string' && background.length == 0)) {
|
|
background = Color.defaultBackground();
|
|
} else if (typeof(background) == 'string') {
|
|
background = Color.parse(background);
|
|
}
|
|
|
|
if (!accent || (typeof(accent) == 'string' && accent.length == 0)) {
|
|
accent = Color.defaultAccent();
|
|
} else if (typeof(accent) == 'string') {
|
|
accent = Color.parse(accent);
|
|
}
|
|
|
|
themeStyleContainer.dataset.backgroundColor = background.toHex();
|
|
themeStyleContainer.dataset.accentColor = accent.toHex();
|
|
|
|
let text = background.textColor();
|
|
accent = accent.makeLegible(background);
|
|
let linkAccent = accent.makeLegible(background);
|
|
|
|
let drop_shadow = Color.black();
|
|
let bg_lum = background.luminance();
|
|
if (bg_lum < 0.01) {
|
|
drop_shadow = Color.white().tint(Color.black(), 0.9);
|
|
}
|
|
|
|
let icon_warn = Color.parse("#f57f17").makeLegible(background);
|
|
let input_label = Color.parse("#9e9e9e").makeLegible(background);
|
|
let danger_text = Color.parse("#FF0000").tint(text, 0.25).makeLegible(background);
|
|
let danger_bg = danger_text.textColor();
|
|
let blue_icon = Color.parse("#0D47A1").makeLegible(background);
|
|
|
|
const styleSheet = {
|
|
":root": {
|
|
// Cls colors
|
|
"--cls-color": text.toCss(),
|
|
"--cls-sec-color": text.toCss(),
|
|
"--cls-size": "2rem",
|
|
"--cls-margin": "1rem",
|
|
"--cls-speed": "4s",
|
|
// Texts & icons colors
|
|
"--helper-text-color": text.tint(background, 0.25).toCss(),
|
|
"--text-primary-color": text.shift(0.13).toCss(),
|
|
"--text-secondary-color": text.shift(0.26).toCss(),
|
|
"--divider-color": text.withAlpha(0.12).toCss(),
|
|
"--shadow-color": drop_shadow.withAlpha(0.14).toCss(),
|
|
"--icon-color": text.shift(0.13).withAlpha(0.6).toCss(),
|
|
"--icon-warn": icon_warn.toCss(),
|
|
"--input-label": input_label.toCss(),
|
|
"--danger-text": danger_text.toCss(),
|
|
"--danger-bg": danger_text.textColor().toCss(),
|
|
"--blue-icon": blue_icon.toCss(),
|
|
// Accent colors
|
|
"--accent-color": accent.toCss(),
|
|
"--accent-dark": accent.darken(0.3).toCss(),
|
|
"--accent-text-color": accent.textColor().toCss(),
|
|
"--accent-focus-color": accent.saturate(1.2).toCss(),
|
|
"--accent-hover-color": accent.textColor().toCss(),
|
|
"--accent-bg-hover-color": accent.toCss(),
|
|
"--accent-link-color": linkAccent.toCss(),
|
|
"--progress-bg": accent.tint(background, 0.75).toCss(),
|
|
"--accent-ripple": linkAccent.withAlpha(0.2).toCss(),
|
|
// Background colors
|
|
"--background-color": background.toCss(),
|
|
"--background-sidenav-color": background.shift(0.03).toCss(),
|
|
"--background-hover": background.shift(0.065),
|
|
"--background-modal-color": background.tint(text, 0.02).toCss(),
|
|
"--background-primary-color": background.shift(0.02).toCss(),
|
|
},
|
|
};
|
|
|
|
const properties = Object.keys(styleSheet).map((selector) => {
|
|
const selectorProperties = Object.keys(styleSheet[selector]).map(
|
|
(property) => ` ${property}: ${styleSheet[selector][property]};`,
|
|
);
|
|
|
|
return `${selector} {\n${selectorProperties.join("\n")}\n}`;
|
|
});
|
|
|
|
themeStyleContainer.innerHTML = properties.join("\n");
|
|
}
|
|
|
|
setColors(themeStyleContainer.dataset.backgroundColor, themeStyleContainer.dataset.accentColor);
|