Падающие листья и другие, на свой выбор & Black Russia Forum

Adds persistent falling particles (sakura, leaves, etc.) with advanced controls (wind, glow, selection) to forum.blackrussia.online

当前为 2025-04-11 提交的版本,查看 最新版本

// ==UserScript==
// @name         Падающие листья и другие, на свой выбор & Black Russia Forum
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description  Adds persistent falling particles (sakura, leaves, etc.) with advanced controls (wind, glow, selection) to forum.blackrussia.online
// @author       M. Ageev Purpl [06]
// @match        https://forum.blackrussia.online/*
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- КОНФИГУРАЦИЯ И НАСТРОЙКИ ПО УМОЛЧАНИЮ ---
    const allParticleTypes = ['🌸', '🍁', '🍂', '🍃', '🌿', '❄️', '✨']; // Все доступные типы частиц
    const defaultSettings = {
        effectEnabled: true,        // Эффект включен по умолчанию
        panelVisible: false,        // Панель скрыта по умолчанию
        density: 50,                // Плотность
        speed: 1.5,                 // Скорость
        size: 20,                   // Размер
        wind: 0.3,                  // Сила ветра
        glowEnabled: false,         // Свечение выключено
        selectedTypes: ['🌸', '🍁', '🍂', '🍃'], // Какие типы выбраны по умолчанию
    };

    // --- ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ ---
    let settings = {}; // Текущие настройки (загрузятся или будут по умолчанию)
    let particles = [];
    let canvas, ctx;
    let animationFrameId;
    let controlPanel, settingsButton; // Элементы UI

    // --- УПРАВЛЕНИЕ НАСТРОЙКАМИ (СОХРАНЕНИЕ/ЗАГРУЗКА) ---
    async function loadSettings() {
        const savedSettings = await GM_getValue('fallingLeavesSettings_v2', defaultSettings);
        // Проверка на случай, если сохраненная структура неполная (после обновления скрипта)
        settings = { ...defaultSettings, ...savedSettings };
        // Убедимся, что selectedTypes - это массив
        if (!Array.isArray(settings.selectedTypes)) {
             settings.selectedTypes = defaultSettings.selectedTypes;
        }
        console.log("Falling Leaves Settings Loaded:", settings);
    }

    async function saveSettings() {
        // Не сохраняем массив частиц или контекст canvas
        const settingsToSave = { ...settings };
        await GM_setValue('fallingLeavesSettings_v2', settingsToSave);
        // console.log("Falling Leaves Settings Saved:", settingsToSave);
    }

    // --- СОЗДАНИЕ И НАСТРОЙКА CANVAS ---
    function setupCanvas() {
        canvas = document.createElement('canvas');
        document.body.appendChild(canvas);
        ctx = canvas.getContext('2d');

        canvas.style.position = 'fixed';
        canvas.style.top = '0';
        canvas.style.left = '0';
        canvas.style.width = '100%';
        canvas.style.height = '100%';
        canvas.style.zIndex = '9998'; // Чуть ниже панели управления
        canvas.style.pointerEvents = 'none';
        canvas.style.display = settings.effectEnabled ? 'block' : 'none'; // Учитываем настройку вкл/выкл

        resizeCanvas();
        window.addEventListener('resize', resizeCanvas);
    }

    function resizeCanvas() {
        if (canvas) {
            canvas.width = window.innerWidth;
            canvas.height = window.innerHeight;
        }
    }

    // --- УПРАВЛЕНИЕ ЧАСТИЦАМИ ---
     function createParticle(index) {
         // Выбираем тип только из выбранных пользователем
         const availableTypes = settings.selectedTypes.length > 0 ? settings.selectedTypes : [allParticleTypes[0]]; // Если ничего не выбрано, берем первый тип
         const char = availableTypes[Math.floor(Math.random() * availableTypes.length)];

         const sizeVariation = (Math.random() - 0.5) * settings.size * 0.5; // +-25% variation
         const speedVariation = (Math.random() - 0.5) * settings.speed * 0.5; // +-25% variation
         const baseSpeed = Math.max(0.5, Math.min(5, settings.speed + speedVariation)); // Ограничиваем скорость

         return {
             x: Math.random() * canvas.width,
             y: -Math.random() * canvas.height * 0.5, // Начинают чуть выше экрана
             char: char,
             size: Math.max(10, Math.min(40, settings.size + sizeVariation)), // Ограничиваем размер
             speedY: baseSpeed,
             // Скорость ветра зависит от настройки и немного случайна
             speedX: (settings.wind / 2 + (Math.random() - 0.5) * settings.wind) * baseSpeed * 0.5,
             opacity: 0.7 + Math.random() * 0.3,
             rotation: Math.random() * 360,
             rotationSpeed: (Math.random() - 0.5) * 2 // Скорость вращения
         };
     }

    function createParticles(num) {
        particles = [];
        const targetNum = Math.min(Math.max(0, num), 300); // Ограничение 0-300
        for (let i = 0; i < targetNum; i++) {
            particles.push(createParticle(i));
        }
    }

     function updateParticles() {
         if (!ctx || !settings.effectEnabled) return;

         particles.forEach((p, index) => {
             p.y += p.speedY;
             p.x += p.speedX;
             p.rotation += p.rotationSpeed;

             // Возвращаем частицу наверх, если она ушла за пределы экрана
             // Увеличиваем буферную зону для X, чтобы учесть ветер
             if (p.y > canvas.height + p.size || p.x < -p.size - Math.abs(settings.wind * 50) || p.x > canvas.width + p.size + Math.abs(settings.wind * 50)) {
                 particles[index] = createParticle(index);
                 particles[index].y = -p.size; // Сразу над экраном
                 particles[index].x = Math.random() * canvas.width; // Случайная X позиция
             }
         });
     }

    function drawParticles() {
        if (!ctx || !settings.effectEnabled) return;
        ctx.clearRect(0, 0, canvas.width, canvas.height);

        // Настройки свечения
        if (settings.glowEnabled) {
            ctx.shadowBlur = 10; // Размер свечения
            ctx.shadowColor = 'rgba(255, 255, 220, 0.6)'; // Цвет свечения (бледно-желтый)
        } else {
            ctx.shadowBlur = 0; // Убираем свечение
            ctx.shadowColor = 'transparent';
        }

        particles.forEach(p => {
            ctx.save();
            ctx.font = `${p.size}px Arial`;
            ctx.globalAlpha = p.opacity;

            // Центрируем вращение и рисуем
            ctx.translate(p.x + p.size / 2, p.y + p.size / 2);
            ctx.rotate(p.rotation * Math.PI / 180);
            ctx.fillText(p.char, -p.size / 2, p.size / 2); // Рисуем относительно центра

            ctx.restore();
        });

        // Сбрасываем тень после отрисовки всех частиц, если она была включена
        if (settings.glowEnabled) {
            ctx.shadowBlur = 0;
        }
        ctx.globalAlpha = 1.0; // Сбрасываем прозрачность
    }

    // --- АНИМАЦИОННЫЙ ЦИКЛ ---
    function animate() {
        updateParticles();
        drawParticles();
        animationFrameId = requestAnimationFrame(animate);
    }

    function stopAnimation() {
        if (animationFrameId) {
            cancelAnimationFrame(animationFrameId);
            animationFrameId = null;
            if (ctx) {
                ctx.clearRect(0, 0, canvas.width, canvas.height);
            }
        }
    }

    function startAnimation() {
        if (!animationFrameId && settings.effectEnabled && settings.density > 0) {
             // Пересоздаем частицы при старте, чтобы они соответствовали настройкам
             createParticles(settings.density);
             animate();
        }
    }

     // --- УПРАВЛЕНИЕ ЭФФЕКТОМ (ВКЛ/ВЫКЛ) ---
     function setEffectEnabled(enabled) {
         settings.effectEnabled = enabled;
         if (canvas) {
             canvas.style.display = enabled ? 'block' : 'none';
         }
         if (enabled) {
             startAnimation();
         } else {
             stopAnimation();
         }
         updateControlsState(); // Обновить состояние контролов в панели
         saveSettings();
     }

    // --- ПАНЕЛЬ УПРАВЛЕНИЯ И ИКОНКА НАСТРОЕК ---
    function createSettingsButton() {
        settingsButton = document.createElement('button');
        settingsButton.id = 'fallingLeavesSettingsButton';
        settingsButton.innerHTML = '⚙️'; // Иконка шестеренки
        settingsButton.title = 'Настройки падающих частиц';
        settingsButton.style.position = 'fixed';
        settingsButton.style.bottom = '10px';
        settingsButton.style.left = '10px';
        settingsButton.style.zIndex = '10001'; // Выше всего, кроме самой панели
        settingsButton.style.background = 'rgba(0, 0, 0, 0.6)';
        settingsButton.style.color = 'white';
        settingsButton.style.border = 'none';
        settingsButton.style.borderRadius = '50%';
        settingsButton.style.width = '40px';
        settingsButton.style.height = '40px';
        settingsButton.style.fontSize = '20px';
        settingsButton.style.lineHeight = '40px';
        settingsButton.style.textAlign = 'center';
        settingsButton.style.cursor = 'pointer';
        settingsButton.style.boxShadow = '0 2px 5px rgba(0,0,0,0.3)';
        settingsButton.style.transition = 'transform 0.2s ease';

        settingsButton.addEventListener('mouseover', () => { settingsButton.style.transform = 'scale(1.1)'; });
        settingsButton.addEventListener('mouseout', () => { settingsButton.style.transform = 'scale(1)'; });

        settingsButton.addEventListener('click', toggleControlPanel);

        document.body.appendChild(settingsButton);
    }

    function createControlPanel() {
        controlPanel = document.createElement('div');
        controlPanel.id = 'fallingLeavesControlPanel';
        // Стили панели (похожи на предыдущую версию, но управляются через `display`)
        controlPanel.style.position = 'fixed';
        controlPanel.style.bottom = '60px'; // Выше иконки настроек
        controlPanel.style.left = '10px';
        controlPanel.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
        controlPanel.style.color = 'white';
        controlPanel.style.padding = '15px';
        controlPanel.style.borderRadius = '8px';
        controlPanel.style.zIndex = '10000'; // Чуть ниже иконки
        controlPanel.style.fontFamily = 'Arial, sans-serif';
        controlPanel.style.fontSize = '13px';
        controlPanel.style.minWidth = '250px';
        controlPanel.style.boxShadow = '0 4px 10px rgba(0,0,0,0.4)';
        controlPanel.style.display = settings.panelVisible ? 'block' : 'none'; // Начальное состояние видимости
        controlPanel.style.transition = 'opacity 0.3s ease, transform 0.3s ease';

        // --- Стили для мобильных устройств ---
        const styleSheet = document.createElement("style");
        styleSheet.type = "text/css";
        styleSheet.innerText = `
            #fallingLeavesControlPanel { /* Добавим transition для плавности */
                 transition: opacity 0.2s ease-out, visibility 0.2s ease-out;
                 opacity: ${settings.panelVisible ? '1' : '0'};
                 visibility: ${settings.panelVisible ? 'visible' : 'hidden'};
            }
             #fallingLeavesControlPanel.visible {
                 opacity: 1;
                 visibility: visible;
             }
            @media (max-width: 600px) {
                #fallingLeavesControlPanel {
                    font-size: 12px;
                    padding: 10px;
                    min-width: 200px;
                    bottom: 55px; /* Чуть ниже на мобильных */
                }
                 #fallingLeavesControlPanel .control-row label,
                 #fallingLeavesControlPanel .particle-type-selector label {
                     display: block; /* Метки над элементами */
                     margin-bottom: 3px;
                 }
                 #fallingLeavesControlPanel input[type="range"] { width: 100%; }
                 #fallingLeavesControlPanel .particle-type-selector { grid-template-columns: repeat(3, 1fr); gap: 5px;} /* 3 колонки для чекбоксов */
            }
            #fallingLeavesControlPanel input[type="range"] {
                width: 130px; height: 6px; vertical-align: middle; margin: 0 5px; cursor: pointer;
            }
            #fallingLeavesControlPanel .control-row, #fallingLeavesControlPanel .checkbox-row {
                 margin-bottom: 8px; display: flex; align-items: center; flex-wrap: wrap;
            }
             #fallingLeavesControlPanel .control-row label { min-width: 70px; display: inline-block; }
             #fallingLeavesControlPanel .value-display { min-width: 30px; display: inline-block; text-align: right; font-weight: bold; }
             #fallingLeavesControlPanel hr { border: none; border-top: 1px solid rgba(255,255,255,0.2); margin: 10px 0; }
             #fallingLeavesControlPanel input[type="checkbox"] { margin-right: 5px; cursor: pointer; vertical-align: middle;}
             #fallingLeavesControlPanel .particle-type-selector {
                  margin-top: 5px;
                  display: grid; /* Используем grid для чекбоксов */
                  grid-template-columns: repeat(4, auto); /* 4 колонки */
                  gap: 8px; /* Промежуток между чекбоксами */
                  font-size: 16px; /* Размер эмодзи */
             }
             #fallingLeavesControlPanel .particle-type-selector label {
                  display: flex;
                  align-items: center;
                  cursor: pointer;
             }
        `;
        document.head.appendChild(styleSheet);


        // --- Элементы управления ---

         // 1. Вкл/Выкл Эффекта (главный)
         const enableRow = document.createElement('div');
         enableRow.className = 'checkbox-row';
         const enableLabel = document.createElement('label');
         enableLabel.htmlFor = 'effectEnabledCheckbox';
         enableLabel.textContent = 'Включить эффект:';
         enableLabel.style.fontWeight = 'bold';
         const enableCheckbox = document.createElement('input');
         enableCheckbox.type = 'checkbox';
         enableCheckbox.id = 'effectEnabledCheckbox';
         enableCheckbox.checked = settings.effectEnabled;
         enableCheckbox.addEventListener('change', (e) => {
             setEffectEnabled(e.target.checked);
         });
         enableRow.appendChild(enableCheckbox);
         enableRow.appendChild(enableLabel);
         controlPanel.appendChild(enableRow);
         controlPanel.appendChild(document.createElement('hr'));


        // 2. Плотность
        const densityRow = createSliderRow('Плотность:', 'densitySlider', 0, 300, settings.density, 1, (value) => {
            settings.density = value;
            // Пересоздаем частицы немедленно при изменении плотности
            createParticles(settings.density);
            if (settings.density === 0 && animationFrameId) stopAnimation();
            else if (settings.density > 0 && !animationFrameId && settings.effectEnabled) startAnimation();
             saveSettings();
        });
        controlPanel.appendChild(densityRow);


        // 3. Скорость
        const speedRow = createSliderRow('Скорость:', 'speedSlider', 0.1, 5, settings.speed, 0.1, (value) => {
            settings.speed = value;
            saveSettings();
            // Скорость обновится для новых/пересозданных частиц
        });
        controlPanel.appendChild(speedRow);


        // 4. Размер
        const sizeRow = createSliderRow('Размер:', 'sizeSlider', 10, 40, settings.size, 1, (value) => {
            settings.size = value;
            saveSettings();
            // Размер обновится для новых/пересозданных частиц
        });
        controlPanel.appendChild(sizeRow);


        // 5. Ветер
        const windRow = createSliderRow('Ветер:', 'windSlider', -2, 2, settings.wind, 0.1, (value) => {
            settings.wind = value;
            saveSettings();
            // Ветер обновится для новых/пересозданных частиц
        });
        controlPanel.appendChild(windRow);
        controlPanel.appendChild(document.createElement('hr'));


         // 6. Свечение
         const glowRow = document.createElement('div');
         glowRow.className = 'checkbox-row';
         const glowLabel = document.createElement('label');
         glowLabel.htmlFor = 'glowCheckbox';
         glowLabel.textContent = 'Свечение частиц:';
         const glowCheckbox = document.createElement('input');
         glowCheckbox.type = 'checkbox';
         glowCheckbox.id = 'glowCheckbox';
         glowCheckbox.checked = settings.glowEnabled;
         glowCheckbox.addEventListener('change', (e) => {
             settings.glowEnabled = e.target.checked;
             saveSettings();
             // Эффект применится при следующей отрисовке
         });
         glowRow.appendChild(glowCheckbox);
         glowRow.appendChild(glowLabel);
         controlPanel.appendChild(glowRow);


        // 7. Выбор типов частиц
        const typesFieldset = document.createElement('fieldset');
        typesFieldset.style.border = '1px solid rgba(255,255,255,0.3)';
        typesFieldset.style.borderRadius = '4px';
        typesFieldset.style.padding = '5px 10px 10px 10px';
        typesFieldset.style.marginTop = '10px';
        const typesLegend = document.createElement('legend');
        typesLegend.textContent = 'Типы частиц';
        typesLegend.style.padding = '0 5px';
        typesFieldset.appendChild(typesLegend);

        const typesContainer = document.createElement('div');
        typesContainer.className = 'particle-type-selector';

        allParticleTypes.forEach(type => {
            const typeLabel = document.createElement('label');
            const typeCheckbox = document.createElement('input');
            typeCheckbox.type = 'checkbox';
            typeCheckbox.value = type;
            typeCheckbox.checked = settings.selectedTypes.includes(type);
            typeCheckbox.addEventListener('change', (e) => {
                 const char = e.target.value;
                 if (e.target.checked) {
                     if (!settings.selectedTypes.includes(char)) {
                         settings.selectedTypes.push(char);
                     }
                 } else {
                     settings.selectedTypes = settings.selectedTypes.filter(t => t !== char);
                 }
                 // Не позволяем убрать последний выбранный тип (опционально)
                 // if (settings.selectedTypes.length === 0 && allParticleTypes.length > 0) {
                 //     e.target.checked = true; // Возвращаем галочку
                 //     settings.selectedTypes.push(char); // Добавляем обратно
                 //     alert("Должен быть выбран хотя бы один тип частиц.");
                 //     return;
                 // }
                 saveSettings();
                 // Изменения вступят в силу при пересоздании частиц
            });
            typeLabel.appendChild(typeCheckbox);
            typeLabel.appendChild(document.createTextNode(type)); // Добавляем сам символ
            typesContainer.appendChild(typeLabel);
        });
        typesFieldset.appendChild(typesContainer);
        controlPanel.appendChild(typesFieldset);


        document.body.appendChild(controlPanel);
        updateControlsState(); // Обновляем состояние контролов при создании
    }

    // Вспомогательная функция для создания строки со слайдером
    function createSliderRow(labelText, id, min, max, value, step, onChange) {
        const row = document.createElement('div');
        row.className = 'control-row';

        const label = document.createElement('label');
        label.htmlFor = id;
        label.textContent = labelText;

        const slider = document.createElement('input');
        slider.type = 'range';
        slider.id = id;
        slider.min = min.toString();
        slider.max = max.toString();
        slider.step = step.toString();
        slider.value = value.toString();

        const valueDisplay = document.createElement('span');
        valueDisplay.className = 'value-display';
        // Форматируем значение в зависимости от шага
        valueDisplay.textContent = Number.isInteger(step) ? value.toString() : value.toFixed(1);

        slider.addEventListener('input', (e) => {
            const newValue = parseFloat(e.target.value);
            valueDisplay.textContent = Number.isInteger(step) ? newValue.toString() : newValue.toFixed(1);
            onChange(newValue); // Вызываем колбэк с новым значением
        });

        row.appendChild(label);
        row.appendChild(slider);
        row.appendChild(valueDisplay);

        return row;
    }

    function toggleControlPanel() {
        settings.panelVisible = !settings.panelVisible;
        if (controlPanel) {
            // Используем классы для управления видимостью через CSS
             controlPanel.style.display = 'block'; // Сначала делаем блочным, чтобы transition сработал
             requestAnimationFrame(() => { // Ждем следующего кадра для применения стилей
                 if (settings.panelVisible) {
                     controlPanel.style.opacity = '1';
                     controlPanel.style.visibility = 'visible';
                 } else {
                     controlPanel.style.opacity = '0';
                     controlPanel.style.visibility = 'hidden';
                     // Можно добавить задержку перед display: none, если нужно
                      // setTimeout(() => { if(!settings.panelVisible) controlPanel.style.display = 'none'; }, 200);
                 }
             });

        }
        saveSettings();
    }

    // Обновление состояния (disabled) контролов в зависимости от главного переключателя
    function updateControlsState() {
         if (!controlPanel) return;
         const controlsToDisable = controlPanel.querySelectorAll('input:not(#effectEnabledCheckbox), fieldset');
         controlsToDisable.forEach(control => {
             control.disabled = !settings.effectEnabled;
             control.style.opacity = settings.effectEnabled ? '1' : '0.5';
             control.style.cursor = settings.effectEnabled ? '' : 'not-allowed';
         });
    }

    // --- ИНИЦИАЛИЗАЦИЯ ---
    async function init() {
        console.log("Falling Leaves Script Initializing (v2.0)...");
        await loadSettings(); // Загружаем настройки ПЕРЕД созданием UI
        setupCanvas();
        createControlPanel(); // Панель создается всегда
        createSettingsButton(); // Кнопка настроек создается всегда
        if (settings.effectEnabled) {
             startAnimation(); // Запускаем анимацию, если она включена в настройках
        }
        console.log("Falling Leaves Script Ready!");
    }

    // Запускаем скрипт после загрузки страницы
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init(); // Инициализировать немедленно
    }

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址