/* ============================================================
theme.js — the "magic": pull a color from the food image and
morph the whole page to it. No external library.
============================================================ */
const Theme = (() => {
const root = document.documentElement;
/* Extract a pleasant dominant color from an using a tiny canvas. */
function extract(imgEl) {
const c = document.createElement('canvas');
const S = 48; // downscale -> fast
c.width = c.height = S;
const ctx = c.getContext('2d');
ctx.drawImage(imgEl, 0, 0, S, S);
const { data } = ctx.getImageData(0, 0, S, S);
let r = 0, g = 0, b = 0, n = 0;
let rs = 0, gs = 0, bs = 0, ns = 0; // saturated-only accumulator
for (let i = 0; i < data.length; i += 4) {
const R = data[i], G = data[i + 1], B = data[i + 2];
const max = Math.max(R, G, B), min = Math.min(R, G, B);
const sat = max - min;
// skip near-white / near-black / near-grey backgrounds
if (max < 40 || min > 225) continue;
r += R; g += G; b += B; n++;
if (sat > 40) { rs += R; gs += G; bs += B; ns++; }
}
// prefer the vibrant average; fall back to plain average; then green
if (ns > n * 0.06) return [rs / ns | 0, gs / ns | 0, bs / ns | 0];
if (n) return [r / n | 0, g / n | 0, b / n | 0];
return [91, 140, 62];
}
function luminance([r, g, b]) { return 0.299 * r + 0.587 * g + 0.114 * b; }
/* ── color space helpers (for building a 2-tone gradient) ─────────── */
function rgb2hsl(r, g, b) {
r /= 255; g /= 255; b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) { h = s = 0; }
else {
const 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;
}
h /= 6;
}
return [h * 360, s, l];
}
function hsl2rgb(h, s, l) {
h /= 360;
let r, g, b;
if (s === 0) { r = g = b = l; }
else {
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1; if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3); g = hue2rgb(p, q, h); b = hue2rgb(p, q, h - 1 / 3);
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
const css = ([r, g, b]) => `rgb(${r},${g},${b})`;
/* Apply a color to the CSS variables, deriving a richer 2-tone gradient. */
function apply(rgb) {
const [r, g, b] = rgb;
const [h, s, l] = rgb2hsl(r, g, b);
const accent2 = hsl2rgb((h + 16) % 360, Math.min(s * 1.05, 1), Math.min(l * 1.18 + 0.05, 0.74));
const accentD = hsl2rgb(h, Math.min(s * 1.08, 1), Math.max(l * 0.7, 0.14));
root.style.setProperty('--accent', css(rgb));
root.style.setProperty('--accent-2', css(accent2));
root.style.setProperty('--accent-d', css(accentD));
root.style.setProperty('--bg-tint', `rgba(${r},${g},${b},0.10)`);
root.style.setProperty('--on-accent', luminance(rgb) > 150 ? '#1c2418' : '#ffffff');
// NOTE: intentionally NOT persisted — the color is local to this page only.
// Navigating away / returning resets to the default theme.
}
/* From an image element (must be loaded + same-origin / blob). */
function fromImage(imgEl) {
try { apply(extract(imgEl)); } catch (e) { /* tainted/empty: keep default */ }
}
return { fromImage, apply, extract };
})();
/* Animate a number counting up to `target` inside an element. */
function countUp(el, target, { dur = 900, suffix = '' } = {}) {
const start = performance.now();
const from = 0;
function tick(now) {
const p = Math.min((now - start) / dur, 1);
const eased = 1 - Math.pow(1 - p, 3);
el.textContent = Math.round(from + (target - from) * eased) + suffix;
if (p < 1) requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
}
/* Lightweight page-visit loader. Uses transform/opacity so it stays smooth on Android. */
(function pageLoader() {
if (document.body.dataset.loader === 'off') return;
const loader = document.createElement('div');
loader.className = 'page-loader';
loader.innerHTML = `