diora-web/static/js/app/30-contrast.js

92 lines
3.1 KiB
JavaScript
Raw Normal View History

'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'));
}