برچسب: React

  • Creating an Immersive 3D Weather Visualization with React Three Fiber

    Creating an Immersive 3D Weather Visualization with React Three Fiber



    I’ve always been interested in data visualization using Three.js / R3F, and I thought a weather web app would be the perfect place to start. One of my favorite open-source libraries, @react-three/drei, already has a bunch of great tools like clouds, sky, and stars that fit perfectly into visualizing the weather in 3D.

    This tutorial explores how to transform API data into a 3D experience, where we add a little flair and fun to weather visualization.

    The Technology Stack

    Our weather world is built on a foundation of some of my favorite technologies:

    Weather Components

    The heart of our visualization lies in conditionally showing a realistic sun, moon, and/or clouds based on the weather
    results from your city or a city you search for, particles that simulate rain or snow, day/night logic, and some fun
    lighting effects during a thunderstorm. We’ll start by building these weather components and then move on to displaying
    them based on the results of the WeatherAPI call.

    Sun + Moon Implementation

    Let’s start simple: we’ll create a sun and moon component that’s just a sphere with a realistic texture wrapped
    around it. We’ll also give it a little rotation and some lighting.

    // Sun.js and Moon.js Component, a texture wrapped sphere
    import React, { useRef } from 'react';
    import { useFrame, useLoader } from '@react-three/fiber';
    import { Sphere } from '@react-three/drei';
    import * as THREE from 'three';
    
    const Sun = () => {
      const sunRef = useRef();
      
      const sunTexture = useLoader(THREE.TextureLoader, '/textures/sun_2k.jpg');
      
      useFrame((state) => {
        if (sunRef.current) {
          sunRef.current.rotation.y = state.clock.getElapsedTime() * 0.1;
        }
      });
    
      const sunMaterial = new THREE.MeshBasicMaterial({
        map: sunTexture,
      });
    
      return (
        <group position={[0, 4.5, 0]}>
          <Sphere ref={sunRef} args={[2, 32, 32]} material={sunMaterial} />
          
          {/* Sun lighting */}
          <pointLight position={[0, 0, 0]} intensity={2.5} color="#FFD700" distance={25} />
        </group>
      );
    };
    
    export default Sun;

    I grabbed the CC0 texture from here. The moon component is essentially the same; I used this image. The pointLight intensity is low because most of our lighting will come from the sky.

    Rain: Instanced Cylinders

    Next, let’s create a rain particle effect. To keep things performant, we’re going to use instancedMesh instead of creating a separate mesh component for each rain particle. We’ll render a single geometry (<cylinderGeometry>) multiple times with different transformations (position, rotation, scale). Also, instead of creating a new THREE.Object3D for each particle in every frame, we’ll reuse a single dummy object. This saves memory and prevents the overhead of creating and garbage-collecting a large number of temporary objects within the animation loop. We’ll also use the useMemo hook to create and initialize the particles array only once when the component mounts.

    // Rain.js - instanced rendering
    const Rain = ({ count = 1000 }) => {
      const meshRef = useRef();
      const dummy = useMemo(() => new THREE.Object3D(), []);
    
      const particles = useMemo(() => {
        const temp = [];
        for (let i = 0; i < count; i++) {
          temp.push({
            x: (Math.random() - 0.5) * 20,
            y: Math.random() * 20 + 10,
            z: (Math.random() - 0.5) * 20,
            speed: Math.random() * 0.1 + 0.05,
          });
        }
        return temp;
      }, [count]);
    
      useFrame(() => {
        particles.forEach((particle, i) => {
          particle.y -= particle.speed;
          if (particle.y < -1) {
            particle.y = 20; // Reset to top
          }
    
          dummy.position.set(particle.x, particle.y, particle.z);
          dummy.updateMatrix();
          meshRef.current.setMatrixAt(i, dummy.matrix);
        });
        meshRef.current.instanceMatrix.needsUpdate = true;
      });
    
      return (
        <instancedMesh ref={meshRef} args={[null, null, count]}>
          <cylinderGeometry args={[0.01, 0.01, 0.5, 8]} />
          <meshBasicMaterial color="#87CEEB" transparent opacity={0.6} />
        </instancedMesh>
      );
    };

    When a particle reaches a negative Y-axis level, it’s immediately recycled to the top of the scene with a new random horizontal position, creating the illusion of continuous rainfall without constantly creating new objects.

    Snow: Physics-Based Tumbling

    We’ll use the same basic template for the snow effect, but instead of the particles falling straight down, we’ll give them some drift.

    // Snow.js - Realistic drift and tumbling with time-based rotation
    useFrame((state) => {
      particles.forEach((particle, i) => {
        particle.y -= particle.speed;
        particle.x += Math.sin(state.clock.elapsedTime + i) * particle.drift;
        
        if (particle.y < -1) {
          particle.y = 20;
          particle.x = (Math.random() - 0.5) * 20;
        }
    
        dummy.position.set(particle.x, particle.y, particle.z);
        // Time-based tumbling rotation for natural snowflake movement
        dummy.rotation.x = state.clock.elapsedTime * 2;
        dummy.rotation.y = state.clock.elapsedTime * 3;
        dummy.updateMatrix();
        meshRef.current.setMatrixAt(i, dummy.matrix);
      });
      meshRef.current.instanceMatrix.needsUpdate = true;
    });

    The horizontal drift uses Math.sin(state.clock.elapsedTime + i), where state.clock.elapsedTime provides a continuously increasing time value and i offsets each particle’s timing. This creates a natural swaying motion in which each snowflake follows its own path. The rotation updates apply small increments to both the X and Y axes, creating the tumbling effect.

    Storm System: Multi-Component Weather Events

    When a storm rolls in, I wanted to simulate dark, brooding clouds and flashes of lightning. This effect requires combining multiple weather effects simultaneously. We’ll import our rain component, add some clouds, and implement a lightning effect with a pointLight that simulates flashes of lightning coming from inside the clouds.

    // Storm.js
    const Storm = () => {
      const cloudsRef = useRef();
      const lightningLightRef = useRef();
      const lightningActive = useRef(false);
    
      useFrame((state) => {
        // Lightning flash with ambient light
        if (Math.random() < 0.003 && !lightningActive.current) {
          lightningActive.current = true;
          
          if (lightningLightRef.current) {
            // Random X position for each flash
            const randomX = (Math.random() - 0.5) * 10; // Range: -5 to 5
            lightningLightRef.current.position.x = randomX;
            
            // Single bright flash
            lightningLightRef.current.intensity = 90;
            
            setTimeout(() => {
              if (lightningLightRef.current) lightningLightRef.current.intensity = 0;
              lightningActive.current = false;
            }, 400);
          }
        }
      });
    
     return (
        <group>
          <group ref={cloudsRef}>
            <DreiClouds material={THREE.MeshLambertMaterial}>
              <Cloud
                segments={60}
                bounds={[12, 3, 3]}
                volume={10}
                color="#8A8A8A"
                fade={100}
                speed={0.2}
                opacity={0.8}
                position={[-3, 4, -2]}
              />
            {/* Additional cloud configurations... */}
          </DreiClouds>
          
          {/* Heavy rain - 1500 particles */}
          <Rain count={1500} />
          
          <pointLight 
            ref={lightningLightRef}
            position={[0, 6, -5.5]}
            intensity={0}
            color="#e6d8b3"
            distance={30}
            decay={0.8}
            castShadow
          />
        </group>
      );
    };

    The lightning system uses a simple ref-based cooldown mechanism to prevent constant flashing. When lightning triggers, it creates a single bright flash with random positioning. The system uses setTimeout to reset the light intensity after 400ms, creating a realistic lightning effect without complex multi-stage sequences.

    Clouds: Drei Cloud

    For weather types like cloudy, partly cloudy, overcast, foggy, rainy, snowy, and misty, we’ll pull in our clouds component. I wanted the storm component to have its own clouds because storms should have darker clouds than the conditions above. The clouds component will simply display Drei clouds, and we’ll pull it all together with the sun or moon component in the next section.

    const Clouds = ({ intensity = 0.7, speed = 0.1 }) => {
      // Determine cloud colors based on weather condition
      const getCloudColors = () => {
          return {
            primary: '#FFFFFF',
            secondary: '#F8F8F8',
            tertiary: '#F0F0F0',
            light: '#FAFAFA',
            intensity: intensity
          };
      };
    
      const colors = getCloudColors();
      return (
        <group>
          <DreiClouds material={THREE.MeshLambertMaterial}>
            {/* Large fluffy cloud cluster */}
            <Cloud
              segments={80}
              bounds={[12, 4, 4]}
              volume={15}
              color={colors.primary}
              fade={50}
              speed={speed}
              opacity={colors.intensity}
              position={[-5, 4, -2]}
            />
            {/* Additional clouds... */}
          </DreiClouds>
        </group>
      );
    };

    API-Driven Logic: Putting It All Together

    Now that we’ve built our weather components, we need a system to decide which ones to display based on real weather data. The WeatherAPI.com service provides detailed current conditions that we’ll transform into our 3D scene parameters. The API gives us condition text like “Partly cloudy,” “Thunderstorm,” or “Light snow,” but we need to convert these into our component types.

    // weatherService.js - Fetching real weather data
    const response = await axios.get(
      `${WEATHER_API_BASE}/forecast.json?key=${API_KEY}&q=${location}&days=3&aqi=no&alerts=no&tz=${Intl.DateTimeFormat().resolvedOptions().timeZone}`,
      { timeout: 10000 }
    );

    The API request includes time zone information so we can accurately determine day or night for our Sun/Moon system. The days=3 parameter grabs forecast data for our portal feature, while aqi=no&alerts=no keeps the payload lean by excluding data we don’t need.

    Converting API Conditions to Component Types

    The heart of our system is a simple parsing function that maps hundreds of possible weather descriptions to our manageable set of visual components:

    // weatherService.js - Converting weather text to renderable types
    export const getWeatherConditionType = (condition) => {
      const conditionLower = condition.toLowerCase();
    
      if (conditionLower.includes('sunny') || conditionLower.includes('clear')) {
        return 'sunny';
      }
      if (conditionLower.includes('thunder') || conditionLower.includes('storm')) {
        return 'stormy';
      }
      if (conditionLower.includes('cloud') || conditionLower.includes('overcast')) {
        return 'cloudy';
      }
      if (conditionLower.includes('rain') || conditionLower.includes('drizzle')) {
        return 'rainy';
      }
      if (conditionLower.includes('snow') || conditionLower.includes('blizzard')) {
        return 'snowy';
      }
      // ... additional fog and mist conditions
      return 'cloudy';
    };

    This string-matching approach handles edge cases gracefully—whether the API returns “Light rain,” “Heavy rain,” or “Patchy light drizzle,” they all map to our rainy type and trigger the appropriate 3D effects. This way, we can reuse our main components without needing a separate component for each weather condition.

    Conditional Component Rendering

    The magic happens in our WeatherVisualization component, where the parsed weather type determines exactly which 3D components to render:

    // WeatherVisualization.js - Bringing weather data to life
    const renderWeatherEffect = () => {
      if (weatherType === 'sunny') {
        if (partlyCloudy) {
          return (
            <>
              {isNight ? <Moon /> : <Sun />}
              <Clouds intensity={0.5} speed={0.1} />
            </>
          );
        }
        return isNight ? <Moon /> : <Sun />;
      } else if (weatherType === 'rainy') {
        return (
          <>
            <Clouds intensity={0.8} speed={0.15} />
            <Rain count={800} />
          </>
        );
      } else if (weatherType === 'stormy') {
        return <Storm />; // Includes its own clouds, rain, and lightning
      }
      // ... additional weather types
    };

    This conditional system ensures we only load the particle systems we actually need. A sunny day renders just our Sun component, while a storm loads our complete Storm system with heavy rain, dark clouds, and lightning effects. Each weather type gets its own combination of the components we built earlier, creating distinct visual experiences that match the real weather conditions.

    Dynamic Time-of-Day System

    Weather isn’t just about conditions—it’s also about timing. Our weather components need to know whether to show the sun or moon, and we need to configure Drei’s Sky component to render the appropriate atmospheric colors for the current time of day. Fortunately, our WeatherAPI response already includes the local time for any location, so we can extract that to drive our day/night logic.

    The API provides local time in a simple format that we can parse to determine the current period:

    // Scene3D.js - Parsing time from weather API data
    const getTimeOfDay = () => {
      if (!weatherData?.location?.localtime) return 'day';
      const localTime = weatherData.location.localtime;
      const currentHour = new Date(localTime).getHours();
    
      if (currentHour >= 19 || currentHour <= 6) return 'night';
      if (currentHour >= 6 && currentHour < 8) return 'dawn';
      if (currentHour >= 17 && currentHour < 19) return 'dusk';
      return 'day';
    };

    This gives us four distinct time periods, each with different lighting and sky configurations. Now we can use these periods to configure Drei’s Sky component, which handles atmospheric scattering and generates realistic sky colors.

    Dynamic Sky Configuration

    Drei’s Sky component is fantastic because it simulates actual atmospheric physics—we just need to adjust atmospheric parameters for each time period:

    // Scene3D.js - Time-responsive Sky configuration
    {timeOfDay !== 'night' && (
      <Sky
        sunPosition={(() => {
          if (timeOfDay === 'dawn') {
            return [100, -5, 100]; // Sun below horizon for darker dawn colors
          } else if (timeOfDay === 'dusk') {
            return [-100, -5, 100]; // Sun below horizon for sunset colors
          } else { // day
            return [100, 20, 100]; // High sun position for bright daylight
          }
        })()}
        inclination={(() => {
          if (timeOfDay === 'dawn' || timeOfDay === 'dusk') {
            return 0.6; // Medium inclination for transitional periods
          } else { // day
            return 0.9; // High inclination for clear daytime sky
          }
        })()}
        turbidity={(() => {
          if (timeOfDay === 'dawn' || timeOfDay === 'dusk') {
            return 8; // Higher turbidity creates warm sunrise/sunset colors
          } else { // day
            return 2; // Lower turbidity for clear blue sky
          }
        })()}
      />
    )}

    The magic happens in the positioning. During dawn and dusk, we place the sun just below the horizon (-5 Y position) so Drei’s Sky component generates those warm orange and pink colors we associate with sunrise and sunset. The turbidity parameter controls atmospheric scattering, with higher values creating more dramatic color effects during transitional periods.

    Nighttime: Simple Black Background + Stars

    For nighttime, I made a deliberate choice to skip Drei’s Sky component entirely and use a simple black background instead. The Sky component can be computationally expensive, and for nighttime scenes, a pure black backdrop actually looks better and performs significantly faster. We complement this with Drei’s Stars component for that authentic nighttime atmosphere:

    // Scene3D.js - Efficient nighttime rendering
    {!portalMode && isNight && <SceneBackground backgroundColor={'#000000'} />}
    
    {/* Stars create the nighttime atmosphere */}
    {isNight && (
      <Stars
        radius={100}
        depth={50}
        count={5000}
        factor={4}
        saturation={0}
        fade
        speed={1}
      />
    )}

    Drei’s Stars component creates 5,000 individual stars scattered across a 100-unit sphere with realistic depth variation. The saturation={0} keeps them properly desaturated for authentic nighttime visibility, while the gentle speed={1} creates subtle movement that simulates the natural motion of celestial bodies. Stars only appear during nighttime hours (7 PM to 6 AM) and automatically disappear at dawn, creating a smooth transition back to Drei’s daytime Sky component.

    This approach gives us four distinct atmospheric moods—bright daylight, warm dawn colors, golden dusk tones, and star-filled nights—all driven automatically by the real local time from our weather data.

    Forecast Portals: Windows Into Tomorrow’s Weather

    Like any good weather app, we don’t want to just show current conditions but also what’s coming next. Our API returns a three-day forecast that we transform into three interactive portals hovering in the 3D scene, each one showing a preview of that day’s weather conditions. Click on a portal and you’re transported into that day’s atmospheric environment.

    Building Portals with MeshPortalMaterial

    The portals use Drei’s MeshPortalMaterial, which renders a complete 3D scene to a texture that gets mapped onto a plane. Each portal becomes a window into its own weather world:

    // ForecastPortals.js - Creating interactive weather portals
    const ForecastPortal = ({ position, dayData, index, onEnter }) => {
      const materialRef = useRef();
    
      // Transform forecast API data into our weather component format
      const portalWeatherData = useMemo(() => ({
        current: {
          temp_f: dayData.day.maxtemp_f,
          condition: dayData.day.condition,
          is_day: 1, // Force daytime for consistent portal lighting
          humidity: dayData.day.avghumidity,
          wind_mph: dayData.day.maxwind_mph,
        },
        location: {
          localtime: dayData.date + 'T12:00' // Set to noon for optimal lighting
        }
      }), [dayData]);
    
      return (
        <group position={position}>
          <mesh onClick={onEnter}>
            <roundedPlaneGeometry args={[2, 2.5, 0.15]} />
            <MeshPortalMaterial
              ref={materialRef}
              blur={0}
              resolution={256}
              worldUnits={false}
            >
              {/* Each portal renders a complete weather scene */}
              <color attach="background" args={['#87CEEB']} />
              <ambientLight intensity={0.4} />
              <directionalLight position={[10, 10, 5]} intensity={1} />
              <WeatherVisualization
                weatherData={portalWeatherData}
                isLoading={false}
                portalMode={true}
              />
            </MeshPortalMaterial>
          </mesh>
    
          {/* Weather info overlay */}
          <Text position={[-0.8, 1.0, 0.1]} fontSize={0.18} color="#FFFFFF">
            {formatDay(dayData.date, index)}
          </Text>
          <Text position={[0.8, 1.0, 0.1]} fontSize={0.15} color="#FFFFFF">
            {Math.round(dayData.day.maxtemp_f)}° / {Math.round(dayData.day.mintemp_f)}°
          </Text>
          <Text position={[-0.8, -1.0, 0.1]} fontSize={0.13} color="#FFFFFF">
            {dayData.day.condition.text}
          </Text>
        </group>
      );
    };

    The roundedPlaneGeometry from the maath library gives our portals those smooth, organic edges instead of sharp rectangles. The [2, 2.5, 0.15] parameters create a 2×2.5 unit portal with 0.15 radius corners, providing enough rounding to look visually appealing.

    Interactive States and Animations

    Portals respond to user interaction with smooth state transitions. The system tracks two primary states: inactive and fullscreen:

    // ForecastPortals.js - State management and blend animations
    const ForecastPortal = ({ position, dayData, isActive, isFullscreen, onEnter }) => {
      const materialRef = useRef();
    
      useFrame(() => {
        if (materialRef.current) {
          // Smooth blend animation - only inactive (0) or fullscreen (1)
          const targetBlend = isFullscreen ? 1 : 0;
          materialRef.current.blend = THREE.MathUtils.lerp(
            materialRef.current.blend || 0,
            targetBlend,
            0.1
          );
        }
      });
    
      // Portal content and UI elements hidden in fullscreen mode
      return (
        <group position={position}>
          <mesh onClick={onEnter}>
            <roundedPlaneGeometry args={[2, 2.5, 0.15]} />
            <MeshPortalMaterial ref={materialRef}>
              <PortalScene />
            </MeshPortalMaterial>
          </mesh>
    
          {!isFullscreen && (
            <>
              {/* Temperature and condition text only show in preview mode */}
              <Text position={[-0.8, 1.0, 0.1]} fontSize={0.18} color="#FFFFFF">
                {formatDay(dayData.date, index)}
              </Text>
            </>
          )}
        </group>
      );
    };

    The blend property controls how much the portal takes over your view. At 0 (inactive), you see the portal as a framed window into the weather scene. At 1 (fullscreen), you’re completely transported inside that day’s weather environment. The THREE.MathUtils.lerp function creates smooth transitions between these two states when clicking in and out of portals.

    Fullscreen Portal Experience

    When you click a portal, it fills your entire view with that day’s weather. Instead of looking at tomorrow’s weather through a window, you’re standing inside it:

    // Scene3D.js - Fullscreen portal handling
    const handlePortalStateChange = (isPortalActive, dayData) => {
      setPortalMode(isPortalActive);
      if (isPortalActive && dayData) {
        // Create immersive weather environment for the selected day
        const portalData = {
          current: {
            temp_f: dayData.day.maxtemp_f,
            condition: dayData.day.condition,
            is_day: 1,
            humidity: dayData.day.avghumidity,
          },
          location: { localtime: dayData.date + 'T12:00' }
        };
        setPortalWeatherData(portalData);
      }
    };

    In fullscreen mode, the portal weather data drives the entire scene: the Sky component, lighting, and all weather effects now represent that forecasted day. You can orbit around inside tomorrow’s storm or bask in the gentle sunlight of the day after. When you exit (click outside the portal), the system smoothly transitions back to the current weather conditions.

    The key insight is that each portal runs our same WeatherVisualization component but with forecast data instead of current conditions. The portalMode={true} prop optimizes the components for smaller render targets—fewer particles, simpler clouds, but the same conditional logic we built earlier.

    Now that we’ve introduced portals, we need to update our weather components to support this optimization. Going back to our conditional rendering examples, we add the portalMode prop:

    // WeatherVisualization.js - Updated with portal support
    if (weatherType === 'rainy') {
      return (
        <>
          <Clouds intensity={0.8} speed={0.15} portalMode={portalMode} />
          <Rain count={portalMode ? 100 : 800} />
        </>
      );
    } else if (weatherType === 'snowy') {
      return (
        <>
          <Clouds intensity={0.6} speed={0.05} portalMode={portalMode} />
          <Snow count={portalMode ? 50 : 400} />
        </>
      );
    }

    And our Clouds component is updated to render fewer, simpler clouds in portal mode:

    // Clouds.js - Portal optimization
    const Clouds = ({ intensity = 0.7, speed = 0.1, portalMode = false }) => {
      if (portalMode) {
        return (
          <DreiClouds material={THREE.MeshLambertMaterial}>
            {/* Only 2 centered clouds for portal preview */}
            <Cloud segments={40} bounds={[8, 3, 3]} volume={8} position={[0, 4, -2]} />
            <Cloud segments={35} bounds={[6, 2.5, 2.5]} volume={6} position={[2, 3, -3]} />
          </DreiClouds>
        );
      }
      // Full cloud system for main scene (6+ detailed clouds)
      return <group>{/* ... full cloud configuration ... */}</group>;
    };

    This dramatically reduces both particle counts (87.5% fewer rain particles) and cloud complexity (a 67% reduction from 6 detailed clouds to 2 centered clouds), ensuring smooth performance when multiple portals show weather effects simultaneously.

    Integration with Scene3D

    The portals are positioned and managed in our main Scene3D component, where they complement the current weather visualization:

    // Scene3D.js - Portal integration
    <>
      {/* Current weather in the main scene */}
      <WeatherVisualization
        weatherData={weatherData}
        isLoading={isLoading}
      />
    
      {/* Three-day forecast portals */}
      <ForecastPortals
        weatherData={weatherData}
        isLoading={isLoading}
        onPortalStateChange={handlePortalStateChange}
      />
    </>

    When you click a portal, the entire scene transitions to fullscreen mode, showing that day’s weather in complete detail. The portal system tracks active states and handles smooth transitions between preview and immersive modes, creating a seamless way to explore future weather conditions alongside the current atmospheric environment.

    The portals transform static forecast numbers into explorable 3D environments. Instead of reading “Tomorrow: 75°, Partly Cloudy,” you see and feel the gentle drift of cumulus clouds with warm sunlight filtering through.

    Adding Cinematic Lens Flares

    Our Sun component looks great, but to really make it feel cinematic, I wanted to implement a subtle lens flare effect. For this, I’m using the R3F-Ultimate-Lens-Flare library (shoutout to Anderson Mancini), which I installed manually by following the repository’s instructions. While lens flares typically work best with distant sun objects rather than our close-up approach, I still think it adds a nice cinematic touch to the scene.

    The lens flare system needs to be smart about when to appear. Just like our weather components, it should only show when it makes meteorological sense:

    // Scene3D.js - Conditional lens flare rendering
    const PostProcessingEffects = ({ showLensFlare }) => {
      if (!showLensFlare) return null;
    
      return (
        <EffectComposer>
          <UltimateLensFlare
            position={[0, 5, 0]} // Positioned near our Sun component at [0, 4.5, 0]
            opacity={1.00}
            glareSize={1.68}
            starPoints={2}
            animated={false}
            flareShape={0.81}
            flareSize={1.68}
            secondaryGhosts={true}
            ghostScale={0.03}
            aditionalStreaks={true}
            haloScale={3.88}
          />
          <Bloom intensity={0.3} threshold={0.9} />
        </EffectComposer>
      );
    };

    The key parameters create a realistic lens flare effect: glareSize and flareSize both at 1.68 give prominent but not overwhelming flares, while ghostScale={0.03} adds subtle lens reflection artifacts. The haloScale={3.88} creates that large atmospheric glow around the sun.

    The lens flare connects to our weather system through a visibility function that determines when the sun should be visible:

    // weatherService.js - When should we show lens flares?
    export const shouldShowSun = (weatherData) => {
      if (!weatherData?.current?.condition) return true;
      const condition = weatherData.current.condition.text.toLowerCase();
    
      // Hide lens flare when weather obscures the sun
      if (condition.includes('overcast') ||
          condition.includes('rain') ||
          condition.includes('storm') ||
          condition.includes('snow')) {
        return false;
      }
    
      return true; // Show for clear, sunny, partly cloudy conditions
    };
    
    // Scene3D.js - Combining weather and time conditions
    const showLensFlare = useMemo(() => {
      if (isNight || !weatherData) return false;
      return shouldShowSun(weatherData);
    }, [isNight, weatherData]);

    This creates realistic behavior where lens flares only appear during daytime clear weather. During storms, the sun (and its lens flare) is hidden by clouds, just like in real life.

    Performance Optimizations

    Since we’re rendering thousands of particles, multiple cloud systems, and interactive portals—sometimes simultaneously—it can get expensive. As mentioned above, all our particle systems use instanced rendering to draw thousands of raindrops or snowflakes in single GPU calls. Conditional rendering ensures we only load the weather effects we actually need: no rain particles during sunny weather, no lens flares during storms. However, there’s still a lot of room for optimization. The most significant improvement comes from our portal system’s adaptive rendering. We already discussed decreasing the number of clouds in portals above, but when multiple forecast portals show precipitation simultaneously, we dramatically reduce particle counts.

    // WeatherVisualization.js - Smart particle scaling
    {weatherType === 'rainy' && <Rain count={portalMode ? 100 : 800} />}
    {weatherType === 'snowy' && <Snow count={portalMode ? 50 : 400} />}

    This prevents the less-than-ideal scenario of rendering 4 × 800 = 3,200 rain particles when all portals show rain. Instead, we get 800 + (3 × 100) = 1,100 total particles while maintaining the visual effect.

    API Reliability and Caching

    Beyond 3D performance, we need the app to work reliably even when the weather API is slow, down, or rate-limited. The system implements smart caching and graceful degradation to keep the experience smooth.

    Intelligent Caching

    Rather than hitting the API for every request, we cache weather responses for 10 minutes:

    // api/weather.js - Simple but effective caching
    const cache = new Map();
    const CACHE_DURATION = 10 * 60 * 1000; // 10 minutes
    
    const cacheKey = `weather:${location.toLowerCase()}`;
    const cachedData = cache.get(cacheKey);
    
    if (cachedData && Date.now() - cachedData.timestamp < CACHE_DURATION) {
      return res.json({ ...cachedData.data, cached: true });
    }

    This gives users instant responses for recently searched locations and keeps the app responsive during API slowdowns.

    Rate Limiting and Fallback

    When users exceed our 15 requests per hour limit, the system smoothly switches to demo data instead of showing errors:

    // weatherService.js - Graceful degradation
    if (error.response?.status === 429) {
      console.log('Too many requests');
      return getDemoWeatherData(location);
    }

    The demo data includes time-aware day/night detection, so even the fallback experience shows proper lighting and sky colors based on the user’s local time.

    Future Enhancements

    There’s plenty of room to expand this weather world. Adding accurate moon phases would bring another layer of realism to nighttime scenes—right now our moon is perpetually full. Wind effects could animate vegetation or create drifting fog patterns, using the wind speed data we’re already fetching but not yet visualizing. Performance-wise, the current optimizations handle most scenarios well, but there’s still room for improvement, especially when all forecast portals show precipitation simultaneously.

    Conclusion

    Building this 3D weather visualization combined React Three Fiber with real-time meteorological data to create something beyond a traditional weather app. By leveraging Drei’s ready-made components alongside custom particle systems, we’ve transformed API responses into explorable atmospheric environments.

    The technical foundation combines several key approaches:

    • Instanced rendering for particle systems that maintain 60fps while simulating thousands of raindrops
    • Conditional component loading that only renders the weather effects currently needed
    • Portal-based scene composition using MeshPortalMaterial for forecast previews
    • Time-aware atmospheric rendering with Drei’s Sky component responding to local sunrise and sunset
    • Smart caching and fallback systems that keep the experience responsive during API limitations

    This was something I always wanted to build, and I had a ton of fun bringing it to life!



    Source link

  • Building an Infinite Marquee Along an SVG Path with React & Motion



    Learn how to create an infinite marquee that follows a custom SVG path using React and Motion.



    Source link

  • Integrating Rive into a React Project: Behind the Scenes of Valley Adventures

    Integrating Rive into a React Project: Behind the Scenes of Valley Adventures


    Bringing new tools into a workflow is always exciting—curiosity bumps up against the comfort of familiar methods. But when our longtime client, Chumbi Valley, came to us with their Valley Adventures project, we saw the perfect opportunity to experiment with Rive and craft cartoon-style animations that matched the playful spirit of the brand.

    Rive is a powerful real-time interactive design tool with built-in support for interactivity through State Machines. In this guide, we’ll walk you through how we integrated a .riv file into a React environment and added mouse-responsive animations.

    We’ll also walk through a modernized integration method using Rive’s newer Data Binding feature—our current preferred approach for achieving the same animation with less complexity and greater flexibility.

    Animation Concept & File Preparation

    Valley Adventures is a gamified Chumbi NFT staking program, where magical creatures called Chumbi inhabit an enchanted world. The visual direction leans heavily into fairytale book illustrations—vibrant colors, playful characters, and a whimsical, cartoon-like aesthetic.

    To immediately immerse users in this world, we went with a full-section hero animation on the landing page. We split the animation into two parts:

    • an idle animation that brings the scene to life;
    • a cursor-triggered parallax effect, adding depth and interactivity.

    Several elements animate simultaneously—background layers like rustling leaves and flickering fireflies, along with foreground characters that react to movement. The result is a dynamic, storybook-like experience that invites users to explore.

    The most interesting—and trickiest—part of the integration was tying animations to mouse tracking. Rive provides a built-in way to handle this: by applying constraints with varying strengths to elements within a group that’s linked to Mouse Tracking, which itself responds to the cursor’s position.

    However, we encountered a limitation with this approach: the HTML buttons layered above the Rive asset were blocking the hover state, preventing it from triggering the animation beneath.

    To work around this, we used a more robust method that gave us finer control and avoided those problems altogether. 

    Here’s how we approached it:

    1. Create four separate timelines, each with a single keyframe representing an extreme position of the animation group:
      • Far left
      • Far right
      • Top
      • Bottom
    2. Add two animation layers, each responsible for blending between opposite keyframes:
      • Layer 1 blends the far-left and far-right timelines
      • Layer 2 blends the top and bottom timelines
    3. Tie each layer’s blend amount to a numeric input—one for the X axis, one for the Y axis.

    By adjusting the values of these inputs based on the cursor’s position, you can control how tightly the animation responds on each axis. This approach gives you a smoother, more customizable parallax effect—and prevents unexpected behavior caused by overlapping UI.

    Once the animation is ready, simply export it as a .riv file—and leave the rest of the magic to the devs.

    How We Did It: Integrating a Rive File into a React Project

    Before we dive further, let’s clarify what a .riv file actually is.

    A .riv file is the export format from the Rive editor. It can include:

    • vector graphics,
    • timeline animations,
    • a State Machine with input parameters.

    In our case, we’re using a State Machine with two numeric inputs: Axis_X and Axis_Y. These inputs are tied to how we control animation in Rive, using values from the X and Y axes of the cursor’s position.

    These inputs drive the movement of different elements—like the swaying leaves, fluttering fireflies, and even subtle character reactions—creating a smooth, interactive experience that responds to the user’s mouse.

    Step-by-Step Integration

    Step 1: Install the Rive React runtime

    Install the official package:

    npm install @rive-app/react-canvas

    Step 2: Create an Animation Component

    Create a component called RiveBackground.tsx to handle loading and rendering the animation.

    Step 3: Connect animation

    const { rive, setCanvasRef, setContainerRef } = useRive({
      src: 'https://cdn.rive.app/animations/hero.riv',
      autoplay: true,
      layout: new Layout({ fit: Fit.Cover, alignment: Alignment.Center }),
      onLoad: () => setIsLoaded(true),
      enableRiveAssetCDN: true,
    });
    

    For a better understanding, let’s take a closer look at each prop you’ll typically use when working with Rive in React:

    What each option does:

    Property Description
    src Path to your .riv file — can be local or hosted via CDN
    autoplay Automatically starts the animation once it’s loaded
    layout Controls how the animation fits into the canvas (we’re using Cover and Center)
    onLoad Callback that fires when the animation is ready — useful for setting isLoaded
    enableRiveAssetCDN Allows loading of external assets (like fonts or textures) from Rive’s CDN

    Step 4: Connect State Machine Inputs

    const numX = useStateMachineInput(rive, 'State Machine 1', 'Axis_X', 0);
    const numY = useStateMachineInput(rive, 'State Machine 1', 'Axis_Y', 0);

    This setup connects directly to the input values defined inside the State Machine, allowing us to update them dynamically in response to user interaction.

    • State Machine 1 — the name of your State Machine, exactly as defined in the Rive editor
    • Axis_X and Axis_Y — numeric inputs that control movement based on cursor position
    • 0 — the initial (default) value for each input

    ☝️ Important: Make sure your .riv file includes the exact names: Axis_X, Axis_Y, and State Machine 1. These must match what’s defined in the Rive editor — otherwise, the animation won’t respond as expected.

    Step 5: Handle Mouse Movement

    useEffect(() => {
      if (!numX || !numY) return;
    
      const handleMouseMove = (e: MouseEvent) => {
        const { innerWidth, innerHeight } = window;
        numX.value = (e.clientX / innerWidth) * 100;
        numY.value = 100 - (e.clientY / innerHeight) * 100;
      };
    
      window.addEventListener('mousemove', handleMouseMove);
      return () => window.removeEventListener('mousemove', handleMouseMove);
    }, [numX, numY]);

    What’s happening here:

    • We use clientX and clientY to track the mouse position within the browser window.
    • The values are normalized to a 0–100 range, matching what the animation expects.
    • These normalized values are then passed to the Axis_X and Axis_Y inputs in the Rive State Machine, driving the interactive animation.

    ⚠️ Important: Always remember to remove the event listener when the component unmounts to avoid memory leaks and unwanted behavior. 

    Step 6: Cleanup and Render the Component

    useEffect(() => {
      return () => rive?.cleanup();
    }, [rive]);

    And the render:

    return (
      <div
        ref={setContainerRef}
        className={`rive-container ${className ?? ''} ${isLoaded ? 'show' : 'hide'}`}
      >
        <canvas ref={setCanvasRef} />
      </div>
    );
    • cleanup() — frees up resources when the component unmounts. Always call this to prevent memory leaks.
    • setCanvasRef and setContainerRef — these must be connected to the correct DOM elements in order for Rive to render the animation properly.

    And here’s the complete code:

    import {
      useRive,
      useStateMachineInput,
      Layout,
      Fit,
      Alignment,
    } from '@rive-app/react-canvas';
    import { useEffect, useState } from 'react';
    
    export function RiveBackground({ className }: { className?: string }) {
      const [isLoaded, setIsLoaded] = useState(false);
    
      const { rive, setCanvasRef, setContainerRef } = useRive({
        src: 'https://cdn.rive.app/animations/hero.riv',
        animations: ['State Machine 1','Timeline 1','Timeline 2'
    ],
        autoplay: true,
        layout: new Layout({ fit: Fit.Cover, alignment: Alignment.Center }),
        onLoad: () => setIsLoaded(true),
        enableRiveAssetCDN: true,
      });
    
      const numX = useStateMachineInput(rive, 'State Machine 1', 'Axis_X', 0);
      const numY = useStateMachineInput(rive, 'State Machine 1', 'Axis_Y', 0);
    
      useEffect(() => {
        if (!numX || !numY) return;
    
        const handleMouseMove = (e: MouseEvent) => {
    	if (!numX || !numY) {
            return;
          }
    
          const { innerWidth, innerHeight } = window;
          numX.value = (e.clientX / innerWidth) * 100;
          numY.value = 100 - (e.clientY / innerHeight) * 100;
        };
    
        window.addEventListener('mousemove', handleMouseMove);
        return () => window.removeEventListener('mousemove', handleMouseMove);
      }, [numX, numY]);
    
      useEffect(() => {
        return () => {
          rive?.cleanup();
        };
      }, [rive]);
    
      return (
        <div
          ref={setContainerRef}
          className={`rive-container ${className ?? ''} ${isLoaded ? 'show' : 'hide'}`}
        >
          <canvas ref={setCanvasRef} />
        </div>
      );
    }
    

    Step 7: Use the Component

    Now you can use the RiveBackground like any other component:

    <RiveBackground className="hero-background" />

    Step 8: Preload the WASM File

    To avoid loading the .wasm file at runtime—which can delay the initial render—you can preload it in App.tsx:

    import riveWASMResource from '@rive-app/canvas/rive.wasm';
    
    <link
      rel="preload"
      href={riveWASMResource}
      as="fetch"
      crossOrigin="anonymous"
    />

    This is especially useful if you’re optimizing for first paint or overall performance.

    Simple Parallax: A New Approach with Data Binding

    In the first part of this article, we used a classic approach with a State Machine to create the parallax animation in Rive. We built four separate animations (top, bottom, left, right), controlled them using input variables, and blended their states to create smooth motion. This method made sense at the time, especially before Data Binding support was introduced.

    But now that Data Binding is available in Rive, achieving the same effect is much simpler—just a few steps. Data binding in Rive is a system that connects editor elements to dynamic data and code via view models, enabling reactive, runtime-driven updates and interactions between design and development.

    In this section, we’ll show how to refactor the original Rive file and code using the new approach.

    Updating the Rive File

    1. Remove the old setup:
      • Go to the State Machine.
      • Delete the input variables: top, bottom, left, right.
      • Remove the blending states and their associated animations.
    2. Group the parallax layers:
      • Wrap all the parallax layers into a new group—e.g., ParallaxGroup.
    3. Create binding parameters:
      • Select ParallaxGroup and add:
        • pointerX (Number)
        • pointerY (Number)
    4. Bind coordinates:
      • In the properties panel, set:
        • X → pointerX
        • Y → pointerY

    Now the group will move dynamically based on values passed from JavaScript.

    The Updated JS Code

    Before we dive into the updated JavaScript, let’s quickly define an important concept:

    When using Data Binding in Rive, viewModelInstance refers to the runtime object that links your Rive file’s bindable properties (like pointerX or pointerY) to your app’s logic. In the Rive editor, you assign these properties to elements like positions, scales, or rotations. At runtime, your code accesses and updates them through the viewModelInstance—allowing for real-time, declarative control without needing a State Machine.

    With that in mind, here’s how the new setup replaces the old input-driven logic:

    import { useRive } from '@rive-app/react-canvas';
    import { useEffect, useState } from 'react';
    
    export function ParallaxEffect({ className }: { className?: string }) {
      const [isLoaded, setIsLoaded] = useState(false);
    
      const { rive, setCanvasRef, setContainerRef } = useRive({
        src: 'https://cdn.rive.app/animations/hero.riv',
        autoplay: true,
        autoBind: true,
        onLoad: () => setIsLoaded(true),
      });
    
      useEffect(() => {
        if (!rive) return;
    
        const vmi = rive.viewModelInstance;
        const pointerX = vmi?.number('pointerX');
        const pointerY = vmi?.number('pointerY');
    
        if (!pointerX || !pointerY) return;
    
        const handleMouseMove = (e: MouseEvent) => {
          const { innerWidth, innerHeight } = window;
          const x = (e.clientX / innerWidth) * 100;
          const y = 100 - (e.clientY / innerHeight) * 100;
          pointerX.value = x;
          pointerY.value = y;
        };
    
        window.addEventListener('mousemove', handleMouseMove);
    
        return () => {
          window.removeEventListener('mousemove', handleMouseMove);
          rive.cleanup();
        };
      }, [rive]);
    
      return (
        <div
          ref={setContainerRef}
          className={`rive-container ${className ?? ''} ${isLoaded ? 'show' : 'hide'}`}
        >
          <canvas ref={setCanvasRef} />
        </div>
      );
    }

    The Result

    You get the same parallax effect, but:

    • without input variables or blending;
    • without a State Machine;
    • with simple control via the ViewModel.

    Official Live Example from Rive

    👉 CodeSandbox: Data Binding Parallax

    Conclusion

    Data Binding is a major step forward for interactive Rive animations. Effects like parallax can now be set up faster, more reliably, and with cleaner logic. We strongly recommend this approach for new projects.

    Final Thoughts

    So why did we choose Rive over Lottie for this project?

    • Interactivity: With Lottie, achieving the same level of interactivity would’ve required building a custom logic layer from scratch. With Rive, we got that behavior baked into the file—plug and play.
    • Optimization: Rive gives you more control over each asset inside the .riv file, and the output tends to be lighter overall.

    Our biggest takeaway? Don’t be afraid to experiment with new tools—especially when they feel like the right fit for your project’s concept. Rive matched the playful, interactive vibe of Valley Adventures perfectly, and we’re excited to keep exploring what it can do.



    Source link

  • JavaScript and TypeScript Projects with React, Angular, or Vue in Visual Studio 2022 with or without .NET

    JavaScript and TypeScript Projects with React, Angular, or Vue in Visual Studio 2022 with or without .NET



    I was reading Gabby’s blog post about the new TypeScript/JavaScript project experience in Visual Studio 2022. You should read the docs on JavaScript and TypeScript in Visual Studio 2022.

    If you’re used to ASP.NET apps when you think about apps that are JavaScript heavy, “front end apps” or TypeScript focused, it can be confusing as to “where does .NET fit in?”

    You need to consider the responsibilities of your various projects or subsystems and the multiple totally valid ways you can build a web site or web app. Let’s consider just a few:

    1. An ASP.NET Web app that renders HTML on the server but uses TS/JS
      • This may have a Web API, Razor Pages, with or without the MVC pattern.
      • You maybe have just added JavaScript via <script> tags
      • Maybe you added a script minimizer/minifier task
      • Can be confusing because it can feel like your app needs to ‘build both the client and the server’ from one project
    2. A mostly JavaScript/TypeScript frontend app where the HTML could be served from any web server (node, kestrel, static web apps, nginx, etc)
      • This app may use Vue or React or Angular but it’s not an “ASP.NET app”
      • It calls backend Web APIs that may be served by ASP.NET, Azure Functions, 3rd party REST APIs, or all of the above
      • This scenario has sometimes been confusing for ASP.NET developers who may get confused about responsibility. Who builds what, where do things end up, how do I build and deploy this?

    VS2022 brings JavaScript and TypeScript support into VS with a full JavaScript Language Service based on TS. It provides a TypeScript NuGet Package so you can build your whole app with MSBuild and VS will do the right thing.

    NEW: Starting in Visual Studio 2022, there is a new JavaScript/TypeScript project type (.esproj) that allows you to create standalone Angular, React, and Vue projects in Visual Studio.

    The .esproj concept is great for folks familiar with Visual Studio as we know that a Solution contains one or more Projects. Visual Studio manages files for a single application in a Project. The project includes source code, resources, and configuration files. In this case we can have a .csproj for a backend Web API and an .esproj that uses a client side template like Angular, React, or Vue.

    Thing is, historically when Visual Studio supported Angular, React, or Vue, it’s templates were out of date and not updated enough. VS2022 uses the native CLIs for these front ends, solving that problem with Angular CLI, Create React App, and Vue CLI.

    If I am in VS and go “File New Project” there are Standalone templates that solve Example 2 above. I’ll pick JavaScript React.

    Standalone JavaScript Templates in VS2022

    Then I’ll click “Add integration for Empty ASP.NET Web API. This will give me a frontend with javascript ready to call a ASP.NET Web API backend. I’ll follow along here.

    Standalone JavaScript React Template

    It then uses the React CLI to make the front end, which again, is cool as it’s whatever version I want it to be.

    React Create CLI

    Then I’ll add my ASP.NET Web API backend to the same solution, so now I have an esproj and a csproj like this

    frontend and backend

    Now I have a nice clean two project system – in this case more JavaScript focused than .NET focused. This one uses npm to startup the project using their web development server and proxyMiddleware to proxy localhost:3000 calls over to the ASP.NET Web API project.

    Here is a React app served by npm calling over to the Weather service served from Kestrel on ASP.NET.

    npm app running in VS 2022 against an ASP.NET Web API

    This is inverted than most ASP.NET Folks are used to, and that’s OK. This shows me that Visual Studio 2022 can support either development style, use the CLI that is installed for whatever Frontend Framework, and allow me to choose what web server and web browser (via Launch.json) I want.

    If you want to flip it, and put ASP.NET Core as the primary and then bring in some TypeScript/JavaScript, follow this tutorial because that’s also possible!


    Sponsor: Make login Auth0’s problem. Not yours. Provide the convenient login features your customers want, like social login, multi-factor authentication, single sign-on, passwordless, and more. Get started for free.




    About Scott

    Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

    facebook
    bluesky
    subscribe
    About   Newsletter

    Hosting By
    Hosted on Linux using .NET in an Azure App Service










    Source link