From 668edc81236aa4a9352e0cc6bbeaa6f05f9c709a Mon Sep 17 00:00:00 2001 From: Nikita <> Date: Sat, 27 Dec 2025 12:43:41 +0100 Subject: [PATCH] mobile adapt and seasons UPD --- assets/fractal_snowflake.js | 352 ++++++++++++++++++++++++++---------- 1 file changed, 258 insertions(+), 94 deletions(-) diff --git a/assets/fractal_snowflake.js b/assets/fractal_snowflake.js index a34a046..560f4ad 100644 --- a/assets/fractal_snowflake.js +++ b/assets/fractal_snowflake.js @@ -1,37 +1,70 @@ /** @license DHTML Snowstorm! JavaScript-based snow for web pages - Исправленная версия с фрактальными снежинками + Версия 2.2 с адаптацией под мобильные и сезонным ограничением ----------------------------------------------------------- - Версия 2.1 с работающим снегопадом + Автоматически работает только с 1 декабря по 31 января */ var fractalSnowstorm = function(window, document) { - // Конфигурация + // Проверяем сезон - снег только с 1 декабря по 31 января + function isSnowSeason() { + const now = new Date(); + const month = now.getMonth() + 1; // 1-12 + const day = now.getDate(); + + // 1 декабря - 31 января + if (month === 12 || month === 1) { + return true; + } + + // Если февраль, но сегодня 1-е февраля, еще можно оставить снег + if (month === 2 && day === 1) { + return true; + } + + return false; + } + + // Если не сезон для снега, не запускаемся + if (!isSnowSeason()) { + console.log('Снегопад активен только с 1 декабря по 31 января. Сейчас не сезон.'); + return { + start: function() { console.log('Снегопад доступен только зимой!'); }, + stop: function() {}, + isSeason: function() { return false; } + }; + } + + // Конфигурация с адаптацией для мобильных + const isMobile = /iPhone|iPad|iPod|Android|BlackBerry|Windows Phone/i.test(navigator.userAgent); + const mobileMultiplier = isMobile ? 0.5 : 1; // В 2 раза меньше на мобильных + const config = { autoStart: true, - excludeMobile: true, - flakesMax: 80, - flakesMaxActive: 60, - animationInterval: 33, // ~30 FPS + excludeMobile: false, // Теперь работаем и на мобильных + flakesMax: isMobile ? 40 : 80, // Меньше снежинок на мобильных + flakesMaxActive: isMobile ? 30 : 60, + animationInterval: 33, useGPU: true, snowColor: "#ffffff", useTwinkleEffect: true, useMeltEffect: true, - snowStick: true, - followMouse: false, // Упростим для начала + snowStick: !isMobile, // На мобильных не прилипает (для производительности) + followMouse: false, freezeOnBlur: true, - flakeWidth: 24, - flakeHeight: 24, - vMaxX: 2, - vMaxY: 3, // Увеличим скорость падения + flakeWidth: Math.round(24 * mobileMultiplier), + flakeHeight: Math.round(24 * mobileMultiplier), + vMaxX: 2 * mobileMultiplier, + vMaxY: 3 * mobileMultiplier, zIndex: 999999, targetElement: null, flakeBottom: null, usePositionFixed: true, - windVariation: 0.3, - rotationSpeed: 0.02, - fractalTypes: 6, - complexity: 2 + windVariation: 0.3 * mobileMultiplier, + rotationSpeed: 0.02 * mobileMultiplier, + fractalTypes: isMobile ? 4 : 6, // Проще фракталы на мобильных + complexity: isMobile ? 1 : 2, // Меньшая сложность на мобильных + isMobile: isMobile }; // Глобальные переменные @@ -64,12 +97,12 @@ var fractalSnowstorm = function(window, document) { constructor(type, x, y) { this.type = type || Math.floor(Math.random() * config.fractalTypes); this.x = x || Math.random() * screenWidth; - this.y = y || -Math.random() * 500; // Начинаем выше экрана + this.y = y || -Math.random() * 500; this.vx = 0; this.vy = 0; this.rotation = Math.random() * Math.PI * 2; this.rotationSpeed = (Math.random() - 0.5) * config.rotationSpeed; - this.size = 8 + Math.random() * 16; + this.size = (8 + Math.random() * 16) * mobileMultiplier; this.branchCount = 6 + this.type * 2; this.complexity = Math.max(1, Math.min(5, config.complexity)); this.opacity = 0.8 + Math.random() * 0.2; @@ -101,9 +134,15 @@ var fractalSnowstorm = function(window, document) { top: 0; width: ${config.flakeWidth}px; height: ${config.flakeHeight}px; + ${config.isMobile ? 'image-rendering: -webkit-optimize-contrast; image-rendering: crisp-edges;' : ''} `; - this.ctx = this.canvas.getContext('2d'); + this.ctx = this.canvas.getContext('2d', { alpha: true }); + + // Для мобильных: более простая обработка + if (config.isMobile) { + this.ctx.imageSmoothingEnabled = false; + } this.element = document.createElement('div'); this.element.style.cssText = ` @@ -116,6 +155,7 @@ var fractalSnowstorm = function(window, document) { height: ${config.flakeHeight}px; transform: translate(${this.x}px, ${this.y}px); opacity: ${this.opacity}; + ${config.isMobile ? 'will-change: transform, opacity;' : ''} `; this.element.appendChild(this.canvas); @@ -139,6 +179,12 @@ var fractalSnowstorm = function(window, document) { // Очищаем canvas ctx.clearRect(0, 0, config.flakeWidth, config.flakeHeight); + // Для мобильных: более простые снежинки + if (config.isMobile && this.type > 2) { + this.drawSimpleFlake(ctx, centerX, centerY, currentRadius); + return; + } + // Сохраняем состояние ctx.save(); ctx.translate(centerX, centerY); @@ -149,7 +195,77 @@ var fractalSnowstorm = function(window, document) { const alpha = this.opacity * (this.melting ? (1 - this.meltProgress) : 1); const color = hexToRgba(config.snowColor, alpha); - // Рисуем фрактальную снежинку + // Для мобильных: более простая отрисовка + if (config.isMobile) { + this.drawMobileFlake(ctx, currentRadius, color); + } else { + this.drawDesktopFlake(ctx, currentRadius, color); + } + + ctx.restore(); + } + + drawSimpleFlake(ctx, centerX, centerY, radius) { + // Простая снежинка для мобильных + ctx.beginPath(); + ctx.arc(centerX, centerY, radius * 0.8, 0, Math.PI * 2); + + const alpha = this.opacity * (this.melting ? (1 - this.meltProgress) : 1); + const color = hexToRgba(config.snowColor, alpha); + + ctx.fillStyle = color; + ctx.fill(); + + // Добавляем простые лучи + for (let i = 0; i < 6; i++) { + const angle = (i * Math.PI) / 3; + const x1 = centerX + Math.cos(angle) * radius * 0.5; + const y1 = centerY + Math.sin(angle) * radius * 0.5; + const x2 = centerX + Math.cos(angle) * radius * 1.5; + const y2 = centerY + Math.sin(angle) * radius * 1.5; + + ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.lineWidth = 1; + ctx.strokeStyle = color; + ctx.stroke(); + } + } + + drawMobileFlake(ctx, radius, color) { + // Упрощенная версия для мобильных + const branches = 6; + + for (let i = 0; i < branches; i++) { + const angle = (i * 2 * Math.PI) / branches; + + // Основная ветвь + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(Math.cos(angle) * radius, Math.sin(angle) * radius); + ctx.lineWidth = 1; + ctx.strokeStyle = color; + ctx.stroke(); + + // Противоположная ветвь + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(Math.cos(angle + Math.PI) * radius, Math.sin(angle + Math.PI) * radius); + ctx.lineWidth = 1; + ctx.strokeStyle = color; + ctx.stroke(); + } + + // Центральный круг + ctx.beginPath(); + ctx.arc(0, 0, radius * 0.15, 0, Math.PI * 2); + ctx.fillStyle = color; + ctx.fill(); + } + + drawDesktopFlake(ctx, radius, color) { + // Полная версия для десктопа const branches = this.branchCount; const maxDepth = this.complexity; @@ -181,27 +297,23 @@ var fractalSnowstorm = function(window, document) { const angle = (i * 2 * Math.PI) / branches; // Основная ветвь - drawBranch(0, 0, currentRadius, angle, 0, 1.5); + drawBranch(0, 0, radius, angle, 0, 1.5); // Противоположная ветвь - drawBranch(0, 0, currentRadius, angle + Math.PI, 0, 1.5); + drawBranch(0, 0, radius, angle + Math.PI, 0, 1.5); } // Центральный круг ctx.beginPath(); - ctx.arc(0, 0, currentRadius * 0.15, 0, Math.PI * 2); + ctx.arc(0, 0, radius * 0.15, 0, Math.PI * 2); ctx.fillStyle = color; ctx.fill(); - - ctx.restore(); } setVelocities() { - // Базовые скорости this.vx = windX + (Math.random() - 0.5) * config.vMaxX; - this.vy = windY + Math.random() * config.vMaxY * 0.5 + 1.5; // Гарантируем падение вниз + this.vy = windY + Math.random() * config.vMaxY * 0.5 + 1.5; - // Добавляем случайные колебания this.vx += (Math.random() - 0.5) * config.windVariation; } @@ -212,8 +324,8 @@ var fractalSnowstorm = function(window, document) { this.x += this.vx; this.y += this.vy; - // Обновляем вращение - this.rotation += this.rotationSpeed; + // Обновляем вращение (медленнее на мобильных) + this.rotation += this.rotationSpeed * (config.isMobile ? 0.5 : 1); // Обновляем мерцание if (this.twinkle) { @@ -239,10 +351,11 @@ var fractalSnowstorm = function(window, document) { } } - // Случайное таяние в воздухе + // Случайное таяние в воздухе (реже на мобильных) + const meltChance = config.isMobile ? 0.0002 : 0.0005; if (config.useMeltEffect && !this.melting && - Math.random() < 0.0005 && + Math.random() < meltChance && Date.now() - this.created > 5000) { this.melting = true; } @@ -256,7 +369,7 @@ var fractalSnowstorm = function(window, document) { } } - // Обновляем отображение + // Обновляем отображение (реже перерисовываем на мобильных) this.updateDisplay(); return true; } @@ -267,8 +380,11 @@ var fractalSnowstorm = function(window, document) { this.element.style.transform = `translate(${this.x}px, ${this.y - scrollTop}px) rotate(${this.rotation}rad)`; this.element.style.opacity = this.opacity * (this.melting ? (1 - this.meltProgress) : 1); - // Перерисовываем фрактал если нужно - if (this.melting || this.twinkle) { + // Перерисовываем фрактал только если нужно (реже на мобильных) + const shouldRedraw = this.melting || + (this.twinkle && Math.random() < (config.isMobile ? 0.1 : 0.3)); + + if (shouldRedraw) { this.drawFractal(); } } @@ -279,10 +395,20 @@ var fractalSnowstorm = function(window, document) { this.vy = 0; this.rotationSpeed = 0; - // Плавное исчезновение + // На мобильных сразу убираем прилипшие снежинки + if (config.isMobile) { + setTimeout(() => { + if (this.element && this.element.parentNode) { + this.element.parentNode.removeChild(this.element); + } + }, 2000); + return; + } + + // На десктопе плавное исчезновение setTimeout(() => { if (this.element) { - let opacity = this.element.style.opacity || 1; + let opacity = parseFloat(this.element.style.opacity) || 1; const fadeInterval = setInterval(() => { opacity -= 0.02; this.element.style.opacity = opacity; @@ -307,7 +433,11 @@ var fractalSnowstorm = function(window, document) { this.active = true; this.created = Date.now(); this.updateDisplay(); - this.drawFractal(); + + // Перерисовываем только если не на мобильных (для производительности) + if (!config.isMobile) { + this.drawFractal(); + } } destroy() { @@ -345,11 +475,10 @@ var fractalSnowstorm = function(window, document) { function randomizeWind() { windX = (Math.random() - 0.5) * config.vMaxX; - // Гарантируем небольшое движение по горизонтали if (Math.abs(windX) < 0.3) { windX = windX < 0 ? -0.3 : 0.3; } - windY = 0; // Вертикальный ветер не нужен, снег падает сам + windY = 0; } function createSnowflake() { @@ -370,15 +499,14 @@ var fractalSnowstorm = function(window, document) { function animate(currentTime) { if (disabled) return; - // Вычисляем дельту времени для плавной анимации const deltaTime = lastTime ? (currentTime - lastTime) / 1000 : 0.016; lastTime = currentTime; updateScreenSize(); updateScroll(); - // Обновляем ветер (медленные изменения) - if (Math.random() < 0.01) { + // Медленнее меняем ветер на мобильных + if (Math.random() < (config.isMobile ? 0.005 : 0.01)) { windX += (Math.random() - 0.5) * 0.2; windX = Math.max(-config.vMaxX, Math.min(config.vMaxX, windX)); } @@ -395,15 +523,16 @@ var fractalSnowstorm = function(window, document) { } } - // Добавляем новые снежинки если нужно + // Добавляем новые снежинки если нужно (реже на мобильных) + const addChance = config.isMobile ? 0.2 : 0.3; if (activeCount < config.flakesMaxActive && snowflakes.length < config.flakesMax && - Math.random() < 0.3) { + Math.random() < addChance) { createSnowflake(); } - // Удаляем неактивные снежинки (редко) - if (Math.random() < 0.01) { + // Удаляем неактивные снежинки (реже на мобильных) + if (Math.random() < (config.isMobile ? 0.005 : 0.01)) { snowflakes = snowflakes.filter(flake => flake.element && flake.element.parentNode && (flake.active || parseFloat(flake.element.style.opacity) > 0) @@ -423,7 +552,6 @@ var fractalSnowstorm = function(window, document) { function handleResize() { updateScreenSize(); - // При ресайзе немного меняем ветер windX *= 0.9; } @@ -445,29 +573,29 @@ var fractalSnowstorm = function(window, document) { const publicAPI = { start: function() { if (active) { - console.log('Snowstorm уже запущен'); + console.log('Снегопад уже запущен'); return; } - console.log('Запуск снегопада...'); + if (!isSnowSeason()) { + console.log('Снегопад доступен только с 1 декабря по 31 января'); + return; + } + + console.log('Запуск зимнего снегопада...', config.isMobile ? '(мобильная версия)' : '(десктоп версия)'); - // Определяем целевой элемент targetElement = config.targetElement || document.body; - // Проверяем, что целевой элемент существует if (!targetElement) { console.error('Не могу найти целевой элемент'); return; } - // Инициализация updateScreenSize(); randomizeWind(); - // Создаем начальные снежинки createInitialSnowflakes(); - // Настраиваем обработчики событий if (config.followMouse) { window.addEventListener('mousemove', handleMouseMove); } @@ -475,13 +603,12 @@ var fractalSnowstorm = function(window, document) { window.addEventListener('scroll', handleScroll); document.addEventListener('visibilitychange', handleVisibilityChange); - // Запускаем анимацию active = true; disabled = false; lastTime = 0; timer = requestAnimationFrame(animate); - console.log(`Снегопад запущен. Снежинок: ${snowflakes.length}, Экран: ${screenWidth}x${screenHeight}`); + console.log(`Снегопад запущен. Снежинок: ${snowflakes.length}, Размер: ${config.flakeWidth}px`); }, stop: function() { @@ -489,28 +616,23 @@ var fractalSnowstorm = function(window, document) { console.log('Остановка снегопада...'); - // Останавливаем анимацию cancelAnimationFrame(timer); timer = null; - // Удаляем обработчики window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('resize', handleResize); window.removeEventListener('scroll', handleScroll); document.removeEventListener('visibilitychange', handleVisibilityChange); - // Удаляем все снежинки snowflakes.forEach(flake => flake.destroy()); snowflakes = []; active = false; - console.log('Снегопад остановлен'); }, pause: function() { if (disabled) return; disabled = true; - console.log('Снегопад приостановлен'); }, resume: function() { @@ -520,7 +642,6 @@ var fractalSnowstorm = function(window, document) { if (active) { timer = requestAnimationFrame(animate); } - console.log('Снегопад возобновлен'); }, toggle: function() { @@ -534,7 +655,6 @@ var fractalSnowstorm = function(window, document) { updateConfig: function(newConfig) { Object.assign(config, newConfig); if (active) { - // Пересоздаем снежинки при изменении конфига this.stop(); setTimeout(() => this.start(), 100); } @@ -549,22 +669,30 @@ var fractalSnowstorm = function(window, document) { totalFlakes: snowflakes.length, activeFlakes: snowflakes.filter(f => f.active).length, screenSize: { width: screenWidth, height: screenHeight }, - wind: { x: windX, y: windY } + wind: { x: windX, y: windY }, + isMobile: config.isMobile, + isSeason: isSnowSeason() }; + }, + + isSeason: function() { + return isSnowSeason(); } }; - // Автозапуск - if (config.autoStart) { + // Автозапуск только в сезон + if (config.autoStart && isSnowSeason()) { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { - console.log('DOM загружен, запускаю снегопад...'); - setTimeout(() => publicAPI.start(), 500); + console.log('DOM загружен, запускаю зимний снегопад...'); + setTimeout(() => publicAPI.start(), 1000); }); } else { - console.log('DOM уже загружен, запускаю снегопад...'); - setTimeout(() => publicAPI.start(), 500); + console.log('Запускаю зимний снегопад...'); + setTimeout(() => publicAPI.start(), 1000); } + } else if (config.autoStart) { + console.log('Сейчас не сезон для снегопада (только с 1 декабря по 31 января)'); } return publicAPI; @@ -573,14 +701,23 @@ var fractalSnowstorm = function(window, document) { // Создаем глобальный объект для доступа window.fractalSnowstorm = fractalSnowstorm; -// Простая версия для быстрого запуска (если сложная не работает) -window.simpleSnowfall = function() { - console.log('Запуск простого снегопада...'); +// Альтернативная простая версия (тоже с сезонным ограничением) +window.createSimpleSnow = function() { + // Проверяем сезон + const now = new Date(); + const month = now.getMonth() + 1; + if (!(month === 12 || month === 1 || (month === 2 && now.getDate() === 1))) { + console.log('Простой снегопад доступен только зимой'); + return { stop: function() {} }; + } + + const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); + const sizeMultiplier = isMobile ? 0.4 : 1; const config = { - flakes: 100, - speed: 3, - size: 20, + flakes: isMobile ? 50 : 150, + speed: 2 * sizeMultiplier, + size: 15 * sizeMultiplier, color: '#ffffff', zIndex: 999999 }; @@ -595,21 +732,25 @@ window.simpleSnowfall = function() { height: 100%; pointer-events: none; z-index: ${config.zIndex}; + overflow: hidden; `; document.body.appendChild(container); // Создаем простые снежинки for (let i = 0; i < config.flakes; i++) { const flake = document.createElement('div'); + const flakeSize = config.size * (0.5 + Math.random() * 0.5); + const opacity = 0.3 + Math.random() * 0.5; + flake.style.cssText = ` position: absolute; - width: ${config.size}px; - height: ${config.size}px; + width: ${flakeSize}px; + height: ${flakeSize}px; background: ${config.color}; border-radius: 50%; - opacity: ${0.3 + Math.random() * 0.7}; - filter: blur(${Math.random() * 3}px); - top: -${config.size}px; + opacity: ${opacity}; + filter: blur(${Math.random() * 2}px); + top: -${flakeSize}px; left: ${Math.random() * 100}%; `; @@ -620,11 +761,16 @@ window.simpleSnowfall = function() { x: Math.random() * window.innerWidth, y: Math.random() * -window.innerHeight, speed: config.speed * (0.5 + Math.random()), - sway: (Math.random() - 0.5) * 2 + sway: (Math.random() - 0.5) * 1.5 * sizeMultiplier, + size: flakeSize, + rotation: Math.random() * Math.PI * 2, + rotationSpeed: (Math.random() - 0.5) * 0.02 }); } // Анимация + let animationId = null; + function animate() { const width = window.innerWidth; const height = window.innerHeight; @@ -632,31 +778,49 @@ window.simpleSnowfall = function() { flakes.forEach(flake => { flake.y += flake.speed; flake.x += flake.sway * Math.sin(flake.y * 0.01); + flake.rotation += flake.rotationSpeed; - // Если снежинка ушла за нижний край, возвращаем ее наверх + // Если снежинка ушла за нижний край if (flake.y > height) { - flake.y = -config.size; + flake.y = -flake.size; flake.x = Math.random() * width; } - // Если снежинка ушла за боковые края, возвращаем с другой стороны - if (flake.x > width) flake.x = 0; - if (flake.x < 0) flake.x = width; + // Если снежинка ушла за боковые края + if (flake.x > width + flake.size) flake.x = -flake.size; + if (flake.x < -flake.size) flake.x = width + flake.size; - flake.element.style.transform = `translate(${flake.x}px, ${flake.y}px)`; + flake.element.style.transform = ` + translate(${flake.x}px, ${flake.y}px) + rotate(${flake.rotation}rad) + `; + + // Легкое мерцание + if (Math.random() < 0.01) { + flake.element.style.opacity = 0.3 + Math.random() * 0.4; + } }); - requestAnimationFrame(animate); + animationId = requestAnimationFrame(animate); } animate(); return { stop: function() { - container.remove(); + if (animationId) { + cancelAnimationFrame(animationId); + } + if (container.parentNode) { + container.parentNode.removeChild(container); + } } }; }; -// Если фрактальный снег не работает, можно вызвать простой вариант: -// window.simpleSnowfall(); \ No newline at end of file +// Автоматический запуск простой версии если фрактальная не доступна +if (window.fractalSnowstorm && window.fractalSnowstorm.isSeason && window.fractalSnowstorm.isSeason()) { + console.log('Зимний сезон - снегопад будет запущен автоматически'); +} else { + console.log('Сейчас не зимний сезон для снегопада'); +} \ No newline at end of file