您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds persistent falling particles (sakura, leaves, etc.) with advanced controls (wind, glow, selection) to forum.blackrussia.online
当前为
// ==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或关注我们的公众号极客氢云获取最新地址