diff --git a/Jellyfin.Plugin.Seasonals/Web/olympia.css b/Jellyfin.Plugin.Seasonals/Web/olympia.css index cae4425..3811a02 100644 --- a/Jellyfin.Plugin.Seasonals/Web/olympia.css +++ b/Jellyfin.Plugin.Seasonals/Web/olympia.css @@ -13,10 +13,50 @@ .olympia-symbol { position: absolute; top: -10vh; - animation: olympia-fall linear infinite; font-size: 3rem; opacity: 0.95; text-shadow: 0 0 10px rgba(255,255,255,0.2); + z-index: 40; +} + +.olympia-flame { + position: absolute; + bottom: 0vh; + z-index: 50; + pointer-events: none; + transform-origin: bottom center; +} + +.olympia-ring-css { + position: relative; + width: 40px; + height: 40px; +} +.olympia-ring-css::before { + content: ''; + position: absolute; + top: 50%; left: 50%; + transform: translate(-50%, -50%); + width: 30px; + height: 30px; + border: 5px solid #0081C8; /* Default blue ring */ + border-radius: 50%; +} +.olympia-ring-css[style*="--ring-color"]::before { + border-color: var(--ring-color); +} +.olympia-symbol { + position: absolute; + top: -10vh; + font-size: 4rem; + opacity: 0.95; + text-shadow: 0 0 10px rgba(255,255,255,0.2); + z-index: 40; +} + +.olympia-inner { + display: inline-block; + animation: olympia-sway linear infinite alternate; } .olympia-symbol img { @@ -26,46 +66,76 @@ object-fit: contain; } +.olympia-confetti-wrapper { + position: fixed; + z-index: 15; + top: 0; + will-change: transform; + animation-name: olympia-fall; + animation-timing-function: linear; + animation-iteration-count: infinite; +} + +.olympia-confetti-sway { + will-change: transform; + animation-name: olympia-confetti-sway; + animation-timing-function: ease-in-out; + animation-iteration-count: infinite; + animation-direction: alternate; +} + +@keyframes olympia-confetti-sway { + 0% { transform: translateX(calc(var(--sway-amount, 50px) * -1)); } + 100% { transform: translateX(var(--sway-amount, 50px)); } +} + .olympia-confetti { - position: absolute; - top: -5vh; width: 8px; height: 16px; - opacity: 0.85; - animation: olympia-confetti-fall linear infinite; - border-radius: 4px; /* slightly rounder confetti */ + background-color: rgb(0, 0, 0); + will-change: transform; + animation-name: olympia-confetti-flutter; + animation-timing-function: linear; + animation-iteration-count: infinite; +} + +.olympia-confetti.circle { + width: 8px; + height: 8px; + border-radius: 50%; +} + +.olympia-confetti.square { + width: 8px; + height: 8px; +} + +.olympia-confetti.triangle { + width: 10px; + height: 10px; + clip-path: polygon(50% 0%, 0% 100%, 100% 100%); } @keyframes olympia-fall { - 0% { - transform: translateY(-10vh) rotate(var(--start-rot, 0deg)); - opacity: 0; - } - 10% { - opacity: 1; - } - 85% { - opacity: 1; - } - 100% { - transform: translateY(110vh) rotate(var(--end-rot, 360deg)); - opacity: 0; - } + 0% { transform: translateY(-10vh); } + 100% { transform: translateY(110vh); } } -@keyframes olympia-confetti-fall { - 0% { - transform: translateY(-5vh) rotateX(0deg) rotateY(0deg); - opacity: 0; - } - 5% { - opacity: 1; - } - 90% { - opacity: 1; - } - 100% { - transform: translateY(105vh) rotateX(720deg) rotateY(360deg); - opacity: 0; - } +@keyframes olympia-sway { + 0% { transform: rotate(-25deg) translateX(-20px); } + 100% { transform: rotate(25deg) translateX(20px); } +} + +@keyframes olympia-tumble-3d { + 0% { transform: rotate3d(calc(var(--rot-x) + 0.001), calc(var(--rot-y) + 0.001), calc(var(--rot-z) + 0.001), 0deg); } + 100% { transform: rotate3d(calc(var(--rot-x) + 0.001), calc(var(--rot-y) + 0.001), calc(var(--rot-z) + 0.001), 360deg); } +} + +@keyframes olympia-confetti-flutter { + 0% { + transform: rotate3d(var(--rx, 1), var(--ry, 1), var(--rz, 0), 0deg); + } + 100% { + transform: rotate3d(var(--rx, 1), var(--ry, 1), var(--rz, 0), var(--rot-dir, 360deg)); + } } diff --git a/Jellyfin.Plugin.Seasonals/Web/olympia.js b/Jellyfin.Plugin.Seasonals/Web/olympia.js index 0cccff6..4d6dee7 100644 --- a/Jellyfin.Plugin.Seasonals/Web/olympia.js +++ b/Jellyfin.Plugin.Seasonals/Web/olympia.js @@ -60,61 +60,187 @@ function createOlympia() { const useRandomDuration = enableDifferentDuration !== false; - const activeItems = ['gold', 'silver', 'bronze', 'torch']; + const activeItems = ['gold', 'silver', 'bronze', 'torch', 'rings_blue', 'rings_yellow', 'rings_black', 'rings_green', 'rings_red']; for (let i = 0; i < finalCount; i++) { let symbol = document.createElement('div'); const randomItem = activeItems[Math.floor(Math.random() * activeItems.length)]; + const isRing = randomItem.startsWith('rings'); + const isMedal = ['gold', 'silver', 'bronze'].includes(randomItem); + symbol.className = `olympia-symbol olympia-${randomItem}`; - let img = document.createElement('img'); - img.src = `../Seasonals/Resources/olympia_images/${randomItem}.png`; - img.onerror = function() { - this.style.display = 'none'; - this.parentElement.innerHTML = getOlympiaEmojiFallback(randomItem); - }; - symbol.appendChild(img); + // Create inner div for sway/rotation + let innerDiv = document.createElement('div'); + innerDiv.className = 'olympia-inner'; + let img = null; - const leftPos = Math.random() * 100; - const delaySeconds = Math.random() * 10; - - let durationSeconds = 8; - if (useRandomDuration) { - durationSeconds = Math.random() * 5 + 6; // 6 to 11 seconds + if (isRing) { + const ringColorMap = { + 'rings_blue': '#0081C8', + 'rings_yellow': '#FCB131', + 'rings_black': '#000000', + 'rings_green': '#00A651', + 'rings_red': '#EE334E' + }; + let ringDiv = document.createElement('div'); + ringDiv.className = 'olympia-ring-css'; + ringDiv.style.setProperty('--ring-color', ringColorMap[randomItem]); + innerDiv.appendChild(ringDiv); + + // Add a 3D flip animation for rings and medals + const spinReverse = Math.random() > 0.5 ? 'reverse' : 'normal'; + innerDiv.style.animation = `olympia-tumble-3d ${Math.random() * 4 + 4}s linear infinite ${spinReverse}`; + + // Random 3D Rotation Axis for Tumbling + innerDiv.style.setProperty('--rot-x', (Math.random() * 2 - 1).toFixed(2)); + innerDiv.style.setProperty('--rot-y', (Math.random() * 2 - 1).toFixed(2)); + innerDiv.style.setProperty('--rot-z', (Math.random() * 2 - 1).toFixed(2)); + } else { + img = document.createElement('img'); + let imgName = randomItem; + if (isMedal) { + imgName = `${randomItem}_coin.gif`; + } else { + imgName = `${randomItem}.png`; + } + img.src = `../Seasonals/Resources/olympic_assets/${imgName}`; + img.onerror = function() { + this.style.display = 'none'; + this.parentElement.innerHTML = getOlympiaEmojiFallback(randomItem); + }; + innerDiv.appendChild(img); + + if (isMedal) { + innerDiv.style.animation = `olympia-flip-3d ${Math.random() * 4 + 3}s linear infinite`; + } else { + // Torch sways, medals flip + const swayDur = Math.random() * 2 + 2; // 2 to 4s + const swayDir = Math.random() > 0.5 ? 'normal' : 'reverse'; + innerDiv.style.animation = `olympia-sway ${swayDur}s ease-in-out infinite alternate ${swayDir}`; + } } - const startRot = Math.random() * 360; - symbol.style.setProperty('--start-rot', `${startRot}deg`); - symbol.style.setProperty('--end-rot', `${startRot + (Math.random() > 0.5 ? 360 : -360)}deg`); + symbol.appendChild(innerDiv); - symbol.style.left = `${leftPos}vw`; - symbol.style.animationDuration = `${durationSeconds}s`; + const leftPos = Math.random() * 95; + const delaySeconds = Math.random() * 10; + + // Depth logic for medals and rings + const depth = Math.random(); + const scale = 0.8 + depth * 0.4; // 0.8 to 1.2 + const zIndex = Math.floor(depth * 30) + 10; + + if (img) { + img.style.transform = `scale(${scale})`; + } else { + innerDiv.firstChild.style.transform = `scale(${scale})`; + } + symbol.style.zIndex = zIndex; + + let durationSeconds = 8; + if (useRandomDuration) { + durationSeconds = (1 - depth) * 5 + 6 + Math.random() * 4; + } + + symbol.style.animation = `olympia-fall ${durationSeconds}s linear infinite`; symbol.style.animationDelay = `${delaySeconds}s`; - + symbol.style.left = `${leftPos}vw`; + container.appendChild(symbol); } - // Olympic Ring Colors + // Olympic Torches (Fixed at bottom corners, symmetrically rotated inward) + // Generate one random inward rotation (10 to 25 deg) for both to share + const sharedTilt = Math.random() * 15 + 10; + + const createTorch = (isLeft) => { + const torch = document.createElement('div'); + torch.className = 'olympia-flame'; + + if (isLeft) { + torch.style.left = '5vw'; + // Lean right, face normal + torch.style.transform = `rotate(${sharedTilt}deg) scaleX(1)`; + } else { + torch.style.right = '5vw'; + // Lean left, mirror image + torch.style.transform = `rotate(-${sharedTilt}deg) scaleX(-1)`; + } + + let torchImg = document.createElement('img'); + torchImg.src = `../Seasonals/Resources/olympic_assets/torch.gif`; + torchImg.style.height = '25vh'; + torchImg.style.objectFit = 'contain'; + torchImg.onerror = function() { + this.style.display = 'none'; + }; + torch.appendChild(torchImg); + container.appendChild(torch); + }; + + createTorch(true); + createTorch(false); + + // Olympic Ring Colors (Carnival Config) const confettiColors = ['#0081C8', '#FCB131', '#000000', '#00A651', '#EE334E']; const confettiCount = isMobile ? 30 : 60; for (let i = 0; i < confettiCount; i++) { + let wrapper = document.createElement('div'); + wrapper.className = 'olympia-confetti-wrapper'; + + let leftPos = Math.random() * 100; + wrapper.style.left = `${leftPos}vw`; + + let fallDuration = Math.random() * 3 + 4; // 4 to 7 seconds to fall + wrapper.style.animationDuration = `${fallDuration}s`; + wrapper.style.animationDelay = `-${Math.random() * fallDuration}s`; // Negative delay so it distributes perfectly immediately + + let swayWrapper = document.createElement('div'); + swayWrapper.className = 'olympia-confetti-sway'; + let swayDuration = Math.random() * 2 + 1.5; // 1.5s to 3.5s + swayWrapper.style.animationDuration = `${swayDuration}s`; + let swayAmount = Math.random() * 30 + 30; // 30px to 60px + swayWrapper.style.setProperty('--sway-amount', `${swayAmount}px`); + let initSwayDelay = Math.random() * swayDuration; + swayWrapper.style.animationDelay = `-${initSwayDelay}s`; + let confetti = document.createElement('div'); confetti.className = 'olympia-confetti'; const color = confettiColors[Math.floor(Math.random() * confettiColors.length)]; confetti.style.backgroundColor = color; - const leftPos = Math.random() * 100; - const delaySeconds = Math.random() * 8; - const duration = Math.random() * 3 + 5; + // Random shape + const shape = Math.random(); + if (shape > 0.66) { + confetti.classList.add('circle'); + const size = Math.random() * 5 + 5; + confetti.style.width = `${size}px`; + confetti.style.height = `${size}px`; + } else if (shape > 0.33) { + confetti.classList.add('rect'); + const width = Math.random() * 4 + 4; + const height = Math.random() * 5 + 8; + confetti.style.width = `${width}px`; + confetti.style.height = `${height}px`; + } else { + confetti.classList.add('triangle'); + } + + // Random 3D Rotation for flutter + confetti.style.setProperty('--rx', Math.random().toFixed(2)); + confetti.style.setProperty('--ry', Math.random().toFixed(2)); + confetti.style.setProperty('--rz', (Math.random() * 0.5).toFixed(2)); + confetti.style.setProperty('--rot-dir', `${(Math.random() > 0.5 ? 1 : -1) * 360}deg`); + let rotateDuration = Math.random() * 0.8 + 0.4; + confetti.style.animationDuration = `${rotateDuration}s`; - confetti.style.left = `${leftPos}vw`; - confetti.style.animationDuration = `${duration}s`; - confetti.style.animationDelay = `${delaySeconds}s`; - - container.appendChild(confetti); + swayWrapper.appendChild(confetti); + wrapper.appendChild(swayWrapper); + container.appendChild(wrapper); } } @@ -122,8 +248,7 @@ function getOlympiaEmojiFallback(type) { if (type === 'gold') return '🥇'; if (type === 'silver') return '🥈'; if (type === 'bronze') return '🥉'; - if (type === 'torch') return '🔥'; - return ''; + return ''; // Rings will be handled by CSS or actual image } function initializeOlympia() {