diff --git a/Jellyfin.Plugin.Seasonals/Web/halloween.css b/Jellyfin.Plugin.Seasonals/Web/halloween.css index a0f6ea7..2cbce01 100644 --- a/Jellyfin.Plugin.Seasonals/Web/halloween.css +++ b/Jellyfin.Plugin.Seasonals/Web/halloween.css @@ -7,7 +7,7 @@ width: 100%; height: 100%; pointer-events: none; - z-index: 10; + z-index: 10000; contain: layout paint; } @@ -82,71 +82,183 @@ .halloween:nth-of-type(0) { left: 1%; -webkit-animation-delay: 0s, 0s; - animation-delay: 0s, 0s + animation-delay: 0s, 0s; } .halloween:nth-of-type(1) { left: 10%; - -webkit-animation-delay: 1s, 1s; - animation-delay: 1s, 1s + -webkit-animation-delay: -1s, -1s; + animation-delay: -1s, -1s; } .halloween:nth-of-type(2) { left: 20%; - -webkit-animation-delay: 6s, .5s; - animation-delay: 6s, .5s + -webkit-animation-delay: -2s, -2s; + animation-delay: -2s, -2s; } .halloween:nth-of-type(3) { left: 30%; - -webkit-animation-delay: 4s, 2s; - animation-delay: 4s, 2s + -webkit-animation-delay: -3s, -3s; + animation-delay: -3s, -3s; } .halloween:nth-of-type(4) { left: 40%; - -webkit-animation-delay: 2s, 2s; - animation-delay: 2s, 2s + -webkit-animation-delay: -4s, -4s; + animation-delay: -4s, -4s; } .halloween:nth-of-type(5) { left: 50%; - -webkit-animation-delay: 8s, 3s; - animation-delay: 8s, 3s + -webkit-animation-delay: -5s, -5s; + animation-delay: -5s, -5s; } .halloween:nth-of-type(6) { left: 60%; - -webkit-animation-delay: 6s, 2s; - animation-delay: 6s, 2s + -webkit-animation-delay: -6s, -6s; + animation-delay: -6s, -6s; } .halloween:nth-of-type(7) { left: 70%; - -webkit-animation-delay: 2.5s, 1s; - animation-delay: 2.5s, 1s + -webkit-animation-delay: -7s, -7s; + animation-delay: -7s, -7s; } .halloween:nth-of-type(8) { left: 80%; - -webkit-animation-delay: 1s, 0s; - animation-delay: 1s, 0s + -webkit-animation-delay: -8s, -8s; + animation-delay: -8s, -8s; } .halloween:nth-of-type(9) { left: 90%; - -webkit-animation-delay: 3s, 1.5s; - animation-delay: 3s, 1.5s + -webkit-animation-delay: -9s, -9s; + animation-delay: -9s, -9s; } .halloween:nth-of-type(10) { left: 25%; - -webkit-animation-delay: 2s, 0s; - animation-delay: 2s, 0s + -webkit-animation-delay: -10s, -10s; + animation-delay: -10s, -10s; } .halloween:nth-of-type(11) { left: 65%; - -webkit-animation-delay: 4s, 2.5s; - animation-delay: 4s, 2.5s + -webkit-animation-delay: -11s, -11s; + animation-delay: -11s, -11s; +} + +/* --- Fog Layer --- */ +.halloween-fog-layer { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 40vh; + pointer-events: none; + z-index: 1000; + overflow: hidden; + mask-image: linear-gradient(to top, black, transparent); + -webkit-mask-image: linear-gradient(to top, black, transparent); +} +.halloween-fog-blob { + position: absolute; + bottom: -10vh; + width: 150vw; + height: 50vh; + background: radial-gradient(ellipse at center, rgba(120, 130, 140, 0.4) 0%, transparent 60%); + border-radius: 50%; + filter: blur(15px); +} +.halloween-fog-blob:nth-child(1) { + left: -20vw; + animation: fog-float1 25s ease-in-out infinite alternate; +} +.halloween-fog-blob:nth-child(2) { + left: -50vw; + background: radial-gradient(ellipse at center, rgba(100, 110, 120, 0.3) 0%, transparent 65%); + animation: fog-float2 35s ease-in-out infinite alternate; +} +@keyframes fog-float1 { + 0% { transform: translateX(0) scale(1); opacity: 0.8; } + 50% { opacity: 1; } + 100% { transform: translateX(20vw) scale(1.1); opacity: 0.6; } +} +@keyframes fog-float2 { + 0% { transform: translateX(0) scale(1.1); opacity: 0.7; } + 50% { opacity: 1; } + 100% { transform: translateX(30vw) scale(1); opacity: 0.5; } +} + +/* --- Spiders --- */ +.halloween-spider-wrapper { + position: absolute; + top: -50px; + display: flex; + flex-direction: column; + align-items: center; + z-index: 1002; + transform-origin: top; + will-change: transform; + pointer-events: auto; + padding: 20px; /* Increase hit area safely */ +} +.halloween-thread { + width: 30px; /* Wider hit area for mouse interaction */ + height: 100vh; + margin-top: -100vh; + position: relative; + cursor: pointer; +} +.halloween-thread::after { + content: ''; + position: absolute; + left: 50%; + transform: translateX(-50%); + width: 2px; + height: 100%; + background: linear-gradient(to bottom, rgba(200, 200, 200, 0.1), rgba(200, 200, 200, 0.6)); +} +.halloween-spider { + animation: spider-swing 3s ease-in-out infinite alternate; + transform-origin: top center; +} + +/* MARK: SPIDER SWAY CONFIGURATION */ +/* Adjust degrees in 'rotate(...)' to change how far spider and thread swing in wind. */ +@keyframes wind-sway { + 0% { transform: rotate(0deg); } + 25% { transform: rotate(2deg); } + 75% { transform: rotate(-2deg); } + 100% { transform: rotate(0deg); } +} + +@keyframes spider-drop { + 0% { transform: translateY(-50px); } + 30% { transform: translateY(var(--drop-height, 50vh)); } + 60% { transform: translateY(var(--drop-height, 50vh)); } + 100% { transform: translateY(-50px); } +} +@keyframes spider-swing { + 0% { transform: rotate(-10deg); } + 100% { transform: rotate(10deg); } +} + +/* Mice */ +.halloween-mouse { + position: absolute; + z-index: 10000; + pointer-events: none; + will-change: left; +} +@keyframes mouse-run-right { + 0% { left: -10vw; } + 100% { left: 110vw; } +} +@keyframes mouse-run-left { + 0% { left: 110vw; } + 100% { left: -10vw; } } \ No newline at end of file diff --git a/Jellyfin.Plugin.Seasonals/Web/halloween.js b/Jellyfin.Plugin.Seasonals/Web/halloween.js index 26882c6..b3ab05a 100644 --- a/Jellyfin.Plugin.Seasonals/Web/halloween.js +++ b/Jellyfin.Plugin.Seasonals/Web/halloween.js @@ -4,8 +4,16 @@ const halloween = config.EnableHalloween !== undefined ? config.EnableHalloween const randomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true; // enable more random symbols const randomSymbolsMobile = config.EnableRandomSymbolsMobile !== undefined ? config.EnableRandomSymbolsMobile : false; // enable random symbols on mobile devices (Warning: High values may affect performance) const enableDiffrentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; // enable different duration for the random halloween symbols +const enableSpiders = config.EnableSpiders !== undefined ? config.EnableSpiders : true; +const enableMice = config.EnableMice !== undefined ? config.EnableMice : true; const halloweenCount = config.SymbolCount || 25; // count of random extra symbols +const images = [ + "../Seasonals/Resources/halloween_images/ghost_20x20.png", + "../Seasonals/Resources/halloween_images/bat_20x20.png", + "../Seasonals/Resources/halloween_images/pumpkin_20x20.png", +]; + let msgPrinted = false; // flag to prevent multiple console messages // function to check and control the halloween @@ -36,21 +44,13 @@ function toggleHalloween() { // observe changes in the DOM const observer = new MutationObserver(toggleHalloween); - -// start observation observer.observe(document.body, { - childList: true, // observe adding/removing of child elements - subtree: true, // observe all levels of the DOM tree - attributes: true // observe changes to attributes (e.g. class changes) + childList: true, + subtree: true, + attributes: true }); -const images = [ - "../Seasonals/Resources/halloween_images/ghost_20x20.png", - "../Seasonals/Resources/halloween_images/bat_20x20.png", - "../Seasonals/Resources/halloween_images/pumpkin_20x20.png", -]; - function addRandomSymbols(count) { const halloweenContainer = document.querySelector('.halloween-container'); // get the halloween container if (!halloweenContainer) return; // exit if halloween container is not found @@ -74,7 +74,7 @@ function addRandomSymbols(count) { // set random horizontal position, animation delay and size(uncomment lines to enable) const randomLeft = Math.random() * 100; // position (0% to 100%) const randomAnimationDelay = Math.random() * 10; // delay (0s to 10s) - const randomAnimationDelay2 = Math.random() * 3; // delay (0s to 3s) + const randomAnimationDelay2 = -(Math.random() * 3); // delay (-3s to 0s) // apply styles halloweenDiv.style.left = `${randomLeft}%`; @@ -124,12 +124,137 @@ function createHalloween() { } } +// create fog layer +function createFog(container) { + const fogContainer = document.createElement('div'); + fogContainer.className = 'halloween-fog-layer'; + + const fog1 = document.createElement('div'); + fog1.className = 'halloween-fog-blob'; + + const fog2 = document.createElement('div'); + fog2.className = 'halloween-fog-blob'; + + fogContainer.appendChild(fog1); + fogContainer.appendChild(fog2); + container.appendChild(fogContainer); +} + +// create dropping spiders +function createSpider(container) { + const wrapper = document.createElement('div'); + wrapper.className = 'halloween-spider-wrapper'; + + wrapper.innerHTML = ` +
+
+ + + + + + + + + +
+ `; + + wrapper.style.left = `${10 + Math.random() * 80}%`; + const dropHeight = 30 + Math.random() * 50; // 30vh to 80vh + wrapper.style.setProperty('--drop-height', `${dropHeight}vh`); + + const duration = Math.random() * 6 + 6; // 6-12s drop + wrapper.style.animation = `spider-drop ${duration}s ease-in-out forwards`; + + // Start the sway animation only after the drop completes (30% of total duration) + const sway = wrapper.querySelector('.halloween-sway'); + sway.style.animation = `wind-sway 8s ease-in-out ${duration * 0.3}s infinite`; + + // Spider retreat logic + let isRetreating = false; + wrapper.addEventListener('mouseenter', () => { + if (isRetreating) return; + isRetreating = true; + // Retreat smoothly by pushing margin up + wrapper.style.transition = 'margin-top 0.4s ease-in'; + wrapper.style.marginTop = '-100vh'; + + setTimeout(() => { + wrapper.remove(); + setTimeout(() => createSpider(container), Math.random() * 5000 + 1000); + }, 500); + }); + + wrapper.addEventListener('animationend', () => { + if (isRetreating) return; + wrapper.remove(); + setTimeout(() => createSpider(container), Math.random() * 5000 + 1000); + }); + + container.appendChild(wrapper); +} + +// create scurrying mice +function createMouse(container) { + const mouse = document.createElement('div'); + mouse.className = 'halloween-mouse'; + mouse.innerHTML = ` + + + + + + + `; + + const direction = Math.random() > 0.5 ? 'right' : 'left'; + const duration = Math.random() * 3 + 2; // 2-5s run (fast) + + if (direction === 'right') { + mouse.style.animation = `mouse-run-right ${duration}s linear forwards`; + mouse.style.transform = 'scaleX(1)'; + } else { + mouse.style.animation = `mouse-run-left ${duration}s linear forwards`; + mouse.style.transform = 'scaleX(-1)'; + } + + mouse.style.bottom = `5px`; // Fixated bottom edge + + mouse.addEventListener('animationend', () => { + mouse.remove(); + setTimeout(() => createMouse(container), Math.random() * 4000 + 2000); + }); + + container.appendChild(mouse); +} + // initialize halloween function initializeHalloween() { if (!halloween) return; // exit if halloween is disabled createHalloween(); toggleHalloween(); + const container = document.querySelector('.halloween-container'); + + if (container) { + createFog(container); + + // Add a few spiders + if (enableSpiders) { + for (let i = 0; i < 4; i++) { + setTimeout(() => createSpider(container), Math.random() * 5000); + } + } + + // Add a few mice + if (enableMice) { + for (let i = 0; i < 3; i++) { + setTimeout(() => createMouse(container), Math.random() * 3000); + } + } + } + const screenWidth = window.innerWidth; // get the screen width to detect mobile devices if (randomSymbols && (screenWidth > 768 || randomSymbolsMobile)) { // add random halloweens only on larger screens, unless enabled for mobile devices addRandomSymbols(halloweenCount);