apex-ui-kit 2.8.0 → 2.10.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/ReadMe.md +9 -4
- package/dist/ApexUI-Kit/templates/ArcGallery/ArcGallery.jsx +29 -11
- package/dist/ApexUI-Kit/templates/CircularCarousel/CircularCarousel.jsx +0 -1
- package/dist/ApexUI-Kit/templates/FlipClock/FlipClock.jsx +3 -2
- package/dist/ApexUI-Kit/templates/FluidCursor/FluidCursor.jsx +418 -0
- package/dist/ApexUI-Kit/templates/GlareCard/GlareCard.jsx +25 -18
- package/dist/ApexUI-Kit/templates/GlassLens/GlassLens.jsx +0 -1
- package/dist/ApexUI-Kit/templates/LiquidGlass/LiquidGlass.jsx +444 -0
- package/dist/ApexUI-Kit/templates/ScrollStack/ScrollStack.jsx +3 -3
- package/dist/ApexUI-Kit/templates/SketchReveal/SketchReveal.jsx +257 -0
- package/dist/ApexUI-Kit/templates/SunriseBackground/SunriseBackground.jsx +68 -2
- package/dist/cli/index.js +3 -0
- package/package.json +2 -2
package/ReadMe.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/apex-ui-kit)
|
|
5
5
|
[](./Licence.md)
|
|
6
6
|
|
|
7
|
-
**
|
|
7
|
+
**118 free, animated UI components for React.** Pick one, add it with a single command, and the source lands in your project so you can read it and change anything.
|
|
8
8
|
|
|
9
9
|
Built with React, Tailwind CSS and Framer Motion, plus GSAP, Canvas and WebGL where a component needs them.
|
|
10
10
|
|
|
@@ -15,7 +15,7 @@ Built with React, Tailwind CSS and Framer Motion, plus GSAP, Canvas and WebGL wh
|
|
|
15
15
|
|
|
16
16
|
## Why ApexUI
|
|
17
17
|
|
|
18
|
-
- **
|
|
18
|
+
- **118 components** across backgrounds, cards, text effects, navigation, buttons, interactive UI, AI interfaces, loaders and maps.
|
|
19
19
|
- **You own the code.** The CLI copies plain `.jsx` files into your project, no runtime package to keep in sync.
|
|
20
20
|
- **Every docs page has** a live preview, a playground to tweak props, a Desktop / Tablet / Mobile preview, the full props table and a ready prompt you can paste into ChatGPT or Claude.
|
|
21
21
|
- **Smooth on real devices.** Animations stick to transform and opacity, respect reduced motion, and work with mouse, touch and keyboard.
|
|
@@ -196,7 +196,7 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
196
196
|
| [Magnetic Button](https://apex-ui.in/components/magnetic-button) | `magnetic-button` |
|
|
197
197
|
| [Star Border](https://apex-ui.in/components/star-border) | `star-border` |
|
|
198
198
|
|
|
199
|
-
### Interactive (
|
|
199
|
+
### Interactive (33)
|
|
200
200
|
|
|
201
201
|
| Component | CLI name |
|
|
202
202
|
| --- | --- |
|
|
@@ -215,10 +215,12 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
215
215
|
| [Elastic Slider](https://apex-ui.in/components/elastic-slider) | `elastic-slider` |
|
|
216
216
|
| [FAQ Accordion](https://apex-ui.in/components/faq-accordion) | `faq-accordion` |
|
|
217
217
|
| [Flip Book](https://apex-ui.in/components/flip-book) | `flip-book` |
|
|
218
|
+
| [Fluid Cursor](https://apex-ui.in/components/fluid-cursor) | `fluid-cursor` |
|
|
218
219
|
| [Glass Lens](https://apex-ui.in/components/glass-lens) | `glass-lens` |
|
|
219
220
|
| [Image Compare](https://apex-ui.in/components/image-compare) | `image-compare` |
|
|
220
221
|
| [Infinite Canvas](https://apex-ui.in/components/infinite-canvas) | `infinite-canvas` |
|
|
221
222
|
| [Liquid Blob 3D](https://apex-ui.in/components/liquid-blob-3d) | `liquid-blob-3d` |
|
|
223
|
+
| [Liquid Glass](https://apex-ui.in/components/liquid-glass) | `liquid-glass` |
|
|
222
224
|
| [Magnet Lines](https://apex-ui.in/components/magnet-lines) | `magnet-lines` |
|
|
223
225
|
| [Magnetic Cursor](https://apex-ui.in/components/magnetic-cursor) | `magnetic-cursor` |
|
|
224
226
|
| [Marquee 3D](https://apex-ui.in/components/marquee-3d) | `marquee-3d` |
|
|
@@ -226,6 +228,7 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
226
228
|
| [Particle Morph](https://apex-ui.in/components/particle-morph) | `particle-morph` |
|
|
227
229
|
| [Pointer Follower](https://apex-ui.in/components/pointer-follower) | `pointer-follower` |
|
|
228
230
|
| [Receipt Printer](https://apex-ui.in/components/receipt-printer) | `receipt-printer` |
|
|
231
|
+
| [Sketch Reveal](https://apex-ui.in/components/sketch-reveal) | `sketch-reveal` |
|
|
229
232
|
| [Stepper](https://apex-ui.in/components/stepper) | `stepper` |
|
|
230
233
|
| [Toast Stack](https://apex-ui.in/components/toast-stack) | `toast-stack` |
|
|
231
234
|
| [ToolTip](https://apex-ui.in/components/tool-tip) | `tooltip` |
|
|
@@ -259,8 +262,10 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
259
262
|
| [Dot Globe](https://apex-ui.in/components/dot-globe) | `dot-globe` |
|
|
260
263
|
| [World Map](https://apex-ui.in/components/world-map) | `world-map` |
|
|
261
264
|
|
|
262
|
-
## What's new in 2.
|
|
265
|
+
## What's new in 2.10
|
|
263
266
|
|
|
267
|
+
- New in 2.10: Fluid Cursor (a real-time WebGL fluid sim that trails the cursor) and Sketch Reveal (a photo turns into a glowing sketch and burns away into the next one as you scroll).
|
|
268
|
+
- New in 2.9: Liquid Glass (iOS style WebGL glass), 10 live page templates on the site, Arc Gallery auto scroll, Scroll Stack on the page scroll, Sunrise trees and birds.
|
|
264
269
|
- New in 2.8: Glass Lens (WebGL liquid glass), Dither Waves (WebGL 1-bit background), Infinite Canvas, Image Accordion, Orbit Icons, Command Palette, Crossed Marquee (tech-stack bands with simple-icons) and Theme Switcher (8 themes with a circle reveal).
|
|
265
270
|
- Toast Stack and Radial Menu no longer blink in production builds; Radial Menu, 3D Text Ring, Animated Tabs and Gooey Nav now fit 320px phones.
|
|
266
271
|
- 38 new components since 2.6, including WebGL and three.js (Sunrise Background, Horizon Glow, Circular Carousel, Arc Gallery, Galaxy 3D, Warp Tunnel, Wireframe Knot 3D, Marquee 3D, Distortion Image, Liquid Blob 3D, Particle Morph, Retro Grid 3D), Scroll Stack, Particle Text, Edge Blur, Magnetic Button, Radial Menu, the AI voice family (Wave, Bars, Ring, Blob), Gooey Nav (WebGL metaball pill), Coverflow 3D, Card Swap 3D, 3D Text Ring, Elastic Slider, Stepper and Pixel Transition.
|
|
@@ -3,9 +3,10 @@ import { Mesh, OrthographicCamera, PlaneGeometry, Scene, ShaderMaterial, SRGBCol
|
|
|
3
3
|
|
|
4
4
|
/*
|
|
5
5
|
* Arc Gallery: an endless row of image cards laid along a gentle arc and
|
|
6
|
-
* drawn in WebGL.
|
|
7
|
-
*
|
|
8
|
-
* the edges fade out, and a title sits under
|
|
6
|
+
* drawn in WebGL. It drifts on its own (pausing on hover); drag, flick or
|
|
7
|
+
* swipe sideways to move it and it eases to a stop on a card. While it
|
|
8
|
+
* moves the cards ripple and bow, the edges fade out, and a title sits under
|
|
9
|
+
* each card. Arrow keys step. Vertical wheel scrolling is left to the page.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
const IK = "https://ik.imagekit.io/ny6yinyut/apexui/assets";
|
|
@@ -54,13 +55,13 @@ void main(){
|
|
|
54
55
|
gl_FragColor = vec4(col, edge * f * uReady);
|
|
55
56
|
}`;
|
|
56
57
|
|
|
57
|
-
const ArcGallery = ({ items = DEFAULT_ITEMS, bend = 1, cardHeight = 0.62, aspectRatio = 0.78, gap = 0.22, cornerRadius = 0.05, wave = 1, pull = 1, fadeEdges = 0.14, ease = 0.08, snap = true, showTitles = true, height = "26rem", onChange, className = "" }) => {
|
|
58
|
+
const ArcGallery = ({ items = DEFAULT_ITEMS, bend = 1, cardHeight = 0.62, aspectRatio = 0.78, gap = 0.22, cornerRadius = 0.05, wave = 1, pull = 1, fadeEdges = 0.14, ease = 0.08, snap = true, autoScroll = 40, pauseOnHover = true, showTitles = true, height = "26rem", onChange, className = "" }) => {
|
|
58
59
|
const wrap = useRef(null);
|
|
59
60
|
const labels = useRef([]);
|
|
60
61
|
const api = useRef(null);
|
|
61
62
|
const [active, setActive] = useState(0);
|
|
62
63
|
const live = useRef({});
|
|
63
|
-
live.current = { bend, wave, pull, fadeEdges, ease, snap, onChange };
|
|
64
|
+
live.current = { bend, wave, pull, fadeEdges, ease, snap, autoScroll, pauseOnHover, onChange };
|
|
64
65
|
const list = items.length ? items : DEFAULT_ITEMS;
|
|
65
66
|
const key = list.map((i) => i.image).join("|");
|
|
66
67
|
|
|
@@ -84,7 +85,6 @@ const ArcGallery = ({ items = DEFAULT_ITEMS, bend = 1, cardHeight = 0.62, aspect
|
|
|
84
85
|
const textures = list.map((item) => {
|
|
85
86
|
const entry = { tex: null, aspect: 1, at: 0 };
|
|
86
87
|
loader.load(item.image, (t) => {
|
|
87
|
-
t.colorSpace = SRGBColorSpace;
|
|
88
88
|
entry.tex = t;
|
|
89
89
|
entry.aspect = t.image.width / t.image.height;
|
|
90
90
|
entry.at = performance.now();
|
|
@@ -116,7 +116,8 @@ const ArcGallery = ({ items = DEFAULT_ITEMS, bend = 1, cardHeight = 0.62, aspect
|
|
|
116
116
|
return { mesh, m };
|
|
117
117
|
};
|
|
118
118
|
|
|
119
|
-
const s = { scroll: 0, target: 0, vel: 0, pull: 0, drag: null, time: 0 };
|
|
119
|
+
const s = { scroll: 0, target: 0, vel: 0, pull: 0, drag: null, time: 0, hover: false, hold: 0 };
|
|
120
|
+
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
120
121
|
let cardW = 1;
|
|
121
122
|
let cardH = 1;
|
|
122
123
|
let spacing = 1;
|
|
@@ -172,6 +173,10 @@ const ArcGallery = ({ items = DEFAULT_ITEMS, bend = 1, cardHeight = 0.62, aspect
|
|
|
172
173
|
const o = live.current;
|
|
173
174
|
const dt = last ? Math.min(0.05, (now - last) / 1000) : 1 / 60;
|
|
174
175
|
last = now;
|
|
176
|
+
// Auto scroll drifts the row along, pausing while hovered, dragged or
|
|
177
|
+
// just after someone moved it by hand.
|
|
178
|
+
const auto = o.autoScroll && !reduced && !s.drag && !(o.pauseOnHover && s.hover) && now > s.hold;
|
|
179
|
+
if (auto) s.target += o.autoScroll * dt;
|
|
175
180
|
const before = s.scroll;
|
|
176
181
|
const follow = 1 - Math.pow(1 - Math.min(1, Math.max(0.01, o.ease)), dt * 60);
|
|
177
182
|
s.scroll += (s.target - s.scroll) * follow;
|
|
@@ -185,7 +190,7 @@ const ArcGallery = ({ items = DEFAULT_ITEMS, bend = 1, cardHeight = 0.62, aspect
|
|
|
185
190
|
const pullGoal = Math.max(-1, Math.min(1, s.vel / (spacing * 7))) * 0.32 * o.pull;
|
|
186
191
|
s.pull += (pullGoal - s.pull) * (1 - Math.exp(-dt / 0.12));
|
|
187
192
|
if (Math.abs(s.pull) < 0.0005 && !pullGoal) s.pull = 0;
|
|
188
|
-
let busy = s.pull !== 0 || s.scroll !== s.target || s.vel !== 0 || !!s.drag;
|
|
193
|
+
let busy = auto || s.pull !== 0 || s.scroll !== s.target || s.vel !== 0 || !!s.drag;
|
|
189
194
|
|
|
190
195
|
const first = Math.floor((s.scroll - view.w / 2) / spacing) - 1;
|
|
191
196
|
slots.forEach((slot, k) => {
|
|
@@ -251,6 +256,7 @@ const ArcGallery = ({ items = DEFAULT_ITEMS, bend = 1, cardHeight = 0.62, aspect
|
|
|
251
256
|
const onDown = (e) => {
|
|
252
257
|
if (e.button !== 0) return;
|
|
253
258
|
s.drag = { x: e.clientX, start: s.target, moved: false, samples: [] };
|
|
259
|
+
wake();
|
|
254
260
|
};
|
|
255
261
|
const onMove = (e) => {
|
|
256
262
|
const g = s.drag;
|
|
@@ -269,6 +275,7 @@ const ArcGallery = ({ items = DEFAULT_ITEMS, bend = 1, cardHeight = 0.62, aspect
|
|
|
269
275
|
const onUp = (e) => {
|
|
270
276
|
const g = s.drag;
|
|
271
277
|
s.drag = null;
|
|
278
|
+
s.hold = performance.now() + 1800;
|
|
272
279
|
if (!g) return;
|
|
273
280
|
if (g.moved && g.samples.length > 1) {
|
|
274
281
|
const a = g.samples[0];
|
|
@@ -286,10 +293,12 @@ const ArcGallery = ({ items = DEFAULT_ITEMS, bend = 1, cardHeight = 0.62, aspect
|
|
|
286
293
|
settle();
|
|
287
294
|
};
|
|
288
295
|
const onWheel = (e) => {
|
|
289
|
-
|
|
290
|
-
|
|
296
|
+
// Only sideways wheel / trackpad swipes move the row; vertical
|
|
297
|
+
// scrolling belongs to the page.
|
|
298
|
+
if (Math.abs(e.deltaX) <= Math.abs(e.deltaY)) return;
|
|
291
299
|
e.preventDefault();
|
|
292
|
-
s.target +=
|
|
300
|
+
s.target += e.deltaX * 1.2;
|
|
301
|
+
s.hold = performance.now() + 1800;
|
|
293
302
|
clearTimeout(snapTimer);
|
|
294
303
|
snapTimer = setTimeout(settle, 140);
|
|
295
304
|
wake();
|
|
@@ -299,6 +308,13 @@ const ArcGallery = ({ items = DEFAULT_ITEMS, bend = 1, cardHeight = 0.62, aspect
|
|
|
299
308
|
canvas.addEventListener("pointerup", onUp);
|
|
300
309
|
canvas.addEventListener("pointercancel", onUp);
|
|
301
310
|
canvas.addEventListener("wheel", onWheel, { passive: false });
|
|
311
|
+
const onEnter = () => (s.hover = true);
|
|
312
|
+
const onLeave = () => {
|
|
313
|
+
s.hover = false;
|
|
314
|
+
wake();
|
|
315
|
+
};
|
|
316
|
+
canvas.addEventListener("pointerenter", onEnter);
|
|
317
|
+
canvas.addEventListener("pointerleave", onLeave);
|
|
302
318
|
const io = new IntersectionObserver(([entry]) => {
|
|
303
319
|
visible = entry.isIntersecting;
|
|
304
320
|
wake();
|
|
@@ -316,6 +332,8 @@ const ArcGallery = ({ items = DEFAULT_ITEMS, bend = 1, cardHeight = 0.62, aspect
|
|
|
316
332
|
canvas.removeEventListener("pointerup", onUp);
|
|
317
333
|
canvas.removeEventListener("pointercancel", onUp);
|
|
318
334
|
canvas.removeEventListener("wheel", onWheel);
|
|
335
|
+
canvas.removeEventListener("pointerenter", onEnter);
|
|
336
|
+
canvas.removeEventListener("pointerleave", onLeave);
|
|
319
337
|
textures.forEach((t) => t.tex?.dispose());
|
|
320
338
|
slots.forEach((slot) => slot.m.dispose());
|
|
321
339
|
geometry.dispose();
|
|
@@ -124,7 +124,6 @@ const CircularCarousel = ({
|
|
|
124
124
|
},
|
|
125
125
|
});
|
|
126
126
|
loader.load(item.src, (t) => {
|
|
127
|
-
t.colorSpace = SRGBColorSpace;
|
|
128
127
|
m.uniforms.uTex.value = t;
|
|
129
128
|
const ia = t.image.width / t.image.height;
|
|
130
129
|
// Cover crop: show the middle of the image at the card's shape.
|
|
@@ -92,8 +92,9 @@ const FlipClock = ({ mode = "seconds", target, seconds = 3 * 86400 + 4 * 3600 +
|
|
|
92
92
|
|
|
93
93
|
return (
|
|
94
94
|
// The clock is sized in em; on narrow screens the font shrinks with the
|
|
95
|
-
// container (cqw) so every unit fits instead of being cut off.
|
|
96
|
-
|
|
95
|
+
// container (cqw) so every unit fits instead of being cut off. The min
|
|
96
|
+
// width keeps it from collapsing inside a shrink-to-fit parent.
|
|
97
|
+
<div className="flex w-full justify-center" style={{ containerType: "inline-size", minWidth: `min(${(size * parts.length * 2.55).toFixed(2)}rem, 92vw)` }}>
|
|
97
98
|
<div role="timer" aria-live="off" aria-label={parts.map(([l, v]) => `${v} ${l}`).join(", ")} className={`inline-flex items-start gap-[0.35em] font-mono font-bold ${className}`} style={{ fontSize: `min(${size}rem, ${(100 / (parts.length * 2.55)).toFixed(2)}cqw)` }}>
|
|
98
99
|
{parts.map(([label, value], i) => (
|
|
99
100
|
<React.Fragment key={label}>
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from "react";
|
|
2
|
+
import { Color, HalfFloatType, LinearFilter, Mesh, OrthographicCamera, PlaneGeometry, RGBAFormat, Scene, ShaderMaterial, Vector2, WebGLRenderer, WebGLRenderTarget } from "three";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Fluid Cursor: a real-time fluid simulation that trails the pointer, drawn
|
|
6
|
+
* in WebGL. Moving the cursor pushes coloured ink through the fluid; it
|
|
7
|
+
* swirls (vorticity), spreads (pressure) and slowly fades. A click blows a
|
|
8
|
+
* burst. It sits over its parent as a transparent layer and never blocks
|
|
9
|
+
* clicks, so content underneath stays usable.
|
|
10
|
+
*
|
|
11
|
+
* Props: colors (cycled; a rainbow when empty), simResolution, dyeResolution,
|
|
12
|
+
* dissipation (how fast the ink fades), velocityDissipation, curl (swirl),
|
|
13
|
+
* splatRadius, splatForce, idle (soft splashes when nobody moves),
|
|
14
|
+
* className, children.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const BASE_VERT = `
|
|
18
|
+
uniform vec2 texelSize;
|
|
19
|
+
varying vec2 vUv; varying vec2 vL; varying vec2 vR; varying vec2 vT; varying vec2 vB;
|
|
20
|
+
void main(){
|
|
21
|
+
vUv = uv;
|
|
22
|
+
vL = vUv - vec2(texelSize.x, 0.0);
|
|
23
|
+
vR = vUv + vec2(texelSize.x, 0.0);
|
|
24
|
+
vT = vUv + vec2(0.0, texelSize.y);
|
|
25
|
+
vB = vUv - vec2(0.0, texelSize.y);
|
|
26
|
+
gl_Position = vec4(position.xy, 0.0, 1.0);
|
|
27
|
+
}`;
|
|
28
|
+
|
|
29
|
+
const HEAD = `precision highp float;
|
|
30
|
+
varying vec2 vUv; varying vec2 vL; varying vec2 vR; varying vec2 vT; varying vec2 vB;
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
const SPLAT = `${HEAD}
|
|
34
|
+
uniform sampler2D uTarget; uniform float aspect; uniform vec3 color; uniform vec2 point; uniform float radius;
|
|
35
|
+
void main(){
|
|
36
|
+
vec2 p = vUv - point;
|
|
37
|
+
p.x *= aspect;
|
|
38
|
+
vec3 splat = exp(-dot(p, p) / radius) * color;
|
|
39
|
+
gl_FragColor = vec4(texture2D(uTarget, vUv).xyz + splat, 1.0);
|
|
40
|
+
}`;
|
|
41
|
+
|
|
42
|
+
const ADVECT = `${HEAD}
|
|
43
|
+
uniform sampler2D uVelocity; uniform sampler2D uSource; uniform vec2 velTexel; uniform float dt; uniform float dissipation;
|
|
44
|
+
void main(){
|
|
45
|
+
vec2 coord = vUv - dt * texture2D(uVelocity, vUv).xy * velTexel;
|
|
46
|
+
gl_FragColor = texture2D(uSource, coord) / (1.0 + dissipation * dt);
|
|
47
|
+
}`;
|
|
48
|
+
|
|
49
|
+
const DIVERGENCE = `${HEAD}
|
|
50
|
+
uniform sampler2D uVelocity;
|
|
51
|
+
void main(){
|
|
52
|
+
float L = texture2D(uVelocity, vL).x;
|
|
53
|
+
float R = texture2D(uVelocity, vR).x;
|
|
54
|
+
float T = texture2D(uVelocity, vT).y;
|
|
55
|
+
float B = texture2D(uVelocity, vB).y;
|
|
56
|
+
vec2 C = texture2D(uVelocity, vUv).xy;
|
|
57
|
+
// Walls: reflect velocity at the edges.
|
|
58
|
+
if (vL.x < 0.0) L = -C.x;
|
|
59
|
+
if (vR.x > 1.0) R = -C.x;
|
|
60
|
+
if (vT.y > 1.0) T = -C.y;
|
|
61
|
+
if (vB.y < 0.0) B = -C.y;
|
|
62
|
+
gl_FragColor = vec4(0.5 * (R - L + T - B), 0.0, 0.0, 1.0);
|
|
63
|
+
}`;
|
|
64
|
+
|
|
65
|
+
const CURL = `${HEAD}
|
|
66
|
+
uniform sampler2D uVelocity;
|
|
67
|
+
void main(){
|
|
68
|
+
float L = texture2D(uVelocity, vL).y;
|
|
69
|
+
float R = texture2D(uVelocity, vR).y;
|
|
70
|
+
float T = texture2D(uVelocity, vT).x;
|
|
71
|
+
float B = texture2D(uVelocity, vB).x;
|
|
72
|
+
gl_FragColor = vec4(0.5 * (R - L - T + B), 0.0, 0.0, 1.0);
|
|
73
|
+
}`;
|
|
74
|
+
|
|
75
|
+
const VORTICITY = `${HEAD}
|
|
76
|
+
uniform sampler2D uVelocity; uniform sampler2D uCurl; uniform float curl; uniform float dt;
|
|
77
|
+
void main(){
|
|
78
|
+
float L = texture2D(uCurl, vL).x;
|
|
79
|
+
float R = texture2D(uCurl, vR).x;
|
|
80
|
+
float T = texture2D(uCurl, vT).x;
|
|
81
|
+
float B = texture2D(uCurl, vB).x;
|
|
82
|
+
float C = texture2D(uCurl, vUv).x;
|
|
83
|
+
vec2 force = 0.5 * vec2(abs(T) - abs(B), abs(R) - abs(L));
|
|
84
|
+
force /= length(force) + 0.0001;
|
|
85
|
+
force *= curl * C;
|
|
86
|
+
force.y *= -1.0;
|
|
87
|
+
vec2 vel = texture2D(uVelocity, vUv).xy + force * dt;
|
|
88
|
+
gl_FragColor = vec4(clamp(vel, -1000.0, 1000.0), 0.0, 1.0);
|
|
89
|
+
}`;
|
|
90
|
+
|
|
91
|
+
const SCALE = `${HEAD}
|
|
92
|
+
uniform sampler2D uTexture; uniform float value;
|
|
93
|
+
void main(){ gl_FragColor = value * texture2D(uTexture, vUv); }`;
|
|
94
|
+
|
|
95
|
+
const PRESSURE = `${HEAD}
|
|
96
|
+
uniform sampler2D uPressure; uniform sampler2D uDivergence;
|
|
97
|
+
void main(){
|
|
98
|
+
float L = texture2D(uPressure, vL).x;
|
|
99
|
+
float R = texture2D(uPressure, vR).x;
|
|
100
|
+
float T = texture2D(uPressure, vT).x;
|
|
101
|
+
float B = texture2D(uPressure, vB).x;
|
|
102
|
+
float div = texture2D(uDivergence, vUv).x;
|
|
103
|
+
gl_FragColor = vec4((L + R + B + T - div) * 0.25, 0.0, 0.0, 1.0);
|
|
104
|
+
}`;
|
|
105
|
+
|
|
106
|
+
const GRADIENT = `${HEAD}
|
|
107
|
+
uniform sampler2D uPressure; uniform sampler2D uVelocity;
|
|
108
|
+
void main(){
|
|
109
|
+
float L = texture2D(uPressure, vL).x;
|
|
110
|
+
float R = texture2D(uPressure, vR).x;
|
|
111
|
+
float T = texture2D(uPressure, vT).x;
|
|
112
|
+
float B = texture2D(uPressure, vB).x;
|
|
113
|
+
vec2 vel = texture2D(uVelocity, vUv).xy - vec2(R - L, T - B);
|
|
114
|
+
gl_FragColor = vec4(vel, 0.0, 1.0);
|
|
115
|
+
}`;
|
|
116
|
+
|
|
117
|
+
// Shaded ink: a little light on the slopes of the dye gives it body.
|
|
118
|
+
const DISPLAY = `${HEAD}
|
|
119
|
+
uniform sampler2D uTexture; uniform vec2 dyeTexel;
|
|
120
|
+
void main(){
|
|
121
|
+
vec3 c = texture2D(uTexture, vUv).rgb;
|
|
122
|
+
vec3 lc = texture2D(uTexture, vUv - vec2(dyeTexel.x, 0.0)).rgb;
|
|
123
|
+
vec3 rc = texture2D(uTexture, vUv + vec2(dyeTexel.x, 0.0)).rgb;
|
|
124
|
+
vec3 tc = texture2D(uTexture, vUv + vec2(0.0, dyeTexel.y)).rgb;
|
|
125
|
+
vec3 bc = texture2D(uTexture, vUv - vec2(0.0, dyeTexel.y)).rgb;
|
|
126
|
+
float dx = length(rc) - length(lc);
|
|
127
|
+
float dy = length(tc) - length(bc);
|
|
128
|
+
vec3 n = normalize(vec3(dx, dy, length(dyeTexel)));
|
|
129
|
+
float diffuse = clamp(dot(n, vec3(0.0, 0.0, 1.0)) + 0.7, 0.7, 1.0);
|
|
130
|
+
c *= diffuse;
|
|
131
|
+
float a = clamp(max(c.r, max(c.g, c.b)), 0.0, 1.0);
|
|
132
|
+
gl_FragColor = vec4(c, a);
|
|
133
|
+
}`;
|
|
134
|
+
|
|
135
|
+
const hsvToRgb = (h, s, v) => {
|
|
136
|
+
const i = Math.floor(h * 6);
|
|
137
|
+
const f = h * 6 - i;
|
|
138
|
+
const p = v * (1 - s);
|
|
139
|
+
const q = v * (1 - f * s);
|
|
140
|
+
const t = v * (1 - (1 - f) * s);
|
|
141
|
+
return [
|
|
142
|
+
[v, t, p],
|
|
143
|
+
[q, v, p],
|
|
144
|
+
[p, v, t],
|
|
145
|
+
[p, q, v],
|
|
146
|
+
[t, p, v],
|
|
147
|
+
[v, p, q],
|
|
148
|
+
][i % 6];
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const FluidCursor = ({ colors = [], simResolution = 128, dyeResolution = 1024, dissipation = 1.6, velocityDissipation = 1.2, curl = 18, splatRadius = 0.22, splatForce = 6000, idle = true, className = "", children }) => {
|
|
152
|
+
const wrap = useRef(null);
|
|
153
|
+
const live = useRef({});
|
|
154
|
+
live.current = { colors, dissipation, velocityDissipation, curl, splatRadius, splatForce, idle };
|
|
155
|
+
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
const el = wrap.current;
|
|
158
|
+
const renderer = new WebGLRenderer({ alpha: true, premultipliedAlpha: true, antialias: false, powerPreference: "high-performance" });
|
|
159
|
+
if (!renderer.capabilities.isWebGL2 && !renderer.extensions.get("OES_texture_half_float")) {
|
|
160
|
+
renderer.dispose();
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
renderer.setPixelRatio(1);
|
|
164
|
+
renderer.autoClear = false;
|
|
165
|
+
Object.assign(renderer.domElement.style, { position: "absolute", inset: "0", width: "100%", height: "100%", display: "block", pointerEvents: "none" });
|
|
166
|
+
el.prepend(renderer.domElement);
|
|
167
|
+
|
|
168
|
+
const scene = new Scene();
|
|
169
|
+
const camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
|
170
|
+
const quad = new Mesh(new PlaneGeometry(2, 2));
|
|
171
|
+
scene.add(quad);
|
|
172
|
+
const texel = { value: new Vector2() };
|
|
173
|
+
const mat = (frag, uniforms) => new ShaderMaterial({ vertexShader: BASE_VERT, fragmentShader: frag, uniforms: { texelSize: texel, ...uniforms }, depthTest: false, depthWrite: false });
|
|
174
|
+
const m = {
|
|
175
|
+
splat: mat(SPLAT, { uTarget: { value: null }, aspect: { value: 1 }, color: { value: new Color() }, point: { value: new Vector2() }, radius: { value: 0.01 } }),
|
|
176
|
+
advect: mat(ADVECT, { uVelocity: { value: null }, uSource: { value: null }, velTexel: { value: new Vector2() }, dt: { value: 0.016 }, dissipation: { value: 1 } }),
|
|
177
|
+
divergence: mat(DIVERGENCE, { uVelocity: { value: null } }),
|
|
178
|
+
curl: mat(CURL, { uVelocity: { value: null } }),
|
|
179
|
+
vorticity: mat(VORTICITY, { uVelocity: { value: null }, uCurl: { value: null }, curl: { value: 18 }, dt: { value: 0.016 } }),
|
|
180
|
+
scale: mat(SCALE, { uTexture: { value: null }, value: { value: 0.8 } }),
|
|
181
|
+
pressure: mat(PRESSURE, { uPressure: { value: null }, uDivergence: { value: null } }),
|
|
182
|
+
gradient: mat(GRADIENT, { uPressure: { value: null }, uVelocity: { value: null } }),
|
|
183
|
+
display: mat(DISPLAY, { uTexture: { value: null }, dyeTexel: { value: new Vector2() } }),
|
|
184
|
+
};
|
|
185
|
+
m.display.transparent = true;
|
|
186
|
+
|
|
187
|
+
const target = (w, h) => new WebGLRenderTarget(w, h, { type: HalfFloatType, format: RGBAFormat, minFilter: LinearFilter, magFilter: LinearFilter, depthBuffer: false, stencilBuffer: false });
|
|
188
|
+
const double = (w, h) => {
|
|
189
|
+
const d = { read: target(w, h), write: target(w, h), w, h };
|
|
190
|
+
d.swap = () => ([d.read, d.write] = [d.write, d.read]);
|
|
191
|
+
d.dispose = () => {
|
|
192
|
+
d.read.dispose();
|
|
193
|
+
d.write.dispose();
|
|
194
|
+
};
|
|
195
|
+
return d;
|
|
196
|
+
};
|
|
197
|
+
let fbo = null;
|
|
198
|
+
const sizeFor = (res, w, h) => {
|
|
199
|
+
const a = w / h;
|
|
200
|
+
const min = Math.round(res);
|
|
201
|
+
const max = Math.round(res * Math.max(a, 1 / a));
|
|
202
|
+
return a > 1 ? [max, min] : [min, max];
|
|
203
|
+
};
|
|
204
|
+
const build = () => {
|
|
205
|
+
const { width, height } = el.getBoundingClientRect();
|
|
206
|
+
if (!width || !height) return false;
|
|
207
|
+
renderer.setSize(width, height, false);
|
|
208
|
+
fbo?.all.forEach((t) => t.dispose());
|
|
209
|
+
const [sw, sh] = sizeFor(simResolution, width, height);
|
|
210
|
+
const [dw, dh] = sizeFor(Math.min(dyeResolution, Math.max(width, height)), width, height);
|
|
211
|
+
const velocity = double(sw, sh);
|
|
212
|
+
const dye = double(dw, dh);
|
|
213
|
+
const pressure = double(sw, sh);
|
|
214
|
+
const divergence = target(sw, sh);
|
|
215
|
+
const curlT = target(sw, sh);
|
|
216
|
+
fbo = { velocity, dye, pressure, divergence, curl: curlT, all: [velocity, dye, pressure, divergence, curlT], aspect: width / height };
|
|
217
|
+
return true;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const pass = (material, out, w, h) => {
|
|
221
|
+
texel.value.set(1 / w, 1 / h);
|
|
222
|
+
quad.material = material;
|
|
223
|
+
renderer.setRenderTarget(out);
|
|
224
|
+
renderer.render(scene, camera);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const splat = (x, y, dx, dy, color) => {
|
|
228
|
+
const o = live.current;
|
|
229
|
+
const f = fbo;
|
|
230
|
+
m.splat.uniforms.aspect.value = f.aspect;
|
|
231
|
+
m.splat.uniforms.point.value.set(x, y);
|
|
232
|
+
m.splat.uniforms.radius.value = (o.splatRadius / 100) * (f.aspect > 1 ? f.aspect : 1);
|
|
233
|
+
m.splat.uniforms.uTarget.value = f.velocity.read.texture;
|
|
234
|
+
m.splat.uniforms.color.value.setRGB(dx, dy, 0);
|
|
235
|
+
pass(m.splat, f.velocity.write, f.velocity.w, f.velocity.h);
|
|
236
|
+
f.velocity.swap();
|
|
237
|
+
m.splat.uniforms.uTarget.value = f.dye.read.texture;
|
|
238
|
+
m.splat.uniforms.color.value.setRGB(color[0], color[1], color[2]);
|
|
239
|
+
pass(m.splat, f.dye.write, f.dye.w, f.dye.h);
|
|
240
|
+
f.dye.swap();
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
let hue = Math.random();
|
|
244
|
+
let pick = 0;
|
|
245
|
+
const nextColor = (strength = 0.3) => {
|
|
246
|
+
const list = live.current.colors;
|
|
247
|
+
if (list?.length) {
|
|
248
|
+
const c = new Color(list[pick++ % list.length]);
|
|
249
|
+
return [c.r * strength, c.g * strength, c.b * strength];
|
|
250
|
+
}
|
|
251
|
+
hue = (hue + 0.07) % 1;
|
|
252
|
+
return hsvToRgb(hue, 1, 1).map((v) => v * strength);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const step = (dt) => {
|
|
256
|
+
const o = live.current;
|
|
257
|
+
const f = fbo;
|
|
258
|
+
const { velocity, dye, pressure } = f;
|
|
259
|
+
// Swirl.
|
|
260
|
+
m.curl.uniforms.uVelocity.value = velocity.read.texture;
|
|
261
|
+
pass(m.curl, f.curl, velocity.w, velocity.h);
|
|
262
|
+
m.vorticity.uniforms.uVelocity.value = velocity.read.texture;
|
|
263
|
+
m.vorticity.uniforms.uCurl.value = f.curl.texture;
|
|
264
|
+
m.vorticity.uniforms.curl.value = o.curl;
|
|
265
|
+
m.vorticity.uniforms.dt.value = dt;
|
|
266
|
+
pass(m.vorticity, velocity.write, velocity.w, velocity.h);
|
|
267
|
+
velocity.swap();
|
|
268
|
+
// Make it incompressible.
|
|
269
|
+
m.divergence.uniforms.uVelocity.value = velocity.read.texture;
|
|
270
|
+
pass(m.divergence, f.divergence, velocity.w, velocity.h);
|
|
271
|
+
m.scale.uniforms.uTexture.value = pressure.read.texture;
|
|
272
|
+
m.scale.uniforms.value.value = 0.8;
|
|
273
|
+
pass(m.scale, pressure.write, pressure.w, pressure.h);
|
|
274
|
+
pressure.swap();
|
|
275
|
+
m.pressure.uniforms.uDivergence.value = f.divergence.texture;
|
|
276
|
+
for (let i = 0; i < 20; i++) {
|
|
277
|
+
m.pressure.uniforms.uPressure.value = pressure.read.texture;
|
|
278
|
+
pass(m.pressure, pressure.write, pressure.w, pressure.h);
|
|
279
|
+
pressure.swap();
|
|
280
|
+
}
|
|
281
|
+
m.gradient.uniforms.uPressure.value = pressure.read.texture;
|
|
282
|
+
m.gradient.uniforms.uVelocity.value = velocity.read.texture;
|
|
283
|
+
pass(m.gradient, velocity.write, velocity.w, velocity.h);
|
|
284
|
+
velocity.swap();
|
|
285
|
+
// Carry velocity and ink along.
|
|
286
|
+
m.advect.uniforms.velTexel.value.set(1 / velocity.w, 1 / velocity.h);
|
|
287
|
+
m.advect.uniforms.dt.value = dt;
|
|
288
|
+
m.advect.uniforms.uVelocity.value = velocity.read.texture;
|
|
289
|
+
m.advect.uniforms.uSource.value = velocity.read.texture;
|
|
290
|
+
m.advect.uniforms.dissipation.value = o.velocityDissipation;
|
|
291
|
+
pass(m.advect, velocity.write, velocity.w, velocity.h);
|
|
292
|
+
velocity.swap();
|
|
293
|
+
m.advect.uniforms.uVelocity.value = velocity.read.texture;
|
|
294
|
+
m.advect.uniforms.uSource.value = dye.read.texture;
|
|
295
|
+
m.advect.uniforms.dissipation.value = o.dissipation;
|
|
296
|
+
pass(m.advect, dye.write, dye.w, dye.h);
|
|
297
|
+
dye.swap();
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const draw = () => {
|
|
301
|
+
m.display.uniforms.uTexture.value = fbo.dye.read.texture;
|
|
302
|
+
m.display.uniforms.dyeTexel.value.set(1 / fbo.dye.w, 1 / fbo.dye.h);
|
|
303
|
+
texel.value.set(1 / fbo.dye.w, 1 / fbo.dye.h);
|
|
304
|
+
quad.material = m.display;
|
|
305
|
+
renderer.setRenderTarget(null);
|
|
306
|
+
renderer.clear();
|
|
307
|
+
renderer.render(scene, camera);
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
311
|
+
const pointer = { x: 0, y: 0, dx: 0, dy: 0, moved: false, inside: false, color: nextColor(), lastMove: 0 };
|
|
312
|
+
let raf = 0;
|
|
313
|
+
let last = 0;
|
|
314
|
+
let visible = true;
|
|
315
|
+
let nextIdle = 0;
|
|
316
|
+
|
|
317
|
+
const frame = (now) => {
|
|
318
|
+
raf = 0;
|
|
319
|
+
const o = live.current;
|
|
320
|
+
const dt = last ? Math.min((now - last) / 1000, 0.016) : 0.016;
|
|
321
|
+
last = now;
|
|
322
|
+
if (pointer.moved) {
|
|
323
|
+
pointer.moved = false;
|
|
324
|
+
splat(pointer.x, pointer.y, pointer.dx * o.splatForce, pointer.dy * o.splatForce, pointer.color);
|
|
325
|
+
}
|
|
326
|
+
// Idle: soft splashes from the edges so the space never looks empty.
|
|
327
|
+
if (o.idle && now - pointer.lastMove > 2500 && now > nextIdle) {
|
|
328
|
+
nextIdle = now + 1400 + Math.random() * 1200;
|
|
329
|
+
const x = Math.random();
|
|
330
|
+
const y = Math.random() * 0.6 + 0.2;
|
|
331
|
+
const ang = Math.random() * Math.PI * 2;
|
|
332
|
+
splat(x, y, Math.cos(ang) * 600, Math.sin(ang) * 600, nextColor(0.35));
|
|
333
|
+
}
|
|
334
|
+
step(dt);
|
|
335
|
+
draw();
|
|
336
|
+
if (visible) raf = requestAnimationFrame(frame);
|
|
337
|
+
};
|
|
338
|
+
const start = () => {
|
|
339
|
+
if (!raf && visible && !reduced) {
|
|
340
|
+
last = 0;
|
|
341
|
+
raf = requestAnimationFrame(frame);
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// Listen on the window so the layer can sit over clickable content.
|
|
346
|
+
const onMove = (e) => {
|
|
347
|
+
const r = el.getBoundingClientRect();
|
|
348
|
+
const inside = e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom;
|
|
349
|
+
if (!inside) {
|
|
350
|
+
pointer.inside = false;
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const x = (e.clientX - r.left) / r.width;
|
|
354
|
+
const y = 1 - (e.clientY - r.top) / r.height;
|
|
355
|
+
if (!pointer.inside) {
|
|
356
|
+
pointer.inside = true;
|
|
357
|
+
pointer.x = x;
|
|
358
|
+
pointer.y = y;
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
pointer.dx = (x - pointer.x) * (fbo.aspect < 1 ? fbo.aspect : 1);
|
|
362
|
+
pointer.dy = (y - pointer.y) / (fbo.aspect > 1 ? fbo.aspect : 1);
|
|
363
|
+
pointer.x = x;
|
|
364
|
+
pointer.y = y;
|
|
365
|
+
pointer.moved = Math.abs(pointer.dx) + Math.abs(pointer.dy) > 0;
|
|
366
|
+
pointer.lastMove = performance.now();
|
|
367
|
+
if (Math.random() < 0.08) pointer.color = nextColor();
|
|
368
|
+
};
|
|
369
|
+
const onDown = (e) => {
|
|
370
|
+
const r = el.getBoundingClientRect();
|
|
371
|
+
if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom) return;
|
|
372
|
+
const x = (e.clientX - r.left) / r.width;
|
|
373
|
+
const y = 1 - (e.clientY - r.top) / r.height;
|
|
374
|
+
const c = nextColor(0.6);
|
|
375
|
+
for (let i = 0; i < 6; i++) {
|
|
376
|
+
const ang = (i / 6) * Math.PI * 2;
|
|
377
|
+
splat(x, y, Math.cos(ang) * 1400, Math.sin(ang) * 1400, c);
|
|
378
|
+
}
|
|
379
|
+
pointer.lastMove = performance.now();
|
|
380
|
+
};
|
|
381
|
+
window.addEventListener("pointermove", onMove, { passive: true });
|
|
382
|
+
window.addEventListener("pointerdown", onDown, { passive: true });
|
|
383
|
+
|
|
384
|
+
const ro = new ResizeObserver(() => {
|
|
385
|
+
if (build()) start();
|
|
386
|
+
});
|
|
387
|
+
ro.observe(el);
|
|
388
|
+
const io = new IntersectionObserver(([entry]) => {
|
|
389
|
+
visible = entry.isIntersecting;
|
|
390
|
+
if (visible) start();
|
|
391
|
+
});
|
|
392
|
+
io.observe(el);
|
|
393
|
+
if (build()) start();
|
|
394
|
+
|
|
395
|
+
return () => {
|
|
396
|
+
cancelAnimationFrame(raf);
|
|
397
|
+
ro.disconnect();
|
|
398
|
+
io.disconnect();
|
|
399
|
+
window.removeEventListener("pointermove", onMove);
|
|
400
|
+
window.removeEventListener("pointerdown", onDown);
|
|
401
|
+
fbo?.all.forEach((t) => t.dispose());
|
|
402
|
+
Object.values(m).forEach((x) => x.dispose());
|
|
403
|
+
quad.geometry.dispose();
|
|
404
|
+
renderer.dispose();
|
|
405
|
+
renderer.forceContextLoss();
|
|
406
|
+
renderer.domElement.remove();
|
|
407
|
+
};
|
|
408
|
+
// Resolutions rebuild the simulation; the rest is read live.
|
|
409
|
+
}, [simResolution, dyeResolution]);
|
|
410
|
+
|
|
411
|
+
return (
|
|
412
|
+
<div ref={wrap} className={`pointer-events-none absolute inset-0 overflow-hidden ${className}`}>
|
|
413
|
+
{children}
|
|
414
|
+
</div>
|
|
415
|
+
);
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
export default FluidCursor;
|