├── 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 | 161 | 162 | {sprites.map((init, i) => ( 163 | 170 | ))} 171 | 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/DHdnd3ZWtrS6y1ICICACLCtWvXxDmH2dlZXL9+HR988AF6vR5mZmawvb2Nw8NDeO+xt7eHpaUlDAYDTE5O4vvf/z7OnTuH1157DR9++CGICNvb2zh58iTubG7iwrP/gvmz61iZG2B5aQG/+e93MDExgW9/+9vHAcgbEWFvbw+Tk5O0uLiIEydOyPnz52lychL9fj89BgD7+/s0MTGh4KT749rU1JRMTU3p22R7y8vL2N3dTWPs7e3JnTt3sLW1BWvtAwF9FK0BQFwUfPLJJ5ibm8Mrr7yiGqCmkgtHZVni4OCAJiYmCAAiEA0AmBlEpGPnDoez1wqMrK2tydraGr333nu4evUqfv/730tZluh2u405PqqWABgOhwCAnZ0dLCws4Ec/+hGdOnUqf5ZEhA4ODtDr9chaS51OB/Pz84QjoRsA6bjOOel0OknY7HMBgL/85S8yGo1kYWEB1loCIBcuXMCFCxdkfX2dNjY25J133sH9+/chIo8UhATA5z//eRweHmJxcRHf+973iIioqioURaGTNQBQlqWJq2GYGWVZUq/XGwcCAVCTkUxg7UDUgtnZWd7e3hZmhrVWkGnH+vo61tfX8eUvfxlvvPGG3Lx5E8vLy8hM6dEA0Ov1MBwO8eKLL+Lpp5/GRx99hLm5ORXGACAiotnZWROFIQDGGJNee+8hIlQURVqibLUku2pnAEJEZnZ2VoXmDKwExPnz57G8vIxf/epXuHXrlnz88ccoyxLe+/8VRiQA7t+/j+npaaytrREAeuKJJ/SjXGACQCEEIyLGOWc6nQ6h1gZTVZWJqv6g8MoAwMwiImyt5UxQ7TlIlIMwPT2Nl156SXZ3d+nKlSuoqkq2t7f/buEbADjncOnSJXrssceAphorABARC8CIiBERKyI2hGCcc0REptPpGGttDhhCCClc5gKKSC50qKqKrbVsjMnvqzbw7u6uMDPNzMzw1NSUXLx4ET/5yU/o5s2b8t5772E0GmFycvIz+4gEwNmzZ/H0008DqD33cDikfr9vNFwDMGVZWmutcc5ZAE5EbDQBS0QqvEF0mAAoCq5NALAxRlqCBmstExEDCLHrZwIgFEUhckRbBQBefvllAYC3334bV65ckY2NDezv738mEBIA3nuMRiOgVmfa39+nbrdL0SsbAKbT6Zi46tZaa4nIEpGN49j4nI2AkYiYLAQmAFQoACwiLCKBiAIRBQA+jqNgMADq9/v63mZjAABeeOEFvPDCC3Tt2jV5/fXX5U9/+hMI+Js4RFoe51yK1c45zM/P58ITAENExtRL6mIvAHS0i0iXmbsAekTUN8b0AfRFpA+gD2AQez+79gD0RETH6cZrkf1GfjUP6FhbW6Mf/OAHeOmll6jb7WBrawvAw7lD0oAzZ86gFfeBzNtrZ2Zna2gLEXFRAwpmLuJnyT+ISMQr+ZTc+wcAuvIetUlV8bVFUxNyZxiy9/l4BAALCwvmu9/9rpxZWaH/evs3Mqw8HpbvJQDW19d1xfPeBsEyswVQeO+dtdZZa9NqWWsLAI6IHOoQaVpjHQMgdi8iPgJQReHzbgFU2ULo9/JQmfMLPPf881j/0pfk3376H9gyI6ycXnw4ALOzs+lmCIEODg7oxIkThohoOBxSp9MxRGSdczZGA0tEBWpNKIwxHX0dx3XRB+QAAE0foDbviaiKwBVRWO3Wex9ql0MBzbAcsnFzcAFAOkVBFy88hW63J1UIGJf6JwCuXLmC9fV1AIAxBv1+H0QEESEiMsxs1NMTke10OiqkE5GO2rAxJtksERkRMaj9R1sLfJysB1CFEEpjjGqA+hiDWrPK+H3ViFxLc2eZTEHbM6trAIB3331XNOEaC8DCwoK+JCKCc47ia1Lq2+oWtboXRFREALpxzA4Ruegj9FnDzDDGCABmZjbGqPqXOFr5Ml5HqE2uin6mxFGk0V4B4LIsiYh8pO05yBxCECLC4uIiXbp0SbJstgnA3t5e7gTbbrNBf9sgKBCoTSBFBUSugMgTAGiMlizmewClMcYDGMXvlxHUMgpeZgInrhEB9Vm48/GawuSNGzdodnYWp06dEu89QsitJgMgQ08R1FhOIQTKihgkIoaZrTEm8QARSUCISBf1ihbRri1iGG0BwKjtvxOFdHoVERdC0N8wkVjlpqTFG2nFe5/P//Tp0+JcLeb169dxeHiI55577jgAjz/+OFqt7TFyDSBmJiIyquIRgEJVH7UZFDiK3xoZdPICgDOvn6/2iIg03ObEikSEtAoVgcznKXF+6V6321VtoNXVVezs7DTkSkRoc3OzDUAtde0PGmSCiKgoCiIiik6uZkiRJEWG6IhInWNXRHpE1ENNfnJSNJFdJwBMiMgEgAkiGhCRPt8D0DPGdBVcZi7UxHDcR7XDOLz3+POf/9yQb5wT1C8pomNbCAHMTDE85TTYoqUJiOaAOpHK9VWjQRE5gM3GyHMKAIAxRqQOS0xE4pxrpMwA2HtvALA68Vwea60sLy+PB+Djjz/G5z73OZ1UTiqkqiohImFmFEUhRCTGmLzUlTcTBVWTKBBXTBki1U1/i1E7N/UVyi5T0RS1xQgza/1gXBrNcXyOmsg4YpLQsbz3qRLVAGB+fr4tCLz3IiKKtMQQJnEwtb8crBQ6lT/ESSQHSURKpAT1CodoLho2rSZRugAikjLFzHmGOG6izhlXaZOvJPTJkyePrRYAIBYWGiqvzEmFNcao0ByLGg1NyX8w8w+5eeRa0RWRDhF1Ee0bNY9QX6G9LyI9EenGZ/NESbsFoKm6EREzGo00cjR8wc7OTgOApAFxdRt1u6IokpqJCHvvWfN2XRGuwwFnK5OnqkkTMmflEHlBZgYBRyGONFTGz5jqBCnPFRwAx8zqcBt+AwBFx50zRgFAw+FQpqenjwNw9+5ddLtdnDhxQifV6MwsRMTGGGHmRkEjqjFHVU3fia9T+IwTUpMw2cRyz60rlhMl1ZqCmQuKeUO09UTTs/HbJqBNBoNB40YygY2NDezt7Y35Tj0Zay0751hEQqSwqefvY4Ejd1LqM6B5QeyaMjsR0SQq74lRRqKkxKqgZhGmHQa1RjkWhHY+kDTga1/7mjrCdtUWcSD23jMRBWttyuSIqBIRVVEPwBtjqgyMoGGrsRS1o1M75YzhtWlyyjjj1ajKM7OlukjTSOOJKGe2DSBipEstacCPf/xjvPnmm405olV0cM6FWMkNAEIIIYhIEJHAzIGIfLRXBScvdXE0k6QRes1UN88xbKYx6jPymoQxxhiKmWrm8Nr1jEbb399vvE8a8Morr2Bubi53hNpUCwxqLQjW2hALGAFHK14BqJi5QqYZCkh8VkFgHK16Hu/y0Jm/TnZtjEkFV2amGgP6mwuhmhdoSxrQ6XRw69YtfPjhh+3vNKo41tpUxnLOqVfOe+L0+WfRcWnxI2lF5i8Sr49JzrjKlIKUkyS9Nljj39oSHLdu3UJZlpiammonFzqohiolJKmaE4X0AKpcG3CU4alj03Q2D1mIgqUQqpEk4yH6TEPAfMVzkhaf1Wca9733yFsC4Jvf/Ga6eePGDSwuLkq3202/EEKQw8ND7vV6wTmXfIBGgBBCZa11RKQakKe4eVZIOMrwEAE1aDo/NRENs6nFvcNxPckOQEII4r3XLfvU2u+PbWHdvXsXP/vZz2R3d7fxAzEfYOeclrG8McZHx1dZa0sAlYhUrUKG9rFmgjHmgiONUocaiEjB5pgWJFDGACHOOXQ6nXYyJwcHB40bSQM0PpZlia9//esgInz66acYDAbS7/el1+vl5WldIR9XXusBlYJgrS2ZeRR5foFY4oo/Rzji+GoOwBG3aFSGc0eqYTUDpr2VxiIiVVVJBKABTntXOQGwsbEBoK4MTUxM4Nq1a7DWyurqKvr9fu4LGltaWtcjokRViaiUowqRFk+1LKarwkQUYgKUp98hRo9cEyoc+Z1kJpnwDUGJSLfZj7XLv/41Ln7jG8cBmJycTDe99xgMBlhZWUG/34fm4DiqtugkTFTHYK2toqClMSavE+bbZg0AolAujoM6fxI1sWQ6GSBKuJR7sDGGiYhDCJwla+N8BUQERcssEgBxXzA1IsL169fhvcezzz6LmZkZnTRQU01NjnRlQlx93cRQB5gXRY8BEEHLa4XJDESkAlBGv6J8Q/1OCHWFs60NHBM0oeaxHGEAZ7/61YacCYCvfOUrjQ9iLAZQcwS9rVctkuS0GEf5uZawx5WqdEWUHCnLUwKWA1ACGBljSmYu0XSMPv42I+4469hVVYlzTjIzEABg7/Hrt97Cd1588TgA4zYNFIiDgwM5efJkftyFu92u7tLoVpWPk05cHc0ymTo6TaN1T1ALIMeywBhSRwoCjkeJPClLGhALoTrXpPLGGLlw7lxDvgRAe8Mgb4PBIP88IT0cDrkoimCt1ZS0kqPydc7rc9Kjds5RrS0zK/UFjtih0ulSREaoQ+UIx0NqDkR7w7ThBzY2NrC7u4szTz752QBotYRop9PRukBgZmOtJWOMCSFUmqigWZBIDjSuvgPgjDGavipzy80qlcrRigo4rgFt4dN8RQR5IeQYABcvXnygxLdv38Zrr72GH/7wh3myxNnWNzvngvfeOOd8FttV7TXuCx1thzf2CzIfoOww3x3OQVBfkK9+mws0vP/W1pZcv35dTp48ibm5ufEA6GnPdhMRTExMYHJyMs4/edbGigKgeERGdz8aiUx0qoIsj4j5fHKCUu9Cae0x8QvUkaCt+rkW5IXSvEwuADA3N4fnn38eRVEc2yEmvfHHP/5xLAAKAhHh3LlzjTMEIQQ3Go3MYDAwAFw8M2BjbE9VHIm7x3HnuMg+y2uFjb2ILBwGHGmAR51yj1DvJ+q9HBzVCjUlvnfvnty4cUO0SLK2tpZkSxqwsrIyVngiSsddx5y5YWOMbjhyzA2AOlevgNrzxqKHZAXUEFmj8oMcAAUhX1HPzJUxplK2GULwqJmor6qKY54iQE3kdnd3OdY35He/+51cvnwZExMTMMY0AEga0CZCbRC89/DeY2pqKs/TDQA6PDw0uolpjLHOOcfMzntvnXPOGFOISBFCKGLG6Jg5rw6bPM9HdogiRgT1GxUze135yBBLLcyo1ohIqKqKO51OKMtS3n//fckLJhcuXDiuAXfu3HkgAABweHiYb5/lbI76/T6JCO/s7ICIMD09jbhzlJe2OdJXZ611xpik+sxsRUTNqyYtzcqzRoQQt9FTPTKEkMryMV1nItJECKPRCH/4wx/SHmcbgKQB77777kMBUAR7vR594QtfQLb3ljRBRCiGQ43tNhNU9wotxd0hZrbWWqrLCibXAM0/NGNMSVCoz+h5730wxngR8cwcnHNp1XHkA0RE+ODgoOH5coefNODnP//5QwGg+GeElZUVefLJJ8fVDYmISP0EM0sIAd1ut6EFEQwTQtBTZCYSKZWfdOs7jpMSHsSSnF5jPpCKJ51Oh9vz8t5ja2urUTMcC4Cex39Y63Q6WFlZQVEUcu/ePep2uxgMBnmWqBrBzjndUzQAJO7aSqwm6Va60mATQtATH3k0yLM6RvQJ6khRO0dmZv2cEfnGaDSSbreLoihQVRVCCGMPTuZbY38VAGstiqLAaDTC66+/Lt/61rfs7zQPqiKnvQU9GxTvKwB65s9E+0x1/QwEHVMTHXbOcfQFbK1VX5GYX1mWuH37NpaXl2l3d5eXl5cfeGp07F9mHtaICGVZYjAYYGZmRgDQaDSSoijywmQuHEUA8tp/ewsrnUkOIehefg5AIl5RzVPPs8DsGTlz5oxYa+XevXu4c+dOntHiXJYQfWYA1Jacc7r7Im+++SZWV1fp7NmzOlktn7XBUHJCIQRDddxUUqUHq5MGVFUFAIibtOrUhJnVNDi7z3fv3hURkaWlpWQ6Tz311EPl+cwAjGvRxnS1xjrI1n0ajUbinCNdGf2vQQyFABqHnZMmhBAkaiADkMPDQ4n/cJPhcIj5+Xl1nvj3n/4UB/v7ODEx0ZjQv7788qMFoNPppBj76aefyi9+8QssLS3JpUuXKNt2b+QGg8EgrzNSr9djzQi1jTlqj+hcARz9+cJ7D+ecnDlzJiVABwcH0GNxh/H/UOPaIwEgb/fv38fVq1fbyVVuy5RdtVGkrzQzM/Mwhyzta/Y7aZE/+ugj/Odbb8n0zAzmTp1CfbJmfHvkADjnMDMzg8ceewzGGNnf38fly5fp9OnTknNwNEEQay2mpqbaf7Bot3alV8a9/u1vfytXr13D6uoqyrJ86Hz/euz7O5uivr29jV/+8peyubmZx/Njh5xiMjMutz/WfVXxzs4O7+zsyL1792R/f78RBRYWFjA5OYm4OXKs5+2Ra0C7OeewsLCA+F+kvKUVGw6H2Nzc1Pxh7DiBGT0iWXr8cbp5+zY+eP99mZ6exsHBAZaWlrC6upqefZjKt1vKBf5Z2/+ZCfx/af8DTo8DJZHbJ6cAAAAASUVORK5CYII='; 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 | 431 | 432 | 433 |
434 | ); 435 | } --------------------------------------------------------------------------------