apex-ui-kit 2.9.0 → 2.11.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 +7 -4
- 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/FluidCursor/FluidCursor.jsx +418 -0
- package/dist/ApexUI-Kit/templates/Folder/Folder.jsx +58 -36
- package/dist/ApexUI-Kit/templates/ImageAccordion/ImageAccordion.jsx +8 -6
- 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/OrbitIcons/OrbitIcons.jsx +1 -1
- package/dist/ApexUI-Kit/templates/ParallaxCard/ParallaxCard.jsx +34 -33
- package/dist/ApexUI-Kit/templates/SketchReveal/SketchReveal.jsx +257 -0
- package/dist/ApexUI-Kit/templates/SmokeBackground/SmokeBackground.jsx +237 -235
- package/dist/cli/index.js +42 -20
- package/package.json +8 -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
|
+
**118 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
|
+
- **118 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.
|
|
@@ -196,7 +196,7 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
196
196
|
| [Magnetic Button](https://apex-ui.in/components/magnetic-button) | `magnetic-button` |
|
|
197
197
|
| [Star Border](https://apex-ui.in/components/star-border) | `star-border` |
|
|
198
198
|
|
|
199
|
-
### Interactive (
|
|
199
|
+
### Interactive (33)
|
|
200
200
|
|
|
201
201
|
| Component | CLI name |
|
|
202
202
|
| --- | --- |
|
|
@@ -215,6 +215,7 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
215
215
|
| [Elastic Slider](https://apex-ui.in/components/elastic-slider) | `elastic-slider` |
|
|
216
216
|
| [FAQ Accordion](https://apex-ui.in/components/faq-accordion) | `faq-accordion` |
|
|
217
217
|
| [Flip Book](https://apex-ui.in/components/flip-book) | `flip-book` |
|
|
218
|
+
| [Fluid Cursor](https://apex-ui.in/components/fluid-cursor) | `fluid-cursor` |
|
|
218
219
|
| [Glass Lens](https://apex-ui.in/components/glass-lens) | `glass-lens` |
|
|
219
220
|
| [Image Compare](https://apex-ui.in/components/image-compare) | `image-compare` |
|
|
220
221
|
| [Infinite Canvas](https://apex-ui.in/components/infinite-canvas) | `infinite-canvas` |
|
|
@@ -227,6 +228,7 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
227
228
|
| [Particle Morph](https://apex-ui.in/components/particle-morph) | `particle-morph` |
|
|
228
229
|
| [Pointer Follower](https://apex-ui.in/components/pointer-follower) | `pointer-follower` |
|
|
229
230
|
| [Receipt Printer](https://apex-ui.in/components/receipt-printer) | `receipt-printer` |
|
|
231
|
+
| [Sketch Reveal](https://apex-ui.in/components/sketch-reveal) | `sketch-reveal` |
|
|
230
232
|
| [Stepper](https://apex-ui.in/components/stepper) | `stepper` |
|
|
231
233
|
| [Toast Stack](https://apex-ui.in/components/toast-stack) | `toast-stack` |
|
|
232
234
|
| [ToolTip](https://apex-ui.in/components/tool-tip) | `tooltip` |
|
|
@@ -260,8 +262,9 @@ Every component's props, variants and copy-ready code are on its docs page.
|
|
|
260
262
|
| [Dot Globe](https://apex-ui.in/components/dot-globe) | `dot-globe` |
|
|
261
263
|
| [World Map](https://apex-ui.in/components/world-map) | `world-map` |
|
|
262
264
|
|
|
263
|
-
## What's new in 2.
|
|
265
|
+
## What's new in 2.10
|
|
264
266
|
|
|
267
|
+
- New in 2.10: Fluid Cursor (a real-time WebGL fluid sim that trails the cursor) and Sketch Reveal (a photo turns into a glowing sketch and burns away into the next one as you scroll).
|
|
265
268
|
- New in 2.9: Liquid Glass (iOS style WebGL glass), 10 live page templates on the site, Arc Gallery auto scroll, Scroll Stack on the page scroll, Sunrise trees and birds.
|
|
266
269
|
- New in 2.8: Glass Lens (WebGL liquid glass), Dither Waves (WebGL 1-bit background), Infinite Canvas, Image Accordion, Orbit Icons, Command Palette, Crossed Marquee (tech-stack bands with simple-icons) and Theme Switcher (8 themes with a circle reveal).
|
|
267
270
|
- Toast Stack and Radial Menu no longer blink in production builds; Radial Menu, 3D Text Ring, Animated Tabs and Gooey Nav now fit 320px phones.
|
|
@@ -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;
|