apex-ui-kit 2.1.2 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ApexUI-Kit/templates/AnimatedButton/AnimatedButton.jsx +251 -0
- package/dist/ApexUI-Kit/templates/AnimatedTabs/AnimatedTabs.jsx +99 -0
- package/dist/ApexUI-Kit/templates/BlurRevealText/BlurRevealText.jsx +60 -0
- package/dist/ApexUI-Kit/templates/Carousel3D/Carousel3D.jsx +222 -0
- package/dist/ApexUI-Kit/templates/DotGlobe/DotGlobe.jsx +289 -0
- package/dist/ApexUI-Kit/templates/DotWaveBackground/DotWaveBackground.jsx +189 -0
- package/dist/ApexUI-Kit/templates/DynamicIsland/DynamicIsland.jsx +221 -0
- package/dist/ApexUI-Kit/templates/FaqAccordion/FaqAccordion.jsx +88 -0
- package/dist/ApexUI-Kit/templates/FlipClock/FlipClock.jsx +121 -0
- package/dist/ApexUI-Kit/templates/LiquidGradient/LiquidGradient.jsx +238 -0
- package/dist/ApexUI-Kit/templates/LoadingScreen/LoadingScreen.jsx +361 -0
- package/dist/ApexUI-Kit/templates/MagneticCursor/MagneticCursor.jsx +306 -0
- package/dist/ApexUI-Kit/templates/MagnifyDock/MagnifyDock.jsx +154 -0
- package/dist/ApexUI-Kit/templates/ProfileCard/ProfileCard.jsx +213 -0
- package/dist/ApexUI-Kit/templates/RollingCounter/RollingCounter.jsx +143 -0
- package/dist/ApexUI-Kit/templates/ScrambleText/ScrambleText.jsx +132 -0
- package/dist/ApexUI-Kit/templates/SidebarNav/SidebarNav.jsx +220 -0
- package/dist/ApexUI-Kit/templates/SpotlightGrid/SpotlightGrid.jsx +110 -0
- package/dist/ApexUI-Kit/templates/ToastStack/ToastStack.jsx +108 -0
- package/dist/ApexUI-Kit/templates/ToolTip/ToolTip.jsx +5 -3
- package/dist/ApexUI-Kit/templates/WebGLCarousel/WebGLCarousel.jsx +326 -0
- package/dist/ApexUI-Kit/templates/WorldMap/WorldMap.jsx +359 -0
- package/dist/cli/index.js +25 -0
- package/package.json +1 -1
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Dot Globe: a 3D globe of land dots drawn on a 2D canvas (no WebGL, no map
|
|
5
|
+
* library). Drag to spin it with inertia; it also turns on its own. Cities
|
|
6
|
+
* pulse as markers and flights arc between them.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// 2° land mask (Natural Earth 110m): 180 × 69 cells from 180°W, 78°N, one bit each.
|
|
10
|
+
const LAND = "AAAAwADkw////wEABAAAAABgAAAAAAAAAADAUT8A/v8PAAAAAAwA/j8AuAEAAAAAcBHtDcD/fwAAAAAwAPz/fwMAABCAAQD5w/wD8P8FAAAcAIL7//9//xOAAP/ff0668QD/HwAA+A8At///////v3/w/////x8++B8AAOD/1//7////////jP////+/+AE/gAcAn9f//////////w/w/////wEs4AEAAHz+////////////gL////8HeAAcAADg5///////////9ADgAf7/f4AnAAAAAH78////////H0QAAAiA//8f8AcAAIBB4////////38ADwAQAOD//5//AQAAHAT/////////A3AAAAAA/v//+T8AAGDz//////////8DAQAAAMD/////AwAAsP//////////LwAAAAAA6P///2IAAAD+//////////8CAAAAAAD///8/CAAA4P//////////JwAAAAAA8P///wYAAAD+/unz/////z8AAAAAAAD///8HAAAA/pgPPP//////MQAAAAAA8P//PwAAAMBD9v7n/////wcBAAAAAAD///8AAAAAPkD7f/7///8hEAAAAAAA4P//DwAAAIDhAv/n////f8YAAAAAAAD8//8AAAAA+AdE//////8jDwAAAAAAgP//AwAAAMD/APD/////PxgAAAAAAADw/x8AAAAA/n/v//////8HAAAAAAAAAPwDAgAAAOD////7////fwAAAAAAAACgHyAAAACA//9/f/7///8DAAAAAAAAAPQBAAAAAPj//+cv+P//PwAAAAAAAAAAHjAAAADA/////g/+//8EAAAAAAAAAOBhCAAAAP7//99/4D//AAAAAAAAAAAAPAMEAADA////+Qf84BcAAAAAAAAAAAA/AAAAAPz//58fgAf+QAAAAAAAAAAAAA8AAADg////ewA4gA8EAAAAAAAAAADAAAAAAPz//38BgAP4QQAAAAAAAAAAAAgPAADA////zwAwgAwQAAAAAAAAAAAA9Q8AAPj///8HAAVIAAAAAAAAAAAAAID/AQAA////fwBAAAAQAAAAAAAAAAAA+P8AAGDh//8DAAA0GAAAAAAAAAAAAID/HwAAAPj/HwAAgMIBAAAAAAAAAAAA/P8BAACA//8AAAAYXgAAAAAAAAAAAMD/fwAAAPz/BwAAAOOBAQAAAAAAAAAA/P8/AACA/z8AAABgbtQBAAAAAAAAAOD//w8AAPD/AwAAAAQIeAAAAAAAAAAA/P//AQAA/z8AAACAA4APAQAAAAAAAID//w8AAPD/AwAAAAARsEAAAAAAAAAA+P9/AAAA/j8AAAAAAAAAAAAAAAAAAAD//wcAAPD/QwAAAACAIwAAAAAAAAAA8P9/AAAA/z8EAAAAAD8GIAAAAAAAAAD8/wMAAPD/cQAAAAD4ZwAAAAAAAAAAgP8/AAAA/w8HAAAAgP8HAAAAAAAAAAD4/wMAAOD/MAAAAAD//wEBAAAAAAAAgP8PAAAA/g8DAAAA+P8fAAAAAAAAAAD4PwAAAOB/EAAAAID//wMAAAAAAAAAgP8DAAAA/AMAAAAA+P9/AAAAAAAAAAD8HwAAAMA/AAAAAID//wcAAAAAAAAAwP8BAAAA+AEAAAAA8P9/AAAAAAAAAAD8DwAAAIAPAAAAAAAP/gMAAAAAAAAAwB8AAAAAAAAAAAAAEIAfAAEAAAAAAAD+AwAAAAAAAAAAAAAA8AEgAAAAAAAA4AcAAAAAAAAAAAAAAAAAAAYAAAAAAABeAAAAAAAAAAAAAAAAwAAwAAAAAAAAwAMAAAAAAAAAAAAAAAAIgAEAAAAAAAAeAAAAAAAAAAAAAAAAAAAMAAAAAAAA4AEAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAABAAAAAAAAAAAAAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
|
|
11
|
+
const STEP = 2;
|
|
12
|
+
const COLS = 180;
|
|
13
|
+
const ROWS = 69;
|
|
14
|
+
|
|
15
|
+
const landDots = (() => {
|
|
16
|
+
const bin = typeof atob === "function" ? atob(LAND) : "";
|
|
17
|
+
const dots = [];
|
|
18
|
+
for (let r = 0; r < ROWS; r++) {
|
|
19
|
+
const lat = 78 - r * STEP - 1;
|
|
20
|
+
// Fewer dots toward the poles so the globe looks evenly filled.
|
|
21
|
+
const skip = Math.max(1, Math.round(1 / Math.max(0.25, Math.cos((lat * Math.PI) / 180))));
|
|
22
|
+
for (let c = 0; c < COLS; c += skip) {
|
|
23
|
+
const i = r * COLS + c;
|
|
24
|
+
if ((bin.charCodeAt(i >> 3) >> (i & 7)) & 1) dots.push([lat, -180 + c * STEP + 1]);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return dots;
|
|
28
|
+
})();
|
|
29
|
+
|
|
30
|
+
const DEFAULT_MARKERS = [
|
|
31
|
+
{ label: "Bhubaneswar", lat: 20.3, lon: 85.8 },
|
|
32
|
+
{ label: "San Francisco", lat: 37.8, lon: -122.4 },
|
|
33
|
+
{ label: "London", lat: 51.5, lon: -0.1 },
|
|
34
|
+
{ label: "Tokyo", lat: 35.7, lon: 139.7 },
|
|
35
|
+
{ label: "Sydney", lat: -33.9, lon: 151.2 },
|
|
36
|
+
{ label: "São Paulo", lat: -23.5, lon: -46.6 },
|
|
37
|
+
{ label: "Dubai", lat: 25.2, lon: 55.3 },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const DEFAULT_ARCS = [
|
|
41
|
+
[0, 1],
|
|
42
|
+
[0, 2],
|
|
43
|
+
[0, 3],
|
|
44
|
+
[0, 4],
|
|
45
|
+
[2, 5],
|
|
46
|
+
[6, 2],
|
|
47
|
+
[3, 1],
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const rad = (d) => (d * Math.PI) / 180;
|
|
51
|
+
const toVec = (lat, lon) => [Math.cos(rad(lat)) * Math.sin(rad(lon)), Math.sin(rad(lat)), Math.cos(rad(lat)) * Math.cos(rad(lon))];
|
|
52
|
+
|
|
53
|
+
// Spherical interpolation between two unit vectors.
|
|
54
|
+
const slerp = (a, b, t) => {
|
|
55
|
+
const dot = Math.min(1, Math.max(-1, a[0] * b[0] + a[1] * b[1] + a[2] * b[2]));
|
|
56
|
+
const om = Math.acos(dot);
|
|
57
|
+
if (om < 1e-4) return a;
|
|
58
|
+
const s = Math.sin(om);
|
|
59
|
+
const k1 = Math.sin((1 - t) * om) / s;
|
|
60
|
+
const k2 = Math.sin(t * om) / s;
|
|
61
|
+
return [a[0] * k1 + b[0] * k2, a[1] * k1 + b[1] * k2, a[2] * k1 + b[2] * k2];
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const DotGlobe = ({
|
|
65
|
+
markers = DEFAULT_MARKERS,
|
|
66
|
+
arcs = DEFAULT_ARCS,
|
|
67
|
+
dotColor = "#a1a1aa",
|
|
68
|
+
markerColor = "#a3e635",
|
|
69
|
+
arcColor = "#a3e635",
|
|
70
|
+
glowColor = "#a3e635",
|
|
71
|
+
labelColor = "rgba(255,255,255,0.9)",
|
|
72
|
+
autoRotateSpeed = 8,
|
|
73
|
+
tilt = 18,
|
|
74
|
+
maxSize = 30,
|
|
75
|
+
showLabels = true,
|
|
76
|
+
className = "",
|
|
77
|
+
}) => {
|
|
78
|
+
const wrapRef = useRef(null);
|
|
79
|
+
const canvasRef = useRef(null);
|
|
80
|
+
const opts = useRef(null);
|
|
81
|
+
opts.current = { markers, arcs, dotColor, markerColor, arcColor, glowColor, labelColor, autoRotateSpeed, tilt, showLabels };
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
const canvas = canvasRef.current;
|
|
85
|
+
const wrap = wrapRef.current;
|
|
86
|
+
const ctx = canvas.getContext("2d");
|
|
87
|
+
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
88
|
+
let size = 300;
|
|
89
|
+
let raf = 0;
|
|
90
|
+
let last = performance.now();
|
|
91
|
+
let time = 0;
|
|
92
|
+
let yaw = -80; // start over India
|
|
93
|
+
let velocity = 0;
|
|
94
|
+
let drag = null;
|
|
95
|
+
let visible = true;
|
|
96
|
+
|
|
97
|
+
const resize = () => {
|
|
98
|
+
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
99
|
+
size = wrap.getBoundingClientRect().width;
|
|
100
|
+
canvas.width = Math.round(size * dpr);
|
|
101
|
+
canvas.height = Math.round(size * dpr);
|
|
102
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
103
|
+
draw();
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Rotate a unit vector by the current yaw and tilt, then project.
|
|
107
|
+
const project = (v, R, c) => {
|
|
108
|
+
const cy = Math.cos(rad(yaw));
|
|
109
|
+
const sy = Math.sin(rad(yaw));
|
|
110
|
+
const x = v[0] * cy + v[2] * sy;
|
|
111
|
+
const z0 = -v[0] * sy + v[2] * cy;
|
|
112
|
+
const ct = Math.cos(rad(opts.current.tilt));
|
|
113
|
+
const st = Math.sin(rad(opts.current.tilt));
|
|
114
|
+
const y = v[1] * ct - z0 * st;
|
|
115
|
+
const z = v[1] * st + z0 * ct;
|
|
116
|
+
return [c + x * R, c - y * R, z];
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
function draw() {
|
|
120
|
+
const o = opts.current;
|
|
121
|
+
const c = size / 2;
|
|
122
|
+
const R = size * 0.4;
|
|
123
|
+
ctx.clearRect(0, 0, size, size);
|
|
124
|
+
|
|
125
|
+
// Atmosphere glow and sphere body.
|
|
126
|
+
const glow = ctx.createRadialGradient(c, c, R * 0.85, c, c, R * 1.25);
|
|
127
|
+
glow.addColorStop(0, `${o.glowColor}33`);
|
|
128
|
+
glow.addColorStop(1, `${o.glowColor}00`);
|
|
129
|
+
ctx.fillStyle = glow;
|
|
130
|
+
ctx.beginPath();
|
|
131
|
+
ctx.arc(c, c, R * 1.25, 0, Math.PI * 2);
|
|
132
|
+
ctx.fill();
|
|
133
|
+
const body = ctx.createRadialGradient(c - R * 0.35, c - R * 0.35, R * 0.1, c, c, R);
|
|
134
|
+
body.addColorStop(0, "rgba(255,255,255,0.07)");
|
|
135
|
+
body.addColorStop(1, "rgba(255,255,255,0.01)");
|
|
136
|
+
ctx.fillStyle = body;
|
|
137
|
+
ctx.beginPath();
|
|
138
|
+
ctx.arc(c, c, R, 0, Math.PI * 2);
|
|
139
|
+
ctx.fill();
|
|
140
|
+
|
|
141
|
+
// Land dots: front bright, back faint.
|
|
142
|
+
const dr = Math.max(0.8, R / 190);
|
|
143
|
+
ctx.fillStyle = o.dotColor;
|
|
144
|
+
for (let i = 0; i < landDots.length; i++) {
|
|
145
|
+
const [lat, lon] = landDots[i];
|
|
146
|
+
const [x, y, z] = project(toVec(lat, lon), R, c);
|
|
147
|
+
ctx.globalAlpha = z > 0 ? 0.35 + 0.65 * z : 0.06;
|
|
148
|
+
ctx.fillRect(x - dr, y - dr, dr * 2, dr * 2);
|
|
149
|
+
}
|
|
150
|
+
ctx.globalAlpha = 1;
|
|
151
|
+
|
|
152
|
+
// Arcs with a travelling head.
|
|
153
|
+
o.arcs.forEach(([a, b], k) => {
|
|
154
|
+
const A = o.markers[a];
|
|
155
|
+
const B = o.markers[b];
|
|
156
|
+
if (!A || !B) return;
|
|
157
|
+
const va = toVec(A.lat, A.lon);
|
|
158
|
+
const vb = toVec(B.lat, B.lon);
|
|
159
|
+
const steps = 48;
|
|
160
|
+
ctx.strokeStyle = o.arcColor;
|
|
161
|
+
ctx.lineWidth = Math.max(1, R / 220);
|
|
162
|
+
ctx.globalAlpha = 0.55;
|
|
163
|
+
ctx.beginPath();
|
|
164
|
+
let drawing = false;
|
|
165
|
+
for (let s = 0; s <= steps; s++) {
|
|
166
|
+
const t = s / steps;
|
|
167
|
+
const v = slerp(va, vb, t);
|
|
168
|
+
const lift = 1 + 0.18 * Math.sin(Math.PI * t);
|
|
169
|
+
const [x, y, z] = project([v[0] * lift, v[1] * lift, v[2] * lift], R, c);
|
|
170
|
+
if (z > -0.1) {
|
|
171
|
+
if (drawing) ctx.lineTo(x, y);
|
|
172
|
+
else ctx.moveTo(x, y);
|
|
173
|
+
drawing = true;
|
|
174
|
+
} else drawing = false;
|
|
175
|
+
}
|
|
176
|
+
ctx.stroke();
|
|
177
|
+
const t = ((time * 0.35 + k * 0.17) % 1 + 1) % 1;
|
|
178
|
+
const v = slerp(va, vb, t);
|
|
179
|
+
const lift = 1 + 0.18 * Math.sin(Math.PI * t);
|
|
180
|
+
const [hx, hy, hz] = project([v[0] * lift, v[1] * lift, v[2] * lift], R, c);
|
|
181
|
+
if (hz > 0) {
|
|
182
|
+
ctx.globalAlpha = 1;
|
|
183
|
+
ctx.fillStyle = o.arcColor;
|
|
184
|
+
ctx.beginPath();
|
|
185
|
+
ctx.arc(hx, hy, Math.max(1.5, R / 110), 0, Math.PI * 2);
|
|
186
|
+
ctx.fill();
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
ctx.globalAlpha = 1;
|
|
190
|
+
|
|
191
|
+
// Markers with a pulse and labels.
|
|
192
|
+
ctx.font = `600 ${Math.max(10, R / 18)}px ui-sans-serif, system-ui, sans-serif`;
|
|
193
|
+
o.markers.forEach((m, k) => {
|
|
194
|
+
const [x, y, z] = project(toVec(m.lat, m.lon), R, c);
|
|
195
|
+
if (z <= 0.05) return;
|
|
196
|
+
const pulse = ((time * 0.8 + k * 0.23) % 1 + 1) % 1;
|
|
197
|
+
ctx.strokeStyle = o.markerColor;
|
|
198
|
+
ctx.globalAlpha = (1 - pulse) * z;
|
|
199
|
+
ctx.lineWidth = 1.5;
|
|
200
|
+
ctx.beginPath();
|
|
201
|
+
ctx.arc(x, y, 3 + pulse * R * 0.06, 0, Math.PI * 2);
|
|
202
|
+
ctx.stroke();
|
|
203
|
+
ctx.globalAlpha = z;
|
|
204
|
+
ctx.fillStyle = o.markerColor;
|
|
205
|
+
ctx.beginPath();
|
|
206
|
+
ctx.arc(x, y, Math.max(2.5, R / 80), 0, Math.PI * 2);
|
|
207
|
+
ctx.fill();
|
|
208
|
+
if (o.showLabels && z > 0.35) {
|
|
209
|
+
ctx.globalAlpha = Math.min(1, (z - 0.35) * 3);
|
|
210
|
+
ctx.fillStyle = o.labelColor;
|
|
211
|
+
ctx.fillText(m.label, x + 8, y + 4);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
ctx.globalAlpha = 1;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const loop = (now) => {
|
|
218
|
+
const dt = Math.min(0.05, (now - last) / 1000);
|
|
219
|
+
last = now;
|
|
220
|
+
time += dt;
|
|
221
|
+
if (!drag) {
|
|
222
|
+
velocity *= 0.94;
|
|
223
|
+
yaw += velocity * dt * 60 + (reduced ? 0 : opts.current.autoRotateSpeed * dt) * (Math.abs(velocity) < 0.05 ? 1 : 0.3);
|
|
224
|
+
}
|
|
225
|
+
draw();
|
|
226
|
+
raf = requestAnimationFrame(loop);
|
|
227
|
+
};
|
|
228
|
+
const start = () => {
|
|
229
|
+
if (raf || !visible) return;
|
|
230
|
+
last = performance.now();
|
|
231
|
+
raf = requestAnimationFrame(loop);
|
|
232
|
+
};
|
|
233
|
+
const stop = () => {
|
|
234
|
+
cancelAnimationFrame(raf);
|
|
235
|
+
raf = 0;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const onDown = (e) => {
|
|
239
|
+
drag = { x: e.clientX, yaw, lastX: e.clientX, lastT: performance.now() };
|
|
240
|
+
canvas.setPointerCapture?.(e.pointerId);
|
|
241
|
+
};
|
|
242
|
+
const onMove = (e) => {
|
|
243
|
+
if (!drag) return;
|
|
244
|
+
const degPerPx = 180 / (size * 0.8);
|
|
245
|
+
// Drag right → the surface under the pointer moves right.
|
|
246
|
+
yaw = drag.yaw + (e.clientX - drag.x) * degPerPx;
|
|
247
|
+
const now = performance.now();
|
|
248
|
+
velocity = ((e.clientX - drag.lastX) * degPerPx) / Math.max(1, (now - drag.lastT) / 16.7);
|
|
249
|
+
drag.lastX = e.clientX;
|
|
250
|
+
drag.lastT = now;
|
|
251
|
+
};
|
|
252
|
+
const onUp = () => {
|
|
253
|
+
drag = null;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
canvas.addEventListener("pointerdown", onDown);
|
|
257
|
+
canvas.addEventListener("pointermove", onMove);
|
|
258
|
+
canvas.addEventListener("pointerup", onUp);
|
|
259
|
+
canvas.addEventListener("pointercancel", onUp);
|
|
260
|
+
const ro = new ResizeObserver(resize);
|
|
261
|
+
ro.observe(wrap);
|
|
262
|
+
const io = new IntersectionObserver(([entry]) => {
|
|
263
|
+
visible = entry.isIntersecting;
|
|
264
|
+
if (visible) start();
|
|
265
|
+
else stop();
|
|
266
|
+
});
|
|
267
|
+
io.observe(wrap);
|
|
268
|
+
resize();
|
|
269
|
+
start();
|
|
270
|
+
|
|
271
|
+
return () => {
|
|
272
|
+
stop();
|
|
273
|
+
ro.disconnect();
|
|
274
|
+
io.disconnect();
|
|
275
|
+
canvas.removeEventListener("pointerdown", onDown);
|
|
276
|
+
canvas.removeEventListener("pointermove", onMove);
|
|
277
|
+
canvas.removeEventListener("pointerup", onUp);
|
|
278
|
+
canvas.removeEventListener("pointercancel", onUp);
|
|
279
|
+
};
|
|
280
|
+
}, []);
|
|
281
|
+
|
|
282
|
+
return (
|
|
283
|
+
<div ref={wrapRef} className={`relative aspect-square w-full ${className}`} style={{ maxWidth: `${maxSize}rem` }}>
|
|
284
|
+
<canvas ref={canvasRef} role="img" aria-label="Interactive globe" className="h-full w-full cursor-grab touch-pan-y active:cursor-grabbing" />
|
|
285
|
+
</div>
|
|
286
|
+
);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
export default DotGlobe;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Dot Wave Background: a grid of dots on a canvas with a slow wave of color
|
|
5
|
+
* rolling through it. Dots near the pointer swell, light up and lean away.
|
|
6
|
+
* Pauses while off screen and stays still for reduced-motion users.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const hexToRgb = (hex) => {
|
|
10
|
+
let h = String(hex).replace("#", "");
|
|
11
|
+
if (h.length === 3) h = h.split("").map((c) => c + c).join("");
|
|
12
|
+
const n = parseInt(h, 16) || 0;
|
|
13
|
+
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const POINTER_RADIUS = 150;
|
|
17
|
+
|
|
18
|
+
const DotWaveBackground = ({
|
|
19
|
+
children,
|
|
20
|
+
className = "",
|
|
21
|
+
containerClassName = "",
|
|
22
|
+
background = "#09090b",
|
|
23
|
+
dotColor = "#ffffff",
|
|
24
|
+
colors = ["#38bdf8", "#a78bfa"],
|
|
25
|
+
gap = 22,
|
|
26
|
+
dotSize = 1.2,
|
|
27
|
+
waveStrength = 1,
|
|
28
|
+
speed = 1,
|
|
29
|
+
interactive = true,
|
|
30
|
+
fade = true,
|
|
31
|
+
}) => {
|
|
32
|
+
const containerRef = useRef(null);
|
|
33
|
+
const canvasRef = useRef(null);
|
|
34
|
+
const pointer = useRef({ x: -9999, y: -9999, active: false, strength: 0 });
|
|
35
|
+
const redraw = useRef(() => {});
|
|
36
|
+
const settings = useRef(null);
|
|
37
|
+
settings.current = { dotColor, colors, gap, dotSize, waveStrength, speed, interactive };
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const canvas = canvasRef.current;
|
|
41
|
+
const container = containerRef.current;
|
|
42
|
+
const ctx = canvas.getContext("2d");
|
|
43
|
+
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
44
|
+
let width = 0;
|
|
45
|
+
let height = 0;
|
|
46
|
+
let raf = 0;
|
|
47
|
+
let t = 0;
|
|
48
|
+
let last = performance.now();
|
|
49
|
+
let visible = true;
|
|
50
|
+
|
|
51
|
+
const draw = () => {
|
|
52
|
+
const s = settings.current;
|
|
53
|
+
const base = hexToRgb(s.dotColor);
|
|
54
|
+
const ca = hexToRgb(s.colors[0]);
|
|
55
|
+
const cb = hexToRgb(s.colors[1] ?? s.colors[0]);
|
|
56
|
+
const p = pointer.current;
|
|
57
|
+
p.strength += ((p.active && s.interactive && !reduced ? 1 : 0) - p.strength) * 0.08;
|
|
58
|
+
|
|
59
|
+
ctx.clearRect(0, 0, width, height);
|
|
60
|
+
const g = Math.max(8, s.gap);
|
|
61
|
+
const cols = Math.ceil(width / g) + 1;
|
|
62
|
+
const rows = Math.ceil(height / g) + 1;
|
|
63
|
+
const ox = (width - (cols - 1) * g) / 2;
|
|
64
|
+
const oy = (height - (rows - 1) * g) / 2;
|
|
65
|
+
|
|
66
|
+
for (let i = 0; i < cols; i++) {
|
|
67
|
+
const x0 = ox + i * g;
|
|
68
|
+
// Highlight color drifts from the first to the second color left to right.
|
|
69
|
+
const m = width ? Math.min(1, Math.max(0, x0 / width)) : 0;
|
|
70
|
+
const hr = ca[0] + (cb[0] - ca[0]) * m;
|
|
71
|
+
const hg = ca[1] + (cb[1] - ca[1]) * m;
|
|
72
|
+
const hb = ca[2] + (cb[2] - ca[2]) * m;
|
|
73
|
+
|
|
74
|
+
for (let j = 0; j < rows; j++) {
|
|
75
|
+
const y0 = oy + j * g;
|
|
76
|
+
// A diagonal band plus a ripple spreading from the centre.
|
|
77
|
+
const ripple = Math.hypot(x0 - width / 2, y0 - height / 2);
|
|
78
|
+
const wave = 0.55 * Math.sin(x0 * 0.009 + y0 * 0.005 - t * 1.3) + 0.45 * Math.sin(ripple * 0.018 - t * 1.6);
|
|
79
|
+
const lift = Math.min(1, Math.max(0, (wave + 0.3) / 0.95));
|
|
80
|
+
const v = Math.min(1, lift * lift * s.waveStrength);
|
|
81
|
+
|
|
82
|
+
let x = x0;
|
|
83
|
+
let y = y0;
|
|
84
|
+
let boost = 0;
|
|
85
|
+
if (p.strength > 0.01) {
|
|
86
|
+
const dx = x0 - p.x;
|
|
87
|
+
const dy = y0 - p.y;
|
|
88
|
+
const d = Math.hypot(dx, dy);
|
|
89
|
+
if (d < POINTER_RADIUS) {
|
|
90
|
+
boost = (1 - d / POINTER_RADIUS) ** 2 * p.strength;
|
|
91
|
+
const push = (boost * 9) / (d || 1);
|
|
92
|
+
x += dx * push;
|
|
93
|
+
y += dy * push;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const k = Math.min(1, v + boost);
|
|
98
|
+
const r = s.dotSize * (1 + v * 1.9 + boost * 1.8);
|
|
99
|
+
ctx.fillStyle = `rgba(${Math.round(base[0] + (hr - base[0]) * k)},${Math.round(base[1] + (hg - base[1]) * k)},${Math.round(base[2] + (hb - base[2]) * k)},${0.13 + k * 0.87})`;
|
|
100
|
+
ctx.beginPath();
|
|
101
|
+
ctx.arc(x, y, r, 0, Math.PI * 2);
|
|
102
|
+
ctx.fill();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
redraw.current = draw;
|
|
107
|
+
|
|
108
|
+
const loop = (now) => {
|
|
109
|
+
const dt = Math.min(0.05, (now - last) / 1000);
|
|
110
|
+
last = now;
|
|
111
|
+
t += dt * settings.current.speed;
|
|
112
|
+
draw();
|
|
113
|
+
raf = requestAnimationFrame(loop);
|
|
114
|
+
};
|
|
115
|
+
const start = () => {
|
|
116
|
+
if (reduced || raf || !visible) return;
|
|
117
|
+
last = performance.now();
|
|
118
|
+
raf = requestAnimationFrame(loop);
|
|
119
|
+
};
|
|
120
|
+
const stop = () => {
|
|
121
|
+
cancelAnimationFrame(raf);
|
|
122
|
+
raf = 0;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const resize = () => {
|
|
126
|
+
const rect = container.getBoundingClientRect();
|
|
127
|
+
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
128
|
+
width = rect.width;
|
|
129
|
+
height = rect.height;
|
|
130
|
+
canvas.width = Math.round(width * dpr);
|
|
131
|
+
canvas.height = Math.round(height * dpr);
|
|
132
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
133
|
+
draw();
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const ro = new ResizeObserver(resize);
|
|
137
|
+
ro.observe(container);
|
|
138
|
+
const io = new IntersectionObserver(([entry]) => {
|
|
139
|
+
visible = entry.isIntersecting;
|
|
140
|
+
if (visible) start();
|
|
141
|
+
else stop();
|
|
142
|
+
});
|
|
143
|
+
io.observe(container);
|
|
144
|
+
resize();
|
|
145
|
+
start();
|
|
146
|
+
|
|
147
|
+
return () => {
|
|
148
|
+
stop();
|
|
149
|
+
ro.disconnect();
|
|
150
|
+
io.disconnect();
|
|
151
|
+
};
|
|
152
|
+
}, []);
|
|
153
|
+
|
|
154
|
+
// Reduced motion draws one still frame, so redraw when the props change.
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
redraw.current();
|
|
157
|
+
}, [dotColor, colors, gap, dotSize, waveStrength]);
|
|
158
|
+
|
|
159
|
+
const onPointerMove = (e) => {
|
|
160
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
161
|
+
pointer.current.x = e.clientX - rect.left;
|
|
162
|
+
pointer.current.y = e.clientY - rect.top;
|
|
163
|
+
pointer.current.active = true;
|
|
164
|
+
};
|
|
165
|
+
const onPointerLeave = () => {
|
|
166
|
+
pointer.current.active = false;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div
|
|
171
|
+
ref={containerRef}
|
|
172
|
+
onPointerMove={interactive ? onPointerMove : undefined}
|
|
173
|
+
onPointerLeave={onPointerLeave}
|
|
174
|
+
onPointerCancel={onPointerLeave}
|
|
175
|
+
className={`relative h-screen w-full overflow-hidden ${containerClassName}`}
|
|
176
|
+
style={{ background }}
|
|
177
|
+
>
|
|
178
|
+
<canvas
|
|
179
|
+
ref={canvasRef}
|
|
180
|
+
aria-hidden="true"
|
|
181
|
+
className="pointer-events-none absolute inset-0 h-full w-full"
|
|
182
|
+
style={fade ? { maskImage: "radial-gradient(ellipse 80% 75% at 50% 50%, #000 35%, transparent 100%)", WebkitMaskImage: "radial-gradient(ellipse 80% 75% at 50% 50%, #000 35%, transparent 100%)" } : undefined}
|
|
183
|
+
/>
|
|
184
|
+
{children && <div className={`relative z-10 h-full w-full ${className}`}>{children}</div>}
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export default DotWaveBackground;
|