PageSourceSearch

https://pupwife.com/scripts/effects/weatherOverlay.js

js pupwife.com collected 2026-09-30 15:05:39 UTC 26,130 bytes, 776 lines download raw bytes

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.