apex-ui-kit 2.1.3 → 2.2.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/dist/ApexUI-Kit/templates/AnimatedButton/AnimatedButton.jsx +251 -0
- package/dist/ApexUI-Kit/templates/AnimatedTabs/AnimatedTabs.jsx +99 -0
- package/dist/ApexUI-Kit/templates/BlurRevealText/BlurRevealText.jsx +60 -0
- package/dist/ApexUI-Kit/templates/Carousel3D/Carousel3D.jsx +222 -0
- package/dist/ApexUI-Kit/templates/DotGlobe/DotGlobe.jsx +289 -0
- package/dist/ApexUI-Kit/templates/FaqAccordion/FaqAccordion.jsx +88 -0
- package/dist/ApexUI-Kit/templates/FlipClock/FlipClock.jsx +121 -0
- package/dist/ApexUI-Kit/templates/LiquidGradient/LiquidGradient.jsx +238 -0
- package/dist/ApexUI-Kit/templates/LoadingScreen/LoadingScreen.jsx +361 -0
- package/dist/ApexUI-Kit/templates/MagneticCursor/MagneticCursor.jsx +306 -0
- package/dist/ApexUI-Kit/templates/ProfileCard/ProfileCard.jsx +213 -0
- package/dist/ApexUI-Kit/templates/SidebarNav/SidebarNav.jsx +220 -0
- package/dist/ApexUI-Kit/templates/SpotlightGrid/SpotlightGrid.jsx +110 -0
- package/dist/ApexUI-Kit/templates/ToastStack/ToastStack.jsx +108 -0
- package/dist/ApexUI-Kit/templates/ToolTip/ToolTip.jsx +5 -3
- package/dist/ApexUI-Kit/templates/WebGLCarousel/WebGLCarousel.jsx +326 -0
- package/dist/ApexUI-Kit/templates/WorldMap/WorldMap.jsx +359 -0
- package/dist/cli/index.js +20 -0
- package/package.json +1 -1
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from "react";
|
|
2
|
+
import { Mesh, Program, Renderer, Triangle } from "ogl";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Liquid Gradient: a slow, flowing four-color mesh gradient drawn by a single
|
|
6
|
+
* OGL fragment shader on one full-screen triangle. The colors lean toward the
|
|
7
|
+
* pointer, a light film grain keeps it from banding, and it pauses off screen.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const hexToVec3 = (hex) => {
|
|
11
|
+
let h = String(hex).replace("#", "");
|
|
12
|
+
if (h.length === 3) h = h.split("").map((c) => c + c).join("");
|
|
13
|
+
const n = parseInt(h, 16) || 0;
|
|
14
|
+
return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const vertex = /* glsl */ `
|
|
18
|
+
attribute vec2 uv;
|
|
19
|
+
attribute vec2 position;
|
|
20
|
+
varying vec2 vUv;
|
|
21
|
+
void main() {
|
|
22
|
+
vUv = uv;
|
|
23
|
+
gl_Position = vec4(position, 0.0, 1.0);
|
|
24
|
+
}
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
const fragment = /* glsl */ `
|
|
28
|
+
precision highp float;
|
|
29
|
+
uniform float uTime;
|
|
30
|
+
uniform vec2 uResolution;
|
|
31
|
+
uniform vec2 uMouse;
|
|
32
|
+
uniform float uMouseStrength;
|
|
33
|
+
uniform float uScale;
|
|
34
|
+
uniform float uGrain;
|
|
35
|
+
uniform vec3 uColor1;
|
|
36
|
+
uniform vec3 uColor2;
|
|
37
|
+
uniform vec3 uColor3;
|
|
38
|
+
uniform vec3 uColor4;
|
|
39
|
+
varying vec2 vUv;
|
|
40
|
+
|
|
41
|
+
vec2 hash(vec2 p) {
|
|
42
|
+
p = vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)));
|
|
43
|
+
return -1.0 + 2.0 * fract(sin(p) * 43758.5453);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
float noise(vec2 p) {
|
|
47
|
+
vec2 i = floor(p);
|
|
48
|
+
vec2 f = fract(p);
|
|
49
|
+
vec2 u = f * f * (3.0 - 2.0 * f);
|
|
50
|
+
return mix(
|
|
51
|
+
mix(dot(hash(i), f), dot(hash(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0)), u.x),
|
|
52
|
+
mix(dot(hash(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0)), dot(hash(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0)), u.x),
|
|
53
|
+
u.y
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
float fbm(vec2 p) {
|
|
58
|
+
float v = 0.0;
|
|
59
|
+
float a = 0.5;
|
|
60
|
+
for (int i = 0; i < 4; i++) {
|
|
61
|
+
v += a * noise(p);
|
|
62
|
+
p *= 2.0;
|
|
63
|
+
a *= 0.5;
|
|
64
|
+
}
|
|
65
|
+
return v;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
void main() {
|
|
69
|
+
float aspect = uResolution.x / uResolution.y;
|
|
70
|
+
vec2 p = (vUv - 0.5) * vec2(aspect, 1.0) * uScale;
|
|
71
|
+
float t = uTime * 0.12;
|
|
72
|
+
|
|
73
|
+
// Pull the field toward the pointer.
|
|
74
|
+
vec2 m = (uMouse - 0.5) * vec2(aspect, 1.0) * uScale;
|
|
75
|
+
float md = length(p - m);
|
|
76
|
+
p += (m - p) * uMouseStrength * 0.35 * exp(-md * md * 1.5);
|
|
77
|
+
|
|
78
|
+
// Domain-warped noise: noise feeding noise gives the liquid folds.
|
|
79
|
+
vec2 q = vec2(fbm(p + t), fbm(p + vec2(5.2, 1.3) - t));
|
|
80
|
+
vec2 r = vec2(fbm(p + 3.0 * q + vec2(1.7, 9.2) + t * 0.6), fbm(p + 3.0 * q + vec2(8.3, 2.8) - t * 0.4));
|
|
81
|
+
float n = fbm(p + 2.5 * r);
|
|
82
|
+
|
|
83
|
+
vec3 col = mix(uColor1, uColor2, smoothstep(-0.4, 0.4, q.x));
|
|
84
|
+
col = mix(col, uColor3, smoothstep(-0.3, 0.5, r.y));
|
|
85
|
+
col = mix(col, uColor4, smoothstep(0.1, 0.7, n));
|
|
86
|
+
|
|
87
|
+
col *= 1.0 - 0.25 * length(vUv - 0.5);
|
|
88
|
+
float g = fract(sin(dot(gl_FragCoord.xy + fract(uTime) * 100.0, vec2(12.9898, 78.233))) * 43758.5453);
|
|
89
|
+
col += (g - 0.5) * uGrain;
|
|
90
|
+
|
|
91
|
+
gl_FragColor = vec4(col, 1.0);
|
|
92
|
+
}
|
|
93
|
+
`;
|
|
94
|
+
|
|
95
|
+
const LiquidGradient = ({
|
|
96
|
+
children,
|
|
97
|
+
className = "",
|
|
98
|
+
containerClassName = "",
|
|
99
|
+
colors = ["#1e1b4b", "#4f46e5", "#db2777", "#f97316"],
|
|
100
|
+
speed = 1,
|
|
101
|
+
scale = 1.6,
|
|
102
|
+
grain = 0.06,
|
|
103
|
+
mouseInteractive = true,
|
|
104
|
+
quality = 1,
|
|
105
|
+
}) => {
|
|
106
|
+
const containerRef = useRef(null);
|
|
107
|
+
const programRef = useRef(null);
|
|
108
|
+
const renderRef = useRef(() => {});
|
|
109
|
+
const speedRef = useRef(speed);
|
|
110
|
+
speedRef.current = speed;
|
|
111
|
+
const pointer = useRef({ x: 0.5, y: 0.5, tx: 0.5, ty: 0.5, strength: 0, target: 0 });
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
const container = containerRef.current;
|
|
115
|
+
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
116
|
+
const renderer = new Renderer({ dpr: Math.min(window.devicePixelRatio || 1, 1.5) * quality, antialias: false, alpha: false });
|
|
117
|
+
const gl = renderer.gl;
|
|
118
|
+
gl.canvas.style.cssText = "position:absolute;inset:0;width:100%;height:100%;display:block;";
|
|
119
|
+
container.prepend(gl.canvas);
|
|
120
|
+
|
|
121
|
+
const program = new Program(gl, {
|
|
122
|
+
vertex,
|
|
123
|
+
fragment,
|
|
124
|
+
uniforms: {
|
|
125
|
+
uTime: { value: 0 },
|
|
126
|
+
uResolution: { value: [1, 1] },
|
|
127
|
+
uMouse: { value: [0.5, 0.5] },
|
|
128
|
+
uMouseStrength: { value: 0 },
|
|
129
|
+
uScale: { value: scale },
|
|
130
|
+
uGrain: { value: grain },
|
|
131
|
+
uColor1: { value: hexToVec3(colors[0]) },
|
|
132
|
+
uColor2: { value: hexToVec3(colors[1] ?? colors[0]) },
|
|
133
|
+
uColor3: { value: hexToVec3(colors[2] ?? colors[0]) },
|
|
134
|
+
uColor4: { value: hexToVec3(colors[3] ?? colors[0]) },
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
programRef.current = program;
|
|
138
|
+
const mesh = new Mesh(gl, { geometry: new Triangle(gl), program });
|
|
139
|
+
|
|
140
|
+
let raf = 0;
|
|
141
|
+
let last = performance.now();
|
|
142
|
+
let time = 7; // start mid-flow so the first frame already looks liquid
|
|
143
|
+
let visible = true;
|
|
144
|
+
|
|
145
|
+
const render = () => renderer.render({ scene: mesh });
|
|
146
|
+
renderRef.current = render;
|
|
147
|
+
const resize = () => {
|
|
148
|
+
const { width, height } = container.getBoundingClientRect();
|
|
149
|
+
renderer.setSize(Math.max(1, width), Math.max(1, height));
|
|
150
|
+
program.uniforms.uResolution.value = [Math.max(1, width), Math.max(1, height)];
|
|
151
|
+
render();
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const loop = (now) => {
|
|
155
|
+
const dt = Math.min(0.05, (now - last) / 1000);
|
|
156
|
+
last = now;
|
|
157
|
+
time += dt * speedRef.current;
|
|
158
|
+
const p = pointer.current;
|
|
159
|
+
p.x += (p.tx - p.x) * 0.06;
|
|
160
|
+
p.y += (p.ty - p.y) * 0.06;
|
|
161
|
+
p.strength += (p.target - p.strength) * 0.05;
|
|
162
|
+
program.uniforms.uTime.value = time;
|
|
163
|
+
program.uniforms.uMouse.value = [p.x, p.y];
|
|
164
|
+
program.uniforms.uMouseStrength.value = p.strength;
|
|
165
|
+
render();
|
|
166
|
+
raf = requestAnimationFrame(loop);
|
|
167
|
+
};
|
|
168
|
+
const start = () => {
|
|
169
|
+
if (reduced || raf || !visible) return;
|
|
170
|
+
last = performance.now();
|
|
171
|
+
raf = requestAnimationFrame(loop);
|
|
172
|
+
};
|
|
173
|
+
const stop = () => {
|
|
174
|
+
cancelAnimationFrame(raf);
|
|
175
|
+
raf = 0;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const ro = new ResizeObserver(resize);
|
|
179
|
+
ro.observe(container);
|
|
180
|
+
const io = new IntersectionObserver(([entry]) => {
|
|
181
|
+
visible = entry.isIntersecting;
|
|
182
|
+
if (visible) start();
|
|
183
|
+
else stop();
|
|
184
|
+
});
|
|
185
|
+
io.observe(container);
|
|
186
|
+
program.uniforms.uTime.value = time;
|
|
187
|
+
resize();
|
|
188
|
+
start();
|
|
189
|
+
|
|
190
|
+
return () => {
|
|
191
|
+
stop();
|
|
192
|
+
ro.disconnect();
|
|
193
|
+
io.disconnect();
|
|
194
|
+
programRef.current = null;
|
|
195
|
+
renderRef.current = () => {};
|
|
196
|
+
gl.canvas.remove();
|
|
197
|
+
gl.getExtension("WEBGL_lose_context")?.loseContext();
|
|
198
|
+
};
|
|
199
|
+
// The renderer is rebuilt only when quality changes; other props update uniforms below.
|
|
200
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
201
|
+
}, [quality]);
|
|
202
|
+
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
const program = programRef.current;
|
|
205
|
+
if (!program) return;
|
|
206
|
+
program.uniforms.uScale.value = scale;
|
|
207
|
+
program.uniforms.uGrain.value = grain;
|
|
208
|
+
program.uniforms.uColor1.value = hexToVec3(colors[0]);
|
|
209
|
+
program.uniforms.uColor2.value = hexToVec3(colors[1] ?? colors[0]);
|
|
210
|
+
program.uniforms.uColor3.value = hexToVec3(colors[2] ?? colors[0]);
|
|
211
|
+
program.uniforms.uColor4.value = hexToVec3(colors[3] ?? colors[0]);
|
|
212
|
+
// Redraw now so a paused (reduced-motion) gradient shows the change too.
|
|
213
|
+
renderRef.current();
|
|
214
|
+
}, [colors, scale, grain]);
|
|
215
|
+
|
|
216
|
+
const onPointerMove = (e) => {
|
|
217
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
218
|
+
pointer.current.tx = (e.clientX - rect.left) / rect.width;
|
|
219
|
+
pointer.current.ty = 1 - (e.clientY - rect.top) / rect.height;
|
|
220
|
+
pointer.current.target = 1;
|
|
221
|
+
};
|
|
222
|
+
const onPointerLeave = () => {
|
|
223
|
+
pointer.current.target = 0;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<div
|
|
228
|
+
ref={containerRef}
|
|
229
|
+
onPointerMove={mouseInteractive ? onPointerMove : undefined}
|
|
230
|
+
onPointerLeave={onPointerLeave}
|
|
231
|
+
className={`relative h-screen w-full overflow-hidden bg-[#0b0b14] ${containerClassName}`}
|
|
232
|
+
>
|
|
233
|
+
{children && <div className={`relative z-10 h-full w-full ${className}`}>{children}</div>}
|
|
234
|
+
</div>
|
|
235
|
+
);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
export default LiquidGradient;
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Loading Screen: five ways to wait.
|
|
6
|
+
* orbit – dots orbit a breathing core around the percentage
|
|
7
|
+
* progress – a big counting percentage, a sliding bar and rotating status lines
|
|
8
|
+
* blocks – a grid of tiles fills in like a wave
|
|
9
|
+
* catch – a mini game: tap the orb before it fades while things load
|
|
10
|
+
* runner – a mini game: jump the blocks (Space / ↑ / tap) while things load
|
|
11
|
+
* Pass `progress` (0–100) yourself, or leave it out and it runs on its own.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const MESSAGES = ["Warming up the engines", "Fetching your components", "Polishing pixels", "Almost there"];
|
|
15
|
+
|
|
16
|
+
// Simulated progress that eases toward 100 over `duration` ms, then calls onComplete.
|
|
17
|
+
function useProgress(progress, duration, loop, onComplete) {
|
|
18
|
+
const [auto, setAuto] = useState(0);
|
|
19
|
+
const doneRef = useRef(onComplete);
|
|
20
|
+
doneRef.current = onComplete;
|
|
21
|
+
const controlled = typeof progress === "number";
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (controlled) return undefined;
|
|
25
|
+
let raf = 0;
|
|
26
|
+
let start = performance.now();
|
|
27
|
+
let fired = false;
|
|
28
|
+
const tick = (now) => {
|
|
29
|
+
const t = Math.min(1, (now - start) / duration);
|
|
30
|
+
// Fast start, slow middle, quick finish — the way real loads feel.
|
|
31
|
+
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);
|
|
32
|
+
setAuto(Math.round(eased * 100));
|
|
33
|
+
if (t >= 1) {
|
|
34
|
+
if (!fired) {
|
|
35
|
+
fired = true;
|
|
36
|
+
doneRef.current?.();
|
|
37
|
+
}
|
|
38
|
+
if (loop) {
|
|
39
|
+
start = now + 900;
|
|
40
|
+
fired = false;
|
|
41
|
+
} else return;
|
|
42
|
+
}
|
|
43
|
+
raf = requestAnimationFrame(tick);
|
|
44
|
+
};
|
|
45
|
+
raf = requestAnimationFrame(tick);
|
|
46
|
+
return () => cancelAnimationFrame(raf);
|
|
47
|
+
}, [controlled, duration, loop]);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (controlled && progress >= 100) doneRef.current?.();
|
|
51
|
+
}, [controlled, progress]);
|
|
52
|
+
|
|
53
|
+
return Math.max(0, Math.min(100, controlled ? progress : auto));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function Bar({ value, color, track }) {
|
|
57
|
+
return (
|
|
58
|
+
<div className="h-1.5 w-full overflow-hidden rounded-full" style={{ background: track }}>
|
|
59
|
+
<div className="h-full rounded-full transition-[width] duration-300 ease-out" style={{ width: `${value}%`, background: color, boxShadow: `0 0 12px ${color}` }} />
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function Orbit({ value, color, moving }) {
|
|
65
|
+
return (
|
|
66
|
+
<div className="flex flex-col items-center">
|
|
67
|
+
<div className="relative grid h-36 w-36 place-items-center">
|
|
68
|
+
{[0, 1, 2].map((ring) => (
|
|
69
|
+
<motion.div
|
|
70
|
+
key={ring}
|
|
71
|
+
className="absolute inset-0"
|
|
72
|
+
style={{ scale: 1 - ring * 0.22, rotate: ring * 40 }}
|
|
73
|
+
// Each ring starts at its own angle so the dots never line up.
|
|
74
|
+
animate={moving ? { rotate: ring * 40 + (ring % 2 ? -360 : 360) } : undefined}
|
|
75
|
+
transition={{ duration: 2.4 + ring * 1.1, repeat: Infinity, ease: "linear" }}
|
|
76
|
+
>
|
|
77
|
+
{[0, 1, 2].map((d) => (
|
|
78
|
+
<div key={d} className="absolute inset-0" style={{ transform: `rotate(${d * 120}deg)` }}>
|
|
79
|
+
<span
|
|
80
|
+
className="absolute left-1/2 top-0 -ml-[5px] -mt-[5px] h-2.5 w-2.5 rounded-full"
|
|
81
|
+
style={{ background: color, opacity: 1 - ring * 0.25, boxShadow: `0 0 10px ${color}` }}
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
))}
|
|
85
|
+
</motion.div>
|
|
86
|
+
))}
|
|
87
|
+
<motion.div
|
|
88
|
+
className="h-12 w-12 rounded-full"
|
|
89
|
+
style={{ background: `radial-gradient(circle at 35% 35%, #fff, ${color} 55%)`, boxShadow: `0 0 40px ${color}` }}
|
|
90
|
+
animate={moving ? { scale: [0.9, 1.08, 0.9] } : undefined}
|
|
91
|
+
transition={{ duration: 1.6, repeat: Infinity, ease: "easeInOut" }}
|
|
92
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
<p className="mt-5 font-mono text-2xl font-semibold tabular-nums">{value}%</p>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function ProgressView({ value, color, track, moving }) {
|
|
100
|
+
const message = MESSAGES[Math.min(MESSAGES.length - 1, Math.floor(value / (100 / MESSAGES.length)))];
|
|
101
|
+
return (
|
|
102
|
+
<div className="w-[min(22rem,80vw)]">
|
|
103
|
+
<p className="font-mono text-[clamp(3.5rem,12vw,5.5rem)] font-bold leading-none tabular-nums tracking-tight">
|
|
104
|
+
{value}
|
|
105
|
+
<span className="text-[0.4em] align-top opacity-50">%</span>
|
|
106
|
+
</p>
|
|
107
|
+
<div className="mt-5">
|
|
108
|
+
<Bar value={value} color={color} track={track} />
|
|
109
|
+
</div>
|
|
110
|
+
<div className="mt-3 h-5 overflow-hidden text-sm opacity-70">
|
|
111
|
+
<AnimatePresence mode="wait" initial={false}>
|
|
112
|
+
<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 }}>
|
|
113
|
+
{message}…
|
|
114
|
+
</motion.p>
|
|
115
|
+
</AnimatePresence>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function Blocks({ value, color, track, moving }) {
|
|
122
|
+
const N = 7;
|
|
123
|
+
const filled = Math.round((value / 100) * N * N);
|
|
124
|
+
return (
|
|
125
|
+
<div className="flex flex-col items-center">
|
|
126
|
+
<div className="grid gap-1.5" style={{ gridTemplateColumns: `repeat(${N}, 1fr)` }}>
|
|
127
|
+
{Array.from({ length: N * N }, (_, i) => {
|
|
128
|
+
const on = i < filled;
|
|
129
|
+
const r = Math.floor(i / N);
|
|
130
|
+
const c = i % N;
|
|
131
|
+
return (
|
|
132
|
+
<motion.span
|
|
133
|
+
key={i}
|
|
134
|
+
className="h-4 w-4 rounded-[5px] sm:h-5 sm:w-5"
|
|
135
|
+
style={{ background: on ? color : track }}
|
|
136
|
+
animate={moving ? { scale: on ? [1, 0.7, 1] : 1, opacity: on ? 1 : [0.5, 1, 0.5] } : undefined}
|
|
137
|
+
transition={{ duration: 1.6, repeat: Infinity, delay: (r + c) * 0.07, ease: "easeInOut" }}
|
|
138
|
+
/>
|
|
139
|
+
);
|
|
140
|
+
})}
|
|
141
|
+
</div>
|
|
142
|
+
<p className="mt-5 font-mono text-lg font-semibold tabular-nums">{value}%</p>
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function CatchGame({ value, color, track }) {
|
|
148
|
+
const [score, setScore] = useState(0);
|
|
149
|
+
const [orb, setOrb] = useState({ id: 0, x: 50, y: 50 });
|
|
150
|
+
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
const id = setTimeout(() => {
|
|
153
|
+
setScore(0);
|
|
154
|
+
setOrb((o) => ({ id: o.id + 1, x: 10 + Math.random() * 80, y: 15 + Math.random() * 70 }));
|
|
155
|
+
}, 1400);
|
|
156
|
+
return () => clearTimeout(id);
|
|
157
|
+
}, [orb.id]);
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<div className="w-[min(22rem,84vw)]">
|
|
161
|
+
<div className="mb-2 flex items-center justify-between font-mono text-[0.7rem] uppercase tracking-[0.16em] opacity-70">
|
|
162
|
+
<span>Catch the orb</span>
|
|
163
|
+
<span>Score {score}</span>
|
|
164
|
+
</div>
|
|
165
|
+
<div className="relative h-40 overflow-hidden rounded-2xl border" style={{ borderColor: track, backgroundImage: `radial-gradient(${track} 1px, transparent 1px)`, backgroundSize: "14px 14px" }}>
|
|
166
|
+
<AnimatePresence>
|
|
167
|
+
<motion.button
|
|
168
|
+
key={orb.id}
|
|
169
|
+
type="button"
|
|
170
|
+
aria-label="Catch the orb"
|
|
171
|
+
onPointerDown={() => {
|
|
172
|
+
setScore((s) => s + 1);
|
|
173
|
+
setOrb((o) => ({ id: o.id + 1, x: 10 + Math.random() * 80, y: 15 + Math.random() * 70 }));
|
|
174
|
+
}}
|
|
175
|
+
className="absolute h-9 w-9 rounded-full outline-none"
|
|
176
|
+
style={{ left: `${orb.x}%`, top: `${orb.y}%`, marginLeft: -18, marginTop: -18, background: color, boxShadow: `0 0 24px ${color}` }}
|
|
177
|
+
initial={{ scale: 0, opacity: 0 }}
|
|
178
|
+
animate={{ scale: [0, 1.15, 1, 0.55], opacity: [0, 1, 1, 0.7] }}
|
|
179
|
+
exit={{ scale: 1.8, opacity: 0, transition: { duration: 0.25 } }}
|
|
180
|
+
transition={{ duration: 1.4, times: [0, 0.15, 0.3, 1], ease: "easeOut" }}
|
|
181
|
+
/>
|
|
182
|
+
</AnimatePresence>
|
|
183
|
+
</div>
|
|
184
|
+
<div className="mt-4 flex items-center gap-3">
|
|
185
|
+
<Bar value={value} color={color} track={track} />
|
|
186
|
+
<span className="font-mono text-xs tabular-nums opacity-70">{value}%</span>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function RunnerGame({ value, color, track, ink }) {
|
|
193
|
+
const canvasRef = useRef(null);
|
|
194
|
+
const [score, setScore] = useState(0);
|
|
195
|
+
const [best, setBest] = useState(0);
|
|
196
|
+
const jumpRef = useRef(() => {});
|
|
197
|
+
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
const canvas = canvasRef.current;
|
|
200
|
+
const ctx = canvas.getContext("2d");
|
|
201
|
+
let raf = 0;
|
|
202
|
+
let last = performance.now();
|
|
203
|
+
let w = 0;
|
|
204
|
+
let h = 0;
|
|
205
|
+
const g = { y: 0, vy: 0, obstacles: [], speed: 220, spawn: 0.9, score: 0, flash: 0 };
|
|
206
|
+
|
|
207
|
+
const resize = () => {
|
|
208
|
+
const r = canvas.getBoundingClientRect();
|
|
209
|
+
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
210
|
+
w = r.width;
|
|
211
|
+
h = r.height;
|
|
212
|
+
canvas.width = Math.round(w * dpr);
|
|
213
|
+
canvas.height = Math.round(h * dpr);
|
|
214
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
215
|
+
};
|
|
216
|
+
const ro = new ResizeObserver(resize);
|
|
217
|
+
ro.observe(canvas);
|
|
218
|
+
resize();
|
|
219
|
+
|
|
220
|
+
jumpRef.current = () => {
|
|
221
|
+
if (g.y === 0) g.vy = 520;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const tick = (now) => {
|
|
225
|
+
const dt = Math.min(0.033, (now - last) / 1000);
|
|
226
|
+
last = now;
|
|
227
|
+
const ground = h - 18;
|
|
228
|
+
const px = 42;
|
|
229
|
+
const size = 18;
|
|
230
|
+
|
|
231
|
+
// Physics.
|
|
232
|
+
g.vy -= 1500 * dt;
|
|
233
|
+
g.y = Math.max(0, g.y + g.vy * dt);
|
|
234
|
+
if (g.y === 0) g.vy = Math.max(0, g.vy);
|
|
235
|
+
g.speed = Math.min(420, g.speed + 6 * dt);
|
|
236
|
+
g.spawn -= dt;
|
|
237
|
+
if (g.spawn <= 0) {
|
|
238
|
+
const tall = Math.random() < 0.35;
|
|
239
|
+
g.obstacles.push({ x: w + 10, w: tall ? 14 : 18, h: tall ? 30 : 18 });
|
|
240
|
+
g.spawn = 0.75 + Math.random() * 0.9;
|
|
241
|
+
}
|
|
242
|
+
g.obstacles.forEach((o) => (o.x -= g.speed * dt));
|
|
243
|
+
g.obstacles = g.obstacles.filter((o) => o.x + o.w > -10);
|
|
244
|
+
|
|
245
|
+
// Collision resets the run.
|
|
246
|
+
const top = ground - g.y - size;
|
|
247
|
+
const hit = g.obstacles.some((o) => px + size > o.x + 2 && px < o.x + o.w - 2 && top + size > ground - o.h + 2);
|
|
248
|
+
if (hit) {
|
|
249
|
+
g.obstacles = [];
|
|
250
|
+
g.speed = 220;
|
|
251
|
+
g.flash = 0.25;
|
|
252
|
+
g.score = 0;
|
|
253
|
+
}
|
|
254
|
+
g.score += dt * 10;
|
|
255
|
+
g.flash = Math.max(0, g.flash - dt);
|
|
256
|
+
const s = Math.floor(g.score);
|
|
257
|
+
setScore((prev) => (prev === s ? prev : s));
|
|
258
|
+
setBest((b) => Math.max(b, s));
|
|
259
|
+
|
|
260
|
+
// Draw.
|
|
261
|
+
ctx.clearRect(0, 0, w, h);
|
|
262
|
+
ctx.fillStyle = track;
|
|
263
|
+
ctx.fillRect(0, ground, w, 2);
|
|
264
|
+
for (let x = (now / 12) % 24; x < w; x += 24) ctx.fillRect(w - x, ground + 8, 6, 2);
|
|
265
|
+
ctx.fillStyle = g.flash ? "#ef4444" : color;
|
|
266
|
+
ctx.shadowColor = color;
|
|
267
|
+
ctx.shadowBlur = 12;
|
|
268
|
+
ctx.beginPath();
|
|
269
|
+
ctx.roundRect(px, top, size, size, 5);
|
|
270
|
+
ctx.fill();
|
|
271
|
+
ctx.shadowBlur = 0;
|
|
272
|
+
ctx.fillStyle = ink;
|
|
273
|
+
ctx.globalAlpha = 0.85;
|
|
274
|
+
g.obstacles.forEach((o) => {
|
|
275
|
+
ctx.beginPath();
|
|
276
|
+
ctx.roundRect(o.x, ground - o.h, o.w, o.h, 3);
|
|
277
|
+
ctx.fill();
|
|
278
|
+
});
|
|
279
|
+
ctx.globalAlpha = 1;
|
|
280
|
+
raf = requestAnimationFrame(tick);
|
|
281
|
+
};
|
|
282
|
+
raf = requestAnimationFrame(tick);
|
|
283
|
+
|
|
284
|
+
const onKey = (e) => {
|
|
285
|
+
if (e.code === "Space" || e.code === "ArrowUp") {
|
|
286
|
+
if (canvas.closest("[data-loading-screen]")?.matches(":hover") || document.activeElement === canvas) {
|
|
287
|
+
e.preventDefault();
|
|
288
|
+
jumpRef.current();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
window.addEventListener("keydown", onKey);
|
|
293
|
+
return () => {
|
|
294
|
+
cancelAnimationFrame(raf);
|
|
295
|
+
ro.disconnect();
|
|
296
|
+
window.removeEventListener("keydown", onKey);
|
|
297
|
+
};
|
|
298
|
+
}, [color, track, ink]);
|
|
299
|
+
|
|
300
|
+
return (
|
|
301
|
+
<div className="w-[min(26rem,86vw)]">
|
|
302
|
+
<div className="mb-2 flex items-center justify-between font-mono text-[0.7rem] uppercase tracking-[0.16em] opacity-70">
|
|
303
|
+
<span>Space / tap to jump</span>
|
|
304
|
+
<span>
|
|
305
|
+
{score} · best {best}
|
|
306
|
+
</span>
|
|
307
|
+
</div>
|
|
308
|
+
<canvas
|
|
309
|
+
ref={canvasRef}
|
|
310
|
+
tabIndex={0}
|
|
311
|
+
aria-label="Runner game: press Space or tap to jump"
|
|
312
|
+
onPointerDown={() => jumpRef.current()}
|
|
313
|
+
className="h-32 w-full cursor-pointer touch-manipulation rounded-2xl border outline-none focus-visible:ring-2"
|
|
314
|
+
style={{ borderColor: track }}
|
|
315
|
+
/>
|
|
316
|
+
<div className="mt-4 flex items-center gap-3">
|
|
317
|
+
<Bar value={value} color={color} track={track} />
|
|
318
|
+
<span className="font-mono text-xs tabular-nums opacity-70">{value}%</span>
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const LoadingScreen = ({
|
|
325
|
+
variant = "orbit",
|
|
326
|
+
progress,
|
|
327
|
+
duration = 6000,
|
|
328
|
+
loop = true,
|
|
329
|
+
accentColor = "#a3e635",
|
|
330
|
+
theme = "dark",
|
|
331
|
+
title = "Loading",
|
|
332
|
+
fullScreen = false,
|
|
333
|
+
onComplete,
|
|
334
|
+
className = "",
|
|
335
|
+
}) => {
|
|
336
|
+
const moving = !useReducedMotion();
|
|
337
|
+
const value = useProgress(progress, duration, loop, onComplete);
|
|
338
|
+
const dark = theme !== "light";
|
|
339
|
+
const track = dark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.08)";
|
|
340
|
+
const ink = dark ? "#e5e5e5" : "#262626";
|
|
341
|
+
const shared = { value, color: accentColor, track, ink, moving };
|
|
342
|
+
|
|
343
|
+
return (
|
|
344
|
+
<div
|
|
345
|
+
data-loading-screen=""
|
|
346
|
+
role="status"
|
|
347
|
+
aria-live="polite"
|
|
348
|
+
aria-label={`${title} ${value}%`}
|
|
349
|
+
className={`flex flex-col items-center justify-center gap-6 p-8 ${fullScreen ? "fixed inset-0 z-[100]" : "relative h-full w-full"} ${dark ? "bg-neutral-950 text-white" : "bg-white text-neutral-900"} ${className}`}
|
|
350
|
+
>
|
|
351
|
+
{variant === "orbit" && <Orbit {...shared} />}
|
|
352
|
+
{variant === "progress" && <ProgressView {...shared} />}
|
|
353
|
+
{variant === "blocks" && <Blocks {...shared} />}
|
|
354
|
+
{variant === "catch" && <CatchGame {...shared} />}
|
|
355
|
+
{variant === "runner" && <RunnerGame {...shared} />}
|
|
356
|
+
{variant !== "progress" && <p className="text-[0.7rem] font-semibold uppercase tracking-[0.3em] opacity-50">{title}</p>}
|
|
357
|
+
</div>
|
|
358
|
+
);
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
export default LoadingScreen;
|