apex-ui-kit 2.4.0 → 2.6.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 -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/LoadingScreen/LoadingScreen.jsx +40 -48
- package/dist/ApexUI-Kit/templates/LoadingScreen/usePageProgress.js +135 -0
- package/dist/ApexUI-Kit/templates/WarpTunnel/WarpTunnel.jsx +141 -0
- 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
|
+
**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,13 +99,14 @@ 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` |
|
|
@@ -115,8 +116,9 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
115
116
|
| [Retro Grid 3D](https://apex-ui.in/components/retro-grid-3d) | `retro-grid-3d` |
|
|
116
117
|
| [Ripple Background](https://apex-ui.in/components/ripple-background) | `ripple-background` |
|
|
117
118
|
| [Smoke Background](https://apex-ui.in/components/smoke-background) | `smoke-background` |
|
|
119
|
+
| [Warp Tunnel](https://apex-ui.in/components/warp-tunnel) | `warp-tunnel` |
|
|
118
120
|
|
|
119
|
-
### Cards (
|
|
121
|
+
### Cards (18)
|
|
120
122
|
|
|
121
123
|
| Component | CLI name |
|
|
122
124
|
| --- | --- |
|
|
@@ -124,6 +126,7 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
124
126
|
| [Card Stack](https://apex-ui.in/components/card-stack) | `card-stack` |
|
|
125
127
|
| [Card Swap 3D](https://apex-ui.in/components/card-swap-3d) | `card-swap-3d` |
|
|
126
128
|
| [Cards](https://apex-ui.in/components/cards) | `basic-card`, `meteor-card` |
|
|
129
|
+
| [Distortion Image](https://apex-ui.in/components/distortion-image) | `distortion-image` |
|
|
127
130
|
| [Folder](https://apex-ui.in/components/folder) | `folder` |
|
|
128
131
|
| [Glare Card](https://apex-ui.in/components/glare-card) | `glare-card` |
|
|
129
132
|
| [Holo Card](https://apex-ui.in/components/holo-card) | `holo-card` |
|
|
@@ -237,11 +240,11 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
237
240
|
| [Dot Globe](https://apex-ui.in/components/dot-globe) | `dot-globe` |
|
|
238
241
|
| [World Map](https://apex-ui.in/components/world-map) | `world-map` |
|
|
239
242
|
|
|
240
|
-
## What's new in 2.
|
|
243
|
+
## What's new in 2.6
|
|
241
244
|
|
|
242
|
-
-
|
|
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.
|
|
243
246
|
- AI Model Prompt now exports `DEFAULT_LLM_MODELS` and an index file, so you can import both from the folder.
|
|
244
|
-
- Loading Screens
|
|
247
|
+
- Loading Screens follow the real page load by default: drop `<LoadingScreen fullScreen />` into your app and it covers the page while it loads, with a live bar and percentage, then hides itself. Pass `progress` to drive it yourself.
|
|
245
248
|
- Hold to Confirm follows a delete flow with an `UndoNotice` that counts down before the action is final.
|
|
246
249
|
- Sliders and drag components no longer stop midway when the page text gets selected.
|
|
247
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;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from "react";
|
|
2
2
|
import { AnimatePresence, motion, useAnimationControls, useReducedMotion } from "framer-motion";
|
|
3
|
+
import { usePageProgress } from "./usePageProgress.js";
|
|
3
4
|
|
|
4
5
|
/*
|
|
5
6
|
* Loading Screen: five ways to wait.
|
|
@@ -9,50 +10,27 @@ import { AnimatePresence, motion, useAnimationControls, useReducedMotion } from
|
|
|
9
10
|
* catch – a mini game: tap the orb before its ring runs out and build a combo
|
|
10
11
|
* runner – a mini game: jump the neon pillars (Space / ↑ / tap) while things load
|
|
11
12
|
* racer – a mini game: steer through traffic (← → / tap a side) while things load
|
|
12
|
-
*
|
|
13
|
+
* By default it shows the real page load: the bar and the count follow the
|
|
14
|
+
* images, scripts, styles and fonts the page is actually loading, and it
|
|
15
|
+
* hides itself once everything has loaded (autoHide). Pass `progress`
|
|
16
|
+
* (0 to 100) to drive it yourself instead.
|
|
13
17
|
*/
|
|
14
18
|
|
|
15
|
-
const MESSAGES = ["
|
|
19
|
+
const MESSAGES = ["Starting up", "Loading page", "Loading content", "Almost ready"];
|
|
16
20
|
const messageFor = (v) => MESSAGES[Math.min(MESSAGES.length - 1, Math.floor(v / (100 / MESSAGES.length)))];
|
|
17
21
|
|
|
18
|
-
//
|
|
19
|
-
function useProgress(progress,
|
|
20
|
-
const
|
|
22
|
+
// Real progress: your own `progress` when given, otherwise the page's real load.
|
|
23
|
+
function useProgress(progress, onComplete) {
|
|
24
|
+
const controlled = typeof progress === "number";
|
|
25
|
+
const page = usePageProgress(!controlled);
|
|
26
|
+
const value = Math.round(Math.max(0, Math.min(100, controlled ? progress : page.progress)));
|
|
21
27
|
const doneRef = useRef(onComplete);
|
|
22
28
|
doneRef.current = onComplete;
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
if (controlled) return undefined;
|
|
27
|
-
let raf = 0;
|
|
28
|
-
let start = performance.now();
|
|
29
|
-
let fired = false;
|
|
30
|
-
const tick = (now) => {
|
|
31
|
-
const t = Math.min(1, Math.max(0, (now - start) / duration));
|
|
32
|
-
// Fast start, slow middle, quick finish — the way real loads feel.
|
|
33
|
-
const eased = t < 0.85 ? 0.92 * (1 - Math.pow(1 - t / 0.85, 2.2)) : 0.92 + 0.08 * ((t - 0.85) / 0.15);
|
|
34
|
-
setAuto(Math.round(eased * 100));
|
|
35
|
-
if (t >= 1) {
|
|
36
|
-
if (!fired) {
|
|
37
|
-
fired = true;
|
|
38
|
-
doneRef.current?.();
|
|
39
|
-
}
|
|
40
|
-
if (loop) {
|
|
41
|
-
start = now + 900;
|
|
42
|
-
fired = false;
|
|
43
|
-
} else return;
|
|
44
|
-
}
|
|
45
|
-
raf = requestAnimationFrame(tick);
|
|
46
|
-
};
|
|
47
|
-
raf = requestAnimationFrame(tick);
|
|
48
|
-
return () => cancelAnimationFrame(raf);
|
|
49
|
-
}, [controlled, duration, loop]);
|
|
50
|
-
|
|
29
|
+
const complete = value >= 100 && (controlled || page.done);
|
|
51
30
|
useEffect(() => {
|
|
52
|
-
if (
|
|
53
|
-
}, [
|
|
54
|
-
|
|
55
|
-
return Math.round(Math.max(0, Math.min(100, controlled ? progress : auto)));
|
|
31
|
+
if (complete) doneRef.current?.();
|
|
32
|
+
}, [complete]);
|
|
33
|
+
return { value, complete };
|
|
56
34
|
}
|
|
57
35
|
|
|
58
36
|
function Bar({ value, color, track }) {
|
|
@@ -108,8 +86,8 @@ function Orbit({ value, color, moving }) {
|
|
|
108
86
|
);
|
|
109
87
|
}
|
|
110
88
|
|
|
111
|
-
function ProgressView({ value, color, track, moving }) {
|
|
112
|
-
const message = messageFor(value)
|
|
89
|
+
function ProgressView({ value, color, track, moving, statusText }) {
|
|
90
|
+
const message = statusText ?? `${messageFor(value)}…`;
|
|
113
91
|
return (
|
|
114
92
|
<div className="w-[min(22rem,80vw)]">
|
|
115
93
|
<p className="font-mono text-[clamp(3.5rem,12vw,5.5rem)] font-bold leading-none tabular-nums tracking-tight">
|
|
@@ -122,7 +100,7 @@ function ProgressView({ value, color, track, moving }) {
|
|
|
122
100
|
<div className="mt-3 h-5 overflow-hidden text-sm opacity-70">
|
|
123
101
|
<AnimatePresence mode="wait" initial={false}>
|
|
124
102
|
<motion.p key={message} initial={{ y: 14, opacity: 0 }} animate={{ y: 0, opacity: 1 }} exit={{ y: -14, opacity: 0 }} transition={{ duration: moving ? 0.25 : 0 }}>
|
|
125
|
-
{message}
|
|
103
|
+
{message}
|
|
126
104
|
</motion.p>
|
|
127
105
|
</AnimatePresence>
|
|
128
106
|
</div>
|
|
@@ -760,30 +738,39 @@ function RacerGame({ value, color, track, dark, statusText }) {
|
|
|
760
738
|
const LoadingScreen = ({
|
|
761
739
|
variant = "orbit",
|
|
762
740
|
progress,
|
|
763
|
-
duration = 6000,
|
|
764
|
-
loop = true,
|
|
765
741
|
accentColor = "#a3e635",
|
|
766
742
|
theme = "dark",
|
|
767
743
|
title = "Loading",
|
|
768
744
|
fullScreen = false,
|
|
745
|
+
autoHide = true,
|
|
769
746
|
onComplete,
|
|
770
747
|
statusText,
|
|
771
748
|
className = "",
|
|
772
749
|
}) => {
|
|
773
750
|
const moving = !useReducedMotion();
|
|
774
|
-
const value = useProgress(progress,
|
|
751
|
+
const { value, complete } = useProgress(progress, onComplete);
|
|
752
|
+
const [gone, setGone] = useState(false);
|
|
775
753
|
const dark = theme !== "light";
|
|
776
754
|
const track = dark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.08)";
|
|
777
|
-
|
|
755
|
+
// Page-style status ("Loading page…", "Almost ready…"), or your own text.
|
|
756
|
+
const status = statusText ?? (complete ? "Page ready" : undefined);
|
|
757
|
+
const shared = { value, color: accentColor, track, moving, dark, statusText: status };
|
|
778
758
|
const game = variant === "catch" || variant === "runner" || variant === "racer";
|
|
759
|
+
const hiding = autoHide && complete;
|
|
779
760
|
|
|
761
|
+
if (gone) return null;
|
|
780
762
|
return (
|
|
781
|
-
<div
|
|
763
|
+
<motion.div
|
|
782
764
|
data-loading-screen=""
|
|
783
765
|
role="status"
|
|
784
766
|
aria-live="polite"
|
|
767
|
+
aria-busy={!complete}
|
|
785
768
|
aria-label={`${title} ${value}%`}
|
|
786
|
-
|
|
769
|
+
initial={false}
|
|
770
|
+
animate={{ opacity: hiding ? 0 : 1 }}
|
|
771
|
+
transition={{ duration: 0.45, delay: hiding ? 0.35 : 0 }}
|
|
772
|
+
onAnimationComplete={() => hiding && setGone(true)}
|
|
773
|
+
className={`flex flex-col items-center justify-center gap-6 p-6 sm:p-8 ${fullScreen ? "fixed inset-0 z-[100]" : "relative h-full w-full"} ${hiding ? "pointer-events-none" : ""} ${dark ? "bg-neutral-950 text-white" : "bg-white text-neutral-900"} ${className}`}
|
|
787
774
|
style={dark ? { backgroundImage: `radial-gradient(60% 50% at 50% 40%, ${accentColor}14, transparent 70%)` } : undefined}
|
|
788
775
|
>
|
|
789
776
|
{game && (
|
|
@@ -798,8 +785,13 @@ const LoadingScreen = ({
|
|
|
798
785
|
{variant === "catch" && <CatchGame {...shared} />}
|
|
799
786
|
{variant === "runner" && <RunnerGame {...shared} />}
|
|
800
787
|
{variant === "racer" && <RacerGame {...shared} />}
|
|
801
|
-
{!game && variant !== "progress" &&
|
|
802
|
-
|
|
788
|
+
{!game && variant !== "progress" && (
|
|
789
|
+
<p className="text-center">
|
|
790
|
+
<span className="block text-[0.7rem] font-semibold uppercase tracking-[0.3em] opacity-50">{title}</span>
|
|
791
|
+
<span className="mt-1.5 block text-[0.75rem] opacity-60">{status ?? `${messageFor(value)}…`}</span>
|
|
792
|
+
</p>
|
|
793
|
+
)}
|
|
794
|
+
</motion.div>
|
|
803
795
|
);
|
|
804
796
|
};
|
|
805
797
|
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Real page-load progress, used by <LoadingScreen /> when you don't pass
|
|
5
|
+
* your own `progress`. It counts what the page is actually loading:
|
|
6
|
+
*
|
|
7
|
+
* - every <img> still loading (including ones added later),
|
|
8
|
+
* - scripts, stylesheets, fonts and fetches as the browser finishes them,
|
|
9
|
+
* - web fonts (document.fonts.ready) and the window "load" event.
|
|
10
|
+
*
|
|
11
|
+
* `progress` is finished / total (0 to 100) and only reaches 100 once the
|
|
12
|
+
* page has fully loaded and nothing new has started for a moment, so the
|
|
13
|
+
* bar and the count always match what really happened.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const SETTLE_MS = 350;
|
|
17
|
+
|
|
18
|
+
export function usePageProgress(enabled = true) {
|
|
19
|
+
const [state, setState] = useState({ loaded: 0, total: 0, done: false });
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!enabled || typeof window === "undefined") return undefined;
|
|
23
|
+
let loaded = 0;
|
|
24
|
+
let total = 0;
|
|
25
|
+
let waitingForPage = document.readyState !== "complete";
|
|
26
|
+
let settle = 0;
|
|
27
|
+
let finished = false;
|
|
28
|
+
const seen = new WeakSet();
|
|
29
|
+
const cleanups = [];
|
|
30
|
+
|
|
31
|
+
const update = () => {
|
|
32
|
+
if (finished) return;
|
|
33
|
+
const idle = !waitingForPage && loaded >= total;
|
|
34
|
+
setState({ loaded, total, done: false });
|
|
35
|
+
clearTimeout(settle);
|
|
36
|
+
if (idle) {
|
|
37
|
+
// Wait a moment: a page often starts a few more requests right after load.
|
|
38
|
+
settle = setTimeout(() => {
|
|
39
|
+
if (!waitingForPage && loaded >= total) {
|
|
40
|
+
finished = true;
|
|
41
|
+
setState({ loaded, total: Math.max(total, loaded), done: true });
|
|
42
|
+
}
|
|
43
|
+
}, SETTLE_MS);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const add = (n = 1) => {
|
|
47
|
+
total += n;
|
|
48
|
+
update();
|
|
49
|
+
};
|
|
50
|
+
const finish = () => {
|
|
51
|
+
loaded += 1;
|
|
52
|
+
update();
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Only what is still loading (or starts) after the loader appears is counted,
|
|
56
|
+
// so the bar runs from 0 to 100 for this load.
|
|
57
|
+
// Images: pending ones count until they load or fail.
|
|
58
|
+
const trackImage = (img) => {
|
|
59
|
+
if (seen.has(img)) return;
|
|
60
|
+
seen.add(img);
|
|
61
|
+
if (img.complete) return;
|
|
62
|
+
add();
|
|
63
|
+
const end = () => {
|
|
64
|
+
img.removeEventListener("load", end);
|
|
65
|
+
img.removeEventListener("error", end);
|
|
66
|
+
finish();
|
|
67
|
+
};
|
|
68
|
+
img.addEventListener("load", end);
|
|
69
|
+
img.addEventListener("error", end);
|
|
70
|
+
cleanups.push(() => {
|
|
71
|
+
img.removeEventListener("load", end);
|
|
72
|
+
img.removeEventListener("error", end);
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
Array.from(document.images).forEach(trackImage);
|
|
76
|
+
const mo = new MutationObserver((list) => {
|
|
77
|
+
for (const m of list) {
|
|
78
|
+
m.addedNodes.forEach((n) => {
|
|
79
|
+
if (n.nodeType !== 1) return;
|
|
80
|
+
if (n.tagName === "IMG") trackImage(n);
|
|
81
|
+
else n.querySelectorAll?.("img").forEach(trackImage);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
mo.observe(document.documentElement, { childList: true, subtree: true });
|
|
86
|
+
|
|
87
|
+
// Other files (scripts, CSS, fonts, fetch) are counted as the browser finishes them.
|
|
88
|
+
let po;
|
|
89
|
+
if (typeof PerformanceObserver !== "undefined") {
|
|
90
|
+
po = new PerformanceObserver((list) => {
|
|
91
|
+
const fresh = list.getEntries().filter((e) => e.initiatorType !== "img");
|
|
92
|
+
if (!fresh.length) return;
|
|
93
|
+
total += fresh.length;
|
|
94
|
+
loaded += fresh.length;
|
|
95
|
+
update();
|
|
96
|
+
});
|
|
97
|
+
try {
|
|
98
|
+
po.observe({ type: "resource", buffered: false });
|
|
99
|
+
} catch {
|
|
100
|
+
po = undefined;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// The page itself.
|
|
105
|
+
if (waitingForPage) {
|
|
106
|
+
total += 1;
|
|
107
|
+
const onLoad = () => {
|
|
108
|
+
waitingForPage = false;
|
|
109
|
+
finish();
|
|
110
|
+
};
|
|
111
|
+
window.addEventListener("load", onLoad, { once: true });
|
|
112
|
+
cleanups.push(() => window.removeEventListener("load", onLoad));
|
|
113
|
+
}
|
|
114
|
+
// Web fonts.
|
|
115
|
+
if (document.fonts && document.fonts.status !== "loaded") {
|
|
116
|
+
total += 1;
|
|
117
|
+
document.fonts.ready.then(finish, finish);
|
|
118
|
+
}
|
|
119
|
+
update();
|
|
120
|
+
|
|
121
|
+
return () => {
|
|
122
|
+
finished = true;
|
|
123
|
+
clearTimeout(settle);
|
|
124
|
+
mo.disconnect();
|
|
125
|
+
po?.disconnect();
|
|
126
|
+
cleanups.forEach((fn) => fn());
|
|
127
|
+
};
|
|
128
|
+
}, [enabled]);
|
|
129
|
+
|
|
130
|
+
const total = Math.max(1, state.total);
|
|
131
|
+
const progress = state.done ? 100 : Math.min(99, Math.round((state.loaded / total) * 100));
|
|
132
|
+
return { progress, loaded: state.loaded, total: state.total, done: state.done };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export default usePageProgress;
|
|
@@ -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
|
@@ -117,6 +117,9 @@ const componentMap = {
|
|
|
117
117
|
"liquid-blob-3d": "LiquidBlob3D",
|
|
118
118
|
"particle-morph": "ParticleMorph",
|
|
119
119
|
"retro-grid-3d": "RetroGrid3D",
|
|
120
|
+
"distortion-image": "DistortionImage",
|
|
121
|
+
"galaxy-3d": "Galaxy3D",
|
|
122
|
+
"warp-tunnel": "WarpTunnel",
|
|
120
123
|
"water-drop-reveal": "MaskView",
|
|
121
124
|
};
|
|
122
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.6.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",
|