apex-ui-kit 2.10.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/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/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/SmokeBackground/SmokeBackground.jsx +237 -235
- package/dist/cli/index.js +40 -20
- package/package.json +7 -1
|
@@ -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;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import React, { useEffect, useState } from "react";
|
|
1
|
+
import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
2
2
|
import { motion } from "framer-motion";
|
|
3
3
|
import { Code2, Layers, Sparkles, Zap } from "lucide-react";
|
|
4
4
|
|
|
5
5
|
/*
|
|
6
|
-
* Card Swap 3D: cards stacked
|
|
7
|
-
* timer the front card drops away and
|
|
8
|
-
* while the rest step forward. Pauses while hovered;
|
|
6
|
+
* Card Swap 3D: cards stacked in real 3D, turned toward the viewer and
|
|
7
|
+
* receding into the distance. On a timer the front card drops away and
|
|
8
|
+
* returns to the back while the rest step forward. Pauses while hovered;
|
|
9
|
+
* click to swap now. The whole stack scales down to fit narrow containers.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
const DEFAULT_CARDS = [
|
|
@@ -15,12 +16,19 @@ const DEFAULT_CARDS = [
|
|
|
15
16
|
{ icon: Zap, title: "Fast", body: "Transforms and opacity only, 60fps on phones." },
|
|
16
17
|
];
|
|
17
18
|
|
|
18
|
-
const CardSwap3D = ({ cards = DEFAULT_CARDS, delay = 3200, distanceX = 3.6, distanceY = 3.2, skew =
|
|
19
|
+
const CardSwap3D = ({ cards = DEFAULT_CARDS, delay = 3200, distanceX = 3.6, distanceY = 3.2, depth = 70, tilt = 18, skew = 0, pauseOnHover = true, accentColor = "#a3e635", theme = "dark", width = 18, className = "" }) => {
|
|
19
20
|
// order[0] is the card in front.
|
|
20
21
|
const [order, setOrder] = useState(() => cards.map((_, i) => i));
|
|
21
22
|
const [dropping, setDropping] = useState(null);
|
|
22
23
|
const [paused, setPaused] = useState(false);
|
|
24
|
+
const [fit, setFit] = useState(1);
|
|
25
|
+
const outer = useRef(null);
|
|
23
26
|
const dark = theme !== "light";
|
|
27
|
+
const n = cards.length;
|
|
28
|
+
// Footprint of the whole stack in rem (card plus the steps behind it).
|
|
29
|
+
// (+5: the 3D turn makes the cards project a little wider than they are.)
|
|
30
|
+
const footW = width + distanceX * (n - 1) + 5;
|
|
31
|
+
const footH = width * 0.68 + distanceY * (n - 1) + 2;
|
|
24
32
|
|
|
25
33
|
const swap = () => {
|
|
26
34
|
if (dropping !== null) return;
|
|
@@ -37,42 +45,58 @@ const CardSwap3D = ({ cards = DEFAULT_CARDS, delay = 3200, distanceX = 3.6, dist
|
|
|
37
45
|
return () => clearInterval(id);
|
|
38
46
|
});
|
|
39
47
|
|
|
48
|
+
// Scale the stack down when its container is narrower than its footprint.
|
|
49
|
+
useLayoutEffect(() => {
|
|
50
|
+
const el = outer.current?.parentElement;
|
|
51
|
+
if (!el) return undefined;
|
|
52
|
+
const ro = new ResizeObserver(() => {
|
|
53
|
+
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
54
|
+
setFit(Math.min(1, el.clientWidth / (footW * rem)));
|
|
55
|
+
});
|
|
56
|
+
ro.observe(el);
|
|
57
|
+
return () => ro.disconnect();
|
|
58
|
+
}, [footW]);
|
|
59
|
+
|
|
40
60
|
return (
|
|
41
|
-
<div
|
|
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
|
-
|
|
61
|
+
<div ref={outer} className={`relative mx-auto ${className}`} style={{ width: `${footW * fit}rem`, height: `${footH * fit}rem` }}>
|
|
62
|
+
<div
|
|
63
|
+
className="absolute left-0 top-0 origin-top-left [perspective:1200px]"
|
|
64
|
+
style={{ width: `${footW}rem`, height: `${footH}rem`, transform: `scale(${fit})` }}
|
|
65
|
+
onPointerEnter={() => pauseOnHover && setPaused(true)}
|
|
66
|
+
onPointerLeave={() => setPaused(false)}
|
|
67
|
+
onClick={swap}
|
|
68
|
+
>
|
|
69
|
+
<div className="absolute [transform-style:preserve-3d]" style={{ left: "2.5rem", bottom: "1rem", width: `${width}rem`, height: `${width * 0.68}rem`, transform: `rotateY(${-tilt}deg) rotateX(${tilt * 0.35}deg)` }}>
|
|
70
|
+
{cards.map((card, i) => {
|
|
71
|
+
const slot = order.indexOf(i);
|
|
72
|
+
const isDropping = dropping === i;
|
|
73
|
+
const Icon = card.icon ?? Sparkles;
|
|
74
|
+
return (
|
|
75
|
+
<motion.div
|
|
76
|
+
key={i}
|
|
77
|
+
className={`absolute inset-0 cursor-pointer overflow-hidden rounded-2xl border p-6 ${dark ? "border-white/10 bg-neutral-950 text-white" : "border-black/10 bg-white text-neutral-900"}`}
|
|
78
|
+
style={{ zIndex: isDropping ? 60 : n - slot, boxShadow: "0 30px 60px -28px rgba(0,0,0,0.65)" }}
|
|
79
|
+
initial={false}
|
|
80
|
+
animate={
|
|
81
|
+
isDropping
|
|
82
|
+
? { x: "-2rem", y: "16rem", z: 120, rotateZ: -8, skewY: skew, opacity: 0 }
|
|
83
|
+
: { x: `${slot * distanceX}rem`, y: `${-slot * distanceY}rem`, z: -slot * depth, rotateZ: 0, skewY: skew, opacity: slot > 4 ? 0 : 1 - slot * 0.12 }
|
|
84
|
+
}
|
|
85
|
+
transition={isDropping ? { duration: 0.5, ease: [0.55, 0, 0.8, 0.2] } : { type: "spring", stiffness: 150, damping: 21 }}
|
|
86
|
+
>
|
|
87
|
+
<div className="flex items-center gap-2.5">
|
|
88
|
+
<span className="grid h-9 w-9 place-items-center rounded-xl text-black" style={{ background: accentColor }}>
|
|
89
|
+
<Icon className="h-[18px] w-[18px]" />
|
|
90
|
+
</span>
|
|
91
|
+
<p className="text-lg font-semibold">{card.title}</p>
|
|
92
|
+
</div>
|
|
93
|
+
<p className={`mt-3 text-[0.92rem] leading-relaxed ${dark ? "text-white/60" : "text-neutral-500"}`}>{card.body}</p>
|
|
94
|
+
<div className="pointer-events-none absolute -bottom-10 -right-10 h-32 w-32 rounded-full opacity-20 blur-2xl" style={{ background: accentColor }} />
|
|
95
|
+
</motion.div>
|
|
96
|
+
);
|
|
97
|
+
})}
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
76
100
|
</div>
|
|
77
101
|
);
|
|
78
102
|
};
|