├── .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 | 
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------