diff --git a/seasonals/santa.css b/seasonals/santa.css new file mode 100644 index 0000000..a89cf75 --- /dev/null +++ b/seasonals/santa.css @@ -0,0 +1,33 @@ +.santa-container { + position: fixed; + width: 100%; + height: 100vh; + background: transparent; + overflow: hidden; + pointer-events: none; +} + +#snowfallCanvas { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; +} + +.santa { + position: fixed; + width: 220px; + height: auto; + z-index: 1000; + pointer-events: none; +} + +.present { + position: fixed; + width: 15px; + height: auto; + z-index: 999; + pointer-events: none; +} \ No newline at end of file diff --git a/seasonals/santa.js b/seasonals/santa.js new file mode 100644 index 0000000..488ea36 --- /dev/null +++ b/seasonals/santa.js @@ -0,0 +1,285 @@ +const santaIsFlying = true; // enable/disable santa +let snowflakesCount = 500; // count of snowflakes (recommended values: 300-600) +const snowflakesCountMobile = 250; // count of snowflakes on mobile devices +const snowFallSpeed = 3; // speed of snowfall (recommended values: 0-5) +const santaSpeed = 10; // speed of santa in seconds (recommended values: 5000-15000) +const santaSpeedMobile = 8; // speed of santa on mobile devices in seconds +const maxSantaRestTime = 8; // maximum time santa rests in seconds +const minSantaRestTime = 3; // minimum time santa rests in seconds +const maxPresentFallSpeed = 5; // maximum speed of falling presents in seconds +const 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(); + } else { + console.warn('could not initialize santa: animation frame is already running'); + } + + 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/santa_images/gift1.png', + 'seasonals/santa_images/gift2.png', + 'seasonals/santa_images/gift3.png', + 'seasonals/santa_images/gift4.png', + 'seasonals/santa_images/gift5.png', + 'seasonals/santa_images/gift6.png', + 'seasonals/santa_images/gift7.png', + 'seasonals/santa_images/gift8.png', +]; + +// credits: https://www.animatedimages.org/img-animated-santa-claus-image-0420-85884.htm +const santaImage = 'seasonals/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 animateSanta() { + const santa = document.querySelector('.santa'); + 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 santaHeight = santa.offsetHeight; + const startY = Math.random() * (screenHeight / 5 - santaHeight - 50) + 50; // Restrict to upper screen + const endY = Math.random() * (screenHeight / 5 - santaHeight - 50) + 50; // Restrict to upper screen + const angle = Math.random() * 20 - 10; // -10 to 10 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); +} + + +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(); \ No newline at end of file diff --git a/seasonals/santa_images/gift1.png b/seasonals/santa_images/gift1.png new file mode 100644 index 0000000..3242563 Binary files /dev/null and b/seasonals/santa_images/gift1.png differ diff --git a/seasonals/santa_images/gift2.png b/seasonals/santa_images/gift2.png new file mode 100644 index 0000000..23e0e19 Binary files /dev/null and b/seasonals/santa_images/gift2.png differ diff --git a/seasonals/santa_images/gift3.png b/seasonals/santa_images/gift3.png new file mode 100644 index 0000000..d8876a7 Binary files /dev/null and b/seasonals/santa_images/gift3.png differ diff --git a/seasonals/santa_images/gift4.png b/seasonals/santa_images/gift4.png new file mode 100644 index 0000000..16e7224 Binary files /dev/null and b/seasonals/santa_images/gift4.png differ diff --git a/seasonals/santa_images/gift5.png b/seasonals/santa_images/gift5.png new file mode 100644 index 0000000..d8876a7 Binary files /dev/null and b/seasonals/santa_images/gift5.png differ diff --git a/seasonals/santa_images/gift6.png b/seasonals/santa_images/gift6.png new file mode 100644 index 0000000..befdd9c Binary files /dev/null and b/seasonals/santa_images/gift6.png differ diff --git a/seasonals/santa_images/gift7.png b/seasonals/santa_images/gift7.png new file mode 100644 index 0000000..681905c Binary files /dev/null and b/seasonals/santa_images/gift7.png differ diff --git a/seasonals/santa_images/gift8.png b/seasonals/santa_images/gift8.png new file mode 100644 index 0000000..96b7d7e Binary files /dev/null and b/seasonals/santa_images/gift8.png differ diff --git a/seasonals/santa_images/santa.gif b/seasonals/santa_images/santa.gif new file mode 100644 index 0000000..657ef2f Binary files /dev/null and b/seasonals/santa_images/santa.gif differ