apex-ui-kit 2.9.0 → 2.11.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 +7 -4
- package/dist/ApexUI-Kit/templates/AuroraBackground/AuroraBackground.jsx +157 -35
- package/dist/ApexUI-Kit/templates/BluePrintBackground/BluePrintBackground.jsx +134 -145
- package/dist/ApexUI-Kit/templates/CardSwap3D/CardSwap3D.jsx +64 -40
- package/dist/ApexUI-Kit/templates/DotGlobe/DotGlobe.jsx +6 -3
- package/dist/ApexUI-Kit/templates/FluidCursor/FluidCursor.jsx +418 -0
- package/dist/ApexUI-Kit/templates/Folder/Folder.jsx +58 -36
- package/dist/ApexUI-Kit/templates/ImageAccordion/ImageAccordion.jsx +8 -6
- package/dist/ApexUI-Kit/templates/InfiniteCanvas/InfiniteCanvas.jsx +1 -1
- package/dist/ApexUI-Kit/templates/IsoStack/IsoStack.jsx +28 -10
- package/dist/ApexUI-Kit/templates/LuminousParticleOcean/LuminousParticleOcean.jsx +8 -6
- package/dist/ApexUI-Kit/templates/MeteorCard/MeteorCard.jsx +5 -5
- package/dist/ApexUI-Kit/templates/OrbitIcons/OrbitIcons.jsx +1 -1
- package/dist/ApexUI-Kit/templates/ParallaxCard/ParallaxCard.jsx +34 -33
- package/dist/ApexUI-Kit/templates/SketchReveal/SketchReveal.jsx +257 -0
- package/dist/ApexUI-Kit/templates/SmokeBackground/SmokeBackground.jsx +237 -235
- package/dist/cli/index.js +42 -20
- package/package.json +8 -2
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import React, { useEffect, useState } from "react";
|
|
1
|
+
import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
2
2
|
import { motion } from "framer-motion";
|
|
3
3
|
import { Code2, Layers, Sparkles, Zap } from "lucide-react";
|
|
4
4
|
|
|
5
5
|
/*
|
|
6
|
-
* Card Swap 3D: cards stacked
|
|
7
|
-
* timer the front card drops away and
|
|
8
|
-
* while the rest step forward. Pauses while hovered;
|
|
6
|
+
* Card Swap 3D: cards stacked in real 3D, turned toward the viewer and
|
|
7
|
+
* receding into the distance. On a timer the front card drops away and
|
|
8
|
+
* returns to the back while the rest step forward. Pauses while hovered;
|
|
9
|
+
* click to swap now. The whole stack scales down to fit narrow containers.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
const DEFAULT_CARDS = [
|
|
@@ -15,12 +16,19 @@ const DEFAULT_CARDS = [
|
|
|
15
16
|
{ icon: Zap, title: "Fast", body: "Transforms and opacity only, 60fps on phones." },
|
|
16
17
|
];
|
|
17
18
|
|
|
18
|
-
const CardSwap3D = ({ cards = DEFAULT_CARDS, delay = 3200, distanceX = 3.6, distanceY = 3.2, skew =
|
|
19
|
+
const CardSwap3D = ({ cards = DEFAULT_CARDS, delay = 3200, distanceX = 3.6, distanceY = 3.2, depth = 70, tilt = 18, skew = 0, pauseOnHover = true, accentColor = "#a3e635", theme = "dark", width = 18, className = "" }) => {
|
|
19
20
|
// order[0] is the card in front.
|
|
20
21
|
const [order, setOrder] = useState(() => cards.map((_, i) => i));
|
|
21
22
|
const [dropping, setDropping] = useState(null);
|
|
22
23
|
const [paused, setPaused] = useState(false);
|
|
24
|
+
const [fit, setFit] = useState(1);
|
|
25
|
+
const outer = useRef(null);
|
|
23
26
|
const dark = theme !== "light";
|
|
27
|
+
const n = cards.length;
|
|
28
|
+
// Footprint of the whole stack in rem (card plus the steps behind it).
|
|
29
|
+
// (+5: the 3D turn makes the cards project a little wider than they are.)
|
|
30
|
+
const footW = width + distanceX * (n - 1) + 5;
|
|
31
|
+
const footH = width * 0.68 + distanceY * (n - 1) + 2;
|
|
24
32
|
|
|
25
33
|
const swap = () => {
|
|
26
34
|
if (dropping !== null) return;
|
|
@@ -37,42 +45,58 @@ const CardSwap3D = ({ cards = DEFAULT_CARDS, delay = 3200, distanceX = 3.6, dist
|
|
|
37
45
|
return () => clearInterval(id);
|
|
38
46
|
});
|
|
39
47
|
|
|
48
|
+
// Scale the stack down when its container is narrower than its footprint.
|
|
49
|
+
useLayoutEffect(() => {
|
|
50
|
+
const el = outer.current?.parentElement;
|
|
51
|
+
if (!el) return undefined;
|
|
52
|
+
const ro = new ResizeObserver(() => {
|
|
53
|
+
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
54
|
+
setFit(Math.min(1, el.clientWidth / (footW * rem)));
|
|
55
|
+
});
|
|
56
|
+
ro.observe(el);
|
|
57
|
+
return () => ro.disconnect();
|
|
58
|
+
}, [footW]);
|
|
59
|
+
|
|
40
60
|
return (
|
|
41
|
-
<div
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
61
|
+
<div ref={outer} className={`relative mx-auto ${className}`} style={{ width: `${footW * fit}rem`, height: `${footH * fit}rem` }}>
|
|
62
|
+
<div
|
|
63
|
+
className="absolute left-0 top-0 origin-top-left [perspective:1200px]"
|
|
64
|
+
style={{ width: `${footW}rem`, height: `${footH}rem`, transform: `scale(${fit})` }}
|
|
65
|
+
onPointerEnter={() => pauseOnHover && setPaused(true)}
|
|
66
|
+
onPointerLeave={() => setPaused(false)}
|
|
67
|
+
onClick={swap}
|
|
68
|
+
>
|
|
69
|
+
<div className="absolute [transform-style:preserve-3d]" style={{ left: "2.5rem", bottom: "1rem", width: `${width}rem`, height: `${width * 0.68}rem`, transform: `rotateY(${-tilt}deg) rotateX(${tilt * 0.35}deg)` }}>
|
|
70
|
+
{cards.map((card, i) => {
|
|
71
|
+
const slot = order.indexOf(i);
|
|
72
|
+
const isDropping = dropping === i;
|
|
73
|
+
const Icon = card.icon ?? Sparkles;
|
|
74
|
+
return (
|
|
75
|
+
<motion.div
|
|
76
|
+
key={i}
|
|
77
|
+
className={`absolute inset-0 cursor-pointer overflow-hidden rounded-2xl border p-6 ${dark ? "border-white/10 bg-neutral-950 text-white" : "border-black/10 bg-white text-neutral-900"}`}
|
|
78
|
+
style={{ zIndex: isDropping ? 60 : n - slot, boxShadow: "0 30px 60px -28px rgba(0,0,0,0.65)" }}
|
|
79
|
+
initial={false}
|
|
80
|
+
animate={
|
|
81
|
+
isDropping
|
|
82
|
+
? { x: "-2rem", y: "16rem", z: 120, rotateZ: -8, skewY: skew, opacity: 0 }
|
|
83
|
+
: { x: `${slot * distanceX}rem`, y: `${-slot * distanceY}rem`, z: -slot * depth, rotateZ: 0, skewY: skew, opacity: slot > 4 ? 0 : 1 - slot * 0.12 }
|
|
84
|
+
}
|
|
85
|
+
transition={isDropping ? { duration: 0.5, ease: [0.55, 0, 0.8, 0.2] } : { type: "spring", stiffness: 150, damping: 21 }}
|
|
86
|
+
>
|
|
87
|
+
<div className="flex items-center gap-2.5">
|
|
88
|
+
<span className="grid h-9 w-9 place-items-center rounded-xl text-black" style={{ background: accentColor }}>
|
|
89
|
+
<Icon className="h-[18px] w-[18px]" />
|
|
90
|
+
</span>
|
|
91
|
+
<p className="text-lg font-semibold">{card.title}</p>
|
|
92
|
+
</div>
|
|
93
|
+
<p className={`mt-3 text-[0.92rem] leading-relaxed ${dark ? "text-white/60" : "text-neutral-500"}`}>{card.body}</p>
|
|
94
|
+
<div className="pointer-events-none absolute -bottom-10 -right-10 h-32 w-32 rounded-full opacity-20 blur-2xl" style={{ background: accentColor }} />
|
|
95
|
+
</motion.div>
|
|
96
|
+
);
|
|
97
|
+
})}
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
76
100
|
</div>
|
|
77
101
|
);
|
|
78
102
|
};
|
|
@@ -49,6 +49,8 @@ const DEFAULT_ARCS = [
|
|
|
49
49
|
|
|
50
50
|
const rad = (d) => (d * Math.PI) / 180;
|
|
51
51
|
const toVec = (lat, lon) => [Math.cos(rad(lat)) * Math.sin(rad(lon)), Math.sin(rad(lat)), Math.cos(rad(lat)) * Math.cos(rad(lon))];
|
|
52
|
+
// Land dot directions never change, so work them out once instead of every frame.
|
|
53
|
+
const landVecs = landDots.map(([lat, lon]) => toVec(lat, lon));
|
|
52
54
|
|
|
53
55
|
// Spherical interpolation between two unit vectors.
|
|
54
56
|
const slerp = (a, b, t) => {
|
|
@@ -141,9 +143,10 @@ const DotGlobe = ({
|
|
|
141
143
|
// Land dots: front bright, back faint.
|
|
142
144
|
const dr = Math.max(0.8, R / 190);
|
|
143
145
|
ctx.fillStyle = o.dotColor;
|
|
144
|
-
for (let i = 0; i <
|
|
145
|
-
const [
|
|
146
|
-
|
|
146
|
+
for (let i = 0; i < landVecs.length; i++) {
|
|
147
|
+
const [x, y, z] = project(landVecs[i], R, c);
|
|
148
|
+
// Dots well behind the globe are nearly invisible; skip them.
|
|
149
|
+
if (z < -0.25) continue;
|
|
147
150
|
ctx.globalAlpha = z > 0 ? 0.35 + 0.65 * z : 0.06;
|
|
148
151
|
ctx.fillRect(x - dr, y - dr, dr * 2, dr * 2);
|
|
149
152
|
}
|
|
@@ -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;
|