From e8e3424cc9b36fe098aaeb57efa6f2415b316418 Mon Sep 17 00:00:00 2001 From: CodeDevMLH <145071728+CodeDevMLH@users.noreply.github.com> Date: Thu, 19 Feb 2026 03:18:06 +0100 Subject: [PATCH] Add spring animation effects with CSS and JavaScript implementation --- Jellyfin.Plugin.Seasonals/Web/spring.css | 179 +++++++++++++++++ Jellyfin.Plugin.Seasonals/Web/spring.js | 245 +++++++++++++++++++++++ 2 files changed, 424 insertions(+) create mode 100644 Jellyfin.Plugin.Seasonals/Web/spring.css create mode 100644 Jellyfin.Plugin.Seasonals/Web/spring.js diff --git a/Jellyfin.Plugin.Seasonals/Web/spring.css b/Jellyfin.Plugin.Seasonals/Web/spring.css new file mode 100644 index 0000000..0823302 --- /dev/null +++ b/Jellyfin.Plugin.Seasonals/Web/spring.css @@ -0,0 +1,179 @@ +.spring-container { + display: block; + position: fixed; + overflow: hidden; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 1000; +} + +/* Petals */ +.spring-petal { + position: fixed; + top: -20px; + z-index: 1005; + width: 15px; + height: 10px; + background-color: #ffc0cb; + border-radius: 15px 0px 15px 0px; + + will-change: transform, top; + animation-name: spring-fall, spring-sway; + animation-timing-function: linear, ease-in-out; + animation-iteration-count: infinite, infinite; + animation-duration: 10s, 3s; +} + +.spring-petal.lighter { + background-color: #ffd1dc; + opacity: 0.8; +} + +.spring-petal.darker { + background-color: #ffb7c5; + opacity: 0.9; +} + +.spring-petal.type2 { + width: 12px; + height: 12px; + border-radius: 10px 0px 10px 5px; +} + +/* Pollen */ +.spring-pollen { + position: fixed; + z-index: 14; + background-color: #fffacd; + border-radius: 50%; + opacity: 0.6; + box-shadow: 0 0 4px rgba(255, 250, 205, 0.4); + + will-change: transform; + animation-name: spring-float; + animation-timing-function: linear; + animation-iteration-count: infinite; +} + +/* Sunbeams */ +.spring-sunbeam { + position: fixed; + top: -50%; + height: 200%; + background: linear-gradient(to bottom, rgba(255, 255, 255, 0), rgba(255, 255, 200, 0.08) 50%, rgba(255, 255, 255, 0)); + z-index: 5; + transform-origin: top center; + pointer-events: none; + + animation-name: spring-beam-pulse; + animation-timing-function: ease-in-out; + animation-iteration-count: infinite; +} + +/* Grass */ +.spring-grass-container { + position: fixed; + bottom: 0; + left: 0; + width: 100%; + height: 80px; + z-index: 1002; + overflow: hidden; + pointer-events: none; +} + +.spring-grass { + position: absolute; + bottom: 0; + width: 3px; + border-radius: 100% 0 0 0; + transform-origin: bottom center; + background-color: #4caf50; + + will-change: transform; + animation-name: spring-grass-sway; + animation-timing-function: ease-in-out; + animation-iteration-count: infinite; +} + +/* Ladybugs */ +.spring-ladybug { + position: absolute; + width: 6px; + height: 4px; + background-color: #e74c3c; /* Red */ + border-radius: 3px 3px 0 0; + z-index: 1003; + + will-change: left, transform; + animation-timing-function: linear; + animation-iteration-count: infinite; +} + +.spring-ladybug.right { + animation-name: spring-bug-crawl-right; + transform: scaleX(1); +} + +.spring-ladybug.left { + animation-name: spring-bug-crawl-left; + transform: scaleX(-1); +} + +.spring-ladybug::before { + content: ''; + position: absolute; + right: -2px; + top: 1px; + width: 2px; + height: 2px; + background-color: #000; + border-radius: 50%; +} + +@keyframes spring-fall { + 0% { top: -10%; } + 100% { top: 100%; } +} + +@keyframes spring-sway { + 0%, 100% { + transform: translateX(0) rotate(0deg); + } + 50% { + transform: translateX(30px) rotate(45deg); + } +} + +@keyframes spring-float { + 0% { transform: translateX(0) translateY(0); } + 25% { transform: translateX(20px) translateY(-10px); } + 50% { transform: translateX(40px) translateY(0); } + 75% { transform: translateX(20px) translateY(10px); } + 100% { transform: translateX(0) translateY(0); } +} + +@keyframes spring-beam-pulse { + 0% { opacity: 0.3; transform: rotate(45deg) scaleX(1); } + 50% { opacity: 0.6; transform: rotate(45deg) scaleX(1.1); } + 100% { opacity: 0.3; transform: rotate(45deg) scaleX(1); } +} + +@keyframes spring-grass-sway { + 0% { transform: rotate(0deg); } + 50% { transform: rotate(8deg); } + 100% { transform: rotate(0deg); } +} + +@keyframes spring-bug-crawl-right { + 0% { left: -5%; } + 100% { left: 105%; } +} + +@keyframes spring-bug-crawl-left { + 0% { left: 105%; } + 100% { left: -5%; } +} diff --git a/Jellyfin.Plugin.Seasonals/Web/spring.js b/Jellyfin.Plugin.Seasonals/Web/spring.js new file mode 100644 index 0000000..e6330b9 --- /dev/null +++ b/Jellyfin.Plugin.Seasonals/Web/spring.js @@ -0,0 +1,245 @@ +const config = window.SeasonalsPluginConfig?.Spring || {}; + +const spring = config.EnableSpring !== undefined ? config.EnableSpring : true; +const petalCount = config.PetalCount || 25; +const pollenCount = config.PollenCount || 15; +const ladybugCountConfig = config.LadybugCount || 5; +const sunbeamCount = config.SunbeamCount || 5; + +const randomSpring = config.EnableRandomSpring !== undefined ? config.EnableRandomSpring : true; +const randomSpringMobile = config.EnableRandomSpringMobile !== undefined ? config.EnableRandomSpringMobile : false; +const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; +const enablePetals = config.EnableSpringPetals !== undefined ? config.EnableSpringPetals : true; +const enableSunbeams = config.EnableSpringSunbeams !== undefined ? config.EnableSpringSunbeams : true; + +let msgPrinted = false; + +function toggleSpring() { + const springContainer = document.querySelector('.spring-container'); + if (!springContainer) return; + + const videoPlayer = document.querySelector('.videoPlayerContainer'); + const trailerPlayer = document.querySelector('.youtubePlayerContainer'); + const isDashboard = document.body.classList.contains('dashboardDocument'); + const hasUserMenu = document.querySelector('#app-user-menu'); + + if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) { + springContainer.style.display = 'none'; + if (!msgPrinted) { + console.log('Spring hidden'); + msgPrinted = true; + } + } else { + springContainer.style.display = 'block'; + if (msgPrinted) { + console.log('Spring visible'); + msgPrinted = false; + } + } +} + +const observer = new MutationObserver(toggleSpring); +observer.observe(document.body, { childList: true, subtree: true, attributes: true }); + +function createPetal(container) { + if (!enablePetals) return; + + const petal = document.createElement('div'); + petal.classList.add('spring-petal'); + + const type = Math.random() > 0.5 ? 'type1' : 'type2'; + petal.classList.add(type); + + const color = Math.random() > 0.7 ? 'darker' : 'lighter'; + petal.classList.add(color); + + const randomLeft = Math.random() * 100; + petal.style.left = `${randomLeft}%`; + + const size = Math.random() * 0.5 + 0.5; + petal.style.transform = `scale(${size})`; + + const duration = Math.random() * 5 + 8; + const delay = Math.random() * 10; + const swayDuration = Math.random() * 2 + 2; + + if (enableDifferentDuration) { + petal.style.animationDuration = `${duration}s, ${swayDuration}s`; + } + petal.style.animationDelay = `${delay}s, ${Math.random() * 3}s`; + + container.appendChild(petal); +} + +function createPollen(container) { + const pollen = document.createElement('div'); + pollen.classList.add('spring-pollen'); + + const startY = Math.random() * 60 + 20; + pollen.style.top = `${startY}%`; + pollen.style.left = `${Math.random() * 100}%`; + + const size = Math.random() * 3 + 1; + pollen.style.width = `${size}px`; + pollen.style.height = `${size}px`; + + const duration = Math.random() * 20 + 20; + pollen.style.animationDuration = `${duration}s`; + pollen.style.animationDelay = `-${Math.random() * 20}s`; + + container.appendChild(pollen); +} + +function createSunbeam(container) { + if (!enableSunbeams) return; + + const beam = document.createElement('div'); + beam.classList.add('spring-sunbeam'); + + const left = Math.random() * 100; // Spread across full width + beam.style.left = `${left}%`; + + // Thinner beams as requested + const width = Math.random() * 20 + 10; // 10-30px wide + beam.style.width = `${width}px`; + + const rotate = Math.random() * 20 - 10 + 45; + beam.style.transform = `rotate(${rotate}deg)`; + + const duration = Math.random() * 10 + 10; + beam.style.animationDuration = `${duration}s`; + beam.style.animationDelay = `-${Math.random() * 10}s`; + + container.appendChild(beam); +} + +function createGrass(container) { + let grassContainer = container.querySelector('.spring-grass-container'); + if (!grassContainer) { + grassContainer = document.createElement('div'); + grassContainer.className = 'spring-grass-container'; + container.appendChild(grassContainer); + } + + // More grass: 1 blade every 3px (was 15px) + const bladeCount = window.innerWidth / 3; + for (let i = 0; i < bladeCount; i++) { + const blade = document.createElement('div'); + blade.classList.add('spring-grass'); + + const height = Math.random() * 40 + 20; // 20-60px height + blade.style.height = `${height}px`; + blade.style.left = `${i * 3 + Math.random() * 2}px`; + + const duration = Math.random() * 2 + 3; + blade.style.animationDuration = `${duration}s`; + blade.style.animationDelay = `-${Math.random() * 5}s`; + + const hue = 100 + Math.random() * 40; + blade.style.backgroundColor = `hsl(${hue}, 60%, 40%)`; + + grassContainer.appendChild(blade); + } + + // Add Ladybugs + const bugs = ladybugCountConfig; + for (let i = 0; i < bugs; i++) { + createLadybug(grassContainer); + } +} + +function createLadybug(container) { + const bug = document.createElement('div'); + bug.classList.add('spring-ladybug'); + + const direction = Math.random() > 0.5 ? 'right' : 'left'; + bug.classList.add(direction); + + // Position lower (bottom of grass), but ensure visibility + const bottomOffset = direction === 'right' ? Math.random() * 5 + 6 : Math.random() * 5 + 2; + bug.style.bottom = `${bottomOffset}px`; + + // Start position depends on direction + if (direction === 'right') { + bug.style.left = '-5%'; // Start off-screen left + } else { + bug.style.left = '105%'; // Start off-screen right + } + + const duration = Math.random() * 20 + 20; // Slow crawl + bug.style.animationDuration = `${duration}s`; + bug.style.animationDelay = `-${Math.random() * 20}s`; + + container.appendChild(bug); +} + +function addRandomSpringObjects() { + const container = document.querySelector('.spring-container'); + if (!container) return; + + if (enablePetals) { + for (let i = 0; i < petalCount; i++) { + createPetal(container); + } + } + + for (let i = 0; i < pollenCount; i++) { + createPollen(container); + } +} + +function initSpringObjects() { + let container = document.querySelector('.spring-container'); + if (!container) { + container = document.createElement("div"); + container.className = "spring-container"; + container.setAttribute("aria-hidden", "true"); + document.body.appendChild(container); + } + + createGrass(container); + + if (enablePetals) { + for (let i = 0; i < 15; i++) { + const petal = document.createElement('div'); + petal.classList.add('spring-petal'); + const type = Math.random() > 0.5 ? 'type1' : 'type2'; + petal.classList.add(type); + const color = Math.random() > 0.7 ? 'darker' : 'lighter'; + petal.classList.add(color); + const randomLeft = Math.random() * 100; + petal.style.left = `${randomLeft}%`; + const size = Math.random() * 0.5 + 0.5; + petal.style.transform = `scale(${size})`; + + const duration = Math.random() * 5 + 8; + const swayDuration = Math.random() * 2 + 2; + + if (enableDifferentDuration) { + petal.style.animationDuration = `${duration}s, ${swayDuration}s`; + } + petal.style.animationDelay = `-${Math.random() * 10}s, -${Math.random() * 3}s`; + container.appendChild(petal); + } + } + + if (enableSunbeams) { + // Initial sunbeams + for (let i = 0; i < sunbeamCount; i++) { + createSunbeam(container); + } + } +} + +function initializeSpring() { + if (!spring) return; + initSpringObjects(); + toggleSpring(); + + const screenWidth = window.innerWidth; + if (randomSpring && (screenWidth > 768 || randomSpringMobile)) { + addRandomSpringObjects(); + } +} + +initializeSpring();