apex-ui-kit 2.3.2 → 2.5.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 +14 -7
- package/dist/ApexUI-Kit/templates/DistortionImage/DistortionImage.jsx +136 -0
- package/dist/ApexUI-Kit/templates/Galaxy3D/Galaxy3D.jsx +152 -0
- 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/ApexUI-Kit/templates/WarpTunnel/WarpTunnel.jsx +141 -0
- package/dist/cli/index.js +6 -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
|
+
**96 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
|
+
- **96 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,23 +99,26 @@ 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 (14)
|
|
103
103
|
|
|
104
104
|
| Component | CLI name |
|
|
105
105
|
| --- | --- |
|
|
106
106
|
| [Aurora Background](https://apex-ui.in/components/aurora-background) | `aurora-background` |
|
|
107
107
|
| [BluePrint Background](https://apex-ui.in/components/blueprint-background) | `blueprint-background` |
|
|
108
108
|
| [Dot Wave Background](https://apex-ui.in/components/dot-wave-background) | `dot-wave-background` |
|
|
109
|
+
| [Galaxy 3D](https://apex-ui.in/components/galaxy-3d) | `galaxy-3d` |
|
|
109
110
|
| [Kinetic Threads Background](https://apex-ui.in/components/kinetic-threads-background) | `kinetic-threads-background` |
|
|
110
111
|
| [Lens Flare Background](https://apex-ui.in/components/lens-flare-background) | `lens-flare-background` |
|
|
111
112
|
| [Liquid Gradient](https://apex-ui.in/components/liquid-gradient) | `liquid-gradient` |
|
|
112
113
|
| [Luminous Particle Ocean](https://apex-ui.in/components/luminous-particle-ocean) | `luminous-particle-ocean` |
|
|
113
114
|
| [Orbs Background](https://apex-ui.in/components/orbs-background) | `orbs-background` |
|
|
114
115
|
| [Rain Background](https://apex-ui.in/components/rain-background) | `rain-background` |
|
|
116
|
+
| [Retro Grid 3D](https://apex-ui.in/components/retro-grid-3d) | `retro-grid-3d` |
|
|
115
117
|
| [Ripple Background](https://apex-ui.in/components/ripple-background) | `ripple-background` |
|
|
116
118
|
| [Smoke Background](https://apex-ui.in/components/smoke-background) | `smoke-background` |
|
|
119
|
+
| [Warp Tunnel](https://apex-ui.in/components/warp-tunnel) | `warp-tunnel` |
|
|
117
120
|
|
|
118
|
-
### Cards (
|
|
121
|
+
### Cards (18)
|
|
119
122
|
|
|
120
123
|
| Component | CLI name |
|
|
121
124
|
| --- | --- |
|
|
@@ -123,6 +126,7 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
123
126
|
| [Card Stack](https://apex-ui.in/components/card-stack) | `card-stack` |
|
|
124
127
|
| [Card Swap 3D](https://apex-ui.in/components/card-swap-3d) | `card-swap-3d` |
|
|
125
128
|
| [Cards](https://apex-ui.in/components/cards) | `basic-card`, `meteor-card` |
|
|
129
|
+
| [Distortion Image](https://apex-ui.in/components/distortion-image) | `distortion-image` |
|
|
126
130
|
| [Folder](https://apex-ui.in/components/folder) | `folder` |
|
|
127
131
|
| [Glare Card](https://apex-ui.in/components/glare-card) | `glare-card` |
|
|
128
132
|
| [Holo Card](https://apex-ui.in/components/holo-card) | `holo-card` |
|
|
@@ -181,7 +185,7 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
181
185
|
| [Animated Switches](https://apex-ui.in/components/animated-switches) | `animated-switch` |
|
|
182
186
|
| [Star Border](https://apex-ui.in/components/star-border) | `star-border` |
|
|
183
187
|
|
|
184
|
-
### Interactive (
|
|
188
|
+
### Interactive (22)
|
|
185
189
|
|
|
186
190
|
| Component | CLI name |
|
|
187
191
|
| --- | --- |
|
|
@@ -198,8 +202,10 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
198
202
|
| [FAQ Accordion](https://apex-ui.in/components/faq-accordion) | `faq-accordion` |
|
|
199
203
|
| [Flip Book](https://apex-ui.in/components/flip-book) | `flip-book` |
|
|
200
204
|
| [Image Compare](https://apex-ui.in/components/image-compare) | `image-compare` |
|
|
205
|
+
| [Liquid Blob 3D](https://apex-ui.in/components/liquid-blob-3d) | `liquid-blob-3d` |
|
|
201
206
|
| [Magnet Lines](https://apex-ui.in/components/magnet-lines) | `magnet-lines` |
|
|
202
207
|
| [Magnetic Cursor](https://apex-ui.in/components/magnetic-cursor) | `magnetic-cursor` |
|
|
208
|
+
| [Particle Morph](https://apex-ui.in/components/particle-morph) | `particle-morph` |
|
|
203
209
|
| [Pointer Follower](https://apex-ui.in/components/pointer-follower) | `pointer-follower` |
|
|
204
210
|
| [Receipt Printer](https://apex-ui.in/components/receipt-printer) | `receipt-printer` |
|
|
205
211
|
| [Stepper](https://apex-ui.in/components/stepper) | `stepper` |
|
|
@@ -234,10 +240,11 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
234
240
|
| [Dot Globe](https://apex-ui.in/components/dot-globe) | `dot-globe` |
|
|
235
241
|
| [World Map](https://apex-ui.in/components/world-map) | `world-map` |
|
|
236
242
|
|
|
237
|
-
## What's new in 2.
|
|
243
|
+
## What's new in 2.5
|
|
238
244
|
|
|
239
|
-
-
|
|
245
|
+
- 27 new components, including WebGL and three.js (Galaxy 3D, Warp Tunnel, Distortion Image, 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
246
|
- AI Model Prompt now exports `DEFAULT_LLM_MODELS` and an index file, so you can import both from the folder.
|
|
247
|
+
- Loading Screens show real progress: `useLoadProgress` loads your images, files or code and drives the bar and the count.
|
|
241
248
|
- Hold to Confirm follows a delete flow with an `UndoNotice` that counts down before the action is final.
|
|
242
249
|
- Sliders and drag components no longer stop midway when the page text gets selected.
|
|
243
250
|
- 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,136 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from "react";
|
|
2
|
+
import { Mesh, OrthographicCamera, PlaneGeometry, Scene, ShaderMaterial, SRGBColorSpace, TextureLoader, Vector2, WebGLRenderer } from "three";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Distortion Image: an image drawn in WebGL that ripples where the pointer
|
|
6
|
+
* moves. The ripple bends the picture, splits it into red / green / blue
|
|
7
|
+
* fringes and fades out smoothly when the pointer leaves. Image is "cover"
|
|
8
|
+
* fitted like CSS object-fit. Pauses off screen, frees the GPU on unmount.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const VERT = `varying vec2 vUv; void main(){ vUv = uv; gl_Position = vec4(position.xy, 0., 1.); }`;
|
|
12
|
+
|
|
13
|
+
const FRAG = `
|
|
14
|
+
uniform sampler2D uTex; uniform vec2 uPointer; uniform float uHover; uniform float uTime;
|
|
15
|
+
uniform float uStrength; uniform float uRgb; uniform vec2 uCover; uniform float uAspect;
|
|
16
|
+
varying vec2 vUv;
|
|
17
|
+
void main(){
|
|
18
|
+
// object-fit: cover
|
|
19
|
+
vec2 uv = (vUv - .5) * uCover + .5;
|
|
20
|
+
vec2 d = vUv - uPointer; d.x *= uAspect;
|
|
21
|
+
float dist = length(d);
|
|
22
|
+
float wave = sin(dist * 28. - uTime * 6.) * exp(-dist * 5.) * uHover;
|
|
23
|
+
vec2 dir = dist > 0. ? normalize(d) : vec2(0.);
|
|
24
|
+
vec2 offset = dir * wave * .03 * uStrength;
|
|
25
|
+
float split = uRgb * uHover * exp(-dist * 4.) * .012;
|
|
26
|
+
float r = texture2D(uTex, uv + offset + dir * split).r;
|
|
27
|
+
float g = texture2D(uTex, uv + offset).g;
|
|
28
|
+
float b = texture2D(uTex, uv + offset - dir * split).b;
|
|
29
|
+
gl_FragColor = vec4(r, g, b, 1.);
|
|
30
|
+
}`;
|
|
31
|
+
|
|
32
|
+
const DistortionImage = ({ src = "https://ik.imagekit.io/ny6yinyut/apexui/assets/doctorStrange.png?tr=f-auto,q-85,w-1000", alt = "", strength = 1, rgbSplit = 1, aspectRatio = "4 / 5", width = 22, rounded = "1.25rem", className = "" }) => {
|
|
33
|
+
const mount = useRef(null);
|
|
34
|
+
const opts = useRef({ strength, rgbSplit });
|
|
35
|
+
opts.current = { strength, rgbSplit };
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
const el = mount.current;
|
|
39
|
+
const renderer = new WebGLRenderer({ antialias: false, alpha: true, powerPreference: "high-performance" });
|
|
40
|
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
|
41
|
+
renderer.outputColorSpace = SRGBColorSpace;
|
|
42
|
+
Object.assign(renderer.domElement.style, { display: "block", width: "100%", height: "100%" });
|
|
43
|
+
el.appendChild(renderer.domElement);
|
|
44
|
+
const scene = new Scene();
|
|
45
|
+
const camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
|
46
|
+
const uniforms = {
|
|
47
|
+
uTex: { value: null },
|
|
48
|
+
uPointer: { value: new Vector2(0.5, 0.5) },
|
|
49
|
+
uHover: { value: 0 },
|
|
50
|
+
uTime: { value: 0 },
|
|
51
|
+
uStrength: { value: strength },
|
|
52
|
+
uRgb: { value: rgbSplit },
|
|
53
|
+
uCover: { value: new Vector2(1, 1) },
|
|
54
|
+
uAspect: { value: 1 },
|
|
55
|
+
};
|
|
56
|
+
const geometry = new PlaneGeometry(2, 2);
|
|
57
|
+
const material = new ShaderMaterial({ vertexShader: VERT, fragmentShader: FRAG, uniforms });
|
|
58
|
+
scene.add(new Mesh(geometry, material));
|
|
59
|
+
|
|
60
|
+
let imgAspect = 1;
|
|
61
|
+
const fit = () => {
|
|
62
|
+
const { width, height } = el.getBoundingClientRect();
|
|
63
|
+
if (!width || !height) return;
|
|
64
|
+
renderer.setSize(width, height, false);
|
|
65
|
+
const box = width / height;
|
|
66
|
+
uniforms.uAspect.value = box;
|
|
67
|
+
// Scale UVs so the image covers the box without stretching.
|
|
68
|
+
uniforms.uCover.value.set(box < imgAspect ? box / imgAspect : 1, box < imgAspect ? 1 : imgAspect / box);
|
|
69
|
+
};
|
|
70
|
+
let texture = null;
|
|
71
|
+
let disposed = false;
|
|
72
|
+
new TextureLoader().setCrossOrigin("anonymous").load(src, (t) => {
|
|
73
|
+
if (disposed) return t.dispose();
|
|
74
|
+
t.colorSpace = SRGBColorSpace;
|
|
75
|
+
texture = t;
|
|
76
|
+
imgAspect = t.image.width / t.image.height;
|
|
77
|
+
uniforms.uTex.value = t;
|
|
78
|
+
fit();
|
|
79
|
+
});
|
|
80
|
+
const ro = new ResizeObserver(fit);
|
|
81
|
+
ro.observe(el);
|
|
82
|
+
fit();
|
|
83
|
+
|
|
84
|
+
const target = { x: 0.5, y: 0.5, hover: 0 };
|
|
85
|
+
const onMove = (e) => {
|
|
86
|
+
const r = el.getBoundingClientRect();
|
|
87
|
+
target.x = (e.clientX - r.left) / r.width;
|
|
88
|
+
target.y = 1 - (e.clientY - r.top) / r.height;
|
|
89
|
+
target.hover = 1;
|
|
90
|
+
};
|
|
91
|
+
const onLeave = () => (target.hover = 0);
|
|
92
|
+
el.addEventListener("pointermove", onMove);
|
|
93
|
+
el.addEventListener("pointerleave", onLeave);
|
|
94
|
+
let visible = true;
|
|
95
|
+
const io = new IntersectionObserver(([entry]) => (visible = entry.isIntersecting));
|
|
96
|
+
io.observe(el);
|
|
97
|
+
|
|
98
|
+
let raf = 0;
|
|
99
|
+
let last = performance.now();
|
|
100
|
+
const frame = (now) => {
|
|
101
|
+
raf = requestAnimationFrame(frame);
|
|
102
|
+
const dt = Math.min(0.05, (now - last) / 1000);
|
|
103
|
+
last = now;
|
|
104
|
+
if (!visible || !uniforms.uTex.value) return;
|
|
105
|
+
uniforms.uTime.value += dt;
|
|
106
|
+
uniforms.uPointer.value.x += (target.x - uniforms.uPointer.value.x) * 0.12;
|
|
107
|
+
uniforms.uPointer.value.y += (target.y - uniforms.uPointer.value.y) * 0.12;
|
|
108
|
+
uniforms.uHover.value += (target.hover - uniforms.uHover.value) * 0.06;
|
|
109
|
+
uniforms.uStrength.value = opts.current.strength;
|
|
110
|
+
uniforms.uRgb.value = opts.current.rgbSplit;
|
|
111
|
+
renderer.render(scene, camera);
|
|
112
|
+
};
|
|
113
|
+
raf = requestAnimationFrame(frame);
|
|
114
|
+
|
|
115
|
+
return () => {
|
|
116
|
+
disposed = true;
|
|
117
|
+
cancelAnimationFrame(raf);
|
|
118
|
+
ro.disconnect();
|
|
119
|
+
io.disconnect();
|
|
120
|
+
el.removeEventListener("pointermove", onMove);
|
|
121
|
+
el.removeEventListener("pointerleave", onLeave);
|
|
122
|
+
texture?.dispose();
|
|
123
|
+
geometry.dispose();
|
|
124
|
+
material.dispose();
|
|
125
|
+
renderer.dispose();
|
|
126
|
+
renderer.forceContextLoss();
|
|
127
|
+
renderer.domElement.remove();
|
|
128
|
+
};
|
|
129
|
+
// Strength and RGB split are read live from opts; only a new image reloads.
|
|
130
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
131
|
+
}, [src]);
|
|
132
|
+
|
|
133
|
+
return <div ref={mount} role="img" aria-label={alt} className={`relative overflow-hidden bg-neutral-900 ${className}`} style={{ width: `min(${width}rem, 88vw)`, aspectRatio, borderRadius: rounded }} />;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export default DistortionImage;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from "react";
|
|
2
|
+
import { AdditiveBlending, BufferAttribute, BufferGeometry, Color, PerspectiveCamera, Points, Scene, ShaderMaterial, WebGLRenderer } from "three";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Galaxy 3D: a spiral galaxy of glowing stars that slowly turns, tilted so
|
|
6
|
+
* you see its arms in depth. Colors blend from a hot core to cool arm tips,
|
|
7
|
+
* and the camera drifts with the pointer for a soft parallax. One draw call;
|
|
8
|
+
* the spin runs in the shader, so the CPU does almost nothing per frame.
|
|
9
|
+
* Put your content on top as children.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const VERT = `
|
|
13
|
+
attribute float aSize; attribute vec3 aColor; attribute float aRadius; attribute float aAngle; attribute float aHeight;
|
|
14
|
+
uniform float uTime; uniform float uSpeed; uniform float uDpr; uniform float uSize;
|
|
15
|
+
varying vec3 vColor;
|
|
16
|
+
void main(){
|
|
17
|
+
// Inner stars orbit faster than outer ones.
|
|
18
|
+
float a = aAngle + uTime * uSpeed * (0.6 / (0.35 + aRadius));
|
|
19
|
+
vec3 p = vec3(cos(a) * aRadius, aHeight, sin(a) * aRadius);
|
|
20
|
+
vec4 mv = modelViewMatrix * vec4(p, 1.);
|
|
21
|
+
gl_PointSize = aSize * uSize * uDpr * (18. / -mv.z);
|
|
22
|
+
gl_Position = projectionMatrix * mv;
|
|
23
|
+
vColor = aColor;
|
|
24
|
+
}`;
|
|
25
|
+
|
|
26
|
+
const FRAG = `
|
|
27
|
+
varying vec3 vColor;
|
|
28
|
+
void main(){
|
|
29
|
+
float d = length(gl_PointCoord - .5);
|
|
30
|
+
float a = pow(smoothstep(.5, 0., d), 1.8);
|
|
31
|
+
gl_FragColor = vec4(vColor * a * 1.35, a);
|
|
32
|
+
}`;
|
|
33
|
+
|
|
34
|
+
const Galaxy3D = ({ count = 30000, arms = 4, spin = 1.2, colors = ["#fde68a", "#a78bfa", "#22d3ee"], speed = 1, starSize = 1, tilt = 0.9, height = "100%", children, className = "" }) => {
|
|
35
|
+
const mount = useRef(null);
|
|
36
|
+
const opts = useRef({ speed, starSize });
|
|
37
|
+
opts.current = { speed, starSize };
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const el = mount.current;
|
|
41
|
+
const renderer = new WebGLRenderer({ antialias: false, alpha: true, powerPreference: "high-performance" });
|
|
42
|
+
const dpr = Math.min(window.devicePixelRatio || 1, 1.75);
|
|
43
|
+
renderer.setPixelRatio(dpr);
|
|
44
|
+
Object.assign(renderer.domElement.style, { display: "block", position: "absolute", inset: "0", width: "100%", height: "100%" });
|
|
45
|
+
el.appendChild(renderer.domElement);
|
|
46
|
+
const scene = new Scene();
|
|
47
|
+
const camera = new PerspectiveCamera(55, 1, 0.1, 100);
|
|
48
|
+
|
|
49
|
+
const radius = new Float32Array(count);
|
|
50
|
+
const angle = new Float32Array(count);
|
|
51
|
+
const heightArr = new Float32Array(count);
|
|
52
|
+
const sizes = new Float32Array(count);
|
|
53
|
+
const cols = new Float32Array(count * 3);
|
|
54
|
+
const inner = new Color(colors[0]);
|
|
55
|
+
const mid = new Color(colors[1] ?? colors[0]);
|
|
56
|
+
const outer = new Color(colors[2] ?? colors[1] ?? colors[0]);
|
|
57
|
+
const c = new Color();
|
|
58
|
+
const R = 5;
|
|
59
|
+
for (let i = 0; i < count; i++) {
|
|
60
|
+
const r = Math.pow(Math.random(), 1.6) * R;
|
|
61
|
+
const arm = ((i % arms) / arms) * Math.PI * 2;
|
|
62
|
+
// Scatter around the arm, tighter in the core.
|
|
63
|
+
const scatter = (Math.random() - 0.5) * (0.35 + r * 0.12);
|
|
64
|
+
radius[i] = r + (Math.random() - 0.5) * 0.25;
|
|
65
|
+
angle[i] = arm + r * spin + scatter;
|
|
66
|
+
heightArr[i] = (Math.random() - 0.5) * 0.35 * Math.exp(-r * 0.45);
|
|
67
|
+
sizes[i] = 0.5 + Math.random() * 1.6 * (r < 0.6 ? 1.8 : 1);
|
|
68
|
+
const t = r / R;
|
|
69
|
+
if (t < 0.5) c.copy(inner).lerp(mid, t * 2);
|
|
70
|
+
else c.copy(mid).lerp(outer, (t - 0.5) * 2);
|
|
71
|
+
cols.set([c.r, c.g, c.b], i * 3);
|
|
72
|
+
}
|
|
73
|
+
const geometry = new BufferGeometry();
|
|
74
|
+
geometry.setAttribute("position", new BufferAttribute(new Float32Array(count * 3), 3));
|
|
75
|
+
geometry.setAttribute("aRadius", new BufferAttribute(radius, 1));
|
|
76
|
+
geometry.setAttribute("aAngle", new BufferAttribute(angle, 1));
|
|
77
|
+
geometry.setAttribute("aHeight", new BufferAttribute(heightArr, 1));
|
|
78
|
+
geometry.setAttribute("aSize", new BufferAttribute(sizes, 1));
|
|
79
|
+
geometry.setAttribute("aColor", new BufferAttribute(cols, 3));
|
|
80
|
+
geometry.boundingSphere = null;
|
|
81
|
+
const uniforms = { uTime: { value: 0 }, uSpeed: { value: speed }, uDpr: { value: dpr }, uSize: { value: starSize } };
|
|
82
|
+
const material = new ShaderMaterial({ vertexShader: VERT, fragmentShader: FRAG, uniforms, transparent: true, depthWrite: false, blending: AdditiveBlending });
|
|
83
|
+
const points = new Points(geometry, material);
|
|
84
|
+
points.frustumCulled = false;
|
|
85
|
+
points.rotation.x = tilt;
|
|
86
|
+
scene.add(points);
|
|
87
|
+
|
|
88
|
+
const resize = () => {
|
|
89
|
+
const { width, height: h } = el.getBoundingClientRect();
|
|
90
|
+
if (!width || !h) return;
|
|
91
|
+
renderer.setSize(width, h, false);
|
|
92
|
+
camera.aspect = width / h;
|
|
93
|
+
camera.updateProjectionMatrix();
|
|
94
|
+
};
|
|
95
|
+
const ro = new ResizeObserver(resize);
|
|
96
|
+
ro.observe(el);
|
|
97
|
+
resize();
|
|
98
|
+
|
|
99
|
+
const pointer = { x: 0, y: 0 };
|
|
100
|
+
const onMove = (e) => {
|
|
101
|
+
const r = el.getBoundingClientRect();
|
|
102
|
+
pointer.x = ((e.clientX - r.left) / r.width) * 2 - 1;
|
|
103
|
+
pointer.y = ((e.clientY - r.top) / r.height) * 2 - 1;
|
|
104
|
+
};
|
|
105
|
+
el.addEventListener("pointermove", onMove);
|
|
106
|
+
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
107
|
+
let visible = true;
|
|
108
|
+
const io = new IntersectionObserver(([entry]) => (visible = entry.isIntersecting));
|
|
109
|
+
io.observe(el);
|
|
110
|
+
|
|
111
|
+
let raf = 0;
|
|
112
|
+
let last = performance.now();
|
|
113
|
+
const cam = { x: 0, y: 2.2 };
|
|
114
|
+
const frame = (now) => {
|
|
115
|
+
raf = requestAnimationFrame(frame);
|
|
116
|
+
const dt = Math.min(0.05, (now - last) / 1000);
|
|
117
|
+
last = now;
|
|
118
|
+
if (!visible) return;
|
|
119
|
+
uniforms.uTime.value += dt * (reduced ? 0.1 : 1);
|
|
120
|
+
uniforms.uSpeed.value = opts.current.speed;
|
|
121
|
+
uniforms.uSize.value = opts.current.starSize;
|
|
122
|
+
cam.x += (pointer.x * 1.2 - cam.x) * 0.03;
|
|
123
|
+
cam.y += (2.2 - pointer.y * 0.8 - cam.y) * 0.03;
|
|
124
|
+
camera.position.set(cam.x, cam.y, 7.5);
|
|
125
|
+
camera.lookAt(0, 0, 0);
|
|
126
|
+
renderer.render(scene, camera);
|
|
127
|
+
};
|
|
128
|
+
raf = requestAnimationFrame(frame);
|
|
129
|
+
|
|
130
|
+
return () => {
|
|
131
|
+
cancelAnimationFrame(raf);
|
|
132
|
+
ro.disconnect();
|
|
133
|
+
io.disconnect();
|
|
134
|
+
el.removeEventListener("pointermove", onMove);
|
|
135
|
+
geometry.dispose();
|
|
136
|
+
material.dispose();
|
|
137
|
+
renderer.dispose();
|
|
138
|
+
renderer.forceContextLoss();
|
|
139
|
+
renderer.domElement.remove();
|
|
140
|
+
};
|
|
141
|
+
// Star layout rebuilds on these; speed and size are read live.
|
|
142
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
143
|
+
}, [count, arms, spin, tilt, colors.join(",")]);
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<div ref={mount} className={`relative isolate w-full overflow-hidden bg-[radial-gradient(ellipse_at_center,#0b1026_0%,#02030a_70%)] ${className}`} style={{ height }}>
|
|
147
|
+
{children && <div className="pointer-events-none relative z-10 flex h-full w-full items-center justify-center [&>*]:pointer-events-auto">{children}</div>}
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export default Galaxy3D;
|
|
@@ -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>
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from "react";
|
|
2
|
+
import { Color, Mesh, OrthographicCamera, PlaneGeometry, Scene, ShaderMaterial, Vector2, WebGLRenderer } from "three";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Warp Tunnel: a hyperspace tunnel drawn entirely in one fragment shader.
|
|
6
|
+
* Glowing rings and streaks rush toward you and the tunnel bends toward the
|
|
7
|
+
* pointer. Hold the mouse (or touch) to boost into warp speed. Cheap: one
|
|
8
|
+
* full-screen triangle pair, no geometry to update. Content goes on top.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const VERT = `varying vec2 vUv; void main(){ vUv = uv; gl_Position = vec4(position.xy, 0., 1.); }`;
|
|
12
|
+
|
|
13
|
+
const FRAG = `
|
|
14
|
+
uniform float uTime; uniform vec2 uRes; uniform vec2 uBend; uniform float uBoost;
|
|
15
|
+
uniform vec3 uC1; uniform vec3 uC2; uniform float uDensity;
|
|
16
|
+
varying vec2 vUv;
|
|
17
|
+
float hash(vec2 p){ return fract(sin(dot(p, vec2(41.3, 289.1))) * 45758.5453); }
|
|
18
|
+
void main(){
|
|
19
|
+
vec2 p = (vUv - .5) * vec2(uRes.x / uRes.y, 1.);
|
|
20
|
+
p -= uBend * .25 * length(p); // bend toward the pointer
|
|
21
|
+
float r = length(p);
|
|
22
|
+
float a = atan(p.y, p.x);
|
|
23
|
+
float depth = .35 / max(r, .001) + uTime; // tunnel coordinate
|
|
24
|
+
// Rings rushing forward
|
|
25
|
+
float rings = smoothstep(.92, 1., sin(depth * 6.2831 * .5 * uDensity) * .5 + .5);
|
|
26
|
+
// Streaks: random angular slices that flicker along the tunnel
|
|
27
|
+
float slice = floor(a / 6.2831 * 64.);
|
|
28
|
+
float h = hash(vec2(slice, floor(depth * 2.)));
|
|
29
|
+
float streak = step(.82, h) * smoothstep(.0, .8, fract(depth * 2.)) * (1. - fract(depth * 2.));
|
|
30
|
+
float glow = (rings * .8 + streak * (1.2 + uBoost * 2.)) * smoothstep(.0, .25, r);
|
|
31
|
+
vec3 col = mix(uC1, uC2, .5 + .5 * sin(a * 2. + depth * .6));
|
|
32
|
+
float core = smoothstep(.5, .0, r) * (.25 + uBoost * .5);
|
|
33
|
+
vec3 c = col * glow * (1.4 - r) + col * core;
|
|
34
|
+
c *= smoothstep(1.2, .2, r); // vignette
|
|
35
|
+
gl_FragColor = vec4(c, 1.);
|
|
36
|
+
}`;
|
|
37
|
+
|
|
38
|
+
const WarpTunnel = ({ colors = ["#a3e635", "#8b5cf6"], speed = 1, density = 1, interactive = true, height = "100%", children, className = "" }) => {
|
|
39
|
+
const mount = useRef(null);
|
|
40
|
+
const opts = useRef({ colors, speed, density, interactive });
|
|
41
|
+
opts.current = { colors, speed, density, interactive };
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
const el = mount.current;
|
|
45
|
+
const renderer = new WebGLRenderer({ antialias: false, powerPreference: "high-performance" });
|
|
46
|
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.5));
|
|
47
|
+
Object.assign(renderer.domElement.style, { display: "block", position: "absolute", inset: "0", width: "100%", height: "100%" });
|
|
48
|
+
el.appendChild(renderer.domElement);
|
|
49
|
+
const scene = new Scene();
|
|
50
|
+
const camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
|
51
|
+
const uniforms = {
|
|
52
|
+
uTime: { value: 0 },
|
|
53
|
+
uRes: { value: new Vector2(1, 1) },
|
|
54
|
+
uBend: { value: new Vector2(0, 0) },
|
|
55
|
+
uBoost: { value: 0 },
|
|
56
|
+
uC1: { value: new Color(colors[0]) },
|
|
57
|
+
uC2: { value: new Color(colors[1] ?? colors[0]) },
|
|
58
|
+
uDensity: { value: density },
|
|
59
|
+
};
|
|
60
|
+
const geometry = new PlaneGeometry(2, 2);
|
|
61
|
+
const material = new ShaderMaterial({ vertexShader: VERT, fragmentShader: FRAG, uniforms });
|
|
62
|
+
scene.add(new Mesh(geometry, material));
|
|
63
|
+
|
|
64
|
+
const resize = () => {
|
|
65
|
+
const { width, height: h } = el.getBoundingClientRect();
|
|
66
|
+
if (!width || !h) return;
|
|
67
|
+
renderer.setSize(width, h, false);
|
|
68
|
+
uniforms.uRes.value.set(width, h);
|
|
69
|
+
};
|
|
70
|
+
const ro = new ResizeObserver(resize);
|
|
71
|
+
ro.observe(el);
|
|
72
|
+
resize();
|
|
73
|
+
|
|
74
|
+
const target = { x: 0, y: 0, boost: 0 };
|
|
75
|
+
const onMove = (e) => {
|
|
76
|
+
const r = el.getBoundingClientRect();
|
|
77
|
+
target.x = ((e.clientX - r.left) / r.width) * 2 - 1;
|
|
78
|
+
target.y = -(((e.clientY - r.top) / r.height) * 2 - 1);
|
|
79
|
+
};
|
|
80
|
+
const onDown = () => (target.boost = 1);
|
|
81
|
+
const onUp = () => (target.boost = 0);
|
|
82
|
+
const onLeave = () => {
|
|
83
|
+
target.x = 0;
|
|
84
|
+
target.y = 0;
|
|
85
|
+
target.boost = 0;
|
|
86
|
+
};
|
|
87
|
+
el.addEventListener("pointermove", onMove);
|
|
88
|
+
el.addEventListener("pointerdown", onDown);
|
|
89
|
+
el.addEventListener("pointerup", onUp);
|
|
90
|
+
el.addEventListener("pointerleave", onLeave);
|
|
91
|
+
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
92
|
+
let visible = true;
|
|
93
|
+
const io = new IntersectionObserver(([entry]) => (visible = entry.isIntersecting));
|
|
94
|
+
io.observe(el);
|
|
95
|
+
|
|
96
|
+
let raf = 0;
|
|
97
|
+
let last = performance.now();
|
|
98
|
+
const frame = (now) => {
|
|
99
|
+
raf = requestAnimationFrame(frame);
|
|
100
|
+
const dt = Math.min(0.05, (now - last) / 1000);
|
|
101
|
+
last = now;
|
|
102
|
+
if (!visible) return;
|
|
103
|
+
const o = opts.current;
|
|
104
|
+
const active = o.interactive ? target : { x: 0, y: 0, boost: 0 };
|
|
105
|
+
uniforms.uBoost.value += (active.boost - uniforms.uBoost.value) * 0.05;
|
|
106
|
+
uniforms.uTime.value += dt * o.speed * (reduced ? 0.15 : 1) * (1 + uniforms.uBoost.value * 3);
|
|
107
|
+
uniforms.uBend.value.x += (active.x - uniforms.uBend.value.x) * 0.05;
|
|
108
|
+
uniforms.uBend.value.y += (active.y - uniforms.uBend.value.y) * 0.05;
|
|
109
|
+
uniforms.uC1.value.set(o.colors[0]);
|
|
110
|
+
uniforms.uC2.value.set(o.colors[1] ?? o.colors[0]);
|
|
111
|
+
uniforms.uDensity.value = o.density;
|
|
112
|
+
renderer.render(scene, camera);
|
|
113
|
+
};
|
|
114
|
+
raf = requestAnimationFrame(frame);
|
|
115
|
+
|
|
116
|
+
return () => {
|
|
117
|
+
cancelAnimationFrame(raf);
|
|
118
|
+
ro.disconnect();
|
|
119
|
+
io.disconnect();
|
|
120
|
+
el.removeEventListener("pointermove", onMove);
|
|
121
|
+
el.removeEventListener("pointerdown", onDown);
|
|
122
|
+
el.removeEventListener("pointerup", onUp);
|
|
123
|
+
el.removeEventListener("pointerleave", onLeave);
|
|
124
|
+
geometry.dispose();
|
|
125
|
+
material.dispose();
|
|
126
|
+
renderer.dispose();
|
|
127
|
+
renderer.forceContextLoss();
|
|
128
|
+
renderer.domElement.remove();
|
|
129
|
+
};
|
|
130
|
+
// Everything visual is read live from opts.
|
|
131
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
132
|
+
}, []);
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div ref={mount} className={`relative isolate w-full touch-none select-none overflow-hidden bg-black ${className}`} style={{ height }}>
|
|
136
|
+
{children && <div className="pointer-events-none relative z-10 flex h-full w-full items-center justify-center [&>*]:pointer-events-auto">{children}</div>}
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export default WarpTunnel;
|
package/dist/cli/index.js
CHANGED
|
@@ -114,6 +114,12 @@ 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",
|
|
120
|
+
"distortion-image": "DistortionImage",
|
|
121
|
+
"galaxy-3d": "Galaxy3D",
|
|
122
|
+
"warp-tunnel": "WarpTunnel",
|
|
117
123
|
"water-drop-reveal": "MaskView",
|
|
118
124
|
};
|
|
119
125
|
// 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.5.0",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "96 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",
|