apex-ui-kit 2.3.2 → 2.4.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 +10 -6
- package/dist/ApexUI-Kit/templates/LiquidBlob3D/LiquidBlob3D.jsx +174 -0
- package/dist/ApexUI-Kit/templates/LoadingScreen/LoadingScreen.jsx +11 -10
- package/dist/ApexUI-Kit/templates/LoadingScreen/useLoadProgress.js +78 -0
- package/dist/ApexUI-Kit/templates/ParticleMorph/ParticleMorph.jsx +216 -0
- package/dist/ApexUI-Kit/templates/PricingCards/PricingCards.jsx +11 -4
- package/dist/ApexUI-Kit/templates/RetroGrid3D/RetroGrid3D.jsx +158 -0
- package/dist/ApexUI-Kit/templates/ShinyText/ShinyText.jsx +2 -0
- package/dist/ApexUI-Kit/templates/StarBorder/StarBorder.jsx +3 -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
|
+
**93 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
|
+
- **93 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.
|
|
@@ -99,7 +99,7 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
99
99
|
|
|
100
100
|
## Components
|
|
101
101
|
|
|
102
|
-
### Backgrounds (
|
|
102
|
+
### Backgrounds (12)
|
|
103
103
|
|
|
104
104
|
| Component | CLI name |
|
|
105
105
|
| --- | --- |
|
|
@@ -112,6 +112,7 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
112
112
|
| [Luminous Particle Ocean](https://apex-ui.in/components/luminous-particle-ocean) | `luminous-particle-ocean` |
|
|
113
113
|
| [Orbs Background](https://apex-ui.in/components/orbs-background) | `orbs-background` |
|
|
114
114
|
| [Rain Background](https://apex-ui.in/components/rain-background) | `rain-background` |
|
|
115
|
+
| [Retro Grid 3D](https://apex-ui.in/components/retro-grid-3d) | `retro-grid-3d` |
|
|
115
116
|
| [Ripple Background](https://apex-ui.in/components/ripple-background) | `ripple-background` |
|
|
116
117
|
| [Smoke Background](https://apex-ui.in/components/smoke-background) | `smoke-background` |
|
|
117
118
|
|
|
@@ -181,7 +182,7 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
181
182
|
| [Animated Switches](https://apex-ui.in/components/animated-switches) | `animated-switch` |
|
|
182
183
|
| [Star Border](https://apex-ui.in/components/star-border) | `star-border` |
|
|
183
184
|
|
|
184
|
-
### Interactive (
|
|
185
|
+
### Interactive (22)
|
|
185
186
|
|
|
186
187
|
| Component | CLI name |
|
|
187
188
|
| --- | --- |
|
|
@@ -198,8 +199,10 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
198
199
|
| [FAQ Accordion](https://apex-ui.in/components/faq-accordion) | `faq-accordion` |
|
|
199
200
|
| [Flip Book](https://apex-ui.in/components/flip-book) | `flip-book` |
|
|
200
201
|
| [Image Compare](https://apex-ui.in/components/image-compare) | `image-compare` |
|
|
202
|
+
| [Liquid Blob 3D](https://apex-ui.in/components/liquid-blob-3d) | `liquid-blob-3d` |
|
|
201
203
|
| [Magnet Lines](https://apex-ui.in/components/magnet-lines) | `magnet-lines` |
|
|
202
204
|
| [Magnetic Cursor](https://apex-ui.in/components/magnetic-cursor) | `magnetic-cursor` |
|
|
205
|
+
| [Particle Morph](https://apex-ui.in/components/particle-morph) | `particle-morph` |
|
|
203
206
|
| [Pointer Follower](https://apex-ui.in/components/pointer-follower) | `pointer-follower` |
|
|
204
207
|
| [Receipt Printer](https://apex-ui.in/components/receipt-printer) | `receipt-printer` |
|
|
205
208
|
| [Stepper](https://apex-ui.in/components/stepper) | `stepper` |
|
|
@@ -234,10 +237,11 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
234
237
|
| [Dot Globe](https://apex-ui.in/components/dot-globe) | `dot-globe` |
|
|
235
238
|
| [World Map](https://apex-ui.in/components/world-map) | `world-map` |
|
|
236
239
|
|
|
237
|
-
## What's new in 2.
|
|
240
|
+
## What's new in 2.4
|
|
238
241
|
|
|
239
|
-
-
|
|
242
|
+
- 24 new components, including three.js 3D (Liquid Blob 3D, Particle Morph, Retro Grid 3D), 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.
|
|
240
243
|
- AI Model Prompt now exports `DEFAULT_LLM_MODELS` and an index file, so you can import both from the folder.
|
|
244
|
+
- Loading Screens show real progress: `useLoadProgress` loads your images, files or code and drives the bar and the count.
|
|
241
245
|
- Hold to Confirm follows a delete flow with an `UndoNotice` that counts down before the action is final.
|
|
242
246
|
- Sliders and drag components no longer stop midway when the page text gets selected.
|
|
243
247
|
- Stepper and Animated Tabs keep a steady height, Shiny Text loops seamlessly, Cube Gallery is a real 3D cube and Flip Book is a hardcover book you can drag.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from "react";
|
|
2
|
+
import { Color, IcosahedronGeometry, Mesh, PerspectiveCamera, Scene, ShaderMaterial, WebGLRenderer } from "three";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Liquid Blob 3D: a glossy blob whose surface flows with 3D noise, shaded
|
|
6
|
+
* with an iridescent fresnel rim. It leans toward the pointer and ripples
|
|
7
|
+
* harder while hovered. One mesh, one shader, no textures, so it stays
|
|
8
|
+
* light. Pauses when off screen and frees the GPU on unmount.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const NOISE = `
|
|
12
|
+
vec3 mod289(vec3 x){return x-floor(x*(1./289.))*289.;}
|
|
13
|
+
vec4 mod289(vec4 x){return x-floor(x*(1./289.))*289.;}
|
|
14
|
+
vec4 permute(vec4 x){return mod289(((x*34.)+1.)*x);}
|
|
15
|
+
vec4 taylorInvSqrt(vec4 r){return 1.79284291400159-.85373472095314*r;}
|
|
16
|
+
float snoise(vec3 v){
|
|
17
|
+
const vec2 C=vec2(1./6.,1./3.);const vec4 D=vec4(0.,.5,1.,2.);
|
|
18
|
+
vec3 i=floor(v+dot(v,C.yyy));vec3 x0=v-i+dot(i,C.xxx);
|
|
19
|
+
vec3 g=step(x0.yzx,x0.xyz);vec3 l=1.-g;vec3 i1=min(g.xyz,l.zxy);vec3 i2=max(g.xyz,l.zxy);
|
|
20
|
+
vec3 x1=x0-i1+C.xxx;vec3 x2=x0-i2+C.yyy;vec3 x3=x0-D.yyy;i=mod289(i);
|
|
21
|
+
vec4 p=permute(permute(permute(i.z+vec4(0.,i1.z,i2.z,1.))+i.y+vec4(0.,i1.y,i2.y,1.))+i.x+vec4(0.,i1.x,i2.x,1.));
|
|
22
|
+
float n_=.142857142857;vec3 ns=n_*D.wyz-D.xzx;vec4 j=p-49.*floor(p*ns.z*ns.z);
|
|
23
|
+
vec4 x_=floor(j*ns.z);vec4 y_=floor(j-7.*x_);vec4 x=x_*ns.x+ns.yyyy;vec4 y=y_*ns.x+ns.yyyy;vec4 h=1.-abs(x)-abs(y);
|
|
24
|
+
vec4 b0=vec4(x.xy,y.xy);vec4 b1=vec4(x.zw,y.zw);vec4 s0=floor(b0)*2.+1.;vec4 s1=floor(b1)*2.+1.;vec4 sh=-step(h,vec4(0.));
|
|
25
|
+
vec4 a0=b0.xzyw+s0.xzyw*sh.xxyy;vec4 a1=b1.xzyw+s1.xzyw*sh.zzww;
|
|
26
|
+
vec3 p0=vec3(a0.xy,h.x);vec3 p1=vec3(a0.zw,h.y);vec3 p2=vec3(a1.xy,h.z);vec3 p3=vec3(a1.zw,h.w);
|
|
27
|
+
vec4 norm=taylorInvSqrt(vec4(dot(p0,p0),dot(p1,p1),dot(p2,p2),dot(p3,p3)));p0*=norm.x;p1*=norm.y;p2*=norm.z;p3*=norm.w;
|
|
28
|
+
vec4 m=max(.6-vec4(dot(x0,x0),dot(x1,x1),dot(x2,x2),dot(x3,x3)),0.);m=m*m;
|
|
29
|
+
return 42.*dot(m*m,vec4(dot(p0,x0),dot(p1,x1),dot(p2,x2),dot(p3,x3)));
|
|
30
|
+
}`;
|
|
31
|
+
|
|
32
|
+
const VERT = `
|
|
33
|
+
uniform float uTime; uniform float uAmp; uniform float uFreq;
|
|
34
|
+
varying vec3 vNormal; varying vec3 vView; varying float vNoise;
|
|
35
|
+
${NOISE}
|
|
36
|
+
float field(vec3 p){ return snoise(p * uFreq + vec3(uTime * .3)) + .18 * snoise(p * uFreq * 2. - vec3(uTime * .2)); }
|
|
37
|
+
void main(){
|
|
38
|
+
float n = field(normal);
|
|
39
|
+
vNoise = n;
|
|
40
|
+
vec3 pos = position + normal * n * uAmp;
|
|
41
|
+
// Rebuild the normal from two nearby displaced points so the shading follows the waves.
|
|
42
|
+
vec3 t = normalize(cross(normal, abs(normal.y) < .99 ? vec3(0.,1.,0.) : vec3(1.,0.,0.)));
|
|
43
|
+
vec3 b = normalize(cross(normal, t));
|
|
44
|
+
float e = .01;
|
|
45
|
+
vec3 n1 = normalize(normal + t * e); vec3 n2 = normalize(normal + b * e);
|
|
46
|
+
vec3 p1 = n1 * (1. + field(n1) * uAmp); vec3 p2 = n2 * (1. + field(n2) * uAmp);
|
|
47
|
+
vec3 nrm = normalize(cross(p1 - pos, p2 - pos));
|
|
48
|
+
if (dot(nrm, normal) < 0.) nrm = -nrm;
|
|
49
|
+
vec4 mv = modelViewMatrix * vec4(pos, 1.);
|
|
50
|
+
vView = -mv.xyz;
|
|
51
|
+
vNormal = normalize(normalMatrix * nrm);
|
|
52
|
+
gl_Position = projectionMatrix * mv;
|
|
53
|
+
}`;
|
|
54
|
+
|
|
55
|
+
const FRAG = `
|
|
56
|
+
uniform vec3 uC1; uniform vec3 uC2; uniform vec3 uC3;
|
|
57
|
+
varying vec3 vNormal; varying vec3 vView; varying float vNoise;
|
|
58
|
+
void main(){
|
|
59
|
+
vec3 n = normalize(vNormal); vec3 v = normalize(vView);
|
|
60
|
+
float fr = pow(1. - max(dot(n, v), 0.), 2.4);
|
|
61
|
+
vec3 col = mix(uC1, uC2, smoothstep(-.7, .7, vNoise));
|
|
62
|
+
col = mix(col, uC3, fr * .9);
|
|
63
|
+
vec3 l = normalize(vec3(.45, .8, .6));
|
|
64
|
+
float diff = max(dot(n, l), 0.);
|
|
65
|
+
float spec = pow(max(dot(reflect(-l, n), v), 0.), 40.);
|
|
66
|
+
col = col * (.55 + .55 * diff) + spec * .8 + fr * .25;
|
|
67
|
+
gl_FragColor = vec4(col, 1.);
|
|
68
|
+
}`;
|
|
69
|
+
|
|
70
|
+
const LiquidBlob3D = ({ colors = ["#a3e635", "#22d3ee", "#a78bfa"], size = 22, speed = 1, distortion = 0.24, detail = 48, interactive = true, className = "" }) => {
|
|
71
|
+
const mount = useRef(null);
|
|
72
|
+
const opts = useRef({ colors, speed, distortion, interactive });
|
|
73
|
+
opts.current = { colors, speed, distortion, interactive };
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
const el = mount.current;
|
|
77
|
+
const renderer = new WebGLRenderer({ antialias: true, alpha: true, powerPreference: "high-performance" });
|
|
78
|
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
|
79
|
+
el.appendChild(renderer.domElement);
|
|
80
|
+
renderer.domElement.style.display = "block";
|
|
81
|
+
|
|
82
|
+
const scene = new Scene();
|
|
83
|
+
const camera = new PerspectiveCamera(35, 1, 0.1, 50);
|
|
84
|
+
camera.position.z = 4.2;
|
|
85
|
+
const uniforms = {
|
|
86
|
+
uTime: { value: 0 },
|
|
87
|
+
uAmp: { value: distortion },
|
|
88
|
+
uFreq: { value: 0.85 },
|
|
89
|
+
uC1: { value: new Color(colors[0]) },
|
|
90
|
+
uC2: { value: new Color(colors[1] ?? colors[0]) },
|
|
91
|
+
uC3: { value: new Color(colors[2] ?? colors[1] ?? colors[0]) },
|
|
92
|
+
};
|
|
93
|
+
const geometry = new IcosahedronGeometry(1, detail);
|
|
94
|
+
const material = new ShaderMaterial({ vertexShader: VERT, fragmentShader: FRAG, uniforms });
|
|
95
|
+
const mesh = new Mesh(geometry, material);
|
|
96
|
+
scene.add(mesh);
|
|
97
|
+
|
|
98
|
+
const resize = () => {
|
|
99
|
+
const { width, height } = el.getBoundingClientRect();
|
|
100
|
+
if (!width || !height) return;
|
|
101
|
+
renderer.setSize(width, height, false);
|
|
102
|
+
renderer.domElement.style.width = "100%";
|
|
103
|
+
renderer.domElement.style.height = "100%";
|
|
104
|
+
camera.aspect = width / height;
|
|
105
|
+
camera.updateProjectionMatrix();
|
|
106
|
+
};
|
|
107
|
+
const ro = new ResizeObserver(resize);
|
|
108
|
+
ro.observe(el);
|
|
109
|
+
resize();
|
|
110
|
+
|
|
111
|
+
const pointer = { x: 0, y: 0, hover: 0 };
|
|
112
|
+
const onMove = (e) => {
|
|
113
|
+
const r = el.getBoundingClientRect();
|
|
114
|
+
pointer.x = ((e.clientX - r.left) / r.width) * 2 - 1;
|
|
115
|
+
pointer.y = ((e.clientY - r.top) / r.height) * 2 - 1;
|
|
116
|
+
pointer.hover = 1;
|
|
117
|
+
};
|
|
118
|
+
const onLeave = () => {
|
|
119
|
+
pointer.x = 0;
|
|
120
|
+
pointer.y = 0;
|
|
121
|
+
pointer.hover = 0;
|
|
122
|
+
};
|
|
123
|
+
el.addEventListener("pointermove", onMove);
|
|
124
|
+
el.addEventListener("pointerleave", onLeave);
|
|
125
|
+
|
|
126
|
+
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
127
|
+
let visible = true;
|
|
128
|
+
const io = new IntersectionObserver(([entry]) => (visible = entry.isIntersecting));
|
|
129
|
+
io.observe(el);
|
|
130
|
+
|
|
131
|
+
let raf = 0;
|
|
132
|
+
let last = performance.now();
|
|
133
|
+
let amp = distortion;
|
|
134
|
+
const frame = (now) => {
|
|
135
|
+
raf = requestAnimationFrame(frame);
|
|
136
|
+
const dt = Math.min(0.05, (now - last) / 1000);
|
|
137
|
+
last = now;
|
|
138
|
+
if (!visible) return;
|
|
139
|
+
const o = opts.current;
|
|
140
|
+
uniforms.uTime.value += dt * o.speed * (reduced ? 0.15 : 1);
|
|
141
|
+
const targetAmp = o.distortion * (o.interactive && pointer.hover ? 1.55 : 1);
|
|
142
|
+
amp += (targetAmp - amp) * 0.06;
|
|
143
|
+
uniforms.uAmp.value = amp;
|
|
144
|
+
uniforms.uC1.value.set(o.colors[0]);
|
|
145
|
+
uniforms.uC2.value.set(o.colors[1] ?? o.colors[0]);
|
|
146
|
+
uniforms.uC3.value.set(o.colors[2] ?? o.colors[1] ?? o.colors[0]);
|
|
147
|
+
const tx = o.interactive ? pointer.y * 0.5 : 0;
|
|
148
|
+
const ty = o.interactive ? pointer.x * 0.7 : 0;
|
|
149
|
+
mesh.rotation.x += (tx - mesh.rotation.x) * 0.05;
|
|
150
|
+
mesh.rotation.y += (ty + uniforms.uTime.value * 0.15 - mesh.rotation.y) * 0.05;
|
|
151
|
+
renderer.render(scene, camera);
|
|
152
|
+
};
|
|
153
|
+
raf = requestAnimationFrame(frame);
|
|
154
|
+
|
|
155
|
+
return () => {
|
|
156
|
+
cancelAnimationFrame(raf);
|
|
157
|
+
ro.disconnect();
|
|
158
|
+
io.disconnect();
|
|
159
|
+
el.removeEventListener("pointermove", onMove);
|
|
160
|
+
el.removeEventListener("pointerleave", onLeave);
|
|
161
|
+
geometry.dispose();
|
|
162
|
+
material.dispose();
|
|
163
|
+
renderer.dispose();
|
|
164
|
+
renderer.forceContextLoss();
|
|
165
|
+
renderer.domElement.remove();
|
|
166
|
+
};
|
|
167
|
+
// Colors, speed and distortion are read live from opts; only geometry detail rebuilds.
|
|
168
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
169
|
+
}, [detail]);
|
|
170
|
+
|
|
171
|
+
return <div ref={mount} className={`relative touch-none ${className}`} style={{ width: `min(${size}rem, 90vw)`, aspectRatio: "1" }} aria-hidden="true" />;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export default LiquidBlob3D;
|
|
@@ -52,7 +52,7 @@ function useProgress(progress, duration, loop, onComplete) {
|
|
|
52
52
|
if (controlled && progress >= 100) doneRef.current?.();
|
|
53
53
|
}, [controlled, progress]);
|
|
54
54
|
|
|
55
|
-
return Math.max(0, Math.min(100, controlled ? progress : auto));
|
|
55
|
+
return Math.round(Math.max(0, Math.min(100, controlled ? progress : auto)));
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
function Bar({ value, color, track }) {
|
|
@@ -64,11 +64,11 @@ function Bar({ value, color, track }) {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// Progress row shared by the game variants.
|
|
67
|
-
function Footer({ value, color, track }) {
|
|
67
|
+
function Footer({ value, color, track, statusText }) {
|
|
68
68
|
return (
|
|
69
69
|
<div className="mt-5 w-full">
|
|
70
70
|
<div className="mb-2 flex items-center justify-between text-[0.75rem]">
|
|
71
|
-
<span className="opacity-60">{messageFor(value)}
|
|
71
|
+
<span className="opacity-60">{statusText ?? `${messageFor(value)}…`}</span>
|
|
72
72
|
<span className="font-mono font-semibold tabular-nums">{value}%</span>
|
|
73
73
|
</div>
|
|
74
74
|
<Bar value={value} color={color} track={track} />
|
|
@@ -160,7 +160,7 @@ function Blocks({ value, color, track, moving }) {
|
|
|
160
160
|
|
|
161
161
|
const ORB_LIFE = 1.5; // seconds before an orb escapes
|
|
162
162
|
|
|
163
|
-
function CatchGame({ value, color, track, dark }) {
|
|
163
|
+
function CatchGame({ value, color, track, dark, statusText }) {
|
|
164
164
|
const [score, setScore] = useState(0);
|
|
165
165
|
const [best, setBest] = useState(0);
|
|
166
166
|
const [combo, setCombo] = useState(0);
|
|
@@ -287,14 +287,14 @@ function CatchGame({ value, color, track, dark }) {
|
|
|
287
287
|
</div>
|
|
288
288
|
))}
|
|
289
289
|
</motion.div>
|
|
290
|
-
<Footer value={value} color={color} track={track} />
|
|
290
|
+
<Footer value={value} color={color} track={track} statusText={statusText} />
|
|
291
291
|
</div>
|
|
292
292
|
);
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
/* --------------------------------------------------------------- Runner game */
|
|
296
296
|
|
|
297
|
-
function RunnerGame({ value, color, track, dark }) {
|
|
297
|
+
function RunnerGame({ value, color, track, dark, statusText }) {
|
|
298
298
|
const canvasRef = useRef(null);
|
|
299
299
|
const jumpRef = useRef(() => {});
|
|
300
300
|
const [score, setScore] = useState(0);
|
|
@@ -542,7 +542,7 @@ function RunnerGame({ value, color, track, dark }) {
|
|
|
542
542
|
)}
|
|
543
543
|
</AnimatePresence>
|
|
544
544
|
</div>
|
|
545
|
-
<Footer value={value} color={color} track={track} />
|
|
545
|
+
<Footer value={value} color={color} track={track} statusText={statusText} />
|
|
546
546
|
</div>
|
|
547
547
|
);
|
|
548
548
|
}
|
|
@@ -552,7 +552,7 @@ function RunnerGame({ value, color, track, dark }) {
|
|
|
552
552
|
const LANES = 3;
|
|
553
553
|
const TRAFFIC = ["#f43f5e", "#38bdf8", "#f59e0b", "#a78bfa", "#e5e7eb"];
|
|
554
554
|
|
|
555
|
-
function RacerGame({ value, color, track, dark }) {
|
|
555
|
+
function RacerGame({ value, color, track, dark, statusText }) {
|
|
556
556
|
const canvasRef = useRef(null);
|
|
557
557
|
const moveRef = useRef(() => {});
|
|
558
558
|
const [score, setScore] = useState(0);
|
|
@@ -752,7 +752,7 @@ function RacerGame({ value, color, track, dark }) {
|
|
|
752
752
|
)}
|
|
753
753
|
</AnimatePresence>
|
|
754
754
|
</div>
|
|
755
|
-
<Footer value={value} color={color} track={track} />
|
|
755
|
+
<Footer value={value} color={color} track={track} statusText={statusText} />
|
|
756
756
|
</div>
|
|
757
757
|
);
|
|
758
758
|
}
|
|
@@ -767,13 +767,14 @@ const LoadingScreen = ({
|
|
|
767
767
|
title = "Loading",
|
|
768
768
|
fullScreen = false,
|
|
769
769
|
onComplete,
|
|
770
|
+
statusText,
|
|
770
771
|
className = "",
|
|
771
772
|
}) => {
|
|
772
773
|
const moving = !useReducedMotion();
|
|
773
774
|
const value = useProgress(progress, duration, loop, onComplete);
|
|
774
775
|
const dark = theme !== "light";
|
|
775
776
|
const track = dark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.08)";
|
|
776
|
-
const shared = { value, color: accentColor, track, moving, dark };
|
|
777
|
+
const shared = { value, color: accentColor, track, moving, dark, statusText };
|
|
777
778
|
const game = variant === "catch" || variant === "runner" || variant === "racer";
|
|
778
779
|
|
|
779
780
|
return (
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Real loading progress for <LoadingScreen progress={...} />.
|
|
5
|
+
*
|
|
6
|
+
* Give it the things your page needs before it can show, and it loads them
|
|
7
|
+
* and reports how many are done:
|
|
8
|
+
*
|
|
9
|
+
* const { progress, loaded, total, done } = useLoadProgress([
|
|
10
|
+
* "/hero.jpg", // images are decoded
|
|
11
|
+
* "/data/products.json", // anything else is fetched
|
|
12
|
+
* () => import("./Dashboard.jsx"), // or any function returning a promise
|
|
13
|
+
* ]);
|
|
14
|
+
* <LoadingScreen variant="runner" progress={progress} statusText={`${loaded} of ${total} loaded`} />
|
|
15
|
+
*
|
|
16
|
+
* Items load in parallel; progress is the share that has finished (0 to 100),
|
|
17
|
+
* so the bar and the number always match what really happened. A failed item
|
|
18
|
+
* still counts as finished (see `errors`) so the screen never gets stuck.
|
|
19
|
+
* Pass a stable list (define it outside the component or memoize it).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const IMAGE = /\.(png|jpe?g|gif|webp|avif|svg)(\?|#|$)/i;
|
|
23
|
+
|
|
24
|
+
function loadItem(item) {
|
|
25
|
+
if (typeof item === "function") return Promise.resolve().then(item);
|
|
26
|
+
if (item && typeof item.then === "function") return item;
|
|
27
|
+
if (typeof item === "string") {
|
|
28
|
+
if (IMAGE.test(item)) {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const img = new Image();
|
|
31
|
+
img.onload = () => (img.decode ? img.decode().then(resolve, resolve) : resolve());
|
|
32
|
+
img.onerror = reject;
|
|
33
|
+
img.src = item;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return fetch(item).then((r) => {
|
|
37
|
+
if (!r.ok) throw new Error(`${r.status} ${item}`);
|
|
38
|
+
return r.blob();
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return Promise.resolve();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function useLoadProgress(items, { enabled = true } = {}) {
|
|
45
|
+
const list = useRef(items);
|
|
46
|
+
list.current = items;
|
|
47
|
+
const total = items.length;
|
|
48
|
+
const key = `${total}|${items.map((i) => (typeof i === "string" ? i : typeof i)).join("|")}`;
|
|
49
|
+
const [state, setState] = useState(() => ({ loaded: 0, total, errors: 0, progress: total ? 0 : 100, done: !total }));
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (!enabled) return undefined;
|
|
53
|
+
const work = list.current;
|
|
54
|
+
const count = work.length;
|
|
55
|
+
if (!count) {
|
|
56
|
+
setState({ loaded: 0, total: 0, errors: 0, progress: 100, done: true });
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
let cancelled = false;
|
|
60
|
+
let loaded = 0;
|
|
61
|
+
let errors = 0;
|
|
62
|
+
setState({ loaded: 0, total: count, errors: 0, progress: 0, done: false });
|
|
63
|
+
const finish = (ok) => {
|
|
64
|
+
if (cancelled) return;
|
|
65
|
+
loaded += 1;
|
|
66
|
+
if (!ok) errors += 1;
|
|
67
|
+
setState({ loaded, total: count, errors, progress: Math.round((loaded / count) * 100), done: loaded === count });
|
|
68
|
+
};
|
|
69
|
+
work.forEach((item) => loadItem(item).then(() => finish(true), () => finish(false)));
|
|
70
|
+
return () => {
|
|
71
|
+
cancelled = true;
|
|
72
|
+
};
|
|
73
|
+
}, [key, enabled]);
|
|
74
|
+
|
|
75
|
+
return state;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default useLoadProgress;
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { AdditiveBlending, BufferAttribute, BufferGeometry, Color, PerspectiveCamera, Points, Scene, ShaderMaterial, WebGLRenderer } from "three";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Particle Morph: thousands of glowing particles that flow between shapes:
|
|
6
|
+
* sphere, cube, torus, DNA helix and a wave. It morphs on a timer or when
|
|
7
|
+
* you click, drifts with the pointer, and each particle takes a slightly
|
|
8
|
+
* different path so the change looks like a swarm, not a snap. One draw
|
|
9
|
+
* call; pauses off screen and frees the GPU on unmount.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const SHAPES = {
|
|
13
|
+
sphere: (i, n) => {
|
|
14
|
+
// Fibonacci sphere: even spacing.
|
|
15
|
+
const y = 1 - (i / (n - 1)) * 2;
|
|
16
|
+
const r = Math.sqrt(1 - y * y);
|
|
17
|
+
const t = i * 2.399963;
|
|
18
|
+
return [Math.cos(t) * r * 1.6, y * 1.6, Math.sin(t) * r * 1.6];
|
|
19
|
+
},
|
|
20
|
+
cube: () => {
|
|
21
|
+
const s = 1.25;
|
|
22
|
+
const p = [(Math.random() * 2 - 1) * s, (Math.random() * 2 - 1) * s, (Math.random() * 2 - 1) * s];
|
|
23
|
+
p[Math.floor(Math.random() * 3)] = Math.sign(Math.random() - 0.5) * s;
|
|
24
|
+
return p;
|
|
25
|
+
},
|
|
26
|
+
torus: () => {
|
|
27
|
+
const u = Math.random() * Math.PI * 2;
|
|
28
|
+
const v = Math.random() * Math.PI * 2;
|
|
29
|
+
const R = 1.3;
|
|
30
|
+
const r = 0.45;
|
|
31
|
+
return [(R + r * Math.cos(v)) * Math.cos(u), r * Math.sin(v), (R + r * Math.cos(v)) * Math.sin(u)];
|
|
32
|
+
},
|
|
33
|
+
helix: (i, n) => {
|
|
34
|
+
const t = (i / n) * Math.PI * 8;
|
|
35
|
+
const y = (i / n) * 3.4 - 1.7;
|
|
36
|
+
const strand = i % 3;
|
|
37
|
+
if (strand === 2) {
|
|
38
|
+
// Rungs between the two strands.
|
|
39
|
+
const k = Math.random();
|
|
40
|
+
const a = Math.round(t / 0.6) * 0.6;
|
|
41
|
+
return [Math.cos(a) * 0.8 * (1 - 2 * k), y, Math.sin(a) * 0.8 * (1 - 2 * k)];
|
|
42
|
+
}
|
|
43
|
+
const a = t + strand * Math.PI;
|
|
44
|
+
return [Math.cos(a) * 0.8, y, Math.sin(a) * 0.8];
|
|
45
|
+
},
|
|
46
|
+
wave: (i, n) => {
|
|
47
|
+
const side = Math.ceil(Math.sqrt(n));
|
|
48
|
+
const x = ((i % side) / side) * 4 - 2;
|
|
49
|
+
const z = (Math.floor(i / side) / side) * 4 - 2;
|
|
50
|
+
return [x, Math.sin(x * 2) * Math.cos(z * 2) * 0.35, z];
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const VERT = `
|
|
55
|
+
attribute float aSeed;
|
|
56
|
+
uniform float uTime; uniform float uSize; uniform float uDpr;
|
|
57
|
+
varying float vSeed;
|
|
58
|
+
void main(){
|
|
59
|
+
vSeed = aSeed;
|
|
60
|
+
vec3 p = position;
|
|
61
|
+
p += 0.012 * vec3(sin(uTime * 1.3 + aSeed * 30.), cos(uTime * 1.1 + aSeed * 20.), sin(uTime * .9 + aSeed * 40.));
|
|
62
|
+
vec4 mv = modelViewMatrix * vec4(p, 1.);
|
|
63
|
+
gl_PointSize = uSize * uDpr * (0.6 + aSeed * 0.8) * (11. / -mv.z);
|
|
64
|
+
gl_Position = projectionMatrix * mv;
|
|
65
|
+
}`;
|
|
66
|
+
|
|
67
|
+
const FRAG = `
|
|
68
|
+
uniform vec3 uC1; uniform vec3 uC2;
|
|
69
|
+
varying float vSeed;
|
|
70
|
+
void main(){
|
|
71
|
+
float d = length(gl_PointCoord - .5);
|
|
72
|
+
float a = smoothstep(.5, .0, d) * .85;
|
|
73
|
+
vec3 col = mix(uC1, uC2, vSeed);
|
|
74
|
+
gl_FragColor = vec4(col * a, a);
|
|
75
|
+
}`;
|
|
76
|
+
|
|
77
|
+
const ORDER = ["sphere", "cube", "torus", "helix", "wave"];
|
|
78
|
+
|
|
79
|
+
const ParticleMorph = ({ shapes = ORDER, count = 6000, colors = ["#a3e635", "#22d3ee"], interval = 3500, pointSize = 3, autoPlay = true, height = 26, className = "" }) => {
|
|
80
|
+
const mount = useRef(null);
|
|
81
|
+
const api = useRef(null);
|
|
82
|
+
const [index, setIndex] = useState(0);
|
|
83
|
+
const list = shapes.filter((s) => SHAPES[s]);
|
|
84
|
+
const opts = useRef({ colors, pointSize });
|
|
85
|
+
opts.current = { colors, pointSize };
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
const el = mount.current;
|
|
89
|
+
const renderer = new WebGLRenderer({ antialias: false, alpha: true, powerPreference: "high-performance" });
|
|
90
|
+
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
91
|
+
renderer.setPixelRatio(dpr);
|
|
92
|
+
el.appendChild(renderer.domElement);
|
|
93
|
+
renderer.domElement.style.display = "block";
|
|
94
|
+
const scene = new Scene();
|
|
95
|
+
const camera = new PerspectiveCamera(40, 1, 0.1, 50);
|
|
96
|
+
camera.position.z = 6;
|
|
97
|
+
|
|
98
|
+
const n = count;
|
|
99
|
+
const pos = new Float32Array(n * 3);
|
|
100
|
+
const seeds = new Float32Array(n);
|
|
101
|
+
const targets = list.map((s) => {
|
|
102
|
+
const arr = new Float32Array(n * 3);
|
|
103
|
+
for (let i = 0; i < n; i++) arr.set(SHAPES[s](i, n), i * 3);
|
|
104
|
+
return arr;
|
|
105
|
+
});
|
|
106
|
+
pos.set(targets[0]);
|
|
107
|
+
for (let i = 0; i < n; i++) seeds[i] = Math.random();
|
|
108
|
+
const geometry = new BufferGeometry();
|
|
109
|
+
geometry.setAttribute("position", new BufferAttribute(pos, 3));
|
|
110
|
+
geometry.setAttribute("aSeed", new BufferAttribute(seeds, 1));
|
|
111
|
+
const uniforms = { uTime: { value: 0 }, uSize: { value: pointSize }, uDpr: { value: dpr }, uC1: { value: new Color(colors[0]) }, uC2: { value: new Color(colors[1] ?? colors[0]) } };
|
|
112
|
+
const material = new ShaderMaterial({ vertexShader: VERT, fragmentShader: FRAG, uniforms, transparent: true, depthWrite: false, blending: AdditiveBlending });
|
|
113
|
+
const points = new Points(geometry, material);
|
|
114
|
+
scene.add(points);
|
|
115
|
+
|
|
116
|
+
const resize = () => {
|
|
117
|
+
const { width, height: h } = el.getBoundingClientRect();
|
|
118
|
+
if (!width || !h) return;
|
|
119
|
+
renderer.setSize(width, h, false);
|
|
120
|
+
renderer.domElement.style.width = "100%";
|
|
121
|
+
renderer.domElement.style.height = "100%";
|
|
122
|
+
camera.aspect = width / h;
|
|
123
|
+
// Keep the shape in frame on narrow screens.
|
|
124
|
+
camera.position.z = width / h < 1 ? 6 / Math.max(0.55, width / h) : 6;
|
|
125
|
+
camera.updateProjectionMatrix();
|
|
126
|
+
};
|
|
127
|
+
const ro = new ResizeObserver(resize);
|
|
128
|
+
ro.observe(el);
|
|
129
|
+
resize();
|
|
130
|
+
|
|
131
|
+
const pointer = { x: 0, y: 0 };
|
|
132
|
+
const onMove = (e) => {
|
|
133
|
+
const r = el.getBoundingClientRect();
|
|
134
|
+
pointer.x = ((e.clientX - r.left) / r.width) * 2 - 1;
|
|
135
|
+
pointer.y = ((e.clientY - r.top) / r.height) * 2 - 1;
|
|
136
|
+
};
|
|
137
|
+
el.addEventListener("pointermove", onMove);
|
|
138
|
+
let visible = true;
|
|
139
|
+
const io = new IntersectionObserver(([entry]) => (visible = entry.isIntersecting));
|
|
140
|
+
io.observe(el);
|
|
141
|
+
|
|
142
|
+
let target = targets[0];
|
|
143
|
+
// Per-particle speed so the swarm flows rather than snapping.
|
|
144
|
+
const rate = new Float32Array(n);
|
|
145
|
+
for (let i = 0; i < n; i++) rate[i] = 0.025 + seeds[i] * 0.05;
|
|
146
|
+
api.current = { go: (k) => (target = targets[((k % targets.length) + targets.length) % targets.length]) };
|
|
147
|
+
|
|
148
|
+
let raf = 0;
|
|
149
|
+
let last = performance.now();
|
|
150
|
+
const frame = (now) => {
|
|
151
|
+
raf = requestAnimationFrame(frame);
|
|
152
|
+
const dt = Math.min(0.05, (now - last) / 1000);
|
|
153
|
+
last = now;
|
|
154
|
+
if (!visible) return;
|
|
155
|
+
uniforms.uTime.value += dt;
|
|
156
|
+
uniforms.uSize.value = opts.current.pointSize;
|
|
157
|
+
uniforms.uC1.value.set(opts.current.colors[0]);
|
|
158
|
+
uniforms.uC2.value.set(opts.current.colors[1] ?? opts.current.colors[0]);
|
|
159
|
+
for (let i = 0; i < n * 3; i++) pos[i] += (target[i] - pos[i]) * rate[(i / 3) | 0] * (dt * 60);
|
|
160
|
+
geometry.attributes.position.needsUpdate = true;
|
|
161
|
+
points.rotation.y += dt * 0.15;
|
|
162
|
+
points.rotation.x += (pointer.y * 0.4 - points.rotation.x) * 0.04;
|
|
163
|
+
points.rotation.z += (-pointer.x * 0.15 - points.rotation.z) * 0.04;
|
|
164
|
+
renderer.render(scene, camera);
|
|
165
|
+
};
|
|
166
|
+
raf = requestAnimationFrame(frame);
|
|
167
|
+
|
|
168
|
+
return () => {
|
|
169
|
+
cancelAnimationFrame(raf);
|
|
170
|
+
ro.disconnect();
|
|
171
|
+
io.disconnect();
|
|
172
|
+
el.removeEventListener("pointermove", onMove);
|
|
173
|
+
geometry.dispose();
|
|
174
|
+
material.dispose();
|
|
175
|
+
renderer.dispose();
|
|
176
|
+
renderer.forceContextLoss();
|
|
177
|
+
renderer.domElement.remove();
|
|
178
|
+
api.current = null;
|
|
179
|
+
};
|
|
180
|
+
// Rebuild only when the particle set changes; colors and size are live.
|
|
181
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
182
|
+
}, [count, list.join(",")]);
|
|
183
|
+
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
api.current?.go(index);
|
|
186
|
+
}, [index]);
|
|
187
|
+
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
if (!autoPlay) return undefined;
|
|
190
|
+
const id = setInterval(() => setIndex((i) => i + 1), interval);
|
|
191
|
+
return () => clearInterval(id);
|
|
192
|
+
}, [autoPlay, interval]);
|
|
193
|
+
|
|
194
|
+
const current = list[((index % list.length) + list.length) % list.length];
|
|
195
|
+
return (
|
|
196
|
+
<div className={`relative flex w-full flex-col items-center gap-4 ${className}`}>
|
|
197
|
+
<div ref={mount} className="w-full cursor-pointer touch-none" style={{ height: `${height}rem` }} onClick={() => setIndex((i) => i + 1)} aria-hidden="true" />
|
|
198
|
+
<div className="flex flex-wrap justify-center gap-1.5" role="group" aria-label="Shape">
|
|
199
|
+
{list.map((s, i) => (
|
|
200
|
+
<button
|
|
201
|
+
key={s}
|
|
202
|
+
type="button"
|
|
203
|
+
onClick={() => setIndex(i)}
|
|
204
|
+
aria-pressed={current === s}
|
|
205
|
+
className={`rounded-full px-3 py-1 text-xs font-medium capitalize transition-colors ${current === s ? "text-black" : "opacity-60 hover:opacity-100"}`}
|
|
206
|
+
style={current === s ? { background: colors[0] } : undefined}
|
|
207
|
+
>
|
|
208
|
+
{s}
|
|
209
|
+
</button>
|
|
210
|
+
))}
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
export default ParticleMorph;
|
|
@@ -16,14 +16,21 @@ const DEFAULT_PLANS = [
|
|
|
16
16
|
|
|
17
17
|
function Price({ value, currency }) {
|
|
18
18
|
return (
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
// Old and new numbers share one grid cell (no layout popping, no flicker);
|
|
20
|
+
// an invisible copy of the current value sets the width.
|
|
21
|
+
<span className="relative inline-grid h-[1.1em] overflow-hidden align-bottom tabular-nums">
|
|
22
|
+
<span aria-hidden="true" className="invisible col-start-1 row-start-1">
|
|
23
|
+
{currency}
|
|
24
|
+
{value}
|
|
25
|
+
</span>
|
|
26
|
+
<AnimatePresence initial={false}>
|
|
21
27
|
<motion.span
|
|
22
28
|
key={value}
|
|
29
|
+
className="col-start-1 row-start-1"
|
|
23
30
|
initial={{ y: "100%", opacity: 0 }}
|
|
24
|
-
animate={{ y: 0, opacity: 1 }}
|
|
31
|
+
animate={{ y: "0%", opacity: 1 }}
|
|
25
32
|
exit={{ y: "-100%", opacity: 0 }}
|
|
26
|
-
transition={{ type: "spring", stiffness: 380, damping:
|
|
33
|
+
transition={{ type: "spring", stiffness: 380, damping: 32 }}
|
|
27
34
|
>
|
|
28
35
|
{currency}
|
|
29
36
|
{value}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from "react";
|
|
2
|
+
import { Color, DoubleSide, Mesh, PerspectiveCamera, PlaneGeometry, Scene, ShaderMaterial, WebGLRenderer } from "three";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Retro Grid 3D: a synthwave landscape. Glowing grid lines run over rolling
|
|
6
|
+
* hills that scroll toward you, with a flat road down the middle and a
|
|
7
|
+
* striped sun on the horizon. Two planes and two small shaders, so it's
|
|
8
|
+
* cheap to run as a hero background. Put your content on top as children.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const NOISE = `
|
|
12
|
+
float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
|
|
13
|
+
float noise(vec2 p){ vec2 i = floor(p); vec2 f = fract(p); vec2 u = f * f * (3. - 2. * f);
|
|
14
|
+
return mix(mix(hash(i), hash(i + vec2(1., 0.)), u.x), mix(hash(i + vec2(0., 1.)), hash(i + vec2(1., 1.)), u.x), u.y); }`;
|
|
15
|
+
|
|
16
|
+
const GROUND_VERT = `
|
|
17
|
+
uniform float uTime; uniform float uSpeed; uniform float uHeight;
|
|
18
|
+
varying vec2 vGrid; varying float vDepth; varying float vH;
|
|
19
|
+
${NOISE}
|
|
20
|
+
void main(){
|
|
21
|
+
vec3 p = position;
|
|
22
|
+
float z = p.y - uTime * uSpeed; // plane is rotated, so y is depth
|
|
23
|
+
float road = smoothstep(1.2, 3.2, abs(p.x)); // flat strip in the middle
|
|
24
|
+
float h = (noise(vec2(p.x * .35, z * .35)) * .7 + noise(vec2(p.x * .9, z * .9)) * .3) * uHeight * road;
|
|
25
|
+
p.z += h;
|
|
26
|
+
vH = h / max(uHeight, .001);
|
|
27
|
+
vGrid = vec2(p.x, z);
|
|
28
|
+
vec4 mv = modelViewMatrix * vec4(p, 1.);
|
|
29
|
+
vDepth = -mv.z;
|
|
30
|
+
gl_Position = projectionMatrix * mv;
|
|
31
|
+
}`;
|
|
32
|
+
|
|
33
|
+
const GROUND_FRAG = `
|
|
34
|
+
uniform vec3 uLine; uniform vec3 uGround; uniform float uFog;
|
|
35
|
+
varying vec2 vGrid; varying float vDepth; varying float vH;
|
|
36
|
+
void main(){
|
|
37
|
+
vec2 g = abs(fract(vGrid) - .5) / fwidth(vGrid);
|
|
38
|
+
float line = 1. - min(min(g.x, g.y), 1.);
|
|
39
|
+
vec3 col = mix(uGround, uGround * 1.6, vH);
|
|
40
|
+
col += uLine * line * 1.4;
|
|
41
|
+
float fog = smoothstep(uFog * .25, uFog, vDepth);
|
|
42
|
+
gl_FragColor = vec4(col, 1. - fog);
|
|
43
|
+
}`;
|
|
44
|
+
|
|
45
|
+
const SUN_VERT = `varying vec2 vUv; void main(){ vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.); }`;
|
|
46
|
+
const SUN_FRAG = `
|
|
47
|
+
uniform vec3 uTop; uniform vec3 uBottom; uniform float uTime;
|
|
48
|
+
varying vec2 vUv;
|
|
49
|
+
void main(){
|
|
50
|
+
vec2 c = vUv - .5;
|
|
51
|
+
float d = length(c);
|
|
52
|
+
float disc = smoothstep(.5, .49, d);
|
|
53
|
+
// Horizontal gaps in the lower half that widen toward the bottom and drift down.
|
|
54
|
+
float y = vUv.y;
|
|
55
|
+
float t = clamp((.55 - y) / .55, 0., 1.);
|
|
56
|
+
float gap = y < .55 ? step(1. - t * .75, fract(y * 16. + uTime * .3)) : 0.;
|
|
57
|
+
vec3 col = mix(uBottom, uTop, y);
|
|
58
|
+
float glow = smoothstep(.75, .3, d) * .35;
|
|
59
|
+
float a = disc * (1. - gap);
|
|
60
|
+
gl_FragColor = vec4(col * (a + glow), a + glow * .6);
|
|
61
|
+
}`;
|
|
62
|
+
|
|
63
|
+
const RetroGrid3D = ({ lineColor = "#e879f9", groundColor = "#0b0618", sunColors = ["#fde047", "#f43f5e"], skyColors = ["#1e0b3a", "#05020d"], speed = 1.2, hills = 1.4, showSun = true, height = "100%", children, className = "" }) => {
|
|
64
|
+
const mount = useRef(null);
|
|
65
|
+
const opts = useRef({ lineColor, groundColor, sunColors, speed, hills });
|
|
66
|
+
opts.current = { lineColor, groundColor, sunColors, speed, hills };
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
const el = mount.current;
|
|
70
|
+
const renderer = new WebGLRenderer({ antialias: true, alpha: true, powerPreference: "high-performance" });
|
|
71
|
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.75));
|
|
72
|
+
el.appendChild(renderer.domElement);
|
|
73
|
+
Object.assign(renderer.domElement.style, { display: "block", position: "absolute", inset: "0" });
|
|
74
|
+
const scene = new Scene();
|
|
75
|
+
const camera = new PerspectiveCamera(60, 1, 0.1, 80);
|
|
76
|
+
camera.position.set(0, 1.25, 5);
|
|
77
|
+
camera.lookAt(0, 1.0, -10);
|
|
78
|
+
|
|
79
|
+
const gUniforms = { uTime: { value: 0 }, uSpeed: { value: speed }, uHeight: { value: hills }, uLine: { value: new Color(lineColor) }, uGround: { value: new Color(groundColor) }, uFog: { value: 34 } };
|
|
80
|
+
const groundGeo = new PlaneGeometry(40, 60, 80, 120);
|
|
81
|
+
const groundMat = new ShaderMaterial({ vertexShader: GROUND_VERT, fragmentShader: GROUND_FRAG, uniforms: gUniforms, transparent: true });
|
|
82
|
+
const ground = new Mesh(groundGeo, groundMat);
|
|
83
|
+
ground.rotation.x = -Math.PI / 2;
|
|
84
|
+
ground.position.z = -22;
|
|
85
|
+
scene.add(ground);
|
|
86
|
+
|
|
87
|
+
const sUniforms = { uTop: { value: new Color(sunColors[0]) }, uBottom: { value: new Color(sunColors[1] ?? sunColors[0]) }, uTime: { value: 0 } };
|
|
88
|
+
const sunGeo = new PlaneGeometry(14, 14);
|
|
89
|
+
const sunMat = new ShaderMaterial({ vertexShader: SUN_VERT, fragmentShader: SUN_FRAG, uniforms: sUniforms, transparent: true, depthWrite: false, side: DoubleSide });
|
|
90
|
+
const sun = new Mesh(sunGeo, sunMat);
|
|
91
|
+
sun.position.set(0, 4.2, -48);
|
|
92
|
+
sun.visible = showSun;
|
|
93
|
+
scene.add(sun);
|
|
94
|
+
|
|
95
|
+
const resize = () => {
|
|
96
|
+
const { width, height: h } = el.getBoundingClientRect();
|
|
97
|
+
if (!width || !h) return;
|
|
98
|
+
renderer.setSize(width, h, false);
|
|
99
|
+
renderer.domElement.style.width = "100%";
|
|
100
|
+
renderer.domElement.style.height = "100%";
|
|
101
|
+
camera.aspect = width / h;
|
|
102
|
+
camera.fov = width / h < 0.8 ? 75 : 60;
|
|
103
|
+
camera.updateProjectionMatrix();
|
|
104
|
+
};
|
|
105
|
+
const ro = new ResizeObserver(resize);
|
|
106
|
+
ro.observe(el);
|
|
107
|
+
resize();
|
|
108
|
+
|
|
109
|
+
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
110
|
+
let visible = true;
|
|
111
|
+
const io = new IntersectionObserver(([entry]) => (visible = entry.isIntersecting));
|
|
112
|
+
io.observe(el);
|
|
113
|
+
|
|
114
|
+
let raf = 0;
|
|
115
|
+
let last = performance.now();
|
|
116
|
+
const frame = (now) => {
|
|
117
|
+
raf = requestAnimationFrame(frame);
|
|
118
|
+
const dt = Math.min(0.05, (now - last) / 1000);
|
|
119
|
+
last = now;
|
|
120
|
+
if (!visible) return;
|
|
121
|
+
const o = opts.current;
|
|
122
|
+
gUniforms.uTime.value += dt * (reduced ? 0.1 : 1);
|
|
123
|
+
sUniforms.uTime.value = gUniforms.uTime.value;
|
|
124
|
+
gUniforms.uSpeed.value = o.speed;
|
|
125
|
+
gUniforms.uHeight.value = o.hills;
|
|
126
|
+
gUniforms.uLine.value.set(o.lineColor);
|
|
127
|
+
gUniforms.uGround.value.set(o.groundColor);
|
|
128
|
+
sUniforms.uTop.value.set(o.sunColors[0]);
|
|
129
|
+
sUniforms.uBottom.value.set(o.sunColors[1] ?? o.sunColors[0]);
|
|
130
|
+
renderer.render(scene, camera);
|
|
131
|
+
};
|
|
132
|
+
raf = requestAnimationFrame(frame);
|
|
133
|
+
|
|
134
|
+
return () => {
|
|
135
|
+
cancelAnimationFrame(raf);
|
|
136
|
+
ro.disconnect();
|
|
137
|
+
io.disconnect();
|
|
138
|
+
groundGeo.dispose();
|
|
139
|
+
groundMat.dispose();
|
|
140
|
+
sunGeo.dispose();
|
|
141
|
+
sunMat.dispose();
|
|
142
|
+
renderer.dispose();
|
|
143
|
+
renderer.forceContextLoss();
|
|
144
|
+
renderer.domElement.remove();
|
|
145
|
+
};
|
|
146
|
+
// Colors, speed and hills are read live from opts.
|
|
147
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
148
|
+
}, [showSun]);
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<div className={`relative isolate w-full overflow-hidden ${className}`} style={{ height, background: `linear-gradient(to bottom, ${skyColors[1] ?? skyColors[0]} 0%, ${skyColors[0]} 55%, ${groundColor} 100%)` }}>
|
|
152
|
+
<div ref={mount} aria-hidden="true" className="absolute inset-0 -z-10" />
|
|
153
|
+
{children && <div className="relative flex h-full w-full items-center justify-center">{children}</div>}
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export default RetroGrid3D;
|
|
@@ -17,6 +17,8 @@ const ShinyText = ({ text = "Shipping soon, join the waitlist", as = "span", spe
|
|
|
17
17
|
const band = Math.min(16, Math.max(4, spread));
|
|
18
18
|
return (
|
|
19
19
|
<Tag
|
|
20
|
+
// A new key restarts the loop, so speed / pause changes apply right away.
|
|
21
|
+
key={`${speed}-${pause}-${still}`}
|
|
20
22
|
className={`inline-block bg-clip-text text-transparent ${className}`}
|
|
21
23
|
style={{
|
|
22
24
|
backgroundImage: `linear-gradient(100deg, ${color} ${50 - band}%, ${shineColor} 50%, ${color} ${50 + band}%)`,
|
|
@@ -16,13 +16,14 @@ const StarBorder = ({ as = "button", color = "#a3e635", speed = 5, thickness = 1
|
|
|
16
16
|
animate: reduced ? undefined : { x: dir > 0 ? ["-35%", "35%"] : ["35%", "-35%"], opacity: [0, 1, 1, 0] },
|
|
17
17
|
transition: { duration: speed, ease: "linear", repeat: Infinity, times: [0, 0.15, 0.85, 1] },
|
|
18
18
|
});
|
|
19
|
+
// Keyed on speed below: a looping animation keeps its old duration otherwise.
|
|
19
20
|
const top = star(-1);
|
|
20
21
|
const bottom = star(1);
|
|
21
22
|
|
|
22
23
|
return (
|
|
23
24
|
<Tag className={`group relative inline-block overflow-hidden rounded-[1.1rem] ${className}`} style={{ padding: `${thickness}px 0` }} {...(as === "button" ? { type: "button" } : {})} {...rest}>
|
|
24
|
-
<motion.span aria-hidden="true" className="pointer-events-none absolute -bottom-[11px] left-[-100%] h-1/2 w-[300%] opacity-70" style={{ background: bottom.background }} animate={bottom.animate} transition={bottom.transition} />
|
|
25
|
-
<motion.span aria-hidden="true" className="pointer-events-none absolute -top-[10px] left-[-100%] h-1/2 w-[300%] opacity-70" style={{ background: top.background }} animate={top.animate} transition={top.transition} />
|
|
25
|
+
<motion.span key={`b-${speed}`} aria-hidden="true" className="pointer-events-none absolute -bottom-[11px] left-[-100%] h-1/2 w-[300%] opacity-70" style={{ background: bottom.background }} animate={bottom.animate} transition={bottom.transition} />
|
|
26
|
+
<motion.span key={`t-${speed}`} aria-hidden="true" className="pointer-events-none absolute -top-[10px] left-[-100%] h-1/2 w-[300%] opacity-70" style={{ background: top.background }} animate={top.animate} transition={top.transition} />
|
|
26
27
|
<span className={`relative z-[1] block rounded-[1.1rem] border px-7 py-3.5 text-center text-[0.95rem] font-semibold transition-colors ${dark ? "border-white/10 bg-gradient-to-b from-neutral-900 to-black text-white group-hover:border-white/20" : "border-black/10 bg-gradient-to-b from-white to-neutral-100 text-neutral-900 group-hover:border-black/20"}`}>
|
|
27
28
|
{children}
|
|
28
29
|
</span>
|
package/dist/cli/index.js
CHANGED
|
@@ -114,6 +114,9 @@ const componentMap = {
|
|
|
114
114
|
"bounce-cards": "BounceCards",
|
|
115
115
|
"card-swap-3d": "CardSwap3D",
|
|
116
116
|
"gooey-nav": "GooeyNav",
|
|
117
|
+
"liquid-blob-3d": "LiquidBlob3D",
|
|
118
|
+
"particle-morph": "ParticleMorph",
|
|
119
|
+
"retro-grid-3d": "RetroGrid3D",
|
|
117
120
|
"water-drop-reveal": "MaskView",
|
|
118
121
|
};
|
|
119
122
|
// Packages only some components need, installed on top of the shared ones.
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apex-ui-kit",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "93 free, animated UI components for React, styled with Tailwind CSS. Add any of them with one CLI command.",
|
|
6
6
|
"homepage": "https://apex-ui.in",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|