From 769dc62b87f8cac1d68ac428d5bda70fcab5e019 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Mon, 15 Jun 2026 08:37:32 -0700 Subject: [PATCH] video: cooler person hero + perf pass on search/person pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Person hero glow-up: - Cinematic ambient — blurred portrait + an accent colour mesh + vignette, masked to fade into the page (was a flat wash). - Portrait gets a slowly-rotating accent gradient ring (masked donut, GPU transform) and a gentle float; an accent ring + glow frame. - An accent role tagline ('ACTOR' / 'DIRECTOR' …) above a gradient-filled name, plus a credits-count chip. Honors prefers-reduced-motion. Performance: - Long filmography grids use content-visibility:auto + contain-intrinsic-size so the browser skips off-screen cards (cheap scroll), and skip replaying the entrance animation as cards recycle. - Hero layers are static (painted once); only two tiny composited transforms animate. Posters/photos stay on small TMDB sizes + lazy-load; trending cached; search debounced + request-sequenced. Pure visual/perf layer — same data + isolation. Shell/JS tests pass. --- tests/test_video_side_shell.py | 3 ++ webui/index.html | 10 +++- webui/static/video/video-person.js | 15 +++++- webui/static/video/video-side.css | 85 ++++++++++++++++++++++++------ 4 files changed, 93 insertions(+), 20 deletions(-) diff --git a/tests/test_video_side_shell.py b/tests/test_video_side_shell.py index 416894aa..b8d0e89b 100644 --- a/tests/test_video_side_shell.py +++ b/tests/test_video_side_shell.py @@ -410,6 +410,9 @@ def test_person_subpage_and_module_isolated(): assert "themoviedb.org" not in src and "imdb.com" not in src # The person page is a registered detail route (reload / back / new-tab work). assert "video-person-detail" in _JS + # Cinematic hero bits: ambient backdrop, rotating accent ring, role tagline. + assert "data-vp-ambient" in _INDEX and "vp-photo-ring" in _INDEX + assert "data-vp-role" in _INDEX def test_detail_keeps_preview_items_in_app(): diff --git a/webui/index.html b/webui/index.html index 2f690a27..4c6ec812 100644 --- a/webui/index.html +++ b/webui/index.html @@ -1033,9 +1033,15 @@
Loading…
-
-
+
+ +
+ + +
+
+

diff --git a/webui/static/video/video-person.js b/webui/static/video/video-person.js index 962f7c24..b8b06531 100644 --- a/webui/static/video/video-person.js +++ b/webui/static/video/video-person.js @@ -17,6 +17,10 @@ var data = null; var currentId = null; var tab = 'all'; // all | movie | show + var ROLE_NOUNS = { + Acting: 'Actor', Directing: 'Director', Writing: 'Writer', Production: 'Producer', + Sound: 'Composer', Camera: 'Cinematographer', Editing: 'Editor', Creator: 'Creator', + }; function esc(s) { return String(s == null ? '' : s) @@ -106,10 +110,19 @@ if (amb) amb.style.setProperty('--vp-bg', d.photo ? "url('" + d.photo + "')" : 'none'); setText('[data-vp-name]', d.name); + + // Role tagline (a friendlier noun than the raw department). + var role = q('[data-vp-role]'); + if (role) { + var noun = ROLE_NOUNS[d.known_for] || d.known_for || ''; + role.textContent = noun; role.hidden = !noun; + } + var meta = []; - if (d.known_for) meta.push(d.known_for); var ls = lifespan(d); if (ls) meta.push(ls); if (d.place_of_birth) meta.push(d.place_of_birth); + var n = (d.credits || []).length; + if (n) meta.push(n + (n === 1 ? ' credit' : ' credits')); var m = q('[data-vp-meta]'); if (m) m.innerHTML = meta.map(function (x) { return '' + esc(x) + ''; }).join(''); diff --git a/webui/static/video/video-side.css b/webui/static/video/video-side.css index 05fde83d..75056088 100644 --- a/webui/static/video/video-side.css +++ b/webui/static/video/video-side.css @@ -970,42 +970,80 @@ body[data-side="video"] .dashboard-header-sweep { --vd-accent-rgb: var(--accent-rgb, 88, 101, 242); position: relative; margin: -40px -40px -90px; min-height: 80vh; } +/* Cinematic hero: a blurred-portrait wash + an accent colour mesh, capped by a + vignette and a fade into the page. Layers are static (painted once) so the + hero stays cheap. */ .vp-ambient { - position: absolute; inset: 0 0 auto 0; height: 520px; z-index: 0; overflow: hidden; + position: absolute; inset: 0 0 auto 0; height: 560px; z-index: 0; overflow: hidden; + -webkit-mask-image: linear-gradient(180deg, #000 62%, transparent); + mask-image: linear-gradient(180deg, #000 62%, transparent); } .vp-ambient::before { - content: ''; position: absolute; inset: -60px; background-size: cover; background-position: center 20%; - background-image: var(--vp-bg, none); filter: blur(46px) saturate(1.25); transform: scale(1.18); - opacity: 0.45; + content: ''; position: absolute; inset: -80px; background-size: cover; background-position: center 18%; + background-image: var(--vp-bg, none); filter: blur(54px) saturate(1.35); transform: scale(1.2); + opacity: 0.5; } .vp-ambient::after { content: ''; position: absolute; inset: 0; - background: linear-gradient(180deg, rgba(11,11,15,0.35) 0%, rgba(11,11,15,0.7) 55%, var(--bg-primary, #0b0b0f) 100%); + background: + radial-gradient(80% 90% at 16% -10%, rgba(var(--vd-accent-rgb), 0.42), transparent 60%), + radial-gradient(70% 80% at 100% 0%, rgba(var(--vd-accent-rgb), 0.2), transparent 65%), + radial-gradient(120% 80% at 50% 120%, rgba(0, 0, 0, 0.5), transparent 60%), + linear-gradient(180deg, rgba(11,11,15,0.25) 0%, rgba(11,11,15,0.62) 58%, var(--bg-primary, #0b0b0f) 100%); } .vp-page[data-has-bg="0"] .vp-ambient::before { - background-image: radial-gradient(80% 120% at 30% 0%, rgba(var(--vd-accent-rgb), 0.5), transparent 70%); - filter: none; opacity: 0.7; + background-image: radial-gradient(80% 120% at 30% 0%, rgba(var(--vd-accent-rgb), 0.55), transparent 70%); + filter: none; opacity: 0.8; } .vp-page .vd-back { z-index: 6; } .vp-hero { - position: relative; z-index: 2; display: flex; gap: 36px; align-items: flex-end; - padding: 130px 56px 30px; animation: vdRise 0.7s cubic-bezier(0.2, 0.7, 0.2, 1) both; + position: relative; z-index: 2; display: flex; gap: 40px; align-items: flex-end; + padding: 150px 56px 34px; animation: vdRise 0.7s cubic-bezier(0.2, 0.7, 0.2, 1) both; } @keyframes vdRise { from { opacity: 0; transform: translateY(26px); } to { opacity: 1; transform: none; } } + +/* Portrait: a frame with a slowly-rotating accent gradient ring (GPU transform — + cheap) + a gentle float. */ .vp-photo-wrap { - flex: 0 0 auto; width: 188px; height: 188px; border-radius: 50%; overflow: hidden; - background: linear-gradient(145deg, rgba(var(--vd-accent-rgb), 0.4), rgba(8, 8, 12, 0.9)); + position: relative; flex: 0 0 auto; width: 200px; height: 200px; + animation: vp-float 7s ease-in-out infinite; +} +@keyframes vp-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-9px); } } +.vp-photo-ring { + position: absolute; inset: -7px; border-radius: 50%; z-index: 0; + background: conic-gradient(from 0deg, + rgba(var(--vd-accent-rgb), 0.95), rgba(var(--vd-accent-rgb), 0.04) 38%, + rgba(var(--vd-accent-rgb), 0.55) 72%, rgba(var(--vd-accent-rgb), 0.95)); + -webkit-mask: radial-gradient(closest-side, transparent calc(100% - 9px), #000 calc(100% - 8px)); + mask: radial-gradient(closest-side, transparent calc(100% - 9px), #000 calc(100% - 8px)); + animation: vp-spin 18s linear infinite; will-change: transform; opacity: 0.9; +} +@keyframes vp-spin { to { transform: rotate(360deg); } } +.vp-photo-frame { + position: absolute; inset: 0; border-radius: 50%; overflow: hidden; z-index: 1; + background: linear-gradient(145deg, rgba(var(--vd-accent-rgb), 0.42), rgba(8, 8, 12, 0.92)); display: flex; align-items: center; justify-content: center; - box-shadow: 0 18px 50px rgba(0, 0, 0, 0.6), 0 0 0 4px rgba(var(--vd-accent-rgb), 0.35), - 0 0 40px rgba(var(--vd-accent-rgb), 0.3); + box-shadow: 0 22px 56px rgba(0, 0, 0, 0.62), 0 0 60px rgba(var(--vd-accent-rgb), 0.32), + inset 0 0 0 2px rgba(255, 255, 255, 0.07); } .vp-photo { width: 100%; height: 100%; object-fit: cover; } -.vp-photo-ph { font-size: 76px; opacity: 0.55; } -.vp-hero-info { flex: 1; min-width: 0; padding-bottom: 6px; } +.vp-photo-ph { font-size: 80px; opacity: 0.55; } + +.vp-hero-info { flex: 1; min-width: 0; padding-bottom: 10px; } +.vp-role { + display: inline-block; margin-bottom: 12px; font-size: 12px; font-weight: 800; + letter-spacing: 1.4px; text-transform: uppercase; color: rgb(var(--vd-accent-rgb)); + padding: 5px 13px; border-radius: 999px; + background: rgba(var(--vd-accent-rgb), 0.14); border: 1px solid rgba(var(--vd-accent-rgb), 0.34); + text-shadow: 0 0 18px rgba(var(--vd-accent-rgb), 0.5); +} .vp-name { - margin: 0 0 12px; font-size: clamp(2.2em, 4.5vw, 3.4em); font-weight: 900; color: #fff; - line-height: 1; letter-spacing: -0.02em; text-shadow: 0 4px 30px rgba(0, 0, 0, 0.6); + margin: 0 0 14px; font-size: clamp(2.4em, 5vw, 3.8em); font-weight: 900; + line-height: 0.98; letter-spacing: -0.025em; + background: linear-gradient(180deg, #fff 55%, rgba(255, 255, 255, 0.72)); + -webkit-background-clip: text; background-clip: text; color: transparent; + filter: drop-shadow(0 6px 30px rgba(0, 0, 0, 0.6)); } .vp-meta { display: flex; flex-wrap: wrap; gap: 9px; } .vp-meta span { @@ -1013,6 +1051,10 @@ body[data-side="video"] .dashboard-header-sweep { padding: 5px 12px; border-radius: 999px; background: rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.14); backdrop-filter: blur(6px); } +@media (prefers-reduced-motion: reduce) { + .vp-photo-wrap { animation: none; } + .vp-photo-ring { animation: none; } +} .vp-body { position: relative; z-index: 2; padding: 16px 56px 60px; } .vp-bio { @@ -1037,6 +1079,15 @@ body[data-side="video"] .dashboard-header-sweep { .vp-known-rail::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.15); border-radius: 4px; } .vp-known-rail .vsr-card { flex: 0 0 152px; width: 152px; scroll-snap-align: start; } +/* Long filmography grids can be hundreds of cards — let the browser skip + rendering the off-screen ones (cheap scrolling) and don't replay the entrance + animation as cards recycle in and out of view. */ +.vp-credits-section .vsr-grid > .vsr-card { + content-visibility: auto; + contain-intrinsic-size: auto 300px; + animation: none; +} + .vp-credits-section { position: relative; z-index: 2; } .vp-credits-head { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 14px; margin-bottom: 20px;