mirror of
https://github.com/ranaldsgift/KefinTweaks.git
synced 2026-11-05 15:52:47 +01:00
Improve support for Seasonal sections. Add Valentines animated overlay and background
This commit is contained in:
1 parent
c91281e31e
commit
d1d979ed13
6 files changed
+403
-90
No files matched your search
+5
-19
@@ -58,28 +58,14 @@
|
||||
order: 11;
|
||||
}
|
||||
|
||||
/* Halloween theme */
|
||||
.home-screen.halloween-theme.with-background #reactRoot:has(.libraryPage:not(.hide) .pageTabContent.is-active[data-index="0"])>.backgroundContainer::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: -25%;
|
||||
background: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url(https://ranaldsgift.github.io/KefinTweaks/pages/images/halloween.jpg);
|
||||
background-position: 0px -250px;
|
||||
background-size: auto 102%;
|
||||
}
|
||||
|
||||
/* Christmas theme */
|
||||
.home-screen.christmas-theme.with-background #reactRoot:has(.libraryPage:not(.hide) .pageTabContent.is-active[data-index="0"])>.backgroundContainer::after {
|
||||
body[data-seasonal-background="true"]:has(.homePage:not(.hide)) #reactRoot:has(.libraryPage:not(.hide) .pageTabContent.is-active[data-index="0"])>.backgroundContainer::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0%;
|
||||
background: linear-gradient(rgba(0, 0, 0, 0.65), rgba(0, 0, 0, 0.85)), url('../pages/images/christmas.jpg');
|
||||
background: linear-gradient(rgba(0, 0, 0, 0.65), rgba(0, 0, 0, 0.85)), var(--seasonal-background-image);
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
}
|
||||
@@ -88,12 +74,12 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.home-screen.christmas-theme .snowverlay {
|
||||
body[data-seasonal-animations="true"]:has(.homePage:not(.hide)) .snowverlay {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.home-screen [data-section-id] h2.sectionTitle,
|
||||
.home-screen [data-section-id] .sectionTitleTextButton>.material-icons {
|
||||
body:has(.homePage:not(.hide)) [data-section-id] h2.sectionTitle,
|
||||
body:has(.homePage:not(.hide)) [data-section-id] .sectionTitleTextButton>.material-icons {
|
||||
margin-bottom: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
+89
-46
@@ -244,51 +244,77 @@
|
||||
observer.observe(sectionEl, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
function manageBodyClasses() {
|
||||
async function manageBodyClasses() {
|
||||
|
||||
const homeScreenConfig = await window.KefinHomeScreen.getConfig();
|
||||
const seasonalSectionGroups = homeScreenConfig.SEASONAL_SECTION_GROUPS || [];
|
||||
const seasonalConfig = homeScreenConfig.SEASONAL_THEME_SETTINGS || {};
|
||||
const enableSeasonalAnimations = seasonalConfig.enableSeasonalAnimations !== false;
|
||||
const enableSeasonalBackground = seasonalConfig.enableSeasonalBackground !== false;
|
||||
const seasonalThemes = seasonalConfig.seasonalThemes || {};
|
||||
|
||||
if (!enableSeasonalBackground && !enableSeasonalAnimations) {
|
||||
LOG('Seasonal background and animations are disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
const body = document.body;
|
||||
const currentView = window.KefinTweaksUtils?.getCurrentView ? window.KefinTweaksUtils.getCurrentView() : null;
|
||||
const isHomePage = currentView === 'home' || currentView === 'home.html' || window.location.href.includes('home.html');
|
||||
|
||||
// Get the active Seasonal groups by checking if any section in the group is enabled and where the current date falls within the StartDate and EndDate of the section
|
||||
const activeSeasonalGroups = seasonalSectionGroups.filter(sectionGroup => {
|
||||
return sectionGroup.sections.some(s => s.enabled && isInSeasonalPeriod(s.startDate, s.endDate));
|
||||
});
|
||||
|
||||
const homeScreenConfig = window.KefinTweaksConfig?.homeScreen || {};
|
||||
const seasonalConfig = homeScreenConfig.seasonal || {};
|
||||
const enableSeasonalAnimations = seasonalConfig.enableSeasonalAnimations !== false;
|
||||
const enableSeasonalBackground = seasonalConfig.enableSeasonalBackground !== false;
|
||||
|
||||
if (isHomePage && body) {
|
||||
body.classList.add('home-screen');
|
||||
} else if (body) {
|
||||
body.classList.remove('home-screen');
|
||||
if (activeSeasonalGroups.length === 0) {
|
||||
LOG('No active seasonal groups found');
|
||||
return;
|
||||
}
|
||||
|
||||
let activeSeasonalThemeGroup;
|
||||
if (enableSeasonalBackground && enableSeasonalAnimations) {
|
||||
activeSeasonalThemeGroup = activeSeasonalGroups.find(sectionGroup => {
|
||||
const theme = seasonalThemes[sectionGroup.id] || {};
|
||||
return theme.backgroundImage && theme.animation;
|
||||
});
|
||||
} else {
|
||||
activeSeasonalThemeGroup = activeSeasonalGroups.find(sectionGroup => {
|
||||
const theme = seasonalThemes[sectionGroup.id] || {};
|
||||
return (enableSeasonalBackground && theme.backgroundImage)
|
||||
|| (enableSeasonalAnimations && theme.animation);
|
||||
});
|
||||
}
|
||||
|
||||
if (!activeSeasonalThemeGroup) {
|
||||
LOG('No seasonal theme found');
|
||||
return;
|
||||
}
|
||||
|
||||
if (body) {
|
||||
const themeClasses = ['halloween-theme', 'christmas-theme', 'valentines-theme', 'newyear-theme', 'with-background'];
|
||||
themeClasses.forEach(themeClass => body.classList.remove(themeClass));
|
||||
|
||||
if (isHomePage) {
|
||||
if (isHalloweenPeriod()) {
|
||||
body.classList.add('halloween-theme');
|
||||
} else if (isChristmasPeriod()) {
|
||||
body.classList.add('christmas-theme');
|
||||
document.body.dataset.seasonalTheme = activeSeasonalThemeGroup.id;
|
||||
|
||||
if (enableSeasonalBackground) {
|
||||
body.classList.add('with-background');
|
||||
}
|
||||
if (enableSeasonalBackground && !document.body.dataset.seasonalBackground) {
|
||||
document.body.dataset.seasonalBackground = 'true';
|
||||
|
||||
if (enableSeasonalAnimations) {
|
||||
const snowverlayScript = document.querySelector('script[src*="snowverlay.js"]');
|
||||
|
||||
if (!snowverlayScript) {
|
||||
const snowverlayScriptSrc = (window.KefinTweaksConfig.kefinTweaksRoot || '') + 'scripts/snowverlay.js';
|
||||
const script = document.createElement('script');
|
||||
script.src = snowverlayScriptSrc;
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
}
|
||||
// Add the background image as a css variable to the body
|
||||
const backgroundImage = seasonalThemes[activeSeasonalThemeGroup.id]?.backgroundImage;
|
||||
if (backgroundImage) {
|
||||
document.body.style.setProperty('--seasonal-background-image', backgroundImage);
|
||||
}
|
||||
}
|
||||
|
||||
} else if (isValentinesPeriod()) {
|
||||
body.classList.add('valentines-theme');
|
||||
} else if (isNewYearsPeriod()) {
|
||||
body.classList.add('newyear-theme');
|
||||
if (enableSeasonalAnimations && !document.body.dataset.seasonalAnimations) {
|
||||
document.body.dataset.seasonalAnimations = 'true';
|
||||
|
||||
// Add the animation script to the head
|
||||
const animation = seasonalThemes[activeSeasonalThemeGroup.id]?.animation;
|
||||
if (animation) {
|
||||
const animationScript = document.querySelector(`script[src*="${animation}.js"]`);
|
||||
if (!animationScript) {
|
||||
const animationScriptSrc = (window.KefinTweaksConfig.kefinTweaksRoot || '') + `scripts/seasonal/${animation}.js`;
|
||||
const script = document.createElement('script');
|
||||
script.src = animationScriptSrc;
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1735,6 +1761,14 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure at least 1 discovery section is enabled
|
||||
const discoverySections = getDiscoverySections();
|
||||
if (!discoverySections || discoverySections.length === 0 || discoverySections.every(section => section.enabled === false)) {
|
||||
LOG('No discovery sections enabled in config.');
|
||||
state.discoveryEnabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!container) {
|
||||
WARN('setupDiscoveryInteraction called without a valid container');
|
||||
return;
|
||||
@@ -2035,11 +2069,7 @@
|
||||
const activeTab = document.querySelector('.headerTabs .emby-tab-button-active').getAttribute('data-index');
|
||||
if (activeTab !== '0') return;
|
||||
|
||||
const activeContainer = document.querySelector('.libraryPage:not(.hide) .kefinHomeSectionsContainer');
|
||||
if (!activeContainer) return;
|
||||
|
||||
|
||||
if (!activeContainer.dataset.sectionsRendered) return;
|
||||
if (!container.dataset.sectionsRendered) return;
|
||||
|
||||
if (state.isRenderingDiscovery) return;
|
||||
|
||||
@@ -2050,7 +2080,7 @@
|
||||
const threshold = windowHeight * 1.5;
|
||||
|
||||
if (scrollTop + windowHeight >= documentHeight - threshold) {
|
||||
renderNextDiscoveryGroup(activeContainer);
|
||||
renderNextDiscoveryGroup(container);
|
||||
}
|
||||
}, { passive: true });
|
||||
LOG('Infinite scroll enabled for discovery.');
|
||||
@@ -2102,8 +2132,20 @@
|
||||
return isInSeasonalPeriod('01-01', '01-05');
|
||||
}
|
||||
|
||||
function isValentinesPeriod() {
|
||||
return isInSeasonalPeriod('02-01', '02-15');
|
||||
async function isValentinesPeriod() {
|
||||
const homeScreenConfig = await window.KefinHomeScreen.getConfig();
|
||||
const valentinesConfig = homeScreenConfig.SEASONAL_SECTION_GROUPS.find(s => s.id === 'seasonal-valentines');
|
||||
if (!valentinesConfig) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const activeSection = valentinesConfig.sections.find(s => s.enabled);
|
||||
|
||||
if (!activeSection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isInSeasonalPeriod(activeSection.startDate, activeSection.endDate);
|
||||
}
|
||||
|
||||
const localCache = new window.LocalStorageCache();
|
||||
@@ -2309,14 +2351,15 @@
|
||||
}
|
||||
|
||||
try {
|
||||
manageBodyClasses();
|
||||
enhanceHomeScreen();
|
||||
} catch (err) { ERR('Home screen page change handler failed:', err); }
|
||||
}, { pages: ['home', 'home.html'] });
|
||||
}
|
||||
|
||||
if (window.KefinTweaksUtils && typeof window.KefinTweaksUtils.onViewPage === 'function') {
|
||||
/* if (window.KefinTweaksUtils && typeof window.KefinTweaksUtils.onViewPage === 'function') {
|
||||
window.KefinTweaksUtils.onViewPage(manageBodyClasses, { pages: [] });
|
||||
}
|
||||
} */
|
||||
|
||||
})();
|
||||
|
||||
@@ -480,8 +480,8 @@
|
||||
id: 'seasonal.valentines.valentines-movies',
|
||||
name: 'Valentine\'s Movies',
|
||||
enabled: true,
|
||||
startDate: '02-01',
|
||||
endDate: '02-15',
|
||||
startDate: '02-07',
|
||||
endDate: '02-14',
|
||||
order: 50,
|
||||
cardFormat: 'Poster',
|
||||
queries: [
|
||||
@@ -499,8 +499,8 @@
|
||||
id: 'seasonal.valentines.valentines-episodes',
|
||||
name: 'Valentine\'s Episodes',
|
||||
enabled: true,
|
||||
startDate: '02-01',
|
||||
endDate: '02-15',
|
||||
startDate: '02-07',
|
||||
endDate: '02-14',
|
||||
order: 51,
|
||||
cardFormat: 'Poster',
|
||||
queries: [
|
||||
@@ -819,31 +819,27 @@
|
||||
}
|
||||
|
||||
const SEASONAL_THEME_SETTINGS = {
|
||||
enabled: true,
|
||||
enableSeasonalAnimations: true,
|
||||
enableSeasonalBackground: true,
|
||||
seasonToggles: [
|
||||
{
|
||||
id: 'halloween',
|
||||
name: 'Halloween',
|
||||
enabled: false
|
||||
enabled: false,
|
||||
enableSeasonalAnimations: false,
|
||||
enableSeasonalBackground: false,
|
||||
seasonalThemes: {
|
||||
'seasonal-halloween': {
|
||||
animation: 'snowverlay',
|
||||
backgroundImage: 'url(../pages/images/halloween.jpg)'
|
||||
},
|
||||
{
|
||||
id: 'christmas',
|
||||
name: 'Christmas',
|
||||
enabled: false
|
||||
'seasonal-christmas': {
|
||||
animation: 'snowverlay',
|
||||
backgroundImage: 'url(../pages/images/christmas.jpg)'
|
||||
},
|
||||
{
|
||||
id: 'new-years',
|
||||
name: 'New Year\'s',
|
||||
enabled: false
|
||||
'seasonal-new-years': {
|
||||
animation: 'snowverlay',
|
||||
backgroundImage: 'url(../pages/images/new-years.jpg)'
|
||||
},
|
||||
{
|
||||
id: 'valentines',
|
||||
name: 'Valentine\'s Day',
|
||||
enabled: false
|
||||
'seasonal-valentines': {
|
||||
animation: 'valentinesOverlay',
|
||||
backgroundImage: 'url(../pages/images/valentines.jpg)'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
const HOME_SETTINGS = {
|
||||
minimumSeriesForPopularTVNetworks: 10,
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
function addSnowverlay() {
|
||||
// Check if snowverlay canvas is already added
|
||||
const snowverlayCanvas = document.querySelector('.snowverlay');
|
||||
if (snowverlayCanvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Configuration constants
|
||||
const CANVAS_HEIGHT_PX = 160;
|
||||
const SNOWFLAKE_AMOUNT = 15;
|
||||
const SNOWFLAKE_SIZE = { min: 10, max: 22 };
|
||||
const SNOWFLAKE_SPEED = { min: 0.2, max: 0.7 };
|
||||
|
||||
// Dynamically inject styles
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
body {
|
||||
height: 100svh;
|
||||
margin: 0;
|
||||
}
|
||||
.snowverlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: ${CANVAS_HEIGHT_PX}px;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Create and inject canvas
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.className = 'snowverlay';
|
||||
document.body.appendChild(canvas);
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Resize handler
|
||||
const resizeCanvas = () => {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = CANVAS_HEIGHT_PX;
|
||||
};
|
||||
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
resizeCanvas();
|
||||
|
||||
// Snowflake factory
|
||||
const createSnowflake = (isAnimated = true, index = 0) => ({
|
||||
x: Math.random() * canvas.width,
|
||||
y: isAnimated
|
||||
? -20 - (index * canvas.height) / SNOWFLAKE_AMOUNT
|
||||
: Math.random() * canvas.height,
|
||||
size: Math.random() * (SNOWFLAKE_SIZE.max - SNOWFLAKE_SIZE.min) + SNOWFLAKE_SIZE.min,
|
||||
speed: Math.random() * (SNOWFLAKE_SPEED.max - SNOWFLAKE_SPEED.min) + SNOWFLAKE_SPEED.min,
|
||||
opacity: isAnimated ? null : Math.random() * 0.5 + 0.2,
|
||||
drift: Math.random() * 0.4 - 0.2, // Random horizontal drift (wind)
|
||||
rotation: Math.random() * 360,
|
||||
rotationSpeed: Math.random() * 2 - 1 // Degrees per frame
|
||||
});
|
||||
|
||||
// Draw single snowflake
|
||||
const drawSnowflake = (flake) => {
|
||||
ctx.save();
|
||||
ctx.translate(flake.x, flake.y);
|
||||
ctx.rotate((flake.rotation * Math.PI) / 180);
|
||||
ctx.font = `${flake.size}px sans-serif`;
|
||||
ctx.fillStyle = `rgba(255, 255, 255, ${flake.opacity ?? (1 - flake.y / canvas.height)})`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText('❄', 0, 0);
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
// Animation control
|
||||
let animationFrame;
|
||||
const stopAnimation = () => {
|
||||
if (animationFrame) cancelAnimationFrame(animationFrame);
|
||||
};
|
||||
|
||||
// Static snow renderer
|
||||
const renderStaticSnow = () => {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
Array.from({ length: SNOWFLAKE_AMOUNT }, () => createSnowflake(false))
|
||||
.forEach(drawSnowflake);
|
||||
};
|
||||
|
||||
// Animated snow renderer
|
||||
const startSnowAnimation = () => {
|
||||
const snowflakes = Array.from(
|
||||
{ length: SNOWFLAKE_AMOUNT },
|
||||
(_, i) => createSnowflake(true, i)
|
||||
);
|
||||
|
||||
const animate = () => {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
snowflakes.forEach(flake => {
|
||||
flake.y += flake.speed;
|
||||
flake.x += flake.drift;
|
||||
flake.rotation += flake.rotationSpeed;
|
||||
|
||||
if (flake.y > canvas.height) {
|
||||
flake.y = -10;
|
||||
flake.x = Math.random() * canvas.width;
|
||||
}
|
||||
|
||||
drawSnowflake(flake);
|
||||
});
|
||||
|
||||
animationFrame = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
animate();
|
||||
return stopAnimation;
|
||||
};
|
||||
|
||||
// Motion preference handler
|
||||
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||
let currentAnimation = null;
|
||||
|
||||
const handleMotionChange = (e) => {
|
||||
stopAnimation();
|
||||
if (currentAnimation) currentAnimation();
|
||||
|
||||
if (e.matches) {
|
||||
renderStaticSnow();
|
||||
currentAnimation = null;
|
||||
} else {
|
||||
currentAnimation = startSnowAnimation();
|
||||
}
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener('change', handleMotionChange);
|
||||
handleMotionChange(mediaQuery);
|
||||
|
||||
// Cleanup on unload
|
||||
window.addEventListener('beforeunload', () => {
|
||||
mediaQuery.removeEventListener('change', handleMotionChange);
|
||||
stopAnimation();
|
||||
});
|
||||
}
|
||||
|
||||
addSnowverlay();
|
||||
@@ -0,0 +1,144 @@
|
||||
function addSnowverlay() {
|
||||
// Check if snowverlay canvas is already added
|
||||
const snowverlayCanvas = document.querySelector('.snowverlay');
|
||||
if (snowverlayCanvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Configuration constants
|
||||
const CANVAS_HEIGHT_PX = 160;
|
||||
const SNOWFLAKE_AMOUNT = 15;
|
||||
const SNOWFLAKE_SIZE = { min: 10, max: 22 };
|
||||
const SNOWFLAKE_SPEED = { min: 0.2, max: 0.7 };
|
||||
|
||||
// Dynamically inject styles
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
body {
|
||||
height: 100svh;
|
||||
margin: 0;
|
||||
}
|
||||
.snowverlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: ${CANVAS_HEIGHT_PX}px;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Create and inject canvas
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.className = 'snowverlay';
|
||||
document.body.appendChild(canvas);
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Resize handler
|
||||
const resizeCanvas = () => {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = CANVAS_HEIGHT_PX;
|
||||
};
|
||||
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
resizeCanvas();
|
||||
|
||||
// Snowflake factory
|
||||
const createSnowflake = (isAnimated = true, index = 0) => ({
|
||||
x: Math.random() * canvas.width,
|
||||
y: isAnimated
|
||||
? -20 - (index * canvas.height) / SNOWFLAKE_AMOUNT
|
||||
: Math.random() * canvas.height,
|
||||
size: Math.random() * (SNOWFLAKE_SIZE.max - SNOWFLAKE_SIZE.min) + SNOWFLAKE_SIZE.min,
|
||||
speed: Math.random() * (SNOWFLAKE_SPEED.max - SNOWFLAKE_SPEED.min) + SNOWFLAKE_SPEED.min,
|
||||
opacity: isAnimated ? null : Math.random() * 0.5 + 0.2,
|
||||
drift: Math.random() * 0.4 - 0.2, // Random horizontal drift (wind)
|
||||
rotation: Math.random() * 360,
|
||||
rotationSpeed: Math.random() * 2 - 1 // Degrees per frame
|
||||
});
|
||||
|
||||
// Draw single snowflake
|
||||
const drawSnowflake = (flake) => {
|
||||
ctx.save();
|
||||
ctx.translate(flake.x, flake.y);
|
||||
ctx.rotate((flake.rotation * Math.PI) / 180);
|
||||
ctx.font = `${flake.size}px sans-serif`;
|
||||
ctx.fillStyle = `rgba(255, 0, 0, ${flake.opacity ?? (1 - flake.y / canvas.height)})`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText('❤', 0, 0);
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
// Animation control
|
||||
let animationFrame;
|
||||
const stopAnimation = () => {
|
||||
if (animationFrame) cancelAnimationFrame(animationFrame);
|
||||
};
|
||||
|
||||
// Static snow renderer
|
||||
const renderStaticSnow = () => {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
Array.from({ length: SNOWFLAKE_AMOUNT }, () => createSnowflake(false))
|
||||
.forEach(drawSnowflake);
|
||||
};
|
||||
|
||||
// Animated snow renderer
|
||||
const startSnowAnimation = () => {
|
||||
const snowflakes = Array.from(
|
||||
{ length: SNOWFLAKE_AMOUNT },
|
||||
(_, i) => createSnowflake(true, i)
|
||||
);
|
||||
|
||||
const animate = () => {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
snowflakes.forEach(flake => {
|
||||
flake.y += flake.speed;
|
||||
flake.x += flake.drift;
|
||||
flake.rotation += flake.rotationSpeed;
|
||||
|
||||
if (flake.y > canvas.height) {
|
||||
flake.y = -10;
|
||||
flake.x = Math.random() * canvas.width;
|
||||
}
|
||||
|
||||
drawSnowflake(flake);
|
||||
});
|
||||
|
||||
animationFrame = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
animate();
|
||||
return stopAnimation;
|
||||
};
|
||||
|
||||
// Motion preference handler
|
||||
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||
let currentAnimation = null;
|
||||
|
||||
const handleMotionChange = (e) => {
|
||||
stopAnimation();
|
||||
if (currentAnimation) currentAnimation();
|
||||
|
||||
if (e.matches) {
|
||||
renderStaticSnow();
|
||||
currentAnimation = null;
|
||||
} else {
|
||||
currentAnimation = startSnowAnimation();
|
||||
}
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener('change', handleMotionChange);
|
||||
handleMotionChange(mediaQuery);
|
||||
|
||||
// Cleanup on unload
|
||||
window.addEventListener('beforeunload', () => {
|
||||
mediaQuery.removeEventListener('change', handleMotionChange);
|
||||
stopAnimation();
|
||||
});
|
||||
}
|
||||
|
||||
addSnowverlay();
|
||||
Reference in new issue
Block a user