apex-ui-kit 2.6.0 → 2.8.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 +32 -10
- package/dist/ApexUI-Kit/templates/AccordionMarquee/AccordionMarquee.jsx +340 -161
- package/dist/ApexUI-Kit/templates/AnimatedTabs/AnimatedTabs.jsx +1 -1
- package/dist/ApexUI-Kit/templates/ArcGallery/ArcGallery.jsx +356 -0
- package/dist/ApexUI-Kit/templates/BounceCards/BounceCards.jsx +22 -2
- package/dist/ApexUI-Kit/templates/CircularCarousel/CircularCarousel.jsx +381 -0
- package/dist/ApexUI-Kit/templates/CommandPalette/CommandPalette.jsx +250 -0
- package/dist/ApexUI-Kit/templates/CrossedMarquee/CrossedMarquee.jsx +158 -0
- package/dist/ApexUI-Kit/templates/DitherWaves/DitherWaves.jsx +207 -0
- package/dist/ApexUI-Kit/templates/EdgeBlur/EdgeBlur.jsx +92 -0
- package/dist/ApexUI-Kit/templates/FlipClock/FlipClock.jsx +5 -1
- package/dist/ApexUI-Kit/templates/GlassLens/GlassLens.jsx +274 -0
- package/dist/ApexUI-Kit/templates/GooeyNav/GooeyNav.jsx +2 -2
- package/dist/ApexUI-Kit/templates/HorizonGlow/HorizonGlow.jsx +259 -0
- package/dist/ApexUI-Kit/templates/ImageAccordion/ImageAccordion.jsx +116 -0
- package/dist/ApexUI-Kit/templates/InfiniteCanvas/InfiniteCanvas.jsx +230 -0
- package/dist/ApexUI-Kit/templates/MagneticButton/MagneticButton.jsx +69 -0
- package/dist/ApexUI-Kit/templates/Marquee3D/Marquee3D.jsx +68 -0
- package/dist/ApexUI-Kit/templates/NavDropDown/NavDropDown.jsx +123 -1
- package/dist/ApexUI-Kit/templates/OrbitIcons/OrbitIcons.jsx +169 -0
- package/dist/ApexUI-Kit/templates/ParticleText/ParticleText.jsx +328 -0
- package/dist/ApexUI-Kit/templates/RadialMenu/RadialMenu.jsx +119 -0
- package/dist/ApexUI-Kit/templates/ScrollStack/ScrollStack.jsx +88 -0
- package/dist/ApexUI-Kit/templates/SidebarNav/SidebarNav.jsx +22 -1
- package/dist/ApexUI-Kit/templates/SunriseBackground/SunriseBackground.jsx +290 -0
- package/dist/ApexUI-Kit/templates/TextRing3D/TextRing3D.jsx +18 -3
- package/dist/ApexUI-Kit/templates/ThemeSwitcher/ThemeSwitcher.jsx +215 -0
- package/dist/ApexUI-Kit/templates/ThemeSwitcher/themes.js +38 -0
- package/dist/ApexUI-Kit/templates/ToastStack/ToastStack.jsx +21 -13
- package/dist/ApexUI-Kit/templates/WireframeKnot3D/WireframeKnot3D.jsx +112 -0
- package/dist/cli/index.js +20 -0
- package/package.json +2 -2
- package/dist/ApexUI-Kit/templates/AccordionMarquee/AccordionMarquee.css +0 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import React, { Children, useLayoutEffect, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Scroll Stack: cards pin one after another as you scroll and pile up. A
|
|
5
|
+
* card that gets covered shrinks a little, darkens and (optionally) blurs or
|
|
6
|
+
* leans back, so the pile reads as depth. It rides on native scrolling
|
|
7
|
+
* (position: sticky + one rAF update per scroll), so it stays smooth with
|
|
8
|
+
* mouse, trackpad, touch and keyboard. Scrolls inside its own box, or with
|
|
9
|
+
* the page (useWindowScroll).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const ScrollStackItem = ({ children, className = "", style }) => (
|
|
13
|
+
<div className={`relative h-[18rem] w-full origin-top overflow-hidden rounded-[1.75rem] p-8 shadow-[0_24px_48px_-24px_rgba(0,0,0,0.45)] will-change-transform ${className}`} style={style}>
|
|
14
|
+
{children}
|
|
15
|
+
<span data-stack-shade="" aria-hidden="true" className="pointer-events-none absolute inset-0 rounded-[inherit] bg-black opacity-0" />
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const ScrollStack = ({ children, gap = 90, offset = 22, top = 64, scaleStep = 0.05, minScale = 0.82, dim = 0.22, blur = 0, tilt = 0, endSpace = 180, useWindowScroll = false, height = "100%", className = "" }) => {
|
|
20
|
+
const root = useRef(null);
|
|
21
|
+
const slots = useRef([]);
|
|
22
|
+
const items = Children.toArray(children);
|
|
23
|
+
|
|
24
|
+
useLayoutEffect(() => {
|
|
25
|
+
const box = root.current;
|
|
26
|
+
const scroller = useWindowScroll ? window : box;
|
|
27
|
+
let raf = 0;
|
|
28
|
+
const update = () => {
|
|
29
|
+
raf = 0;
|
|
30
|
+
const els = slots.current.filter(Boolean);
|
|
31
|
+
const viewTop = useWindowScroll ? 0 : box.getBoundingClientRect().top;
|
|
32
|
+
const rects = els.map((e) => e.getBoundingClientRect());
|
|
33
|
+
// How much each later card covers the one before it (0 to 1).
|
|
34
|
+
const depth = els.map(() => 0);
|
|
35
|
+
for (let j = 1; j < els.length; j++) {
|
|
36
|
+
const pinPrev = viewTop + top + (j - 1) * offset;
|
|
37
|
+
const startCover = pinPrev + rects[j - 1].height;
|
|
38
|
+
const span = Math.max(1, rects[j - 1].height - offset);
|
|
39
|
+
const cover = Math.min(1, Math.max(0, (startCover - rects[j].top) / span));
|
|
40
|
+
for (let i = 0; i < j; i++) depth[i] += cover;
|
|
41
|
+
}
|
|
42
|
+
els.forEach((slot, i) => {
|
|
43
|
+
const card = slot.firstElementChild;
|
|
44
|
+
if (!card) return;
|
|
45
|
+
const d = depth[i];
|
|
46
|
+
const scale = Math.max(minScale, 1 - d * scaleStep);
|
|
47
|
+
const lean = Math.min(1, d) * tilt;
|
|
48
|
+
card.style.transform = d > 0.001 ? `perspective(1000px) rotateX(${-lean}deg) scale(${scale.toFixed(4)})` : "";
|
|
49
|
+
card.style.filter = blur && d > 0.01 ? `blur(${(d * blur).toFixed(2)}px)` : "";
|
|
50
|
+
const shade = card.querySelector("[data-stack-shade]");
|
|
51
|
+
if (shade) shade.style.opacity = (1 - Math.pow(1 - dim, d)).toFixed(3);
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
const schedule = () => {
|
|
55
|
+
if (!raf) raf = requestAnimationFrame(update);
|
|
56
|
+
};
|
|
57
|
+
scroller.addEventListener("scroll", schedule, { passive: true });
|
|
58
|
+
window.addEventListener("resize", schedule);
|
|
59
|
+
const ro = new ResizeObserver(schedule);
|
|
60
|
+
ro.observe(box);
|
|
61
|
+
update();
|
|
62
|
+
return () => {
|
|
63
|
+
cancelAnimationFrame(raf);
|
|
64
|
+
scroller.removeEventListener("scroll", schedule);
|
|
65
|
+
window.removeEventListener("resize", schedule);
|
|
66
|
+
ro.disconnect();
|
|
67
|
+
};
|
|
68
|
+
}, [useWindowScroll, top, offset, scaleStep, minScale, dim, blur, tilt, items.length]);
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div ref={root} className={`relative w-full ${useWindowScroll ? "" : "overflow-y-auto overscroll-contain"} ${className}`} style={useWindowScroll ? undefined : { height }}>
|
|
72
|
+
<div className="px-[clamp(1rem,6%,4rem)]" style={{ paddingTop: useWindowScroll ? 0 : top, paddingBottom: endSpace }}>
|
|
73
|
+
{items.map((child, i) => (
|
|
74
|
+
<div
|
|
75
|
+
key={i}
|
|
76
|
+
ref={(e) => (slots.current[i] = e)}
|
|
77
|
+
className="sticky"
|
|
78
|
+
style={{ top: top + i * offset, marginTop: i ? gap : 0, zIndex: i + 1 }}
|
|
79
|
+
>
|
|
80
|
+
{child}
|
|
81
|
+
</div>
|
|
82
|
+
))}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export default ScrollStack;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useId, useRef, useState } from "react";
|
|
1
|
+
import React, { useEffect, useId, useRef, useState } from "react";
|
|
2
2
|
import { AnimatePresence, LayoutGroup, motion } from "framer-motion";
|
|
3
3
|
import { BarChart3, Bell, ChevronDown, FolderKanban, Home, Inbox, LifeBuoy, PanelLeftClose, PanelLeftOpen, Settings, Users } from "lucide-react";
|
|
4
4
|
|
|
@@ -61,6 +61,7 @@ const SidebarNav = ({
|
|
|
61
61
|
user = { name: "Mohan Dalei", email: "mohan@apex-ui.in", avatar: "https://ik.imagekit.io/ny6yinyut/apexui/assets/captainamerica.png?tr=f-auto,q-80,w-96" },
|
|
62
62
|
defaultActive = "home",
|
|
63
63
|
defaultCollapsed = false,
|
|
64
|
+
autoCollapse = true,
|
|
64
65
|
accentColor = "#a3e635",
|
|
65
66
|
theme = "dark",
|
|
66
67
|
onSelect,
|
|
@@ -73,6 +74,25 @@ const SidebarNav = ({
|
|
|
73
74
|
const [open, setOpen] = useState(() => new Set(["projects"]));
|
|
74
75
|
const [tip, setTip] = useState(null);
|
|
75
76
|
const asideRef = useRef(null);
|
|
77
|
+
const userToggled = useRef(false);
|
|
78
|
+
|
|
79
|
+
// On narrow screens the full sidebar would squeeze the page, so it becomes
|
|
80
|
+
// an icon rail there and opens again when there is room, until the user
|
|
81
|
+
// picks a state with the toggle.
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
const host = asideRef.current?.parentElement;
|
|
84
|
+
if (!autoCollapse || !host) return undefined;
|
|
85
|
+
let narrow = null;
|
|
86
|
+
const ro = new ResizeObserver(([entry]) => {
|
|
87
|
+
const now = entry.contentRect.width < 560;
|
|
88
|
+
if (now !== narrow) {
|
|
89
|
+
if (!userToggled.current && (now || narrow !== null)) setCollapsed(now || defaultCollapsed);
|
|
90
|
+
narrow = now;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
ro.observe(host);
|
|
94
|
+
return () => ro.disconnect();
|
|
95
|
+
}, [autoCollapse, defaultCollapsed]);
|
|
76
96
|
|
|
77
97
|
const select = (item) => {
|
|
78
98
|
setActive(item.id);
|
|
@@ -159,6 +179,7 @@ const SidebarNav = ({
|
|
|
159
179
|
type="button"
|
|
160
180
|
aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
|
|
161
181
|
onClick={() => {
|
|
182
|
+
userToggled.current = true;
|
|
162
183
|
setCollapsed((c) => !c);
|
|
163
184
|
setTip(null);
|
|
164
185
|
}}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from "react";
|
|
2
|
+
import { Color, Mesh, OrthographicCamera, PlaneGeometry, Scene, ShaderMaterial, Vector2, WebGLRenderer } from "three";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Sunrise Background: a sun rising out of the valley between two mountain
|
|
6
|
+
* ranges, drawn in one WebGL shader. The sky warms from night to dawn as it
|
|
7
|
+
* climbs, light rays fan out, the ridges catch a rim of light, mist glows in
|
|
8
|
+
* the valley and the stars fade. The mountains drift a little with the
|
|
9
|
+
* pointer. Put your hero content on top as children.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const PRESETS = {
|
|
13
|
+
dawn: { sunColor: "#ffb15c", skyTop: "#1d2350", skyHorizon: "#ff7a59", mountainColor: "#140f24" },
|
|
14
|
+
golden: { sunColor: "#ffd36e", skyTop: "#2b3a67", skyHorizon: "#ffa94d", mountainColor: "#1b1420" },
|
|
15
|
+
dusk: { sunColor: "#ff8fb1", skyTop: "#1a1033", skyHorizon: "#c86dd7", mountainColor: "#100a1c" },
|
|
16
|
+
arctic: { sunColor: "#b8f3ff", skyTop: "#0c1f33", skyHorizon: "#5fd4e6", mountainColor: "#08141f" },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const VERT = `void main(){ gl_Position = vec4(position.xy, 0.0, 1.0); }`;
|
|
20
|
+
|
|
21
|
+
const FRAG = `
|
|
22
|
+
precision highp float;
|
|
23
|
+
uniform vec2 uRes; uniform float uTime; uniform float uRise; uniform vec2 uMouse;
|
|
24
|
+
uniform vec3 uSun; uniform vec3 uSkyTop; uniform vec3 uSkyLow; uniform vec3 uMount;
|
|
25
|
+
uniform float uRays; uniform float uStars; uniform float uHaze; uniform float uSunSize; uniform float uSunHeight;
|
|
26
|
+
|
|
27
|
+
float h1(float x){ return fract(sin(x * 127.1) * 43758.5453); }
|
|
28
|
+
float h2(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
|
|
29
|
+
float n1(float x){ float i = floor(x); float f = fract(x); return mix(h1(i), h1(i + 1.0), f * f * (3.0 - 2.0 * f)); }
|
|
30
|
+
float n2(vec2 p){
|
|
31
|
+
vec2 i = floor(p); vec2 f = fract(p); vec2 u = f * f * (3.0 - 2.0 * f);
|
|
32
|
+
return mix(mix(h2(i), h2(i + vec2(1, 0)), u.x), mix(h2(i + vec2(0, 1)), h2(i + vec2(1, 1)), u.x), u.y);
|
|
33
|
+
}
|
|
34
|
+
// Craggy ridge detail: a few octaves of folded noise.
|
|
35
|
+
float crag(float x){
|
|
36
|
+
float v = 0.0; float a = 0.5;
|
|
37
|
+
for (int i = 0; i < 5; i++) { v += a * (1.0 - abs(n1(x) * 2.0 - 1.0)); x *= 2.03; a *= 0.5; }
|
|
38
|
+
return v;
|
|
39
|
+
}
|
|
40
|
+
// Two peaks with a valley between them, plus detail.
|
|
41
|
+
float range(float x, float c, float w, float peak, float base, float amp, float freq, float seed){
|
|
42
|
+
float tl = clamp(1.0 - abs(x + c) / w, 0.0, 1.0);
|
|
43
|
+
float tr = clamp(1.0 - abs(x - c) / w, 0.0, 1.0);
|
|
44
|
+
float sl = mix(tl, tl * tl * (3.0 - 2.0 * tl), 0.45);
|
|
45
|
+
float sr = mix(tr, tr * tr * (3.0 - 2.0 * tr), 0.45);
|
|
46
|
+
float s = max(sl, sr);
|
|
47
|
+
return base + s * peak + (crag(x * freq + seed) - 0.45) * amp * (0.35 + s);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
void main(){
|
|
51
|
+
vec2 uv = gl_FragCoord.xy / uRes;
|
|
52
|
+
float asp = uRes.x / uRes.y;
|
|
53
|
+
// x normalised to the half width (clamped so phones still show both slopes).
|
|
54
|
+
float xn = (uv.x - 0.5) * asp / (0.5 * clamp(asp, 0.7, 2.2));
|
|
55
|
+
float y = uv.y;
|
|
56
|
+
float rise = uRise;
|
|
57
|
+
|
|
58
|
+
vec2 sp = vec2(0.0, mix(-0.16, uSunHeight, rise));
|
|
59
|
+
vec2 q = vec2((uv.x - 0.5) * asp, y);
|
|
60
|
+
float d = length(q - sp);
|
|
61
|
+
|
|
62
|
+
// Sky: night blue warming toward the horizon as the sun climbs.
|
|
63
|
+
vec3 top = mix(uSkyTop * 0.28, uSkyTop, smoothstep(0.0, 0.8, rise));
|
|
64
|
+
vec3 low = mix(uSkyTop * 0.5, uSkyLow, smoothstep(0.05, 0.9, rise));
|
|
65
|
+
vec3 col = mix(low, top, smoothstep(0.0, 0.9, y + 0.15 * (1.0 - rise)));
|
|
66
|
+
float warm = exp(-max(y - sp.y * 0.6, 0.0) * 3.2) * exp(-abs(q.x) * 0.9);
|
|
67
|
+
col += uSun * warm * (0.15 + 0.75 * rise);
|
|
68
|
+
|
|
69
|
+
// Stars fade out with the dawn.
|
|
70
|
+
vec2 cell = floor(gl_FragCoord.xy / 3.0);
|
|
71
|
+
float star = step(0.9965, h2(cell)) * (0.6 + 0.4 * sin(uTime * 2.0 + h2(cell + 7.0) * 30.0));
|
|
72
|
+
col += star * uStars * (1.0 - smoothstep(0.1, 0.75, rise)) * smoothstep(0.35, 0.9, y);
|
|
73
|
+
|
|
74
|
+
// Sun disc, halo and rays.
|
|
75
|
+
float r = uSunSize;
|
|
76
|
+
float disc = smoothstep(r + 0.003, r - 0.003, d);
|
|
77
|
+
vec3 sunCore = mix(uSun, vec3(1.0, 0.98, 0.92), 0.65);
|
|
78
|
+
col = mix(col, sunCore, disc);
|
|
79
|
+
col += uSun * (exp(-d * 9.0 / (r * 10.0)) * 0.55 + exp(-d * 2.2) * 0.35) * (0.4 + 0.6 * rise);
|
|
80
|
+
float ang = atan(q.x - sp.x, q.y - sp.y);
|
|
81
|
+
float beam = n2(vec2(ang * 7.0, uTime * 0.12)) * 0.7 + n2(vec2(ang * 17.0, -uTime * 0.07)) * 0.3;
|
|
82
|
+
float rays = smoothstep(0.45, 0.95, beam) * exp(-d * 1.6) * smoothstep(r, r * 2.5, d);
|
|
83
|
+
col += uSun * rays * uRays * 0.55 * rise;
|
|
84
|
+
|
|
85
|
+
// Mountains, far to near. Each layer drifts with the pointer by depth.
|
|
86
|
+
float mx = uMouse.x; float my = uMouse.y;
|
|
87
|
+
float hf = range(xn + mx * 0.012, 0.6, 0.85, 0.36, 0.16, 0.06, 2.6, 3.0) + my * 0.004;
|
|
88
|
+
float hm = range(xn + mx * 0.028, 0.42, 0.78, 0.33, 0.08, 0.07, 3.8, 11.0) + my * 0.008;
|
|
89
|
+
float hn = range(xn + mx * 0.05, 0.66, 0.86, 0.30, -0.02, 0.06, 5.2, 27.0) + my * 0.014;
|
|
90
|
+
|
|
91
|
+
float toSun = exp(-abs(xn) * 1.4) * rise;
|
|
92
|
+
vec3 haze = mix(low, uSun, 0.35 + 0.3 * rise);
|
|
93
|
+
|
|
94
|
+
// Far range: soft and hazy.
|
|
95
|
+
float mf = smoothstep(hf + 0.0025, hf - 0.0025, y);
|
|
96
|
+
vec3 cf = mix(uMount, haze, 0.62 * uHaze + 0.1);
|
|
97
|
+
cf += uSun * exp(-max(hf - y, 0.0) * 40.0) * toSun * 0.5;
|
|
98
|
+
col = mix(col, cf, mf);
|
|
99
|
+
|
|
100
|
+
// Valley mist lit by the sun.
|
|
101
|
+
float mist = exp(-abs(y - (hm - 0.03)) * 9.0) * exp(-abs(xn) * 1.1) * uHaze;
|
|
102
|
+
col += uSun * mist * 0.35 * rise;
|
|
103
|
+
|
|
104
|
+
// Middle range.
|
|
105
|
+
float mm = smoothstep(hm + 0.002, hm - 0.002, y);
|
|
106
|
+
vec3 cm = mix(uMount, haze, 0.32 * uHaze + 0.04);
|
|
107
|
+
cm *= 0.75 + 0.35 * smoothstep(hm - 0.35, hm, y);
|
|
108
|
+
cm += uSun * exp(-max(hm - y, 0.0) * 70.0) * toSun * 0.7;
|
|
109
|
+
col = mix(col, cm, mm);
|
|
110
|
+
|
|
111
|
+
// Near range: darkest, with a sharp rim of light on the ridge.
|
|
112
|
+
float mn = smoothstep(hn + 0.0018, hn - 0.0018, y);
|
|
113
|
+
vec3 cn = uMount * (0.65 + 0.35 * smoothstep(hn - 0.3, hn, y));
|
|
114
|
+
cn += uSun * exp(-max(hn - y, 0.0) * 120.0) * toSun * 0.9;
|
|
115
|
+
col = mix(col, cn, mn);
|
|
116
|
+
|
|
117
|
+
// Low fog over everything and light bleeding over the ridges.
|
|
118
|
+
col += haze * exp(-y * 7.0) * 0.12 * uHaze * rise;
|
|
119
|
+
col += uSun * exp(-d * 3.0) * 0.18 * rise;
|
|
120
|
+
|
|
121
|
+
// Gentle tone curve and dither (no banding in the gradients).
|
|
122
|
+
col = col / (1.0 + col * 0.35);
|
|
123
|
+
col += (h2(gl_FragCoord.xy + fract(uTime) * 61.0) - 0.5) / 255.0;
|
|
124
|
+
gl_FragColor = vec4(col, 1.0);
|
|
125
|
+
}`;
|
|
126
|
+
|
|
127
|
+
const SunriseBackground = ({
|
|
128
|
+
preset = "dawn",
|
|
129
|
+
sunColor,
|
|
130
|
+
skyTop,
|
|
131
|
+
skyHorizon,
|
|
132
|
+
mountainColor,
|
|
133
|
+
duration = 6,
|
|
134
|
+
autoRise = true,
|
|
135
|
+
sunHeight = 0.36,
|
|
136
|
+
sunSize = 0.075,
|
|
137
|
+
rays = 1,
|
|
138
|
+
stars = 1,
|
|
139
|
+
haze = 1,
|
|
140
|
+
speed = 1,
|
|
141
|
+
interactive = true,
|
|
142
|
+
replay = 0,
|
|
143
|
+
className = "",
|
|
144
|
+
children,
|
|
145
|
+
}) => {
|
|
146
|
+
const wrap = useRef(null);
|
|
147
|
+
const live = useRef({});
|
|
148
|
+
const p = PRESETS[preset] ?? PRESETS.dawn;
|
|
149
|
+
live.current = {
|
|
150
|
+
sun: sunColor ?? p.sunColor,
|
|
151
|
+
top: skyTop ?? p.skyTop,
|
|
152
|
+
low: skyHorizon ?? p.skyHorizon,
|
|
153
|
+
mount: mountainColor ?? p.mountainColor,
|
|
154
|
+
duration,
|
|
155
|
+
autoRise,
|
|
156
|
+
sunHeight,
|
|
157
|
+
sunSize,
|
|
158
|
+
rays,
|
|
159
|
+
stars,
|
|
160
|
+
haze,
|
|
161
|
+
speed,
|
|
162
|
+
interactive,
|
|
163
|
+
};
|
|
164
|
+
const rise = useRef({ start: 0 });
|
|
165
|
+
|
|
166
|
+
// Replaying (or turning autoRise back on) starts the climb again.
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
rise.current.start = performance.now();
|
|
169
|
+
}, [replay, autoRise]);
|
|
170
|
+
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
const el = wrap.current;
|
|
173
|
+
const renderer = new WebGLRenderer({ antialias: false, alpha: false, powerPreference: "high-performance" });
|
|
174
|
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.75));
|
|
175
|
+
Object.assign(renderer.domElement.style, { position: "absolute", inset: "0", width: "100%", height: "100%", display: "block" });
|
|
176
|
+
el.prepend(renderer.domElement);
|
|
177
|
+
const scene = new Scene();
|
|
178
|
+
const camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
|
179
|
+
const u = {
|
|
180
|
+
uRes: { value: new Vector2(1, 1) },
|
|
181
|
+
uTime: { value: 0 },
|
|
182
|
+
uRise: { value: 0 },
|
|
183
|
+
uMouse: { value: new Vector2() },
|
|
184
|
+
uSun: { value: new Color() },
|
|
185
|
+
uSkyTop: { value: new Color() },
|
|
186
|
+
uSkyLow: { value: new Color() },
|
|
187
|
+
uMount: { value: new Color() },
|
|
188
|
+
uRays: { value: 1 },
|
|
189
|
+
uStars: { value: 1 },
|
|
190
|
+
uHaze: { value: 1 },
|
|
191
|
+
uSunSize: { value: 0.075 },
|
|
192
|
+
uSunHeight: { value: 0.3 },
|
|
193
|
+
};
|
|
194
|
+
const material = new ShaderMaterial({ vertexShader: VERT, fragmentShader: FRAG, uniforms: u, depthTest: false, depthWrite: false });
|
|
195
|
+
const geometry = new PlaneGeometry(2, 2);
|
|
196
|
+
scene.add(new Mesh(geometry, material));
|
|
197
|
+
|
|
198
|
+
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
199
|
+
const mouse = { x: 0, y: 0, tx: 0, ty: 0 };
|
|
200
|
+
let raf = 0;
|
|
201
|
+
let last = 0;
|
|
202
|
+
let time = 0;
|
|
203
|
+
let visible = true;
|
|
204
|
+
|
|
205
|
+
const fit = () => {
|
|
206
|
+
const { width, height } = el.getBoundingClientRect();
|
|
207
|
+
if (!width || !height) return;
|
|
208
|
+
renderer.setSize(width, height, false);
|
|
209
|
+
renderer.getDrawingBufferSize(u.uRes.value);
|
|
210
|
+
if (!raf) draw(performance.now());
|
|
211
|
+
};
|
|
212
|
+
const draw = (now) => {
|
|
213
|
+
const o = live.current;
|
|
214
|
+
const dt = last ? Math.min(0.05, (now - last) / 1000) : 1 / 60;
|
|
215
|
+
last = now;
|
|
216
|
+
time += dt * o.speed;
|
|
217
|
+
const t = o.autoRise && !reduced ? Math.min(1, (now - rise.current.start) / (Math.max(0.1, o.duration) * 1000)) : 1;
|
|
218
|
+
// Ease out: quick at first, then a slow settle at the top.
|
|
219
|
+
u.uRise.value = 1 - Math.pow(1 - t, 3);
|
|
220
|
+
const k = 1 - Math.exp(-dt / 0.35);
|
|
221
|
+
mouse.x += ((o.interactive ? mouse.tx : 0) - mouse.x) * k;
|
|
222
|
+
mouse.y += ((o.interactive ? mouse.ty : 0) - mouse.y) * k;
|
|
223
|
+
u.uMouse.value.set(mouse.x, mouse.y);
|
|
224
|
+
u.uTime.value = time;
|
|
225
|
+
u.uSun.value.set(o.sun);
|
|
226
|
+
u.uSkyTop.value.set(o.top);
|
|
227
|
+
u.uSkyLow.value.set(o.low);
|
|
228
|
+
u.uMount.value.set(o.mount);
|
|
229
|
+
u.uRays.value = o.rays;
|
|
230
|
+
u.uStars.value = o.stars;
|
|
231
|
+
u.uHaze.value = o.haze;
|
|
232
|
+
u.uSunSize.value = o.sunSize;
|
|
233
|
+
u.uSunHeight.value = o.sunHeight;
|
|
234
|
+
renderer.render(scene, camera);
|
|
235
|
+
};
|
|
236
|
+
const loop = (now) => {
|
|
237
|
+
raf = 0;
|
|
238
|
+
draw(now);
|
|
239
|
+
if (visible && !reduced) raf = requestAnimationFrame(loop);
|
|
240
|
+
};
|
|
241
|
+
const start = () => {
|
|
242
|
+
if (!raf && visible && !reduced) {
|
|
243
|
+
last = 0;
|
|
244
|
+
raf = requestAnimationFrame(loop);
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const onMove = (e) => {
|
|
249
|
+
const r = el.getBoundingClientRect();
|
|
250
|
+
mouse.tx = ((e.clientX - r.left) / r.width) * 2 - 1;
|
|
251
|
+
mouse.ty = ((e.clientY - r.top) / r.height) * 2 - 1;
|
|
252
|
+
};
|
|
253
|
+
const onLeave = () => {
|
|
254
|
+
mouse.tx = 0;
|
|
255
|
+
mouse.ty = 0;
|
|
256
|
+
};
|
|
257
|
+
el.addEventListener("pointermove", onMove);
|
|
258
|
+
el.addEventListener("pointerleave", onLeave);
|
|
259
|
+
const ro = new ResizeObserver(fit);
|
|
260
|
+
ro.observe(el);
|
|
261
|
+
const io = new IntersectionObserver(([entry]) => {
|
|
262
|
+
visible = entry.isIntersecting;
|
|
263
|
+
if (visible) start();
|
|
264
|
+
});
|
|
265
|
+
io.observe(el);
|
|
266
|
+
fit();
|
|
267
|
+
start();
|
|
268
|
+
|
|
269
|
+
return () => {
|
|
270
|
+
cancelAnimationFrame(raf);
|
|
271
|
+
ro.disconnect();
|
|
272
|
+
io.disconnect();
|
|
273
|
+
el.removeEventListener("pointermove", onMove);
|
|
274
|
+
el.removeEventListener("pointerleave", onLeave);
|
|
275
|
+
geometry.dispose();
|
|
276
|
+
material.dispose();
|
|
277
|
+
renderer.dispose();
|
|
278
|
+
renderer.forceContextLoss();
|
|
279
|
+
renderer.domElement.remove();
|
|
280
|
+
};
|
|
281
|
+
}, []);
|
|
282
|
+
|
|
283
|
+
return (
|
|
284
|
+
<div ref={wrap} className={`relative h-full w-full overflow-hidden bg-[#0b0b18] ${className}`}>
|
|
285
|
+
{children && <div className="relative z-10 h-full w-full">{children}</div>}
|
|
286
|
+
</div>
|
|
287
|
+
);
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
export default SunriseBackground;
|
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
import React, { useRef } from "react";
|
|
1
|
+
import React, { useEffect, useRef } from "react";
|
|
2
2
|
import { useAnimationFrame, useReducedMotion } from "framer-motion";
|
|
3
3
|
|
|
4
4
|
/*
|
|
5
5
|
* 3D Text Ring: letters stand on a ring in real 3D space and the ring
|
|
6
6
|
* turns, so text sweeps around the back and comes forward again. Letters
|
|
7
|
-
* fade and soften as they pass behind. Drag to spin it yourself.
|
|
7
|
+
* fade and soften as they pass behind. Drag to spin it yourself. On narrow
|
|
8
|
+
* screens the whole ring scales down to fit its container.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
const TextRing3D = ({ text = "APEX UI · 3D TYPE · ", radius = 9, speed = 30, tilt = -14, fontSize = 2, color = "currentColor", accentColor = "#a3e635", className = "" }) => {
|
|
11
12
|
const reduced = useReducedMotion();
|
|
13
|
+
const box = useRef(null);
|
|
12
14
|
const ring = useRef(null);
|
|
15
|
+
const fit = useRef(1);
|
|
13
16
|
const letters = useRef([]);
|
|
14
17
|
const angle = useRef(0);
|
|
15
18
|
const vel = useRef(0);
|
|
@@ -17,13 +20,24 @@ const TextRing3D = ({ text = "APEX UI · 3D TYPE · ", radius = 9, speed = 30, t
|
|
|
17
20
|
const chars = Array.from(text);
|
|
18
21
|
const step = 360 / chars.length;
|
|
19
22
|
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const el = box.current;
|
|
25
|
+
const ro = new ResizeObserver(() => {
|
|
26
|
+
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
27
|
+
const want = radius * 2.4 * rem;
|
|
28
|
+
fit.current = Math.min(1, el.getBoundingClientRect().width / want);
|
|
29
|
+
});
|
|
30
|
+
ro.observe(el);
|
|
31
|
+
return () => ro.disconnect();
|
|
32
|
+
}, [radius]);
|
|
33
|
+
|
|
20
34
|
useAnimationFrame((_, delta) => {
|
|
21
35
|
const dt = Math.min(0.05, delta / 1000);
|
|
22
36
|
if (!drag.current) {
|
|
23
37
|
vel.current *= 0.95;
|
|
24
38
|
angle.current += (reduced ? 0 : speed) * dt + vel.current * dt;
|
|
25
39
|
}
|
|
26
|
-
if (ring.current) ring.current.style.transform = `rotateX(${tilt}deg) rotateY(${-angle.current}deg)`;
|
|
40
|
+
if (ring.current) ring.current.style.transform = `scale(${fit.current}) rotateX(${tilt}deg) rotateY(${-angle.current}deg)`;
|
|
27
41
|
letters.current.forEach((el, i) => {
|
|
28
42
|
if (!el) return;
|
|
29
43
|
// How much this letter faces the viewer (1 front, -1 back).
|
|
@@ -35,6 +49,7 @@ const TextRing3D = ({ text = "APEX UI · 3D TYPE · ", radius = 9, speed = 30, t
|
|
|
35
49
|
|
|
36
50
|
return (
|
|
37
51
|
<div
|
|
52
|
+
ref={box}
|
|
38
53
|
className={`grid cursor-grab touch-none select-none place-items-center active:cursor-grabbing [perspective:900px] ${className}`}
|
|
39
54
|
style={{ width: `${radius * 2.4}rem`, maxWidth: "100%", height: `${fontSize * 3.2 + 4}rem`, color }}
|
|
40
55
|
onPointerDown={(e) => {
|