Add seasonal effects for Pi Day, Pride, Rain, and Storm; enhance existing styles
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 45s

- Introduced new CSS and JS files for Pi Day, Pride, Rain, and Storm effects.
- Updated existing seasonal styles (e.g., Halloween, Hearts, Resurrection) to improve performance with 'contain: layout paint'.
- Enhanced animations for seasonal effects, including adjustments to keyframes and element creation logic.
- Added configuration options for new effects in the main seasonals.js file.
- Updated test-site.html to include new seasonal options in the dropdown.
This commit is contained in:
CodeDevMLH
2026-02-23 01:31:52 +01:00
parent 63ec6d5e52
commit e4b3a132b1
31 changed files with 1445 additions and 75 deletions

View File

@@ -8,6 +8,7 @@
height: 100%;
pointer-events: none;
z-index: 10;
contain: layout paint;
}
.leaf {
@@ -44,7 +45,7 @@
}
100% {
top: 100%;
top: 110%;
}
}
@@ -54,7 +55,7 @@
}
100% {
top: 100%;
top: 110%;
}
}

View File

@@ -9,13 +9,14 @@
pointer-events: none;
z-index: 10;
perspective: 600px;
contain: layout paint;
}
.carnival-wrapper {
position: fixed;
z-index: 15;
top: -20px;
will-change: top;
will-change: transform;
animation-name: carnival-fall;
animation-timing-function: linear;
animation-iteration-count: 1;
@@ -59,10 +60,10 @@
@keyframes carnival-fall {
0% {
top: -10%;
transform: translateY(0);
}
100% {
top: 110%;
transform: translateY(120vh);
}
}

View File

@@ -8,6 +8,7 @@
height: 100%;
pointer-events: none;
z-index: 1000;
contain: layout paint;
}
/* Petals */
@@ -45,7 +46,7 @@
@keyframes cherryblossom-fall {
0% { top: -10%; }
100% { top: 100%; }
100% { top: 110%; }
}
@keyframes cherryblossom-sway {

View File

@@ -8,6 +8,7 @@
height: 100%;
pointer-events: none;
z-index: 10;
contain: layout paint;
}
.christmas {
@@ -37,7 +38,7 @@
}
100% {
top: 100%;
top: 110%;
}
}
@@ -61,7 +62,7 @@
}
100% {
top: 100%;
top: 110%;
}
}

View File

@@ -0,0 +1,35 @@
.earthday-container {
position: fixed;
bottom: 0;
left: 0;
width: 100vw;
height: 15vh;
pointer-events: none;
z-index: 1000;
overflow: hidden;
}
.earthday-meadow {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
transform-origin: bottom;
animation: grow-meadow 3s cubic-bezier(0.1, 0.8, 0.2, 1) forwards;
}
@keyframes grow-meadow {
0% { transform: translateY(100%); opacity: 0; }
100% { transform: translateY(0); opacity: 0.95; }
}
.earthday-sway {
transform-origin: bottom center;
animation: sway-grass 4s ease-in-out infinite alternate;
}
@keyframes sway-grass {
0% { transform: skewX(-2deg); }
100% { transform: skewX(2deg); }
}

View File

@@ -0,0 +1,126 @@
// 1. Read Configuration
const config = window.SeasonalsPluginConfig?.EarthDay || {};
const enabled = config.EnableEarthDay !== undefined ? config.EnableEarthDay : true;
const vineCount = config.VineCount || 4;
let msgPrinted = false;
// 2. Toggle Function
function toggleEarthDay() {
const container = document.querySelector('.earthday-container');
if (!container) 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) {
container.style.display = 'none';
if (!msgPrinted) {
console.log('EarthDay hidden');
msgPrinted = true;
}
} else {
container.style.display = 'block';
if (msgPrinted) {
console.log('EarthDay visible');
msgPrinted = false;
}
}
}
// 3. MutationObserver
const observer = new MutationObserver(toggleEarthDay);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
// 4. Element Creation
function createElements() {
const container = document.querySelector('.earthday-container') || document.createElement('div');
if (!document.querySelector('.earthday-container')) {
container.className = 'earthday-container';
container.setAttribute('aria-hidden', 'true');
document.body.appendChild(container);
}
const w = window.innerWidth;
const hSVG = Math.floor(window.innerHeight * 0.15) || 100; // 15vh roughly
let paths = '';
// Generate Grass
for (let i = 0; i < 400; i++) {
const x = Math.random() * w;
const h = hSVG * 0.2 + Math.random() * (hSVG * 0.8);
const cY = hSVG - h;
const bend = x + (Math.random() * 40 - 20);
const color = Math.random() > 0.5 ? '#2E8B57' : '#3CB371';
const width = 1 + Math.random() * 2;
paths += `<path d="M ${x} ${hSVG} Q ${bend} ${cY+hSVG*0.2} ${bend} ${cY}" stroke="${color}" stroke-width="${width}" fill="none"/>`;
}
// Generate Flowers
const colors = ['#FF69B4', '#FFD700', '#87CEFA', '#FF4500', '#BA55D3', '#FFA500', '#FF1493'];
const flowerCount = Math.max(10, vineCount * 15);
for (let i = 0; i < flowerCount; i++) {
const x = 10 + Math.random() * (w - 20);
const y = hSVG * 0.1 + Math.random() * (hSVG * 0.5);
const col = colors[Math.floor(Math.random() * colors.length)];
paths += `<path d="M ${x} ${hSVG} Q ${x - 5 + Math.random() * 10} ${y+15} ${x} ${y}" stroke="#006400" stroke-width="1.5" fill="none"/>`;
const r = 2 + Math.random() * 1.5;
paths += `<circle cx="${x-r}" cy="${y-r}" r="${r}" fill="${col}"/>`;
paths += `<circle cx="${x+r}" cy="${y-r}" r="${r}" fill="${col}"/>`;
paths += `<circle cx="${x-r}" cy="${y+r}" r="${r}" fill="${col}"/>`;
paths += `<circle cx="${x+r}" cy="${y+r}" r="${r}" fill="${col}"/>`;
paths += `<circle cx="${x}" cy="${y}" r="${r*0.7}" fill="#FFF8DC"/>`;
}
const svgContent = `
<svg class="earthday-meadow" viewBox="0 0 ${w} ${hSVG}" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
<g class="earthday-sway">
${paths}
</g>
</svg>
`;
container.innerHTML = svgContent;
}
// 5. Responsive Resize
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
const handleResize = debounce(() => {
const container = document.querySelector('.earthday-container');
if (container) {
container.innerHTML = '';
createElements();
}
}, 250);
window.addEventListener('resize', handleResize);
// 6. Initialization
function initializeEarthDay() {
if (!enabled) return;
createElements();
toggleEarthDay();
}
initializeEarthDay();

View File

@@ -8,6 +8,7 @@
height: 100%;
pointer-events: none;
z-index: 10;
contain: layout paint;
}
.hopping-rabbit {
@@ -58,7 +59,7 @@
}
100% {
top: 100%;
top: 110%;
}
}
@@ -82,7 +83,7 @@
}
100% {
top: 100%;
top: 110%;
}
}

View File

@@ -0,0 +1,43 @@
.eurovision-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 1000;
overflow: hidden;
contain: layout paint;
}
.music-note-wrapper {
position: absolute;
left: 0;
/* initial top will be set via JS */
opacity: 0;
animation: move-right linear infinite;
will-change: transform, opacity;
}
.music-note {
display: block;
font-size: 2rem;
color: rgba(255, 255, 255, 0.9);
text-shadow: 0 0 8px rgba(255, 255, 255, 0.6);
animation: sway ease-in-out infinite alternate;
will-change: transform;
}
/* Horizontal scroll from left to right */
@keyframes move-right {
0% { transform: translateX(-10vw); opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { transform: translateX(110vw); opacity: 0; }
}
/* Sine-wave style vertical bouncing for the note itself */
@keyframes sway {
0% { transform: translateY(-30px); }
100% { transform: translateY(30px); }
}

View File

@@ -0,0 +1,105 @@
// 1. Read Configuration
const config = window.SeasonalsPluginConfig?.Eurovision || {};
const enabled = config.EnableEurovision !== undefined ? config.EnableEurovision : true;
const elementCount = config.SymbolCount || 25;
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true;
const enableColorfulNotes = config.EnableColorfulNotes !== undefined ? config.EnableColorfulNotes : true;
const eurovisionColorsStr = config.EurovisionColors || '#ff0026ff, #17a6ffff, #32d432ff, #FFD700, #f0821bff, #f826f8ff';
const glowSize = config.EurovisionGlowSize !== undefined ? config.EurovisionGlowSize : 2;
let msgPrinted = false;
// 2. Toggle Function
function toggleEurovision() {
const container = document.querySelector('.eurovision-container');
if (!container) 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) {
container.style.display = 'none';
if (!msgPrinted) {
console.log('Eurovision hidden');
msgPrinted = true;
}
} else {
container.style.display = 'block';
if (msgPrinted) {
console.log('Eurovision visible');
msgPrinted = false;
}
}
}
// 3. MutationObserver
const observer = new MutationObserver(toggleEurovision);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
// 4. Element Creation
function createElements() {
const container = document.querySelector('.eurovision-container') || document.createElement('div');
if (!document.querySelector('.eurovision-container')) {
container.className = 'eurovision-container';
container.setAttribute('aria-hidden', 'true');
document.body.appendChild(container);
}
const notesSymbols = ['♪', '♫', '♬', '♭', '♮', '♯', '𝄞', '𝄢'];
const pColors = eurovisionColorsStr.split(',').map(s => s.trim()).filter(s => s);
for (let i = 0; i < elementCount; i++) {
const wrapper = document.createElement('div');
wrapper.className = 'music-note-wrapper';
const note = document.createElement('span');
note.className = 'music-note';
note.textContent = notesSymbols[Math.floor(Math.random() * notesSymbols.length)];
wrapper.appendChild(note);
wrapper.style.top = `${Math.random() * 90}vh`;
const minMoveDur = 10;
const maxMoveDur = 25;
const moveDur = enableDifferentDuration
? minMoveDur + Math.random() * (maxMoveDur - minMoveDur)
: (minMoveDur + maxMoveDur) / 2;
wrapper.style.animationDuration = `${moveDur}s`;
wrapper.style.animationDelay = `${Math.random() * 15}s`;
const minSwayDur = 1;
const maxSwayDur = 3;
const swayDur = minSwayDur + Math.random() * (maxSwayDur - minSwayDur);
note.style.animationDuration = `${swayDur}s`;
note.style.animationDelay = `${Math.random() * 2}s`;
note.style.fontSize = `${Math.random() * 1.5 + 1.5}rem`;
if (enableColorfulNotes && pColors.length > 0) {
note.style.color = pColors[Math.floor(Math.random() * pColors.length)];
note.style.textShadow = `0 0 ${glowSize}px ${note.style.color}`;
} else {
note.style.color = `rgba(255, 255, 255, 0.9)`;
note.style.textShadow = `0 0 ${glowSize}px rgba(255, 255, 255, 0.6)`;
}
container.appendChild(wrapper);
}
}
// 5. Initialization
function initializeEurovision() {
if (!enabled) return;
createElements();
toggleEurovision();
}
initializeEurovision();

View File

@@ -7,6 +7,7 @@
height: 100%;
pointer-events: none;
z-index: 10;
contain: layout paint;
}
.rocket-trail {

View File

@@ -8,6 +8,7 @@
height: 100%;
pointer-events: none;
z-index: 10;
contain: layout paint;
}
.halloween {
@@ -34,11 +35,11 @@
@-webkit-keyframes halloween-fall {
0% {
bottom: -10%
bottom: -10%;
}
100% {
bottom: 100%
bottom: 110%;
}
}
@@ -58,11 +59,11 @@
@keyframes halloween-fall {
0% {
bottom: -10%
bottom: -10%;
}
100% {
bottom: 100%
bottom: 110%;
}
}

View File

@@ -8,6 +8,7 @@
height: 100%;
pointer-events: none;
z-index: 10;
contain: layout paint;
}
.heart {
@@ -32,11 +33,11 @@
@-webkit-keyframes heart-fall {
0% {
bottom: -10%
bottom: -10%;
}
100% {
bottom: 100%
bottom: 110%;
}
}
@@ -56,11 +57,11 @@
@keyframes heart-fall {
0% {
bottom: -10%
bottom: -10%;
}
100% {
bottom: 100%
bottom: 110%;
}
}

View File

@@ -0,0 +1,11 @@
.piday-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 1000;
overflow: hidden;
contain: layout paint;
}

View File

@@ -0,0 +1,165 @@
// 1. Read Configuration
const config = window.SeasonalsPluginConfig?.PiDay || {};
const enabled = config.EnablePiDay !== undefined ? config.EnablePiDay : true;
const maxTrails = config.SymbolCount || 25; // Directly mapped, smaller default
let msgPrinted = false;
let isHidden = false;
// 2. Toggle Function
function togglePiDay() {
const container = document.querySelector('.piday-container');
if (!container) 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) {
if (!isHidden) {
container.style.display = 'none';
isHidden = true;
if (!msgPrinted) {
console.log('PiDay hidden');
msgPrinted = true;
}
}
} else {
if (isHidden) {
container.style.display = 'block';
isHidden = false;
if (msgPrinted) {
console.log('PiDay visible');
msgPrinted = false;
}
}
}
}
// 3. MutationObserver
const observer = new MutationObserver(togglePiDay);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
// 4. Element Creation
function createElements() {
const container = document.querySelector('.piday-container') || document.createElement('div');
if (!document.querySelector('.piday-container')) {
container.className = 'piday-container';
container.setAttribute('aria-hidden', 'true');
document.body.appendChild(container);
}
const canvas = document.createElement('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style.display = 'block';
container.appendChild(canvas);
const ctx = canvas.getContext('2d');
// const chars = '0123456789πABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
const chars = '0123456789'.split('');
const fontSize = 18;
class Trail {
constructor() {
this.reset();
this.y = Math.random() * -100; // Allow initial staggered start
}
reset() {
const cols = Math.floor(canvas.width / fontSize);
this.x = Math.floor(Math.random() * cols);
this.y = -Math.round(Math.random() * 20);
this.speed = 0.5 + Math.random() * 0.5;
this.len = 10 + Math.floor(Math.random() * 20);
this.chars = [];
for(let i=0; i<this.len; i++) {
this.chars.push(chars[Math.floor(Math.random() * chars.length)]);
}
}
update() {
const oldY = Math.floor(this.y);
this.y += this.speed;
const newY = Math.floor(this.y);
// If crossed a full vertical unit, push a new character and pop the old one to preserve screen positions
if (newY > oldY) {
this.chars.unshift(chars[Math.floor(Math.random() * chars.length)]);
this.chars.pop();
}
// Randomly mutate some characters (heads mutate faster)
for (let i = 0; i < this.len; i++) {
const chance = i < 3 ? 0.90 : 0.98;
if (Math.random() > chance) {
this.chars[i] = chars[Math.floor(Math.random() * chars.length)];
}
}
if (this.y - this.len > Math.ceil(canvas.height / fontSize)) {
this.reset();
}
}
draw(ctx) {
const headY = Math.floor(this.y);
for (let i = 0; i < this.len; i++) {
const charY = headY - i;
if (charY < 0 || charY * fontSize > canvas.height + fontSize) continue;
const ratio = i / this.len;
const alpha = 1 - ratio;
if (i === 0) {
ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`;
ctx.shadowBlur = 8;
ctx.shadowColor = '#0F0';
} else if (i === 1) {
ctx.fillStyle = `rgba(150, 255, 150, ${alpha})`;
ctx.shadowBlur = 4;
ctx.shadowColor = '#0F0';
} else {
ctx.fillStyle = `rgba(0, 255, 0, ${alpha * 0.8})`;
ctx.shadowBlur = 0;
}
ctx.fillText(this.chars[i], this.x * fontSize + fontSize/2, charY * fontSize);
}
}
}
const trails = [];
for(let i=0; i<maxTrails; i++) trails.push(new Trail());
function loop() {
if (isHidden) return; // Pause drawing when hidden
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.font = 'bold ' + fontSize + 'px monospace';
ctx.textAlign = 'center';
for (const t of trails) {
t.update();
t.draw(ctx);
}
}
if (window.pidayInterval) clearInterval(window.pidayInterval);
window.pidayInterval = setInterval(loop, 50);
window.addEventListener('resize', () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
});
}
// 5. Initialization
function initializePiDay() {
if (!enabled) return;
createElements();
togglePiDay();
}
initializePiDay();

View File

@@ -0,0 +1,32 @@
.pride-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 9999;
overflow: hidden;
contain: layout paint;
}
.pride-heart {
position: absolute;
bottom: -50px;
animation: pride-rise ease-in infinite;
will-change: transform;
}
@keyframes pride-rise {
0% { transform: translateY(0) scale(0.8); opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { transform: translateY(-115vh) scale(1.2); opacity: 0; }
}
/* Coloring the Jellyfin Header */
.skinHeader.pride-header {
background: linear-gradient(90deg, #E40303, #FF8C00, #FFED00, #008026, #24408E, #732982) !important;
}

View File

@@ -0,0 +1,88 @@
// 1. Read Configuration
const config = window.SeasonalsPluginConfig?.Pride || {};
const enabled = config.EnablePride !== undefined ? config.EnablePride : true;
const elementCount = config.HeartCount || 20;
const heartSize = config.HeartSize || 1.5;
const colorHeader = config.ColorHeader !== undefined ? config.ColorHeader : true;
let msgPrinted = false;
// 2. Toggle Function
// Hides the effect when a video player, trailer (in full width mode), dashboard, or user menu is active.
function togglePride() {
const container = document.querySelector('.pride-container');
if (!container) 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) {
container.style.display = 'none';
if (!msgPrinted) {
console.log('Pride hidden');
msgPrinted = true;
}
} else {
container.style.display = 'block';
if (msgPrinted) {
console.log('Pride visible');
msgPrinted = false;
}
}
}
// 3. MutationObserver
// Watches the DOM for changes so the effect can auto-hide/show.
const observer = new MutationObserver(togglePride);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
// 4. Element Creation
// Create and append your animated elements to the container.
function createElements() {
const container = document.querySelector('.pride-container') || document.createElement('div');
if (!document.querySelector('.pride-container')) {
container.className = 'pride-container';
container.setAttribute('aria-hidden', 'true');
document.body.appendChild(container);
}
if (colorHeader) {
const header = document.querySelector('.skinHeader');
if (header) {
header.classList.add('pride-header');
}
}
const heartEmojis = ['❤️', '🧡', '💛', '💚', '💙', '💜'];
for (let i = 0; i < elementCount; i++) {
const el = document.createElement('div');
el.className = 'pride-heart';
el.innerText = heartEmojis[Math.floor(Math.random() * heartEmojis.length)];
el.style.fontSize = `${heartSize}rem`;
el.style.left = `${Math.random() * 100}vw`;
el.style.animationDuration = `${5 + Math.random() * 5}s`;
el.style.animationDelay = `${Math.random() * 5}s`;
el.style.marginLeft = `${(Math.random() - 0.5) * 100}px`;
container.appendChild(el);
}
}
// 5. Initialization
function initializePride() {
if (!enabled) return;
createElements();
togglePride();
}
initializePride();

View File

@@ -0,0 +1,26 @@
.rain-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 1000;
overflow: hidden;
contain: layout paint;
}
.raindrop-pure {
position: absolute;
width: 2px;
height: 40px;
background: linear-gradient(to bottom, rgba(200, 200, 255, 0), rgba(200, 200, 255, 0.7));
transform-origin: bottom;
}
@keyframes pure-rain {
0% { transform: translateY(-30vh) translateX(0) rotate(20deg); opacity: 0; }
5% { opacity: 1; }
95% { opacity: 1; }
100% { transform: translateY(110vh) translateX(-40vh) rotate(20deg); opacity: 0; }
}

View File

@@ -0,0 +1,77 @@
// 1. Read Configuration
const config = window.SeasonalsPluginConfig?.Rain || {};
const enabled = config.EnableRain !== undefined ? config.EnableRain : true;
const isMobile = window.innerWidth <= 768;
const elementCount = isMobile ? (config.RaindropCountMobile || 150) : (config.RaindropCount || 300);
const rainSpeed = config.RainSpeed || 1.0;
let msgPrinted = false;
// 2. Toggle Function
function toggleRain() {
const container = document.querySelector('.rain-container');
if (!container) 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) {
container.style.display = 'none';
if (!msgPrinted) {
console.log('Rain hidden');
msgPrinted = true;
}
} else {
container.style.display = 'block';
if (msgPrinted) {
console.log('Rain visible');
msgPrinted = false;
}
}
}
// 3. MutationObserver
const observer = new MutationObserver(toggleRain);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
// 4. Element Creation
function createElements() {
const container = document.querySelector('.rain-container') || document.createElement('div');
if (!document.querySelector('.rain-container')) {
container.className = 'rain-container';
container.setAttribute('aria-hidden', 'true');
document.body.appendChild(container);
}
for (let i = 0; i < elementCount; i++) {
const drop = document.createElement('div');
drop.className = 'raindrop-pure';
drop.style.left = `${Math.random() * 140}vw`;
drop.style.top = `${-20 - Math.random() * 50}vh`;
const duration = (0.5 + Math.random() * 0.5) / (rainSpeed || 1);
drop.style.animation = `pure-rain ${duration}s linear infinite`;
drop.style.animationDelay = `${Math.random() * 2}s`;
drop.style.opacity = Math.random() * 0.5 + 0.3;
container.appendChild(drop);
}
}
// 5. Initialization
function initializeRain() {
if (!enabled) return;
createElements();
toggleRain();
}
initializeRain();

View File

@@ -8,6 +8,7 @@
height: 100%;
pointer-events: none;
z-index: 10;
contain: layout paint;
}
.resurrection-symbol {
@@ -43,7 +44,7 @@
}
100% {
top: 105%;
top: 110%;
}
}

View File

@@ -78,6 +78,36 @@ const ThemeConfigs = {
js: '../Seasonals/Resources/cherryblossom.js',
containerClass: 'cherryblossom-container'
},
piday: {
css: '../Seasonals/Resources/piday.css',
js: '../Seasonals/Resources/piday.js',
containerClass: 'piday-container'
},
eurovision: {
css: '../Seasonals/Resources/eurovision.css',
js: '../Seasonals/Resources/eurovision.js',
containerClass: 'eurovision-container'
},
storm: {
css: '../Seasonals/Resources/storm.css',
js: '../Seasonals/Resources/storm.js',
containerClass: 'storm-container'
},
pride: {
css: '../Seasonals/Resources/pride.css',
js: '../Seasonals/Resources/pride.js',
containerClass: 'pride-container'
},
rain: {
css: '../Seasonals/Resources/rain.css',
js: '../Seasonals/Resources/rain.js',
containerClass: 'rain-container'
},
earthday: {
css: '../Seasonals/Resources/earthday.css',
js: '../Seasonals/Resources/earthday.js',
containerClass: 'earthday-container'
},
none: {
containerClass: 'none'
},

View File

@@ -8,6 +8,7 @@
top: 0;
left: 0;
z-index: 10;
contain: layout paint;
}
#snowfallCanvas {

View File

@@ -8,6 +8,7 @@
height: 100%;
pointer-events: none;
z-index: 10;
contain: layout paint;
}
.snowflake {
@@ -37,7 +38,7 @@
}
100% {
top: 100%;
top: 110%;
}
}
@@ -61,7 +62,7 @@
}
100% {
top: 100%;
top: 110%;
}
}

View File

@@ -8,6 +8,7 @@
top: 0;
left: 0;
z-index: 10;
contain: layout paint;
}
#snowfallCanvas {

View File

@@ -8,6 +8,7 @@
height: 100vh;
pointer-events: none;
z-index: 1000;
contain: layout paint;
}
/* Pollen */
@@ -37,7 +38,7 @@
opacity: 0;
}
/* Grass */
/* Grass Container (Wrapper) */
.spring-grass-container {
position: absolute;
bottom: 0;
@@ -45,8 +46,10 @@
width: 100%;
height: 80px;
pointer-events: none;
transform-origin: bottom;
}
/* HTML Grass Overlayer */
.spring-grass {
position: absolute;
bottom: 0;
@@ -61,6 +64,45 @@
animation-iteration-count: infinite;
}
@keyframes spring-grass-sway {
0% { transform: rotate(0deg); }
50% { transform: rotate(8deg); }
100% { transform: rotate(0deg); }
}
/* SVG Meadow Layer */
.spring-meadow-layer {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
animation: spring-grow-meadow 3s cubic-bezier(0.1, 0.8, 0.2, 1) forwards;
}
.spring-meadow {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
}
@keyframes spring-grow-meadow {
0% { transform: translateY(100%); opacity: 0; }
100% { transform: translateY(0); opacity: 0.95; }
}
.spring-sway {
transform-origin: bottom center;
animation: spring-meadow-sway 4s ease-in-out infinite alternate;
}
@keyframes spring-meadow-sway {
0% { transform: skewX(-2deg); }
100% { transform: skewX(2deg); }
}
/* Birds */
.spring-bird {
position: static !important;
@@ -123,6 +165,11 @@
top: 0; left: 0;
}
.spring-align-y {
width: 100%; height: 100%;
will-change: transform;
}
.spring-mirror-wrapper {
width: 100%; height: 100%;
will-change: transform;
@@ -144,11 +191,7 @@
100% { opacity: 0; transform: rotate(var(--beam-rotation, 45deg)) scaleX(0.8); }
}
@keyframes spring-grass-sway {
0% { transform: rotate(0deg); }
50% { transform: rotate(8deg); }
100% { transform: rotate(0deg); }
}
/* Wrapper animations (Flight across screen) */
@keyframes spring-fly-right-wrapper {
@@ -163,8 +206,8 @@
/* Vertical Drift for Sloped Flight */
@keyframes spring-vertical-drift {
0% { top: var(--start-y, 10%); }
100% { top: var(--end-y, 10%); }
0% { transform: translateY(var(--start-y, 10vh)); }
100% { transform: translateY(var(--end-y, 10vh)); }
}
/* Inner animations (Bobbing/Fluttering) */

View File

@@ -121,30 +121,80 @@ function createGrass(container) {
grassContainer.innerHTML = '';
const bladeCount = window.innerWidth / 3;
let pathsBg = '';
let pathsFg = '';
const w = window.innerWidth;
const hSVG = 80;
// 1. Generate Straight Line HTML-Style Grass (converted to SVG Paths)
const bladeCount = w / 5; // Reduced from w/3
for (let i = 0; i < bladeCount; i++) {
const blade = document.createElement('div');
blade.classList.add('spring-grass');
// MARK: GRASS HEIGHT
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 x = i * 5 + Math.random() * 3;
const hue = 100 + Math.random() * 40;
blade.style.backgroundColor = `hsl(${hue}, 60%, 40%)`;
const color = `hsl(${hue}, 60%, 40%)`;
// Random z-index to interleave with Ladybug (1002)
// Values: 1001 (behind) or 1003 (front)
const z = Math.random() > 0.5 ? 1001 : 1003;
blade.style.zIndex = z;
grassContainer.appendChild(blade);
const line = `<line x1="${x}" y1="${hSVG}" x2="${x}" y2="${hSVG - height}" stroke="${color}" stroke-width="2" />`;
// ~66% chance to be in background (1001), 33% foreground (1003)
if (Math.random() > 0.33) pathsBg += line; else pathsFg += line;
}
// 2. Generate Curved Earth-Day Style Grass
for (let i = 0; i < 200; i++) { // Reduced from 400
const x = Math.random() * w;
const h = 20 + Math.random() * 50;
const cY = hSVG - h;
const bend = x + (Math.random() * 40 - 20);
const color = Math.random() > 0.5 ? '#4caf50' : '#45a049';
const width = 1 + Math.random() * 2;
const path = `<path d="M ${x} ${hSVG} Q ${bend} ${cY+20} ${bend} ${cY}" stroke="${color}" stroke-width="${width}" fill="none"/>`;
// ~66% chance to be in background (1001), 33% foreground (1003)
if (Math.random() > 0.33) pathsBg += path; else pathsFg += path;
}
// 3. Generate SVG Flowers
const colors = ['#FF69B4', '#FFD700', '#87CEFA', '#FF4500', '#BA55D3', '#FFA500', '#FF1493', '#FFFFFF'];
const flowerCount = Math.floor(w / 40); // Reduced from w/30
for (let i = 0; i < flowerCount; i++) {
const x = 10 + Math.random() * (w - 20);
const y = 10 + Math.random() * 40; // 10-50px from top of SVG
const col = colors[Math.floor(Math.random() * colors.length)];
let flower = '';
// Stem
flower += `<path d="M ${x} ${hSVG} Q ${x - 5 + Math.random() * 10} ${y+15} ${x} ${y}" stroke="#2e7d32" stroke-width="1.5" fill="none"/>`;
// Petals
const r = 2 + Math.random() * 1.5;
flower += `<circle cx="${x-r}" cy="${y-r}" r="${r}" fill="${col}"/>`;
flower += `<circle cx="${x+r}" cy="${y-r}" r="${r}" fill="${col}"/>`;
flower += `<circle cx="${x-r}" cy="${y+r}" r="${r}" fill="${col}"/>`;
flower += `<circle cx="${x+r}" cy="${y+r}" r="${r}" fill="${col}"/>`;
// Center
flower += `<circle cx="${x}" cy="${y}" r="${r*0.7}" fill="#FFF8DC"/>`;
// ~66% chance to be in background (1001), 33% foreground (1003)
if (Math.random() > 0.33) pathsBg += flower; else pathsFg += flower;
}
// Inject purely SVG based grass container
grassContainer.innerHTML = `
<div class="spring-meadow-layer" style="z-index: 1001;">
<svg class="spring-meadow" viewBox="0 0 ${w} ${hSVG}" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
<g class="spring-sway">
${pathsBg}
</g>
</svg>
</div>
<div class="spring-meadow-layer" style="z-index: 1003;">
<svg class="spring-meadow" viewBox="0 0 ${w} ${hSVG}" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
<g class="spring-sway" style="animation-delay: -2s;">
${pathsFg}
</g>
</svg>
</div>
`;
}
function initSpringObjects() {
@@ -205,6 +255,9 @@ function createBird(container) {
wrapper.classList.add('spring-anim-wrapper');
wrapper.classList.add('spring-bird-wrapper');
const alignY = document.createElement('div');
alignY.classList.add('spring-align-y');
const mirror = document.createElement('div');
mirror.classList.add('spring-mirror-wrapper');
@@ -218,19 +271,20 @@ function createBird(container) {
// MARK: BIRD SPEED (10-15s)
const duration = Math.random() * 5 + 10;
// MARK: BIRD HEIGHT RANGE (in %)
const startY = Math.random() * 55 + 5; // Start 5-60%
const endY = Math.random() * 55 + 5; // End 5-60%
wrapper.style.setProperty('--start-y', `${startY}%`);
wrapper.style.setProperty('--end-y', `${endY}%`);
// MARK: BIRD HEIGHT RANGE (in vh)
const startY = Math.random() * 55 + 5; // Start 5-60vh
const endY = Math.random() * 55 + 5; // End 5-60vh
alignY.style.setProperty('--start-y', `${startY}vh`);
alignY.style.setProperty('--end-y', `${endY}vh`);
if (direction === 'right') {
wrapper.style.animation = `spring-fly-right-wrapper ${duration}s linear forwards, spring-vertical-drift ${duration}s linear forwards`;
wrapper.style.animation = `spring-fly-right-wrapper ${duration}s linear forwards`;
mirror.style.transform = 'scaleX(-1)';
} else {
wrapper.style.animation = `spring-fly-left-wrapper ${duration}s linear forwards, spring-vertical-drift ${duration}s linear forwards`;
wrapper.style.animation = `spring-fly-left-wrapper ${duration}s linear forwards`;
mirror.style.transform = 'scaleX(1)';
}
alignY.style.animation = `spring-vertical-drift ${duration}s linear forwards`;
wrapper.addEventListener('animationend', (e) => {
if (e.animationName.includes('fly-')) {
@@ -240,11 +294,10 @@ function createBird(container) {
});
bird.style.animation = `spring-bob 2s ease-in-out infinite`;
wrapper.style.top = `${startY}%`;
mirror.appendChild(bird);
wrapper.appendChild(mirror);
alignY.appendChild(mirror);
wrapper.appendChild(alignY);
container.appendChild(wrapper);
}
@@ -253,6 +306,9 @@ function createButterfly(container) {
wrapper.classList.add('spring-anim-wrapper');
wrapper.classList.add('spring-butterfly-wrapper');
const alignY = document.createElement('div');
alignY.classList.add('spring-align-y');
const mirror = document.createElement('div');
mirror.classList.add('spring-mirror-wrapper');
@@ -284,12 +340,13 @@ function createButterfly(container) {
butterfly.style.animation = `spring-flutter 3s ease-in-out infinite`;
butterfly.style.animationDelay = `-${Math.random() * 3}s`;
// MARK: BUTTERFLY HEIGHT (in %)
const top = Math.random() * 35 + 30; // 30-65%
wrapper.style.top = `${top}%`;
// MARK: BUTTERFLY HEIGHT (in vh)
const top = Math.random() * 35 + 30; // 30-65vh
alignY.style.transform = `translateY(${top}vh)`;
mirror.appendChild(butterfly);
wrapper.appendChild(mirror);
alignY.appendChild(mirror);
wrapper.appendChild(alignY);
container.appendChild(wrapper);
}
@@ -298,6 +355,9 @@ function createBee(container) {
wrapper.classList.add('spring-anim-wrapper');
wrapper.classList.add('spring-bee-wrapper');
const alignY = document.createElement('div');
alignY.classList.add('spring-align-y');
const mirror = document.createElement('div');
mirror.classList.add('spring-mirror-wrapper');
@@ -323,12 +383,13 @@ function createBee(container) {
}
});
// MARK: BEE HEIGHT (in %)
const top = Math.random() * 60 + 20; // 20-80%
wrapper.style.top = `${top}%`;
// MARK: BEE HEIGHT (in vh)
const top = Math.random() * 60 + 20; // 20-80vh
alignY.style.transform = `translateY(${top}vh)`;
mirror.appendChild(bee);
wrapper.appendChild(mirror);
alignY.appendChild(mirror);
wrapper.appendChild(alignY);
container.appendChild(wrapper);
}
@@ -337,6 +398,9 @@ function createLadybugGif(container) {
wrapper.classList.add('spring-anim-wrapper');
wrapper.classList.add('spring-ladybug-wrapper');
const alignY = document.createElement('div');
alignY.classList.add('spring-align-y');
const mirror = document.createElement('div');
mirror.classList.add('spring-mirror-wrapper');
@@ -364,11 +428,12 @@ function createLadybugGif(container) {
bug.style.animation = `spring-crawl 2s ease-in-out infinite`;
wrapper.style.top = 'auto';
wrapper.style.bottom = '5px';
// Target the Ladybug to walk on the ground visually (aligning properly with the CSS/SVG grass size)
alignY.style.transform = `translateY(calc(100vh - 5px - 30px))`;
mirror.appendChild(bug);
wrapper.appendChild(mirror);
alignY.appendChild(mirror);
wrapper.appendChild(alignY);
container.appendChild(wrapper);
}

View File

@@ -0,0 +1,39 @@
.storm-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 1000;
overflow: hidden;
contain: layout paint;
}
.raindrop {
position: absolute;
width: 2px;
height: 40px;
background: linear-gradient(to bottom, rgba(200, 200, 255, 0), rgba(200, 200, 255, 0.7));
transform-origin: bottom;
}
@keyframes stormy-rain {
0% { transform: translateY(-30vh) translateX(0) rotate(20deg); opacity: 0; }
5% { opacity: 1; }
95% { opacity: 1; }
100% { transform: translateY(110vh) translateX(-40vh) rotate(20deg); opacity: 0; }
}
.lightning-flash {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: #fff;
opacity: 0;
pointer-events: none;
z-index: 999;
will-change: opacity;
}

View File

@@ -0,0 +1,99 @@
// 1. Read Configuration
const config = window.SeasonalsPluginConfig?.Storm || {};
const enabled = config.EnableStorm !== undefined ? config.EnableStorm : true;
const isMobile = window.innerWidth <= 768;
const elementCount = isMobile ? (config.RaindropCountMobile || 150) : (config.RaindropCount || 300);
const enableLightning = config.EnableLightning !== undefined ? config.EnableLightning : true;
const rainSpeed = config.RainSpeed || 1.0;
let msgPrinted = false;
// 2. Toggle Function
function toggleStorm() {
const container = document.querySelector('.storm-container');
if (!container) 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) {
container.style.display = 'none';
if (!msgPrinted) {
console.log('Storm hidden');
msgPrinted = true;
}
} else {
container.style.display = 'block';
if (msgPrinted) {
console.log('Storm visible');
msgPrinted = false;
}
}
}
// 3. MutationObserver
const observer = new MutationObserver(toggleStorm);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
// 4. Element Creation
function createElements() {
const container = document.querySelector('.storm-container') || document.createElement('div');
if (!document.querySelector('.storm-container')) {
container.className = 'storm-container';
container.setAttribute('aria-hidden', 'true');
document.body.appendChild(container);
}
for (let i = 0; i < elementCount; i++) {
const drop = document.createElement('div');
drop.className = 'raindrop';
drop.style.left = `${Math.random() * 140}vw`;
drop.style.top = `${-20 - Math.random() * 50}vh`;
const duration = (0.5 + Math.random() * 0.5) / (rainSpeed || 1);
drop.style.animation = `stormy-rain ${duration}s linear infinite`;
drop.style.animationDelay = `${Math.random() * 2}s`;
drop.style.opacity = Math.random() * 0.5 + 0.3;
container.appendChild(drop);
}
if (enableLightning) {
const flash = document.createElement('div');
flash.className = 'lightning-flash';
container.appendChild(flash);
function triggerFlash() {
const nextFlashDelay = 5000 + Math.random() * 10000;
setTimeout(() => {
flash.style.opacity = '0.8';
setTimeout(() => { flash.style.opacity = '0'; }, 50);
setTimeout(() => { flash.style.opacity = '0.5'; }, 100);
setTimeout(() => { flash.style.opacity = '0'; }, 150);
triggerFlash();
}, nextFlashDelay);
}
triggerFlash();
}
}
// 5. Initialization
function initializeStorm() {
if (!enabled) return;
createElements();
toggleStorm();
}
initializeStorm();

View File

@@ -8,6 +8,7 @@
height: 100%;
pointer-events: none;
z-index: 10;
contain: layout paint;
}
.summer-bubble {

View File

@@ -249,6 +249,12 @@
<option value="summer">Summer (Bubbles)</option>
<option value="carnival">Carnival (Confetti)</option>
<option value="cherryblossom">Cherryblossom</option>
<option value="earthday">Earth Day</option>
<option value="eurovision">Eurovision</option>
<option value="piday">Pi-Day</option>
<option value="pride">Pride</option>
<option value="rain">Rain</option>
<option value="storm">Storm (Epilepsy Warning!)</option>
<option value="custom">⚙ Custom (Local Files)</option>
</select>
@@ -335,6 +341,12 @@
summer: { css: 'summer.css', js: 'summer.js', container: 'summer-container' },
carnival: { css: 'carnival.css', js: 'carnival.js', container: 'carnival-container' },
cherryblossom: { css: 'cherryblossom.css', js: 'cherryblossom.js', container: 'cherryblossom-container' },
earthday: { css: 'earthday.css', js: 'earthday.js', container: 'earthday-container' },
eurovision: { css: 'eurovision.css', js: 'eurovision.js', container: 'eurovision-container' },
piday: { css: 'piday.css', js: 'piday.js', container: 'piday-container' },
pride: { css: 'pride.css', js: 'pride.js', container: 'pride-container' },
rain: { css: 'rain.css', js: 'rain.js', container: 'rain-container' },
storm: { css: 'storm.css', js: 'storm.js', container: 'storm-container' },
};
const select = document.getElementById('theme-select');
@@ -362,7 +374,9 @@
'.christmas-container', '.santa-container', '.autumn-container',
'.christmas-container', '.santa-container', '.autumn-container',
'.easter-container', '.resurrection-container', '.spring-container',
'.summer-container', '.carnival-container'
'.summer-container', '.carnival-container', '.cherryblossom-container',
'.earthday-container', '.eurovision-container', '.piday-container',
'.pride-container', '.rain-container', '.storm-container'
];
knownContainers.forEach(sel => {
document.querySelectorAll(sel).forEach(el => {