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