From 9099f9490405cf17122daf5821ec52f8663697a9 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Sun, 1 Feb 2026 08:18:17 -0600 Subject: [PATCH] feat(immersive): add real coverflow with queue navigation - Rename old CoverflowPanel to StaticPanel (single artwork view) - Create new CoverflowPanel with actual coverflow effect: - Shows 5 album covers (2 prev, current, 2 next) - 3D perspective rotation on side covers - Click on side covers to navigate queue - Smooth animation on track change - Add 'static' tab to focus mode (4 tabs total) - Update keyboard shortcuts: 1-4 for focus tabs --- .../immersive/ImmersiveHeader.svelte | 5 +- .../immersive/ImmersivePlayer.svelte | 23 +- .../immersive/panels/CoverflowPanel.svelte | 297 ++++++++++++++++-- .../immersive/panels/StaticPanel.svelte | 199 ++++++++++++ 4 files changed, 489 insertions(+), 35 deletions(-) create mode 100644 src/lib/components/immersive/panels/StaticPanel.svelte diff --git a/src/lib/components/immersive/ImmersiveHeader.svelte b/src/lib/components/immersive/ImmersiveHeader.svelte index 4ff780a1c..bcfc9e729 100644 --- a/src/lib/components/immersive/ImmersiveHeader.svelte +++ b/src/lib/components/immersive/ImmersiveHeader.svelte @@ -1,10 +1,10 @@
-
-
- {trackTitle} -
+
+ {#each coverflowItems as item (item.index)} + + {/each}
@@ -62,46 +145,150 @@ flex-direction: column; align-items: center; justify-content: center; - gap: 20px; - /* Offset for header (70px) and controls (120px) to achieve true visual center */ + gap: 24px; padding-top: 70px; padding-bottom: 120px; padding-left: 40px; padding-right: 40px; z-index: 5; + perspective: 1000px; } - .artwork-wrapper { - display: flex; - flex-direction: column; - align-items: center; - } - - .artwork-container { + .coverflow-container { position: relative; - width: min(45vh, 360px); + display: flex; + align-items: center; + justify-content: center; + width: 100%; + max-width: 900px; height: min(45vh, 360px); + perspective: 1200px; + transform-style: preserve-3d; + } + + .coverflow-item { + position: absolute; + background: none; + border: none; + padding: 0; + cursor: pointer; + transition: all 500ms cubic-bezier(0.4, 0, 0.2, 1); + transform-style: preserve-3d; + } + + .coverflow-item:disabled { + cursor: default; + } + + .coverflow-item:not(:disabled):hover { + transform: translateX(var(--hover-x, 0)) rotateY(var(--hover-rotate, 0)) scale(1.05); + } + + .cover-wrapper { + position: relative; + width: min(38vh, 300px); + height: min(38vh, 300px); border-radius: 8px; - overflow: hidden; - box-shadow: - 0 8px 32px rgba(0, 0, 0, 0.5), - 0 20px 60px rgba(0, 0, 0, 0.3); - transition: transform 300ms ease, box-shadow 300ms ease; + overflow: visible; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); + transition: box-shadow 300ms ease; } - .artwork-container:hover { - transform: scale(1.02) translateY(-4px); - box-shadow: - 0 12px 40px rgba(0, 0, 0, 0.5), - 0 28px 80px rgba(0, 0, 0, 0.3); + .coverflow-item:not(:disabled):hover .cover-wrapper { + box-shadow: 0 12px 48px rgba(0, 0, 0, 0.5); } - .artwork { + .cover-image { width: 100%; height: 100%; object-fit: cover; + border-radius: 8px; } + .cover-reflection { + position: absolute; + bottom: -60%; + left: 0; + right: 0; + height: 60%; + background: linear-gradient( + to bottom, + rgba(255, 255, 255, 0.1) 0%, + transparent 100% + ); + transform: scaleY(-1); + mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.3) 0%, transparent 50%); + -webkit-mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.3) 0%, transparent 50%); + pointer-events: none; + border-radius: 8px; + } + + /* Center item */ + .coverflow-item.center { + z-index: 5; + transform: translateX(0) rotateY(0deg) scale(1); + } + + .coverflow-item.center .cover-wrapper { + width: min(45vh, 360px); + height: min(45vh, 360px); + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.5), + 0 20px 60px rgba(0, 0, 0, 0.3); + } + + /* Left items */ + .coverflow-item.left-1 { + z-index: 4; + transform: translateX(-180px) rotateY(45deg) scale(0.85); + --hover-x: -170px; + --hover-rotate: 40deg; + opacity: 0.9; + } + + .coverflow-item.left-2 { + z-index: 3; + transform: translateX(-300px) rotateY(55deg) scale(0.7); + --hover-x: -290px; + --hover-rotate: 50deg; + opacity: 0.6; + } + + /* Right items */ + .coverflow-item.right-1 { + z-index: 4; + transform: translateX(180px) rotateY(-45deg) scale(0.85); + --hover-x: 170px; + --hover-rotate: -40deg; + opacity: 0.9; + } + + .coverflow-item.right-2 { + z-index: 3; + transform: translateX(300px) rotateY(-55deg) scale(0.7); + --hover-x: 290px; + --hover-rotate: -50deg; + opacity: 0.6; + } + + /* Animation classes */ + .coverflow-container.animate-left .coverflow-item { + animation: slideLeft 500ms cubic-bezier(0.4, 0, 0.2, 1); + } + + .coverflow-container.animate-right .coverflow-item { + animation: slideRight 500ms cubic-bezier(0.4, 0, 0.2, 1); + } + + @keyframes slideLeft { + 0% { transform: translateX(calc(var(--translate-x, 0px) + 100px)); } + } + + @keyframes slideRight { + 0% { transform: translateX(calc(var(--translate-x, 0px) - 100px)); } + } + + /* Track info */ .track-info { display: flex; flex-direction: column; @@ -174,20 +361,68 @@ } /* Responsive */ + @media (max-width: 900px) { + .coverflow-item.left-1 { + transform: translateX(-140px) rotateY(45deg) scale(0.8); + --hover-x: -130px; + } + + .coverflow-item.left-2 { + transform: translateX(-230px) rotateY(55deg) scale(0.65); + --hover-x: -220px; + } + + .coverflow-item.right-1 { + transform: translateX(140px) rotateY(-45deg) scale(0.8); + --hover-x: 130px; + } + + .coverflow-item.right-2 { + transform: translateX(230px) rotateY(-55deg) scale(0.65); + --hover-x: 220px; + } + } + @media (max-width: 768px) { .coverflow-panel { - padding: 70px 24px 130px; + padding: 70px 16px 130px; gap: 16px; } - .artwork-container { - width: min(55vw, 280px); - height: min(55vw, 280px); + .cover-wrapper { + width: min(30vh, 200px); + height: min(30vh, 200px); + } + + .coverflow-item.center .cover-wrapper { + width: min(38vh, 260px); + height: min(38vh, 260px); + } + + .coverflow-item.left-1 { + transform: translateX(-100px) rotateY(45deg) scale(0.75); + } + + .coverflow-item.left-2 { + display: none; + } + + .coverflow-item.right-1 { + transform: translateX(100px) rotateY(-45deg) scale(0.75); + } + + .coverflow-item.right-2 { + display: none; } } @media (max-height: 600px) { - .artwork-container { + .cover-wrapper { + width: min(28vh, 180px); + height: min(28vh, 180px); + } + + .coverflow-item.center .cover-wrapper { width: min(32vh, 220px); height: min(32vh, 220px); } diff --git a/src/lib/components/immersive/panels/StaticPanel.svelte b/src/lib/components/immersive/panels/StaticPanel.svelte new file mode 100644 index 000000000..780bda029 --- /dev/null +++ b/src/lib/components/immersive/panels/StaticPanel.svelte @@ -0,0 +1,199 @@ + + +
+
+
+ {trackTitle} +
+
+ +
+ {#if isPlaying} +
+
+ + + + +
+ Now Playing +
+ {/if} +

{trackTitle}

+

{artist}

+ {#if album} +

{album}

+ {/if} +
+ +
+
+
+ +