react-smokey-fluid-cursor 1.0.4 β†’ 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Farasat Ali
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,183 +1,335 @@
1
- <h1 align="center">πŸ’¨ React Smokey Fluid Cursor</h1>
2
-
3
1
  <p align="center">
4
- A beautiful, interactive fluid simulation that creates stunning visual effects following your cursor movements for your React and Next.js application on web. Built with WebGL for high-performance real-time fluid dynamics.
2
+ <img src="https://raw.githubusercontent.com/faraasat/react-smokey-fluid-cursor/main/.github/assets/banner.svg" alt="react-smokey-fluid-cursor" width="100%" />
5
3
  </p>
6
4
 
7
- ![npm version](https://img.shields.io/npm/v/react-smokey-fluid-cursor.svg)
8
- ![package size minified](https://img.shields.io/bundlephobia/min/react-smokey-fluid-cursor?style=plastic)
9
- [![Badge](https://data.jsdelivr.com/v1/package/npm/react-smokey-fluid-cursor/badge)](https://www.jsdelivr.com/package/npm/react-smokey-fluid-cursor)
10
- [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com)
5
+ <p align="center">
6
+ A GPU-accelerated fluid-simulation cursor trail for React and Next.js β€” one component, no configuration required.
7
+ </p>
11
8
 
12
- ![total downloads](https://img.shields.io/npm/dt/react-smokey-fluid-cursor.svg)
13
- ![total downloads per year](https://img.shields.io/npm/dy/react-smokey-fluid-cursor.svg)
14
- ![total downloads per week](https://img.shields.io/npm/dw/react-smokey-fluid-cursor.svg)
15
- ![total downloads per month](https://img.shields.io/npm/dm/react-smokey-fluid-cursor.svg)
16
- ![download-image](https://img.shields.io/npm/dm/react-smokey-fluid-cursor.svg)
9
+ <p align="center">
10
+ <a href="https://www.npmjs.com/package/react-smokey-fluid-cursor"><img alt="npm version" src="https://img.shields.io/npm/v/react-smokey-fluid-cursor?color=cb3837&label=npm&logo=npm"></a>
11
+ <a href="https://www.npmjs.com/package/react-smokey-fluid-cursor"><img alt="downloads" src="https://img.shields.io/npm/dm/react-smokey-fluid-cursor?color=cb3837&label=downloads"></a>
12
+ <a href="https://bundlephobia.com/package/react-smokey-fluid-cursor"><img alt="bundle size" src="https://img.shields.io/bundlephobia/minzip/react-smokey-fluid-cursor?label=minzipped"></a>
13
+ <a href="https://github.com/faraasat/react-smokey-fluid-cursor/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/faraasat/react-smokey-fluid-cursor/actions/workflows/ci.yml/badge.svg"></a>
14
+ <img alt="types" src="https://img.shields.io/badge/types-included-3178c6?logo=typescript&logoColor=white">
15
+ <a href="https://github.com/faraasat/react-smokey-fluid-cursor/blob/main/LICENSE"><img alt="license" src="https://img.shields.io/npm/l/react-smokey-fluid-cursor?color=blue"></a>
16
+ </p>
17
17
 
18
- [![react-smokey-fluid-cursor](https://nodei.co/npm/react-smokey-fluid-cursor.png)](https://npmjs.org/package/react-smokey-fluid-cursor)
18
+ <p align="center">
19
+ <a href="https://faraasat.github.io/react-smokey-fluid-cursor/"><b>Live demo</b></a> Β·
20
+ <a href="https://www.npmjs.com/package/react-smokey-fluid-cursor">npm</a> Β·
21
+ <a href="https://github.com/faraasat/react-smokey-fluid-cursor/blob/main/CHANGELOG.md">Changelog</a> Β·
22
+ <a href="https://github.com/faraasat/react-smokey-fluid-cursor/issues">Issues</a>
23
+ </p>
19
24
 
20
25
  ---
21
26
 
22
- ## πŸ“¦ Installation
27
+ ## Upgrading from 1.x
23
28
 
24
- ```bash
25
- npm i react-smokey-fluid-cursor
29
+ `2.0.0` adds scoped rendering, an imperative handle and live config updates.
30
+ `<SmokeyFluidCursor />` keeps its props, but four behaviours differ.
26
31
 
27
- yarn add react-smokey-fluid-cursor
32
+ | Change | Impact | What to do |
33
+ | --- | --- | --- |
34
+ | **The component renders `null`** when not `scoped` | The canvas is created and appended by the engine rather than returned from render, so it no longer appears where you placed the component | Nothing, unless you relied on its position in the DOM. Use `scoped` to keep it inside your own wrapper |
35
+ | **Placement is applied inline**, not via an injected `<style>` | CSS you wrote against `#smokey-fluid-canvas` no longer wins | Use the `position`, `zIndex`, `pointerEvents` and `className` options, or add `!important` |
36
+ | **Device pixel ratio is capped at 2** (`maxDpr`) | Slightly softer on 3x displays, markedly better frame rate and battery | `maxDpr: Infinity` restores the old behaviour |
37
+ | **`prefers-reduced-motion` is honoured** | Visitors who asked for reduced motion get a still canvas | `respectReducedMotion: false` opts out |
28
38
 
29
- pnpm i react-smokey-fluid-cursor
39
+ ### The effect is invisible after upgrading?
30
40
 
31
- bun add react-smokey-fluid-cursor
41
+ The canvas sits behind your content at `z-index: -9999`. If your page sets an
42
+ opaque background on `<body>`, it paints over the effect β€” see
43
+ [the section above](#the-effect-is-invisible-check-your-page-background). This
44
+ applied to 1.x too, but the package now warns about it in development.
45
+
46
+ ### New, optional
47
+
48
+ ```tsx
49
+ const fluid = useRef<FluidHandle>(null);
50
+
51
+ <SmokeyFluidCursor ref={fluid} scoped config={{ palette: ["#ff4ecd"] }} />;
52
+ fluid.current?.pause();
53
+ fluid.current?.setConfig({ curl: 30 });
32
54
  ```
33
55
 
34
- ---
56
+ ## Why
35
57
 
36
- ## πŸ“Έ Demo
58
+ A real-time Navier–Stokes fluid solver running in WebGL, wired to your pointer
59
+ and wrapped as a single React component. It cleans up after itself on unmount,
60
+ survives React StrictMode's double-invoke, and degrades quietly on devices
61
+ without WebGL instead of crashing your page.
37
62
 
38
- Checkout demo here: [Demo](https://react-smokey-fluid-cursor.vercel.app/)
63
+ > Not using React? See
64
+ > [`smokey-fluid-cursor`](https://github.com/faraasat/smokey-fluid-cursor).
39
65
 
40
- Also see more details in [Example](https://github.com/faraasat/react-smokey-fluid-cursor/tree/main/example):
66
+ ## Installation
41
67
 
42
- ![Demo](https://github.com/faraasat/react-smokey-fluid-cursor/blob/main/images/demo.gif)
68
+ ```bash
69
+ npm install react-smokey-fluid-cursor
70
+ ```
43
71
 
44
- ---
72
+ <details>
73
+ <summary>yarn / pnpm / bun</summary>
45
74
 
46
- ## πŸš€ Quick Start
75
+ ```bash
76
+ yarn add react-smokey-fluid-cursor
77
+ pnpm add react-smokey-fluid-cursor
78
+ bun add react-smokey-fluid-cursor
79
+ ```
80
+ </details>
47
81
 
48
- ### **React (CRA)**
82
+ **Peer dependencies:** `react >= 17`, `react-dom >= 17`.
49
83
 
50
- ```tsx
51
- // src/App.tsx|jsx
52
- import React from "react";
84
+ ## Quick start
53
85
 
86
+ ```tsx
54
87
  import { SmokeyFluidCursor } from "react-smokey-fluid-cursor";
55
88
 
56
- function App() {
89
+ export default function Layout({ children }) {
57
90
  return (
58
- <div className="App">
59
- {/* Place observer once globally */}
91
+ <>
60
92
  <SmokeyFluidCursor />
61
- </div>
93
+ {children}
94
+ </>
62
95
  );
63
96
  }
64
97
  ```
65
98
 
66
- ### **Vite + React**
99
+ That is the whole integration. The component renders nothing itself β€” it
100
+ creates a `fixed`, full-viewport canvas behind your content with
101
+ `pointer-events: none`, and tears it down on unmount.
102
+
103
+ > **Next.js App Router:** the package ships the `"use client"` directive, so it
104
+ > can be imported straight into a server component.
105
+
106
+ ## Scope it to one section
107
+
108
+ Pass `scoped` and an `absolute` position, and the effect stays inside the
109
+ component's own wrapper instead of covering the page:
67
110
 
68
111
  ```tsx
69
- // src/main.tsx|jsx
70
- import React from "react";
112
+ <SmokeyFluidCursor
113
+ scoped
114
+ style={{ height: 320, borderRadius: 16 }}
115
+ config={{ position: "absolute", zIndex: 0, palette: ["#4ea8ff", "#7c4dff"] }}
116
+ >
117
+ <h2>Hover me</h2>
118
+ </SmokeyFluidCursor>
119
+ ```
120
+
121
+ When `scoped`, the wrapper gets `position: relative` and `overflow: hidden`
122
+ automatically, so the fluid is clipped to it.
123
+
124
+ ## Controlling it
71
125
 
126
+ Grab a ref for a live handle β€” pause, resume or retune without remounting:
127
+
128
+ ```tsx
129
+ import { useRef } from "react";
72
130
  import { SmokeyFluidCursor } from "react-smokey-fluid-cursor";
131
+ import type { FluidHandle } from "react-smokey-fluid-cursor";
132
+
133
+ function Page() {
134
+ const fluid = useRef<FluidHandle>(null);
73
135
 
74
- function Main() {
75
136
  return (
76
137
  <>
77
- <SmokeyFluidCursor />
138
+ <SmokeyFluidCursor ref={fluid} />
139
+ <button onClick={() => fluid.current?.pause()}>Pause</button>
140
+ <button onClick={() => fluid.current?.setConfig({ curl: 30 })}>Swirl</button>
78
141
  </>
79
142
  );
80
143
  }
81
-
82
- export default Main;
83
144
  ```
84
145
 
85
- ### **Next.js Pages Router**
146
+ | Method | Description |
147
+ | --- | --- |
148
+ | `pause()` / `resume()` | Freeze or restart the simulation. |
149
+ | `isPaused()` | Current state. |
150
+ | `setConfig(partial)` | Retune in place, no remount. |
151
+ | `splat(x, y, color?)` | Inject a splash, in CSS pixels relative to the canvas. |
152
+ | `dispose()` | Tear down early (the component already does this on unmount). |
153
+ | `canvas` | The canvas being rendered into. |
86
154
 
87
- ```tsx
88
- // pages/_app.tsx|jsx
89
- import type { AppProps } from "next/app";
155
+ ### The hook
90
156
 
91
- import { SmokeyFluidCursor } from "react-smokey-fluid-cursor";
157
+ For full control over where the effect lives:
92
158
 
93
- function MyApp({ Component, pageProps }: AppProps) {
94
- return (
95
- <>
96
- {/* Global ad observer */}
97
- <SmokeyFluidCursor />
98
- <Component {...pageProps} />
99
- </>
100
- );
159
+ ```tsx
160
+ import { useSmokeyFluidCursor } from "react-smokey-fluid-cursor";
161
+
162
+ function Hero() {
163
+ const ref = useRef<HTMLDivElement>(null);
164
+ useSmokeyFluidCursor({ position: "absolute" }, ref);
165
+ return <div ref={ref} style={{ position: "relative", height: 300 }} />;
101
166
  }
167
+ ```
168
+
169
+ ## Updating config
170
+
171
+ Cheap options (`curl`, `splatForce`, `palette`, `colorIntensity`, dissipation,
172
+ `paused`, `zIndex`, …) are pushed straight into the running simulation.
173
+
174
+ Structural options (`id`, `position`, `simResolution`, `dyeResolution`) rebuild
175
+ it, because they reallocate buffers or move DOM. Changing those every render
176
+ would be expensive, so keep them stable.
177
+
178
+ ## The effect is invisible? Check your page background
179
+
180
+ This is the single most common integration problem, and it looks like the
181
+ package is broken when it is not.
102
182
 
103
- export default MyApp;
183
+ The canvas defaults to `z-index: -9999` so it sits behind your content. Per the
184
+ CSS painting order, a negatively-stacked element paints **above the root
185
+ background but below the background of block-level descendants** β€” so this
186
+ extremely common setup hides the effect completely:
187
+
188
+ ```css
189
+ /* βœ— body's background paints straight over the canvas */
190
+ body { background: #0b0f17; }
191
+ ```
192
+
193
+ Put the page background on `<html>` instead:
194
+
195
+ ```css
196
+ /* βœ“ the canvas paints above the root background, below your content */
197
+ html { background: #0b0f17; }
198
+ body { background: transparent; }
104
199
  ```
105
200
 
106
- ### **Next.js (App Router)**
201
+ Alternatively, lift the canvas above your background and push your content
202
+ above the canvas:
107
203
 
108
204
  ```tsx
109
- // app/layout.tsx|jsx
110
- import "./globals.css";
205
+ initFluid({ zIndex: 0 });
206
+ ```
207
+ ```css
208
+ main { position: relative; z-index: 1; }
209
+ ```
111
210
 
112
- import { SmokeyFluidCursor } from "react-smokey-fluid-cursor";
211
+ In development the package detects this and warns in the console rather than
212
+ leaving you with a blank screen.
113
213
 
114
- export default function RootLayout({
115
- children,
116
- }: {
117
- children: React.ReactNode;
118
- }) {
119
- return (
120
- <html lang="en">
121
- <body>
122
- <SmokeyFluidCursor />
123
- {children}
124
- </body>
125
- </html>
126
- );
127
- }
214
+ ## Configuration
215
+
216
+ Every option is optional.
217
+
218
+ ### Mounting & placement
219
+
220
+ | Option | Type | Default | Description |
221
+ | --- | --- | --- | --- |
222
+ | `container` | `HTMLElement \| string` | `document.body` | Where to create the canvas. Set automatically when `scoped`. |
223
+ | `canvas` | `HTMLCanvasElement \| string` | β€” | Render into an existing canvas instead. |
224
+ | `id` | `string` | `"smokey-fluid-canvas"` | Id assigned to the canvas. |
225
+ | `position` | `"fixed" \| "absolute" \| "relative" \| "static"` | `"fixed"` | `absolute` confines the effect to its container. |
226
+ | `zIndex` | `number` | `-9999` | Stacking order. |
227
+ | `pointerEvents` | `boolean` | `false` | Whether the canvas swallows clicks. |
228
+ | `className` | `string` | β€” | Extra class on the canvas. |
229
+
230
+ ### Performance & accessibility
231
+
232
+ | Option | Type | Default | Description |
233
+ | --- | --- | --- | --- |
234
+ | `maxDpr` | `number` | `2` | Caps the device pixel ratio. Uncapped, a 3x phone renders **nine times** the pixels of a 1x display. |
235
+ | `pauseOnHidden` | `boolean` | `true` | Stop the loop while the tab is backgrounded. |
236
+ | `respectReducedMotion` | `boolean` | `true` | Start paused for `prefers-reduced-motion: reduce`. |
237
+ | `paused` | `boolean` | `false` | Start frozen. |
238
+
239
+ ### Appearance
240
+
241
+ | Option | Type | Default | Description |
242
+ | --- | --- | --- | --- |
243
+ | `palette` | `string[]` | `null` | Hex colours to draw from, e.g. `["#ff4ecd"]`. Omit for random hues. |
244
+ | `colorIntensity` | `number` | `0.15` | Brightness multiplier. |
245
+ | `backColor` | `{ r, g, b }` | `{ r: 0, g: 0, b: 0 }` | Canvas background. |
246
+ | `transparent` | `boolean` | `true` | Blend with the page background. |
247
+ | `shading` | `boolean` | `true` | Lighting, for depth. |
248
+ | `colorUpdateSpeed` | `number` | `10` | How fast the palette rotates. |
249
+
250
+ ### Simulation
251
+
252
+ | Option | Type | Default | Description |
253
+ | --- | --- | --- | --- |
254
+ | `simResolution` | `number` | `128` | Velocity/pressure grid. |
255
+ | `dyeResolution` | `number` | `1440` | Colour buffer resolution β€” the main quality/cost dial. |
256
+ | `densityDissipation` | `number` | `3.5` | How fast colour fades. |
257
+ | `velocityDissipation` | `number` | `2` | How fast motion slows. |
258
+ | `pressure` | `number` | `0.1` | Initial pressure multiplier. |
259
+ | `pressureIteration` | `number` | `20` | Jacobi iterations. |
260
+ | `curl` | `number` | `10` | Vorticity confinement β€” the swirliness. |
261
+ | `splatRadius` | `number` | `0.5` | Size of each splat. |
262
+ | `splatForce` | `number` | `6000` | Force per splat. |
263
+
264
+ ## Lifecycle
265
+
266
+ On unmount the component stops the render loop, detaches its window listeners,
267
+ releases the WebGL context and removes the canvas it created. Mounting and
268
+ unmounting repeatedly β€” including StrictMode's development double-invoke β€”
269
+ does not stack simulations.
270
+
271
+ ## Performance
272
+
273
+ The big levers, in order of impact:
274
+
275
+ 1. **`maxDpr`** β€” already capped at `2`. Drop to `1` for the weakest devices.
276
+ 2. **`dyeResolution`** β€” `512` is much cheaper and still looks good.
277
+ 3. **`pressureIteration`** β€” `10` roughly halves the solver cost.
278
+
279
+ ```tsx
280
+ <SmokeyFluidCursor config={{ maxDpr: 1, dyeResolution: 512, pressureIteration: 10 }} />
128
281
  ```
129
282
 
130
- ---
283
+ ## Accessibility
131
284
 
132
- ## βš™οΈ Configuration Options
133
-
134
- Customize the fluid simulation with these configuration options:
135
-
136
- | Property | Default Value | Description |
137
- | --------------------- | ----------------------------- | ---------------------------------------------- |
138
- | `id` | `"react-smokey-fluid-cursor"` | Canvas element ID |
139
- | `simResolution` | `128` | Simulation resolution (higher = more detailed) |
140
- | `dyeResolution` | `512` | Dye/color resolution |
141
- | `densityDissipation` | `0.98` | How quickly colors fade (0–1) |
142
- | `velocityDissipation` | `0.98` | How quickly movement slows down |
143
- | `pressureIteration` | `10` | Pressure solver iterations |
144
- | `curl` | `30` | Vorticity/swirl intensity |
145
- | `splatRadius` | `0.25` | Size of cursor splats |
146
- | `splatForce` | `6000` | Force of cursor movements |
147
- | `shading` | `true` | Enable 3D lighting effects |
148
- | `colorUpdateSpeed` | `0.5` | Speed of color transitions |
149
- | `transparent` | `false` | Transparent background |
285
+ A full-screen animation is a real problem for people with vestibular
286
+ disorders. By default this honours `prefers-reduced-motion: reduce` by starting
287
+ paused, and reacts if the preference changes while the page is open.
150
288
 
151
- ---
289
+ ## Browser support
152
290
 
153
- ## 🌟 Features
291
+ Requires WebGL (WebGL 2 when available, WebGL 1 fallback). Without it the
292
+ component logs a warning and renders nothing β€” it never throws, so a decorative
293
+ effect cannot take down your app.
154
294
 
155
- - **Real-time Fluid Dynamics**: Physics-based simulation using Navier-Stokes equations
156
- - **WebGL Accelerated**: High-performance rendering for smooth 60fps
157
- - **Interactive**: Responds to mouse and touch movements
158
- - **Customizable**: Extensive configuration options
159
- - **Mobile Support**: Touch-optimized interactions
160
- - **Auto-scaling**: Adapts to screen size and pixel ratio
161
- - **Color Cycling**: Dynamic, evolving color palettes
162
- - **3D Lighting**: Optional shading for depth perception
295
+ ## Contributing
163
296
 
164
- ---
297
+ Issues and pull requests are welcome.
165
298
 
166
- ## 🎯 Use Cases
299
+ ```bash
300
+ git clone https://github.com/faraasat/react-smokey-fluid-cursor.git
301
+ cd react-smokey-fluid-cursor
302
+ npm install
303
+ npm test # vitest unit tests
304
+ npm run typecheck # tsc --noEmit
305
+ npm run build # tsup
306
+ ```
167
307
 
168
- - **Website Backgrounds**: Immersive animated backgrounds
169
- - **Cursor Effects**: Enhanced user interaction feedback
170
- - **Data Visualization**: Fluid-based data representations
171
- - **Art Installations**: Digital art and creative coding
172
- - **Game Effects**: Atmospheric and UI effects
173
- - **Product Demos**: Eye-catching technology showcases
308
+ End-to-end tests run against the built demo in a real browser (desktop and
309
+ mobile viewports), and cover the things unit tests cannot: layout, CSS and
310
+ keyboard behaviour.
174
311
 
175
- ---
312
+ ```bash
313
+ npm run build && npm --prefix example install && npm --prefix example run build
314
+ npm run test:e2e # playwright
315
+ npm run test:e2e:ui # interactive
316
+ ```
317
+
318
+ To run the demo site against your local build:
319
+
320
+ ```bash
321
+ npm run example:dev
322
+ ```
323
+
324
+ Releases are manual β€” nothing publishes on a push to `main`. Maintainers run
325
+ the **Release** workflow from the Actions tab.
326
+
327
+ ## Privacy
176
328
 
177
- ## πŸ§‘β€πŸ’» Author
329
+ The published package contains **no telemetry**. The demo site at
330
+ [faraasat.github.io/react-smokey-fluid-cursor](https://faraasat.github.io/react-smokey-fluid-cursor/) uses
331
+ Google Analytics and Aptabase; the library itself never phones home.
178
332
 
179
- Built and maintained by [**Farasat Ali**](https://www.farasat.me)
333
+ ## License
180
334
 
181
- - Website: [www.farasat.me](https://www.farasat.me)
182
- - LinkedIn: [linkedin.com/in/faraasat](https://linkedin.com/in/faraasat)
183
- - GitHub: [github.com/faraasat](https://github.com/faraasat)
335
+ [MIT](./LICENSE) Β© [Farasat Ali](https://github.com/faraasat)