apex-ui-kit 2.10.0 → 2.12.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 +2 -3
- package/dist/ApexUI-Kit/templates/AnimatedTabs/AnimatedTabs.jsx +17 -3
- package/dist/ApexUI-Kit/templates/AuroraBackground/AuroraBackground.jsx +157 -35
- package/dist/ApexUI-Kit/templates/BluePrintBackground/BluePrintBackground.jsx +134 -145
- package/dist/ApexUI-Kit/templates/CardSwap3D/CardSwap3D.jsx +64 -40
- package/dist/ApexUI-Kit/templates/DotGlobe/DotGlobe.jsx +6 -3
- package/dist/ApexUI-Kit/templates/FlipClock/FlipClock.jsx +1 -1
- package/dist/ApexUI-Kit/templates/Folder/Folder.jsx +58 -36
- package/dist/ApexUI-Kit/templates/GooeyNav/GooeyNav.jsx +16 -2
- package/dist/ApexUI-Kit/templates/ImageAccordion/ImageAccordion.jsx +20 -10
- package/dist/ApexUI-Kit/templates/InfiniteCanvas/InfiniteCanvas.jsx +1 -1
- package/dist/ApexUI-Kit/templates/IsoStack/IsoStack.jsx +28 -10
- package/dist/ApexUI-Kit/templates/LuminousParticleOcean/LuminousParticleOcean.jsx +8 -6
- package/dist/ApexUI-Kit/templates/MeteorCard/MeteorCard.jsx +5 -5
- package/dist/ApexUI-Kit/templates/NavMenu/NavMenu.jsx +175 -158
- package/dist/ApexUI-Kit/templates/OrbitIcons/OrbitIcons.jsx +1 -1
- package/dist/ApexUI-Kit/templates/ParallaxCard/ParallaxCard.jsx +34 -33
- package/dist/ApexUI-Kit/templates/SmokeBackground/SmokeBackground.jsx +237 -235
- package/dist/ApexUI-Kit/templates/Testimonials/Testimonials.jsx +140 -120
- package/dist/cli/index.js +40 -21
- package/package.json +8 -2
- package/dist/ApexUI-Kit/templates/SmartGridCard/SmartGridCard.jsx +0 -243
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
|
+
**117 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
|
+
- **117 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.
|
|
@@ -141,7 +141,6 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
141
141
|
| [Pricing Cards](https://apex-ui.in/components/pricing-cards) | `pricing-cards` |
|
|
142
142
|
| [Profile](https://apex-ui.in/components/profile) | `flip-profile`, `parallax-profile`, `profile-card` |
|
|
143
143
|
| [Scroll Stack](https://apex-ui.in/components/scroll-stack) | `scroll-stack` |
|
|
144
|
-
| [Smart Grid Card](https://apex-ui.in/components/smart-grid-card) | `smart-grid-card` |
|
|
145
144
|
| [Spotlight Grid](https://apex-ui.in/components/spotlight-grid) | `spotlight-grid` |
|
|
146
145
|
| [Swipe Cards](https://apex-ui.in/components/swipe-cards) | `swipe-cards` |
|
|
147
146
|
| [Testimonials](https://apex-ui.in/components/testimonials) | `testimonials` |
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useId, useRef, useState } from "react";
|
|
1
|
+
import React, { useId, useLayoutEffect, useRef, useState } from "react";
|
|
2
2
|
import { AnimatePresence, motion } from "framer-motion";
|
|
3
3
|
|
|
4
4
|
/*
|
|
@@ -19,7 +19,21 @@ const AnimatedTabs = ({ tabs = DEFAULT_TABS, defaultTab, variant = "pill", accen
|
|
|
19
19
|
const [active, setActive] = useState(defaultTab ?? tabs[0]?.id);
|
|
20
20
|
const [dir, setDir] = useState(1);
|
|
21
21
|
const refs = useRef([]);
|
|
22
|
+
const root = useRef(null);
|
|
22
23
|
const dark = theme !== "light";
|
|
24
|
+
// Compact tabs when the box itself is narrow (not the screen), so labels never get cut.
|
|
25
|
+
const [compact, setCompact] = useState(false);
|
|
26
|
+
|
|
27
|
+
useLayoutEffect(() => {
|
|
28
|
+
const el = root.current;
|
|
29
|
+
if (!el) return undefined;
|
|
30
|
+
const ro = new ResizeObserver(() => {
|
|
31
|
+
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
32
|
+
setCompact(el.clientWidth / rem < 30);
|
|
33
|
+
});
|
|
34
|
+
ro.observe(el);
|
|
35
|
+
return () => ro.disconnect();
|
|
36
|
+
}, []);
|
|
23
37
|
const index = tabs.findIndex((t) => t.id === active);
|
|
24
38
|
const current = tabs[index];
|
|
25
39
|
|
|
@@ -44,7 +58,7 @@ const AnimatedTabs = ({ tabs = DEFAULT_TABS, defaultTab, variant = "pill", accen
|
|
|
44
58
|
}[variant];
|
|
45
59
|
|
|
46
60
|
return (
|
|
47
|
-
<div className={`w-full max-w-xl ${dark ? "text-white" : "text-neutral-900"} ${className}`}>
|
|
61
|
+
<div ref={root} className={`w-full max-w-xl ${dark ? "text-white" : "text-neutral-900"} ${className}`}>
|
|
48
62
|
<div role="tablist" aria-orientation="horizontal" onKeyDown={onKeyDown} className={`relative flex overflow-x-auto [scrollbar-width:none] ${list}`}>
|
|
49
63
|
{tabs.map((t, i) => {
|
|
50
64
|
const on = t.id === active;
|
|
@@ -58,7 +72,7 @@ const AnimatedTabs = ({ tabs = DEFAULT_TABS, defaultTab, variant = "pill", accen
|
|
|
58
72
|
aria-controls={`${id}-panel`}
|
|
59
73
|
tabIndex={on ? 0 : -1}
|
|
60
74
|
onClick={() => select(i)}
|
|
61
|
-
className={`relative flex-1 whitespace-nowrap
|
|
75
|
+
className={`relative flex-1 whitespace-nowrap py-2 ${compact ? "px-2 text-[0.76rem]" : "px-4 text-[0.88rem]"} font-medium outline-none transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-current/40 ${variant === "underline" ? "pb-3" : variant === "pill" ? "rounded-full" : "rounded-lg"} ${on ? (variant === "pill" ? "text-neutral-950" : "") : dark ? "text-white/55 hover:text-white" : "text-neutral-500 hover:text-neutral-900"}`}
|
|
62
76
|
>
|
|
63
77
|
{on && (
|
|
64
78
|
<motion.span
|
|
@@ -1,45 +1,167 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useEffect, useRef } from "react";
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
|
-
* Aurora Background:
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Aurora Background: northern lights over a night sky, drawn in one small
|
|
5
|
+
* WebGL shader. Curtains of light hang from the top, ripple sideways and
|
|
6
|
+
* flicker in fine vertical rays; the colours run from the first (lower edge)
|
|
7
|
+
* to the last (upper fringe). Faint stars sit behind them. Reduced-motion
|
|
8
|
+
* visitors get a still frame. No library needed.
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
const VERT = `attribute vec2 p; void main(){ gl_Position = vec4(p, 0.0, 1.0); }`;
|
|
12
|
+
|
|
13
|
+
const FRAG = `
|
|
14
|
+
precision highp float;
|
|
15
|
+
uniform vec2 uRes; uniform float uTime; uniform float uIntensity; uniform float uLight;
|
|
16
|
+
uniform vec3 uC0; uniform vec3 uC1; uniform vec3 uC2; uniform vec3 uC3; uniform vec3 uSky;
|
|
17
|
+
|
|
18
|
+
float h(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
|
|
19
|
+
float n(vec2 p){
|
|
20
|
+
vec2 i = floor(p); vec2 f = fract(p); f = f * f * (3.0 - 2.0 * f);
|
|
21
|
+
return mix(mix(h(i), h(i + vec2(1, 0)), f.x), mix(h(i + vec2(0, 1)), h(i + vec2(1, 1)), f.x), f.y);
|
|
22
|
+
}
|
|
23
|
+
float fbm(vec2 p){ float v = 0.0; float a = 0.5; for (int i = 0; i < 4; i++) { v += a * n(p); p *= 2.1; a *= 0.5; } return v; }
|
|
24
|
+
|
|
25
|
+
vec3 ramp(float t){
|
|
26
|
+
t = clamp(t, 0.0, 1.0) * 3.0;
|
|
27
|
+
return t < 1.0 ? mix(uC0, uC1, t) : t < 2.0 ? mix(uC1, uC2, t - 1.0) : mix(uC2, uC3, t - 2.0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// One curtain: a wavy band whose lower edge is bright and which fades upward.
|
|
31
|
+
vec3 curtain(vec2 uv, float t, float seed, float base, float amp){
|
|
32
|
+
float x = uv.x * 1.6 + seed;
|
|
33
|
+
float wave = (fbm(vec2(x * 0.9, t * 0.12 + seed)) - 0.5) * amp + sin(x * 1.7 + t * 0.35 + seed) * amp * 0.35;
|
|
34
|
+
float edge = base + wave; // lower edge of the curtain
|
|
35
|
+
float d = uv.y - edge; // height above the edge
|
|
36
|
+
if (d < -0.05) return vec3(0.0);
|
|
37
|
+
float rays = 0.55 + 0.45 * n(vec2(x * 38.0, t * 0.6 + seed)); // fine vertical rays
|
|
38
|
+
float body = exp(-max(d, 0.0) * 3.2) * smoothstep(-0.05, 0.015, d);
|
|
39
|
+
float glow = exp(-abs(d) * 26.0) * 0.9; // bright lower rim
|
|
40
|
+
float shimmer = 0.75 + 0.25 * sin(t * 1.3 + x * 6.0 + seed * 3.0);
|
|
41
|
+
return ramp(clamp(d * 1.9, 0.0, 1.0)) * (body * rays + glow) * shimmer;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
void main(){
|
|
45
|
+
vec2 uv = gl_FragCoord.xy / uRes;
|
|
46
|
+
uv.x *= uRes.x / uRes.y;
|
|
47
|
+
float t = uTime;
|
|
48
|
+
vec3 col = uSky * (0.55 + 0.45 * (1.0 - gl_FragCoord.y / uRes.y));
|
|
49
|
+
// Stars (dark sky only).
|
|
50
|
+
vec2 cell = floor(gl_FragCoord.xy / 2.0);
|
|
51
|
+
col += step(0.9975, h(cell)) * (0.5 + 0.5 * sin(t * 2.0 + h(cell) * 40.0)) * (1.0 - uLight) * 0.8;
|
|
52
|
+
vec3 a = curtain(uv, t, 0.0, 0.42, 0.22) + curtain(uv, t * 0.8, 4.7, 0.52, 0.18) * 0.75 + curtain(uv, t * 1.15, 9.3, 0.33, 0.26) * 0.55;
|
|
53
|
+
a *= uIntensity;
|
|
54
|
+
col = uLight > 0.5 ? mix(col, col * (1.0 - min(a, 0.85)) + a * 0.55, 1.0) : col + a;
|
|
55
|
+
col += (h(gl_FragCoord.xy + t) - 0.5) / 255.0;
|
|
56
|
+
gl_FragColor = vec4(col, 1.0);
|
|
57
|
+
}`;
|
|
58
|
+
|
|
59
|
+
const toRgb = (hex) => {
|
|
60
|
+
const m = /^#?([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i.exec(String(hex).trim());
|
|
61
|
+
return m ? [parseInt(m[1], 16) / 255, parseInt(m[2], 16) / 255, parseInt(m[3], 16) / 255] : [0.4, 0.9, 0.6];
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const AuroraBackground = ({ children, colors = ["#22c55e", "#2dd4bf", "#38bdf8", "#c084fc"], theme = "dark", speed = 1, intensity = 1, className = "", containerClassName = "" }) => {
|
|
65
|
+
const box = useRef(null);
|
|
66
|
+
const live = useRef({});
|
|
67
|
+
live.current = { colors, theme, speed, intensity };
|
|
18
68
|
const dark = theme !== "light";
|
|
19
|
-
|
|
20
|
-
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
const el = box.current;
|
|
72
|
+
const cv = document.createElement("canvas");
|
|
73
|
+
Object.assign(cv.style, { position: "absolute", inset: "0", width: "100%", height: "100%", display: "block" });
|
|
74
|
+
el.prepend(cv);
|
|
75
|
+
const gl = cv.getContext("webgl", { antialias: false, alpha: false, premultipliedAlpha: false });
|
|
76
|
+
if (!gl) {
|
|
77
|
+
cv.remove();
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
const shader = (type, src) => {
|
|
81
|
+
const s = gl.createShader(type);
|
|
82
|
+
gl.shaderSource(s, src);
|
|
83
|
+
gl.compileShader(s);
|
|
84
|
+
return s;
|
|
85
|
+
};
|
|
86
|
+
const prog = gl.createProgram();
|
|
87
|
+
gl.attachShader(prog, shader(gl.VERTEX_SHADER, VERT));
|
|
88
|
+
gl.attachShader(prog, shader(gl.FRAGMENT_SHADER, FRAG));
|
|
89
|
+
gl.linkProgram(prog);
|
|
90
|
+
gl.useProgram(prog);
|
|
91
|
+
const buf = gl.createBuffer();
|
|
92
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
|
93
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
94
|
+
const loc = gl.getAttribLocation(prog, "p");
|
|
95
|
+
gl.enableVertexAttribArray(loc);
|
|
96
|
+
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
|
|
97
|
+
const U = (name) => gl.getUniformLocation(prog, name);
|
|
98
|
+
const u = { res: U("uRes"), time: U("uTime"), int: U("uIntensity"), light: U("uLight"), sky: U("uSky"), c: [U("uC0"), U("uC1"), U("uC2"), U("uC3")] };
|
|
99
|
+
|
|
100
|
+
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
101
|
+
let raf = 0;
|
|
102
|
+
let last = 0;
|
|
103
|
+
let time = 8;
|
|
104
|
+
let visible = true;
|
|
105
|
+
const scale = 0.75; // render a little under full size: the light is soft anyway
|
|
106
|
+
|
|
107
|
+
const fit = () => {
|
|
108
|
+
const r = el.getBoundingClientRect();
|
|
109
|
+
const dpr = Math.min(window.devicePixelRatio || 1, 1.5) * scale;
|
|
110
|
+
cv.width = Math.max(1, Math.round(r.width * dpr));
|
|
111
|
+
cv.height = Math.max(1, Math.round(r.height * dpr));
|
|
112
|
+
gl.viewport(0, 0, cv.width, cv.height);
|
|
113
|
+
};
|
|
114
|
+
const draw = (now) => {
|
|
115
|
+
const o = live.current;
|
|
116
|
+
const dt = last ? Math.min(0.05, (now - last) / 1000) : 0;
|
|
117
|
+
last = now;
|
|
118
|
+
time += dt * o.speed;
|
|
119
|
+
const cs = [0, 1, 2, 3].map((i) => toRgb(o.colors[Math.min(i, o.colors.length - 1)] ?? "#22c55e"));
|
|
120
|
+
const light = o.theme === "light";
|
|
121
|
+
gl.uniform2f(u.res, cv.width, cv.height);
|
|
122
|
+
gl.uniform1f(u.time, time);
|
|
123
|
+
gl.uniform1f(u.int, o.intensity);
|
|
124
|
+
gl.uniform1f(u.light, light ? 1 : 0);
|
|
125
|
+
gl.uniform3fv(u.sky, light ? [0.95, 0.97, 0.99] : [0.02, 0.03, 0.07]);
|
|
126
|
+
cs.forEach((c, i) => gl.uniform3fv(u.c[i], c));
|
|
127
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
128
|
+
};
|
|
129
|
+
const loop = (now) => {
|
|
130
|
+
raf = 0;
|
|
131
|
+
draw(now);
|
|
132
|
+
if (visible && !reduced) raf = requestAnimationFrame(loop);
|
|
133
|
+
};
|
|
134
|
+
const start = () => {
|
|
135
|
+
if (!raf && visible) {
|
|
136
|
+
last = 0;
|
|
137
|
+
raf = requestAnimationFrame(loop);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const ro = new ResizeObserver(() => {
|
|
141
|
+
fit();
|
|
142
|
+
start();
|
|
143
|
+
});
|
|
144
|
+
ro.observe(el);
|
|
145
|
+
const io = new IntersectionObserver(([entry]) => {
|
|
146
|
+
visible = entry.isIntersecting;
|
|
147
|
+
if (visible) start();
|
|
148
|
+
});
|
|
149
|
+
io.observe(el);
|
|
150
|
+
fit();
|
|
151
|
+
start();
|
|
152
|
+
return () => {
|
|
153
|
+
cancelAnimationFrame(raf);
|
|
154
|
+
ro.disconnect();
|
|
155
|
+
io.disconnect();
|
|
156
|
+
gl.deleteBuffer(buf);
|
|
157
|
+
gl.deleteProgram(prog);
|
|
158
|
+
gl.getExtension("WEBGL_lose_context")?.loseContext();
|
|
159
|
+
cv.remove();
|
|
160
|
+
};
|
|
161
|
+
}, []);
|
|
21
162
|
|
|
22
163
|
return (
|
|
23
|
-
<div className={`relative h-screen w-full overflow-hidden ${dark ? "bg-[#
|
|
24
|
-
<style>{`
|
|
25
|
-
@keyframes apex-aurora { from { background-position: 50% 50%, 50% 50%; } to { background-position: 350% 50%, 350% 50%; } }
|
|
26
|
-
@media (prefers-reduced-motion: reduce) { .apex-aurora, .apex-aurora::after { animation: none !important; } }
|
|
27
|
-
`}</style>
|
|
28
|
-
<div
|
|
29
|
-
aria-hidden="true"
|
|
30
|
-
className="apex-aurora pointer-events-none absolute -inset-[10px] will-change-transform after:absolute after:inset-0 after:content-['']"
|
|
31
|
-
style={{
|
|
32
|
-
opacity: Math.min(1, (dark ? 0.9 : 0.75) * intensity),
|
|
33
|
-
filter: `blur(${dark ? 12 : 10}px) ${dark ? "" : "invert(0)"}`,
|
|
34
|
-
backgroundImage: `repeating-linear-gradient(100deg, ${dark ? "#000" : "#fff"} 0%, ${dark ? "#000" : "#fff"} 7%, transparent 10%, transparent 12%, ${dark ? "#000" : "#fff"} 16%), repeating-linear-gradient(100deg, ${stops}, ${colors[0]} 100%)`,
|
|
35
|
-
backgroundSize: "300%, 200%",
|
|
36
|
-
backgroundPosition: "50% 50%, 50% 50%",
|
|
37
|
-
maskImage: "radial-gradient(ellipse 120% 95% at 70% 0%, black 25%, transparent 78%)",
|
|
38
|
-
WebkitMaskImage: "radial-gradient(ellipse 120% 95% at 70% 0%, black 25%, transparent 78%)",
|
|
39
|
-
animation: `apex-aurora ${duration} linear infinite`,
|
|
40
|
-
mixBlendMode: dark ? "normal" : "multiply",
|
|
41
|
-
}}
|
|
42
|
-
/>
|
|
164
|
+
<div ref={box} className={`relative h-screen w-full overflow-hidden ${dark ? "bg-[#05070f]" : "bg-[#f1f5f9]"} ${containerClassName}`}>
|
|
43
165
|
{children && <div className={`relative z-10 h-full w-full ${className}`}>{children}</div>}
|
|
44
166
|
</div>
|
|
45
167
|
);
|
|
@@ -1,145 +1,134 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
<div
|
|
136
|
-
className="absolute inset-0 z-10 pointer-events-none blur-3xl"
|
|
137
|
-
style={{
|
|
138
|
-
background: `radial-gradient(circle, ${circleColor} 0%, rgba(0, 0, 255, 0.1) 100%)`
|
|
139
|
-
}}
|
|
140
|
-
/>
|
|
141
|
-
</div >
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export default BluePrintBackground;
|
|
1
|
+
import React, { useEffect, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Blueprint Background: a drafting grid that drifts upward, with the cell
|
|
5
|
+
* under the pointer lighting up and fading out behind it like a trail. Drawn
|
|
6
|
+
* on one canvas (no per-cell elements), so it stays smooth on phones. Fills
|
|
7
|
+
* its nearest positioned parent; put content on top as children.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const BluePrintBackground = ({ color = "#9ae600", borderColor = "rgba(98, 116, 142, 0.22)", bgColor = "#1e293b", circleColor = "rgba(154, 230, 0, 0.2)", cellSize = 40, speed = 1, children, className = "" }) => {
|
|
11
|
+
const wrap = useRef(null);
|
|
12
|
+
const canvas = useRef(null);
|
|
13
|
+
const live = useRef({});
|
|
14
|
+
live.current = { color, borderColor, cellSize, speed };
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const el = wrap.current;
|
|
18
|
+
const cv = canvas.current;
|
|
19
|
+
const ctx = cv.getContext("2d");
|
|
20
|
+
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
21
|
+
const lit = new Map(); // "col,row" -> glow 0..1
|
|
22
|
+
const pointer = { x: -1, y: -1, in: false };
|
|
23
|
+
let w = 0;
|
|
24
|
+
let h = 0;
|
|
25
|
+
let dpr = 1;
|
|
26
|
+
let total = 0; // how far the grid has scrolled, in px
|
|
27
|
+
let raf = 0;
|
|
28
|
+
let last = 0;
|
|
29
|
+
let visible = true;
|
|
30
|
+
|
|
31
|
+
const fit = () => {
|
|
32
|
+
const r = el.getBoundingClientRect();
|
|
33
|
+
dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
34
|
+
w = r.width;
|
|
35
|
+
h = r.height;
|
|
36
|
+
cv.width = Math.round(w * dpr);
|
|
37
|
+
cv.height = Math.round(h * dpr);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const frame = (now) => {
|
|
41
|
+
raf = 0;
|
|
42
|
+
const o = live.current;
|
|
43
|
+
const size = Math.max(12, o.cellSize);
|
|
44
|
+
const dt = last ? Math.min(0.05, (now - last) / 1000) : 1 / 60;
|
|
45
|
+
last = now;
|
|
46
|
+
if (!reduced) total += dt * 24 * o.speed;
|
|
47
|
+
const offset = total % size;
|
|
48
|
+
|
|
49
|
+
// Light the cell under the pointer (in grid space, so it scrolls with the grid).
|
|
50
|
+
if (pointer.in) {
|
|
51
|
+
lit.set(`${Math.floor(pointer.x / size)},${Math.floor((pointer.y + total) / size)}`, 1);
|
|
52
|
+
}
|
|
53
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
54
|
+
ctx.clearRect(0, 0, w, h);
|
|
55
|
+
|
|
56
|
+
// Glowing cells, fading out.
|
|
57
|
+
ctx.shadowColor = o.color;
|
|
58
|
+
for (const [key, a] of lit) {
|
|
59
|
+
const [c, row] = key.split(",").map(Number);
|
|
60
|
+
const y = row * size - total;
|
|
61
|
+
if (a <= 0.02 || y < -size || y > h + size) {
|
|
62
|
+
lit.delete(key);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
ctx.globalAlpha = a;
|
|
66
|
+
ctx.shadowBlur = 14 * a;
|
|
67
|
+
ctx.fillStyle = o.color;
|
|
68
|
+
ctx.fillRect(c * size + 1, y + 1, size - 2, size - 2);
|
|
69
|
+
lit.set(key, a - dt * 1.6);
|
|
70
|
+
}
|
|
71
|
+
ctx.globalAlpha = 1;
|
|
72
|
+
ctx.shadowBlur = 0;
|
|
73
|
+
|
|
74
|
+
// Grid lines.
|
|
75
|
+
ctx.strokeStyle = o.borderColor;
|
|
76
|
+
ctx.lineWidth = 1;
|
|
77
|
+
ctx.beginPath();
|
|
78
|
+
for (let x = 0; x <= w; x += size) {
|
|
79
|
+
ctx.moveTo(Math.round(x) + 0.5, 0);
|
|
80
|
+
ctx.lineTo(Math.round(x) + 0.5, h);
|
|
81
|
+
}
|
|
82
|
+
for (let y = -offset; y <= h; y += size) {
|
|
83
|
+
ctx.moveTo(0, Math.round(y) + 0.5);
|
|
84
|
+
ctx.lineTo(w, Math.round(y) + 0.5);
|
|
85
|
+
}
|
|
86
|
+
ctx.stroke();
|
|
87
|
+
|
|
88
|
+
if (visible && (!reduced || lit.size)) raf = requestAnimationFrame(frame);
|
|
89
|
+
else last = 0;
|
|
90
|
+
};
|
|
91
|
+
const start = () => {
|
|
92
|
+
if (!raf && visible) raf = requestAnimationFrame(frame);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Listen on the window so content placed on top doesn't block the effect.
|
|
96
|
+
const onMove = (e) => {
|
|
97
|
+
const r = el.getBoundingClientRect();
|
|
98
|
+
pointer.x = e.clientX - r.left;
|
|
99
|
+
pointer.y = e.clientY - r.top;
|
|
100
|
+
pointer.in = pointer.x >= 0 && pointer.y >= 0 && pointer.x <= r.width && pointer.y <= r.height;
|
|
101
|
+
if (pointer.in) start();
|
|
102
|
+
};
|
|
103
|
+
window.addEventListener("pointermove", onMove, { passive: true });
|
|
104
|
+
const ro = new ResizeObserver(() => {
|
|
105
|
+
fit();
|
|
106
|
+
start();
|
|
107
|
+
});
|
|
108
|
+
ro.observe(el);
|
|
109
|
+
const io = new IntersectionObserver(([entry]) => {
|
|
110
|
+
visible = entry.isIntersecting;
|
|
111
|
+
if (visible) start();
|
|
112
|
+
});
|
|
113
|
+
io.observe(el);
|
|
114
|
+
fit();
|
|
115
|
+
start();
|
|
116
|
+
return () => {
|
|
117
|
+
cancelAnimationFrame(raf);
|
|
118
|
+
ro.disconnect();
|
|
119
|
+
io.disconnect();
|
|
120
|
+
window.removeEventListener("pointermove", onMove);
|
|
121
|
+
};
|
|
122
|
+
}, []);
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<div ref={wrap} className={`absolute inset-0 z-0 h-full w-full overflow-hidden ${className}`} style={{ backgroundColor: bgColor }}>
|
|
126
|
+
<canvas ref={canvas} aria-hidden="true" className="absolute inset-0 h-full w-full" />
|
|
127
|
+
{/* Soft spotlight in the middle. */}
|
|
128
|
+
<div aria-hidden="true" className="pointer-events-none absolute inset-0 blur-3xl" style={{ background: `radial-gradient(circle, ${circleColor} 0%, transparent 70%)` }} />
|
|
129
|
+
{children && <div className="relative z-10 h-full w-full">{children}</div>}
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export default BluePrintBackground;
|