├── .gitignore ├── README.md ├── examples ├── .gitignore ├── README.md ├── index.html ├── package.json ├── public │ ├── favicon.ico │ ├── images │ │ └── wawasensei-white.png │ ├── models │ │ └── sword.glb │ ├── textures │ │ ├── Rotated │ │ │ ├── flame_05_rotated.png │ │ │ ├── flame_06_rotated.png │ │ │ ├── muzzle_01_rotated.png │ │ │ ├── muzzle_02_rotated.png │ │ │ ├── muzzle_03_rotated.png │ │ │ ├── muzzle_04_rotated.png │ │ │ ├── muzzle_05_rotated.png │ │ │ ├── spark_05_rotated.png │ │ │ ├── spark_06_rotated.png │ │ │ ├── trace_01_rotated.png │ │ │ ├── trace_02_rotated.png │ │ │ ├── trace_03_rotated.png │ │ │ ├── trace_04_rotated.png │ │ │ ├── trace_05_rotated.png │ │ │ ├── trace_06_rotated.png │ │ │ └── trace_07_rotated.png │ │ ├── circle_01.png │ │ ├── circle_02.png │ │ ├── circle_03.png │ │ ├── circle_04.png │ │ ├── circle_05.png │ │ ├── dirt_01.png │ │ ├── dirt_02.png │ │ ├── dirt_03.png │ │ ├── fire_01.png │ │ ├── fire_02.png │ │ ├── flame_01.png │ │ ├── flame_02.png │ │ ├── flame_03.png │ │ ├── flame_04.png │ │ ├── flame_05.png │ │ ├── flame_06.png │ │ ├── flare_01.png │ │ ├── light_01.png │ │ ├── light_02.png │ │ ├── light_03.png │ │ ├── magic_01.png │ │ ├── magic_02.png │ │ ├── magic_03.png │ │ ├── magic_04.png │ │ ├── magic_05.png │ │ ├── muzzle_01.png │ │ ├── muzzle_02.png │ │ ├── muzzle_03.png │ │ ├── muzzle_04.png │ │ ├── muzzle_05.png │ │ ├── scorch_01.png │ │ ├── scorch_02.png │ │ ├── scorch_03.png │ │ ├── scratch_01.png │ │ ├── slash_01.png │ │ ├── slash_02.png │ │ ├── slash_03.png │ │ ├── slash_04.png │ │ ├── smoke_01.png │ │ ├── smoke_02.png │ │ ├── smoke_03.png │ │ ├── smoke_04.png │ │ ├── smoke_05.png │ │ ├── smoke_06.png │ │ ├── smoke_07.png │ │ ├── smoke_08.png │ │ ├── smoke_09.png │ │ ├── smoke_10.png │ │ ├── spark_01.png │ │ ├── spark_02.png │ │ ├── spark_03.png │ │ ├── spark_04.png │ │ ├── spark_05.png │ │ ├── spark_06.png │ │ ├── spark_07.png │ │ ├── star_01.png │ │ ├── star_02.png │ │ ├── star_03.png │ │ ├── star_04.png │ │ ├── star_05.png │ │ ├── star_06.png │ │ ├── star_07.png │ │ ├── star_08.png │ │ ├── star_09.png │ │ ├── symbol_01.png │ │ ├── symbol_02.png │ │ ├── trace_01.png │ │ ├── trace_02.png │ │ ├── trace_03.png │ │ ├── trace_04.png │ │ ├── trace_05.png │ │ ├── trace_06.png │ │ ├── trace_07.png │ │ ├── twirl_01.png │ │ ├── twirl_02.png │ │ ├── twirl_03.png │ │ ├── window_01.png │ │ ├── window_02.png │ │ ├── window_03.png │ │ └── window_04.png │ └── vite.svg ├── src │ ├── App.jsx │ ├── assets │ │ └── react.svg │ ├── components │ │ ├── Experience.jsx │ │ ├── GradientSky.jsx │ │ ├── UI.jsx │ │ └── demo-components │ │ │ ├── AlphaMap.jsx │ │ │ ├── Basic.jsx │ │ │ ├── CustomGeometry.jsx │ │ │ ├── Emitter.jsx │ │ │ ├── MultipleEmitters.jsx │ │ │ ├── Reverse.jsx │ │ │ └── StretchedBillboard.jsx │ ├── index.css │ └── main.jsx ├── vite.config.js └── yarn.lock └── packages ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── vite.svg ├── src ├── App.tsx ├── assets │ └── react.svg ├── components │ ├── Experience.tsx │ └── vfxs │ │ ├── VFXBuilder.tsx │ │ ├── VFXEmitter.tsx │ │ ├── VFXParticles.tsx │ │ ├── VFXStore.ts │ │ ├── easings.ts │ │ └── types.ts ├── index.css ├── index.ts ├── main.tsx └── types.d.ts ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wawa VFX 2 | 3 | A simple and easy-to-use library for creating visual effects with Three.js and React Three Fiber. 4 | 5 | [Live demo](https://wawa-vfx.wawasensei.dev/) - [Fireworks demo](https://fireworks.wawasensei.dev/) - [Wizard Game demo](https://wizard.wawasensei.dev/) 6 | 7 | > This powerful VFX particle system was developed as part of the comprehensive **VFX & Advanced Rendering Chapter** in my [React Three Fiber: The Ultimate Guide to 3D Web Development](https://lessons.wawasensei.dev/courses/react-three-fiber/) course. 8 | > 9 | > In the course, we break down every aspect of this system, explaining the mathematics, optimization techniques, and design patterns that make it work. 10 | 11 | https://github.com/user-attachments/assets/4c00c0e1-ae4f-4501-a648-0811c7a4ca7d 12 | 13 | ## Install 14 | 15 | ```bash 16 | npm install wawa-vfx 17 | ``` 18 | 19 | or 20 | 21 | ```bash 22 | yarn add wawa-vfx 23 | ``` 24 | 25 | ## Usage 26 | 27 | Wawa VFX makes it easy to create particle effects in your React Three Fiber projects. The library uses a two-component system: 28 | 29 | - `VFXParticles`: Defines the particle system and its rendering properties 30 | - `VFXEmitter`: Controls how and when particles are emitted into the scene 31 | 32 | ### Basic Example 33 | 34 | ```jsx 35 | import { VFXEmitter, VFXParticles } from "wawa-vfx"; 36 | 37 | const MyEffect = () => { 38 | return ( 39 | <> 40 | {/* Step 1: Define your particle system */} 41 | 54 | 55 | {/* Step 2: Define your emitter */} 56 | 100 | 101 | ); 102 | }; 103 | ``` 104 | 105 | ### Key Features 106 | 107 | - **Easy to Use**: Create complex particle effects with minimal code 108 | - **Flexible Customization**: Extensive settings for fine-tuning visual effects 109 | - **Performance Optimized**: Uses instanced rendering for efficient particle systems 110 | - **Integrated with React Three Fiber**: Works seamlessly with your existing project 111 | 112 | ### New features ✨ 113 | 114 | ##### VFXParticles : 115 | 116 | ###### 🔷 Explicit Appearance Mode 117 | 118 | You can now explicitly define the default appearance to be plane (default) or circular 119 | 120 | ###### 🔷 Stretch Billboard renderMode 121 | 122 | A new renderMode: "stretchBillboard" option has been added. This renders particles as billboards that stretch along their velocity direction, ideal for effects like trails, speed lines, or fire streaks. 123 | 124 | ###### 🔷 Particle Easings 125 | 126 | You can now apply easing function enabling smooth transitions over the particle’s lifetime. Easing options includes 42 functions from which you can choose using typescript's autocomplete feature. 127 | 128 | ##### VFXEmitter : 129 | 130 | 🔷 useLocalDirection Setting 131 | 132 | A new boolean setting: 133 | 134 | ```jsx 135 | useLocalDirection?: boolean; // true | false 136 | ``` 137 | 138 | When true, the emitter will emit particles using its local axes (transformed by its world rotation). When false, particles are emitted using the world axes, ignoring the emitter’s rotation. 139 | 140 | ### VFXParticles Properties 141 | 142 | | Property | Type | Description | 143 | | ---------- | ------------- | ------------------------------------------------ | 144 | | `name` | string | Unique identifier for this particle system | 145 | | `settings` | object | Configuration options for particles | 146 | | `alphaMap` | THREE.Texture | Optional texture for particle alpha/transparency | 147 | | `geometry` | ReactElement | Optional custom geometry for particles | 148 | 149 | ### VFXEmitter Properties 150 | 151 | | Property | Type | Description | 152 | | ---------- | ------- | ------------------------------------------- | 153 | | `emitter` | string | Name of the target particle system | 154 | | `settings` | object | Configuration options for emission behavior | 155 | | `debug` | boolean | Show controls to adjust the settings | 156 | 157 | ## Advanced Usage 158 | 159 | Check out the `examples` directory for more complex implementations and techniques. 160 | 161 | ## Roadmap 162 | 163 | Do you want to contribute to the project? Here are some ideas for future features: 164 | 165 | - [ ] WebGPU/TSL `VFXParticles`/`VFXParticlesMaterial` versions 166 | - [ ] Performance optimizations (Points / Sprites) 167 | - [ ] More controls on the `VFXEmitter` component (`emit`, `emitStart`, `emitStop`, `emitByDistance`) 168 | - [ ] More customization options for the particle system 169 | - [x] More rendering modes (`stretched billboard`) 170 | - [ ] More examples and documentation 171 | 172 | Feel free to open an issue or PR if you have any suggestions or improvements! 173 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # r3f-vite-starter 2 | A boilerplate to build R3F projects 3 | 4 | ``` 5 | yarn 6 | yarn dev 7 | ``` 8 | 9 | 10 | ![image](https://user-images.githubusercontent.com/6551176/221732091-23ee52cb-4150-42fa-b998-43628d7a6b0d.png) 11 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | VFX Engine Demo - Wawa Sensei 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "r3f-vite-starter", 3 | "private": true, 4 | "version": "1.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@react-three/drei": "^10.0.3", 13 | "@react-three/fiber": "^9.0.4", 14 | "@react-three/postprocessing": "^3.0.4", 15 | "@tailwindcss/vite": "^4.0.9", 16 | "leva": "^0.10.0", 17 | "react": "^19.0.0", 18 | "react-dom": "^19.0.0", 19 | "tailwindcss": "^4.0.9", 20 | "three": "^0.174.0", 21 | "wawa-vfx": "^1.0.16" 22 | }, 23 | "devDependencies": { 24 | "@types/react": "^19.0.10", 25 | "@types/react-dom": "^19.0.4", 26 | "@vitejs/plugin-react": "^4.3.4", 27 | "globals": "^15.15.0", 28 | "vite": "^6.2.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/favicon.ico -------------------------------------------------------------------------------- /examples/public/images/wawasensei-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/images/wawasensei-white.png -------------------------------------------------------------------------------- /examples/public/models/sword.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/models/sword.glb -------------------------------------------------------------------------------- /examples/public/textures/Rotated/flame_05_rotated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/Rotated/flame_05_rotated.png -------------------------------------------------------------------------------- /examples/public/textures/Rotated/flame_06_rotated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/Rotated/flame_06_rotated.png -------------------------------------------------------------------------------- /examples/public/textures/Rotated/muzzle_01_rotated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/Rotated/muzzle_01_rotated.png -------------------------------------------------------------------------------- /examples/public/textures/Rotated/muzzle_02_rotated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/Rotated/muzzle_02_rotated.png -------------------------------------------------------------------------------- /examples/public/textures/Rotated/muzzle_03_rotated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/Rotated/muzzle_03_rotated.png -------------------------------------------------------------------------------- /examples/public/textures/Rotated/muzzle_04_rotated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/Rotated/muzzle_04_rotated.png -------------------------------------------------------------------------------- /examples/public/textures/Rotated/muzzle_05_rotated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/Rotated/muzzle_05_rotated.png -------------------------------------------------------------------------------- /examples/public/textures/Rotated/spark_05_rotated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/Rotated/spark_05_rotated.png -------------------------------------------------------------------------------- /examples/public/textures/Rotated/spark_06_rotated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/Rotated/spark_06_rotated.png -------------------------------------------------------------------------------- /examples/public/textures/Rotated/trace_01_rotated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/Rotated/trace_01_rotated.png -------------------------------------------------------------------------------- /examples/public/textures/Rotated/trace_02_rotated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/Rotated/trace_02_rotated.png -------------------------------------------------------------------------------- /examples/public/textures/Rotated/trace_03_rotated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/Rotated/trace_03_rotated.png -------------------------------------------------------------------------------- /examples/public/textures/Rotated/trace_04_rotated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/Rotated/trace_04_rotated.png -------------------------------------------------------------------------------- /examples/public/textures/Rotated/trace_05_rotated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/Rotated/trace_05_rotated.png -------------------------------------------------------------------------------- /examples/public/textures/Rotated/trace_06_rotated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/Rotated/trace_06_rotated.png -------------------------------------------------------------------------------- /examples/public/textures/Rotated/trace_07_rotated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/Rotated/trace_07_rotated.png -------------------------------------------------------------------------------- /examples/public/textures/circle_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/circle_01.png -------------------------------------------------------------------------------- /examples/public/textures/circle_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/circle_02.png -------------------------------------------------------------------------------- /examples/public/textures/circle_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/circle_03.png -------------------------------------------------------------------------------- /examples/public/textures/circle_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/circle_04.png -------------------------------------------------------------------------------- /examples/public/textures/circle_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/circle_05.png -------------------------------------------------------------------------------- /examples/public/textures/dirt_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/dirt_01.png -------------------------------------------------------------------------------- /examples/public/textures/dirt_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/dirt_02.png -------------------------------------------------------------------------------- /examples/public/textures/dirt_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/dirt_03.png -------------------------------------------------------------------------------- /examples/public/textures/fire_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/fire_01.png -------------------------------------------------------------------------------- /examples/public/textures/fire_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/fire_02.png -------------------------------------------------------------------------------- /examples/public/textures/flame_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/flame_01.png -------------------------------------------------------------------------------- /examples/public/textures/flame_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/flame_02.png -------------------------------------------------------------------------------- /examples/public/textures/flame_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/flame_03.png -------------------------------------------------------------------------------- /examples/public/textures/flame_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/flame_04.png -------------------------------------------------------------------------------- /examples/public/textures/flame_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/flame_05.png -------------------------------------------------------------------------------- /examples/public/textures/flame_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/flame_06.png -------------------------------------------------------------------------------- /examples/public/textures/flare_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/flare_01.png -------------------------------------------------------------------------------- /examples/public/textures/light_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/light_01.png -------------------------------------------------------------------------------- /examples/public/textures/light_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/light_02.png -------------------------------------------------------------------------------- /examples/public/textures/light_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/light_03.png -------------------------------------------------------------------------------- /examples/public/textures/magic_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/magic_01.png -------------------------------------------------------------------------------- /examples/public/textures/magic_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/magic_02.png -------------------------------------------------------------------------------- /examples/public/textures/magic_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/magic_03.png -------------------------------------------------------------------------------- /examples/public/textures/magic_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/magic_04.png -------------------------------------------------------------------------------- /examples/public/textures/magic_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/magic_05.png -------------------------------------------------------------------------------- /examples/public/textures/muzzle_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/muzzle_01.png -------------------------------------------------------------------------------- /examples/public/textures/muzzle_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/muzzle_02.png -------------------------------------------------------------------------------- /examples/public/textures/muzzle_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/muzzle_03.png -------------------------------------------------------------------------------- /examples/public/textures/muzzle_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/muzzle_04.png -------------------------------------------------------------------------------- /examples/public/textures/muzzle_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/muzzle_05.png -------------------------------------------------------------------------------- /examples/public/textures/scorch_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/scorch_01.png -------------------------------------------------------------------------------- /examples/public/textures/scorch_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/scorch_02.png -------------------------------------------------------------------------------- /examples/public/textures/scorch_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/scorch_03.png -------------------------------------------------------------------------------- /examples/public/textures/scratch_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/scratch_01.png -------------------------------------------------------------------------------- /examples/public/textures/slash_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/slash_01.png -------------------------------------------------------------------------------- /examples/public/textures/slash_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/slash_02.png -------------------------------------------------------------------------------- /examples/public/textures/slash_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/slash_03.png -------------------------------------------------------------------------------- /examples/public/textures/slash_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/slash_04.png -------------------------------------------------------------------------------- /examples/public/textures/smoke_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/smoke_01.png -------------------------------------------------------------------------------- /examples/public/textures/smoke_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/smoke_02.png -------------------------------------------------------------------------------- /examples/public/textures/smoke_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/smoke_03.png -------------------------------------------------------------------------------- /examples/public/textures/smoke_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/smoke_04.png -------------------------------------------------------------------------------- /examples/public/textures/smoke_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/smoke_05.png -------------------------------------------------------------------------------- /examples/public/textures/smoke_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/smoke_06.png -------------------------------------------------------------------------------- /examples/public/textures/smoke_07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/smoke_07.png -------------------------------------------------------------------------------- /examples/public/textures/smoke_08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/smoke_08.png -------------------------------------------------------------------------------- /examples/public/textures/smoke_09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/smoke_09.png -------------------------------------------------------------------------------- /examples/public/textures/smoke_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/smoke_10.png -------------------------------------------------------------------------------- /examples/public/textures/spark_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/spark_01.png -------------------------------------------------------------------------------- /examples/public/textures/spark_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/spark_02.png -------------------------------------------------------------------------------- /examples/public/textures/spark_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/spark_03.png -------------------------------------------------------------------------------- /examples/public/textures/spark_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/spark_04.png -------------------------------------------------------------------------------- /examples/public/textures/spark_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/spark_05.png -------------------------------------------------------------------------------- /examples/public/textures/spark_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/spark_06.png -------------------------------------------------------------------------------- /examples/public/textures/spark_07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/spark_07.png -------------------------------------------------------------------------------- /examples/public/textures/star_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/star_01.png -------------------------------------------------------------------------------- /examples/public/textures/star_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/star_02.png -------------------------------------------------------------------------------- /examples/public/textures/star_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/star_03.png -------------------------------------------------------------------------------- /examples/public/textures/star_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/star_04.png -------------------------------------------------------------------------------- /examples/public/textures/star_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/star_05.png -------------------------------------------------------------------------------- /examples/public/textures/star_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/star_06.png -------------------------------------------------------------------------------- /examples/public/textures/star_07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/star_07.png -------------------------------------------------------------------------------- /examples/public/textures/star_08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/star_08.png -------------------------------------------------------------------------------- /examples/public/textures/star_09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/star_09.png -------------------------------------------------------------------------------- /examples/public/textures/symbol_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/symbol_01.png -------------------------------------------------------------------------------- /examples/public/textures/symbol_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/symbol_02.png -------------------------------------------------------------------------------- /examples/public/textures/trace_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/trace_01.png -------------------------------------------------------------------------------- /examples/public/textures/trace_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/trace_02.png -------------------------------------------------------------------------------- /examples/public/textures/trace_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/trace_03.png -------------------------------------------------------------------------------- /examples/public/textures/trace_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/trace_04.png -------------------------------------------------------------------------------- /examples/public/textures/trace_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/trace_05.png -------------------------------------------------------------------------------- /examples/public/textures/trace_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/trace_06.png -------------------------------------------------------------------------------- /examples/public/textures/trace_07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/trace_07.png -------------------------------------------------------------------------------- /examples/public/textures/twirl_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/twirl_01.png -------------------------------------------------------------------------------- /examples/public/textures/twirl_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/twirl_02.png -------------------------------------------------------------------------------- /examples/public/textures/twirl_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/twirl_03.png -------------------------------------------------------------------------------- /examples/public/textures/window_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/window_01.png -------------------------------------------------------------------------------- /examples/public/textures/window_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/window_02.png -------------------------------------------------------------------------------- /examples/public/textures/window_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/window_03.png -------------------------------------------------------------------------------- /examples/public/textures/window_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/examples/public/textures/window_04.png -------------------------------------------------------------------------------- /examples/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { Loader } from "@react-three/drei"; 2 | import { Canvas } from "@react-three/fiber"; 3 | import { Leva } from "leva"; 4 | import { Suspense } from "react"; 5 | import { Experience } from "./components/Experience"; 6 | import { UI } from "./components/UI"; 7 | 8 | function App() { 9 | return ( 10 | <> 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | 24 | export default App; 25 | -------------------------------------------------------------------------------- /examples/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/src/components/Experience.jsx: -------------------------------------------------------------------------------- 1 | import { CameraControls, Stars, Stats } from "@react-three/drei"; 2 | import { Bloom, EffectComposer } from "@react-three/postprocessing"; 3 | import { useEffect, useRef, useState } from "react"; 4 | import { AlphaMap } from "./demo-components/AlphaMap"; 5 | import { Basic } from "./demo-components/Basic"; 6 | import { CustomGeometry } from "./demo-components/CustomGeometry"; 7 | import { Emitter } from "./demo-components/Emitter"; 8 | import { MultipleEmitters } from "./demo-components/MultipleEmitters"; 9 | import { Reverse } from "./demo-components/Reverse"; 10 | import { StretchedBillboard } from "./demo-components/StretchedBillboard"; 11 | import { GradientSky } from "./GradientSky"; 12 | 13 | export const Experience = () => { 14 | const controls = useRef(); 15 | 16 | useEffect(() => { 17 | controls.current.setLookAt(0, 15, 10, 0, 25, 0); 18 | controls.current.setLookAt(12, 8, 26, 0, 0, 0, true); 19 | }, []); 20 | 21 | const [currentHash, setCurrentHash] = useState( 22 | window.location.hash.replace("#", "") 23 | ); 24 | 25 | useEffect(() => { 26 | // When hash in the url changes, update the href state 27 | const handleHashChange = () => { 28 | setCurrentHash(window.location.hash.replace("#", "")); 29 | }; 30 | window.addEventListener("hashchange", handleHashChange); 31 | 32 | // Cleanup the event listener on component unmount 33 | return () => { 34 | window.removeEventListener("hashchange", handleHashChange); 35 | }; 36 | }, []); 37 | 38 | return ( 39 | <> 40 | 41 | 42 | 47 | 48 | 49 | 50 | 51 | {currentHash === "" && } 52 | {currentHash === "emitter" && } 53 | {currentHash === "reverse" && } 54 | {currentHash === "multiple-emitters" && } 55 | {currentHash === "alpha-map" && } 56 | {currentHash === "custom-geometry" && } 57 | {currentHash === "stretched-billboard" && } 58 | 59 | 60 | 61 | 62 | 63 | ); 64 | }; 65 | -------------------------------------------------------------------------------- /examples/src/components/GradientSky.jsx: -------------------------------------------------------------------------------- 1 | import { shaderMaterial } from "@react-three/drei"; 2 | import { extend } from "@react-three/fiber"; 3 | import { useControls } from "leva"; 4 | import { BackSide, Color } from "three"; 5 | import { degToRad } from "three/src/math/MathUtils.js"; 6 | 7 | export const GradientSky = () => { 8 | const { colorBottom, colorTop, colorMiddle, blendMiddle, blendIntensity } = 9 | useControls("Sky 🌄", { 10 | colorTop: "#000000", 11 | colorMiddle: "#221341", 12 | colorBottom: "#0a0a0a", 13 | blendMiddle: { 14 | value: 0.24, 15 | min: 0, 16 | max: 1, 17 | step: 0.01, 18 | }, 19 | blendIntensity: { 20 | value: 0.29, 21 | min: 0, 22 | max: 1, 23 | step: 0.01, 24 | }, 25 | }); 26 | 27 | return ( 28 | 29 | 30 | 40 | 41 | ); 42 | }; 43 | 44 | const GradientMaterial = shaderMaterial( 45 | { 46 | colorTop: new Color("white"), 47 | colorBottom: new Color("skyblue"), 48 | colorMiddle: new Color("pink"), 49 | blendMiddle: 0.2, 50 | blendIntensity: 1, 51 | }, 52 | /* glsl */ ` 53 | varying vec2 vUv; 54 | void main() { 55 | vUv = uv; 56 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 57 | } 58 | `, 59 | /* glsl */ ` 60 | uniform vec3 colorTop; 61 | uniform vec3 colorBottom; 62 | uniform vec3 colorMiddle; 63 | uniform float blendMiddle; 64 | uniform float blendIntensity; 65 | varying vec2 vUv; 66 | void main() { 67 | vec3 mixedTop = mix(colorMiddle, colorTop, smoothstep(0.498, 0.502, vUv.y)); 68 | vec3 mixedBottom = mix(colorMiddle, colorBottom, smoothstep(0.502, 0.498, vUv.y)); 69 | 70 | vec3 mixedColor = mix(colorBottom, colorTop, smoothstep(0.45, 0.55, vUv.y)); 71 | float blendMiddle = smoothstep(0.5-blendMiddle, 0.5, vUv.y) * smoothstep(0.5 + blendMiddle, 0.5, vUv.y) * blendIntensity; 72 | vec3 finalColor = mix(mixedColor, colorMiddle, blendMiddle); 73 | gl_FragColor = vec4(finalColor, 1.0); 74 | 75 | } 76 | ` 77 | ); 78 | 79 | extend({ GradientMaterial }); 80 | -------------------------------------------------------------------------------- /examples/src/components/UI.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | const examples = [ 4 | { 5 | label: "Simple particle emissions", 6 | href: "#", 7 | }, 8 | { 9 | label: "Reverse emissions", 10 | href: "#reverse", 11 | }, 12 | { 13 | label: "Follow emitter", 14 | href: "#emitter", 15 | }, 16 | { 17 | label: "Multiple emitters", 18 | href: "#multiple-emitters", 19 | }, 20 | { 21 | label: "Alpha map", 22 | href: "#alpha-map", 23 | }, 24 | { 25 | label: "Stretched billboard", 26 | href: "#stretched-billboard", 27 | }, 28 | { 29 | label: "Custom geometry", 30 | href: "#custom-geometry", 31 | }, 32 | ]; 33 | 34 | export const UI = () => { 35 | const [currentHash, setCurrentHash] = useState( 36 | window.location.hash.replace("#", "") 37 | ); 38 | 39 | useEffect(() => { 40 | // When hash in the url changes, update the href state 41 | const handleHashChange = () => { 42 | setCurrentHash(window.location.hash.replace("#", "")); 43 | }; 44 | window.addEventListener("hashchange", handleHashChange); 45 | 46 | // Cleanup the event listener on component unmount 47 | return () => { 48 | window.removeEventListener("hashchange", handleHashChange); 49 | }; 50 | }, []); 51 | 52 | return ( 53 |
54 |
55 | 60 | Wawa Sensei logo 65 | 66 |
67 | 76 |
77 | {examples.map((example, index) => ( 78 | 87 | {example.label} 88 | 89 | ))} 90 |
91 |
92 | ); 93 | }; 94 | -------------------------------------------------------------------------------- /examples/src/components/demo-components/AlphaMap.jsx: -------------------------------------------------------------------------------- 1 | import { useTexture } from "@react-three/drei"; 2 | import { VFXEmitter, VFXParticles } from "wawa-vfx"; 3 | 4 | export const AlphaMap = () => { 5 | const heartTexture = useTexture("/textures/symbol_01.png"); 6 | const starTexture = useTexture("/textures/symbol_02.png"); 7 | return ( 8 | <> 9 | 21 | 33 | 53 | 73 | 74 | ); 75 | }; 76 | 77 | useTexture.preload("/textures/symbol_01.png"); 78 | useTexture.preload("/textures/symbol_02.png"); 79 | -------------------------------------------------------------------------------- /examples/src/components/demo-components/Basic.jsx: -------------------------------------------------------------------------------- 1 | import { VFXEmitter, VFXParticles } from "wawa-vfx"; 2 | 3 | export const Basic = () => { 4 | return ( 5 | <> 6 | 17 | 18 | 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /examples/src/components/demo-components/CustomGeometry.jsx: -------------------------------------------------------------------------------- 1 | import { useGLTF } from "@react-three/drei"; 2 | import { useEffect, useState } from "react"; 3 | import { VFXEmitter, VFXParticles } from "wawa-vfx"; 4 | 5 | export const CustomGeometry = () => { 6 | const { nodes } = useGLTF("/models/sword.glb"); 7 | const [burst, setBurst] = useState(true); 8 | 9 | useEffect(() => { 10 | const timeout = setTimeout(() => { 11 | setBurst((prev) => !prev); 12 | }, 2000); 13 | return () => clearTimeout(timeout); 14 | }, [burst]); 15 | return ( 16 | <> 17 | } 20 | settings={{ 21 | nbParticles: 1000, 22 | gravity: [0, 0, 0], 23 | fadeSize: [0.3, 0.95], 24 | renderMode: "mesh", 25 | intensity: 2, 26 | }} 27 | /> 28 | 29 | {burst && ( 30 | 49 | )} 50 | 51 | ); 52 | }; 53 | useGLTF.preload("/models/sword.glb"); 54 | -------------------------------------------------------------------------------- /examples/src/components/demo-components/Emitter.jsx: -------------------------------------------------------------------------------- 1 | import { useFrame } from "@react-three/fiber"; 2 | import { useRef } from "react"; 3 | import { Vector3 } from "three"; 4 | import { VFXEmitter, VFXParticles } from "wawa-vfx"; 5 | 6 | const tmpVector = new Vector3(); 7 | 8 | export const Emitter = () => { 9 | const emitter = useRef(); 10 | 11 | useFrame(({ clock }, delta) => { 12 | if (emitter.current) { 13 | const time = clock.getElapsedTime(); 14 | tmpVector.set( 15 | Math.sin(time) * 4, 16 | Math.cos(time * 2) * 6, 17 | Math.sin(time * 4) * 8 18 | ); 19 | emitter.current.position.lerp(tmpVector, delta * 2); 20 | } 21 | }); 22 | return ( 23 | <> 24 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 62 | 63 | 64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /examples/src/components/demo-components/MultipleEmitters.jsx: -------------------------------------------------------------------------------- 1 | import { useFrame } from "@react-three/fiber"; 2 | import { useRef } from "react"; 3 | import { Vector3 } from "three"; 4 | import { VFXEmitter, VFXParticles } from "wawa-vfx"; 5 | 6 | const tmpVector = new Vector3(); 7 | 8 | export const MultipleEmitters = () => { 9 | const emitter = useRef(); 10 | const emitter2 = useRef(); 11 | const emitter3 = useRef(); 12 | const emitter4 = useRef(); 13 | 14 | useFrame(({ clock }, delta) => { 15 | if (emitter.current) { 16 | const distance = 6; 17 | const time = clock.getElapsedTime(); 18 | tmpVector.set( 19 | Math.sin(time * 8) * distance, 20 | Math.sin(time * 2) * 8, 21 | Math.cos(time * 8) * distance 22 | ); 23 | emitter.current.position.lerp(tmpVector, delta * 2); 24 | tmpVector.set( 25 | Math.cos(time * 8) * distance, 26 | Math.sin(time * 2 + 50) * 8, 27 | Math.sin(time * 8) * distance 28 | ); 29 | emitter2.current.position.lerp(tmpVector, delta * 2); 30 | 31 | tmpVector.set( 32 | Math.sin(time * 8) * distance, 33 | Math.sin(time * 2 + 100) * 8, 34 | Math.cos(time * 8) * distance 35 | ); 36 | emitter3.current.position.lerp(tmpVector, delta * 2); 37 | 38 | tmpVector.set( 39 | Math.cos(time * 8) * distance, 40 | Math.sin(time * 2 + 150) * 8, 41 | Math.sin(time * 8) * distance 42 | ); 43 | emitter4.current.position.lerp(tmpVector, delta * 2); 44 | } 45 | }); 46 | 47 | const sharedSettings = { 48 | loop: true, 49 | duration: 1, 50 | nbParticles: 2000, 51 | startPositionMin: [0, 0, 0], 52 | startPositionMax: [0, 0, 0], 53 | directionMin: [-0.5, 0, -0.5], 54 | directionMax: [0.5, 0.5, 0.5], 55 | size: [0.01, 0.15], 56 | speed: [1, 3], 57 | }; 58 | 59 | return ( 60 | <> 61 | 72 | 73 | 74 | 75 | 76 | 81 | 82 | 90 | 91 | 92 | 93 | 94 | 99 | 100 | 108 | 109 | 110 | 111 | 112 | 117 | 118 | 126 | 127 | 128 | 129 | 130 | 135 | 136 | 144 | 145 | 146 | ); 147 | }; 148 | -------------------------------------------------------------------------------- /examples/src/components/demo-components/Reverse.jsx: -------------------------------------------------------------------------------- 1 | import { AppearanceMode, VFXEmitter, VFXParticles } from "wawa-vfx"; 2 | 3 | export const Reverse = () => { 4 | return ( 5 | <> 6 | 18 | 19 | 34 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /examples/src/components/demo-components/StretchedBillboard.jsx: -------------------------------------------------------------------------------- 1 | import { useFrame } from "@react-three/fiber"; 2 | import { useRef } from "react"; 3 | import { Vector3 } from "three"; 4 | import { AppearanceMode, RenderMode, VFXEmitter, VFXParticles } from "wawa-vfx"; 5 | 6 | const tmpVector = new Vector3(); 7 | 8 | export const StretchedBillboard = () => { 9 | const emitterBlue = useRef(null); 10 | const groupRef = useRef(null); 11 | 12 | useFrame((_, delta) => { 13 | if (emitterBlue.current && groupRef.current) { 14 | groupRef.current.rotation.z += delta * 10; 15 | } 16 | }); 17 | return ( 18 | <> 19 | 33 | 34 | 35 | 61 | 62 | 63 | 64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /examples/src/index.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"); 2 | @import "tailwindcss"; 3 | 4 | @theme { 5 | --font-sans: "Inter", "sans-serif"; 6 | } 7 | 8 | #root { 9 | width: 100vw; 10 | height: 100vh; 11 | } 12 | 13 | body { 14 | margin: 0; 15 | } 16 | 17 | .animate-fade-in-up { 18 | animation: fadeInUp 1s ease-out forwards; 19 | } 20 | 21 | .animate-fade-in-down { 22 | animation: fadeInDown 1s ease-out forwards; 23 | } 24 | 25 | .animation-delay-200 { 26 | animation-delay: 200ms; 27 | } 28 | 29 | .animation-delay-500 { 30 | animation-delay: 500ms; 31 | } 32 | 33 | .animation-delay-1000 { 34 | animation-delay: 1000ms; 35 | } 36 | 37 | .animation-delay-1500 { 38 | animation-delay: 1500ms; 39 | } 40 | 41 | @keyframes fadeInUp { 42 | from { 43 | opacity: 0; 44 | transform: translateY(200px); 45 | } 46 | to { 47 | opacity: 1; 48 | transform: translateY(0); 49 | } 50 | } 51 | 52 | @keyframes fadeInDown { 53 | from { 54 | opacity: 0; 55 | transform: translateY(-200px); 56 | } 57 | to { 58 | opacity: 1; 59 | transform: translateX(0); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /examples/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /examples/vite.config.js: -------------------------------------------------------------------------------- 1 | import tailwindcss from "@tailwindcss/vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import { defineConfig } from "vite"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react(), tailwindcss()], 8 | }); 9 | -------------------------------------------------------------------------------- /packages/README.md: -------------------------------------------------------------------------------- 1 | # Wawa VFX 2 | 3 | A simple and easy-to-use library for creating visual effects with Three.js and React Three Fiber. 4 | 5 | [Live demo](https://wawa-vfx.wawasensei.dev/) - [Fireworks demo](https://fireworks.wawasensei.dev/) - [Wizard Game demo](https://wizard.wawasensei.dev/) 6 | 7 | > This powerful VFX particle system was developed as part of the comprehensive **VFX & Advanced Rendering Chapter** in my [React Three Fiber: The Ultimate Guide to 3D Web Development](https://lessons.wawasensei.dev/courses/react-three-fiber/) course. 8 | > 9 | > In the course, we break down every aspect of this system, explaining the mathematics, optimization techniques, and design patterns that make it work. 10 | 11 | https://github.com/user-attachments/assets/4c00c0e1-ae4f-4501-a648-0811c7a4ca7d 12 | 13 | ## Install 14 | 15 | ```bash 16 | npm install wawa-vfx 17 | ``` 18 | 19 | or 20 | 21 | ```bash 22 | yarn add wawa-vfx 23 | ``` 24 | 25 | ## Usage 26 | 27 | Wawa VFX makes it easy to create particle effects in your React Three Fiber projects. The library uses a two-component system: 28 | 29 | - `VFXParticles`: Defines the particle system and its rendering properties 30 | - `VFXEmitter`: Controls how and when particles are emitted into the scene 31 | 32 | ### Basic Example 33 | 34 | ```jsx 35 | import { VFXEmitter, VFXParticles } from "wawa-vfx"; 36 | 37 | const MyEffect = () => { 38 | return ( 39 | <> 40 | {/* Step 1: Define your particle system */} 41 | 54 | 55 | {/* Step 2: Define your emitter */} 56 | 100 | 101 | ); 102 | }; 103 | ``` 104 | 105 | ### Key Features 106 | 107 | - **Easy to Use**: Create complex particle effects with minimal code 108 | - **Flexible Customization**: Extensive settings for fine-tuning visual effects 109 | - **Performance Optimized**: Uses instanced rendering for efficient particle systems 110 | - **Integrated with React Three Fiber**: Works seamlessly with your existing project 111 | 112 | ### New features ✨ 113 | 114 | ##### VFXParticles : 115 | 116 | ###### 🔷 Explicit Appearance Mode 117 | 118 | You can now explicitly define the default appearance to be plane (default) or circular 119 | 120 | ###### 🔷 Stretch Billboard renderMode 121 | 122 | A new renderMode: "stretchBillboard" option has been added. This renders particles as billboards that stretch along their velocity direction, ideal for effects like trails, speed lines, or fire streaks. 123 | 124 | ###### 🔷 Particle Easings 125 | 126 | You can now apply easing function enabling smooth transitions over the particle’s lifetime. Easing options includes 42 functions from which you can choose using typescript's autocomplete feature. 127 | 128 | ##### VFXEmitter : 129 | 130 | 🔷 useLocalDirection Setting 131 | 132 | A new boolean setting: 133 | 134 | ```jsx 135 | useLocalDirection?: boolean; // true | false 136 | ``` 137 | 138 | When true, the emitter will emit particles using its local axes (transformed by its world rotation). When false, particles are emitted using the world axes, ignoring the emitter’s rotation. 139 | 140 | ### VFXParticles Properties 141 | 142 | | Property | Type | Description | 143 | | ---------- | ------------- | ------------------------------------------------ | 144 | | `name` | string | Unique identifier for this particle system | 145 | | `settings` | object | Configuration options for particles | 146 | | `alphaMap` | THREE.Texture | Optional texture for particle alpha/transparency | 147 | | `geometry` | ReactElement | Optional custom geometry for particles | 148 | 149 | ### VFXEmitter Properties 150 | 151 | | Property | Type | Description | 152 | | ---------- | ------- | ------------------------------------------- | 153 | | `emitter` | string | Name of the target particle system | 154 | | `settings` | object | Configuration options for emission behavior | 155 | | `debug` | boolean | Show controls to adjust the settings | 156 | 157 | ## Advanced Usage 158 | 159 | Check out the `examples` directory for more complex implementations and techniques. 160 | 161 | ## Roadmap 162 | 163 | Do you want to contribute to the project? Here are some ideas for future features: 164 | 165 | - [ ] WebGPU/TSL `VFXParticles`/`VFXParticlesMaterial` versions 166 | - [ ] Performance optimizations (Points / Sprites) 167 | - [ ] More controls on the `VFXEmitter` component (`emit`, `emitStart`, `emitStop`, `emitByDistance`) 168 | - [ ] More customization options for the particle system 169 | - [x] More rendering modes (`stretched billboard`) 170 | - [ ] More examples and documentation 171 | 172 | Feel free to open an issue or PR if you have any suggestions or improvements! 173 | -------------------------------------------------------------------------------- /packages/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Wawa Sensei - VFX Engine 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wawa-vfx", 3 | "private": false, 4 | "version": "1.0.16", 5 | "license": "MIT", 6 | "types": "dist/index.d.ts", 7 | "main": "dist/wawa-vfx.umd.js", 8 | "module": "dist/wawa-vfx.es.js", 9 | "keywords": [ 10 | "react", 11 | "three", 12 | "react-three", 13 | "react-three-fiber", 14 | "react-three-drei", 15 | "react-three-postprocessing", 16 | "vfx", 17 | "particles", 18 | "effects" 19 | ], 20 | "description": "A simple and easy-to-use library for creating visual effects in React Three Fiber.", 21 | "exports": { 22 | "types": "./dist/index.d.ts", 23 | "import": "./dist/wawa-vfx.es.js", 24 | "require": "./dist/wawa-vfx.umd.js" 25 | }, 26 | "files": [ 27 | "dist" 28 | ], 29 | "scripts": { 30 | "dev": "vite", 31 | "build": "cp ../README.md ./ && vite build", 32 | "preview": "vite preview" 33 | }, 34 | "dependencies": { 35 | "leva": "^0.10.0", 36 | "zustand": "^5.0.3" 37 | }, 38 | "peerDependencies": { 39 | "@react-three/fiber": "^9.0.0", 40 | "react": "^19", 41 | "react-dom": "^19", 42 | "three": ">=0.159" 43 | }, 44 | "devDependencies": { 45 | "@react-three/drei": "^10.0.3", 46 | "@react-three/fiber": "^9.0.4", 47 | "@react-three/postprocessing": "^3.0.4", 48 | "react": "^19.0.0", 49 | "react-dom": "^19.0.0", 50 | "three": "^0.174.0", 51 | "@types/react": "^19.0.12", 52 | "@types/react-dom": "^19.0.4", 53 | "@types/three": "^0.174.0", 54 | "@vitejs/plugin-react": "^4.3.4", 55 | "globals": "^15.15.0", 56 | "typescript": "^5.8.2", 57 | "vite": "^6.2.0", 58 | "vite-plugin-dts": "^4.5.3" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wass08/wawa-vfx/e2abd90976d758b6899749d6e9aad8e39024d9ae/packages/public/favicon.ico -------------------------------------------------------------------------------- /packages/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Canvas } from "@react-three/fiber"; 2 | import { Experience } from "./components/Experience"; 3 | 4 | function App() { 5 | return ( 6 | 7 | 8 | 9 | 10 | ); 11 | } 12 | 13 | export default App; 14 | -------------------------------------------------------------------------------- /packages/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/src/components/Experience.tsx: -------------------------------------------------------------------------------- 1 | import { Environment, OrbitControls, Stats } from "@react-three/drei"; 2 | import { useFrame } from "@react-three/fiber"; 3 | import { Bloom, EffectComposer } from "@react-three/postprocessing"; 4 | import { button, useControls } from "leva"; 5 | import { useRef } from "react"; 6 | import { Group, Vector3 } from "three"; 7 | import VFXEmitter, { VFXEmitterRef } from "./vfxs/VFXEmitter"; 8 | import VFXParticles from "./vfxs/VFXParticles"; 9 | import { 10 | AppearanceMode, 11 | EaseFunction, 12 | easeFunctionList, 13 | RenderMode, 14 | } from "./vfxs/types"; 15 | 16 | export const Experience = () => { 17 | const { component } = useControls("Component", { 18 | component: { 19 | label: "Component", 20 | options: ["Fireworks", "BaseVFX", "StretchBillboard", "Energy"], 21 | value: "Energy", 22 | }, 23 | }); 24 | return ( 25 | <> 26 | 27 | 28 | 29 | {component === "BaseVFX" && } 30 | {component === "StretchBillboard" && } 31 | {component === "Fireworks" && } 32 | {component === "Energy" && } 33 | 34 | 35 | 36 | 37 | ); 38 | }; 39 | 40 | function Energy() { 41 | return ( 42 | <> 43 | 53 | 75 | 76 | ); 77 | } 78 | 79 | function Fireworks() { 80 | const emitter = useRef(null); 81 | 82 | const lastShotTime = useRef(0); 83 | 84 | const xTarget = useRef((Math.random() - 0.5) * 6); 85 | const yTarget = useRef(Math.random() * 2 + 1); 86 | const zTarget = useRef((Math.random() - 0.5) * 6); 87 | useFrame((_, delta) => { 88 | if (emitter.current) { 89 | lastShotTime.current += delta; 90 | if (lastShotTime.current > Math.random() * 2 + 0.5) { 91 | emitter.current.emitAtPos( 92 | new Vector3(xTarget.current, yTarget.current, zTarget.current), 93 | true 94 | ); 95 | lastShotTime.current = 0; 96 | 97 | xTarget.current = (Math.random() - 0.5) * 6; 98 | yTarget.current = Math.random() * 2 + 1; 99 | zTarget.current = (Math.random() - 0.5) * 6; 100 | } 101 | } 102 | }); 103 | 104 | return ( 105 | <> 106 | 120 | 121 | 148 | 149 | ); 150 | } 151 | 152 | function StretchBillboard() { 153 | const emitterBlue = useRef(null); 154 | const groupRef = useRef(null); 155 | 156 | const { easing, renderMode } = useControls("Emitter External Controls", { 157 | start: button(() => { 158 | emitterBlue.current?.startEmitting(); 159 | }), 160 | startWithReset: button(() => { 161 | emitterBlue.current?.startEmitting(true); 162 | }), 163 | stop: button(() => { 164 | emitterBlue.current?.stopEmitting(); 165 | }), 166 | easing: { 167 | label: "Easing", 168 | options: easeFunctionList, 169 | value: "easeLinear", 170 | }, 171 | renderMode: { 172 | label: "renderMode", 173 | options: ["mesh", "billboard", "stretchBillboard"], 174 | value: "stretchBillboard", 175 | }, 176 | }); 177 | 178 | useFrame((_, delta) => { 179 | if (emitterBlue.current && groupRef.current) { 180 | groupRef.current.rotation.z += delta * 10; 181 | } 182 | }); 183 | return ( 184 | <> 185 | 199 | 200 | 201 | 227 | 228 | 229 | 230 | ); 231 | } 232 | 233 | function BaseVFX() { 234 | const emitterBlue = useRef(null); 235 | 236 | useControls("Emitter External Controls", { 237 | start: button(() => { 238 | emitterBlue.current?.startEmitting(); 239 | }), 240 | startWithReset: button(() => { 241 | emitterBlue.current?.startEmitting(true); 242 | }), 243 | stop: button(() => { 244 | emitterBlue.current?.stopEmitting(); 245 | }), 246 | }); 247 | 248 | useFrame(({ clock }) => { 249 | const time = clock.getElapsedTime(); 250 | 251 | if (emitterBlue.current) { 252 | emitterBlue.current.position.x = Math.cos(time * 3) * 1.5; 253 | emitterBlue.current.position.y = Math.sin(time * 3) * 1.5; 254 | // emitterBlue.current.position.z = Math.cos(time * 4) * 1.5; 255 | 256 | // now you can stop or start emitting using the methods stopEmitting or startEmitting by accessing the emitterBlue ref.current object 257 | } 258 | }); 259 | return ( 260 | <> 261 | } 264 | settings={{ 265 | nbParticles: 100000, 266 | intensity: 1.5, 267 | renderMode: RenderMode.Billboard, 268 | fadeSize: [0, 1], 269 | fadeAlpha: [0.5, 0.5], 270 | gravity: [0, -10, 0], 271 | }} 272 | /> 273 | 298 | 299 | ); 300 | } 301 | -------------------------------------------------------------------------------- /packages/src/components/vfxs/VFXBuilder.tsx: -------------------------------------------------------------------------------- 1 | import { button, folder, useControls } from "leva"; 2 | import { useEffect, useRef } from "react"; 3 | import { VFXEmitterSettings } from "./VFXEmitter"; 4 | 5 | interface VFXBuilderEmitterProps { 6 | settings?: VFXEmitterSettings; 7 | onChange: (settings: VFXEmitterSettings) => void; 8 | onRestart: () => void; 9 | } 10 | 11 | export const VFXBuilderEmitter: React.FC = ({ 12 | settings, 13 | onChange, 14 | onRestart, 15 | }) => { 16 | useControls("⚙️ Emitter Settings", { 17 | Restart: button(() => onRestart()), 18 | Export: button(() => { 19 | const exportValues = JSON.stringify(vfxSettingsClone.current); 20 | console.log("📋 Values saved to clipboard: ", exportValues); 21 | navigator.clipboard.writeText(exportValues); 22 | }), 23 | }); 24 | 25 | const [{ ...vfxSettings }, set] = useControls(() => ({ 26 | "🪄 Emitter": folder({ 27 | duration: 4, 28 | delay: 0, 29 | nbParticles: 2000, 30 | spawnMode: { 31 | options: ["time", "burst"], 32 | value: "time", 33 | }, 34 | loop: false, 35 | startPositionMin: { 36 | value: [-1, -1, -1], 37 | min: -10, 38 | max: 10, 39 | step: 0.1, 40 | label: "startPositionMin", 41 | }, 42 | startPositionMax: { 43 | value: [1, 1, 1], 44 | min: -10, 45 | max: 10, 46 | step: 0.1, 47 | label: "startPositionMax", 48 | }, 49 | startRotationMin: { 50 | value: [0, 0, 0], 51 | min: -Math.PI * 2, 52 | max: Math.PI * 2, 53 | step: 0.1, 54 | label: "startRotationMin", 55 | }, 56 | startRotationMax: { 57 | value: [0, 0, 0], 58 | min: -Math.PI * 2, 59 | max: Math.PI * 2, 60 | step: 0.1, 61 | label: "startRotationMax", 62 | }, 63 | }), 64 | "✨ Particles": folder({ 65 | particlesLifetime: { 66 | value: [0.1, 1], 67 | min: 0.0, 68 | max: 10, 69 | step: 0.1, 70 | label: "lifetime", 71 | }, 72 | }), 73 | "🌪 Forces": folder({ 74 | speed: { 75 | value: [5, 20], 76 | min: -100.0, 77 | max: 100, 78 | }, 79 | directionMin: { 80 | value: [-1, -1, -1], 81 | min: -1, 82 | max: 1, 83 | step: 0.1, 84 | }, 85 | directionMax: { 86 | value: [1, 1, 1], 87 | min: -1, 88 | max: 1, 89 | step: 0.1, 90 | }, 91 | rotationSpeedMin: { 92 | value: [0, 0, 0], 93 | min: 0.0, 94 | max: 10, 95 | step: 0.1, 96 | }, 97 | rotationSpeedMax: { 98 | value: [0, 0, 0], 99 | min: 0.0, 100 | max: 10, 101 | step: 0.1, 102 | }, 103 | }), 104 | "🎨 Appearance": folder({ 105 | nbColors: { 106 | options: [1, 2, 3], 107 | }, 108 | colorStart: "#ffffff", 109 | colorEnd: "#ffffff", 110 | colorStart2: { 111 | value: "#ff0000", 112 | render: (get) => get("🎨 Appearance.nbColors") > 1, 113 | }, 114 | colorEnd2: { 115 | value: "#ffffff", 116 | render: (get) => get("🎨 Appearance.nbColors") > 1, 117 | }, 118 | colorStart3: { 119 | value: "#ff0000", 120 | render: (get) => get("🎨 Appearance.nbColors") > 2, 121 | }, 122 | colorEnd3: { 123 | value: "#ff0000", 124 | render: (get) => get("🎨 Appearance.nbColors") > 2, 125 | }, 126 | size: { 127 | value: [0.01, 1], 128 | min: 0.0, 129 | max: 5, 130 | step: 0.01, 131 | label: "size", 132 | }, 133 | }), 134 | })); 135 | 136 | const { 137 | nbColors, 138 | colorStart2, 139 | colorEnd2, 140 | colorStart3, 141 | colorEnd3, 142 | ...builtSettings 143 | } = { 144 | ...vfxSettings, 145 | colorStart: [vfxSettings.colorStart], 146 | colorEnd: [vfxSettings.colorEnd], 147 | }; 148 | 149 | vfxSettings.nbColors > 1 && 150 | builtSettings.colorStart.push(vfxSettings.colorStart2); 151 | vfxSettings.nbColors > 1 && 152 | builtSettings.colorEnd.push(vfxSettings.colorEnd2); 153 | vfxSettings.nbColors > 2 && 154 | builtSettings.colorStart.push(vfxSettings.colorStart3); 155 | vfxSettings.nbColors > 2 && 156 | builtSettings.colorEnd.push(vfxSettings.colorEnd3); 157 | 158 | // Ugly hack to get the current settings in the export button 159 | const vfxSettingsClone = useRef(builtSettings); 160 | vfxSettingsClone.current = builtSettings; 161 | 162 | useEffect(() => { 163 | if (settings) { 164 | const builderSettings: any = { 165 | ...settings, 166 | }; 167 | for (let i = 0; i < 2; i++) { 168 | if (settings.colorStart && settings.colorStart.length > i) { 169 | builderSettings[i === 0 ? "colorStart" : `colorStart${i + 1}`] = 170 | settings.colorStart[i]; 171 | builderSettings.nbColors = i + 1; 172 | } 173 | if (settings.colorEnd && settings.colorEnd.length > i) { 174 | builderSettings[i === 0 ? "colorEnd" : `colorEnd${i + 1}`] = 175 | settings.colorEnd[i]; 176 | } 177 | } 178 | 179 | set({ 180 | ...builderSettings, 181 | }); 182 | } 183 | }, [settings]); 184 | 185 | onChange(builtSettings as VFXEmitterSettings); 186 | 187 | return null; 188 | }; 189 | -------------------------------------------------------------------------------- /packages/src/components/vfxs/VFXEmitter.tsx: -------------------------------------------------------------------------------- 1 | import { useFrame } from "@react-three/fiber"; 2 | import { 3 | forwardRef, 4 | useCallback, 5 | useImperativeHandle, 6 | useMemo, 7 | useRef, 8 | useState, 9 | } from "react"; 10 | import { Euler, Quaternion, Vector3 } from "three"; 11 | import { randFloat, randInt } from "three/src/math/MathUtils.js"; 12 | import { VFXBuilderEmitter } from "./VFXBuilder"; 13 | import { useVFX } from "./VFXStore"; 14 | 15 | const worldPosition = new Vector3(); 16 | const worldQuaternion = new Quaternion(); 17 | const worldEuler = new Euler(); 18 | const worldRotation = new Euler(); 19 | const worldScale = new Vector3(); 20 | 21 | import * as THREE from "three"; 22 | 23 | export interface VFXEmitterSettings { 24 | duration?: number; 25 | nbParticles?: number; 26 | spawnMode?: "time" | "burst"; 27 | loop?: boolean; 28 | delay?: number; 29 | colorStart?: string[]; 30 | colorEnd?: string[]; 31 | particlesLifetime?: [number, number]; 32 | speed?: [number, number]; 33 | size?: [number, number]; 34 | startPositionMin?: [number, number, number]; 35 | startPositionMax?: [number, number, number]; 36 | startRotationMin?: [number, number, number]; 37 | startRotationMax?: [number, number, number]; 38 | rotationSpeedMin?: [number, number, number]; 39 | rotationSpeedMax?: [number, number, number]; 40 | directionMin?: [number, number, number]; 41 | directionMax?: [number, number, number]; 42 | } 43 | 44 | interface VFXEmitterProps { 45 | debug?: boolean; 46 | settings: VFXEmitterSettings; 47 | emitter: string; 48 | localDirection?: boolean; 49 | autoStart?: boolean; 50 | } 51 | 52 | export interface VFXEmitterRef extends THREE.Object3D { 53 | startEmitting: (reset?: boolean) => void; 54 | stopEmitting: () => void; 55 | emitAtPos: (position: THREE.Vector3 | null, reset?: boolean) => void; 56 | } 57 | 58 | const VFXEmitter = forwardRef( 59 | ( 60 | { 61 | debug, 62 | emitter, 63 | settings = {}, 64 | localDirection, 65 | autoStart = true, 66 | ...props 67 | }, 68 | forwardedRef 69 | ) => { 70 | const [ 71 | { 72 | duration = 1, 73 | nbParticles = 1000, 74 | spawnMode = "time", // time, burst 75 | loop = false, 76 | delay = 0, 77 | colorStart = ["white", "skyblue"], 78 | colorEnd = [], 79 | particlesLifetime = [0.1, 1], 80 | speed = [5, 20], 81 | size = [0.1, 1], 82 | startPositionMin = [-1, -1, -1], 83 | startPositionMax = [1, 1, 1], 84 | startRotationMin = [0, 0, 0], 85 | startRotationMax = [0, 0, 0], 86 | rotationSpeedMin = [0, 0, 0], 87 | rotationSpeedMax = [0, 0, 0], 88 | directionMin = [0, 0, 0], 89 | directionMax = [0, 0, 0], 90 | }, 91 | setSettings, 92 | ] = useState(settings); 93 | 94 | const emitted = useRef(0); 95 | const elapsedTime = useRef(0); 96 | const currentTime = useRef(0); 97 | const ref = useRef(null!); 98 | 99 | const emit = useVFX((state) => state.emit); 100 | 101 | const shouldEmitRef = useRef(autoStart); 102 | 103 | const stopEmitting = useCallback(() => { 104 | shouldEmitRef.current = false; 105 | }, []); 106 | 107 | const startEmitting = useCallback((reset: boolean = false) => { 108 | if (reset) { 109 | emitted.current = 0; 110 | elapsedTime.current = 0; 111 | } 112 | shouldEmitRef.current = true; 113 | }, []); 114 | 115 | const emitAtPos = useCallback( 116 | (pos: Vector3 | null, reset: boolean = false) => { 117 | if (spawnMode !== "burst") { 118 | console.error( 119 | "This function is meant to be used with burst spawn mode only." 120 | ); 121 | } 122 | const rate = nbParticles - emitted.current; 123 | if (reset) { 124 | emitted.current = 0; 125 | elapsedTime.current = 0; 126 | } 127 | 128 | if (pos) { 129 | ref.current.position.x = pos.x; 130 | ref.current.position.y = pos.y; 131 | ref.current.position.z = pos.z; 132 | } 133 | 134 | ref.current.updateWorldMatrix(true, true); 135 | const worldMatrix = ref.current.matrixWorld; 136 | worldMatrix.decompose(worldPosition, worldQuaternion, worldScale); 137 | worldEuler.setFromQuaternion(worldQuaternion); 138 | worldRotation.setFromQuaternion(worldQuaternion); 139 | 140 | emit(emitter, rate, () => { 141 | const randSize = randFloat(size[0], size[1]); 142 | const color = colorStart[randInt(0, colorStart.length - 1)]; 143 | return { 144 | position: [ 145 | worldPosition.x + 146 | randFloat(startPositionMin[0], startPositionMax[0]), 147 | worldPosition.y + 148 | randFloat(startPositionMin[1], startPositionMax[1]), 149 | worldPosition.z + 150 | randFloat(startPositionMin[2], startPositionMax[2]), 151 | ], 152 | direction: (() => { 153 | const dir = new Vector3( 154 | randFloat(directionMin[0], directionMax[0]), 155 | randFloat(directionMin[1], directionMax[1]), 156 | randFloat(directionMin[2], directionMax[2]) 157 | ); 158 | localDirection && dir.applyQuaternion(worldQuaternion); 159 | return [dir.x, dir.y, dir.z]; 160 | })(), 161 | scale: [randSize, randSize, randSize], 162 | rotation: [ 163 | worldRotation.x + 164 | randFloat(startRotationMin[0], startRotationMax[0]), 165 | worldRotation.y + 166 | randFloat(startRotationMin[1], startRotationMax[1]), 167 | worldRotation.z + 168 | randFloat(startRotationMin[2], startRotationMax[2]), 169 | ], 170 | rotationSpeed: [ 171 | randFloat(rotationSpeedMin[0], rotationSpeedMax[0]), 172 | randFloat(rotationSpeedMin[1], rotationSpeedMax[1]), 173 | randFloat(rotationSpeedMin[2], rotationSpeedMax[2]), 174 | ], 175 | lifetime: [ 176 | currentTime.current, 177 | randFloat(particlesLifetime[0], particlesLifetime[1]), 178 | ], 179 | colorStart: color, 180 | colorEnd: colorEnd?.length 181 | ? colorEnd[randInt(0, colorEnd.length - 1)] 182 | : color, 183 | speed: [randFloat(speed[0], speed[1])], 184 | }; 185 | }); 186 | }, 187 | [] 188 | ); 189 | 190 | useImperativeHandle(forwardedRef, () => ({ 191 | ...ref.current, 192 | stopEmitting, 193 | startEmitting, 194 | emitAtPos, 195 | })); 196 | 197 | useFrame(({ clock }, delta) => { 198 | const time = clock.getElapsedTime(); 199 | currentTime.current = time; 200 | const shouldEmit = shouldEmitRef.current; 201 | 202 | if (emitted.current < nbParticles || loop) { 203 | if (!ref || !shouldEmit) { 204 | return; 205 | } 206 | const particlesToEmit = 207 | spawnMode === "burst" 208 | ? nbParticles 209 | : Math.max( 210 | 0, 211 | Math.floor( 212 | ((elapsedTime.current - delay) / duration) * nbParticles 213 | ) 214 | ); 215 | 216 | const rate = particlesToEmit - emitted.current; 217 | if (rate > 0 && elapsedTime.current >= delay) { 218 | emit(emitter, rate, () => { 219 | ref.current.updateWorldMatrix(true, true); 220 | const worldMatrix = ref.current.matrixWorld; 221 | worldMatrix.decompose(worldPosition, worldQuaternion, worldScale); 222 | worldEuler.setFromQuaternion(worldQuaternion); 223 | worldRotation.setFromQuaternion(worldQuaternion); 224 | 225 | const randSize = randFloat(size[0], size[1]); 226 | const color = colorStart[randInt(0, colorStart.length - 1)]; 227 | return { 228 | position: [ 229 | worldPosition.x + 230 | randFloat(startPositionMin[0], startPositionMax[0]), 231 | worldPosition.y + 232 | randFloat(startPositionMin[1], startPositionMax[1]), 233 | worldPosition.z + 234 | randFloat(startPositionMin[2], startPositionMax[2]), 235 | ], 236 | direction: (() => { 237 | const dir = new Vector3( 238 | randFloat(directionMin[0], directionMax[0]), 239 | randFloat(directionMin[1], directionMax[1]), 240 | randFloat(directionMin[2], directionMax[2]) 241 | ); 242 | localDirection && dir.applyQuaternion(worldQuaternion); 243 | return [dir.x, dir.y, dir.z]; 244 | })(), 245 | scale: [randSize, randSize, randSize], 246 | rotation: [ 247 | worldRotation.x + 248 | randFloat(startRotationMin[0], startRotationMax[0]), 249 | worldRotation.y + 250 | randFloat(startRotationMin[1], startRotationMax[1]), 251 | worldRotation.z + 252 | randFloat(startRotationMin[2], startRotationMax[2]), 253 | ], 254 | rotationSpeed: [ 255 | randFloat(rotationSpeedMin[0], rotationSpeedMax[0]), 256 | randFloat(rotationSpeedMin[1], rotationSpeedMax[1]), 257 | randFloat(rotationSpeedMin[2], rotationSpeedMax[2]), 258 | ], 259 | lifetime: [ 260 | time, 261 | randFloat(particlesLifetime[0], particlesLifetime[1]), 262 | ], 263 | colorStart: color, 264 | colorEnd: colorEnd?.length 265 | ? colorEnd[randInt(0, colorEnd.length - 1)] 266 | : color, 267 | speed: [randFloat(speed[0], speed[1])], 268 | }; 269 | }); 270 | emitted.current += rate; 271 | } 272 | } 273 | elapsedTime.current += delta; 274 | }); 275 | 276 | const onRestart = useCallback(() => { 277 | emitted.current = 0; 278 | elapsedTime.current = 0; 279 | }, []); 280 | 281 | const settingsBuilder = useMemo( 282 | () => 283 | debug ? ( 284 | 289 | ) : null, 290 | [debug] 291 | ); 292 | 293 | return ( 294 | <> 295 | {settingsBuilder} 296 | 297 | 298 | ); 299 | } 300 | ); 301 | 302 | export default VFXEmitter; 303 | -------------------------------------------------------------------------------- /packages/src/components/vfxs/VFXParticles.tsx: -------------------------------------------------------------------------------- 1 | import { shaderMaterial } from "@react-three/drei"; 2 | import { extend, useFrame } from "@react-three/fiber"; 3 | import { useEffect, useMemo, useRef, useState } from "react"; 4 | import * as THREE from "three"; 5 | import { 6 | AdditiveBlending, 7 | Color, 8 | DynamicDrawUsage, 9 | Euler, 10 | Matrix4, 11 | PlaneGeometry, 12 | Quaternion, 13 | Vector3, 14 | } from "three"; 15 | import { EmitCallbackSettingsFn, useVFX } from "./VFXStore"; 16 | import { easings } from "./easings"; 17 | import { 18 | AppearanceMode, 19 | EaseFunction, 20 | easeFunctionList, 21 | RenderMode, 22 | } from "./types"; 23 | 24 | const tmpPosition = new Vector3(); 25 | const tmpRotationEuler = new Euler(); 26 | const tmpRotation = new Quaternion(); 27 | const tmpScale = new Vector3(1, 1, 1); 28 | const tmpMatrix = new Matrix4(); 29 | const tmpColor = new Color(); 30 | 31 | interface VFXParticlesSettings { 32 | nbParticles?: number; 33 | intensity?: number; 34 | renderMode?: RenderMode; 35 | stretchScale?: number; 36 | fadeSize?: [number, number]; 37 | fadeAlpha?: [number, number]; 38 | gravity?: [number, number, number]; 39 | frustumCulled?: boolean; 40 | appearance?: AppearanceMode; 41 | easeFunction?: EaseFunction; 42 | } 43 | 44 | interface VFXParticlesProps { 45 | name: string; 46 | settings?: VFXParticlesSettings; 47 | alphaMap?: THREE.Texture; 48 | geometry?: React.ReactElement; 49 | } 50 | 51 | const VFXParticles: React.FC = ({ 52 | name, 53 | settings = {}, 54 | alphaMap, 55 | geometry, 56 | }) => { 57 | const { 58 | nbParticles = 1000, 59 | intensity = 1, 60 | renderMode = RenderMode.Mesh, 61 | stretchScale = 1.0, 62 | fadeSize = [0.1, 0.9], 63 | fadeAlpha = [0, 1.0], 64 | gravity = [0, 0, 0], 65 | frustumCulled = true, 66 | appearance = AppearanceMode.Square, 67 | easeFunction = "easeLinear", 68 | } = settings; 69 | const mesh = useRef(null!); 70 | const defaultGeometry = useMemo(() => new PlaneGeometry(0.5, 0.5), []); 71 | const easingIndex = easeFunctionList.indexOf(easeFunction); 72 | 73 | const onBeforeRender = () => { 74 | if (!needsUpdate.current || !mesh.current) { 75 | return; 76 | } 77 | const attributes = [ 78 | mesh.current.instanceMatrix, 79 | mesh.current.geometry.getAttribute("instanceColor"), 80 | mesh.current.geometry.getAttribute("instanceColorEnd"), 81 | mesh.current.geometry.getAttribute("instanceDirection"), 82 | mesh.current.geometry.getAttribute("instanceLifetime"), 83 | mesh.current.geometry.getAttribute("instanceSpeed"), 84 | mesh.current.geometry.getAttribute("instanceRotationSpeed"), 85 | ]; 86 | attributes.forEach((attr) => { 87 | const attribute = attr as THREE.InstancedBufferAttribute; 88 | attribute.clearUpdateRanges(); 89 | if (lastCursor.current > cursor.current) { 90 | attribute.addUpdateRange(0, cursor.current * attribute.itemSize); 91 | attribute.addUpdateRange( 92 | lastCursor.current * attribute.itemSize, 93 | nbParticles * attribute.itemSize - 94 | lastCursor.current * attribute.itemSize 95 | ); 96 | } else { 97 | attribute.addUpdateRange( 98 | lastCursor.current * attribute.itemSize, 99 | cursor.current * attribute.itemSize - 100 | lastCursor.current * attribute.itemSize 101 | ); 102 | } 103 | attribute.needsUpdate = true; 104 | }); 105 | lastCursor.current = cursor.current; 106 | needsUpdate.current = false; 107 | }; 108 | 109 | const cursor = useRef(0); 110 | const lastCursor = useRef(0); 111 | const needsUpdate = useRef(false); 112 | 113 | const emit = (count: number, setup: EmitCallbackSettingsFn) => { 114 | const instanceColor = mesh.current.geometry.getAttribute( 115 | "instanceColor" 116 | ) as THREE.BufferAttribute; 117 | const instanceColorEnd = mesh.current.geometry.getAttribute( 118 | "instanceColorEnd" 119 | ) as THREE.BufferAttribute; 120 | const instanceDirection = mesh.current.geometry.getAttribute( 121 | "instanceDirection" 122 | ) as THREE.BufferAttribute; 123 | const instanceLifetime = mesh.current.geometry.getAttribute( 124 | "instanceLifetime" 125 | ) as THREE.BufferAttribute; 126 | const instanceSpeed = mesh.current.geometry.getAttribute( 127 | "instanceSpeed" 128 | ) as THREE.BufferAttribute; 129 | const instanceRotationSpeed = mesh.current.geometry.getAttribute( 130 | "instanceRotationSpeed" 131 | ) as THREE.BufferAttribute; 132 | 133 | for (let i = 0; i < count; i++) { 134 | if (cursor.current >= nbParticles) { 135 | cursor.current = 0; 136 | } 137 | const { 138 | scale, 139 | rotation, 140 | rotationSpeed, 141 | position, 142 | direction, 143 | lifetime, 144 | colorStart, 145 | colorEnd, 146 | speed, 147 | } = setup(); 148 | 149 | tmpPosition.set(...position); 150 | tmpRotationEuler.set(...rotation); 151 | if (renderMode === "billboard" || renderMode === "stretchBillboard") { 152 | tmpRotationEuler.x = 0; 153 | tmpRotationEuler.y = 0; 154 | } 155 | tmpRotation.setFromEuler(tmpRotationEuler); 156 | tmpScale.set(...scale); 157 | tmpMatrix.compose(tmpPosition, tmpRotation, tmpScale); 158 | mesh.current.setMatrixAt(cursor.current, tmpMatrix); 159 | 160 | tmpColor.set(colorStart); 161 | instanceColor.set( 162 | [tmpColor.r, tmpColor.g, tmpColor.b], 163 | cursor.current * 3 164 | ); 165 | tmpColor.set(colorEnd); 166 | instanceColorEnd.set( 167 | [tmpColor.r, tmpColor.g, tmpColor.b], 168 | cursor.current * 3 169 | ); 170 | instanceDirection.set(direction, cursor.current * 3); 171 | instanceLifetime.set(lifetime, cursor.current * 2); 172 | instanceSpeed.set(speed, cursor.current); 173 | instanceRotationSpeed.set(rotationSpeed, cursor.current * 3); 174 | cursor.current++; 175 | cursor.current = cursor.current % nbParticles; 176 | } 177 | 178 | mesh.current.instanceMatrix.needsUpdate = true; 179 | instanceColor.needsUpdate = true; 180 | instanceColorEnd.needsUpdate = true; 181 | instanceDirection.needsUpdate = true; 182 | instanceLifetime.needsUpdate = true; 183 | instanceSpeed.needsUpdate = true; 184 | instanceRotationSpeed.needsUpdate = true; 185 | needsUpdate.current = true; 186 | }; 187 | 188 | const [attributeArrays] = useState({ 189 | instanceColor: new Float32Array(nbParticles * 3), 190 | instanceColorEnd: new Float32Array(nbParticles * 3), 191 | instanceDirection: new Float32Array(nbParticles * 3), 192 | instanceLifetime: new Float32Array(nbParticles * 2), 193 | instanceSpeed: new Float32Array(nbParticles * 1), 194 | instanceRotationSpeed: new Float32Array(nbParticles * 3), 195 | }); 196 | 197 | useFrame(({ clock }) => { 198 | if (!mesh.current) { 199 | return; 200 | } 201 | const material = mesh.current.material as THREE.ShaderMaterial; 202 | material.uniforms.uTime.value = clock.elapsedTime; 203 | material.uniforms.uIntensity.value = intensity; 204 | material.uniforms.uStretchScale.value = stretchScale; 205 | material.uniforms.uFadeSize.value = fadeSize; 206 | material.uniforms.uFadeAlpha.value = fadeAlpha; 207 | material.uniforms.uGravity.value = gravity; 208 | material.uniforms.uAppearanceMode.value = appearance; 209 | material.uniforms.uEasingFunction.value = easingIndex; 210 | }); 211 | 212 | const registerEmitter = useVFX((state) => state.registerEmitter); 213 | const unregisterEmitter = useVFX((state) => state.unregisterEmitter); 214 | 215 | useEffect(() => { 216 | registerEmitter(name, emit); 217 | return () => { 218 | unregisterEmitter(name); 219 | }; 220 | }, []); 221 | 222 | return ( 223 | <> 224 | } 233 | frustumCulled={frustumCulled} 234 | onBeforeRender={onBeforeRender} 235 | > 236 | {geometry} 237 | 248 | 255 | 262 | 269 | 276 | 283 | 290 | 291 | 292 | ); 293 | }; 294 | 295 | const ParticlesMaterial = shaderMaterial( 296 | { 297 | uTime: 0, 298 | uIntensity: 1, 299 | uStretchScale: 1, 300 | uFadeSize: [0.1, 0.9], 301 | uFadeAlpha: [0, 1.0], 302 | uGravity: [0, 0, 0], 303 | uAppearanceMode: 0, 304 | alphaMap: null, 305 | uEasingFunction: 0, 306 | }, 307 | /* glsl */ ` 308 | ${easings} 309 | mat4 rotationX(float angle) { 310 | float s = sin(angle); 311 | float c = cos(angle); 312 | return mat4( 313 | 1, 0, 0, 0, 314 | 0, c, -s, 0, 315 | 0, s, c, 0, 316 | 0, 0, 0, 1 317 | ); 318 | } 319 | 320 | mat4 rotationY(float angle) { 321 | float s = sin(angle); 322 | float c = cos(angle); 323 | return mat4( 324 | c, 0, s, 0, 325 | 0, 1, 0, 0, 326 | -s, 0, c, 0, 327 | 0, 0, 0, 1 328 | ); 329 | } 330 | 331 | mat4 rotationZ(float angle) { 332 | float s = sin(angle); 333 | float c = cos(angle); 334 | return mat4( 335 | c, -s, 0, 0, 336 | s, c, 0, 0, 337 | 0, 0, 1, 0, 338 | 0, 0, 0, 1 339 | ); 340 | } 341 | 342 | 343 | vec3 billboard(vec2 v, mat4 view) { 344 | vec3 up = vec3(view[0][1], view[1][1], view[2][1]); 345 | vec3 right = vec3(view[0][0], view[1][0], view[2][0]); 346 | vec3 p = right * v.x + up * v.y; 347 | return p; 348 | } 349 | 350 | uniform float uTime; 351 | uniform vec2 uFadeSize; 352 | uniform vec3 uGravity; 353 | uniform float uStretchScale; 354 | uniform int uEasingFunction; 355 | 356 | varying vec2 vUv; 357 | varying vec3 vColor; 358 | varying vec3 vColorEnd; 359 | varying float vProgress; 360 | 361 | attribute float instanceSpeed; 362 | attribute vec3 instanceRotationSpeed; 363 | attribute vec3 instanceDirection; 364 | attribute vec3 instanceColor; 365 | attribute vec3 instanceColorEnd; 366 | attribute vec2 instanceLifetime; // x: startTime, y: duration 367 | 368 | void main() { 369 | float startTime = instanceLifetime.x; 370 | float duration = instanceLifetime.y; 371 | float age = uTime - startTime; 372 | 373 | // Adjust age based on instanceSpeed direction 374 | age = instanceSpeed < 0.0 ? duration - (uTime - startTime) : uTime - startTime; 375 | float progress = clamp(age / duration, 0.0, 1.0); 376 | vProgress = applyEasing(progress, uEasingFunction); 377 | 378 | if (vProgress < 0.0 || vProgress > 1.0) { 379 | gl_Position = vec4(vec3(9999.0), 1.0); 380 | return; 381 | } 382 | 383 | float scale = smoothstep(0.0, uFadeSize.x, vProgress) * smoothstep(1.01, uFadeSize.y, vProgress); 384 | 385 | vec3 normalizedDirection = length(instanceDirection) > 0.0 ? normalize(instanceDirection) : vec3(0.0); 386 | vec3 gravityForce = 0.5 * uGravity * (age * age); 387 | float easedAge = vProgress * duration; 388 | vec3 offset = normalizedDirection * easedAge * instanceSpeed; 389 | offset += gravityForce; 390 | 391 | vec3 rotationSpeed = instanceRotationSpeed * age; 392 | mat4 rotX = rotationX(rotationSpeed.x); 393 | mat4 rotY = rotationY(rotationSpeed.y); 394 | mat4 rotZ = rotationZ(rotationSpeed.z); 395 | mat4 rotationMatrix = rotZ * rotY * rotX; 396 | 397 | vec4 mvPosition; 398 | #ifdef MESH_MODE 399 | /* Mesh Mode */ 400 | vec4 startPosition = modelMatrix * instanceMatrix * rotationMatrix * vec4(position * scale, 1.0); 401 | 402 | vec3 instancePosition = startPosition.xyz; 403 | 404 | vec3 finalPosition = instancePosition + offset; 405 | mvPosition = modelViewMatrix * vec4(finalPosition, 1.0); 406 | #endif 407 | #ifdef BILLBOARD_MODE 408 | /* Billboard Mode */ 409 | // Instance position (translation from instanceMatrix) 410 | vec3 instancePosition = (instanceMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz + offset; 411 | 412 | // Compute billboard's local coordinate system 413 | // Forward vector (billboard's local Z-axis, points toward camera) 414 | vec3 localZ = normalize(cameraPosition - instancePosition); 415 | // World up vector (assuming Y-up world) 416 | vec3 worldUp = vec3(0.0, 1.0, 0.0); 417 | // Local X-axis (right vector) 418 | vec3 localX = normalize(cross(worldUp, localZ)); 419 | // Local Y-axis (up vector) 420 | vec3 localY = cross(localZ, localX); 421 | 422 | // Construct billboard's orientation matrix (converts from local to world space) 423 | mat3 billboardMatrix = mat3(localX, localY, localZ); 424 | 425 | float scaleX = length(instanceMatrix[0].xyz); 426 | float scaleY = length(instanceMatrix[1].xyz); 427 | float scaleZ = length(instanceMatrix[2].xyz); 428 | vec3 instanceScale = vec3(scaleX, scaleY, scaleZ); 429 | 430 | // Combine billboard orientation with local rotation 431 | mat3 finalMatrix = billboardMatrix * mat3(rotationMatrix); 432 | 433 | // Extract final right and up vectors, apply scale 434 | vec3 finalRight = finalMatrix[0] * instanceScale * scale; 435 | vec3 finalUp = finalMatrix[1] * instanceScale * scale; 436 | // Compute vertex position in world space 437 | vec3 vertexWorldPos = instancePosition + 438 | finalRight * position.x + 439 | finalUp * position.y; 440 | mvPosition = viewMatrix * vec4(vertexWorldPos, 1.0); 441 | #endif 442 | #ifdef STRETCH_BILLBOARD_MODE 443 | vec3 particleWorldPos = (instanceMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz + offset; 444 | 445 | vec3 worldVelocity = normalizedDirection * instanceSpeed + uGravity * age; 446 | float currentSpeed = length(worldVelocity); 447 | 448 | if (currentSpeed < 0.001) { 449 | vec3 instancePositionBillboard = particleWorldPos; 450 | 451 | // Use camera's Up vector in World for a more robust billboard 452 | vec3 camUpWorld = normalize(vec3(viewMatrix[0][1], viewMatrix[1][1], viewMatrix[2][1])); 453 | vec3 eyeVecBillboard = normalize(cameraPosition - instancePositionBillboard); 454 | 455 | vec3 bLocalX = normalize(cross(camUpWorld, eyeVecBillboard)); 456 | 457 | if (length(bLocalX) < 0.001) { 458 | 459 | bLocalX = normalize(vec3(viewMatrix[0][0], viewMatrix[1][0], viewMatrix[2][0])); 460 | } 461 | vec3 bLocalY = normalize(cross(eyeVecBillboard, bLocalX)); 462 | mat3 billboardBasis = mat3(bLocalX, bLocalY, eyeVecBillboard); 463 | 464 | float instScaleX = length(instanceMatrix[0].xyz); 465 | float instScaleY = length(instanceMatrix[1].xyz); 466 | 467 | mat3 rotatedBillboardBasis = billboardBasis * mat3(rotationMatrix); 468 | 469 | vec3 finalRight = rotatedBillboardBasis[0] * instScaleX * scale; 470 | vec3 finalUp = rotatedBillboardBasis[1] * instScaleY * scale; 471 | vec3 vertexWorldPos = instancePositionBillboard + finalRight * position.x + finalUp * position.y; 472 | mvPosition = viewMatrix * vec4(vertexWorldPos, 1.0); 473 | 474 | } else { 475 | vec3 eyeVector = normalize(cameraPosition - particleWorldPos); 476 | 477 | vec3 tangent = normalize(worldVelocity); 478 | 479 | vec3 projectedTangent = tangent - dot(tangent, eyeVector) * eyeVector; 480 | 481 | vec3 particlePlaneUp; 482 | vec3 particlePlaneRight; 483 | 484 | if (length(projectedTangent) < 0.001) { 485 | 486 | particlePlaneUp = tangent; 487 | 488 | vec3 camUpWorld = normalize(vec3(viewMatrix[0][1], viewMatrix[1][1], viewMatrix[2][1])); 489 | particlePlaneRight = normalize(cross(particlePlaneUp, camUpWorld)); 490 | 491 | if (length(particlePlaneRight) < 0.001) { 492 | vec3 camRightWorld = normalize(vec3(viewMatrix[0][0], viewMatrix[1][0], viewMatrix[2][0])); 493 | particlePlaneRight = normalize(cross(particlePlaneUp, camRightWorld)); 494 | } 495 | } else { 496 | particlePlaneUp = normalize(projectedTangent); 497 | particlePlaneRight = normalize(cross(particlePlaneUp, eyeVector)); 498 | } 499 | 500 | float baseWidth = length(instanceMatrix[0].xyz); 501 | float baseLength = length(instanceMatrix[1].xyz); 502 | 503 | float wid = baseWidth * scale; 504 | float len = baseLength * scale * (1.0 + currentSpeed * uStretchScale); 505 | 506 | float zAngle = instanceRotationSpeed.z * age; 507 | mat2 spinMatrix = mat2(cos(zAngle), -sin(zAngle), sin(zAngle), cos(zAngle)); 508 | vec2 localSpunPos = spinMatrix * position.xy; 509 | 510 | vec3 worldSpaceVertexOffset = particlePlaneRight * localSpunPos.x * wid + 511 | particlePlaneUp * localSpunPos.y * len; 512 | 513 | vec3 finalVertexPos = particleWorldPos + worldSpaceVertexOffset; 514 | mvPosition = viewMatrix * vec4(finalVertexPos, 1.0); 515 | } 516 | #endif 517 | 518 | gl_Position = projectionMatrix * mvPosition; 519 | 520 | vUv = uv; 521 | vColor = instanceColor; 522 | vColorEnd = instanceColorEnd; 523 | } 524 | `, 525 | /* glsl */ ` 526 | uniform float uIntensity; 527 | uniform vec2 uFadeAlpha; 528 | uniform sampler2D alphaMap; 529 | uniform int uAppearanceMode; 530 | 531 | varying vec3 vColor; 532 | varying vec3 vColorEnd; 533 | varying float vProgress; 534 | varying vec2 vUv; 535 | 536 | 537 | void main() { 538 | if (vProgress < 0.0 || vProgress > 1.0) { 539 | discard; 540 | } 541 | vec3 finalColor = mix(vColor, vColorEnd, vProgress); 542 | finalColor *= uIntensity; 543 | 544 | float alpha = smoothstep(0.0, uFadeAlpha.x, vProgress) * smoothstep(1.01, uFadeAlpha.y, vProgress); 545 | 546 | #ifdef USE_ALPHAMAP 547 | vec2 uv = vUv; 548 | vec4 tex = texture2D(alphaMap, uv); 549 | gl_FragColor = vec4(finalColor, tex.a * alpha); 550 | #else 551 | if(uAppearanceMode == 1){ // Circular 552 | vec2 center = vec2(0.5); 553 | float dist = distance(vUv, center); 554 | 555 | if(dist > 0.5){ 556 | discard; // creating circular shape 557 | } 558 | } 559 | gl_FragColor = vec4(finalColor, alpha); 560 | #endif 561 | }` 562 | ); 563 | 564 | extend({ ParticlesMaterial }); 565 | declare module "@react-three/fiber" { 566 | interface ThreeElements { 567 | particlesMaterial: ThreeElements["shaderMaterial"] & { 568 | alphaMap?: THREE.Texture; 569 | }; 570 | } 571 | } 572 | 573 | export default VFXParticles; 574 | -------------------------------------------------------------------------------- /packages/src/components/vfxs/VFXStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | interface EmitCallbackSettings { 4 | position: [number, number, number]; 5 | direction: [number, number, number]; 6 | scale: [number, number, number]; 7 | rotation: [number, number, number]; 8 | rotationSpeed: [number, number, number]; 9 | lifetime: [number, number]; 10 | colorStart: string; 11 | colorEnd: string; 12 | speed: [number]; 13 | } 14 | 15 | export type EmitCallbackSettingsFn = () => EmitCallbackSettings; 16 | 17 | interface VFXStore { 18 | emitters: Record void>; 19 | shouldEmit: boolean; 20 | registerEmitter: (name: string, emitter: (...args: any[]) => void) => void; 21 | unregisterEmitter: (name: string) => void; 22 | emit: (name: string, rate: number, callback: EmitCallbackSettingsFn) => void; 23 | } 24 | 25 | export const useVFX = create((set, get) => ({ 26 | emitters: {}, 27 | shouldEmit: true, 28 | registerEmitter: (name: string, emitter) => { 29 | if (get().emitters[name]) { 30 | console.warn(`Emitter ${name} already exists`); 31 | return; 32 | } 33 | set((state) => { 34 | state.emitters[name] = emitter; 35 | return state; 36 | }); 37 | }, 38 | unregisterEmitter: (name) => { 39 | set((state) => { 40 | delete state.emitters[name]; 41 | return state; 42 | }); 43 | }, 44 | emit: (name, rate, callback) => { 45 | const emitter = get().emitters[name]; 46 | if (!emitter) { 47 | console.warn(`Emitter ${name} not found`); 48 | return; 49 | } 50 | emitter(rate, callback); 51 | }, 52 | })); 53 | -------------------------------------------------------------------------------- /packages/src/components/vfxs/easings.ts: -------------------------------------------------------------------------------- 1 | export const easings = ` 2 | #define PI 3.1415926535897932384626433832795 3 | // Linear 4 | float easeLinear(float t) 5 | { 6 | 7 | return t; 8 | 9 | } 10 | 11 | // --------- Power1 --------- 12 | float easeInPower1(float t) { 13 | return t; 14 | } 15 | 16 | float easeOutPower1(float t) { 17 | return 1.0 - (1.0 - t); 18 | } 19 | 20 | float easeInOutPower1(float t) { 21 | return t; 22 | } 23 | 24 | // --------- Power2 --------- 25 | float easeInPower2(float t) { 26 | return t * t; 27 | } 28 | 29 | float easeOutPower2(float t) { 30 | return 1.0 - pow(1.0 - t, 2.0); 31 | } 32 | 33 | float easeInOutPower2(float t) { 34 | return t < 0.5 35 | ? 2.0 * t * t 36 | : 1.0 - pow(-2.0 * t + 2.0, 2.0) / 2.0; 37 | } 38 | 39 | // --------- Power3 --------- 40 | float easeInPower3(float t) { 41 | return t * t * t; 42 | } 43 | 44 | float easeOutPower3(float t) { 45 | return 1.0 - pow(1.0 - t, 3.0); 46 | } 47 | 48 | float easeInOutPower3(float t) { 49 | return t < 0.5 50 | ? 4.0 * t * t * t 51 | : 1.0 - pow(-2.0 * t + 2.0, 3.0) / 2.0; 52 | } 53 | 54 | // --------- Power4 --------- 55 | float easeInPower4(float t) { 56 | return t * t * t * t; 57 | } 58 | 59 | float easeOutPower4(float t) { 60 | return 1.0 - pow(1.0 - t, 4.0); 61 | } 62 | 63 | float easeInOutPower4(float t) { 64 | return t < 0.5 65 | ? 8.0 * t * t * t * t 66 | : 1.0 - pow(-2.0 * t + 2.0, 4.0) / 2.0; 67 | } 68 | 69 | // Quad 70 | float easeInQuad(float t) { 71 | return t * t; 72 | } 73 | 74 | float easeOutQuad(float t) { 75 | return t * (2.0 - t); 76 | } 77 | 78 | float easeInOutQuad(float t) { 79 | return t < 0.5 80 | ? 2.0 * t * t 81 | : -1.0 + (4.0 - 2.0 * t) * t; 82 | } 83 | 84 | // Cubic 85 | float easeInCubic(float t) { 86 | return t * t * t; 87 | } 88 | 89 | float easeOutCubic(float t) { 90 | float t1 = t - 1.0; 91 | return 1.0 + t1 * t1 * t1; 92 | } 93 | 94 | float easeInOutCubic(float t) { 95 | return t < 0.5 96 | ? 4.0 * t * t * t 97 | : (t - 1.0) * (2.0 * t - 2.0) * (2.0 * t - 2.0) + 1.0; 98 | } 99 | 100 | // Quart 101 | float easeInQuart(float t) { 102 | return t * t * t * t; 103 | } 104 | 105 | float easeOutQuart(float t) { 106 | float t1 = t - 1.0; 107 | return 1.0 - t1 * t1 * t1 * t1; 108 | } 109 | 110 | float easeInOutQuart(float t) { 111 | float t1 = t - 1.0; 112 | return t < 0.5 113 | ? 8.0 * t * t * t * t 114 | : 1.0 - 8.0 * t1 * t1 * t1 * t1; 115 | } 116 | 117 | // Quint 118 | float easeInQuint(float t) { 119 | return t * t * t * t * t; 120 | } 121 | 122 | float easeOutQuint(float t) { 123 | float t1 = t - 1.0; 124 | return 1.0 + t1 * t1 * t1 * t1 * t1; 125 | } 126 | 127 | float easeInOutQuint(float t) { 128 | float t1 = t - 1.0; 129 | return t < 0.5 130 | ? 16.0 * t * t * t * t * t 131 | : 1.0 + 16.0 * t1 * t1 * t1 * t1 * t1; 132 | } 133 | 134 | // Sine 135 | float easeInSine(float t) { 136 | return -1.0 * cos(t * PI * 0.5) + 1.0; 137 | } 138 | 139 | float easeOutSine(float t) { 140 | return sin(t * PI * 0.5); 141 | } 142 | 143 | float easeInOutSine(float t) { 144 | return -0.5 * (cos(PI * t) - 1.0); 145 | } 146 | 147 | // Expo 148 | float easeInExpo(float t) { 149 | return t == 0.0 ? 0.0 : pow(2.0, 10.0 * (t - 1.0)); 150 | } 151 | 152 | float easeOutExpo(float t) { 153 | return t == 1.0 ? 1.0 : 1.0 - pow(2.0, -10.0 * t); 154 | } 155 | 156 | float easeInOutExpo(float t) { 157 | if (t == 0.0 || t == 1.0) return t; 158 | 159 | return t < 0.5 160 | ? 0.5 * pow(2.0, (20.0 * t) - 10.0) 161 | : 0.5 * (-pow(2.0, (-20.0 * t) + 10.0) + 2.0); 162 | } 163 | 164 | // Circ 165 | float easeInCirc(float t) { 166 | return -1.0 * (sqrt(1.0 - t * t) - 1.0); 167 | } 168 | 169 | float easeOutCirc(float t) { 170 | float t1 = t - 1.0; 171 | return sqrt(1.0 - t1 * t1); 172 | } 173 | 174 | float easeInOutCirc(float t) { 175 | float t1 = 2.0 * t; 176 | float t2 = t1 - 2.0; 177 | return t < 0.5 178 | ? -0.5 * (sqrt(1.0 - t1 * t1) - 1.0) 179 | : 0.5 * (sqrt(1.0 - t2 * t2) + 1.0); 180 | } 181 | 182 | // Elastic 183 | float easeInElastic(float t) { 184 | if (t == 0.0 || t == 1.0) return t; 185 | return -pow(2.0, 10.0 * (t - 1.0)) * sin((t - 1.075) * (2.0 * PI) / 0.3); 186 | } 187 | 188 | float easeOutElastic(float t) { 189 | if (t == 0.0 || t == 1.0) return t; 190 | return pow(2.0, -10.0 * t) * sin((t - 0.075) * (2.0 * PI) / 0.3) + 1.0; 191 | } 192 | 193 | float easeInOutElastic(float t) { 194 | if (t < 0.5) { 195 | return -0.5 * pow(2.0, 20.0 * t - 10.0) * 196 | sin((20.0 * t - 11.125) * (2.0 * PI) / 4.5); 197 | } 198 | return pow(2.0, -20.0 * t + 10.0) * 199 | sin((20.0 * t - 11.125) * (2.0 * PI) / 4.5) * 0.5 + 1.0; 200 | } 201 | 202 | // Back 203 | float easeInBack(float t) { 204 | float s = 1.70158; 205 | return t * t * ((s + 1.0) * t - s); 206 | } 207 | 208 | float easeOutBack(float t) { 209 | float s = 1.70158; 210 | float t1 = t - 1.0; 211 | return t1 * t1 * ((s + 1.0) * t1 + s) + 1.0; 212 | } 213 | 214 | float easeInOutBack(float t) { 215 | float s = 1.70158 * 1.525; 216 | t *= 2.0; 217 | if (t < 1.0) { 218 | return 0.5 * (t * t * ((s + 1.0) * t - s)); 219 | } 220 | t -= 2.0; 221 | return 0.5 * (t * t * ((s + 1.0) * t + s) + 2.0); 222 | } 223 | 224 | // Bounce 225 | float easeOutBounce(float t) { 226 | if (t < 1.0 / 2.75) { 227 | return 7.5625 * t * t; 228 | } else if (t < 2.0 / 2.75) { 229 | t -= 1.5 / 2.75; 230 | return 7.5625 * t * t + 0.75; 231 | } else if (t < 2.5 / 2.75) { 232 | t -= 2.25 / 2.75; 233 | return 7.5625 * t * t + 0.9375; 234 | } else { 235 | t -= 2.625 / 2.75; 236 | return 7.5625 * t * t + 0.984375; 237 | } 238 | } 239 | 240 | float easeInBounce(float t) { 241 | return 1.0 - easeOutBounce(1.0 - t); 242 | } 243 | 244 | float easeInOutBounce(float t) { 245 | return t < 0.5 246 | ? (1.0 - easeOutBounce(1.0 - 2.0 * t)) * 0.5 247 | : (1.0 + easeOutBounce(2.0 * t - 1.0)) * 0.5; 248 | } 249 | 250 | float applyEasing(float t, int easingId) { 251 | if (easingId == 0) return easeLinear(t); 252 | else if (easingId == 1) return easeInPower1(t); 253 | else if (easingId == 2) return easeOutPower1(t); 254 | else if (easingId == 3) return easeInOutPower1(t); 255 | else if (easingId == 4) return easeInPower2(t); 256 | else if (easingId == 5) return easeOutPower2(t); 257 | else if (easingId == 6) return easeInOutPower2(t); 258 | else if (easingId == 7) return easeInPower3(t); 259 | else if (easingId == 8) return easeOutPower3(t); 260 | else if (easingId == 9) return easeInOutPower3(t); 261 | else if (easingId == 10) return easeInPower4(t); 262 | else if (easingId == 11) return easeOutPower4(t); 263 | else if (easingId == 12) return easeInOutPower4(t); 264 | else if (easingId == 13) return easeInQuad(t); 265 | else if (easingId == 14) return easeOutQuad(t); 266 | else if (easingId == 15) return easeInOutQuad(t); 267 | else if (easingId == 16) return easeInCubic(t); 268 | else if (easingId == 17) return easeOutCubic(t); 269 | else if (easingId == 18) return easeInOutCubic(t); 270 | else if (easingId == 19) return easeInQuart(t); 271 | else if (easingId == 20) return easeOutQuart(t); 272 | else if (easingId == 21) return easeInOutQuart(t); 273 | else if (easingId == 22) return easeInQuint(t); 274 | else if (easingId == 23) return easeOutQuint(t); 275 | else if (easingId == 24) return easeInOutQuint(t); 276 | else if (easingId == 25) return easeInSine(t); 277 | else if (easingId == 26) return easeOutSine(t); 278 | else if (easingId == 27) return easeInOutSine(t); 279 | else if (easingId == 28) return easeInExpo(t); 280 | else if (easingId == 29) return easeOutExpo(t); 281 | else if (easingId == 30) return easeInOutExpo(t); 282 | else if (easingId == 31) return easeInCirc(t); 283 | else if (easingId == 32) return easeOutCirc(t); 284 | else if (easingId == 33) return easeInOutCirc(t); 285 | else if (easingId == 34) return easeInElastic(t); 286 | else if (easingId == 35) return easeOutElastic(t); 287 | else if (easingId == 36) return easeInOutElastic(t); 288 | else if (easingId == 37) return easeInBack(t); 289 | else if (easingId == 38) return easeOutBack(t); 290 | else if (easingId == 39) return easeInOutBack(t); 291 | else if (easingId == 40) return easeInBounce(t); 292 | else if (easingId == 41) return easeOutBounce(t); 293 | else if (easingId == 42) return easeInOutBounce(t); 294 | // fallback 295 | return t; 296 | } 297 | 298 | `; -------------------------------------------------------------------------------- /packages/src/components/vfxs/types.ts: -------------------------------------------------------------------------------- 1 | export enum AppearanceMode { 2 | Square = 0, 3 | Circular = 1, 4 | } 5 | 6 | export enum RenderMode { 7 | StretchBillboard = "stretchBillboard", 8 | Billboard = "billboard", 9 | Mesh = "mesh", 10 | } 11 | 12 | export type EaseFunction = 13 | | "easeLinear" 14 | 15 | // Power1 16 | | "easeInPower1" 17 | | "easeOutPower1" 18 | | "easeInOutPower1" 19 | 20 | // Power2 21 | | "easeInPower2" 22 | | "easeOutPower2" 23 | | "easeInOutPower2" 24 | 25 | // Power3 26 | | "easeInPower3" 27 | | "easeOutPower3" 28 | | "easeInOutPower3" 29 | 30 | // Power4 31 | | "easeInPower4" 32 | | "easeOutPower4" 33 | | "easeInOutPower4" 34 | 35 | // Quad 36 | | "easeInQuad" 37 | | "easeOutQuad" 38 | | "easeInOutQuad" 39 | 40 | // Cubic 41 | | "easeInCubic" 42 | | "easeOutCubic" 43 | | "easeInOutCubic" 44 | 45 | // Quart 46 | | "easeInQuart" 47 | | "easeOutQuart" 48 | | "easeInOutQuart" 49 | 50 | // Quint 51 | | "easeInQuint" 52 | | "easeOutQuint" 53 | | "easeInOutQuint" 54 | 55 | // Sine 56 | | "easeInSine" 57 | | "easeOutSine" 58 | | "easeInOutSine" 59 | 60 | // Expo 61 | | "easeInExpo" 62 | | "easeOutExpo" 63 | | "easeInOutExpo" 64 | 65 | // Circ 66 | | "easeInCirc" 67 | | "easeOutCirc" 68 | | "easeInOutCirc" 69 | 70 | // Elastic 71 | | "easeInElastic" 72 | | "easeOutElastic" 73 | | "easeInOutElastic" 74 | 75 | // Back 76 | | "easeInBack" 77 | | "easeOutBack" 78 | | "easeInOutBack" 79 | 80 | // Bounce 81 | | "easeInBounce" 82 | | "easeOutBounce" 83 | | "easeInOutBounce"; 84 | 85 | export const easeFunctionList: EaseFunction[] = [ 86 | "easeLinear", 87 | "easeInPower1", 88 | "easeOutPower1", 89 | "easeInOutPower1", 90 | "easeInPower2", 91 | "easeOutPower2", 92 | "easeInOutPower2", 93 | "easeInPower3", 94 | "easeOutPower3", 95 | "easeInOutPower3", 96 | "easeInPower4", 97 | "easeOutPower4", 98 | "easeInOutPower4", 99 | "easeInQuad", 100 | "easeOutQuad", 101 | "easeInOutQuad", 102 | "easeInCubic", 103 | "easeOutCubic", 104 | "easeInOutCubic", 105 | "easeInQuart", 106 | "easeOutQuart", 107 | "easeInOutQuart", 108 | "easeInQuint", 109 | "easeOutQuint", 110 | "easeInOutQuint", 111 | "easeInSine", 112 | "easeOutSine", 113 | "easeInOutSine", 114 | "easeInExpo", 115 | "easeOutExpo", 116 | "easeInOutExpo", 117 | "easeInCirc", 118 | "easeOutCirc", 119 | "easeInOutCirc", 120 | "easeInElastic", 121 | "easeOutElastic", 122 | "easeInOutElastic", 123 | "easeInBack", 124 | "easeOutBack", 125 | "easeInOutBack", 126 | "easeInBounce", 127 | "easeOutBounce", 128 | "easeInOutBounce", 129 | ]; 130 | -------------------------------------------------------------------------------- /packages/src/index.css: -------------------------------------------------------------------------------- 1 | #root { 2 | width: 100vw; 3 | height: 100vh; 4 | } 5 | 6 | body { 7 | margin: 0; 8 | } 9 | -------------------------------------------------------------------------------- /packages/src/index.ts: -------------------------------------------------------------------------------- 1 | export { AppearanceMode, RenderMode } from "./components/vfxs/types"; 2 | export { default as VFXEmitter } from "./components/vfxs/VFXEmitter"; 3 | export { default as VFXParticles } from "./components/vfxs/VFXParticles"; 4 | -------------------------------------------------------------------------------- /packages/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | import "./index.css"; 5 | 6 | ReactDOM.createRoot(document.getElementById("root") as Element).render( 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /packages/src/types.d.ts: -------------------------------------------------------------------------------- 1 | import { ThreeElements } from "@react-three/fiber"; 2 | 3 | declare global { 4 | namespace React { 5 | namespace JSX { 6 | interface IntrinsicElements extends ThreeElements {} 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /packages/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/vite.config.js: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react"; 2 | import { defineConfig } from "vite"; 3 | import dtsPlugin from "vite-plugin-dts"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [ 8 | react(), 9 | dtsPlugin({ 10 | insertTypesEntry: true, 11 | rollupTypes: true, 12 | }), 13 | ], 14 | build: { 15 | lib: { 16 | entry: "src/index.ts", 17 | name: "Wawa-VFX", 18 | fileName: (format) => `wawa-vfx.${format}.js`, 19 | }, 20 | rollupOptions: { 21 | external: [ 22 | "react", 23 | "react/jsx-runtime", 24 | "react-dom", 25 | "react-dom/client", 26 | "@react-three/fiber", 27 | "three", 28 | ], // Don't bundle these 29 | output: { 30 | globals: { 31 | react: "React", 32 | "react-dom": "ReactDOM", 33 | "react-dom/client": "ReactDOMClient", 34 | "react/jsx-runtime": "react/jsx-runtime", 35 | "@react-three/fiber": "reactThreeFiber", 36 | three: "THREE", 37 | }, 38 | }, 39 | }, 40 | }, 41 | }); 42 | --------------------------------------------------------------------------------