react-smokey-fluid-cursor 1.0.3 β†’ 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,181 +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
32
- ```
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.
33
45
 
34
- ---
46
+ ### New, optional
35
47
 
36
- ## πŸ“Έ Demo
48
+ ```tsx
49
+ const fluid = useRef<FluidHandle>(null);
37
50
 
38
- Also see more details in [Example](https://github.com/faraasat/react-smokey-fluid-cursor/tree/main/example):
51
+ <SmokeyFluidCursor ref={fluid} scoped config={{ palette: ["#ff4ecd"] }} />;
52
+ fluid.current?.pause();
53
+ fluid.current?.setConfig({ curl: 30 });
54
+ ```
39
55
 
40
- ![Demo](https://github.com/faraasat/react-smokey-fluid-cursor/blob/main/images/demo.gif)
56
+ ## Why
41
57
 
42
- ---
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.
43
62
 
44
- ## πŸš€ Quick Start
63
+ > Not using React? See
64
+ > [`smokey-fluid-cursor`](https://github.com/faraasat/smokey-fluid-cursor).
45
65
 
46
- ### **React (CRA)**
66
+ ## Installation
47
67
 
48
- ```tsx
49
- // src/App.tsx|jsx
50
- import React from "react";
68
+ ```bash
69
+ npm install react-smokey-fluid-cursor
70
+ ```
51
71
 
52
- import { SmokeyFluidCursor } from "react-smokey-fluid-cursor";
72
+ <details>
73
+ <summary>yarn / pnpm / bun</summary>
53
74
 
54
- function App() {
55
- return (
56
- <div className="App">
57
- {/* Place observer once globally */}
58
- <SmokeyFluidCursor />
59
- </div>
60
- );
61
- }
75
+ ```bash
76
+ yarn add react-smokey-fluid-cursor
77
+ pnpm add react-smokey-fluid-cursor
78
+ bun add react-smokey-fluid-cursor
62
79
  ```
80
+ </details>
63
81
 
64
- ### **Vite + React**
82
+ **Peer dependencies:** `react >= 17`, `react-dom >= 17`.
65
83
 
66
- ```tsx
67
- // src/main.tsx|jsx
68
- import React from "react";
84
+ ## Quick start
69
85
 
86
+ ```tsx
70
87
  import { SmokeyFluidCursor } from "react-smokey-fluid-cursor";
71
88
 
72
- function Main() {
89
+ export default function Layout({ children }) {
73
90
  return (
74
91
  <>
75
92
  <SmokeyFluidCursor />
93
+ {children}
76
94
  </>
77
95
  );
78
96
  }
79
-
80
- export default Main;
81
97
  ```
82
98
 
83
- ### **Next.js Pages Router**
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:
84
110
 
85
111
  ```tsx
86
- // pages/_app.tsx|jsx
87
- import type { AppProps } from "next/app";
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
125
+
126
+ Grab a ref for a live handle β€” pause, resume or retune without remounting:
88
127
 
128
+ ```tsx
129
+ import { useRef } from "react";
89
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);
90
135
 
91
- function MyApp({ Component, pageProps }: AppProps) {
92
136
  return (
93
137
  <>
94
- {/* Global ad observer */}
95
- <SmokeyFluidCursor />
96
- <Component {...pageProps} />
138
+ <SmokeyFluidCursor ref={fluid} />
139
+ <button onClick={() => fluid.current?.pause()}>Pause</button>
140
+ <button onClick={() => fluid.current?.setConfig({ curl: 30 })}>Swirl</button>
97
141
  </>
98
142
  );
99
143
  }
144
+ ```
145
+
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. |
154
+
155
+ ### The hook
100
156
 
101
- export default MyApp;
157
+ For full control over where the effect lives:
158
+
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 }} />;
166
+ }
102
167
  ```
103
168
 
104
- ### **Next.js (App Router)**
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.
182
+
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; }
199
+ ```
200
+
201
+ Alternatively, lift the canvas above your background and push your content
202
+ above the canvas:
105
203
 
106
204
  ```tsx
107
- // app/layout.tsx|jsx
108
- import "./globals.css";
205
+ initFluid({ zIndex: 0 });
206
+ ```
207
+ ```css
208
+ main { position: relative; z-index: 1; }
209
+ ```
109
210
 
110
- 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.
111
213
 
112
- export default function RootLayout({
113
- children,
114
- }: {
115
- children: React.ReactNode;
116
- }) {
117
- return (
118
- <html lang="en">
119
- <body>
120
- <SmokeyFluidCursor />
121
- {children}
122
- </body>
123
- </html>
124
- );
125
- }
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 }} />
126
281
  ```
127
282
 
128
- ---
283
+ ## Accessibility
129
284
 
130
- ## βš™οΈ Configuration Options
131
-
132
- Customize the fluid simulation with these configuration options:
133
-
134
- | Property | Default Value | Description |
135
- | --------------------- | ----------------------------- | ---------------------------------------------- |
136
- | `id` | `"react-smokey-fluid-cursor"` | Canvas element ID |
137
- | `simResolution` | `128` | Simulation resolution (higher = more detailed) |
138
- | `dyeResolution` | `512` | Dye/color resolution |
139
- | `densityDissipation` | `0.98` | How quickly colors fade (0–1) |
140
- | `velocityDissipation` | `0.98` | How quickly movement slows down |
141
- | `pressureIteration` | `10` | Pressure solver iterations |
142
- | `curl` | `30` | Vorticity/swirl intensity |
143
- | `splatRadius` | `0.25` | Size of cursor splats |
144
- | `splatForce` | `6000` | Force of cursor movements |
145
- | `shading` | `true` | Enable 3D lighting effects |
146
- | `colorUpdateSpeed` | `0.5` | Speed of color transitions |
147
- | `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.
148
288
 
149
- ---
289
+ ## Browser support
150
290
 
151
- ## 🌟 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.
152
294
 
153
- - **Real-time Fluid Dynamics**: Physics-based simulation using Navier-Stokes equations
154
- - **WebGL Accelerated**: High-performance rendering for smooth 60fps
155
- - **Interactive**: Responds to mouse and touch movements
156
- - **Customizable**: Extensive configuration options
157
- - **Mobile Support**: Touch-optimized interactions
158
- - **Auto-scaling**: Adapts to screen size and pixel ratio
159
- - **Color Cycling**: Dynamic, evolving color palettes
160
- - **3D Lighting**: Optional shading for depth perception
295
+ ## Contributing
161
296
 
162
- ---
297
+ Issues and pull requests are welcome.
163
298
 
164
- ## 🎯 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
+ ```
165
307
 
166
- - **Website Backgrounds**: Immersive animated backgrounds
167
- - **Cursor Effects**: Enhanced user interaction feedback
168
- - **Data Visualization**: Fluid-based data representations
169
- - **Art Installations**: Digital art and creative coding
170
- - **Game Effects**: Atmospheric and UI effects
171
- - **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.
172
311
 
173
- ---
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
174
328
 
175
- ## πŸ§‘β€πŸ’» 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.
176
332
 
177
- Built and maintained by [**Farasat Ali**](https://www.farasat.me)
333
+ ## License
178
334
 
179
- - Website: [www.farasat.me](https://www.farasat.me)
180
- - LinkedIn: [linkedin.com/in/faraasat](https://linkedin.com/in/faraasat)
181
- - GitHub: [github.com/faraasat](https://github.com/faraasat)
335
+ [MIT](./LICENSE) Β© [Farasat Ali](https://github.com/faraasat)