Files
Jellyfin-Seasonals-Plugin/Jellyfin.Plugin.Seasonals/Web/santa.js
CodeDevMLH 25a0be221b feat: Bump version to 1.1.0.0 and add advanced configuration options for seasonal effects
Enhanced JavaScript files for autumn, christmas, easter, fireworks, halloween, hearts, santa, snowfall, snowflakes, and snowstorm to support configuration options via window.SeasonalsPluginConfig.

Added automatic theme selection based on date in README.md.
2025-12-16 01:26:27 +01:00

305 lines
11 KiB
JavaScript

const config = window.SeasonalsPluginConfig?.santa || {};
const santaIsFlying = config.enableSanta !== undefined ? config.enableSanta : true; // enable/disable santa
let snowflakesCount = config.snowflakesCount || 500; // count of snowflakes (recommended values: 300-600)
const snowflakesCountMobile = config.snowflakesCountMobile || 250; // count of snowflakes on mobile devices (Warning: High values may affect performance)
const snowFallSpeed = config.snowFallSpeed || 3; // speed of snowfall (recommended values: 0-5)
const santaSpeed = config.santaSpeed || 10; // speed of santa in seconds (recommended values: 5000-15000)
const santaSpeedMobile = config.santaSpeedMobile || 8; // speed of santa on mobile devices in seconds
const maxSantaRestTime = config.maxSantaRestTime || 8; // maximum time santa rests in seconds
const minSantaRestTime = config.minSantaRestTime || 3; // minimum time santa rests in seconds
const maxPresentFallSpeed = config.maxPresentFallSpeed || 5; // maximum speed of falling presents in seconds
const minPresentFallSpeed = config.minPresentFallSpeed || 2; // minimum speed of falling presents in seconds
let msgPrinted = false; // flag to prevent multiple console messages
let isMobile = false; // flag to detect mobile devices
let canvas, ctx; // canvas and context for drawing snowflakes
let animationFrameId; // ID of the animation frame
let animationFrameIdSanta; // ID of the animation frame for santa
// function to check and control the santa
function toggleSnowfall() {
const santaContainer = document.querySelector('.santa-container');
if (!santaContainer) 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');
// hide santa if video/trailer player is active or dashboard is visible
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
santaContainer.style.display = 'none'; // hide santa
removeCanvas();
if (!msgPrinted) {
console.log('Snowfall hidden');
msgPrinted = true;
}
} else {
santaContainer.style.display = 'block'; // show santa
if (!animationFrameId && !animationFrameIdSanta) {
initializeCanvas();
snowflakes = createSnowflakes(santaContainer);
animateAll();
}
if (msgPrinted) {
console.log('Snowfall visible');
msgPrinted = false;
}
}
}
// observe changes in the DOM
const observer = new MutationObserver(toggleSnowfall);
// 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)
});
function initializeCanvas() {
if (document.getElementById('snowfallCanvas')) {
console.warn('Canvas already exists.');
return;
}
const container = document.querySelector('.santa-container');
if (!container) {
console.error('Error: No element with class "santa-container" found.');
return;
}
canvas = document.createElement('canvas');
canvas.id = 'snowfallCanvas';
container.appendChild(canvas);
ctx = canvas.getContext('2d');
resizeCanvas(container);
window.addEventListener('resize', () => resizeCanvas(container));
}
function removeCanvas() {
const canvas = document.getElementById('snowfallCanvas');
if (canvas) {
canvas.remove();
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
console.log('Animation frame canceled');
}
if (animationFrameIdSanta) {
cancelAnimationFrame(animationFrameIdSanta);
animationFrameIdSanta = null;
console.log('Santa animation frame canceled');
}
console.log('Canvas removed');
}
}
function resizeCanvas(container) {
if (!canvas) return;
const rect = container.getBoundingClientRect();
canvas.width = rect.width;
canvas.height = rect.height;
}
function createSnowflakes(container) {
return Array.from({ length: snowflakesCount }, () => ({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
radius: Math.random() * 0.6 + 1,
speed: Math.random() * snowFallSpeed + 1,
swing: Math.random() * 2 - 1,
}));
}
// Initialize snowflakes
let snowflakes = [];
function drawSnowflakes() {
if (!ctx || !canvas) {
console.error('Error: Canvas or context not found.');
return;
}
ctx.clearRect(0, 0, canvas.width, canvas.height); // empty canvas
snowflakes.forEach(flake => {
ctx.beginPath();
ctx.arc(flake.x, flake.y, flake.radius, 0, Math.PI * 2);
ctx.fillStyle = 'white'; // color of snowflakes
ctx.fill();
});
}
function updateSnowflakes() {
snowflakes.forEach(flake => {
flake.y += flake.speed; // downwards movement
flake.x += flake.swing; // sideways movement
// reset snowflake if it reaches the bottom
if (flake.y > canvas.height) {
flake.y = 0;
flake.x = Math.random() * canvas.width; // with new random X position
}
// wrap snowflakes around the screen edges
if (flake.x > canvas.width) flake.x = 0;
if (flake.x < 0) flake.x = canvas.width;
});
}
// credits: flaticon.com
const presentImages = [
'Seasonals/Resources/santa_images/gift1.png',
'Seasonals/Resources/santa_images/gift2.png',
'Seasonals/Resources/santa_images/gift3.png',
'Seasonals/Resources/santa_images/gift4.png',
'Seasonals/Resources/santa_images/gift5.png',
'Seasonals/Resources/santa_images/gift6.png',
'Seasonals/Resources/santa_images/gift7.png',
'Seasonals/Resources/santa_images/gift8.png',
];
// credits: https://www.animatedimages.org/img-animated-santa-claus-image-0420-85884.htm
const santaImage = 'Seasonals/Resources/santa_images/santa.gif';
function createSantaElement() {
const santa = document.createElement('img');
santa.src = santaImage;
santa.classList.add('santa');
const santaContainer = document.querySelector('.santa-container');
santaContainer.appendChild(santa);
}
function dropPresent(santa, fromLeft) {
const presentSrc = presentImages[Math.floor(Math.random() * presentImages.length)];
const present = document.createElement('img');
present.src = presentSrc;
present.classList.add('present');
santa.parentElement.appendChild(present);
// Get Santa's position
const santaRect = santa.getBoundingClientRect();
present.style.left = fromLeft ? `${santaRect.left}px` : `${santaRect.left + santaRect.width - 15}px`;
present.style.top = `${santaRect.bottom - 50}px`;
// Start falling
const duration = Math.random() * (maxPresentFallSpeed - minPresentFallSpeed) + minPresentFallSpeed;
present.style.transition = `top ${duration}s linear`;
requestAnimationFrame(() => {
present.style.top = `${window.innerHeight}px`;
});
// Remove from DOM after animation
present.addEventListener('transitionend', () => {
present.remove();
});
}
function reloadSantaGif() {
const santa = document.querySelector('.santa');
const src = santa.src;
santa.src = '';
santa.src = src;
}
function animateSanta() {
const santa = document.querySelector('.santa');
function startAnimation() {
const santaHeight = santa.offsetHeight;
if (santaHeight === 0) {
setTimeout(startAnimation, 100);
return;
}
// console.log('Santa height: ', santaHeight);
const screenWidth = window.innerWidth;
const screenHeight = window.innerHeight;
const fromLeft = Math.random() < 0.5;
const startX = fromLeft ? -220 : screenWidth + 220;
const endX = fromLeft ? screenWidth + 220 : -220;
const startY = Math.random() * (screenHeight / 5) + santaHeight; // Restrict to upper screen
const endY = Math.random() * (screenHeight / 5) + santaHeight; // Restrict to upper screen
const angle = Math.random() * 16 - 8; // -8 to 8 degrees
santa.style.left = `${startX}px`;
santa.style.top = `${startY}px`;
santa.style.transform = `rotate(${angle}deg) ${fromLeft ? 'scaleX(-1)' : 'scaleX(1)'}`; // Mirror if not from left
let duration;
if (isMobile) {
duration = santaSpeedMobile * 1000;
} else {
duration = santaSpeed * 1000;
}
const deltaX = endX - startX;
const deltaY = endY - startY;
const startTime = performance.now();
function move() {
const currentTime = performance.now();
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const currentY = startY + deltaY * progress - 50 * Math.sin(progress * Math.PI);
santa.style.left = `${startX + deltaX * progress}px`;
santa.style.top = `${currentY}px`;
if (Math.random() < 0.05) { // 5% chance to drop a present
dropPresent(santa, fromLeft);
}
if (progress < 1) {
animationFrameIdSanta = requestAnimationFrame(move);
} else {
const pause = Math.random() * ((maxSantaRestTime - minSantaRestTime) * 1000) + minSantaRestTime * 1000;
setTimeout(animateSanta, pause);
}
}
animationFrameIdSanta = requestAnimationFrame(move);
}
reloadSantaGif();
startAnimation();
}
function animateAll() {
drawSnowflakes();
updateSnowflakes();
animationFrameId = requestAnimationFrame(animateAll);
}
// initialize santa
function initializeSanta() {
if (!santaIsFlying) {
console.warn('Sante is disabled.');
return; // exit if santa is disabled
}
const container = document.querySelector('.santa-container');
if (container) {
const screenWidth = window.innerWidth; // get the screen width to detect mobile devices
if (screenWidth < 768) { // lower count of snowflakes on mobile devices
isMobile = true;
console.log('Mobile device detected. Reducing snowflakes count.');
snowflakesCount = snowflakesCountMobile;
}
console.log('Santa enabled.');
initializeCanvas();
snowflakes = createSnowflakes(container);
createSantaElement();
animateAll();
animateSanta();
}
}
initializeSanta();