From 036e2fe7ea9bb017004b03c078c671b872f56f35 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sun, 12 Jul 2026 20:40:20 +0200 Subject: [PATCH] feat(glass): dev-prototype feed backdrop (blurred rotating thumbnail) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Idea from UAT-2: give the feeds the same 'content behind glass' lift the Plex detail page gets. New FeedBackdrop paints a faint, slowly-rotating feed thumbnail as a fixed backdrop on
(the proven art-backdrop path — a fixed z:-1 layer is occluded by the root's opaque bg-bg). Blur = tiny-canvas downscale + cover upscale (no CSS filter; ytimg is CORS-readable so it uses a data-URL, else raw). Flat --bg stays as the 'perf mode' look. Behind the GlassTuner toggle + a Backdrop-fade slider (--feedbg-fade) so it's judged/tuned in UAT before anything is committed for real. --- frontend/src/components/FeedBackdrop.tsx | 93 ++++++++++++++++++++++++ frontend/src/components/GlassTuner.tsx | 40 +++++++++- frontend/src/index.css | 4 + 3 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/FeedBackdrop.tsx diff --git a/frontend/src/components/FeedBackdrop.tsx b/frontend/src/components/FeedBackdrop.tsx new file mode 100644 index 0000000..6ad7009 --- /dev/null +++ b/frontend/src/components/FeedBackdrop.tsx @@ -0,0 +1,93 @@ +import { useEffect, useState } from "react"; + +/** + * DEV prototype (toggled by GlassTuner): a faint, blurred, slowly-rotating feed thumbnail painted + * as a fixed backdrop so the glass surfaces have real content to refract — the same trick that makes + * the Plex detail page pop, brought to the YT/Plex feeds. The flat `--bg` then reads as "perf mode". + * + * It writes `
`'s background (NOT a separate fixed layer): a fixed z-index:-1 layer is occluded + * by the app root's opaque `bg-bg`, whereas `
` is a transparent descendant whose own background + * paints above it — this is exactly how `useArtBackdrop` (Plex) works, reused here. + * + * Blur: downscale the thumbnail into a tiny canvas and let `background-size: cover` upscale it back — + * a cheap gaussian-ish blur with no CSS filter (which can't touch a background-image). Falls back to + * the raw url if the image isn't CORS-readable (still soft once upscaled + heavily faded). + */ +export default function FeedBackdrop() { + const [srcs, setSrcs] = useState([]); + const [idx, setIdx] = useState(0); + const [art, setArt] = useState(null); + + // Sample the video thumbnails currently on screen. + useEffect(() => { + const found: string[] = []; + document.querySelectorAll("img").forEach((img) => { + const s = img.currentSrc || img.src; + if (/i\.ytimg\.com\/vi\//.test(s)) found.push(s); + }); + setSrcs([...new Set(found)]); + }, []); + + // Slowly cycle through them. + useEffect(() => { + if (srcs.length < 2) return; + const t = setInterval(() => setIdx((i) => (i + 1) % srcs.length), 14000); + return () => clearInterval(t); + }, [srcs]); + + // Blur the current pick via a tiny-canvas downscale (fallback: raw url on CORS taint). + useEffect(() => { + const url = srcs[idx]; + if (!url) return; + let cancelled = false; + const img = new Image(); + img.crossOrigin = "anonymous"; + img.onload = () => { + if (cancelled) return; + try { + const c = document.createElement("canvas"); + c.width = 40; + c.height = 22; + c.getContext("2d")!.drawImage(img, 0, 0, 40, 22); + setArt(c.toDataURL("image/jpeg", 0.6)); + } catch { + setArt(url); + } + }; + img.onerror = () => { + if (!cancelled) setArt(url); + }; + img.src = url; + return () => { + cancelled = true; + }; + }, [srcs, idx]); + + // Paint (and clean up) the faded backdrop on
. + useEffect(() => { + const main = document.querySelector("main"); + if (!main) return; + const s = main.style; + const clear = () => { + s.backgroundImage = ""; + s.backgroundSize = ""; + s.backgroundPosition = ""; + s.backgroundRepeat = ""; + s.backgroundAttachment = ""; + }; + if (art) { + s.backgroundImage = + `linear-gradient(color-mix(in srgb, var(--bg) var(--feedbg-fade), transparent), ` + + `color-mix(in srgb, var(--bg) var(--feedbg-fade), transparent)), url("${art}")`; + s.backgroundSize = "cover"; + s.backgroundPosition = "center"; + s.backgroundRepeat = "no-repeat"; + s.backgroundAttachment = "fixed"; + } else { + clear(); + } + return clear; + }, [art]); + + return null; +} diff --git a/frontend/src/components/GlassTuner.tsx b/frontend/src/components/GlassTuner.tsx index 5b899c5..f287cba 100644 --- a/frontend/src/components/GlassTuner.tsx +++ b/frontend/src/components/GlassTuner.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useState } from "react"; +import FeedBackdrop from "./FeedBackdrop"; /** * DEV-ONLY live appearance tuner. Mounted at the App root so it stays reachable on every page. @@ -77,13 +78,19 @@ const PRESETS: { id: string; name: string; hint: string; over: Record document.documentElement; const readVar = (k: string) => getComputedStyle(root()).getPropertyValue(k).trim(); type Saved = { vars?: Record; palette?: Record; + feedbg?: boolean; open?: boolean; }; @@ -103,6 +110,7 @@ export default function GlassTuner() { return { ...base, ...(initial.vars || {}) }; }); const [palette, setPalette] = useState>(() => initial.palette || {}); + const [feedbg, setFeedbg] = useState(() => Boolean(initial.feedbg)); const [open, setOpen] = useState(() => initial.open ?? true); const [tab, setTab] = useState<"glass" | "palette">("glass"); const [copied, setCopied] = useState(false); @@ -199,10 +207,17 @@ export default function GlassTuner() { persist({ open: v }); } + function toggleFeedbg(v: boolean) { + setFeedbg(v); + persist({ feedbg: v }); + } + if (!DEV_TUNER) return null; return ( -
+ <> + {feedbg && } +
{!open && (
+ {/* Feed backdrop (dev prototype) */} + + {/* Tab switch */}
{(["glass", "palette"] as const).map((t) => ( @@ -269,6 +295,13 @@ export default function GlassTuner() { {SLIDERS.map((s) => ( setVar(s.k, v)} /> ))} + {feedbg && ( +
+ {FEEDBG_SLIDERS.map((s) => ( + setVar(s.k, v)} /> + ))} +
+ )}
)} @@ -313,7 +346,8 @@ export default function GlassTuner() { )} - + + ); } diff --git a/frontend/src/index.css b/frontend/src/index.css index 2ed21a0..7471124 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -21,6 +21,10 @@ --glass-edge: 10%; /* extra edge-light mixed into the TOP border (#fff N%) — 0 = off */ --glass-scrim: 14%; /* under-content scrim: darkens surfaces toward --bg for text contrast — 0 = off */ --ambient: 1.1; /* multiplier on the ambient accent backdrop pools */ + + /* Dev-prototype feed backdrop (see ): how strongly the faded --bg overlay hides the + blurred thumbnail — higher = fainter image. Only in effect while the backdrop is toggled on. */ + --feedbg-fade: 86%; } /* ===== Rich-backdrop scope: surfaces that float over real imagery (Plex art hero/detail, and