├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── assets ├── backgrounds_1.png ├── backgrounds_2.png ├── main-preview.gif ├── node-links_2.gif ├── node-links_options.png ├── snowfall.gif └── snowfall_4.png ├── js ├── aether-snow.js ├── background-themes.js ├── link_animations.js └── react-integration │ └── comfy-react-integration.js └── pyproject.toml /.gitattributes: -------------------------------------------------------------------------------- 1 | images/*.png filter=lfs diff=lfs merge=lfs -text 2 | images/*.jpg filter=lfs diff=lfs merge=lfs -text 3 | images/*.jpeg filter=lfs diff=lfs merge=lfs -text 4 | images/*.webp filter=lfs diff=lfs merge=lfs -text 5 | images/*.gif filter=lfs diff=lfs merge=lfs -text -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | #.idea/ 169 | 170 | # PyPI configuration file 171 | .pypirc 172 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Æmotion 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ComfyUI Christmas Theme 🎄✨ 2 | 3 | ![ComfyUI Christmas Theme Overview](assets/main-preview.gif) 4 | 5 | A beautiful theme extension for ComfyUI that adds a festive touch to your workflow with dynamic backgrounds, snowfall effects, and animated node connections. 6 | 7 | ## Features 🌟 8 | 9 | ### 1. Christmas Node Links 🎄 10 | ![Node Link Animations](assets/node-links_2.gif) 11 | ![Node Link Animations](assets/node-links_options.png) 12 | - Animated light effects along connection links 13 | - Multiple color schemes: 14 | - Traditional Christmas 🎄 15 | - Warm White ☀️ 16 | - Cool White ❄️ 17 | - Multicolor 🌈 18 | - Pastel 🎀 19 | - New Year's Eve 🎉 20 | - Customizable light effects: 21 | - Light size adjustment 22 | - Glow intensity control 23 | - Flow direction control 24 | - Multiple link styles (spline, straight, linear, hidden) 25 | - Light animation effects: 26 | - Steady 27 | - Gentle Twinkle 28 | - Sparkle 29 | 30 | ### 2. Dynamic Background Effects 🌌 31 | ![Background Themes](assets/backgrounds_1.png) 32 | ![Background Themes](assets/backgrounds_2.png) 33 | - Animated starry background with smooth transitions 34 | - Multiple color themes: 35 | - Classic Night 🌌 36 | - Christmas Forest 🎄 37 | - Candy Cane Red 🍬 38 | - Frost Night ❄️ 39 | - Gingerbread 🍪 40 | - Dark Night 🌑 41 | 42 | ### 3. Snowfall Effect ❄️ 43 | ![Snowfall Effect](assets/snowfall.gif) 44 | ![Snowfall Effect](assets/snowfall_4.png) 45 | - Beautiful falling snowflakes with smooth animations 46 | - Customizable snowflake colors: 47 | - Classic White ❄️ 48 | - Ice Blue 💠 49 | - Rainbow 🌈 50 | - Match Theme Colors 🎨 51 | - New Year's Eve 🎉 52 | - Adjustable snowflake glow intensity 53 | - Performance-optimized rendering with batch processing 54 | 55 | ### 4. Performance Features ⚡ 56 | - Smart performance monitoring and optimization 57 | - Automatic animation pausing during workflow execution 58 | - Optimized React-based rendering system 59 | - Batch processing for efficient animation 60 | - Smart state management for smooth transitions 61 | - Memory-efficient implementation 62 | 63 | ## Installation 📦 64 | 65 | 1. Clone this repository into your ComfyUI custom_nodes directory: 66 | ```bash 67 | cd ComfyUI/custom_nodes 68 | git clone https://github.com/AEmotionStudio/ComfyUI-ChristmasTheme 69 | ``` 70 | 71 | 2. Restart ComfyUI to load the extension 72 | 73 | ## Settings ⚙️ 74 | 75 | ### Christmas Effects Settings 76 | - **Christmas Lights**: Toggle node link effects on/off 77 | - **Color Scheme**: Choose from multiple themes: 78 | - Traditional Christmas 79 | - Warm White 80 | - Cool White 81 | - Multicolor 82 | - Pastel 83 | - New Year's Eve 84 | - **Light Effect**: Select animation style: 85 | - Steady 86 | - Gentle Twinkle 87 | - Sparkle 88 | - **Light Size**: Adjust the size of light bulbs 89 | - **Glow Intensity**: Control the glow effect strength 90 | - **Flow Direction**: Choose forward or reverse animation 91 | - **Link Style**: Select connection style (spline, straight, linear, hidden) 92 | 93 | ### Background Theme Settings 94 | - **Background Effect**: Toggle background on/off 95 | - **Color Theme**: Choose from various atmospheric themes: 96 | - Classic Night 97 | - Christmas Forest 98 | - Candy Cane Red 99 | - Frost Night 100 | - Gingerbread 101 | - Dark Night 102 | 103 | ### Snow Effect Settings 104 | - **Snow Effect**: Toggle snow on/off 105 | - **Snowflake Color**: Choose from various color schemes: 106 | - White (Default) 107 | - Blue 108 | - Rainbow 109 | - Match (matches current theme colors) 110 | - New Year 111 | - **Snowflake Glow**: Adjust the glow intensity of snowflakes 112 | 113 | ### Performance Settings 114 | - **Pause Effects During Render**: Automatically pause animations during workflow execution for optimal performance 115 | 116 | ## Technical Details 🔧 117 | 118 | - Built with a custom lightweight React implementation for optimal performance 119 | - Uses Canvas for efficient background rendering 120 | - Implements efficient batch processing for snowflake animations 121 | - Smart performance monitoring with automatic optimizations 122 | - Responsive design that adapts to the ComfyUI window size 123 | 124 | ## Compatibility 🤝 125 | 126 | - Tested with ComfyUI's latest version 127 | - Compatible with other UI extensions 128 | - Lightweight implementation with minimal performance impact 129 | 130 | ## Contributing 🤝 131 | 132 | Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change. 133 | 134 | ## Acknowledgments 👏 135 | 136 | - Thanks to the ComfyUI team for creating an amazing platform 137 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ComfyUI Christmas Theme Extension 3 | """ 4 | 5 | NODE_CLASS_MAPPINGS = {} 6 | NODE_DISPLAY_NAME_MAPPINGS = {} 7 | 8 | WEB_DIRECTORY = "./js" 9 | 10 | __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] -------------------------------------------------------------------------------- /assets/backgrounds_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AEmotionStudio/ComfyUI-ChristmasTheme/27bcffda135f7aaa88aeb438855335814529d112/assets/backgrounds_1.png -------------------------------------------------------------------------------- /assets/backgrounds_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AEmotionStudio/ComfyUI-ChristmasTheme/27bcffda135f7aaa88aeb438855335814529d112/assets/backgrounds_2.png -------------------------------------------------------------------------------- /assets/main-preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AEmotionStudio/ComfyUI-ChristmasTheme/27bcffda135f7aaa88aeb438855335814529d112/assets/main-preview.gif -------------------------------------------------------------------------------- /assets/node-links_2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AEmotionStudio/ComfyUI-ChristmasTheme/27bcffda135f7aaa88aeb438855335814529d112/assets/node-links_2.gif -------------------------------------------------------------------------------- /assets/node-links_options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AEmotionStudio/ComfyUI-ChristmasTheme/27bcffda135f7aaa88aeb438855335814529d112/assets/node-links_options.png -------------------------------------------------------------------------------- /assets/snowfall.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AEmotionStudio/ComfyUI-ChristmasTheme/27bcffda135f7aaa88aeb438855335814529d112/assets/snowfall.gif -------------------------------------------------------------------------------- /assets/snowfall_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AEmotionStudio/ComfyUI-ChristmasTheme/27bcffda135f7aaa88aeb438855335814529d112/assets/snowfall_4.png -------------------------------------------------------------------------------- /js/aether-snow.js: -------------------------------------------------------------------------------- 1 | // aether-snow.js 2 | import { app } from "../../scripts/app.js"; 3 | import { loadReactDependencies } from "./react-integration/comfy-react-integration.js"; 4 | 5 | const SNOWFLAKE_CONFIG = { 6 | MIN_SIZE: 8, 7 | MAX_SIZE: 24, 8 | TOTAL_FLAKES: 50, 9 | BASE_OPACITY: 0.8, 10 | FALL_DURATION: { 11 | MIN: 25, 12 | MAX: 35 13 | }, 14 | BATCH_SIZE: 10 15 | }; 16 | 17 | const SNOWFLAKE_CHARS = [ 18 | '❅', // Current snowflake 19 | '❆', // Heavier snowflake 20 | '❄', // Classic snowflake 21 | ]; 22 | 23 | app.registerExtension({ 24 | name: "Christmas.Theme.SnowEffect", // Changed name to match theme namespace 25 | async setup() { 26 | console.log("✨ Initializing Snow Effect..."); 27 | 28 | try { 29 | // Load React dependencies 30 | const { React: MiniReact, ReactDOM: MiniReactDOM } = await loadReactDependencies(); 31 | 32 | // Create container 33 | const container = document.createElement('div'); 34 | container.id = 'comfy-aether-snow'; 35 | container.style.cssText = 'position: fixed; top: -10vh; left: 0; width: 100%; height: 200vh; overflow: hidden; pointer-events: none; z-index: 3;'; 36 | document.body.appendChild(container); 37 | 38 | // Create root for React rendering 39 | const root = MiniReactDOM.createRoot(container); 40 | 41 | const getSnowflakeColor = () => { 42 | const colorScheme = app.ui.settings.getSettingValue("ChristmasTheme.Snowflake.ColorScheme", "white"); 43 | const christmasColors = app.ui.settings.getSettingValue("ChristmasTheme.ChristmasEffects.ColorScheme", "traditional"); 44 | 45 | switch(colorScheme) { 46 | case "blue": 47 | return '#B0E2FF'; 48 | case "rainbow": 49 | const rainbowPalette = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#ffeead', '#d4a5a5']; 50 | return rainbowPalette[Math.floor(Math.random() * rainbowPalette.length)]; 51 | case "match": 52 | const colorSchemes = { 53 | traditional: ['#ff0000', '#00ff00', '#ffff00', '#0000ff', '#ffffff'], 54 | warm: ['#ffd700', '#ffb347', '#ffa07a', '#ff8c69', '#fff0f5'], 55 | cool: ['#f0ffff', '#e0ffff', '#b0e2ff', '#87cefa', '#b0c4de'], 56 | multicolor: ['#ff1493', '#00ff7f', '#ff4500', '#4169e1', '#9370db'], 57 | pastel: ['#ffb6c1', '#98fb98', '#87ceeb', '#dda0dd', '#f0e68c'], 58 | newyear: ['#00ffff', '#ff1493', '#ffd700', '#4b0082', '#7fff00'] 59 | }; 60 | const selectedPalette = colorSchemes[christmasColors] || colorSchemes.traditional; 61 | return selectedPalette[Math.floor(Math.random() * selectedPalette.length)]; 62 | case "newyear": 63 | return ['#00ffff', '#ff1493', '#ffd700', '#4b0082', '#7fff00'][Math.floor(Math.random() * 5)]; 64 | default: 65 | return '#FFFFFF'; 66 | } 67 | }; 68 | 69 | // Add styles 70 | const style = document.createElement('style'); 71 | style.textContent = ` 72 | @keyframes snowfall { 73 | 0% { 74 | transform: translate3d(0, -10vh, 0); 75 | opacity: 0; 76 | } 77 | 3% { 78 | opacity: ${SNOWFLAKE_CONFIG.BASE_OPACITY}; 79 | } 80 | 25% { 81 | transform: translate3d(15px, 20vh, 0); 82 | opacity: ${SNOWFLAKE_CONFIG.BASE_OPACITY}; 83 | } 84 | 50% { 85 | transform: translate3d(-15px, 50vh, 0); 86 | opacity: ${SNOWFLAKE_CONFIG.BASE_OPACITY}; 87 | } 88 | 75% { 89 | transform: translate3d(15px, 75vh, 0); 90 | opacity: ${SNOWFLAKE_CONFIG.BASE_OPACITY}; 91 | } 92 | 95% { 93 | opacity: ${SNOWFLAKE_CONFIG.BASE_OPACITY}; 94 | } 95 | 100% { 96 | transform: translate3d(0, 110vh, 0); 97 | opacity: 0; 98 | } 99 | } 100 | 101 | .snowflake { 102 | position: absolute; 103 | pointer-events: none; 104 | user-select: none; 105 | will-change: transform; 106 | z-index: 3; 107 | backface-visibility: hidden; 108 | animation-timing-function: linear; 109 | animation-fill-mode: forwards; 110 | animation-iteration-count: infinite; 111 | contain: layout style; 112 | } 113 | 114 | #comfy-aether-snow { 115 | contain: strict; 116 | transform: translateZ(0); 117 | } 118 | `; 119 | document.head.appendChild(style); 120 | 121 | // Create snowflakes 122 | let flakes = []; 123 | const totalFlakes = SNOWFLAKE_CONFIG.TOTAL_FLAKES; 124 | const batchSize = SNOWFLAKE_CONFIG.BATCH_SIZE; 125 | let currentBatch = 0; 126 | let isInitializing = true; 127 | 128 | // Update the render function to use React root 129 | const renderSnowflakes = () => { 130 | root.render( 131 | MiniReact.createElement('div', { 132 | style: { 133 | position: 'relative', 134 | width: '100%', 135 | height: '100%', 136 | transform: 'translateZ(0)' 137 | } 138 | }, flakes) 139 | ); 140 | }; 141 | 142 | // Update addBatch to use the new render function 143 | const addBatch = () => { 144 | if (currentBatch * batchSize >= totalFlakes) { 145 | isInitializing = false; // Mark initialization as complete 146 | return; 147 | } 148 | 149 | const start = currentBatch * batchSize; 150 | const end = Math.min(start + batchSize, totalFlakes); 151 | 152 | for (let i = start; i < end; i++) { 153 | flakes.push(createSnowflake(i)); 154 | } 155 | 156 | renderSnowflakes(); 157 | 158 | currentBatch++; 159 | if (currentBatch * batchSize < totalFlakes) { 160 | setTimeout(addBatch, 100); 161 | } 162 | }; 163 | 164 | const createSnowflake = (id) => { 165 | const size = Math.random() * (SNOWFLAKE_CONFIG.MAX_SIZE - SNOWFLAKE_CONFIG.MIN_SIZE) + SNOWFLAKE_CONFIG.MIN_SIZE; 166 | const duration = Math.random() * (SNOWFLAKE_CONFIG.FALL_DURATION.MAX - SNOWFLAKE_CONFIG.FALL_DURATION.MIN) + SNOWFLAKE_CONFIG.FALL_DURATION.MIN; 167 | const startPosition = Math.random() * 100; 168 | const color = getSnowflakeColor(); 169 | const glowIntensity = app.ui.settings.getSettingValue("ChristmasTheme.Snowflake.Glow", 10); 170 | 171 | // Add initial delay only during initialization 172 | const initialDelay = isInitializing ? Math.random() * duration : 0; 173 | 174 | return MiniReact.createElement('div', { 175 | class: 'snowflake', 176 | style: { 177 | left: `${startPosition}vw`, 178 | top: '0', 179 | fontSize: `${size}px`, 180 | animation: `snowfall ${duration}s linear infinite`, 181 | animationDelay: `${initialDelay}s`, 182 | color: color, 183 | textShadow: `0 0 ${glowIntensity}px ${color}`, 184 | transform: 'translateZ(0)' 185 | } 186 | }, SNOWFLAKE_CHARS[Math.floor(Math.random() * SNOWFLAKE_CHARS.length)]); 187 | }; 188 | 189 | // Store state in window object for access from settings 190 | window.snowflakeState = { 191 | flakes, 192 | currentBatch, 193 | isInitializing, 194 | renderSnowflakes, 195 | addBatch, 196 | getSnowflakeColor, // Add the color function to the state 197 | updateSnowflakeColors: () => { 198 | // Update colors of existing snowflakes 199 | const snowflakeElements = document.querySelectorAll('.snowflake'); 200 | snowflakeElements.forEach(flake => { 201 | const newColor = getSnowflakeColor(); 202 | flake.style.color = newColor; 203 | const glowIntensity = app.ui.settings.getSettingValue("ChristmasTheme.Snowflake.Glow", 10); 204 | flake.style.textShadow = `0 0 ${glowIntensity}px ${newColor}`; 205 | }); 206 | }, 207 | updateSnowflakeGlow: (value) => { 208 | // Update glow of existing snowflakes 209 | const snowflakeElements = document.querySelectorAll('.snowflake'); 210 | snowflakeElements.forEach(flake => { 211 | flake.style.textShadow = `0 0 ${value}px ${flake.style.color}`; 212 | }); 213 | } 214 | }; 215 | 216 | // Register settings after variables are defined 217 | const snowContainer = document.getElementById('comfy-aether-snow'); 218 | if (snowContainer) { 219 | // Set initial state based on setting 220 | const isEnabled = app.ui.settings.getSettingValue("ChristmasTheme.Snowflake.Enabled", 1); 221 | snowContainer.style.display = isEnabled ? 'block' : 'none'; 222 | 223 | // Initialize snowflakes if enabled 224 | if (isEnabled) { 225 | isInitializing = true; 226 | addBatch(); 227 | } 228 | } 229 | 230 | // Track the last known values 231 | let lastKnownColorScheme = app.ui.settings.getSettingValue("ChristmasTheme.ChristmasEffects.ColorScheme", "traditional"); 232 | let lastKnownGlowValue = app.ui.settings.getSettingValue("ChristmasTheme.Snowflake.Glow", 10); 233 | let lastKnownSnowflakeColorScheme = app.ui.settings.getSettingValue("ChristmasTheme.Snowflake.ColorScheme", "white"); 234 | let lastUpdateTime = Date.now(); 235 | 236 | // Update snowflakes periodically to reflect settings changes 237 | const updateInterval = setInterval(() => { 238 | const currentTime = Date.now(); 239 | const timeSinceLastUpdate = currentTime - lastUpdateTime; 240 | 241 | // Only update if enough time has passed (prevent too frequent updates) 242 | if (timeSinceLastUpdate < 100) return; 243 | 244 | const currentSnowSetting = app.ui.settings.getSettingValue("ChristmasTheme.Snowflake.Enabled", 1); 245 | const currentColorScheme = app.ui.settings.getSettingValue("ChristmasTheme.ChristmasEffects.ColorScheme", "traditional"); 246 | const currentSnowflakeColorScheme = app.ui.settings.getSettingValue("ChristmasTheme.Snowflake.ColorScheme", "white"); 247 | const currentGlowValue = app.ui.settings.getSettingValue("ChristmasTheme.Snowflake.Glow", 10); 248 | 249 | // Check for any changes 250 | if (currentSnowflakeColorScheme !== lastKnownSnowflakeColorScheme) { 251 | window.snowflakeState.updateSnowflakeColors(); 252 | lastKnownSnowflakeColorScheme = currentSnowflakeColorScheme; 253 | lastUpdateTime = currentTime; 254 | } 255 | 256 | if (currentGlowValue !== lastKnownGlowValue) { 257 | window.snowflakeState.updateSnowflakeGlow(currentGlowValue); 258 | lastKnownGlowValue = currentGlowValue; 259 | lastUpdateTime = currentTime; 260 | } 261 | 262 | // Check if we need to update due to color scheme changes when in match mode 263 | const needsColorUpdate = currentSnowSetting === 1 && 264 | currentSnowflakeColorScheme === "match" && 265 | currentColorScheme !== lastKnownColorScheme; 266 | 267 | if (needsColorUpdate) { 268 | window.snowflakeState.updateSnowflakeColors(); 269 | lastKnownColorScheme = currentColorScheme; 270 | lastUpdateTime = currentTime; 271 | } else if (currentSnowSetting === 0) { 272 | // Ensure snowflakes stay hidden when disabled 273 | flakes = []; 274 | currentBatch = 0; 275 | renderSnowflakes(); 276 | if (snowContainer) { 277 | snowContainer.style.display = 'none'; 278 | } 279 | } 280 | }, 100); // Check more frequently for smoother updates 281 | 282 | // Initial render - respect the current setting 283 | const snowEnabled = app.ui.settings.getSettingValue("ChristmasTheme.Snowflake.Enabled", 1); 284 | if (snowEnabled === 1) { 285 | addBatch(); 286 | } else { 287 | // Make sure container is hidden if snow is disabled 288 | container.style.display = 'none'; 289 | } 290 | 291 | return () => { 292 | clearInterval(updateInterval); 293 | container.remove(); 294 | style.remove(); 295 | }; 296 | } catch (error) { 297 | console.error("❌ Failed to initialize Snow Effect:", error); 298 | } 299 | } 300 | }); -------------------------------------------------------------------------------- /js/background-themes.js: -------------------------------------------------------------------------------- 1 | import { app } from "../../scripts/app.js"; 2 | 3 | class EnhancedBackground { 4 | constructor() { 5 | this.container = null; 6 | this.canvas = null; 7 | this.ctx = null; 8 | this.width = window.innerWidth; 9 | this.height = window.innerHeight; 10 | this.entities = []; 11 | this.animationFrame = null; 12 | this.initialized = false; 13 | 14 | // Bind methods 15 | this._boundRender = this.animate.bind(this); 16 | this._boundUpdateCanvasSize = this.updateCanvasSize.bind(this); 17 | } 18 | 19 | async init() { 20 | console.log("Initializing Enhanced Background..."); 21 | try { 22 | // Check if background is enabled 23 | if (!app.ui.settings.getSettingValue("ChristmasTheme.Background.Enabled", true)) { 24 | return false; 25 | } 26 | 27 | // Remove any existing background containers first 28 | this.cleanup(); 29 | 30 | // Create container 31 | this.container = document.createElement("div"); 32 | this.container.id = "enhanced-background-container"; 33 | Object.assign(this.container.style, { 34 | position: "fixed", 35 | top: "0", 36 | left: "0", 37 | width: "100vw", 38 | height: "100vh", 39 | zIndex: "0", 40 | pointerEvents: "none", 41 | userSelect: "none", 42 | overflow: "hidden" 43 | }); 44 | 45 | // Create canvas 46 | this.canvas = document.createElement("canvas"); 47 | this.canvas.id = "enhanced-background-canvas"; 48 | Object.assign(this.canvas.style, { 49 | width: "100%", 50 | height: "100%", 51 | opacity: "0.25", 52 | display: "block" 53 | }); 54 | 55 | this.container.appendChild(this.canvas); 56 | 57 | // Wait for app.canvas to be available 58 | if (!app.canvas?.canvas) { 59 | console.log("Waiting for app.canvas..."); 60 | await new Promise(resolve => { 61 | const checkCanvas = () => { 62 | if (app.canvas?.canvas) { 63 | resolve(); 64 | } else { 65 | setTimeout(checkCanvas, 100); 66 | } 67 | }; 68 | checkCanvas(); 69 | }); 70 | } 71 | 72 | const graphCanvas = app.canvas.canvas; 73 | if (!graphCanvas.parentElement) { 74 | console.error("Cannot find graph canvas parent element"); 75 | return false; 76 | } 77 | 78 | // Insert before graph canvas 79 | graphCanvas.parentElement.insertBefore(this.container, graphCanvas); 80 | 81 | // Initialize Canvas 2D context 82 | this.ctx = this.canvas.getContext("2d"); 83 | this.updateCanvasSize(); 84 | this.initEntities(); 85 | this.setupEventListeners(); 86 | this.initialized = true; 87 | this.animate(); 88 | 89 | return true; 90 | } catch (error) { 91 | console.error("Error during initialization:", error); 92 | return false; 93 | } 94 | } 95 | 96 | initEntities() { 97 | // Star class 98 | class Star { 99 | constructor(options) { 100 | this.size = Math.random() * 2.5; 101 | this.speed = Math.random() * 0.02; 102 | this.x = options.x; 103 | this.y = options.y; 104 | this.brightness = 0.35 + Math.random() * 0.2; 105 | this.twinkleSpeed = 0.005 + Math.random() * 0.01; 106 | this.twinklePhase = Math.random() * Math.PI * 2; 107 | this.twinkleRange = 0.15 + Math.random() * 0.15; 108 | } 109 | 110 | reset() { 111 | this.size = Math.random() * 2.5; 112 | this.speed = Math.random() * 0.02; 113 | this.x = this.width; 114 | this.y = Math.random() * this.height; 115 | this.brightness = 0.35 + Math.random() * 0.2; 116 | this.twinkleSpeed = 0.005 + Math.random() * 0.01; 117 | this.twinkleRange = 0.15 + Math.random() * 0.15; 118 | } 119 | 120 | update(ctx, width, height) { 121 | this.x -= this.speed; 122 | if (this.x < 0) { 123 | this.width = width; 124 | this.height = height; 125 | this.reset(); 126 | } else { 127 | this.twinklePhase += this.twinkleSpeed; 128 | const twinkle = (Math.sin(this.twinklePhase) + 1) * 0.5; 129 | const alpha = this.brightness - (this.twinkleRange * twinkle); 130 | 131 | ctx.globalAlpha = alpha; 132 | ctx.fillRect(this.x, this.y, this.size, this.size); 133 | ctx.globalAlpha = 1.0; 134 | } 135 | } 136 | } 137 | 138 | // Initialize stars 139 | this.entities = []; 140 | for (let i = 0; i < this.height * 0.7; i++) { 141 | this.entities.push(new Star({ 142 | x: Math.random() * this.width, 143 | y: Math.random() * this.height 144 | })); 145 | } 146 | } 147 | 148 | setupEventListeners() { 149 | if (this._resizeObserver) { 150 | this._resizeObserver.disconnect(); 151 | } 152 | this._resizeObserver = new ResizeObserver(this._boundUpdateCanvasSize); 153 | this._resizeObserver.observe(this.container); 154 | } 155 | 156 | updateCanvasSize() { 157 | if (!this.ctx || !this.canvas || !this.container) return; 158 | const devicePixelRatio = Math.min(window.devicePixelRatio, 2); 159 | this.width = this.container.clientWidth * devicePixelRatio; 160 | this.height = this.container.clientHeight * devicePixelRatio; 161 | 162 | if (this.canvas.width !== this.width || this.canvas.height !== this.height) { 163 | this.canvas.width = this.width; 164 | this.canvas.height = this.height; 165 | } 166 | } 167 | 168 | animate() { 169 | if (!this.ctx || !this.initialized) return; 170 | 171 | try { 172 | // Get background colors from settings 173 | const colorTheme = app.ui.settings.getSettingValue("ChristmasTheme.Background.ColorTheme", "classic"); 174 | const themes = { 175 | classic: { top: '#05004c', bottom: '#110E19', star: '#ffffff' }, 176 | christmas: { top: '#1a472a', bottom: '#0d2115', star: '#ffffff' }, 177 | candycane: { top: '#8b0000', bottom: '#4a0404', star: '#ffffff' }, 178 | frostnight: { top: '#0a2351', bottom: '#051428', star: '#e0ffff' }, 179 | gingerbread: { top: '#8b4513', bottom: '#3c1f0d', star: '#ffd700' }, 180 | darknight: { top: '#000000', bottom: '#000000', star: '#808080' } 181 | }; 182 | 183 | const { top, bottom, star } = themes[colorTheme] || themes.classic; 184 | 185 | // Clear and set background with a gradient 186 | const gradient = this.ctx.createLinearGradient(0, 0, 0, this.height); 187 | gradient.addColorStop(0, top); 188 | gradient.addColorStop(1, bottom); 189 | this.ctx.fillStyle = gradient; 190 | this.ctx.fillRect(0, 0, this.width, this.height); 191 | 192 | // Set styles for stars 193 | this.ctx.fillStyle = star; 194 | this.ctx.strokeStyle = star; 195 | 196 | // Update all entities 197 | for (let i = 0; i < this.entities.length; i++) { 198 | this.entities[i].update(this.ctx, this.width, this.height); 199 | } 200 | } catch (error) { 201 | console.error("Render error:", error); 202 | } 203 | 204 | this.animationFrame = requestAnimationFrame(this._boundRender); 205 | } 206 | 207 | cleanup() { 208 | // Cancel animation frame 209 | if (this.animationFrame) { 210 | cancelAnimationFrame(this.animationFrame); 211 | this.animationFrame = null; 212 | } 213 | 214 | // Disconnect resize observer 215 | if (this._resizeObserver) { 216 | this._resizeObserver.disconnect(); 217 | this._resizeObserver = null; 218 | } 219 | 220 | // Remove any existing background containers 221 | const existingContainer = document.getElementById('enhanced-background-container'); 222 | if (existingContainer) { 223 | existingContainer.remove(); 224 | } 225 | 226 | this.initialized = false; 227 | this.ctx = null; 228 | this.canvas = null; 229 | this.container = null; 230 | this.entities = []; 231 | } 232 | 233 | stop() { 234 | this.cleanup(); 235 | } 236 | } 237 | 238 | // Create and export the effect instance 239 | let backgroundInstance = null; 240 | 241 | app.registerExtension({ 242 | name: "Comfy.EnhancedBackground", 243 | async setup() { 244 | console.log("🎨 Setting up Enhanced Background extension..."); 245 | 246 | // Clean up any existing instance 247 | if (backgroundInstance) { 248 | backgroundInstance.stop(); 249 | backgroundInstance = null; 250 | } 251 | 252 | // Add settings 253 | app.ui.settings.addSetting({ 254 | id: "ChristmasTheme.Background.Enabled", 255 | name: "🌟 Background Effect", 256 | type: "combo", 257 | options: [ 258 | { value: true, text: "✨ On" }, 259 | { value: false, text: "⭘ Off" } 260 | ], 261 | defaultValue: true, 262 | section: "Background Theme", 263 | onChange: async (value) => { 264 | if (backgroundInstance) { 265 | backgroundInstance.stop(); 266 | backgroundInstance = null; 267 | } 268 | if (value) { 269 | backgroundInstance = new EnhancedBackground(); 270 | await backgroundInstance.init(); 271 | } 272 | } 273 | }); 274 | 275 | app.ui.settings.addSetting({ 276 | id: "ChristmasTheme.Background.ColorTheme", 277 | name: "🎨 Color Theme", 278 | type: "combo", 279 | options: [ 280 | { value: "classic", text: "🌌 Classic Night" }, 281 | { value: "christmas", text: "🎄 Christmas Forest" }, 282 | { value: "candycane", text: "🍬 Candy Cane Red" }, 283 | { value: "frostnight", text: "❄️ Frost Night" }, 284 | { value: "gingerbread", text: "🍪 Gingerbread" }, 285 | { value: "darknight", text: "🌑 Dark Night" } 286 | ], 287 | defaultValue: "classic", 288 | section: "Background Theme", 289 | onChange: async () => { 290 | // Reinitialize the background with new colors 291 | if (backgroundInstance && app.ui.settings.getSettingValue("ChristmasTheme.Background.Enabled", true)) { 292 | await backgroundInstance.init(); 293 | } 294 | } 295 | }); 296 | 297 | // Create initial instance if enabled 298 | if (app.ui.settings.getSettingValue("ChristmasTheme.Background.Enabled", true)) { 299 | backgroundInstance = new EnhancedBackground(); 300 | await backgroundInstance.init(); 301 | } 302 | 303 | // Return cleanup function 304 | return () => { 305 | if (backgroundInstance) { 306 | backgroundInstance.stop(); 307 | backgroundInstance = null; 308 | } 309 | }; 310 | } 311 | }); -------------------------------------------------------------------------------- /js/link_animations.js: -------------------------------------------------------------------------------- 1 | import { app } from "../../../scripts/app.js"; 2 | import { api } from "../../../scripts/api.js"; 3 | 4 | app.registerExtension({ 5 | name: "Christmas.Theme.LightSwitch", 6 | async setup() { 7 | // 🔮 Basic Constants 8 | const PHI = 1.618033988749895; 9 | 10 | // Add Performance Monitoring 11 | const PerformanceMonitor = { 12 | frameTimeHistory: new Array(60).fill(0), 13 | currentIndex: 0, 14 | warningThreshold: 16.67, // 60fps threshold 15 | criticalThreshold: 33.33, // 30fps threshold 16 | 17 | addFrameTime(time) { 18 | this.frameTimeHistory[this.currentIndex] = time; 19 | this.currentIndex = (this.currentIndex + 1) % this.frameTimeHistory.length; 20 | 21 | const avgFrameTime = this.frameTimeHistory.reduce((a, b) => a + b) / this.frameTimeHistory.length; 22 | 23 | if (avgFrameTime > this.criticalThreshold) { 24 | console.warn('Performance Critical: Average frame time:', avgFrameTime.toFixed(2), 'ms'); 25 | return 'critical'; 26 | } else if (avgFrameTime > this.warningThreshold) { 27 | console.warn('Performance Warning: Average frame time:', avgFrameTime.toFixed(2), 'ms'); 28 | return 'warning'; 29 | } 30 | return 'normal'; 31 | } 32 | }; 33 | 34 | // Add Performance Settings 35 | app.ui.settings.addSetting({ 36 | id: "ChristmasTheme.PauseDuringRender", 37 | name: "⏸️ Pause Effects During Render", 38 | type: "combo", 39 | options: [ 40 | {value: true, text: "✅ Enabled"}, 41 | {value: false, text: "❌ Disabled"} 42 | ], 43 | defaultValue: true, 44 | section: "Performance", 45 | tooltip: "Pause animations during rendering to improve performance" 46 | }); 47 | 48 | // Reusable Arrays Pool 49 | const ArrayPool = { 50 | float32Pool: [], 51 | 52 | getFloat32Array(size) { 53 | if (this.float32Pool.length > 0) { 54 | return this.float32Pool.pop(); 55 | } 56 | return new Float32Array(2); 57 | }, 58 | 59 | releaseFloat32Array(array) { 60 | if (this.float32Pool.length < 100) { // Limit pool size 61 | this.float32Pool.push(array); 62 | } 63 | } 64 | }; 65 | 66 | // ⚡ State Management System 67 | const State = { 68 | isRunning: false, 69 | phase: 0, 70 | lastFrame: performance.now(), 71 | animationFrame: null, 72 | renderQueue: new Map(), // Reuse render queue 73 | performanceMode: 'normal', 74 | isRendering: false // Add this new state property 75 | }; 76 | 77 | // 🎭 Animation State Controller 78 | const AnimationState = { 79 | targetPhase: 0, 80 | Direction: 1, 81 | transitionSpeed: PHI, 82 | smoothFactor: 0.95, 83 | 84 | update(delta) { 85 | const flowDirection = app.ui.settings.getSettingValue("ChristmasTheme.ChristmasEffects.Direction", 1); 86 | 87 | if (this.Direction !== flowDirection) { 88 | this.Direction = flowDirection; 89 | this.targetPhase = State.phase + Math.PI * 2 * this.Direction; 90 | } 91 | 92 | const phaseStep = this.transitionSpeed * delta * PHI; 93 | 94 | if (Math.abs(this.targetPhase - State.phase) > 0.01) { 95 | State.phase += Math.sign(this.targetPhase - State.phase) * phaseStep; 96 | } else { 97 | State.phase = (State.phase + phaseStep * this.Direction) % (Math.PI * 2); 98 | this.targetPhase = State.phase; 99 | } 100 | 101 | return State.phase; 102 | } 103 | }; 104 | 105 | // ⚙️ Performance-Optimized Timing System 106 | const TimingManager = { 107 | smoothDelta: 0, 108 | frameCount: 0, 109 | 110 | update() { 111 | const now = performance.now(); 112 | const rawDelta = Math.min((now - State.lastFrame) / 1000, 1/30); 113 | State.lastFrame = now; 114 | 115 | this.frameCount++; 116 | this.smoothDelta = this.smoothDelta * AnimationState.smoothFactor + 117 | rawDelta * (1 - AnimationState.smoothFactor); 118 | return this.smoothDelta; 119 | } 120 | }; 121 | 122 | // 🎨 Christmas Animation Settings 123 | app.ui.settings.addSetting({ 124 | id: "ChristmasTheme.ChristmasEffects.LightSwitch", 125 | name: "🎄 Christmas Lights", 126 | type: "combo", 127 | options: [ 128 | {value: 0, text: "⭘️ Off"}, 129 | {value: 1, text: "🎄 On"} 130 | ], 131 | defaultValue: 1, 132 | section: "Christmas Effects" 133 | }); 134 | 135 | app.ui.settings.addSetting({ 136 | id: "ChristmasTheme.ChristmasEffects.ColorScheme", 137 | name: "🎨 Color Scheme", 138 | type: "combo", 139 | options: [ 140 | {value: "traditional", text: " 🎄 Traditional"}, 141 | {value: "warm", text: " 🔆 Warm White"}, 142 | {value: "cool", text: " ❄️ Cool White"}, 143 | {value: "multicolor", text: " 🌈 Multicolor"}, 144 | {value: "pastel", text: " 🎀 Pastel"}, 145 | {value: "newyear", text: " 🎉 New Year's Eve"} 146 | ], 147 | defaultValue: "traditional", 148 | section: "Christmas Effects" 149 | }); 150 | 151 | app.ui.settings.addSetting({ 152 | id: "ChristmasTheme.ChristmasEffects.Twinkle", 153 | name: "✨ Light Effect", 154 | type: "combo", 155 | options: [ 156 | {value: "steady", text: "Steady"}, 157 | {value: "gentle", text: "Gentle Twinkle"}, 158 | {value: "sparkle", text: "Sparkle"} 159 | ], 160 | defaultValue: "gentle", 161 | section: "Christmas Effects" 162 | }); 163 | 164 | app.ui.settings.addSetting({ 165 | id: "ChristmasTheme.ChristmasEffects.Thickness", 166 | name: "💫 Light Size", 167 | type: "slider", 168 | default: 3, 169 | min: 1, 170 | max: 10, 171 | step: 0.5, 172 | section: "Christmas Effects" 173 | }); 174 | 175 | app.ui.settings.addSetting({ 176 | id: "ChristmasTheme.ChristmasEffects.GlowIntensity", 177 | name: "✨ Glow Intensity", 178 | type: "slider", 179 | default: 20, 180 | min: 0, 181 | max: 30, 182 | step: 1, 183 | section: "Christmas Effects" 184 | }); 185 | 186 | app.ui.settings.addSetting({ 187 | id: "ChristmasTheme.ChristmasEffects.Direction", 188 | name: "🔄 Flow Direction", 189 | type: "combo", 190 | options: [ 191 | {value: 1, text: "Forward ➡️"}, 192 | {value: -1, text: "Reverse ⬅️"} 193 | ], 194 | defaultValue: 1, 195 | section: "Christmas Effects" 196 | }); 197 | 198 | app.ui.settings.addSetting({ 199 | id: "ChristmasTheme.Link Style", 200 | name: "🔗 Link Style", 201 | type: "combo", 202 | options: [ 203 | {value: "spline", text: "Spline"}, 204 | {value: "straight", text: "Straight"}, 205 | {value: "linear", text: "Linear"}, 206 | {value: "hidden", text: "Hidden"} 207 | ], 208 | defaultValue: "spline", 209 | section: "Link Style" 210 | }); 211 | 212 | // Add Snowflake Settings 213 | app.ui.settings.addSetting({ 214 | id: "ChristmasTheme.Snowflake.Enabled", 215 | name: "❄️ Snow Effect", 216 | type: "combo", 217 | options: [ 218 | {value: 0, text: "⭘️ Off"}, 219 | {value: 1, text: "❄️ On"} 220 | ], 221 | defaultValue: 1, 222 | section: "Snowflake", 223 | onChange: (value) => { 224 | if (window.snowflakeState) { 225 | if (!value) { 226 | // Clear existing snowflakes 227 | window.snowflakeState.flakes = []; 228 | window.snowflakeState.currentBatch = 0; 229 | window.snowflakeState.renderSnowflakes(); 230 | const snowContainer = document.getElementById('comfy-aether-snow'); 231 | if (snowContainer) { 232 | snowContainer.style.display = 'none'; 233 | } 234 | } else { 235 | // Only start new batch if turning on and no flakes exist 236 | if (window.snowflakeState.flakes.length === 0) { 237 | window.snowflakeState.isInitializing = true; 238 | const snowContainer = document.getElementById('comfy-aether-snow'); 239 | if (snowContainer) { 240 | snowContainer.style.display = 'block'; 241 | } 242 | window.snowflakeState.addBatch(); 243 | } 244 | } 245 | } 246 | } 247 | }); 248 | 249 | app.ui.settings.addSetting({ 250 | id: "ChristmasTheme.Snowflake.ColorScheme", 251 | name: "❄️ Snowflake Color", 252 | type: "combo", 253 | options: [ 254 | {value: "white", text: "❄️ Classic White"}, 255 | {value: "blue", text: "💠 Ice Blue"}, 256 | {value: "rainbow", text: "🌈 Rainbow"}, 257 | {value: "match", text: "🎨 Match Lights"}, 258 | {value: "newyear", text: "🎉 New Year's Eve"} 259 | ], 260 | defaultValue: "white", 261 | section: "Snowflake" 262 | }); 263 | 264 | app.ui.settings.addSetting({ 265 | id: "ChristmasTheme.Snowflake.Glow", 266 | name: "✨ Snowflake Glow", 267 | type: "slider", 268 | default: 10, 269 | min: 0, 270 | max: 20, 271 | step: 1, 272 | section: "Snowflake" 273 | }); 274 | 275 | // 🛠 Override default connection drawing 276 | const origDrawConnections = LGraphCanvas.prototype.drawConnections; 277 | 278 | LGraphCanvas.prototype.drawConnections = function(ctx) { 279 | try { 280 | const startTime = performance.now(); 281 | const animStyle = app.ui.settings.getSettingValue("ChristmasTheme.ChristmasEffects.LightSwitch", 1); 282 | 283 | if (animStyle === 0) { 284 | origDrawConnections.call(this, ctx); 285 | return; 286 | } 287 | 288 | const delta = TimingManager.update(); 289 | const phase = AnimationState.update(delta); 290 | 291 | ctx.save(); 292 | 293 | // Clear existing render queue 294 | State.renderQueue.clear(); 295 | 296 | for (const linkId in this.graph.links) { 297 | const linkData = this.graph.links[linkId]; 298 | if (!linkData) continue; 299 | 300 | const originNode = this.graph._nodes_by_id[linkData.origin_id]; 301 | const targetNode = this.graph._nodes_by_id[linkData.target_id]; 302 | 303 | if (!originNode || !targetNode || originNode.flags.collapsed || targetNode.flags.collapsed) continue; 304 | 305 | const startPos = ArrayPool.getFloat32Array(2); 306 | const endPos = ArrayPool.getFloat32Array(2); 307 | 308 | originNode.getConnectionPos(false, linkData.origin_slot, startPos); 309 | targetNode.getConnectionPos(true, linkData.target_slot, endPos); 310 | 311 | const color = linkData.type ? 312 | LGraphCanvas.link_type_colors[linkData.type] : 313 | this.default_connection_color; 314 | 315 | if (!State.renderQueue.has(1)) { 316 | State.renderQueue.set(1, []); 317 | } 318 | State.renderQueue.get(1).push({ 319 | start: startPos, 320 | end: endPos, 321 | color: color, 322 | linkId: linkId 323 | }); 324 | } 325 | 326 | // Process render queue 327 | State.renderQueue.forEach((items) => { 328 | this.renderChristmasLights(ctx, items, phase); 329 | }); 330 | 331 | // Release arrays back to pool 332 | State.renderQueue.get(1)?.forEach(item => { 333 | ArrayPool.releaseFloat32Array(item.start); 334 | ArrayPool.releaseFloat32Array(item.end); 335 | }); 336 | 337 | ctx.restore(); 338 | 339 | // Monitor performance 340 | const frameTime = performance.now() - startTime; 341 | State.performanceMode = PerformanceMonitor.addFrameTime(frameTime); 342 | 343 | } catch (error) { 344 | console.error("Error in drawConnections:", error, error.stack); 345 | origDrawConnections.call(this, ctx); 346 | } 347 | }; 348 | 349 | // Link rendering functions with path calculations 350 | const LinkRenderers = { 351 | spline: { 352 | getLength: function(start, end) { 353 | const dist = Math.sqrt(Math.pow(end[0] - start[0], 2) + Math.pow(end[1] - start[1], 2)); 354 | const bendDistance = Math.min(dist * 0.5, 100); 355 | 356 | // Increase samples for more accurate length calculation 357 | const samples = 40; 358 | let length = 0; 359 | let prevPoint = this.getPoint(start, end, 0); 360 | 361 | for (let i = 1; i <= samples; i++) { 362 | const t = i / samples; 363 | const point = this.getPoint(start, end, t); 364 | length += Math.sqrt( 365 | Math.pow(point[0] - prevPoint[0], 2) + 366 | Math.pow(point[1] - prevPoint[1], 2) 367 | ); 368 | prevPoint = point; 369 | } 370 | 371 | return length; 372 | }, 373 | 374 | getNormalizedT: function(start, end, targetDist, totalLength) { 375 | // Increase samples for smoother spacing 376 | const samples = 40; 377 | let accumulatedLength = 0; 378 | let prevPoint = this.getPoint(start, end, 0); 379 | 380 | for (let i = 1; i <= samples; i++) { 381 | const t = i / samples; 382 | const point = this.getPoint(start, end, t); 383 | const segmentLength = Math.sqrt( 384 | Math.pow(point[0] - prevPoint[0], 2) + 385 | Math.pow(point[1] - prevPoint[1], 2) 386 | ); 387 | 388 | accumulatedLength += segmentLength; 389 | 390 | if (accumulatedLength >= targetDist) { 391 | const prevT = (i - 1) / samples; 392 | const excess = accumulatedLength - targetDist; 393 | return prevT + ((t - prevT) * (1 - excess / segmentLength)); 394 | } 395 | 396 | prevPoint = point; 397 | } 398 | 399 | return 1; 400 | }, 401 | 402 | getPoint: function(start, end, t) { 403 | const dist = Math.sqrt(Math.pow(end[0] - start[0], 2) + Math.pow(end[1] - start[1], 2)); 404 | const bendDistance = Math.min(dist * 0.5, 100); 405 | 406 | const p0 = { x: start[0], y: start[1] }; 407 | const p1 = { x: start[0] + bendDistance, y: start[1] }; 408 | const p2 = { x: end[0] - bendDistance, y: end[1] }; 409 | const p3 = { x: end[0], y: end[1] }; 410 | 411 | const cx = 3 * (p1.x - p0.x); 412 | const bx = 3 * (p2.x - p1.x) - cx; 413 | const ax = p3.x - p0.x - cx - bx; 414 | 415 | const cy = 3 * (p1.y - p0.y); 416 | const by = 3 * (p2.y - p1.y) - cy; 417 | const ay = p3.y - p0.y - cy - by; 418 | 419 | const x = ax * Math.pow(t, 3) + bx * Math.pow(t, 2) + cx * t + p0.x; 420 | const y = ay * Math.pow(t, 3) + by * Math.pow(t, 2) + cy * t + p0.y; 421 | 422 | return [x, y]; 423 | }, 424 | draw: function(ctx, start, end, color, Thickness) { 425 | const dist = Math.sqrt(Math.pow(end[0] - start[0], 2) + Math.pow(end[1] - start[1], 2)); 426 | const bendDistance = Math.min(dist * 0.5, 100); 427 | 428 | ctx.beginPath(); 429 | ctx.moveTo(start[0], start[1]); 430 | ctx.bezierCurveTo( 431 | start[0] + bendDistance, start[1], 432 | end[0] - bendDistance, end[1], 433 | end[0], end[1] 434 | ); 435 | ctx.strokeStyle = color; 436 | ctx.lineWidth = Thickness * 0.8; 437 | ctx.stroke(); 438 | } 439 | }, 440 | 441 | straight: { 442 | getLength: function(start, end) { 443 | return Math.sqrt( 444 | Math.pow(end[0] - start[0], 2) + 445 | Math.pow(end[1] - start[1], 2) 446 | ); 447 | }, 448 | getNormalizedT: function(start, end, targetDist, totalLength) { 449 | return targetDist / totalLength; 450 | }, 451 | getPoint: function(start, end, t) { 452 | return [ 453 | start[0] + (end[0] - start[0]) * t, 454 | start[1] + (end[1] - start[1]) * t 455 | ]; 456 | }, 457 | draw: function(ctx, start, end, color, Thickness) { 458 | ctx.beginPath(); 459 | ctx.moveTo(start[0], start[1]); 460 | ctx.lineTo(end[0], end[1]); 461 | ctx.strokeStyle = color; 462 | ctx.lineWidth = Thickness * 0.8; 463 | ctx.stroke(); 464 | } 465 | }, 466 | 467 | linear: { 468 | getLength: function(start, end) { 469 | const midX = (start[0] + end[0]) / 2; 470 | const horizontalLength1 = Math.abs(midX - start[0]); 471 | const verticalLength = Math.abs(end[1] - start[1]); 472 | const horizontalLength2 = Math.abs(end[0] - midX); 473 | 474 | return horizontalLength1 + verticalLength + horizontalLength2; 475 | }, 476 | getNormalizedT: function(start, end, targetDist, totalLength) { 477 | const midX = (start[0] + end[0]) / 2; 478 | const horizontalLength1 = Math.abs(midX - start[0]); 479 | const verticalLength = Math.abs(end[1] - start[1]); 480 | const horizontalLength2 = Math.abs(end[0] - midX); 481 | 482 | // Calculate segment proportions of total length 483 | const segment1Proportion = horizontalLength1 / totalLength; 484 | const segment2Proportion = verticalLength / totalLength; 485 | const segment3Proportion = horizontalLength2 / totalLength; 486 | 487 | const normalizedDist = targetDist / totalLength; 488 | 489 | // Determine which segment we're in and calculate appropriate t 490 | if (normalizedDist <= segment1Proportion) { 491 | // First horizontal segment 492 | return (normalizedDist / segment1Proportion) * 0.33; 493 | } else if (normalizedDist <= segment1Proportion + segment2Proportion) { 494 | // Vertical segment 495 | const segmentProgress = (normalizedDist - segment1Proportion) / segment2Proportion; 496 | return 0.33 + (segmentProgress * 0.34); 497 | } else { 498 | // Final horizontal segment 499 | const segmentProgress = (normalizedDist - (segment1Proportion + segment2Proportion)) / segment3Proportion; 500 | return 0.67 + (segmentProgress * 0.33); 501 | } 502 | }, 503 | getPoint: function(start, end, t) { 504 | const midX = (start[0] + end[0]) / 2; 505 | 506 | if (t <= 0.33) { 507 | // First horizontal segment 508 | const segmentT = t / 0.33; 509 | return [ 510 | start[0] + (midX - start[0]) * segmentT, 511 | start[1] 512 | ]; 513 | } else if (t <= 0.67) { 514 | // Vertical segment 515 | const segmentT = (t - 0.33) / 0.34; 516 | return [ 517 | midX, 518 | start[1] + (end[1] - start[1]) * segmentT 519 | ]; 520 | } else { 521 | // Final horizontal segment 522 | const segmentT = (t - 0.67) / 0.33; 523 | return [ 524 | midX + (end[0] - midX) * segmentT, 525 | end[1] 526 | ]; 527 | } 528 | }, 529 | draw: function(ctx, start, end, color, Thickness) { 530 | const midX = (start[0] + end[0]) / 2; 531 | 532 | ctx.beginPath(); 533 | ctx.moveTo(start[0], start[1]); 534 | ctx.lineTo(midX, start[1]); 535 | ctx.lineTo(midX, end[1]); 536 | ctx.lineTo(end[0], end[1]); 537 | ctx.strokeStyle = color; 538 | ctx.lineWidth = Thickness * 0.8; 539 | ctx.stroke(); 540 | } 541 | }, 542 | 543 | hidden: { 544 | getLength: function(start, end) { 545 | return Math.sqrt( 546 | Math.pow(end[0] - start[0], 2) + 547 | Math.pow(end[1] - start[1], 2) 548 | ); 549 | }, 550 | getNormalizedT: function(start, end, targetDist, totalLength) { 551 | return targetDist / totalLength; 552 | }, 553 | getPoint: function(start, end, t) { 554 | return [ 555 | start[0] + (end[0] - start[0]) * t, 556 | start[1] + (end[1] - start[1]) * t 557 | ]; 558 | }, 559 | draw: function(ctx, start, end, color, Thickness) { 560 | // Don't render anything 561 | } 562 | } 563 | }; 564 | 565 | // 🎄 Christmas Lights Pattern 566 | LGraphCanvas.prototype.renderChristmasLights = function(ctx, items, phase) { 567 | const Direction = AnimationState.Direction; 568 | const Thickness = app.ui.settings.getSettingValue("ChristmasTheme.ChristmasEffects.Thickness", 2); 569 | const glowIntensity = app.ui.settings.getSettingValue("ChristmasTheme.ChristmasEffects.GlowIntensity", 10); 570 | const colorScheme = app.ui.settings.getSettingValue("ChristmasTheme.ChristmasEffects.ColorScheme", "traditional"); 571 | const twinkleMode = app.ui.settings.getSettingValue("ChristmasTheme.ChristmasEffects.Twinkle", "gentle"); 572 | const linkStyle = app.ui.settings.getSettingValue("ChristmasTheme.Link Style", "spline"); 573 | 574 | // Color schemes 575 | const colorSchemes = { 576 | traditional: [ 577 | '#ff0000', // Red 578 | '#00ff00', // Green 579 | '#ffff00', // Yellow 580 | '#0000ff', // Blue 581 | '#ffffff' // White 582 | ], 583 | warm: [ 584 | '#ffd700', // Warm gold 585 | '#ffb347', // Warm yellow 586 | '#ffa07a', // Light salmon 587 | '#ff8c69', // Salmon 588 | '#fff0f5' // Warm white 589 | ], 590 | cool: [ 591 | '#f0ffff', // Cool white 592 | '#e0ffff', // Light cyan 593 | '#b0e2ff', // Light blue 594 | '#87cefa', // Light sky blue 595 | '#b0c4de' // Light steel blue 596 | ], 597 | multicolor: [ 598 | '#ff1493', // Deep pink 599 | '#00ff7f', // Spring green 600 | '#ff4500', // Orange red 601 | '#4169e1', // Royal blue 602 | '#9370db' // Medium purple 603 | ], 604 | pastel: [ 605 | '#ffb6c1', // Light pink 606 | '#98fb98', // Pale green 607 | '#87ceeb', // Sky blue 608 | '#dda0dd', // Plum 609 | '#f0e68c' // Khaki 610 | ], 611 | newyear: [ 612 | '#00ffff', // Electric Cyan/Neon Blue 613 | '#ff1493', // Deep Pink/Hot Pink 614 | '#ffd700', // Gold 615 | '#4b0082', // Indigo (night sky) 616 | '#7fff00' // Electric Lime 617 | ] 618 | }; 619 | 620 | // Get current zoom level for spacing adjustment 621 | const zoomLevel = this.ds ? this.ds.scale : 1; 622 | 623 | items.forEach(({start, end, color}) => { 624 | // Draw base string (wire) with original link color 625 | if (linkStyle !== 'hidden') { 626 | ctx.globalAlpha = 0.8; 627 | ctx.shadowBlur = 0; 628 | LinkRenderers[linkStyle].draw(ctx, start, end, color, Thickness); 629 | ctx.globalAlpha = 1; 630 | } 631 | 632 | if (linkStyle === 'hidden' && !app.ui.settings.getSettingValue("ChristmasTheme.ChristmasEffects.LightSwitch", 1)) { 633 | return; 634 | } 635 | 636 | // Calculate total path length for even spacing 637 | const totalLength = LinkRenderers[linkStyle].getLength(start, end); 638 | const baseSpacing = 30; 639 | const numLights = Math.floor(totalLength / baseSpacing); 640 | 641 | // Select colors based on scheme 642 | const christmasColors = colorSchemes[colorScheme] || colorSchemes.traditional; 643 | 644 | // Configure twinkle effect 645 | const getTwinkle = (i, phase) => { 646 | switch(twinkleMode) { 647 | case "steady": 648 | return 1; 649 | case "gentle": 650 | return 0.85 + Math.sin(-phase * 5 + i * 3) * 0.15; 651 | case "sparkle": 652 | return 0.7 + Math.sin(-phase * 8 + i * 5) * 0.3 * Math.random(); 653 | default: 654 | return 0.85 + Math.sin(-phase * 5 + i * 3) * 0.15; 655 | } 656 | }; 657 | 658 | // Draw lights with even spacing 659 | const baseBatchSize = 5; 660 | const dynamicBatchSize = Math.max( 661 | baseBatchSize, 662 | Math.floor(numLights / (60 / TimingManager.smoothDelta)) 663 | ); 664 | const batchSize = Math.min(dynamicBatchSize, Math.ceil(numLights * 0.2)); 665 | 666 | for (let batch = 0; batch <= numLights; batch += batchSize) { 667 | const endBatch = Math.min(batch + batchSize, numLights + 1); 668 | 669 | for (let i = batch; i < endBatch; i++) { 670 | // Calculate target distance along path for this light 671 | const targetDist = (i / numLights) * totalLength; 672 | // Get normalized t parameter for even spacing 673 | const t = LinkRenderers[linkStyle].getNormalizedT(start, end, targetDist, totalLength); 674 | const wobble = Math.sin(t * Math.PI * 4 - phase * Direction * 0) * 5; 675 | 676 | const pos = LinkRenderers[linkStyle].getPoint(start, end, t); 677 | const x = pos[0]; 678 | const y = pos[1] + wobble; 679 | 680 | // Light color cycling - match the Direction 681 | const colorIndex = (i - Math.floor(phase * 2 * Direction)) % christmasColors.length; 682 | const adjustedIndex = colorIndex < 0 ? christmasColors.length + colorIndex : colorIndex; 683 | const lightColor = christmasColors[adjustedIndex]; 684 | 685 | // Apply twinkle effect 686 | const flicker = getTwinkle(i, phase); 687 | 688 | // Light bulb with glow 689 | ctx.beginPath(); 690 | ctx.shadowBlur = glowIntensity * 1.5 * flicker; 691 | ctx.arc(x, y, Thickness * 1.5, 0, Math.PI * 2); 692 | ctx.fillStyle = lightColor; 693 | ctx.shadowColor = lightColor; 694 | ctx.globalAlpha = flicker; 695 | ctx.fill(); 696 | 697 | // Light cap 698 | ctx.beginPath(); 699 | ctx.shadowBlur = 0; // No Glow on cap 700 | ctx.arc(x, y - Thickness, Thickness * 0.5, 0, Math.PI * 2); 701 | ctx.fillStyle = '#silver'; 702 | ctx.globalAlpha = 1; 703 | ctx.fill(); 704 | } 705 | } 706 | ctx.globalAlpha = 1; 707 | }); 708 | }; 709 | 710 | // 🔄 Improved State Management System 711 | const WorkflowState = { 712 | isRendering: false, 713 | isExecuting: false, 714 | jobCount: 0, 715 | lastQueueLength: 0, 716 | executionStartTime: 0, 717 | 718 | checkState() { 719 | // Safely check queue length and execution state 720 | const currentQueueLength = (app.queue && Array.isArray(app.queue)) ? app.queue.length : 0; 721 | const isGraphRunning = app.graph && (app.graph._is_running === true); 722 | 723 | // Check if we're still within the minimum execution time window 724 | const minExecutionTime = 5000; // 5 seconds minimum execution time 725 | const isWithinExecutionWindow = (Date.now() - this.executionStartTime) < minExecutionTime; 726 | 727 | const isProcessing = this.isRendering || 728 | this.isExecuting || 729 | isGraphRunning || 730 | this.jobCount > 0 || 731 | currentQueueLength > 0 || 732 | isWithinExecutionWindow; 733 | 734 | // Debug state 735 | if (isProcessing) { 736 | console.log("Processing state:", { 737 | isRendering: this.isRendering, 738 | isExecuting: this.isExecuting, 739 | graphRunning: isGraphRunning, 740 | jobCount: this.jobCount, 741 | queueLength: currentQueueLength, 742 | executionTime: Date.now() - this.executionStartTime, 743 | isWithinWindow: isWithinExecutionWindow 744 | }); 745 | } 746 | 747 | return isProcessing; 748 | }, 749 | 750 | startExecution() { 751 | this.isRendering = true; 752 | this.isExecuting = true; 753 | this.jobCount++; 754 | this.executionStartTime = Date.now(); 755 | console.log("Starting execution at:", this.executionStartTime); 756 | }, 757 | 758 | reset() { 759 | // Safely check conditions 760 | const queueLength = (app.queue && Array.isArray(app.queue)) ? app.queue.length : 0; 761 | const isGraphRunning = app.graph && (app.graph._is_running === true); 762 | const minExecutionTime = 5000; // 5 seconds minimum execution time 763 | const isWithinExecutionWindow = (Date.now() - this.executionStartTime) < minExecutionTime; 764 | 765 | console.log("Reset check:", { 766 | queueLength, 767 | isGraphRunning, 768 | executionTime: Date.now() - this.executionStartTime, 769 | isWithinWindow: isWithinExecutionWindow 770 | }); 771 | 772 | // Only reset if all conditions are met 773 | if (queueLength === 0 && !isGraphRunning && !isWithinExecutionWindow) { 774 | console.log("Reset conditions met, resuming animations"); 775 | this.isRendering = false; 776 | this.isExecuting = false; 777 | this.jobCount = 0; 778 | 779 | // Force animation resume 780 | this.resumeAnimations(); 781 | } else { 782 | console.log("Reset attempted but conditions not met"); 783 | } 784 | }, 785 | 786 | resumeAnimations() { 787 | console.log("Attempting to resume animations"); 788 | 789 | // Cancel any existing animation frame first 790 | if (State.animationFrame) { 791 | cancelAnimationFrame(State.animationFrame); 792 | State.animationFrame = null; 793 | } 794 | 795 | // Force a canvas update 796 | if (app.graph && app.graph.canvas) { 797 | app.graph.setDirtyCanvas(true, true); 798 | } 799 | 800 | // Ensure snow is visible if it was hidden 801 | const snowContainer = document.getElementById('comfy-aether-snow'); 802 | if (snowContainer) { 803 | snowContainer.style.display = 'block'; 804 | } 805 | 806 | // Start a new animation frame 807 | console.log("Starting new animation frame"); 808 | State.animationFrame = requestAnimationFrame(animate); 809 | } 810 | }; 811 | 812 | // Monitor queue events 813 | const origQueuePrompt = app.queuePrompt; 814 | app.queuePrompt = function() { 815 | WorkflowState.startExecution(); 816 | console.log("Starting render - pausing animations"); 817 | 818 | const result = origQueuePrompt.apply(this, arguments); 819 | 820 | if (result && typeof result.then === 'function') { 821 | result.finally(() => { 822 | console.log("Queue prompt completed"); 823 | // Add multiple reset attempts with increasing delays 824 | [5000, 6000, 7000].forEach(delay => { 825 | setTimeout(() => { 826 | WorkflowState.reset(); 827 | }, delay); 828 | }); 829 | }); 830 | } 831 | 832 | return result; 833 | }; 834 | 835 | // Add event listener for workflow execution complete 836 | app.eventBus?.addEventListener("execution_complete", () => { 837 | console.log("Execution complete event received"); 838 | // Add multiple reset attempts with increasing delays 839 | [5000, 6000, 7000].forEach(delay => { 840 | setTimeout(() => { 841 | WorkflowState.reset(); 842 | }, delay); 843 | }); 844 | }); 845 | 846 | // 🎬 Enhanced Animation Loop 847 | function animate() { 848 | const shouldAnimate = app.ui.settings.getSettingValue("ChristmasTheme.ChristmasEffects.LightSwitch", 1) > 0; 849 | const isPaused = WorkflowState.checkState() && 850 | app.ui.settings.getSettingValue("ChristmasTheme.PauseDuringRender", true); 851 | 852 | // Handle snow animation pause 853 | const snowContainer = document.getElementById('comfy-aether-snow'); 854 | if (snowContainer) { 855 | snowContainer.style.display = isPaused ? 'none' : 'block'; 856 | } 857 | 858 | // Handle Christmas lights animation 859 | if (shouldAnimate && !isPaused) { 860 | app.graph.setDirtyCanvas(true, true); 861 | // Continue animation loop 862 | State.animationFrame = requestAnimationFrame(animate); 863 | } else if (isPaused) { 864 | console.log("Animation paused, clearing animation frame"); 865 | if (State.animationFrame) { 866 | cancelAnimationFrame(State.animationFrame); 867 | State.animationFrame = null; 868 | } 869 | } else { 870 | // Continue animation loop even if not drawing 871 | State.animationFrame = requestAnimationFrame(animate); 872 | } 873 | } 874 | 875 | // Initialize Animation System 876 | console.log("Initializing animation system"); 877 | animate(); 878 | 879 | // 🧹 Cleanup on Extension Unload 880 | return () => { 881 | if (State.animationFrame) { 882 | cancelAnimationFrame(State.animationFrame); 883 | State.animationFrame = null; 884 | } 885 | }; 886 | } 887 | }); -------------------------------------------------------------------------------- /js/react-integration/comfy-react-integration.js: -------------------------------------------------------------------------------- 1 | // comfy-react-integration.js 2 | import { app } from "../../../../scripts/app.js"; 3 | 4 | // Export the minimal React implementation for offline use 5 | export async function loadReactDependencies() { 6 | try { 7 | // Create isolated state container 8 | const ReactState = { 9 | currentComponent: null, 10 | states: new Map(), 11 | updateQueued: false, 12 | updateTimeout: null, 13 | initialized: false 14 | }; 15 | 16 | // Create MiniReact object with error boundaries 17 | const MiniReact = { 18 | createElement(type, props = {}, ...children) { 19 | try { 20 | return { type, props: { ...props, children: children.flat() } }; 21 | } catch (e) { 22 | console.warn("createElement error:", e); 23 | return { type: 'div', props: { children: [] } }; 24 | } 25 | }, 26 | 27 | useState(initialValue) { 28 | try { 29 | if (!ReactState.initialized) return [initialValue, () => {}]; 30 | 31 | const state = this._getState(); 32 | if (!state.hasOwnProperty('value')) { 33 | state.value = typeof initialValue === 'function' ? initialValue() : initialValue; 34 | } 35 | 36 | const setState = (newValue) => { 37 | try { 38 | const nextValue = typeof newValue === 'function' ? newValue(state.value) : newValue; 39 | if (JSON.stringify(state.value) !== JSON.stringify(nextValue)) { 40 | state.value = nextValue; 41 | this._queueUpdate(); 42 | } 43 | } catch (e) { 44 | console.warn("setState error:", e); 45 | } 46 | }; 47 | 48 | return [state.value, setState]; 49 | } catch (e) { 50 | console.warn("useState error:", e); 51 | return [initialValue, () => {}]; 52 | } 53 | }, 54 | 55 | useCallback(callback) { 56 | return callback; 57 | }, 58 | 59 | useEffect(effect, deps) { 60 | try { 61 | if (!ReactState.initialized) return; 62 | 63 | const state = this._getState(); 64 | if (!state.effects) state.effects = new Map(); 65 | 66 | const currentDeps = state.effects.get(effect); 67 | const depsChanged = !currentDeps || !deps || 68 | deps.some((dep, i) => dep !== currentDeps[i]); 69 | 70 | if (depsChanged) { 71 | if (state.cleanup) { 72 | try { 73 | state.cleanup(); 74 | } catch (e) { 75 | console.warn("Effect cleanup error:", e); 76 | } 77 | } 78 | try { 79 | state.cleanup = effect(); 80 | state.effects.set(effect, deps); 81 | } catch (e) { 82 | console.warn("Effect execution error:", e); 83 | } 84 | } 85 | } catch (e) { 86 | console.warn("useEffect error:", e); 87 | } 88 | }, 89 | 90 | useMemo(factory) { 91 | try { 92 | return factory(); 93 | } catch (e) { 94 | console.warn("useMemo error:", e); 95 | return null; 96 | } 97 | }, 98 | 99 | memo(component) { 100 | return component; 101 | }, 102 | 103 | _getState() { 104 | try { 105 | if (!ReactState.states.has(ReactState.currentComponent)) { 106 | ReactState.states.set(ReactState.currentComponent, {}); 107 | } 108 | return ReactState.states.get(ReactState.currentComponent); 109 | } catch (e) { 110 | console.warn("_getState error:", e); 111 | return {}; 112 | } 113 | }, 114 | 115 | _queueUpdate() { 116 | try { 117 | if (!ReactState.updateQueued && ReactState.currentComponent) { 118 | ReactState.updateQueued = true; 119 | if (ReactState.updateTimeout) { 120 | clearTimeout(ReactState.updateTimeout); 121 | } 122 | ReactState.updateTimeout = setTimeout(() => { 123 | try { 124 | if (ReactState.currentComponent?._update) { 125 | ReactState.currentComponent._update(); 126 | } 127 | } catch (e) { 128 | console.warn("Update error:", e); 129 | } finally { 130 | ReactState.updateQueued = false; 131 | ReactState.updateTimeout = null; 132 | } 133 | }, 0); 134 | } 135 | } catch (e) { 136 | console.warn("_queueUpdate error:", e); 137 | } 138 | } 139 | }; 140 | 141 | // Create MiniReactDOM object with error handling 142 | const MiniReactDOM = { 143 | createRoot(container) { 144 | return { 145 | render(element) { 146 | try { 147 | ReactState.initialized = true; 148 | 149 | const renderElement = (el) => { 150 | try { 151 | if (!el) return null; 152 | 153 | if (typeof el === 'string' || typeof el === 'number') { 154 | return document.createTextNode(el); 155 | } 156 | 157 | if (typeof el.type === 'function') { 158 | const component = { 159 | _update: () => { 160 | try { 161 | const oldNode = component._node; 162 | const prevComponent = ReactState.currentComponent; 163 | ReactState.currentComponent = component; 164 | const newElement = el.type(el.props); 165 | const newNode = renderElement(newElement); 166 | if (oldNode?.parentNode) { 167 | oldNode.parentNode.replaceChild(newNode, oldNode); 168 | } 169 | component._node = newNode; 170 | ReactState.currentComponent = prevComponent; 171 | } catch (e) { 172 | console.warn("Component update error:", e); 173 | } 174 | } 175 | }; 176 | 177 | const prevComponent = ReactState.currentComponent; 178 | ReactState.currentComponent = component; 179 | const rendered = el.type(el.props); 180 | component._node = renderElement(rendered); 181 | ReactState.currentComponent = prevComponent; 182 | return component._node; 183 | } 184 | 185 | const node = document.createElement(el.type); 186 | 187 | Object.entries(el.props || {}).forEach(([key, value]) => { 188 | try { 189 | if (key === 'style' && typeof value === 'object') { 190 | Object.assign(node.style, value); 191 | } else if (key === 'children') { 192 | value.forEach(child => { 193 | const childNode = renderElement(child); 194 | if (childNode) node.appendChild(childNode); 195 | }); 196 | } else if (key.startsWith('on') && typeof value === 'function') { 197 | node.addEventListener(key.toLowerCase().slice(2), value); 198 | } else if (typeof value !== 'object' && key !== 'children') { 199 | node.setAttribute(key, value); 200 | } 201 | } catch (e) { 202 | console.warn("Property application error:", e); 203 | } 204 | }); 205 | 206 | return node; 207 | } catch (e) { 208 | console.warn("renderElement error:", e); 209 | return document.createElement('div'); 210 | } 211 | }; 212 | 213 | container.innerHTML = ''; 214 | container.appendChild(renderElement(element)); 215 | } catch (e) { 216 | console.error("Render error:", e); 217 | } 218 | }, 219 | unmount() { 220 | try { 221 | ReactState.initialized = false; 222 | container.innerHTML = ''; 223 | ReactState.states.clear(); 224 | if (ReactState.updateTimeout) { 225 | clearTimeout(ReactState.updateTimeout); 226 | ReactState.updateTimeout = null; 227 | } 228 | } catch (e) { 229 | console.warn("Unmount error:", e); 230 | } 231 | } 232 | }; 233 | } 234 | }; 235 | 236 | return { 237 | React: MiniReact, 238 | ReactDOM: MiniReactDOM 239 | }; 240 | } catch (e) { 241 | console.error("React dependencies loading error:", e); 242 | // Return a dummy implementation that won't break other extensions 243 | return { 244 | React: { 245 | createElement: () => ({ type: 'div', props: { children: [] } }), 246 | useState: (init) => [init, () => {}], 247 | useCallback: (cb) => cb, 248 | useEffect: () => {}, 249 | useMemo: (f) => f(), 250 | memo: (c) => c 251 | }, 252 | ReactDOM: { 253 | createRoot: () => ({ 254 | render: () => {}, 255 | unmount: () => {} 256 | }) 257 | } 258 | }; 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "comfyui-christmastheme" 3 | description = "A beautiful theme extension for ComfyUI that adds a festive touch with dynamic backgrounds, snowfall effects, and animated node connections" 4 | version = "1.0.0" 5 | license = {file = "LICENSE"} 6 | 7 | [project.urls] 8 | Repository = "https://github.com/AEmotionStudio/ComfyUI-ChristmasTheme" 9 | # Used by Comfy Registry https://comfyregistry.org 10 | 11 | [tool.comfy] 12 | PublisherId = "aemotionstudio" 13 | DisplayName = "ComfyUI-ChristmasTheme" 14 | Icon = "" 15 | --------------------------------------------------------------------------------