92 lines
3.1 KiB
JavaScript
92 lines
3.1 KiB
JavaScript
|
|
'use strict';
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Contrast scheme
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
// Accent palette — ordered by preference. Algorithm picks the one with the
|
||
|
|
// highest WCAG contrast ratio against the detected background luminance.
|
||
|
|
const ACCENT_PALETTE = [
|
||
|
|
{ base: '#e63946', hover: '#ff4d58' }, // red
|
||
|
|
{ base: '#ff9500', hover: '#ffaa33' }, // orange
|
||
|
|
{ base: '#f1c40f', hover: '#f9d439' }, // yellow
|
||
|
|
{ base: '#2ecc71', hover: '#4ee88a' }, // green
|
||
|
|
{ base: '#00b4d8', hover: '#33c7e5' }, // cyan
|
||
|
|
{ base: '#4361ee', hover: '#6d84f4' }, // blue
|
||
|
|
{ base: '#c77dff', hover: '#d89fff' }, // purple
|
||
|
|
{ base: '#ff6b9d', hover: '#ff8fb5' }, // pink
|
||
|
|
{ base: '#ffffff', hover: '#cccccc' }, // white (last resort)
|
||
|
|
];
|
||
|
|
|
||
|
|
function _linearise(c) {
|
||
|
|
c /= 255;
|
||
|
|
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
||
|
|
}
|
||
|
|
function _luminance(r, g, b) {
|
||
|
|
return 0.2126 * _linearise(r) + 0.7152 * _linearise(g) + 0.0722 * _linearise(b);
|
||
|
|
}
|
||
|
|
function _contrast(l1, l2) {
|
||
|
|
return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
|
||
|
|
}
|
||
|
|
function _hexRgb(hex) {
|
||
|
|
return [parseInt(hex.slice(1,3),16), parseInt(hex.slice(3,5),16), parseInt(hex.slice(5,7),16)];
|
||
|
|
}
|
||
|
|
|
||
|
|
function analyzeBackground(url) {
|
||
|
|
return new Promise(resolve => {
|
||
|
|
const img = new Image();
|
||
|
|
img.crossOrigin = 'anonymous';
|
||
|
|
img.onload = () => {
|
||
|
|
const canvas = document.createElement('canvas');
|
||
|
|
canvas.width = 64; canvas.height = 64;
|
||
|
|
const ctx = canvas.getContext('2d');
|
||
|
|
ctx.drawImage(img, 0, 0, 64, 64);
|
||
|
|
const data = ctx.getImageData(0, 0, 64, 64).data;
|
||
|
|
let tR = 0, tG = 0, tB = 0, tBt601 = 0;
|
||
|
|
const n = data.length / 4;
|
||
|
|
for (let i = 0; i < data.length; i += 4) {
|
||
|
|
tR += data[i]; tG += data[i+1]; tB += data[i+2];
|
||
|
|
tBt601 += 0.299*data[i] + 0.587*data[i+1] + 0.114*data[i+2];
|
||
|
|
}
|
||
|
|
resolve({
|
||
|
|
bright: (tBt601 / n) > 127,
|
||
|
|
bgLuminance: _luminance(tR/n, tG/n, tB/n),
|
||
|
|
});
|
||
|
|
};
|
||
|
|
img.onerror = () => resolve({ bright: false, bgLuminance: 0 });
|
||
|
|
img.src = url;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function pickBestAccent(bgLuminance) {
|
||
|
|
let best = ACCENT_PALETTE[0], bestRatio = 0;
|
||
|
|
for (const entry of ACCENT_PALETTE) {
|
||
|
|
const [r, g, b] = _hexRgb(entry.base);
|
||
|
|
const ratio = _contrast(bgLuminance, _luminance(r, g, b));
|
||
|
|
if (ratio > bestRatio) { bestRatio = ratio; best = entry; }
|
||
|
|
}
|
||
|
|
return best;
|
||
|
|
}
|
||
|
|
|
||
|
|
function applyAccent(entry) {
|
||
|
|
const root = document.documentElement;
|
||
|
|
root.style.setProperty('--accent', entry.base);
|
||
|
|
root.style.setProperty('--accent-hover', entry.hover);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Light and dark: the switch is in base.html (every page has it). The reader's
|
||
|
|
// "Wie die App" follows it.
|
||
|
|
window.addEventListener('diora-theme', () => {
|
||
|
|
if (currentBookId) applyReaderSettings(!!currentPdfDoc);
|
||
|
|
});
|
||
|
|
|
||
|
|
function setScheme(bright) {
|
||
|
|
document.body.classList.toggle('bright-bg', bright);
|
||
|
|
const btn = document.getElementById('contrast-toggle');
|
||
|
|
if (btn) btn.style.opacity = bright ? '1' : '0.5';
|
||
|
|
}
|
||
|
|
|
||
|
|
function toggleContrast() {
|
||
|
|
setScheme(!document.body.classList.contains('bright-bg'));
|
||
|
|
}
|