From 6a83981e1d4979ba7c4098e0de36dd01d50ecb67 Mon Sep 17 00:00:00 2001 From: CodeDevMLH <145071728+CodeDevMLH@users.noreply.github.com> Date: Thu, 26 Feb 2026 21:52:48 +0100 Subject: [PATCH] Enhance space animation: add nebula glow effect, shooting stars, and random space images for improved visual experience --- Jellyfin.Plugin.Seasonals/Web/space.css | 60 +++++++--- Jellyfin.Plugin.Seasonals/Web/space.js | 139 ++++++++++++++++++++---- 2 files changed, 162 insertions(+), 37 deletions(-) diff --git a/Jellyfin.Plugin.Seasonals/Web/space.css b/Jellyfin.Plugin.Seasonals/Web/space.css index 3c8b31b..cc42c3d 100644 --- a/Jellyfin.Plugin.Seasonals/Web/space.css +++ b/Jellyfin.Plugin.Seasonals/Web/space.css @@ -10,13 +10,53 @@ contain: strict; } +.space-bg-glow { + position: absolute; + top: 0; left: 0; width: 100vw; height: 100vh; + background: radial-gradient(circle at 70% 30%, rgba(138, 43, 226, 0.15), transparent 60%), + radial-gradient(circle at 20% 80%, rgba(65, 105, 225, 0.15), transparent 50%); + pointer-events: none; + z-index: 10; + animation: space-nebula-pulse 10s ease-in-out infinite alternate; +} + +@keyframes space-nebula-pulse { + 0% { opacity: 0.6; } + 100% { opacity: 1; } +} + +.space-starfield { + position: absolute; + top: 0; left: 0; width: 100vw; height: 100vh; + background: transparent; + z-index: 11; +} + +.space-shooting-star { + position: absolute; + width: 250px; + height: 3px; + background: linear-gradient(90deg, rgba(255,255,255,1) 0%, rgba(255,255,255,0) 100%); + border-radius: 50%; + animation: space-shoot 8s linear infinite; + opacity: 0; + z-index: 12; +} + +@keyframes space-shoot { + 0% { left: var(--shoot-start-x); top: var(--shoot-start-y); opacity: 1; width: 0; } + 5% { width: 300px; } + 15% { left: var(--shoot-end-x); top: var(--shoot-end-y); opacity: 0; width: 0; } + 100% { left: var(--shoot-end-x); top: var(--shoot-end-y); opacity: 0; width: 0; } +} + .space-symbol { position: absolute; animation-timing-function: linear; animation-iteration-count: infinite; font-size: 3rem; opacity: 0.85; - z-index: 9999; + z-index: 20; } .space-symbol img { @@ -35,21 +75,15 @@ .space-star img { width: 3vh; max-width: 30px; } @keyframes space-drift-right { - 0% { - transform: translateX(0) scaleX(-1); - } - 100% { - transform: translateX(120vw) scaleX(-1); - } + 0% { transform: translateX(-10vw) translateY(0) scaleX(-1); } + 50% { transform: translateX(50vw) translateY(-5vh) scaleX(-1); } + 100% { transform: translateX(110vw) translateY(0) scaleX(-1); } } @keyframes space-drift-left { - 0% { - transform: translateX(0); - } - 100% { - transform: translateX(-120vw); - } + 0% { transform: translateX(10vw) translateY(0); } + 50% { transform: translateX(-50vw) translateY(5vh); } + 100% { transform: translateX(-110vw) translateY(0); } } @keyframes space-slow-spin { diff --git a/Jellyfin.Plugin.Seasonals/Web/space.js b/Jellyfin.Plugin.Seasonals/Web/space.js index a986cb2..92c5f4d 100644 --- a/Jellyfin.Plugin.Seasonals/Web/space.js +++ b/Jellyfin.Plugin.Seasonals/Web/space.js @@ -6,6 +6,24 @@ const useRandomSymbols = config.EnableRandomSymbols !== undefined ? config.Enabl const enableRandomMobile = config.EnableRandomSymbolsMobile !== undefined ? config.EnableRandomSymbolsMobile : false; const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; +const spaceImages = [ + "../Seasonals/Resources/space_assets/astronaut_1.gif", + "../Seasonals/Resources/space_assets/planet_1.png", + "../Seasonals/Resources/space_assets/planet_2.png", + "../Seasonals/Resources/space_assets/planet_3.png", + "../Seasonals/Resources/space_assets/planet_4.png", + "../Seasonals/Resources/space_assets/planet_5.png", + "../Seasonals/Resources/space_assets/planet_6.png", + "../Seasonals/Resources/space_assets/planet_7.png", + "../Seasonals/Resources/space_assets/planet_8.png", + "../Seasonals/Resources/space_assets/planet_9.png", + "../Seasonals/Resources/space_assets/rocket.gif", + "../Seasonals/Resources/space_assets/Satellite_1.gif", + "../Seasonals/Resources/space_assets/Satellite_2.gif", + "../Seasonals/Resources/space_assets/space-shuttle.png", + "../Seasonals/Resources/space_assets/iss.png" +]; + let msgPrinted = false; function toggleSpace() { @@ -58,63 +76,136 @@ function createSpace() { finalCount = enableRandomMobile ? totalSymbols : standardCount; } - const useRandomDuration = enableDifferentDuration !== false; + // Add Nebula Glow + const bgGlow = document.createElement('div'); + bgGlow.className = 'space-bg-glow'; + container.appendChild(bgGlow); - const activeItems = ['planet1', 'planet2', 'star', 'astronaut', 'rocket']; + // Add CSS Starfield + const starfield = document.createElement('div'); + starfield.className = 'space-starfield'; + let boxShadows1 = []; + let boxShadows2 = []; + let boxShadows3 = []; + + // Generate random stars for parallax starfield using CSS % / vw sizes for responsiveness + for (let i = 0; i < 150; i++) { + boxShadows1.push(`${(Math.random() * 100).toFixed(2)}vw ${(Math.random() * 100).toFixed(2)}vh #FFF`); + if (i < 50) boxShadows2.push(`${(Math.random() * 100).toFixed(2)}vw ${(Math.random() * 100).toFixed(2)}vh #FFF`); + if (i < 20) boxShadows3.push(`${(Math.random() * 100).toFixed(2)}vw ${(Math.random() * 100).toFixed(2)}vh #FFF`); + } + + const starLayer1 = document.createElement('div'); + starLayer1.style.width = '1px'; starLayer1.style.height = '1px'; + starLayer1.style.background = 'transparent'; + starLayer1.style.boxShadow = boxShadows1.join(", "); + starLayer1.style.animation = 'space-slow-spin 200s linear infinite'; + starfield.appendChild(starLayer1); + + const starLayer2 = document.createElement('div'); + starLayer2.style.width = '2px'; starLayer2.style.height = '2px'; + starLayer2.style.background = 'transparent'; + starLayer2.style.boxShadow = boxShadows2.join(", "); + starLayer2.style.animation = 'space-slow-spin 150s linear infinite reverse'; + starfield.appendChild(starLayer2); + + const starLayer3 = document.createElement('div'); + starLayer3.style.width = '3px'; starLayer3.style.height = '3px'; + starLayer3.style.background = 'transparent'; + starLayer3.style.boxShadow = boxShadows3.join(", "); + starLayer3.style.animation = 'space-slow-spin 100s linear infinite'; + starfield.appendChild(starLayer3); + + container.appendChild(starfield); + + // Shooting stars + const shootingStarCount = isMobile ? 1 : 2; // Less frequent + for (let i = 0; i < shootingStarCount; i++) { + const streak = document.createElement('div'); + streak.className = 'space-shooting-star'; + // Pick a random tail direction and fall direction to match + const isFromLeft = Math.random() > 0.5; + const rotateAngle = isFromLeft ? 45 : -45; + + streak.style.transform = `rotate(${rotateAngle}deg)`; + streak.style.transformOrigin = isFromLeft ? 'left center' : 'right center'; + + const topStart = Math.random() * 50; + streak.style.setProperty('--shoot-start-x', isFromLeft ? '-20vw' : '120vw'); + streak.style.setProperty('--shoot-end-x', isFromLeft ? '120vw' : '-20vw'); + streak.style.setProperty('--shoot-start-y', `${topStart}vh`); + streak.style.setProperty('--shoot-end-y', `${topStart + 140}vh`); // 140vh drop to cross screen diagonally + + streak.style.animationDelay = `${Math.random() * 20}s`; // Less frequent + streak.style.animationDuration = `${Math.random() * 2 + 3}s`; // 3-5s + container.appendChild(streak); + } + + const useRandomDuration = enableDifferentDuration !== false; for (let i = 0; i < finalCount; i++) { let symbol = document.createElement('div'); - const randomItem = activeItems[Math.floor(Math.random() * activeItems.length)]; - symbol.className = `space-symbol space-${randomItem}`; + const randomImage = spaceImages[Math.floor(Math.random() * spaceImages.length)]; + symbol.className = `space-symbol`; let img = document.createElement('img'); - img.src = `../Seasonals/Resources/space_images/${randomItem}.png`; + img.src = randomImage; img.onerror = function() { this.style.display = 'none'; - this.parentElement.innerHTML = getSpaceEmojiFallback(randomItem); - }; + }; // removed emoji fallback symbol.appendChild(img); const topPos = Math.random() * 90; // 0 to 90vh const delaySeconds = Math.random() * 10; - let durationSeconds = 15; + // Zero gravity sizes / speeds + const depth = Math.random(); + const distanceScale = 0.3 + (depth * 0.7); // 0.3 to 1.0 (decently small) + const blurAmount = depth < 0.3 ? (1 - depth) * 2 : 0; + + symbol.style.filter = `blur(${blurAmount}px)`; + symbol.style.zIndex = Math.floor(depth * 30) + 20; + + let durationSeconds = 30; // Very slow if (useRandomDuration) { - durationSeconds = Math.random() * 15 + 15; // 15 to 30 seconds for slow drift + durationSeconds = (1 - depth) * 40 + 30 + Math.random() * 10 - 5; } // Randomly pick direction: left-to-right OR right-to-left const goRight = Math.random() > 0.5; + const baseTransformScale = goRight ? 'scaleX(-1)' : 'scaleX(1)'; + if (goRight) { symbol.style.animationName = 'space-drift-right'; - symbol.style.left = '-10vw'; - symbol.style.transform = 'scaleX(-1)'; // flip some items horizontally if moving right + symbol.style.left = '-15vw'; } else { symbol.style.animationName = 'space-drift-left'; - symbol.style.right = '-10vw'; + symbol.style.right = '-15vw'; } symbol.style.top = `${topPos}vh`; symbol.style.animationDuration = `${durationSeconds}s`; symbol.style.animationDelay = `${delaySeconds}s`; - - // Add a slow rotation - symbol.style.setProperty('--rot-dur', `${durationSeconds}s`); + + // Slow rotation inside inner div + const rotationDiv = document.createElement('div'); + const rotDur = Math.random() * 20 + 20; // 20-40s spin + const spinReverse = Math.random() > 0.5 ? 'reverse' : 'normal'; + rotationDiv.style.animation = `space-slow-spin ${rotDur}s linear infinite ${spinReverse}`; + + rotationDiv.appendChild(symbol.cloneNode(true)); + + // Apply final static scaling and facing to inner image + rotationDiv.firstChild.style.transform = `scale(${distanceScale}) ${baseTransformScale}`; + + symbol.innerHTML = ''; + symbol.appendChild(rotationDiv); container.appendChild(symbol); } } -function getSpaceEmojiFallback(type) { - if (type === 'planet1') return '🪐'; - if (type === 'planet2') return '🌍'; - if (type === 'star') return '⭐'; - if (type === 'astronaut') return '👨‍🚀'; - if (type === 'rocket') return '🚀'; - return '✨'; -} - function initializeSpace() { if (!space) return; createSpace();