1/** 2 * Weather Overlay System 3 * Visual effects based on current local weather! ð§ï¸âï¸âï¸ 4 */ 5 6function emitDevLog() { 7 if (typeof window !== 'undefined' && typeof window.devLog === 'function') { 8 window.devLog.apply(window, arguments); 9 } 10} 11function emitDevWarn() { 12 if (typeof window !== 'undefined' && typeof window.devWarn === 'function') { 13 window.devWarn.apply(window, arguments); 14 } 15} 16 17class WeatherOverlay { 18 constructor() { 19 this.container = null; 20 this.effects = []; 21 this.intervals = []; 22 this.isActive = true; 23 this.currentWeather = null; 24 this.weatherData = null; 25 this.userLocation = null; 26 this.updateTimer = null; 27 28 this.WEATHER_API_KEY = 'ab1b303a56c24bdc8ac80659252911'; 29 this.WEATHER_API_URL = 'https://api.weatherapi.com/v1/current.json'; 30 this.UPDATE_INTERVAL = 15 * 60 * 1000; // Update every 15 minutes 31 32 this.init(); 33 } 34 35 async init() { 36 // Create container 37 this.container = document.createElement('div'); 38 this.container.id = 'weather-overlay'; 39 this.container.style.cssText = ` 40 position: fixed; 41 top: 0; 42 left: 0; 43 width: 100%; 44 height: 100%; 45 pointer-events: none; 46 z-index: 9998; 47 overflow: hidden; 48 `; 49 document.body.appendChild(this.container); 50 51 // Check for debug weather override first 52 let debugWeather = null; 53 try { 54 debugWeather = sessionStorage.getItem('debugWeather'); 55 } catch (e) { 56 // Ignore 57 } 58 59 // If debug weather is set, use it instead of fetching from API 60 if (debugWeather) { 61 this.currentWeather = debugWeather; 62 this.applyWeather(debugWeather); 63 emitDevLog(`ð¤ï¸ Using debug weather: ${debugWeather} (call resetWeather() to clear)`); 64 } else { 65 // Load weather from API 66 await this.loadWeather(); 67 } 68 69 // Update loop 70 this.lastTime = performance.now(); 71 this.animate(); 72 } 73 74 // Get user location 75 async getUserLocation() { 76 // Request location directly (backgroundSwitcher removed) 77 return new Promise((resolve, reject) => { 78 if (!navigator.geolocation) { 79 reject(new Error('Geolocation is not supported by this browser')); 80 return; 81 } 82 83 navigator.geolocation.getCurrentPosition( 84 (position) => { 85 resolve({ 86 lat: position.coords.latitude, 87 lng: position.coords.longitude 88 }); 89 }, 90 (error) => { 91 reject(error); 92 }, 93 {
94 enableHighAccuracy: false, 95 timeout: 10000, 96 maximumAge: 300000 // Cache for 5 minutes 97 } 98 ); 99 }); 100 } 101 102 // Fetch weather data from WeatherAPI 103 async fetchWeatherData(lat, lng) { 104 try { 105 const url = `${this.WEATHER_API_URL}?key=${this.WEATHER_API_KEY}&q=${lat},${lng}&aqi=no`; 106 const response = await fetch(url); 107 108 if (!response.ok) { 109 throw new Error(`Weather API error: ${response.status} ${response.statusText}`); 110 } 111 112 const data = await response.json(); 113 return { 114 condition: data.current.condition.text.toLowerCase(), 115 code: data.current.condition.code, 116 temp: Math.round(data.current.temp_f), 117 humidity: data.current.humidity, 118 windSpeed: Math.round(data.current.wind_mph), 119 icon: data.current.condition.icon, 120 location: data.location.name + ', ' + data.location.region 121 }; 122 } catch (error) { 123 console.error('Weather Overlay: Error fetching weather data:', error); 124 throw error; 125 } 126 } 127 128 // Parse weather condition to determine effect type 129 parseWeatherType(condition, code) { 130 const cond = condition.toLowerCase(); 131 132 // Snow conditions 133 if (cond.includes('snow') || cond.includes('blizzard') || code === 1063 || code === 1114 || 134 code === 1117 || code === 1219 || code === 1222 || code === 1225 || code === 1237 || 135 code === 1249 || code === 1252 || code === 1255 || code === 1258 || code === 1261 || 136 code === 1264 || code === 1279 || code === 1282) { 137 return 'snow'; 138 } 139 140 // Rain conditions 141 if (cond.includes('rain') || cond.includes('drizzle') || cond.includes('shower') || 142 code === 1063 || code === 1066 || code === 1069 || code === 1072 || code === 1087 || 143 code === 1150 || code === 1153 || code === 1168 || code === 1171 || code === 1180 || 144 code === 1183 || code === 1186 || code === 1189 || code === 1192 || code === 1195 || 145 code === 1198 || code === 1201 || code === 1204 || code === 1207 || code === 1240 || 146 code === 1243 || code === 1246) { 147 return 'rain'; 148 } 149 150 // Thunderstorm 151 if (cond.includes('thunder') || cond.includes('storm') || 152 code === 1087 || code === 1273 || code === 1276 || code === 1279 || code === 1282) { 153 return 'thunderstorm'; 154 } 155 156 // Fog/Mist 157 if (cond.includes('fog') || cond.includes('mist') || cond.includes('haze') || 158 code === 1030 || code === 1135 || code === 1147) { 159 return 'fog'; 160 } 161 162 // Cloudy conditions 163 if (cond.includes('cloud') || cond.includes('overcast') || 164 code === 1006 || code === 1009) { 165 return 'cloudy'; 166 } 167 168 // Clear/Sunny 169 if (cond.includes('clear') || cond.includes('sunny') || code === 1000) { 170 return 'sunny'; 171 } 172 173 // Partly cloudy 174 if (cond.includes('partly') || code === 1003) { 175 return 'partly-cloudy'; 176 } 177 178 // Default to partly cloudy 179 return 'partly-cloudy'; 180 } 181 182 // Load weather and apply effects 183 async loadWeather() { 184 // Check for debug weather override first 185 let debugWeather = null; 186 try { 187 debugWeather = sessionStorage.getItem('debugWeather'); 188 } catch (e) { 189 // Ignore 190 } 191 192 // If debug weather is set, use it instead of fetching from API 193 if (debugWeather) { 194 this.clearEffects(); 195 this.currentWeather = debugWeather; 196 this.applyWeather(debugWeather); 197 emitDevLog(`ð¤ï¸ Weather Overlay: ${debugWeather} (debug mode)`); 198 return; 199 } 200 201 try { 202 // Get user location 203 this.userLocation = await this.getUserLocation(); 204 205 // Fetch weather data 206 this.weatherData = await this.fetchWeatherData(this.userLocation.lat, this.userLocation.lng); 207 208 // Parse weather type 209 const weatherType = this.parseWeatherType(this.weatherData.condition, this.weatherData.code); 210 211 // Clear existing effects 212 this.clearEffects(); 213 214 // Apply weather effects 215 this.currentWeather = weatherType; 216 this.applyWeather(weatherType); 217 218 emitDevLog(`ð¤ï¸ Weather Overlay: ${weatherType} (${this.weatherData.condition})`); 219 220 // Schedule next update 221 if (this.updateTimer) clearTimeout(this.updateTimer); 222 this.updateTimer = setTimeout(() => this.loadWeather(), this.UPDATE_INTERVAL); 223 224 } catch (error) { 225 console.error('Weather Overlay: Failed to load weather', error); 226 // Don't show effects if weather can't be loaded 227 } 228 } 229 230 // Apply weather effects based on type 231 applyWeather(weatherType) { 232 switch(weatherType) { 233 case 'rain': 234 this.applyRain(); 235 break; 236 case 'snow': 237 this.applySnow(); 238 break; 239 case 'thunderstorm': 240 this.applyThunderstorm(); 241 break; 242 case 'fog': 243 this.applyFog(); 244 break; 245 case 'cloudy': 246 this.applyCloudy(); 247 break; 248 case 'sunny': 249 this.applySunny(); 250 break; 251 case 'partly-cloudy': 252 this.applyPartlyCloudy(); 253 break; 254 default: 255 // No effects for unknown weather 256 break; 257 } 258 } 259 260 // ======================================== 261 // RAIN - Falling raindrops 262 // ======================================== 263 264 applyRain() { 265 // Spawn raindrops 266 this.spawnRaindrop(); 267 const rainInterval = setInterval(() => { 268 if (!this.isActive) return; 269 this.spawnRaindrop(); 270 }, 50 + Math.random() * 50); 271 this.intervals.push(rainInterval); 272 } 273 274 spawnRaindrop() { 275 if (!this.isActive) return; 276 277 const raindrop = document.createElement('div'); 278 raindrop.style.cssText = ` 279 position: fixed; 280 left: ${Math.random() * window.innerWidth}px; 281 top: -10px; 282 width: 2px; 283 height: ${15 + Math.random() * 20}px; 284 background: linear-gradient(to bottom, rgba(173, 216, 230, 0.8), rgba(135, 206, 250, 0.6)); 285 pointer-events: none; 286 z-index: 9996; 287 `; 288 289 this.container.appendChild(raindrop); 290 291 const raindropData = { 292 element: raindrop, 293 type: 'raindrop', 294 x: parseFloat(raindrop.style.left), 295 y: -10, 296 vy: 200 + Math.random() * 150, 297 startTime: performance.now() 298 }; 299 300 this.effects.push(raindropData); 301 302 setTimeout(() => { 303 if (raindrop.parentNode) { 304 raindrop.remove(); 305 } 306 this.effects = this.effects.filter(e => e !== raindropData); 307 }, 5000); 308 } 309 310 // ======================================== 311 // SNOW - Falling snowflakes 312 // ======================================== 313 314 applySnow() { 315 this.spawnSnowflake(); 316 const snowInterval = setInterval(() => { 317 if (!this.isActive) return; 318 this.spawnSnowflake(); 319 }, 500 + Math.random() * 500); 320 this.intervals.push(snowInterval); 321 } 322 323 spawnSnowflake() { 324 if (!this.isActive) return; 325 326 const snowflake = document.createElement('div'); 327 snowflake.textContent = ['â', 'â ', 'â'][Math.floor(Math.random() * 3)]; 328 snowflake.style.cssText = ` 329 position: fixed; 330 left: ${Math.random() * window.innerWidth}px; 331 top: -20px; 332 font-size: ${10 + Math.random() * 15}px; 333 color: rgba(255, 255, 255, ${0.6 + Math.random() * 0.4}); 334 pointer-events: none; 335 z-index: 9996; 336 `; 337 338 this.container.appendChild(snowflake); 339 340 const snowData = { 341 element: snowflake, 342 type: 'snow', 343 x: parseFloat(snowflake.style.left), 344 y: -20, 345 vx: (Math.random() - 0.5) * 20, 346 vy: 20 + Math.random() * 30, 347 rotation: 0, 348 rotationSpeed: (Math.random() - 0.5) * 2 349 }; 350 351 this.effects.push(snowData); 352 353 setTimeout(() => { 354 if (snowflake.parentNode) { 355 snowflake.remove(); 356 } 357 this.effects = this.effects.filter(e => e !== snowData); 358 }, 15000); 359 } 360 361 // ======================================== 362 // THUNDERSTORM - Rain + lightning flashes 363 // ======================================== 364 365 applyThunderstorm() { 366 // Rain 367 this.spawnRaindrop(); 368 const rainInterval = setInterval(() => { 369 if (!this.isActive) return; 370 this.spawnRaindrop(); 371 }, 30 + Math.random() * 40); 372 this.intervals.push(rainInterval); 373 374 // Lightning flashes 375 const lightningInterval = setInterval(() => { 376 if (!this.isActive) return; 377 if (Math.random() < 0.3) { 378 this.spawnLightning(); 379 } 380 }, 3000 + Math.random() * 5000); 381 this.intervals.push(lightningInterval); 382 } 383 384 spawnLightning() { 385 if (!this.isActive) return; 386 387 const flash = document.createElement('div'); 388 flash.style.cssText = ` 389 position: fixed; 390 top: 0; 391 left: 0; 392 width: 100%; 393 height: 100%; 394 background: rgba(255, 255, 255, 0.3); 395 pointer-events: none; 396 z-index: 9997; 397 opacity: 0; 398 transition: opacity 0.1s ease-out; 399 `; 400 401 this.container.appendChild(flash); 402 403 // Flash on 404 setTimeout(() => { 405 flash.style.opacity = '0.3'; 406 }, 10); 407 408 // Flash off 409 setTimeout(() => { 410 flash.style.opacity = '0'; 411 setTimeout(() => { 412 if (flash.parentNode) { 413 flash.remove(); 414 } 415 }, 100); 416 }, 150); 417 } 418 419 // ======================================== 420 // FOG - Fog effect 421 // ======================================== 422 423 applyFog() { 424 this.createFog('#C0C0C0', '#E8E8E840'); 425 } 426 427 createFog(color1, color2) { 428 for (let i = 0; i < 3; i++) { 429 const fog = document.createElement('div'); 430 fog.className = 'weather-fog'; 431 const baseOpacity = 0.1 + i * 0.03; 432 433 fog.style.cssText = ` 434 position: fixed; 435 bottom: 0px; 436 left: -100%; 437 width: 300%; 438 height: 300px; 439 background: linear-gradient(to top, 440 ${color1}${Math.round(baseOpacity * 255).toString(16).padStart(2, '0')}, 441 ${color2} 50%, 442 transparent); 443 filter: blur(${40 + i * 20}px); 444 animation: fog-drift ${50 + i * 30}s linear infinite; 445 pointer-events: none; 446 z-index: ${1 + i}; 447 `; 448 this.container.appendChild(fog); 449 this.effects.push({ element: fog, type: 'fog', index: i }); 450 } 451 452 if (!document.getElementById('weather-fog-style')) { 453 const style = document.createElement('style'); 454 style.id = 'weather-fog-style'; 455 style.textContent = ` 456 @keyframes fog-drift { 457 0% { transform: translateX(0); } 458 100% { transform: translateX(33.33%); } 459 } 460 `; 461 document.head.appendChild(style); 462 } 463 } 464 465 // ======================================== 466 // CLOUDY - Subtle floating clouds 467 // ======================================== 468 469 applyCloudy() { 470 this.spawnCloud(); 471 const cloudInterval = setInterval(() => { 472 if (!this.isActive) return; 473 if (Math.random() < 0.4) { 474 this.spawnCloud(); 475 } 476 }, 8000 + Math.random() * 7000); 477 this.intervals.push(cloudInterval); 478 } 479 480 spawnCloud() { 481 if (!this.isActive) return; 482 483 const cloud = document.createElement('div'); 484 cloud.textContent = 'âï¸'; 485 cloud.style.cssText = ` 486 position: fixed; 487 left: ${-100}px; 488 top: ${Math.random() * window.innerHeight * 0.3}px; 489 font-size: ${40 + Math.random() * 60}px; 490 opacity: ${0.3 + Math.random() * 0.3}; 491 pointer-events: none; 492 z-index: 9996; 493 `; 494 495 this.container.appendChild(cloud); 496 497 const cloudData = { 498 element: cloud, 499 type: 'cloud', 500 x: -100, 501 y: parseFloat(cloud.style.top), 502 vx: 15 + Math.random() * 20, 503 startTime: performance.now() 504 }; 505 506 this.effects.push(cloudData); 507 508 setTimeout(() => { 509 if (cloud.parentNode) { 510 cloud.remove(); 511 } 512 this.effects = this.effects.filter(e => e !== cloudData); 513 }, 30000); 514 } 515 516 // ======================================== 517 // SUNNY - Subtle sunbeams 518 // ======================================== 519 520 applySunny() { 521 // Very subtle - just occasional light rays 522 const sunbeamInterval = setInterval(() => { 523 if (!this.isActive) return; 524 if (Math.random() < 0.2) { 525 this.spawnSunbeam(); 526 } 527 }, 10000); 528 this.intervals.push(sunbeamInterval); 529 } 530 531 spawnSunbeam() { 532 if (!this.isActive) return; 533 534 const sunbeam = document.createElement('div'); 535 const angle = Math.random() * Math.PI * 2; 536 const length = 200 + Math.random() * 300; 537 const x = Math.random() * window.innerWidth; 538 const y = -50; 539 540 sunbeam.style.cssText = ` 541 position: fixed; 542 left: ${x}px; 543 top: ${y}px; 544 width: 2px; 545 height: ${length}px; 546 background: linear-gradient(to bottom, 547 rgba(255, 255, 200, 0.3), 548 transparent); 549 pointer-events: none; 550 z-index: 9995; 551 transform: rotate(${angle * 180 / Math.PI}deg); 552 transform-origin: top center; 553 opacity: 0; 554 transition: opacity 2s ease-in-out; 555 `; 556 557 this.container.appendChild(sunbeam); 558 559 setTimeout(() => { 560 sunbeam.style.opacity = '0.3'; 561 }, 10); 562 563 setTimeout(() => { 564 sunbeam.style.opacity = '0'; 565 setTimeout(() => { 566 if (sunbeam.parentNode) { 567 sunbeam.remove(); 568 } 569 }, 2000); 570 }, 4000); 571 } 572 573 // ======================================== 574 // PARTLY CLOUDY - Occasional clouds 575 // ======================================== 576 577 applyPartlyCloud
577y() { 578 this.spawnCloud(); 579 const cloudInterval = setInterval(() => { 580 if (!this.isActive) return; 581 if (Math.random() < 0.3) { 582 this.spawnCloud(); 583 } 584 }, 12000 + Math.random() * 8000); 585 this.intervals.push(cloudInterval); 586 } 587 588 // ======================================== 589 // ANIMATION LOOP 590 // ======================================== 591 592 animate() { 593 if (!this.isActive) return; 594 595 const currentTime = performance.now(); 596 const deltaTime = (currentTime - this.lastTime) / 1000; 597 this.lastTime = currentTime; 598 599 // Update all effects 600 this.effects.forEach(effect => { 601 if (effect.type === 'raindrop') { 602 this.updateRaindrop(effect, deltaTime); 603 } else if (effect.type === 'snow') { 604 this.updateSnow(effect, deltaTime); 605 } else if (effect.type === 'cloud') { 606 this.updateCloud(effect, deltaTime); 607 } 608 }); 609 610 requestAnimationFrame(() => this.animate()); 611 } 612 613 updateRaindrop(effect, deltaTime) { 614 effect.y += effect.vy * deltaTime; 615 effect.element.style.top = `${effect.y}px`; 616 617 if (effect.y > window.innerHeight + 50) { 618 if (effect.element.parentNode) { 619 effect.element.remove(); 620 } 621 this.effects = this.effects.filter(e => e !== effect); 622 } 623 } 624 625 updateSnow(effect, deltaTime) { 626 effect.x += effect.vx * deltaTime; 627 effect.y += effect.vy * deltaTime; 628 effect.rotation += effect.rotationSpeed; 629 630 effect.element.style.left = `${effect.x}px`; 631 effect.element.style.top = `${effect.y}px`; 632 effect.element.style.transform = `rotate(${effect.rotation}deg)`; 633 634 if (effect.y > window.innerHeight + 50) { 635 if (effect.element.parentNode) { 636 effect.element.remove(); 637 } 638 this.effects = this.effects.filter(e => e !== effect); 639 } 640 } 641 642 updateCloud(effect, deltaTime) { 643 effect.x += effect.vx * deltaTime; 644 effect.element.style.left = `${effect.x}px`; 645 646 if (effect.x > window.innerWidth + 100) { 647 if (effect.element.parentNode) { 648 effect.element.remove(); 649 } 650 this.effects = this.effects.filter(e => e !== effect); 651 } 652 } 653 654 // ======================================== 655 // DEBUG / TESTING 656 // ======================================== 657 658 setWeather(weatherType) { 659 const validWeather = ['rain', 'snow', 'thunderstorm', 'fog', 'cloudy', 'sunny', 'partly-cloudy']; 660 661 if (!validWeather.includes(weatherType)) { 662 console.error(`â Invalid weather type: ${weatherType}`); 663 console.log(`Valid weather types: ${validWeather.join(', ')}`); 664 return; 665 } 666 667 // Clear current effects 668 this.clearEffects(); 669 670 // Apply new weather 671 this.currentWeather = weatherType; 672 this.applyWeather(weatherType); 673 674 // Save to sessionStorage so it persists on refresh during testing 675 try { 676 sessionStorage.setItem('debugWeather', weatherType); 677 } catch (e) { 678 console.warn('Could not save debug weather to sessionStorage'); 679 } 680 681 console.log(`ð¤ï¸ Switched to ${weatherType} weather!`); 682 console.log(`ð¡ This will persist until you close the tab or call resetWeather()`); 683 } 684 685 resetWeather() { 686 // Clear debug weather and go back to actual weather detection 687 try { 688 sessionStorage.removeItem('debugWeather'); 689 } catch (e) { 690 // Ignore 691 } 692 693 // Reload weather from API 694 this.loadWeather(); 695 696 console.log(`ð¤ï¸ Reset to actual weather`); 697 } 698 699 listWeatherTypes() { 700 const weatherTypes = [ 701 'ð§ï¸ rain - Falling raindrops', 702 'âï¸ snow - Falling snowflakes', 703 'âï¸ thunderstorm - Rain + lightning flashes', 704 'ð«ï¸ fog - Fog effect', 705 'âï¸ cloudy - Floating clouds', 706 'âï¸ sunny - Subtle sunbeams', 707 'â partly-cloudy - Occasional clouds' 708 ]; 709 710 console.log('ð¤ï¸ Available weather overlays:');
711 weatherTypes.forEach(w => console.log(` ${w}`)); 712 console.log('\nð¡ Commands:'); 713 console.log(' window.weatherOverlay.setWeather("rain") - Switch to a weather type'); 714 console.log(' window.weatherOverlay.resetWeather() - Go back to actual weather'); 715 } 716 717 // ======================================== 718 // CLEANUP 719 // ======================================== 720 721 clearEffects() { 722 // Clear all intervals 723 this.intervals.forEach(interval => clearInterval(interval)); 724 this.intervals = []; 725 726 // Remove all effects 727 this.effects.forEach(effect => { 728 if (effect.element && effect.element.parentNode) { 729 effect.element.remove(); 730 } 731 }); 732 this.effects = []; 733 } 734 735 destroy() { 736 this.isActive = false; 737 this.clearEffects(); 738 739 // Clear update timer 740 if (this.updateTimer) { 741 clearTimeout(this.updateTimer); 742 this.updateTimer = null; 743 } 744 745 // Remove container 746 if (this.container && this.container.parentNode) { 747 this.container.remove(); 748 } 749 750 emitDevLog('ð¤ï¸ Weather overlay destroyed'); 751 } 752} 753 754// Auto-initialize when DOM is ready 755document.addEventListener('DOMContentLoaded', () => { 756 // Check if weather overlay is enabled in settings 757 let weatherEnabled = false; 758 759 try { 760 const savedSettings = localStorage.getItem('puppywifey_settings'); 761 if (savedSettings) { 762 const settings = JSON.parse(savedSettings); 763 weatherEnabled = settings.weatherOverlay || false; 764 } 765 } catch (e) { 766 emitDevWarn('Failed to load settings for weather overlay:', e); 767 } 768 769 // Only initialize if enabled 770 if (weatherEnabled) { 771 window.weatherOverlay = new WeatherOverlay(); 772 emitDevLog('ð¤ï¸ Weather overlay initialized!'); 773 } else { 774 emitDevLog('ð¤ï¸ Weather overlay disabled in settings'); 775 } 776});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.