Fix login theme layering, restyle login card, add photo rotation with dynamic accent
- theme.properties: keep loading the keycloak.v2 parent stylesheet (previously shadowed by our same-named css/styles.css, so the base card grid/layout never loaded); our overrides now live in resources/css/custom.css. - custom.css: rewritten against the real rendered markup/PatternFly tokens (#keycloak-bg, .pf-v5-c-login__main, #kc-login) instead of guessed selectors, with a frosted-glass card, dark-mode variant, and rounded inputs/buttons. - bg-picker.js: picks the background photo deterministically by epoch-day from an IMAGES list (add filenames as new photos are uploaded), and derives the card's accent color (top border + primary button) from the chosen photo via saturation-weighted dominant-hue extraction. - dev/: local static preview (real login markup snapshot + vendored Font Awesome glyph) to iterate on the theme without redeploying.
This commit is contained in:
112
assets/login/resources/js/bg-picker.js
Normal file
112
assets/login/resources/js/bg-picker.js
Normal file
@@ -0,0 +1,112 @@
|
||||
(function () {
|
||||
// Add new filenames here as they're uploaded to resources/img/.
|
||||
var IMAGES = ["1.jpg"];
|
||||
|
||||
var epochDay = Math.floor(Date.now() / 86400000);
|
||||
var chosen = IMAGES[epochDay % IMAGES.length];
|
||||
var url = new URL("../img/" + chosen, document.currentScript.src).href;
|
||||
|
||||
document.documentElement.style.setProperty("--login-bg-image", "url(" + JSON.stringify(url) + ")");
|
||||
|
||||
// Derive the login card's accent color (--keycloak-card-top-color, the top
|
||||
// border strip) from whichever background photo was picked above, instead
|
||||
// of hard-coding one color that only suits a single image.
|
||||
var img = new Image();
|
||||
img.crossOrigin = "anonymous";
|
||||
img.onload = function () {
|
||||
var accent = dominantAccentColor(img);
|
||||
if (accent) {
|
||||
document.documentElement.style.setProperty("--keycloak-card-top-color", accent);
|
||||
}
|
||||
};
|
||||
img.src = url;
|
||||
|
||||
function dominantAccentColor(img) {
|
||||
var canvas = document.createElement("canvas");
|
||||
var w = 100;
|
||||
var h = Math.max(1, Math.round((img.naturalHeight / img.naturalWidth) * w));
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
var ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(img, 0, 0, w, h);
|
||||
|
||||
var data;
|
||||
try {
|
||||
data = ctx.getImageData(0, 0, w, h).data;
|
||||
} catch (e) {
|
||||
return null; // canvas tainted (image not same-origin / no CORS headers)
|
||||
}
|
||||
|
||||
// Bucket pixels by hue. A large, pale, uniform area (sky) still adds up
|
||||
// to a bigger SUM of vividness than a smaller, richly-colored subject
|
||||
// (lavender rows), so summing alone still lets area win. Score each bin
|
||||
// by its MEAN vividness instead - that measures "how saturated is this
|
||||
// hue, typically" rather than "how much area does this hue cover" - with
|
||||
// a minimum-population floor so a handful of stray outlier pixels can't
|
||||
// win by having a freakishly high average.
|
||||
var HUE_BINS = 36; // 10 degrees each
|
||||
var binCount = new Array(HUE_BINS).fill(0);
|
||||
var binScore = new Array(HUE_BINS).fill(0);
|
||||
var binR = new Array(HUE_BINS).fill(0);
|
||||
var binG = new Array(HUE_BINS).fill(0);
|
||||
var binB = new Array(HUE_BINS).fill(0);
|
||||
var totalPixels = 0;
|
||||
|
||||
for (var i = 0; i < data.length; i += 4) {
|
||||
var r = data[i], g = data[i + 1], b = data[i + 2];
|
||||
totalPixels++;
|
||||
var hsl = rgbToHsl(r, g, b);
|
||||
if (hsl.s < 0.12) continue; // skip near-greys/whites/blacks
|
||||
|
||||
var lightnessWeight = 1 - Math.abs(hsl.l - 0.5) * 2;
|
||||
var score = hsl.s * lightnessWeight;
|
||||
if (score <= 0) continue;
|
||||
|
||||
var bin = Math.floor(hsl.h / (360 / HUE_BINS)) % HUE_BINS;
|
||||
binCount[bin]++;
|
||||
binScore[bin] += score;
|
||||
binR[bin] += r * score;
|
||||
binG[bin] += g * score;
|
||||
binB[bin] += b * score;
|
||||
}
|
||||
|
||||
var minPopulation = totalPixels * 0.02; // ignore hues covering <2% of the image
|
||||
var best = -1;
|
||||
var bestMean = 0;
|
||||
for (var j = 0; j < HUE_BINS; j++) {
|
||||
if (binCount[j] < minPopulation) continue;
|
||||
var mean = binScore[j] / binCount[j];
|
||||
if (mean > bestMean) {
|
||||
bestMean = mean;
|
||||
best = j;
|
||||
}
|
||||
}
|
||||
if (best === -1) return null;
|
||||
|
||||
var r2 = Math.round(binR[best] / binScore[best]);
|
||||
var g2 = Math.round(binG[best] / binScore[best]);
|
||||
var b2 = Math.round(binB[best] / binScore[best]);
|
||||
return "rgb(" + r2 + ", " + g2 + ", " + b2 + ")";
|
||||
}
|
||||
|
||||
function rgbToHsl(r, g, b) {
|
||||
r /= 255;
|
||||
g /= 255;
|
||||
b /= 255;
|
||||
var max = Math.max(r, g, b), min = Math.min(r, g, b);
|
||||
var h, s, l = (max + min) / 2;
|
||||
if (max === min) {
|
||||
h = s = 0;
|
||||
} else {
|
||||
var d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
switch (max) {
|
||||
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
|
||||
case g: h = (b - r) / d + 2; break;
|
||||
default: h = (r - g) / d + 4; break;
|
||||
}
|
||||
h *= 60;
|
||||
}
|
||||
return { h: h, s: s, l: l };
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user