├── src
└── app
│ ├── favicon.ico
│ ├── rain
│ ├── img
│ │ ├── bg.jpg
│ │ ├── drop-alpha.png
│ │ ├── drop-color.png
│ │ ├── drop-shine.png
│ │ ├── drop-shine2.png
│ │ ├── water
│ │ │ ├── texture-bg.png
│ │ │ └── texture-fg.png
│ │ └── weather
│ │ │ ├── texture-rain-bg.png
│ │ │ ├── texture-rain-fg.png
│ │ │ ├── texture-sun-bg.png
│ │ │ ├── texture-sun-fg.png
│ │ │ ├── texture-drizzle-bg.png
│ │ │ ├── texture-drizzle-fg.png
│ │ │ ├── texture-fallout-bg.png
│ │ │ ├── texture-fallout-fg.png
│ │ │ ├── texture-storm-lightning-bg.png
│ │ │ └── texture-storm-lightning-fg.png
│ ├── times.jsx
│ ├── shaders
│ │ ├── simple.vert
│ │ └── water.frag
│ ├── create-canvas.jsx
│ ├── random.jsx
│ ├── rain-utils.jsx
│ ├── image-loader.jsx
│ ├── gl-obj.jsx
│ ├── page.jsx
│ ├── webgl.jsx
│ ├── RainEffect.jsx
│ ├── rain-renderer.jsx
│ ├── weather-utils.jsx
│ └── raindrops.jsx
│ ├── snow
│ ├── snowflake.png
│ ├── page.jsx
│ ├── ShaderProgram.jsx
│ └── SnowEffect.jsx
│ ├── fog
│ ├── fog-element.png
│ ├── dense-fog-element.png
│ ├── page.jsx
│ └── FogEffect.jsx
│ ├── page.js
│ ├── globals.css
│ ├── layout.js
│ └── components
│ └── Navbar.jsx
├── jsconfig.json
├── postcss.config.mjs
├── public
└── assets
│ ├── budapest-rain.png
│ ├── new-york-snow.png
│ └── san-francisco-fog.png
├── next.config.mjs
├── eslint.config.mjs
├── .gitignore
├── package.json
├── LICENSE
└── README.md
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauschermate/react-weather-effects/HEAD/src/app/favicon.ico
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "paths": {
4 | "@/*": ["./src/*"]
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/app/rain/img/bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauschermate/react-weather-effects/HEAD/src/app/rain/img/bg.jpg
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | const config = {
2 | plugins: ["@tailwindcss/postcss"],
3 | };
4 |
5 | export default config;
6 |
--------------------------------------------------------------------------------
/src/app/snow/snowflake.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauschermate/react-weather-effects/HEAD/src/app/snow/snowflake.png
--------------------------------------------------------------------------------
/src/app/fog/fog-element.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauschermate/react-weather-effects/HEAD/src/app/fog/fog-element.png
--------------------------------------------------------------------------------
/public/assets/budapest-rain.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauschermate/react-weather-effects/HEAD/public/assets/budapest-rain.png
--------------------------------------------------------------------------------
/public/assets/new-york-snow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauschermate/react-weather-effects/HEAD/public/assets/new-york-snow.png
--------------------------------------------------------------------------------
/src/app/rain/img/drop-alpha.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauschermate/react-weather-effects/HEAD/src/app/rain/img/drop-alpha.png
--------------------------------------------------------------------------------
/src/app/rain/img/drop-color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauschermate/react-weather-effects/HEAD/src/app/rain/img/drop-color.png
--------------------------------------------------------------------------------
/src/app/rain/img/drop-shine.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauschermate/react-weather-effects/HEAD/src/app/rain/img/drop-shine.png
--------------------------------------------------------------------------------
/src/app/fog/dense-fog-element.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauschermate/react-weather-effects/HEAD/src/app/fog/dense-fog-element.png
--------------------------------------------------------------------------------
/src/app/page.js:
--------------------------------------------------------------------------------
1 | import { redirect } from 'next/navigation';
2 |
3 | export default function Home() {
4 | redirect('/snow');
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/rain/img/drop-shine2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauschermate/react-weather-effects/HEAD/src/app/rain/img/drop-shine2.png
--------------------------------------------------------------------------------
/public/assets/san-francisco-fog.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauschermate/react-weather-effects/HEAD/public/assets/san-francisco-fog.png
--------------------------------------------------------------------------------
/src/app/rain/times.jsx:
--------------------------------------------------------------------------------
1 | export default function times(n,f){
2 | for (let i = 0; i < n; i++) {
3 | f.call(this,i);
4 | }
5 | }
--------------------------------------------------------------------------------
/src/app/rain/img/water/texture-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauschermate/react-weather-effects/HEAD/src/app/rain/img/water/texture-bg.png
--------------------------------------------------------------------------------
/src/app/rain/img/water/texture-fg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauschermate/react-weather-effects/HEAD/src/app/rain/img/water/texture-fg.png
--------------------------------------------------------------------------------
/src/app/rain/img/weather/texture-rain-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauschermate/react-weather-effects/HEAD/src/app/rain/img/weather/texture-rain-bg.png
--------------------------------------------------------------------------------
/src/app/rain/img/weather/texture-rain-fg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauschermate/react-weather-effects/HEAD/src/app/rain/img/weather/texture-rain-fg.png
--------------------------------------------------------------------------------
/src/app/rain/img/weather/texture-sun-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauschermate/react-weather-effects/HEAD/src/app/rain/img/weather/texture-sun-bg.png
--------------------------------------------------------------------------------
/src/app/rain/img/weather/texture-sun-fg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauschermate/react-weather-effects/HEAD/src/app/rain/img/weather/texture-sun-fg.png
--------------------------------------------------------------------------------
/src/app/rain/img/weather/texture-drizzle-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauschermate/react-weather-effects/HEAD/src/app/rain/img/weather/texture-drizzle-bg.png
--------------------------------------------------------------------------------
/src/app/rain/img/weather/texture-drizzle-fg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauschermate/react-weather-effects/HEAD/src/app/rain/img/weather/texture-drizzle-fg.png
--------------------------------------------------------------------------------
/src/app/rain/img/weather/texture-fallout-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauschermate/react-weather-effects/HEAD/src/app/rain/img/weather/texture-fallout-bg.png
--------------------------------------------------------------------------------
/src/app/rain/img/weather/texture-fallout-fg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauschermate/react-weather-effects/HEAD/src/app/rain/img/weather/texture-fallout-fg.png
--------------------------------------------------------------------------------
/src/app/rain/shaders/simple.vert:
--------------------------------------------------------------------------------
1 | precision mediump float;
2 |
3 | attribute vec2 a_position;
4 |
5 | void main() {
6 | gl_Position = vec4(a_position,0.0,1.0);
7 | }
--------------------------------------------------------------------------------
/src/app/rain/img/weather/texture-storm-lightning-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauschermate/react-weather-effects/HEAD/src/app/rain/img/weather/texture-storm-lightning-bg.png
--------------------------------------------------------------------------------
/src/app/rain/img/weather/texture-storm-lightning-fg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rauschermate/react-weather-effects/HEAD/src/app/rain/img/weather/texture-storm-lightning-fg.png
--------------------------------------------------------------------------------
/src/app/rain/create-canvas.jsx:
--------------------------------------------------------------------------------
1 | export default function createCanvas(width, height) {
2 | if (typeof document === "undefined") {
3 | return null;
4 | }
5 | let canvas = document.createElement("canvas");
6 | canvas.width = width;
7 | canvas.height = height;
8 | return canvas;
9 | }
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | webpack(config) {
4 | config.module.rules.push({
5 | test: /\.(glsl|vert|frag)$/,
6 | use: 'raw-loader',
7 | type: 'javascript/auto',
8 | });
9 | return config;
10 | },
11 | };
12 |
13 | export default nextConfig;
14 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { dirname } from "path";
2 | import { fileURLToPath } from "url";
3 | import { FlatCompat } from "@eslint/eslintrc";
4 |
5 | const __filename = fileURLToPath(import.meta.url);
6 | const __dirname = dirname(__filename);
7 |
8 | const compat = new FlatCompat({
9 | baseDirectory: __dirname,
10 | });
11 |
12 | const eslintConfig = [...compat.extends("next/core-web-vitals")];
13 |
14 | export default eslintConfig;
15 |
--------------------------------------------------------------------------------
/src/app/rain/random.jsx:
--------------------------------------------------------------------------------
1 | export function random(from=null,to=null,interpolation=null){
2 | if(from==null){
3 | from=0;
4 | to=1;
5 | }else if(from!=null && to==null){
6 | to=from;
7 | from=0;
8 | }
9 | const delta=to-from;
10 |
11 | if(interpolation==null){
12 | interpolation=(n)=>{
13 | return n;
14 | }
15 | }
16 | return from+(interpolation(Math.random())*delta);
17 | }
18 | export function chance(c){
19 | return random()<=c;
20 | }
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 |
3 | :root {
4 | --background: #ffffff;
5 | --foreground: #171717;
6 | }
7 |
8 | @theme inline {
9 | --color-background: var(--background);
10 | --color-foreground: var(--foreground);
11 | --font-sans: var(--font-geist-sans);
12 | --font-mono: var(--font-geist-mono);
13 | }
14 |
15 | @media (prefers-color-scheme: dark) {
16 | :root {
17 | --background: #0a0a0a;
18 | --foreground: #ededed;
19 | }
20 | }
21 |
22 | body {
23 | background: var(--background);
24 | color: var(--foreground);
25 | font-family: Arial, Helvetica, sans-serif;
26 | }
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.*
7 | .yarn/*
8 | !.yarn/patches
9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/versions
12 |
13 | # testing
14 | /coverage
15 |
16 | # next.js
17 | /.next/
18 | /out/
19 |
20 | # production
21 | /build
22 |
23 | # misc
24 | .DS_Store
25 | *.pem
26 |
27 | # debug
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 | .pnpm-debug.log*
32 |
33 | # env files (can opt-in for committing if needed)
34 | .env*
35 |
36 | # vercel
37 | .vercel
38 |
39 | # typescript
40 | *.tsbuildinfo
41 | next-env.d.ts
42 |
--------------------------------------------------------------------------------
/src/app/layout.js:
--------------------------------------------------------------------------------
1 | import { Geist, Geist_Mono } from "next/font/google";
2 | import "./globals.css";
3 | import Navbar from "./components/Navbar";
4 |
5 | const geistSans = Geist({
6 | variable: "--font-geist-sans",
7 | subsets: ["latin"],
8 | });
9 |
10 | const geistMono = Geist_Mono({
11 | variable: "--font-geist-mono",
12 | subsets: ["latin"],
13 | });
14 |
15 | export const metadata = {
16 | title: "3D demo",
17 | description: "by Mate Rauscher",
18 | };
19 |
20 | export default function RootLayout({ children }) {
21 | return (
22 |
23 |
26 |
27 | {children}
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "threejs",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev --turbopack",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@gsap/react": "^2.1.2",
13 | "@react-three/drei": "^10.3.0",
14 | "@react-three/fiber": "^9.1.2",
15 | "gsap": "^3.13.0",
16 | "maath": "^0.10.8",
17 | "next": "15.3.4",
18 | "raw-loader": "^4.0.2",
19 | "react": "^19.0.0",
20 | "react-dom": "^19.0.0",
21 | "three": "^0.177.0"
22 | },
23 | "devDependencies": {
24 | "@eslint/eslintrc": "^3",
25 | "@tailwindcss/postcss": "^4",
26 | "eslint": "^9",
27 | "eslint-config-next": "15.3.4",
28 | "tailwindcss": "^4"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Mate Rauscher
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/app/rain/rain-utils.jsx:
--------------------------------------------------------------------------------
1 | // Weather data for different types
2 | export const weatherData = {
3 | rain: {
4 | minR: 10,
5 | maxR: 40,
6 | rainChance: 0.35,
7 | rainLimit: 6,
8 | drizzle: 50,
9 | drizzleSize: [2, 4.5],
10 | raining: true,
11 | trailRate: 1,
12 | trailScaleRange: [0.2, 0.35],
13 | flashChance: 0
14 | },
15 | storm: {
16 | minR: 15,
17 | maxR: 45,
18 | rainChance: 0.55,
19 | rainLimit: 6,
20 | drizzle: 80,
21 | drizzleSize: [2, 6],
22 | trailRate: 1,
23 | trailScaleRange: [0.15, 0.3],
24 | flashChance: 0.1
25 | },
26 | fallout: {
27 | minR: 15,
28 | maxR: 45,
29 | rainChance: 0.45,
30 | rainLimit: 6,
31 | drizzle: 20,
32 | drizzleSize: [2, 4.5],
33 | raining: true,
34 | trailRate: 4,
35 | trailScaleRange: [0.2, 0.35],
36 | flashChance: 0.6
37 | },
38 | drizzle: {
39 | minR: 10,
40 | maxR: 40,
41 | rainChance: 0.15,
42 | rainLimit: 2,
43 | drizzle: 10,
44 | drizzleSize: [2, 4.5],
45 | raining: true,
46 | trailRate: 1,
47 | trailScaleRange: [0.2, 0.35],
48 | flashChance: 0
49 | }
50 | };
51 |
--------------------------------------------------------------------------------
/src/app/rain/image-loader.jsx:
--------------------------------------------------------------------------------
1 | function loadImage(src,i,onLoad){
2 | return new Promise((resolve,reject)=>{
3 | if(typeof src=="string"){
4 | src={
5 | name:"image"+i,
6 | src,
7 | };
8 | }
9 |
10 | let img=new Image();
11 | src.img=img;
12 |
13 | img.addEventListener("load",(event)=>{
14 | if(typeof onLoad=="function"){
15 | onLoad.call(null,img,i);
16 | }
17 |
18 | resolve(src);
19 | });
20 |
21 | img.src=src.src.src;
22 | })
23 | }
24 |
25 | function loadImages(images,onLoad){
26 | return Promise.all(images.map((src,i)=>{
27 | return loadImage(src,i,onLoad);
28 | }));
29 | }
30 |
31 | export default function ImageLoader(images,onLoad){
32 | return new Promise((resolve,reject)=>{
33 | loadImages(images,onLoad).then((loadedImages)=>{
34 | let r={};
35 | loadedImages.forEach((curImage)=>{
36 | r[curImage.name]={
37 | img:curImage.img,
38 | src:curImage.src,
39 | };
40 | });
41 | resolve(r);
42 | });
43 | })
44 | }
--------------------------------------------------------------------------------
/src/app/rain/gl-obj.jsx:
--------------------------------------------------------------------------------
1 | import * as WebGL from "./webgl";
2 |
3 | function GL(canvas,options,vert,frag){
4 | this.init(canvas,options,vert,frag);
5 | }
6 | GL.prototype={
7 | canvas:null,
8 | gl:null,
9 | program:null,
10 | width:0,
11 | height:0,
12 | init(canvas,options,vert,frag){
13 | this.canvas=canvas;
14 | this.width=canvas.width;
15 | this.height=canvas.height;
16 | this.gl=WebGL.getContext(canvas,options);
17 | this.program=this.createProgram(vert,frag);
18 | this.useProgram(this.program);
19 | },
20 | createProgram(vert,frag){
21 | let program=WebGL.createProgram(this.gl,vert,frag);
22 | return program;
23 | },
24 | useProgram(program){
25 | this.program=program;
26 | this.gl.useProgram(program);
27 | },
28 | createTexture(source,i){
29 | return WebGL.createTexture(this.gl,source,i);
30 | },
31 | createUniform(type,name,...v){
32 | WebGL.createUniform(this.gl,this.program,type,name,...v);
33 | },
34 | activeTexture(i){
35 | WebGL.activeTexture(this.gl,i);
36 | },
37 | updateTexture(source){
38 | WebGL.updateTexture(this.gl,source);
39 | },
40 | draw(){
41 | WebGL.setRectangle(this.gl, -1, -1, 2, 2);
42 | this.gl.drawArrays(this.gl.TRIANGLES, 0, 6);
43 | }
44 | }
45 |
46 | export default GL;
--------------------------------------------------------------------------------
/src/app/snow/page.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import SnowEffect from "./SnowEffect";
4 | import React, { useState } from "react";
5 |
6 | const SNOW_TYPES = [
7 | { type: "gentle", label: "Gentle" },
8 | { type: "storm", label: "Storm" },
9 | ];
10 |
11 | const BG_IMAGE = "https://images.unsplash.com/photo-1518391846015-55a9cc003b25?q=80&w=3540&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D";
12 |
13 | export default function SnowPage() {
14 | const [snowType, setSnowType] = useState("gentle");
15 |
16 | return (
17 | <>
18 |
19 |
28 | {SNOW_TYPES.map(({ type, label }) => (
29 |
42 | ))}
43 |
44 | >
45 | );
46 | }
--------------------------------------------------------------------------------
/src/app/fog/page.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import dynamic from 'next/dynamic';
4 | import React, { useState } from "react";
5 |
6 | const FogEffect = dynamic(() => import('./FogEffect'), { ssr: false });
7 |
8 | const FOG_TYPES = [
9 | { type: "light", label: "Light" },
10 | { type: "dense", label: "Dense" },
11 | ];
12 |
13 | const BG_IMAGE = "https://images.unsplash.com/photo-1719858403455-9a2582eca805?q=80&w=1599&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D";
14 |
15 | export default function FogPage() {
16 | const [fogType, setFogType] = useState("light");
17 |
18 | return (
19 | <>
20 |
21 |
30 | {FOG_TYPES.map(({ type, label }) => (
31 |
44 | ))}
45 |
46 | >
47 | );
48 | }
--------------------------------------------------------------------------------
/src/app/rain/page.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import RainEffect from "./RainEffect";
4 | import React, { useState } from "react";
5 |
6 | const RAIN_TYPES = [
7 | { type: "rain", label: "Rain" },
8 | { type: "storm", label: "Storm" },
9 | { type: "drizzle", label: "Drizzle" },
10 | { type: "fallout", label: "Fallout" },
11 | ];
12 |
13 | const BG_IMAGE = "https://images.unsplash.com/photo-1541343672885-9be56236302a?q=80&w=1587&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D";
14 |
15 | export default function RainPage() {
16 | const [rainType, setRainType] = useState("rain");
17 |
18 | return (
19 | <>
20 |
21 |
30 | {RAIN_TYPES.map(({ type, label }) => (
31 |
44 | ))}
45 |
46 | >
47 | );
48 | }
--------------------------------------------------------------------------------
/src/app/components/Navbar.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Link from "next/link";
4 | import { usePathname } from 'next/navigation';
5 |
6 | import Image from 'next/image';
7 |
8 | const NAV_ITEMS = [
9 | {
10 | href: '/rain',
11 | label: 'Rain',
12 | icon: '/assets/budapest-rain.png',
13 | },
14 | {
15 | href: '/snow',
16 | label: 'Snow',
17 | icon: '/assets/new-york-snow.png',
18 | },
19 | {
20 | href: '/fog',
21 | label: 'Fog',
22 | icon: '/assets/san-francisco-fog.png',
23 | },
24 | ];
25 |
26 | export default function Navbar() {
27 | const pathname = usePathname();
28 | return (
29 |
68 | );
69 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🌦️ Animated React Weather Effects
2 |
3 | A beautiful, interactive weather effects demo built with React, Next.js, WebGL, and Three.js. Experience realistic **rain**, **snow**, and **fog** scenes, each with unique visual effects and controls.
4 |
5 | ## 🚀 Demo
6 |
7 | [Live demo](https://react-weather-effects.vercel.app/)
8 |
9 | ## ✨ Features
10 |
11 | - **Rain:** Realistic WebGL rain with custom shaders, and lightning effects.
12 | - **Snow:** Gentle and stormy snow scenes powered by Three.js particle systems.
13 | - **Fog:** Light and dense fog overlays using Three.js and custom blending.
14 | - **Interactive:** Switch between weather types and subtypes (e.g., storm, drizzle) with a modern UI.
15 | - **Responsive:** Works on desktop and mobile browsers.
16 |
17 | ## 🌈 Weather Types
18 |
19 | - **Rain**
20 | - Rain
21 | - Storm (with lightning)
22 | - Drizzle
23 | - Fallout
24 | - **Snow**
25 | - Gentle
26 | - Storm
27 | - **Fog**
28 | - Light
29 | - Dense
30 |
31 | ## 🛠️ Technology
32 |
33 | - **React** & **Next.js** (App Router)
34 | - **WebGL** (custom shaders for rain)
35 | - **Three.js** (snow and fog effects)
36 | - **GSAP** (for smooth lightning and fog animations)
37 | - **Tailwind CSS** (for modern UI)
38 |
39 | ## 🖥️ Local Development
40 |
41 | ```bash
42 | npm install
43 | npm run dev
44 | ```
45 |
46 | Open [http://localhost:3000](http://localhost:3000) in your browser.
47 |
48 | ## 🗂️ Project Structure
49 |
50 | - `/src/app/rain/` – Rain effect (WebGL, shaders, rain types)
51 | - `/src/app/snow/` – Snow effect (Three.js, snow types)
52 | - `/src/app/fog/` – Fog effect (Three.js, fog types)
53 | - `/src/app/components/Navbar.jsx` – Navigation bar for switching weather types
54 |
55 | ---
56 |
57 | ## 🙏 Credits
58 |
59 | - **Rain shaders & inspiration:**
60 | [Lucas Bebber – RainEffect](https://github.com/codrops/RainEffect)
61 | [Shadertoy rain shader](https://www.shadertoy.com/view/ltffzl)
62 | - **Snow & fog inspiration:**
63 | [React three Fiber docs](https://r3f.docs.pmnd.rs/getting-started/examples)
64 | [Freezing cube](https://boytchev.github.io/etudes/webgl/freezing-cube.html)
65 | [Images from Unsplash](https://unsplash.com)
66 | [Snow effect inspiration #1](https://codepen.io/bsehovac/pen/GPwXxq)
67 | [Snow effect inspiration #2](https://codepen.io/bsehovac/full/GPwXxq)
68 | [Smoke effect](https://codepen.io/daniel3toma/pen/dybjNbZ)
69 |
70 | ---
71 |
72 | ## 📄 License
73 |
74 | MIT
75 |
--------------------------------------------------------------------------------
/src/app/rain/shaders/water.frag:
--------------------------------------------------------------------------------
1 |
2 | precision mediump float;
3 |
4 | // textures
5 | uniform sampler2D u_waterMap;
6 | uniform sampler2D u_textureShine;
7 | uniform sampler2D u_textureFg;
8 | uniform sampler2D u_textureBg;
9 |
10 | // the texCoords passed in from the vertex shader.
11 | varying vec2 v_texCoord;
12 | uniform vec2 u_resolution;
13 | uniform vec2 u_parallax;
14 | uniform float u_parallaxFg;
15 | uniform float u_parallaxBg;
16 | uniform float u_textureRatio;
17 | uniform bool u_renderShine;
18 | uniform bool u_renderShadow;
19 | uniform float u_minRefraction;
20 | uniform float u_refractionDelta;
21 | uniform float u_brightness;
22 | uniform float u_alphaMultiply;
23 | uniform float u_alphaSubtract;
24 |
25 | // alpha-blends two colors
26 | vec4 blend(vec4 bg,vec4 fg){
27 | vec3 bgm=bg.rgb*bg.a;
28 | vec3 fgm=fg.rgb*fg.a;
29 | float ia=1.0-fg.a;
30 | float a=(fg.a + bg.a * ia);
31 | vec3 rgb;
32 | if(a!=0.0){
33 | rgb=(fgm + bgm * ia) / a;
34 | }else{
35 | rgb=vec3(0.0,0.0,0.0);
36 | }
37 | return vec4(rgb,a);
38 | }
39 |
40 | vec2 pixel(){
41 | return vec2(1.0,1.0)/u_resolution;
42 | }
43 |
44 | vec2 parallax(float v){
45 | return u_parallax*pixel()*v;
46 | }
47 |
48 | vec2 texCoord(){
49 | return vec2(gl_FragCoord.x, u_resolution.y-gl_FragCoord.y)/u_resolution;
50 | }
51 |
52 | // scales the bg up and proportionally to fill the container
53 | vec2 scaledTexCoord(){
54 | float ratio=u_resolution.x/u_resolution.y;
55 | vec2 scale=vec2(1.0,1.0);
56 | vec2 offset=vec2(0.0,0.0);
57 | float ratioDelta=ratio-u_textureRatio;
58 | if(ratioDelta>=0.0){
59 | scale.y=(1.0+ratioDelta);
60 | offset.y=ratioDelta/2.0;
61 | }else{
62 | scale.x=(1.0-ratioDelta);
63 | offset.x=-ratioDelta/2.0;
64 | }
65 | return (texCoord()+offset)/scale;
66 | }
67 |
68 | // get color from fg
69 | vec4 fgColor(float x, float y){
70 | float p2=u_parallaxFg*2.0;
71 | vec2 scale=vec2(
72 | (u_resolution.x+p2)/u_resolution.x,
73 | (u_resolution.y+p2)/u_resolution.y
74 | );
75 |
76 | vec2 scaledTexCoord=texCoord()/scale;
77 | vec2 offset=vec2(
78 | (1.0-(1.0/scale.x))/2.0,
79 | (1.0-(1.0/scale.y))/2.0
80 | );
81 |
82 | return texture2D(u_waterMap,
83 | (scaledTexCoord+offset)+(pixel()*vec2(x,y))+parallax(u_parallaxFg)
84 | );
85 | }
86 |
87 | void main() {
88 | vec4 bg=texture2D(u_textureBg,scaledTexCoord()+parallax(u_parallaxBg));
89 |
90 | vec4 cur = fgColor(0.0,0.0);
91 |
92 | float d=cur.b; // "thickness"
93 | float x=cur.g;
94 | float y=cur.r;
95 |
96 | float a=clamp(cur.a*u_alphaMultiply-u_alphaSubtract, 0.0,1.0);
97 |
98 | vec2 refraction = (vec2(x,y)-0.5)*2.0;
99 | vec2 refractionParallax=parallax(u_parallaxBg-u_parallaxFg);
100 | vec2 refractionPos = scaledTexCoord()
101 | + (pixel()*refraction*(u_minRefraction+(d*u_refractionDelta)))
102 | + refractionParallax;
103 |
104 | vec4 tex=texture2D(u_textureFg,refractionPos);
105 |
106 | if(u_renderShine){
107 | float maxShine=490.0;
108 | float minShine=maxShine*0.18;
109 | vec2 shinePos=vec2(0.5,0.5) + ((1.0/512.0)*refraction)* -(minShine+((maxShine-minShine)*d));
110 | vec4 shine=texture2D(u_textureShine,shinePos);
111 | tex=blend(tex,shine);
112 | }
113 |
114 | vec4 fg=vec4(tex.rgb*u_brightness,a);
115 |
116 | if(u_renderShadow){
117 | float borderAlpha = fgColor(0.,0.-(d*6.0)).a;
118 | borderAlpha=borderAlpha*u_alphaMultiply-(u_alphaSubtract+0.5);
119 | borderAlpha=clamp(borderAlpha,0.,1.);
120 | borderAlpha*=0.2;
121 | vec4 border=vec4(0.,0.,0.,borderAlpha);
122 | fg=blend(border,fg);
123 | }
124 |
125 | gl_FragColor = blend(bg,fg);
126 | }
127 |
--------------------------------------------------------------------------------
/src/app/rain/webgl.jsx:
--------------------------------------------------------------------------------
1 | export function getContext(canvas, options={}) {
2 | let contexts = ["webgl", "experimental-webgl"];
3 | let context = null;
4 |
5 | contexts.some(name=>{
6 | try{
7 | context = canvas.getContext(name,options);
8 | }catch(e){};
9 | return context!=null;
10 | });
11 |
12 | if(context==null){
13 | document.body.classList.add("no-webgl");
14 | }
15 |
16 | return context;
17 | }
18 |
19 | export function createProgram(gl,vertexScript,fragScript){
20 | let vertexShader = createShader(gl, vertexScript, gl.VERTEX_SHADER);
21 | let fragShader = createShader(gl, fragScript, gl.FRAGMENT_SHADER);
22 |
23 | let program = gl.createProgram();
24 | gl.attachShader(program, vertexShader);
25 | gl.attachShader(program, fragShader);
26 |
27 | gl.linkProgram(program);
28 |
29 | let linked = gl.getProgramParameter(program, gl.LINK_STATUS);
30 | if (!linked) {
31 | var lastError = gl.getProgramInfoLog(program);
32 | error("Error in program linking: " + lastError);
33 | gl.deleteProgram(program);
34 | return null;
35 | }
36 |
37 | var positionLocation = gl.getAttribLocation(program, "a_position");
38 | var texCoordLocation = gl.getAttribLocation(program, "a_texCoord");
39 |
40 | var texCoordBuffer = gl.createBuffer();
41 | gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
42 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
43 | -1.0, -1.0,
44 | 1.0, -1.0,
45 | -1.0, 1.0,
46 | -1.0, 1.0,
47 | 1.0, -1.0,
48 | 1.0, 1.0
49 | ]), gl.STATIC_DRAW);
50 | gl.enableVertexAttribArray(texCoordLocation);
51 | gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);
52 |
53 | // Create a buffer for the position of the rectangle corners.
54 | var buffer = gl.createBuffer();
55 | gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
56 | gl.enableVertexAttribArray(positionLocation);
57 | gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
58 |
59 | return program;
60 | }
61 |
62 | export function createShader(gl,script,type){
63 | let shader = gl.createShader(type);
64 | gl.shaderSource(shader,script);
65 | gl.compileShader(shader);
66 |
67 | let compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
68 |
69 | if (!compiled) {
70 | let lastError = gl.getShaderInfoLog(shader);
71 | error("Error compiling shader '" + shader + "':" + lastError);
72 | gl.deleteShader(shader);
73 | return null;
74 | }
75 | return shader;
76 | }
77 | export function createTexture(gl,source,i){
78 | var texture = gl.createTexture();
79 | activeTexture(gl,i);
80 | gl.bindTexture(gl.TEXTURE_2D, texture);
81 |
82 | // Set the parameters so we can render any size image.
83 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
84 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
85 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
86 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
87 |
88 | if ( source == null ) {
89 | return texture;
90 | } else {
91 | updateTexture(gl,source);
92 | }
93 |
94 | return texture;
95 | }
96 | export function createUniform(gl,program,type,name,...args){
97 | let location=gl.getUniformLocation(program,"u_"+name);
98 | gl["uniform"+type](location,...args);
99 | }
100 | export function activeTexture(gl,i){
101 | gl.activeTexture(gl["TEXTURE"+i]);
102 | }
103 | export function updateTexture(gl, source) {
104 | const isCanvas = typeof HTMLCanvasElement !== "undefined" && source instanceof HTMLCanvasElement;
105 | const isImage = typeof HTMLImageElement !== "undefined" && source instanceof HTMLImageElement;
106 | const isVideo = typeof HTMLVideoElement !== "undefined" && source instanceof HTMLVideoElement;
107 | if (!source || (!isCanvas && !isImage && !isVideo)) {
108 | console.warn("updateTexture: source is not a valid canvas/image/video", source, new Error().stack);
109 | return;
110 | }
111 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
112 | }
113 | export function setRectangle(gl, x, y, width, height) {
114 | var x1 = x;
115 | var x2 = x + width;
116 | var y1 = y;
117 | var y2 = y + height;
118 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
119 | x1, y1,
120 | x2, y1,
121 | x1, y2,
122 | x1, y2,
123 | x2, y1,
124 | x2, y2]), gl.STATIC_DRAW);
125 | }
126 |
127 | function error(msg){
128 | console.error(msg);
129 | }
--------------------------------------------------------------------------------
/src/app/rain/RainEffect.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 |
3 | import RainRenderer from "./rain-renderer";
4 | import TweenLite from 'gsap';
5 | import times from './times';
6 | import {random,chance} from './random';
7 | import Raindrops from "./raindrops";
8 | import loadImages from "./image-loader";
9 | import createCanvas from "./create-canvas";
10 | import { weatherData } from './rain-utils';
11 |
12 | import DropColor from './img/drop-color.png';
13 | import DropAlpha from './img/drop-alpha.png';
14 |
15 |
16 | const RainEffect = (props) => {
17 | const {type, backgroundImageUrl } = props;
18 |
19 | let canvas, dropAlpha, dropColor, raindrops, textureFg, textureFgCtx, textureBg, textureBgCtx, renderer, curWeatherData;
20 | let backgroundImage = null;
21 | let intervalId = undefined;
22 | let blend = {v:0};
23 |
24 | let textureFgSize = {
25 | width: 100,
26 | height: 100
27 | }
28 | let textureBgSize = {
29 | width: 100,
30 | height: 100
31 | }
32 |
33 |
34 | useEffect(() => {
35 | setBackgroundImage(backgroundImageUrl);
36 | },[backgroundImageUrl, type]);
37 |
38 | // Set the background image and initialize rain effect after image loads
39 | const setBackgroundImage = (backgroundImageUrl) => {
40 | if (typeof window === 'undefined') return; // SSR safety
41 | backgroundImage = new window.Image();
42 | backgroundImage.crossOrigin = 'anonymous';
43 | backgroundImage.src = backgroundImageUrl;
44 |
45 | backgroundImage.onload = () => {
46 | textureFgSize = {
47 | width: backgroundImage.naturalWidth,
48 | height: backgroundImage.naturalHeight
49 | }
50 | textureBgSize = {
51 | width: backgroundImage.naturalWidth,
52 | height: backgroundImage.naturalHeight
53 | }
54 | }
55 |
56 |
57 |
58 | loadTextures().then(() => {
59 | init(type);
60 | });
61 | }
62 |
63 | // Load drop textures
64 | const loadTextures = () => {
65 | return loadImages([
66 | { name:"dropAlpha", src: DropAlpha },
67 | { name:"dropColor", src: DropColor },
68 | ]).then(function (images){
69 | dropColor = images.dropColor.img;
70 | dropAlpha = images.dropAlpha.img;
71 | });
72 | }
73 |
74 | const init = (type = 'rain') => {
75 | canvas = document.querySelector('#container-weather');
76 | const container = canvas.parentElement; // or a specific container element
77 | const dpi = window.devicePixelRatio || 1;
78 |
79 | const rect = container.getBoundingClientRect();
80 |
81 | canvas.width = rect.width * dpi;
82 | canvas.height = rect.height * dpi;
83 |
84 | canvas.style.width = `${rect.width}px`;
85 | canvas.style.height = `${rect.height}px`;
86 |
87 |
88 | raindrops=new Raindrops(
89 | canvas.width,
90 | canvas.height,
91 | dpi,
92 | dropAlpha,
93 | dropColor,{
94 | trailRate:1,
95 | trailScaleRange:[0.2,0.45],
96 | collisionRadius : 0.45,
97 | dropletsCleaningRadiusMultiplier : 0.28,
98 | }
99 | );
100 |
101 |
102 | textureFg = createCanvas(textureFgSize.width,textureFgSize.height);
103 | textureFgCtx = textureFg.getContext('2d');
104 | textureBg = createCanvas(textureBgSize.width,textureBgSize.height);
105 |
106 | textureBgCtx = textureBg.getContext('2d');
107 |
108 |
109 | generateTextures(backgroundImage, backgroundImage);
110 | renderer = new RainRenderer(canvas, raindrops.canvas, textureFg, textureBg, null,{
111 | brightness:1.04,
112 | alphaMultiply:16,
113 | alphaSubtract:4,
114 | minRefraction: 128
115 | // minRefraction:256,
116 | // maxRefraction:512
117 | });
118 |
119 | curWeatherData = { ...weatherData[type], fg: backgroundImage, bg: backgroundImage };
120 | if (type === 'storm' || type === 'fallout') {
121 | setupLightningFlicker();
122 | }
123 | if (raindrops && curWeatherData) {
124 | raindrops.options = Object.assign(raindrops.options, curWeatherData);
125 | raindrops.clearDrops();
126 | }
127 | }
128 |
129 | // Generate foreground/background textures for the rain renderer
130 | const generateTextures = (fg, bg, x=0, y=0, alpha=1) => {
131 | if (!fg || !bg || (fg instanceof HTMLImageElement && !fg.complete) || (bg instanceof HTMLImageElement && !bg.complete)) { return; }
132 | textureFgCtx.globalAlpha = alpha;
133 | textureFgCtx.drawImage(fg, x, y, textureFgSize.width, textureFgSize.height);
134 | textureBgCtx.globalAlpha = alpha;
135 | textureBgCtx.drawImage(bg, x, y, textureBgSize.width, textureBgSize.height);
136 | }
137 |
138 | // Lightning flicker effect for storm weather
139 | const setupLightningFlicker = () => {
140 | const minInterval = 1000; // minimum time between flickers
141 | const maxInterval = 5000; // maximum time between flickers
142 | const flashChance = curWeatherData && typeof curWeatherData.flashChance === 'number' ? curWeatherData.flashChance : 0;
143 | const interval = minInterval + (maxInterval - minInterval) * (1 - flashChance);
144 |
145 | intervalId = setInterval(() => {
146 | const flicker = Math.random() * 2.0;
147 | if (renderer && renderer.gl) {
148 | renderer.gl.useProgram(renderer.programWater);
149 | renderer.gl.createUniform("1f", "lightningFlash", flicker);
150 | setTimeout(() => {
151 | renderer.gl.useProgram(renderer.programWater);
152 | renderer.gl.createUniform("1f", "lightningFlash", 0.0);
153 | }, 100 + Math.random() * 200); // Flicker lasts 100-300ms
154 | }
155 | }, interval);
156 | }
157 |
158 |
159 | return (
160 |
161 |
162 |
163 | )
164 |
165 | }
166 |
167 | export default RainEffect;
--------------------------------------------------------------------------------
/src/app/fog/FogEffect.jsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useRef, useMemo } from 'react';
4 | import { Canvas, useFrame, useLoader } from '@react-three/fiber';
5 | import * as THREE from 'three';
6 | import fogElementImg from './fog-element.png';
7 | import denseFogElementImg from './dense-fog-element.png';
8 | import gsap from 'gsap';
9 |
10 | // Settings for light and dense fog
11 | const LIGHT_FOG_SETTINGS = {
12 | count: 18,
13 | fogElementRatio: 0.8, // 80% fog-element, 20% dense-fog-element
14 | alphaMin: 0.18,
15 | alphaMax: 0.32,
16 | scaleMin: 0.7,
17 | scaleMax: 1.7,
18 | moveSpeed: 0.02,
19 | };
20 | const DENSE_FOG_SETTINGS = {
21 | count: 38,
22 | fogElementRatio: 0.35, // 35% fog-element, 65% dense-fog-element
23 | alphaMin: 0.10,
24 | alphaMax: 0.22,
25 | scaleMin: 1.0,
26 | scaleMax: 2.2,
27 | moveSpeed: 0.04,
28 | };
29 |
30 | function FogSprite({ texture, initial, moveSpeed, windOffset }) {
31 | const mesh = useRef();
32 | const visibleRef = useRef(false); // Track if currently visible
33 | useFrame((state) => {
34 | if (mesh.current) {
35 | const t = state.clock.getElapsedTime();
36 | // Add wind offset to base position
37 | let x = initial.x + (windOffset?.current?.x || 0) + Math.sin(t * initial.driftSpeedX + initial.driftPhaseX) * initial.driftAmountX;
38 | let y = initial.y + (windOffset?.current?.y || 0) + Math.cos(t * initial.driftSpeedY + initial.driftPhaseY) * initial.driftAmountY;
39 | // Loop position in [-1.1, 1.1] for seamless looping
40 | if (x > 1.1) x -= 2.2;
41 | if (x < -1.1) x += 2.2;
42 | if (y > 1.1) y -= 2.2;
43 | if (y < -1.1) y += 2.2;
44 | mesh.current.position.x = x;
45 | mesh.current.position.y = y;
46 | // Determine if inside visible area
47 | const isVisible = x > -1 && x < 1 && y > -1 && y < 1;
48 | // Animate opacity with GSAP only on enter/exit
49 | if (mesh.current.material) {
50 | if (isVisible && !visibleRef.current) {
51 | // Entering: fade in
52 | gsap.to(mesh.current.material, { opacity: initial.alpha, duration: .5, overwrite: true });
53 | visibleRef.current = true;
54 | } else if (!isVisible && visibleRef.current) {
55 | // Exiting: fade out
56 | gsap.to(mesh.current.material, { opacity: 0, duration: .5, overwrite: true });
57 | visibleRef.current = false;
58 | }
59 | }
60 | }
61 | });
62 | return (
63 |
64 |
65 |
71 |
72 | );
73 | }
74 |
75 | // Add a helper component for global wind animation
76 | function FogWindController({ windAngle, windOffset }) {
77 | useFrame(() => {
78 | // Occasionally nudge wind angle
79 | if (Math.random() > 0.995) {
80 | windAngle.current += (Math.random() - 0.5) * 0.2; // Small nudge
81 | // Clamp angle to [-PI, PI]
82 | if (windAngle.current > Math.PI) windAngle.current -= 2 * Math.PI;
83 | if (windAngle.current < -Math.PI) windAngle.current += 2 * Math.PI;
84 | }
85 | // Much slower wind movement
86 | const windSpeed = 0.0007; // Much slower
87 | windOffset.current.x += Math.cos(windAngle.current) * windSpeed;
88 | windOffset.current.y += Math.sin(windAngle.current) * windSpeed;
89 | // Loop wind offset in [-1, 1] for seamless looping
90 | if (windOffset.current.x > 1) windOffset.current.x -= 2;
91 | if (windOffset.current.x < -1) windOffset.current.x += 2;
92 | if (windOffset.current.y > 1) windOffset.current.y -= 2;
93 | if (windOffset.current.y < -1) windOffset.current.y += 2;
94 | });
95 | return null;
96 | }
97 |
98 | export default function FogEffect({ backgroundImageUrl, type = 'light' }) {
99 | // Load both fog textures
100 | const [fogElement, denseFogElement] = useLoader(THREE.TextureLoader, [
101 | fogElementImg.src,
102 | denseFogElementImg.src,
103 | ]);
104 | const settings = type === 'dense' ? DENSE_FOG_SETTINGS : LIGHT_FOG_SETTINGS;
105 |
106 | // Wind angle (in radians) and wind offset
107 | const windAngle = React.useRef(0); // 0 = right
108 | const windOffset = React.useRef({ x: 0, y: 0 });
109 |
110 | // Precompute sprite initial states
111 | const sprites = useMemo(() => {
112 | return Array.from({ length: settings.count }).map(() => {
113 | // Randomly choose which texture to use
114 | const useFogElement = Math.random() < settings.fogElementRatio;
115 | const texture = useFogElement ? fogElement : denseFogElement;
116 | // Random position in [-1, 1] (covering the screen)
117 | const x = (Math.random() - 0.5) * 2.2;
118 | const y = (Math.random() - 0.5) * 2.2;
119 | // Random scale
120 | const scale = settings.scaleMin + Math.random() * (settings.scaleMax - settings.scaleMin);
121 | // Random alpha
122 | const alpha = settings.alphaMin + Math.random() * (settings.alphaMax - settings.alphaMin);
123 | // Random drift parameters
124 | const driftAmountX = 0.08 + Math.random() * 0.18;
125 | const driftAmountY = 0.08 + Math.random() * 0.18;
126 | const driftSpeedX = settings.moveSpeed * (0.7 + Math.random() * 0.6);
127 | const driftSpeedY = settings.moveSpeed * (0.7 + Math.random() * 0.6);
128 | const driftPhaseX = Math.random() * Math.PI * 2;
129 | const driftPhaseY = Math.random() * Math.PI * 2;
130 | return {
131 | texture,
132 | x,
133 | y,
134 | scale,
135 | alpha,
136 | driftAmountX,
137 | driftAmountY,
138 | driftSpeedX,
139 | driftSpeedY,
140 | driftPhaseX,
141 | driftPhaseY,
142 | };
143 | });
144 | }, [fogElement, denseFogElement, settings]);
145 |
146 | return (
147 |
157 |
172 |
173 | );
174 | }
--------------------------------------------------------------------------------
/src/app/rain/rain-renderer.jsx:
--------------------------------------------------------------------------------
1 | // import * as WebGL from "./webgl.jsx";
2 | import GL from "./gl-obj.jsx";
3 | // import loadImages from "./image-loader.jsx";
4 | import createCanvas from "./create-canvas.jsx";
5 | // let requireShaderScript = require("glslify");
6 |
7 | // let vertShader = requireShaderScript('./shaders/simple.vert');
8 | // let fragShader = requireShaderScript('./shaders/water.frag');
9 |
10 | let vertShader =
11 | 'precision mediump float;\n' +
12 | 'attribute vec2 a_position;\n' +
13 | 'void main() {\n' +
14 | ' gl_Position = vec4(a_position,0.0,1.0);\n' +
15 | '}\n' ;
16 |
17 | let fragShader =
18 | ' precision mediump float;\n ' +
19 | ' \n' +
20 | ' // textures \n' +
21 | ' uniform sampler2D u_waterMap;\n ' +
22 | ' uniform sampler2D u_textureShine;\n ' +
23 | ' uniform sampler2D u_textureFg;\n ' +
24 | ' uniform sampler2D u_textureBg;\n ' +
25 | ' ' +
26 | ' // the texCoords passed in from the vertex shader. \n' +
27 | ' varying vec2 v_texCoord;\n ' +
28 | ' uniform vec2 u_resolution;\n ' +
29 | ' uniform vec2 u_parallax;\n ' +
30 | ' uniform float u_parallaxFg;\n ' +
31 | ' uniform float u_parallaxBg;\n ' +
32 | ' uniform float u_textureRatio;\n ' +
33 | ' uniform bool u_renderShine;\n ' +
34 | ' uniform bool u_renderShadow;\n ' +
35 | ' uniform float u_minRefraction;\n ' +
36 | ' uniform float u_refractionDelta;\n ' +
37 | ' uniform float u_brightness;\n ' +
38 | ' uniform float u_alphaMultiply;\n ' +
39 | ' uniform float u_alphaSubtract;\n ' +
40 | ' uniform float u_lightningFlash;\n ' +
41 | ' ' +
42 | ' // alpha-blends two colors \n' +
43 | ' vec4 blend(vec4 bg,vec4 fg){ \n' +
44 | ' vec3 bgm=bg.rgb*bg.a;\n ' +
45 | ' vec3 fgm=fg.rgb*fg.a;\n ' +
46 | ' float ia=1.0-fg.a;\n ' +
47 | ' float a=(fg.a + bg.a * ia);\n ' +
48 | ' vec3 rgb;\n ' +
49 | ' if(a!=0.0){ \n' +
50 | ' rgb=(fgm + bgm * ia) / a;\n ' +
51 | ' }else{ \n' +
52 | ' rgb=vec3(0.0,0.0,0.0);\n ' +
53 | ' } \n' +
54 | ' return vec4(rgb,a);\n ' +
55 | ' } \n' +
56 | ' \n' +
57 | ' vec2 pixel(){ \n' +
58 | ' return vec2(1.0,1.0)/u_resolution;\n ' +
59 | ' } \n' +
60 | ' \n' +
61 | ' vec2 parallax(float v){ \n' +
62 | ' return u_parallax*pixel()*v;\n ' +
63 | ' } \n' +
64 | ' \n' +
65 | ' vec2 texCoord(){ \n' +
66 | ' return vec2(gl_FragCoord.x, u_resolution.y-gl_FragCoord.y)/u_resolution;\n ' +
67 | ' } \n' +
68 | ' \n' +
69 | ' // scales the bg up and proportionally to fill the container \n' +
70 | ' vec2 scaledTexCoord(){ \n' +
71 | ' float ratio=u_resolution.x/u_resolution.y;\n ' +
72 | ' vec2 scale=vec2(1.0,1.0);\n ' +
73 | ' vec2 offset=vec2(0.0,0.0);\n ' +
74 | ' float ratioDelta=ratio-u_textureRatio;\n ' +
75 | ' if(ratioDelta>=0.0){ \n' +
76 | ' scale.y=(1.0+ratioDelta);\n ' +
77 | ' offset.y=ratioDelta/2.0;\n ' +
78 | ' }else{ \n' +
79 | ' scale.x=(1.0-ratioDelta);\n ' +
80 | ' offset.x=-ratioDelta/2.0;\n ' +
81 | ' } \n' +
82 | ' return (texCoord()+offset)/scale;\n ' +
83 | ' } \n' +
84 | ' \n' +
85 | ' // get color from fg \n' +
86 | ' vec4 fgColor(float x, float y){ \n' +
87 | ' float p2=u_parallaxFg*2.0;\n ' +
88 | ' vec2 scale=vec2( \n' +
89 | ' (u_resolution.x+p2)/u_resolution.x, \n' +
90 | ' (u_resolution.y+p2)/u_resolution.y \n' +
91 | ' );\n ' +
92 | ' \n' +
93 | ' vec2 scaledTexCoord=texCoord()/scale;\n ' +
94 | ' vec2 offset=vec2( \n' +
95 | ' (1.0-(1.0/scale.x))/2.0, \n' +
96 | ' (1.0-(1.0/scale.y))/2.0 \n' +
97 | ' );\n ' +
98 | ' \n' +
99 | ' return texture2D(u_waterMap, \n' +
100 | ' (scaledTexCoord+offset)+(pixel()*vec2(x,y))+parallax(u_parallaxFg) \n' +
101 | ' );\n ' +
102 | ' } \n' +
103 | ' \n' +
104 | ' void main() { \n' +
105 | ' vec4 bg=texture2D(u_textureBg,scaledTexCoord()+parallax(u_parallaxBg));\n ' +
106 | ' bg.rgb *= (1.0 + 0.5 * u_lightningFlash);\n ' +
107 | ' \n' +
108 | ' vec4 cur = fgColor(0.0,0.0);\n ' +
109 | ' ' +
110 | ' float d=cur.b;\n // "thickness" \n' +
111 | ' float x=cur.g;\n ' +
112 | ' float y=cur.r;\n ' +
113 | ' \n' +
114 | ' float a=clamp(cur.a*u_alphaMultiply-u_alphaSubtract, 0.0,1.0);\n ' +
115 | ' \n' +
116 | ' vec2 refraction = (vec2(x,y)-0.5)*2.0;\n ' +
117 | ' vec2 refractionParallax=parallax(u_parallaxBg-u_parallaxFg);\n ' +
118 | ' vec2 refractionPos = scaledTexCoord() \n' +
119 | ' + (pixel()*refraction*(u_minRefraction+(d*u_refractionDelta))) \n' +
120 | ' + refractionParallax;\n ' +
121 | ' \n' +
122 | ' vec4 tex=texture2D(u_textureFg,refractionPos);\n ' +
123 | ' \n' +
124 | ' if(u_renderShine){ \n' +
125 | ' float maxShine=490.0;\n ' +
126 | ' float minShine=maxShine*0.18;\n ' +
127 | ' vec2 shinePos=vec2(0.5,0.5) + ((1.0/512.0)*refraction)* -(minShine+((maxShine-minShine)*d));\n ' +
128 | ' vec4 shine=texture2D(u_textureShine,shinePos);\n ' +
129 | ' tex=blend(tex,shine);\n ' +
130 | ' } \n' +
131 | ' \n' +
132 | ' vec4 fg=vec4(tex.rgb*u_brightness*a*(1.0 + 0.5 * u_lightningFlash),a);\n ' +
133 | ' \n' +
134 | ' if(u_renderShadow){ \n' +
135 | ' float borderAlpha = fgColor(0.,0.-(d*6.0)).a;\n ' +
136 | ' borderAlpha=borderAlpha*u_alphaMultiply-(u_alphaSubtract+0.5);\n ' +
137 | ' borderAlpha=clamp(borderAlpha,0.,1.);\n ' +
138 | ' borderAlpha*=0.2;\n ' +
139 | ' vec4 border=vec4(0.,0.,0.,borderAlpha);\n ' +
140 | ' fg=blend(border,fg);\n ' +
141 | ' } \n' +
142 | ' \n' +
143 | ' gl_FragColor = blend(bg,fg);\n ' +
144 | ' } \n';
145 |
146 | const defaultOptions={
147 | renderShadow:false,
148 | minRefraction:256,
149 | maxRefraction:512,
150 | brightness:1,
151 | alphaMultiply:20,
152 | alphaSubtract:5,
153 | parallaxBg:5,
154 | parallaxFg:20
155 | }
156 | function RainRenderer(canvas,canvasLiquid, imageFg, imageBg, imageShine=null,options={}){
157 | this.canvas=canvas;
158 | this.canvasLiquid=canvasLiquid;
159 | this.imageShine=imageShine;
160 | this.imageFg=imageFg;
161 | this.imageBg=imageBg;
162 | this.options=Object.assign({},defaultOptions, options);
163 | this.init();
164 | }
165 |
166 | RainRenderer.prototype={
167 | canvas:null,
168 | gl:null,
169 | canvasLiquid:null,
170 | width:0,
171 | height:0,
172 | imageShine:"",
173 | imageFg:"",
174 | imageBg:"",
175 | textures:null,
176 | programWater:null,
177 | programBlurX:null,
178 | programBlurY:null,
179 | parallaxX:0,
180 | parallaxY:0,
181 | renderShadow:false,
182 | options:null,
183 | init(){
184 | this.width=this.canvas.width;
185 | this.height=this.canvas.height;
186 | this.gl=new GL(this.canvas, {alpha:false},vertShader,fragShader);
187 | let gl=this.gl;
188 | this.programWater=gl.program;
189 |
190 | gl.createUniform("2f","resolution",this.width,this.height);
191 | gl.createUniform("1f","textureRatio",this.imageBg.width/this.imageBg.height);
192 | gl.createUniform("1i","renderShine",this.imageShine==null?false:true);
193 | gl.createUniform("1i","renderShadow",this.options.renderShadow);
194 | gl.createUniform("1f","minRefraction",this.options.minRefraction);
195 | gl.createUniform("1f","refractionDelta",this.options.maxRefraction-this.options.minRefraction);
196 | gl.createUniform("1f","brightness",this.options.brightness);
197 | gl.createUniform("1f","alphaMultiply",this.options.alphaMultiply);
198 | gl.createUniform("1f","alphaSubtract",this.options.alphaSubtract);
199 | gl.createUniform("1f","parallaxBg",this.options.parallaxBg);
200 | gl.createUniform("1f","parallaxFg",this.options.parallaxFg);
201 | gl.createUniform("1f","lightningFlash",0.0);
202 |
203 |
204 | gl.createTexture(null,0);
205 |
206 | this.textures=[
207 | {name:'textureShine', img:this.imageShine==null?createCanvas(2,2):this.imageShine},
208 | {name:'textureFg', img:this.imageFg},
209 | {name:'textureBg', img:this.imageBg}
210 | ];
211 |
212 | this.textures.forEach((texture,i)=>{
213 | gl.createTexture(texture.img,i+1);
214 | gl.createUniform("1i",texture.name,i+1);
215 | });
216 |
217 | this.draw();
218 | },
219 | draw(){
220 | this.gl.useProgram(this.programWater);
221 | this.gl.createUniform("2f", "parallax", this.parallaxX,this.parallaxY);
222 | this.updateTexture();
223 | this.gl.draw();
224 |
225 | requestAnimationFrame(this.draw.bind(this));
226 | },
227 | updateTextures(){
228 | this.textures.forEach((texture,i)=>{
229 | this.gl.activeTexture(i+1);
230 | this.gl.updateTexture(texture.img);
231 | })
232 | },
233 | updateTexture(){
234 | this.gl.activeTexture(0);
235 | this.gl.updateTexture(this.canvasLiquid);
236 | },
237 | resize(){
238 |
239 | },
240 | // get overlayTexture(){
241 |
242 | // },
243 | // set overlayTexture(v){
244 |
245 | // }
246 | }
247 |
248 | export default RainRenderer;
--------------------------------------------------------------------------------
/src/app/rain/weather-utils.jsx:
--------------------------------------------------------------------------------
1 | import TweenLite from 'gsap';
2 | import RainRenderer from "./rain-renderer";
3 | import Raindrops from "./raindrops";
4 | import loadImages from "./image-loader";
5 | import createCanvas from "./create-canvas";
6 | import times from './times';
7 | import {random,chance} from './random';
8 |
9 | import DropColor from './img/drop-color.png';
10 | import DropAlpha from './img/drop-alpha.png';
11 |
12 | let textureStormLightningFg, textureStormLightningBg, dropColor, dropAlpha;
13 |
14 | let textureFg,
15 | textureFgCtx,
16 | textureBg,
17 | textureBgCtx;
18 |
19 | let textureBgSize = {
20 | width: window.innerWidth,
21 | height: window.innerHeight
22 | }
23 | let textureFgSize = {
24 | width:96,
25 | height:64
26 | }
27 |
28 | // const blankFg = createCanvas(textureFgSize.width, textureFgSize.height);
29 |
30 | let raindrops,
31 | renderer,
32 | canvas;
33 |
34 | let weatherData = null;
35 | let curWeatherData = null;
36 | let blend = {v:0};
37 | let intervalId = undefined;
38 |
39 | let backgroundImage = null;
40 | let lastWeatherType = 'rain'; // Track the last used weather type
41 | let resizeTimeout = null;
42 |
43 | // Set the background image and initialize rain effect after image loads
44 | export function setBackgroundImage(url, type = 'rain') {
45 | if (typeof window === 'undefined') return; // SSR safety
46 | backgroundImage = new window.Image();
47 | backgroundImage.crossOrigin = 'anonymous';
48 |
49 | console.log('textureBgSize', textureBgSize);
50 | backgroundImage.onload = () => {
51 | // Calculate scaled dimensions while preserving aspect ratio
52 | console.log('backgroundImage natural w h', backgroundImage.naturalWidth, backgroundImage.naturalHeight);
53 | const imageRatio = backgroundImage.naturalWidth / backgroundImage.naturalHeight;
54 | const containerRatio = textureBgSize.width / textureBgSize.height;
55 | console.log('imageRatio', imageRatio);
56 | console.log('containerRatio', containerRatio);
57 | if (imageRatio > containerRatio) {
58 | // Image is wider than target - scale by width
59 | backgroundImage.height = textureBgSize.height;
60 | backgroundImage.width = textureBgSize.height * imageRatio;
61 | } else {
62 | // Image is taller than target - scale by height
63 | backgroundImage.width = textureBgSize.width;
64 | backgroundImage.height = textureBgSize.width / imageRatio;
65 | }
66 | console.log('backgroundImage w h', backgroundImage.width, backgroundImage.height);
67 | loadTextures().then(() => {
68 | init(type, backgroundImage);
69 | });
70 | };
71 | backgroundImage.src = url;
72 | console.log('backgroundImage at onload', backgroundImage);
73 | }
74 |
75 | // Load drop textures
76 | export function loadTextures() {
77 |
78 | return loadImages([
79 | { name:"dropAlpha", src: DropAlpha },
80 | { name:"dropColor", src: DropColor },
81 | ]).then(function (images){
82 | dropColor = images.dropColor.img;
83 | dropAlpha = images.dropAlpha.img;
84 | });
85 | }
86 |
87 | // Set up weather data and current weather
88 | function setupWeather(type) {
89 |
90 | setupWeatherData();
91 | curWeatherData = weatherData[type];
92 |
93 | if (raindrops && curWeatherData) {
94 | raindrops.options = Object.assign(raindrops.options, curWeatherData);
95 | raindrops.clearDrops();
96 | }
97 | }
98 |
99 | // Handle window resize for responsive canvas
100 | function handleResize() {
101 | // resizeCanvas();
102 | if (typeof window === 'undefined') return;
103 | if (backgroundImage && backgroundImage.complete) {
104 | setBackgroundImage(backgroundImage.src, lastWeatherType);
105 | }
106 | }
107 |
108 | // Make canvas match its CSS size
109 | // function resizeCanvas() {
110 | // canvas.width = canvas.clientWidth;
111 | // canvas.height = canvas.clientHeight;
112 | // }
113 |
114 | // Enable responsive canvas resizing
115 | export function enableResponsiveCanvas() {
116 | if (typeof window === 'undefined') return;
117 | window.addEventListener('resize', handleResize);
118 | }
119 |
120 | // Initialize rain effect and renderer
121 | function init(type = 'rain', backgroundImage) {
122 | if (typeof window === 'undefined' || typeof document === 'undefined') return;
123 | lastWeatherType = type; // Save the last used weather type
124 | canvas = document.querySelector('#container-weather');
125 | // var dpi = window.devicePixelRatio;
126 | // canvas.width = window.innerWidth * dpi;
127 | // canvas.height = window.innerHeight * dpi;
128 | // canvas.style.width = window.innerWidth + "px";
129 | // canvas.style.height = window.innerHeight + "px";
130 | const container = canvas.parentElement; // or a specific container element
131 | console.log('container', container);
132 | const dpi = window.devicePixelRatio || 1;
133 |
134 | const rect = container.getBoundingClientRect();
135 | console.log('rect', rect);
136 | canvas.width = rect.width * dpi;
137 | canvas.height = rect.height * dpi;
138 |
139 | canvas.style.width = `${rect.width}px`;
140 | canvas.style.height = `${rect.height}px`;
141 |
142 | // // Center the image in the canvas
143 | // const x = (canvas.width - backgroundImage.width) / 2;
144 | // const y = (canvas.height - backgroundImage.height) / 2;
145 |
146 |
147 | console.log('window.innerWidth', window.innerWidth);
148 | console.log('dpi', dpi);
149 | console.log('canvas.clientWidth', canvas.clientWidth);
150 | console.log('canvas.clientHeight', canvas.clientHeight);
151 | console.log('canvas.width', canvas.width);
152 | console.log('canvas.height', canvas.height);
153 | console.log('canvas.style.width', canvas.style.width);
154 | console.log('canvas.style.height', canvas.style.height);
155 |
156 | textureBgSize = { width: rect.width * dpi, height: rect.height * dpi };
157 |
158 | raindrops=new Raindrops(
159 | canvas.width,
160 | canvas.height,
161 | dpi,
162 | dropAlpha,
163 | dropColor,{
164 | trailRate:1,
165 | trailScaleRange:[0.2,0.45],
166 | collisionRadius : 0.45,
167 | dropletsCleaningRadiusMultiplier : 0.28,
168 | }
169 | );
170 |
171 | textureFg = createCanvas(textureFgSize.width,textureFgSize.height);
172 | textureFgCtx = textureFg.getContext('2d');
173 | textureBg = createCanvas(textureBgSize.width,textureBgSize.height);
174 | textureBgCtx = textureBg.getContext('2d');
175 |
176 |
177 | generateTextures(backgroundImage, backgroundImage);
178 | renderer = new RainRenderer(canvas, raindrops.canvas, textureFg, textureBg, null,{
179 | brightness:1.04,
180 | alphaMultiply:6,
181 | alphaSubtract:3,
182 | minRefraction: 128
183 | // minRefraction:256,
184 | // maxRefraction:512
185 | });
186 |
187 | setupWeather(type);
188 | if (curWeatherData && curWeatherData.flashChance) {
189 | setupFlash();
190 | }
191 | }
192 |
193 | // Set up lightning flash effect for storm weather
194 | function setupFlash() {
195 | intervalId = setInterval(()=>{
196 | if(chance(curWeatherData.flashChance)){
197 | flash(curWeatherData.bg,curWeatherData.fg,curWeatherData.flashBg,curWeatherData.flashFg);
198 | }
199 | },500);
200 | }
201 |
202 | // Set up weather data for different types
203 | function setupWeatherData() {
204 | var defaultWeather = {
205 | minR: 10,
206 | maxR: 40,
207 | rainChance: 0.35,
208 | rainLimit: 6,
209 | drizzle: 50,
210 | drizzleSize: [2, 4.5],
211 | raining: true,
212 | trailRate: 1,
213 | trailScaleRange: [0.2, 0.35],
214 | fg: backgroundImage,
215 | bg: backgroundImage,
216 | flashFg: null,
217 | flashBg: null,
218 | flashChance: 0
219 | };
220 |
221 | function weather(data) {
222 | return Object.assign({}, defaultWeather, data);
223 | };
224 |
225 | weatherData = {
226 | rain: weather({
227 | rainChance: 0.35,
228 | rainLimit: 6,
229 | drizzle: 50,
230 | raining: true,
231 | // trailRate:2.5,
232 | fg: backgroundImage,
233 | bg: backgroundImage
234 | }),
235 | storm: weather({
236 | minR: 20,
237 | maxR: 45,
238 | rainChance: 0.55,
239 | rainLimit: 6,
240 | drizzle: 80,
241 | drizzleSize: [2, 6],
242 | trailRate: 1,
243 | trailScaleRange: [0.15, 0.3],
244 | fg: backgroundImage,
245 | bg: backgroundImage,
246 | flashFg: textureStormLightningFg,
247 | flashBg: textureStormLightningBg,
248 | flashChance: 0.1
249 | }),
250 | fallout: weather({
251 | rainChance: 0.35,
252 | rainLimit: 6,
253 | drizzle: 20,
254 | trailRate: 4,
255 | fg: backgroundImage,
256 | bg: backgroundImage
257 | }),
258 | drizzle: weather({
259 | rainChance: 0.15,
260 | rainLimit: 2,
261 | drizzle: 10,
262 | fg: backgroundImage,
263 | bg: backgroundImage
264 | }),
265 | sunny: weather({
266 | rainChance: 0,
267 | rainLimit: 0,
268 | drizzle: 0,
269 | raining: false,
270 | fg: backgroundImage,
271 | bg: backgroundImage
272 | })
273 | };
274 | }
275 |
276 | // Lightning flash animation
277 | function flash(baseBg, baseFg, flashBg, flashFg) {
278 | let flashValue={v:0};
279 | function transitionFlash(to,t=0.025){
280 | return new Promise((resolve,reject)=>{
281 | TweenLite.to(flashValue,t,{
282 | v:to,
283 | // ease:Quint.easeOut,
284 | onUpdate:()=>{
285 | generateTextures(baseFg,baseBg);
286 | generateTextures(flashFg,flashBg,flashValue.v);
287 | renderer.updateTextures();
288 | },
289 | onComplete:()=>{
290 | resolve();
291 | }
292 | });
293 | });
294 | }
295 |
296 | let lastFlash=transitionFlash(1);
297 | times(random(2,7),(i)=>{
298 | lastFlash=lastFlash.then(()=>{
299 | return transitionFlash(random(0.1,1))
300 | })
301 | })
302 | lastFlash=lastFlash.then(()=>{
303 | return transitionFlash(1,0.1);
304 | }).then(()=>{
305 | transitionFlash(0,0.25);
306 | });
307 | }
308 |
309 | // Generate foreground/background textures for the rain renderer
310 | function generateTextures(fg, bg, x=0, y=0, alpha=1) {
311 | if (
312 | !fg ||
313 | !bg ||
314 | (fg instanceof HTMLImageElement && !fg.complete) ||
315 | (bg instanceof HTMLImageElement && !bg.complete)
316 | ) {
317 | // Image not ready, skip drawing
318 | return;
319 | }
320 | textureFgCtx.globalAlpha = alpha;
321 | textureFgCtx.drawImage(fg, x, y, textureFgSize.width, textureFgSize.height);
322 | console.log('x and y in generateTextures', x, y);
323 |
324 | textureBgCtx.globalAlpha = alpha;
325 | textureBgCtx.drawImage(bg, x, y, textureBgSize.width, textureBgSize.height);
326 | }
327 |
328 | // Clean up weather effect and clear intervals
329 | export function cleanWeather(){
330 | //TODO enhance the cleanup function
331 | // raindrops.clean();
332 | clearInterval(intervalId);
333 | // textureRainFg = null;
334 | // textureRainBg = null;
335 | // textureStormLightningFg = null;
336 | // textureStormLightningBg = null;
337 | // textureFalloutFg = null;
338 | // textureFalloutBg = null;
339 | // textureSunFg = null;
340 | // textureSunBg = null;
341 | // textureDrizzleFg = null;
342 | // textureDrizzleBg = null;
343 | // dropColor = null;
344 | // dropAlpha = null;
345 | // textureFg = null;
346 | // textureFgCtx = null;
347 | // textureBg = null;
348 | // textureBgCtx = null;
349 | // textureBgSize = null;
350 | // textureFgSize = null;
351 | // raindrops = null;
352 | // renderer = null;
353 | // canvas = null;
354 | // parallax = null;
355 | weatherData = null;
356 | curWeatherData = null;
357 | // blend = null;
358 | intervalId = null;
359 | }
--------------------------------------------------------------------------------
/src/app/rain/raindrops.jsx:
--------------------------------------------------------------------------------
1 | // import loadImages from "./image-loader.jsx";
2 | import times from "./times.jsx";
3 | import createCanvas from "./create-canvas.jsx";
4 | import {random, chance} from "./random.jsx";
5 |
6 | let dropSize=64;
7 | const Drop={
8 | x:0,
9 | y:0,
10 | r:0,
11 | spreadX:0,
12 | spreadY:0,
13 | momentum:0,
14 | momentumX:0,
15 | lastSpawn:0,
16 | nextSpawn:0,
17 | parent:null,
18 | isNew:true,
19 | killed:false,
20 | shrink:0,
21 | }
22 | const defaultOptions={
23 | minR:10,
24 | maxR:40,
25 | maxDrops:900,
26 | rainChance:0.3,
27 | rainLimit:3,
28 | dropletsRate:50,
29 | dropletsSize:[2,4],
30 | dropletsCleaningRadiusMultiplier:0.43,
31 | raining:true,
32 | globalTimeScale:1,
33 | trailRate:1,
34 | autoShrink:true,
35 | spawnArea:[-0.1,0.95],
36 | trailScaleRange:[0.2,0.5],
37 | collisionRadius:0.65,
38 | collisionRadiusIncrease:0.01,
39 | dropFallMultiplier:1,
40 | collisionBoostMultiplier:0.05,
41 | collisionBoost:1,
42 | }
43 |
44 | function Raindrops(width,height,scale,dropAlpha,dropColor,options={}){
45 | this.width=width;
46 | this.height=height;
47 | this.scale=scale;
48 | this.dropAlpha=dropAlpha;
49 | this.dropColor=dropColor;
50 | this.options=Object.assign({},defaultOptions,options);
51 | this.init();
52 | }
53 | Raindrops.prototype={
54 | dropColor:null,
55 | dropAlpha:null,
56 | canvas:null,
57 | ctx:null,
58 | width:0,
59 | height:0,
60 | scale:0,
61 | dropletsPixelDensity:1,
62 | droplets:null,
63 | dropletsCtx:null,
64 | dropletsCounter:0,
65 | drops:null,
66 | dropsGfx:null,
67 | clearDropletsGfx:null,
68 | textureCleaningIterations:0,
69 | lastRender:null,
70 |
71 | options:null,
72 |
73 | init(){
74 | this.timeoutID = undefined;
75 | this.canvas = createCanvas(this.width,this.height);
76 | this.ctx = this.canvas.getContext('2d');
77 |
78 | this.droplets = createCanvas(this.width*this.dropletsPixelDensity,this.height*this.dropletsPixelDensity);
79 | this.dropletsCtx = this.droplets.getContext('2d');
80 |
81 | this.drops=[];
82 | this.dropsGfx=[];
83 |
84 | this.renderDropsGfx();
85 |
86 | this.update();
87 | },
88 | get deltaR(){
89 | return this.options.maxR-this.options.minR;
90 | },
91 | get area(){
92 | return (this.width*this.height)/this.scale;
93 | },
94 | get areaMultiplier(){
95 | return Math.sqrt(this.area/(1024*768));
96 | },
97 | drawDroplet(x,y,r){
98 | this.drawDrop(this.dropletsCtx,Object.assign(Object.create(Drop),{
99 | x:x*this.dropletsPixelDensity,
100 | y:y*this.dropletsPixelDensity,
101 | r:r*this.dropletsPixelDensity
102 | }));
103 | },
104 |
105 | renderDropsGfx(){
106 | let dropBuffer=createCanvas(dropSize,dropSize);
107 | let dropBufferCtx=dropBuffer.getContext('2d');
108 | this.dropsGfx=Array.apply(null,Array(255))
109 | .map((cur,i)=>{
110 | let drop=createCanvas(dropSize,dropSize);
111 | let dropCtx=drop.getContext('2d');
112 |
113 | dropBufferCtx.clearRect(0,0,dropSize,dropSize);
114 |
115 | // color
116 | dropBufferCtx.globalCompositeOperation="source-over";
117 | dropBufferCtx.drawImage(this.dropColor,0,0,dropSize,dropSize);
118 |
119 | // blue overlay, for depth
120 | dropBufferCtx.globalCompositeOperation="screen";
121 | dropBufferCtx.fillStyle="rgba(0,0,"+i+",1)";
122 | dropBufferCtx.fillRect(0,0,dropSize,dropSize);
123 |
124 | // alpha
125 | dropCtx.globalCompositeOperation="source-over";
126 | dropCtx.drawImage(this.dropAlpha,0,0,dropSize,dropSize);
127 |
128 | dropCtx.globalCompositeOperation="source-in";
129 | dropCtx.drawImage(dropBuffer,0,0,dropSize,dropSize);
130 | return drop;
131 | });
132 |
133 | // create circle that will be used as a brush to remove droplets
134 | this.clearDropletsGfx=createCanvas(128,128);
135 | let clearDropletsCtx=this.clearDropletsGfx.getContext("2d");
136 | clearDropletsCtx.fillStyle="#000";
137 | clearDropletsCtx.beginPath();
138 | clearDropletsCtx.arc(64,64,64,0,Math.PI*2);
139 | clearDropletsCtx.fill();
140 | },
141 | drawDrop(ctx,drop){
142 | if(this.dropsGfx.length>0){
143 | let x=drop.x;
144 | let y=drop.y;
145 | let r=drop.r;
146 | let spreadX=drop.spreadX;
147 | let spreadY=drop.spreadY;
148 |
149 | let scaleX=1;
150 | let scaleY=1.5;
151 |
152 | let d=Math.max(0,Math.min(1,((r-this.options.minR)/(this.deltaR))*0.9));
153 | d*=1/(((drop.spreadX+drop.spreadY)*0.5)+1);
154 |
155 | ctx.globalAlpha=1;
156 | ctx.globalCompositeOperation="source-over";
157 |
158 | d=Math.floor(d*(this.dropsGfx.length-1));
159 | ctx.drawImage(
160 | this.dropsGfx[d],
161 | (x-(r*scaleX*(spreadX+1)))*this.scale,
162 | (y-(r*scaleY*(spreadY+1)))*this.scale,
163 | (r*2*scaleX*(spreadX+1))*this.scale,
164 | (r*2*scaleY*(spreadY+1))*this.scale
165 | );
166 | }
167 | },
168 | clearDroplets(x,y,r=30){
169 | let ctx=this.dropletsCtx;
170 | ctx.globalCompositeOperation="destination-out";
171 | ctx.drawImage(
172 | this.clearDropletsGfx,
173 | (x-r)*this.dropletsPixelDensity*this.scale,
174 | (y-r)*this.dropletsPixelDensity*this.scale,
175 | (r*2)*this.dropletsPixelDensity*this.scale,
176 | (r*2)*this.dropletsPixelDensity*this.scale*1.5
177 | )
178 | },
179 | clearCanvas(){
180 | this.ctx.clearRect(0,0,this.width,this.height);
181 | },
182 | createDrop(options){
183 | if(this.drops.length >= this.options.maxDrops*this.areaMultiplier) return null;
184 |
185 | return Object.assign(Object.create(Drop),options);
186 | },
187 | addDrop(drop){
188 | if(this.drops.length >= this.options.maxDrops*this.areaMultiplier || drop==null) return false;
189 |
190 | this.drops.push(drop);
191 | return true;
192 | },
193 | updateRain(timeScale){
194 | let rainDrops=[];
195 | if(this.options.raining){
196 | let limit=this.options.rainLimit*timeScale*this.areaMultiplier;
197 | let count=0;
198 | while(chance(this.options.rainChance*timeScale*this.areaMultiplier) && count{
201 | return Math.pow(n,3);
202 | });
203 | let rainDrop=this.createDrop({
204 | x:random(this.width/this.scale),
205 | y:random((this.height/this.scale)*this.options.spawnArea[0],(this.height/this.scale)*this.options.spawnArea[1]),
206 | r:r,
207 | momentum:1+((r-this.options.minR)*0.1)+random(2),
208 | spreadX:1.5,
209 | spreadY:1.5,
210 | });
211 | if(rainDrop!=null){
212 | rainDrops.push(rainDrop);
213 | }
214 | }
215 | }
216 | return rainDrops;
217 | },
218 | clearDrops(){
219 | this.drops.forEach((drop)=>{
220 | // TODO this has to be an array of timeouts
221 | this.timeoutID = setTimeout(()=>{
222 | drop.shrink=0.1+(random(0.5));
223 | },random(1200))
224 | })
225 | this.clearTexture();
226 | },
227 | clearTexture(){
228 | this.textureCleaningIterations=50;
229 | // clearTimeout(this.timeoutID);
230 | },
231 | clean(){
232 | // TODO clear array of timeouts
233 | // clearTimeout(this.timeoutID);
234 | },
235 | updateDroplets(timeScale){
236 | if(this.textureCleaningIterations>0){
237 | this.textureCleaningIterations-=1*timeScale;
238 | this.dropletsCtx.globalCompositeOperation="destination-out";
239 | this.dropletsCtx.fillStyle="rgba(0,0,0,"+(0.05*timeScale)+")";
240 | this.dropletsCtx.fillRect(0,0,
241 | this.width*this.dropletsPixelDensity,this.height*this.dropletsPixelDensity);
242 | }
243 | if(this.options.raining){
244 | this.dropletsCounter+=this.options.dropletsRate*timeScale*this.areaMultiplier;
245 | times(this.dropletsCounter,(i)=>{
246 | this.dropletsCounter--;
247 | this.drawDroplet(
248 | random(this.width/this.scale),
249 | random(this.height/this.scale),
250 | random(...this.options.dropletsSize,(n)=>{
251 | return n*n;
252 | })
253 | )
254 | });
255 | }
256 | this.ctx.drawImage(this.droplets,0,0,this.width,this.height);
257 | },
258 | updateDrops(timeScale){
259 | let newDrops=[];
260 |
261 | this.updateDroplets(timeScale);
262 | let rainDrops=this.updateRain(timeScale);
263 | newDrops=newDrops.concat(rainDrops);
264 |
265 | this.drops.sort((a,b)=>{
266 | let va=(a.y*(this.width/this.scale))+a.x;
267 | let vb=(b.y*(this.width/this.scale))+b.x;
268 | return va>vb?1:va===vb?0:-1;
269 | });
270 |
271 | this.drops.forEach(function(drop,i){
272 | if(!drop.killed){
273 | // update gravity
274 | // (chance of drops "creeping down")
275 | if(chance((drop.r-(this.options.minR*this.options.dropFallMultiplier)) * (0.1/this.deltaR) * timeScale)){
276 | drop.momentum += random((drop.r/this.options.maxR)*4);
277 | }
278 | // clean small drops
279 | if(this.options.autoShrink && drop.r<=this.options.minR && chance(0.05*timeScale)){
280 | drop.shrink+=0.01;
281 | }
282 | //update shrinkage
283 | drop.r -= drop.shrink*timeScale;
284 | if(drop.r<=0) drop.killed=true;
285 |
286 | // update trails
287 | if(this.options.raining){
288 | drop.lastSpawn+=drop.momentum*timeScale*this.options.trailRate;
289 | if(drop.lastSpawn>drop.nextSpawn){
290 | let trailDrop=this.createDrop({
291 | x:drop.x+(random(-drop.r,drop.r)*0.1),
292 | y:drop.y-(drop.r*0.01),
293 | r:drop.r*random(...this.options.trailScaleRange),
294 | spreadY:drop.momentum*0.1,
295 | parent:drop,
296 | });
297 |
298 | if(trailDrop!=null){
299 | newDrops.push(trailDrop);
300 |
301 | drop.r*=Math.pow(0.97,timeScale);
302 | drop.lastSpawn=0;
303 | drop.nextSpawn=random(this.options.minR,this.options.maxR)-(drop.momentum*2*this.options.trailRate)+(this.options.maxR-drop.r);
304 | }
305 | }
306 | }
307 |
308 | //normalize spread
309 | drop.spreadX*=Math.pow(0.4,timeScale);
310 | drop.spreadY*=Math.pow(0.7,timeScale);
311 |
312 | //update position
313 | let moved=drop.momentum>0;
314 | if(moved && !drop.killed){
315 | drop.y+=drop.momentum*this.options.globalTimeScale;
316 | drop.x+=drop.momentumX*this.options.globalTimeScale;
317 | if(drop.y>(this.height/this.scale)+drop.r){
318 | drop.killed=true;
319 | }
320 | }
321 |
322 | // collision
323 | let checkCollision=(moved || drop.isNew) && !drop.killed;
324 | drop.isNew=false;
325 |
326 | if(checkCollision){
327 | this.drops.slice(i+1,i+70).forEach((drop2)=>{
328 | //basic check
329 | if(
330 | drop !== drop2 &&
331 | drop.r > drop2.r &&
332 | drop.parent !== drop2 &&
333 | drop2.parent !== drop &&
334 | !drop2.killed
335 | ){
336 | let dx=drop2.x-drop.x;
337 | let dy=drop2.y-drop.y;
338 | var d=Math.sqrt((dx*dx)+(dy*dy));
339 | //if it's within acceptable distance
340 | if(d<(drop.r+drop2.r)*(this.options.collisionRadius+(drop.momentum*this.options.collisionRadiusIncrease*timeScale))){
341 | let pi=Math.PI;
342 | let r1=drop.r;
343 | let r2=drop2.r;
344 | let a1=pi*(r1*r1);
345 | let a2=pi*(r2*r2);
346 | let targetR=Math.sqrt((a1+(a2*0.8))/pi);
347 | if(targetR>this.maxR){
348 | targetR=this.maxR;
349 | }
350 | drop.r=targetR;
351 | drop.momentumX+=dx*0.1;
352 | drop.spreadX=0;
353 | drop.spreadY=0;
354 | drop2.killed=true;
355 | drop.momentum=Math.max(drop2.momentum,Math.min(40,drop.momentum+(targetR*this.options.collisionBoostMultiplier)+this.options.collisionBoost));
356 | }
357 | }
358 | });
359 | }
360 |
361 | //slowdown momentum
362 | drop.momentum-=Math.max(1,(this.options.minR*0.5)-drop.momentum)*0.1*timeScale;
363 | if(drop.momentum<0) drop.momentum=0;
364 | drop.momentumX*=Math.pow(0.7,timeScale);
365 |
366 |
367 | if(!drop.killed){
368 | newDrops.push(drop);
369 | if(moved && this.options.dropletsRate>0) this.clearDroplets(drop.x,drop.y,drop.r*this.options.dropletsCleaningRadiusMultiplier);
370 | this.drawDrop(this.ctx, drop);
371 | }
372 |
373 | }
374 | },this);
375 |
376 | this.drops = newDrops;
377 | },
378 | update(){
379 | this.clearCanvas();
380 |
381 | let now=Date.now();
382 | if(this.lastRender==null) this.lastRender=now;
383 | let deltaT=now-this.lastRender;
384 | let timeScale=deltaT/((1/60)*1000);
385 | if(timeScale>1.1) timeScale=1.1;
386 | timeScale*=this.options.globalTimeScale;
387 | this.lastRender=now;
388 |
389 | this.updateDrops(timeScale);
390 |
391 | requestAnimationFrame(this.update.bind(this));
392 | }
393 | }
394 |
395 | export default Raindrops;
--------------------------------------------------------------------------------
/src/app/snow/ShaderProgram.jsx:
--------------------------------------------------------------------------------
1 | export default class ShaderProgram {
2 |
3 | constructor( holder, options = {} ) {
4 |
5 | options = Object.assign( {
6 | antialias: false,
7 | depthTest: false,
8 | mousemove: false,
9 | autosize: true,
10 | msaa: 0,
11 | vertex: `
12 | precision highp float;
13 |
14 | attribute vec4 a_position;
15 | attribute vec4 a_color;
16 |
17 | uniform float u_time;
18 | uniform vec2 u_resolution;
19 | uniform vec2 u_mousemove;
20 | uniform mat4 u_projection;
21 |
22 | varying vec4 v_color;
23 |
24 | void main() {
25 |
26 | gl_Position = u_projection * a_position;
27 | gl_PointSize = (10.0 / gl_Position.w) * 100.0;
28 |
29 | v_color = a_color;
30 |
31 | }`,
32 | fragment: `
33 | precision highp float;
34 |
35 | uniform sampler2D u_texture;
36 | uniform int u_hasTexture;
37 |
38 | varying vec4 v_color;
39 |
40 | void main() {
41 |
42 | if ( u_hasTexture == 1 ) {
43 |
44 | gl_FragColor = v_color * texture2D(u_texture, gl_PointCoord);
45 |
46 | } else {
47 |
48 | gl_FragColor = v_color;
49 |
50 | }
51 |
52 | }`,
53 | uniforms: {},
54 | buffers: {},
55 | camera: {},
56 | texture: null,
57 | onUpdate: ( () => {} ),
58 | onResize: ( () => {} ),
59 | }, options )
60 |
61 | const uniforms = Object.assign( {
62 | time: { type: 'float', value: 0 },
63 | hasTexture: { type: 'int', value: 0 },
64 | resolution: { type: 'vec2', value: [ 0, 0 ] },
65 | mousemove: { type: 'vec2', value: [ 0, 0 ] },
66 | projection: { type: 'mat4', value: [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ] },
67 | }, options.uniforms )
68 |
69 | const buffers = Object.assign( {
70 | position: { size: 3, data: [] },
71 | color: { size: 4, data: [] },
72 | }, options.buffers )
73 |
74 | const camera = Object.assign( {
75 | fov: 60,
76 | near: 1,
77 | far: 10000,
78 | aspect: 1,
79 | z: 100,
80 | perspective: true,
81 | }, options.camera )
82 |
83 | const canvas = document.createElement( 'canvas' )
84 | const gl = canvas.getContext( 'webgl', { antialias: options.antialias } )
85 |
86 | if ( ! gl ) return false
87 |
88 | this.count = 0
89 | this.gl = gl
90 | this.canvas = canvas
91 | this.camera = camera
92 | this.holder = holder
93 | this.msaa = options.msaa
94 | this.onUpdate = options.onUpdate
95 | this.onResize = options.onResize
96 | this.data = {}
97 |
98 | holder.appendChild( canvas )
99 |
100 | this.createProgram( options.vertex, options.fragment )
101 |
102 | this.createBuffers( buffers )
103 | this.createUniforms( uniforms )
104 |
105 | this.updateBuffers()
106 | this.updateUniforms()
107 |
108 | this.createTexture( options.texture )
109 |
110 | gl.enable( gl.BLEND )
111 | gl.enable( gl.CULL_FACE )
112 | gl.blendFunc( gl.SRC_ALPHA, gl.ONE )
113 | gl[ options.depthTest ? 'enable' : 'disable' ]( gl.DEPTH_TEST )
114 |
115 | if ( options.autosize )
116 | window.addEventListener( 'resize', e => this.resize( e ), false )
117 | if ( options.mousemove )
118 | window.addEventListener( 'mousemove', e => this.mousemove( e ), false )
119 |
120 | this.resize()
121 |
122 | this.update = this.update.bind( this )
123 | this.time = { start: performance.now(), old: performance.now() }
124 | this.update()
125 |
126 | }
127 |
128 | mousemove( e ) {
129 |
130 | let x = e.pageX / this.width * 2 - 1
131 | let y = e.pageY / this.height * 2 - 1
132 |
133 | this.uniforms.mousemove = [ x, y ]
134 |
135 | }
136 |
137 | resize( e ) {
138 |
139 | const holder = this.holder
140 | const canvas = this.canvas
141 | const gl = this.gl
142 |
143 | const width = this.width = holder.offsetWidth
144 | const height = this.height = holder.offsetHeight
145 | const aspect = this.aspect = width / height
146 | const dpi = this.dpi = Math.max( this.msaa ? 2 : 1, devicePixelRatio )
147 |
148 | canvas.width = width * dpi
149 | canvas.height = height * dpi
150 | canvas.style.width = width + 'px'
151 | canvas.style.height = height + 'px'
152 |
153 | gl.viewport( 0, 0, width * dpi, height * dpi )
154 | gl.clearColor( 0, 0, 0, 0 )
155 |
156 | this.uniforms.resolution = [ width, height ]
157 | this.uniforms.projection = this.setProjection( aspect )
158 |
159 | this.onResize( width, height, dpi )
160 |
161 | }
162 |
163 | setProjection( aspect ) {
164 |
165 | const camera = this.camera
166 |
167 | if ( camera.perspective ) {
168 |
169 | camera.aspect = aspect
170 |
171 | const fovRad = camera.fov * ( Math.PI / 180 )
172 | const f = Math.tan( Math.PI * 0.5 - 0.5 * fovRad )
173 | const rangeInv = 1.0 / ( camera.near - camera.far )
174 |
175 | const matrix = [
176 | f / camera.aspect, 0, 0, 0,
177 | 0, f, 0, 0,
178 | 0, 0, (camera.near + camera.far) * rangeInv, -1,
179 | 0, 0, camera.near * camera.far * rangeInv * 2, 0
180 | ]
181 |
182 | matrix[ 14 ] += camera.z
183 | matrix[ 15 ] += camera.z
184 |
185 | return matrix
186 |
187 | } else {
188 |
189 | return [
190 | 2 / this.width, 0, 0, 0,
191 | 0, -2 / this.height, 0, 0,
192 | 0, 0, 1, 0,
193 | -1, 1, 0, 1,
194 | ]
195 |
196 | }
197 |
198 | }
199 |
200 | createShader( type, source ) {
201 |
202 | const gl = this.gl
203 | const shader = gl.createShader( type )
204 |
205 | gl.shaderSource( shader, source )
206 | gl.compileShader( shader )
207 |
208 | if ( gl.getShaderParameter (shader, gl.COMPILE_STATUS ) ) {
209 |
210 | return shader
211 |
212 | } else {
213 |
214 | console.log( gl.getShaderInfoLog( shader ) )
215 | gl.deleteShader( shader )
216 |
217 | }
218 |
219 | }
220 |
221 | createProgram( vertex, fragment ) {
222 |
223 | const gl = this.gl
224 |
225 | const vertexShader = this.createShader( gl.VERTEX_SHADER, vertex )
226 | const fragmentShader = this.createShader( gl.FRAGMENT_SHADER, fragment )
227 |
228 | const program = gl.createProgram()
229 |
230 | gl.attachShader( program, vertexShader )
231 | gl.attachShader( program, fragmentShader )
232 | gl.linkProgram( program )
233 |
234 | if ( gl.getProgramParameter( program, gl.LINK_STATUS ) ) {
235 |
236 | gl.useProgram( program )
237 | this.program = program
238 |
239 | } else {
240 |
241 | console.log( gl.getProgramInfoLog( program ) )
242 | gl.deleteProgram( program )
243 |
244 | }
245 |
246 | }
247 |
248 | createUniforms( data ) {
249 |
250 | const gl = this.gl
251 | const uniforms = this.data.uniforms = data
252 | const values = this.uniforms = {}
253 |
254 | Object.keys( uniforms ).forEach( name => {
255 |
256 | const uniform = uniforms[ name ]
257 |
258 | uniform.location = gl.getUniformLocation( this.program, 'u_' + name )
259 |
260 | Object.defineProperty( values, name, {
261 | set: value => {
262 |
263 | uniforms[ name ].value = value
264 | this.setUniform( name, value )
265 |
266 | },
267 | get: () => uniforms[ name ].value
268 | } )
269 |
270 | } )
271 |
272 | }
273 |
274 | setUniform( name, value ) {
275 |
276 | const gl = this.gl
277 | const uniform = this.data.uniforms[ name ]
278 |
279 | uniform.value = value
280 |
281 | switch ( uniform.type ) {
282 | case 'int': {
283 | gl.uniform1i( uniform.location, value )
284 | break
285 | }
286 | case 'float': {
287 | gl.uniform1f( uniform.location, value )
288 | break
289 | }
290 | case 'vec2': {
291 | gl.uniform2f( uniform.location, ...value )
292 | break
293 | }
294 | case 'vec3': {
295 | gl.uniform3f( uniform.location, ...value )
296 | break
297 | }
298 | case 'vec4': {
299 | gl.uniform4f( uniform.location, ...value )
300 | break
301 | }
302 | case 'mat2': {
303 | gl.uniformMatrix2fv( uniform.location, false, value )
304 | break
305 | }
306 | case 'mat3': {
307 | gl.uniformMatrix3fv( uniform.location, false, value )
308 | break
309 | }
310 | case 'mat4': {
311 | gl.uniformMatrix4fv( uniform.location, false, value )
312 | break
313 | }
314 | }
315 |
316 | // ivec2 : uniform2i,
317 | // ivec3 : uniform3i,
318 | // ivec4 : uniform4i,
319 | // sampler2D : uniform1i,
320 | // samplerCube : uniform1i,
321 | // bool : uniform1i,
322 | // bvec2 : uniform2i,
323 | // bvec3 : uniform3i,
324 | // bvec4 : uniform4i,
325 |
326 | }
327 |
328 | updateUniforms() {
329 |
330 | const gl = this.gl
331 | const uniforms = this.data.uniforms
332 |
333 | Object.keys( uniforms ).forEach( name => {
334 |
335 | const uniform = uniforms[ name ]
336 |
337 | this.uniforms[ name ] = uniform.value
338 |
339 | } )
340 |
341 | }
342 |
343 | createBuffers( data ) {
344 |
345 | const gl = this.gl
346 | const buffers = this.data.buffers = data
347 | const values = this.buffers = {}
348 |
349 | Object.keys( buffers ).forEach( name => {
350 |
351 | const buffer = buffers[ name ]
352 |
353 | buffer.buffer = this.createBuffer( 'a_' + name, buffer.size )
354 |
355 | Object.defineProperty( values, name, {
356 | set: data => {
357 |
358 | buffers[ name ].data = data
359 | this.setBuffer( name, data )
360 |
361 | if ( name == 'position' )
362 | this.count = buffers.position.data.length / 3
363 |
364 | },
365 | get: () => buffers[ name ].data
366 | } )
367 |
368 | } )
369 |
370 | }
371 |
372 | createBuffer( name, size ) {
373 |
374 | const gl = this.gl
375 | const program = this.program
376 |
377 | const index = gl.getAttribLocation( program, name )
378 | const buffer = gl.createBuffer()
379 |
380 | gl.bindBuffer( gl.ARRAY_BUFFER, buffer )
381 | gl.enableVertexAttribArray( index )
382 | gl.vertexAttribPointer( index, size, gl.FLOAT, false, 0, 0 )
383 |
384 | return buffer
385 |
386 | }
387 |
388 | setBuffer( name, data ) {
389 |
390 | const gl = this.gl
391 | const buffers = this.data.buffers
392 |
393 | if ( name == null && ! gl.bindBuffer( gl.ARRAY_BUFFER, null ) ) return
394 |
395 | gl.bindBuffer( gl.ARRAY_BUFFER, buffers[ name ].buffer )
396 | gl.bufferData( gl.ARRAY_BUFFER, new Float32Array( data ), gl.STATIC_DRAW )
397 |
398 | }
399 |
400 | updateBuffers() {
401 |
402 | const gl = this.gl
403 | const buffers = this.buffers
404 |
405 | Object.keys( buffers ).forEach( name =>
406 | buffers[ name ] = buffer.data
407 | )
408 |
409 | this.setBuffer( null )
410 |
411 | }
412 |
413 | createTexture( src ) {
414 |
415 | const gl = this.gl
416 | const texture = gl.createTexture()
417 |
418 | gl.bindTexture( gl.TEXTURE_2D, texture )
419 | gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array( [ 0, 0, 0, 0 ] ) )
420 |
421 | this.texture = texture
422 |
423 | if ( src ) {
424 |
425 | this.uniforms.hasTexture = 1
426 | this.loadTexture( src )
427 |
428 | }
429 |
430 | }
431 |
432 | loadTexture( src ) {
433 |
434 | const gl = this.gl
435 | const texture = this.texture
436 |
437 | const textureImage = new Image()
438 |
439 | textureImage.onload = () => {
440 |
441 | gl.bindTexture( gl.TEXTURE_2D, texture )
442 |
443 | gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textureImage )
444 |
445 | gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR )
446 | gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR )
447 |
448 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
449 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
450 |
451 | // gl.generateMipmap( gl.TEXTURE_2D )
452 |
453 | }
454 |
455 | textureImage.src = src
456 |
457 | }
458 |
459 | update() {
460 |
461 | const gl = this.gl
462 |
463 | const now = performance.now()
464 | const elapsed = ( now - this.time.start ) / 5000
465 | const delta = now - this.time.old
466 | this.time.old = now
467 |
468 | this.uniforms.time = elapsed
469 |
470 | if ( this.count > 0 ) {
471 | gl.clear( gl.COLORBUFFERBIT )
472 | gl.drawArrays( gl.POINTS, 0, this.count )
473 | }
474 |
475 | this.onUpdate( delta )
476 |
477 | requestAnimationFrame( this.update )
478 |
479 | }
480 |
481 | }
--------------------------------------------------------------------------------
/src/app/snow/SnowEffect.jsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useMemo } from "react";
2 | import { Canvas, useFrame, extend } from "@react-three/fiber";
3 | import * as THREE from "three";
4 | import snowflake from "./snowflake.png";
5 |
6 |
7 | // https://github.com/bsehovac/shader-program
8 |
9 | // const snowflake = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAGAGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDAgNzkuMTYwNDUxLCAyMDE3LzA1LzA2LTAxOjA4OjIxICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoTWFjaW50b3NoKSIgeG1wOkNyZWF0ZURhdGU9IjIwMTUtMDctMDNUMTg6NTk6MjIrMDI6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDE5LTAxLTEyVDE1OjE0OjQwKzAxOjAwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDE5LTAxLTEyVDE1OjE0OjQwKzAxOjAwIiBkYzpmb3JtYXQ9ImltYWdlL3BuZyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIgcGhvdG9zaG9wOklDQ1Byb2ZpbGU9InNSR0IgSUVDNjE5NjYtMi4xIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOmIzMzBlMWI0LTk5ZDctNGU2NS05MGQ2LTNmYjFiYmE2ZTE0MCIgeG1wTU06RG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjAyNThjNzMxLTQ4ZjQtYTA0MS1hNGFkLTQ4MTA2MTVjY2FlYSIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOjJjY2VkMTUyLTRjNzAtNDFlZC1hMzcyLWRlOWY4NjgyZTcwMSI+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNyZWF0ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6MmNjZWQxNTItNGM3MC00MWVkLWEzNzItZGU5Zjg2ODJlNzAxIiBzdEV2dDp3aGVuPSIyMDE1LTA3LTAzVDE4OjU5OjIyKzAyOjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoTWFjaW50b3NoKSIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6YjMzMGUxYjQtOTlkNy00ZTY1LTkwZDYtM2ZiMWJiYTZlMTQwIiBzdEV2dDp3aGVuPSIyMDE5LTAxLTEyVDE1OjE0OjQwKzAxOjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoTWFjaW50b3NoKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz50mbqsAAAToElEQVR4nOVbW49dR5X+VlXtc2v3Ne52N6bTTnAc0u02GRJQwEhYQkJ5QhmhPPOUl0iBB/4AP4JfMA95IEIaJEYiQhlesCZMEE1iGxJHIjK21XbSwX1xd5+zd9Va87BrVdfefWwI4xEaUVJpn7PPPnVqfbUu31pVh0QE/8zN/KMn8I9uTl+8+uqrAAARgYgghIBer4dnnnkGAPDFL34RANDr9eiJJ55AVVUgIhhjwMwYDAbY3Nyk8+fPP/DHdnd3ZWtr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';
10 |
11 | const count = 7000;
12 |
13 | let wind = {
14 | current: 0,
15 | force: 0.1,
16 | target: 0.1,
17 | min: 0.1,
18 | max: 0.25,
19 | easing: 0.005
20 | };
21 |
22 | // Shaders from your original code
23 | const vertexShader = `
24 | precision highp float;
25 | attribute float size;
26 | attribute vec3 rotation;
27 | attribute vec3 speed;
28 | attribute vec4 a_color;
29 | attribute float scale;
30 | attribute float distortion;
31 | attribute float brightness;
32 | attribute float contrast;
33 | attribute float rotationOffset;
34 | attribute float flipX;
35 | attribute float flipY;
36 | attribute float warp;
37 | varying vec4 v_color;
38 | varying float v_rotation;
39 | varying float v_scale;
40 | varying float v_distortion;
41 | varying float v_brightness;
42 | varying float v_contrast;
43 | varying float v_rotationOffset;
44 | varying float v_flipX;
45 | varying float v_flipY;
46 | varying float v_warp;
47 | uniform float u_time;
48 | uniform vec3 u_worldSize;
49 | uniform float u_gravity;
50 | uniform float u_wind;
51 | void main() {
52 | v_color = a_color;
53 | v_rotation = rotation.x + u_time * rotation.y;
54 | v_scale = scale;
55 | v_distortion = distortion;
56 | v_brightness = brightness;
57 | v_contrast = contrast;
58 | v_rotationOffset = rotationOffset;
59 | v_flipX = flipX;
60 | v_flipY = flipY;
61 | v_warp = warp;
62 | vec3 pos = position;
63 | pos.x = mod(pos.x + u_time + u_wind * speed.x, u_worldSize.x * 2.0) - u_worldSize.x;
64 | pos.y = mod(pos.y - u_time * speed.y * u_gravity, u_worldSize.y * 2.0) - u_worldSize.y;
65 | pos.x += sin(u_time * speed.z) * rotation.z;
66 | pos.z += cos(u_time * speed.z) * rotation.z;
67 | gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
68 | gl_PointSize = (size * v_scale / gl_Position.w) * 100.0;
69 | }
70 | `;
71 |
72 | const fragmentShader = `
73 | precision highp float;
74 | uniform sampler2D u_texture;
75 | uniform float u_time;
76 | varying vec4 v_color;
77 | varying float v_rotation;
78 | varying float v_scale;
79 | varying float v_distortion;
80 | varying float v_brightness;
81 | varying float v_contrast;
82 | varying float v_rotationOffset;
83 | varying float v_flipX;
84 | varying float v_flipY;
85 | varying float v_warp;
86 |
87 | // Simple noise function
88 | float noise(vec2 p) {
89 | return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
90 | }
91 |
92 | void main() {
93 | vec2 coord = gl_PointCoord - 0.5;
94 | // Apply random scale
95 | coord *= v_scale;
96 | // Add random distortion based on noise and time
97 | float distortion_amount = v_distortion * 0.15;
98 | coord += vec2(
99 | noise(coord * 10.0 + u_time * 0.1) * distortion_amount,
100 | noise(coord * 10.0 + u_time * 0.1 + 1.0) * distortion_amount
101 | );
102 | // Apply warp (skew)
103 | coord.x += v_warp * coord.y;
104 | coord.y += v_warp * coord.x;
105 | // Apply flip
106 | if (v_flipX > 0.5) coord.x = -coord.x;
107 | if (v_flipY > 0.5) coord.y = -coord.y;
108 | // Apply random rotation offset
109 | float angle = v_rotation + v_rotationOffset;
110 | vec2 rotated = vec2(
111 | cos(angle) * coord.x + sin(angle) * coord.y,
112 | cos(angle) * coord.y - sin(angle) * coord.x
113 | ) + 0.5;
114 | vec4 snowflake = texture2D(u_texture, rotated);
115 | // Apply brightness and contrast
116 | vec3 color = snowflake.rgb;
117 | color = (color - 0.5) * v_contrast + 0.5 + v_brightness;
118 | color = clamp(color, 0.0, 1.0);
119 | gl_FragColor = vec4(color, snowflake.a * v_color.a);
120 | }
121 | `;
122 |
123 | // Settings for gentle and storm snow
124 | const GENTLE_SETTINGS = {
125 | count: 3000,
126 | gravity: 20,
127 | colorAlphaMin: 0.2,
128 | colorAlphaMax: 0.6,
129 | sizeMin: 5,
130 | sizeMax: 15,
131 | scaleMin: 0.5,
132 | scaleMax: 1.5,
133 | distortionMin: 0.1,
134 | distortionMax: 0.5,
135 | brightnessMin: -0.1,
136 | brightnessMax: 0.2,
137 | contrastMin: 0.8,
138 | contrastMax: 1.2,
139 | wind: {
140 | force: 0.05,
141 | target: 0.05,
142 | min: 0.02,
143 | max: 0.1,
144 | easing: 0.002,
145 | },
146 | windDirectionChangeFreq: 0.97,
147 | windDirectionChangeAmount: 1.0,
148 | speedYMin: 0.5,
149 | speedYMax: 1.0,
150 | speedXMin: 0.2,
151 | speedXMax: 0.5,
152 | swayMax: 5,
153 | };
154 |
155 | const STORM_SETTINGS = {
156 | count: 7000,
157 | gravity: 45,
158 | colorAlphaMin: 0.25,
159 | colorAlphaMax: 0.8,
160 | sizeMin: 7,
161 | sizeMax: 18,
162 | scaleMin: 0.7,
163 | scaleMax: 2.2,
164 | distortionMin: 0.2,
165 | distortionMax: 1.0,
166 | brightnessMin: -0.2,
167 | brightnessMax: 0.3,
168 | contrastMin: 0.7,
169 | contrastMax: 1.3,
170 | wind: {
171 | force: 0.15,
172 | target: 0.2,
173 | min: 0.08,
174 | max: 0.35,
175 | easing: 0.01,
176 | },
177 | windDirectionChangeFreq: 0.995,
178 | windDirectionChangeAmount: 0.2,
179 | speedYMin: 1.2,
180 | speedYMax: 2.0,
181 | speedXMin: 0.4,
182 | speedXMax: 1.0,
183 | swayMax: 12,
184 | };
185 |
186 | function SnowParticles({ settings }) {
187 | const mesh = useRef();
188 | const worldSize = [110, 110, 80];
189 | const gravity = settings.gravity;
190 | const wind = useRef({
191 | current: 0,
192 | force: settings.wind.force,
193 | target: settings.wind.target,
194 | min: settings.wind.min,
195 | max: settings.wind.max,
196 | easing: settings.wind.easing,
197 | });
198 | // Wind angle in radians
199 | const windAngle = React.useRef(0); // 0 = right, PI = left
200 |
201 | // Generate attributes
202 | const { positions, colors, sizes, rotations, speeds, scales, distortions, brightnesses, contrasts, rotationOffsets, flipXs, flipYs, warps } = useMemo(() => {
203 | const positions = [];
204 | const colors = [];
205 | const sizes = [];
206 | const rotations = [];
207 | const speeds = [];
208 | const scales = [];
209 | const distortions = [];
210 | const brightnesses = [];
211 | const contrasts = [];
212 | const rotationOffsets = [];
213 | const flipXs = [];
214 | const flipYs = [];
215 | const warps = [];
216 | for (let i = 0; i < settings.count; i++) {
217 | // Position
218 | positions.push(
219 | -worldSize[0] + Math.random() * worldSize[0] * 2,
220 | -worldSize[1] + Math.random() * worldSize[1] * 2,
221 | Math.random() * worldSize[2] * 2
222 | );
223 | // Speed
224 | speeds.push(
225 | settings.speedXMin + Math.random() * (settings.speedXMax - settings.speedXMin),
226 | settings.speedYMin + Math.random() * (settings.speedYMax - settings.speedYMin),
227 | Math.random() * settings.swayMax
228 | );
229 | // Rotation
230 | rotations.push(
231 | Math.random() * 2 * Math.PI,
232 | Math.random() * 20,
233 | Math.random() * 10
234 | );
235 | // Color (RGBA)
236 | colors.push(1, 1, 1, settings.colorAlphaMin + Math.random() * (settings.colorAlphaMax - settings.colorAlphaMin));
237 | // Size
238 | sizes.push(settings.sizeMin + Math.random() * (settings.sizeMax - settings.sizeMin));
239 | // Random scale
240 | scales.push(settings.scaleMin + Math.random() * (settings.scaleMax - settings.scaleMin));
241 | // Random distortion
242 | distortions.push(settings.distortionMin + Math.random() * (settings.distortionMax - settings.distortionMin));
243 | // Random brightness
244 | brightnesses.push(settings.brightnessMin + Math.random() * (settings.brightnessMax - settings.brightnessMin));
245 | // Random contrast
246 | contrasts.push(settings.contrastMin + Math.random() * (settings.contrastMax - settings.contrastMin));
247 | // Random rotation offset
248 | rotationOffsets.push(Math.random() * Math.PI * 2);
249 | // Random flip
250 | flipXs.push(Math.random() > 0.5 ? 1 : 0);
251 | flipYs.push(Math.random() > 0.5 ? 1 : 0);
252 | // Random warp
253 | warps.push(-0.3 + Math.random() * 0.6); // -0.3 to 0.3
254 | }
255 | return {
256 | positions: new Float32Array(positions),
257 | colors: new Float32Array(colors),
258 | sizes: new Float32Array(sizes),
259 | rotations: new Float32Array(rotations),
260 | speeds: new Float32Array(speeds),
261 | scales: new Float32Array(scales),
262 | distortions: new Float32Array(distortions),
263 | brightnesses: new Float32Array(brightnesses),
264 | contrasts: new Float32Array(contrasts),
265 | rotationOffsets: new Float32Array(rotationOffsets),
266 | flipXs: new Float32Array(flipXs),
267 | flipYs: new Float32Array(flipYs),
268 | warps: new Float32Array(warps),
269 | };
270 | }, [settings]);
271 |
272 | // Texture loading (async)
273 | const [texture, setTexture] = React.useState();
274 | React.useEffect(() => {
275 | const loader = new THREE.TextureLoader();
276 | loader.load(snowflake.src, (tex) => {
277 | setTexture(tex);
278 | });
279 | }, []);
280 |
281 | // Uniforms
282 | const uniforms = useMemo(
283 | () => ({
284 | u_time: { value: 0 },
285 | u_texture: { value: texture },
286 | u_worldSize: { value: worldSize },
287 | u_gravity: { value: gravity },
288 | u_wind: { value: 0 },
289 | }),
290 | [texture, gravity]
291 | );
292 |
293 | useFrame((state, delta) => {
294 | // Wind logic
295 | const w = wind.current;
296 | w.force += (w.target - w.force) * w.easing;
297 | w.current += w.force * (delta * 0.2);
298 | // Wind angle logic: nudge angle
299 | if (Math.random() > settings.windDirectionChangeFreq) {
300 | // Gentle: nudge more often/larger, Storm: less often/smaller
301 | const nudge = (Math.random() - 0.5) * settings.windDirectionChangeAmount;
302 | windAngle.current += nudge;
303 | // Clamp angle to [-PI, PI] for stability
304 | if (windAngle.current > Math.PI) windAngle.current -= 2 * Math.PI;
305 | if (windAngle.current < -Math.PI) windAngle.current += 2 * Math.PI;
306 | }
307 | // Compute wind X (horizontal) from angle
308 | const windX = Math.cos(windAngle.current);
309 | // Optionally, you could use windY = Math.sin(windAngle.current) for vertical modulation
310 | uniforms.u_wind.value = w.current * windX;
311 | // Subtle wind change (gentle drift)
312 | if (Math.random() > 0.98) {
313 | w.target += (Math.random() - 0.5) * 0.01; // Small nudge
314 | w.target = Math.max(w.min, Math.min(w.max, w.target));
315 | }
316 | // Occasional strong/random wind change
317 | if (Math.random() > 0.995) {
318 | w.target = (w.min + Math.random() * (w.max - w.min)) * (Math.random() > 0.5 ? -1 : 1);
319 | }
320 | uniforms.u_time.value = state.clock.getElapsedTime();
321 | });
322 |
323 | if (!texture) return null;
324 |
325 | return (
326 |
327 |
328 |
334 |
340 |
346 |
352 |
358 |
364 |
370 |
376 |
382 |
388 |
394 |
400 |
406 |
407 |
414 |
415 | );
416 | }
417 |
418 | /**
419 | * SnowEffect component
420 | * Props:
421 | * type: 'gentle' | 'storm' (default: 'gentle')
422 | * - 'gentle': Gentle snowfall
423 | * - 'storm': Heavy snowstorm
424 | */
425 |
426 | export default function SnowEffect({ type = 'gentle', backgroundImageUrl }) {
427 | const settings = type === 'storm' ? STORM_SETTINGS : GENTLE_SETTINGS;
428 | return (
429 |
430 |
433 |
434 | );
435 | }
--------------------------------------------------------------------------------