apex-ui-kit 2.0.6 → 2.1.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/AccordionMarquee/AccordionMarquee.jsx +74 -53
- package/dist/ApexUI-Kit/templates/BeamBar/BeamBar.jsx +5 -3
- package/dist/ApexUI-Kit/templates/BluePrintBackground/BluePrintBackground.jsx +14 -2
- package/dist/ApexUI-Kit/templates/CardStack/CardStack.jsx +129 -0
- package/dist/ApexUI-Kit/templates/Cards/BasicCard.jsx +3 -3
- package/dist/ApexUI-Kit/templates/Carousel/ParallaxCarousel.jsx +0 -161
- package/dist/ApexUI-Kit/templates/DragCarousel/DragCarousel.jsx +39 -7
- package/dist/ApexUI-Kit/templates/EdgeGlowForm/EdgeGlowFrom.jsx +116 -98
- package/dist/ApexUI-Kit/templates/FlipProfile/FlipProfile.jsx +69 -37
- package/dist/ApexUI-Kit/templates/GlareCard/GlareCard.jsx +2 -2
- package/dist/ApexUI-Kit/templates/HoverText/HoverText.jsx +14 -13
- package/dist/ApexUI-Kit/templates/HyperCard/HyperCard.jsx +59 -42
- package/dist/ApexUI-Kit/templates/KineticThreadsBackground/KineticThreadBackground.jsx +17 -2
- package/dist/ApexUI-Kit/templates/LensFlareBackground/LensFlareBackground.jsx +18 -3
- package/dist/ApexUI-Kit/templates/LuminousParticleOcean/LuminousParticleOcean.jsx +15 -1
- package/dist/ApexUI-Kit/templates/MaskView/MaskView.jsx +345 -0
- package/dist/ApexUI-Kit/templates/MeteorCard/MeteorCard.jsx +34 -45
- package/dist/ApexUI-Kit/templates/NavDropDown/NavDropDown.jsx +272 -101
- package/dist/ApexUI-Kit/templates/NavMenu/NavMenu.jsx +8 -2
- package/dist/ApexUI-Kit/templates/ParallaxCard/ParallaxCard.jsx +13 -11
- package/dist/ApexUI-Kit/templates/ParallaxCarousel/ParallaxCarousel.jsx +60 -67
- package/dist/ApexUI-Kit/templates/ParallaxProfile/ParallaxProfile.jsx +69 -55
- package/dist/ApexUI-Kit/templates/PointerFollower/PointerFollower.jsx +34 -14
- package/dist/ApexUI-Kit/templates/ScrollMarquee/CardMarquee.jsx +0 -0
- package/dist/ApexUI-Kit/templates/SmartGridCard/SmartGridCard.jsx +4 -3
- package/dist/ApexUI-Kit/templates/SmokeBackground/SmokeBackground.jsx +35 -16
- package/dist/ApexUI-Kit/templates/Testimonials/Testimonials.jsx +11 -8
- package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeAnimation.js +40 -69
- package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeToggle.css +5 -1
- package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeToggle.jsx +25 -7
- package/dist/ApexUI-Kit/templates/ToolTip/ToolTip.jsx +71 -52
- package/dist/cli/index.js +25 -14
- package/package.json +2 -2
- package/dist/ApexUI-Kit/templates/GlassCardStack/GlassCardStack.jsx +0 -111
- package/dist/ApexUI-Kit/templates/WaterDropReveal/WaterDropReveal.jsx +0 -151
|
@@ -6,7 +6,8 @@ const HoverText = ({
|
|
|
6
6
|
fontSize = "2.5rem",
|
|
7
7
|
className = "",
|
|
8
8
|
effect, // if not passed, use 'defaultReveal'
|
|
9
|
-
effectColor = "#C27AFF"
|
|
9
|
+
effectColor = "#C27AFF",
|
|
10
|
+
textColor = "#fff" // resting colour of the letters
|
|
10
11
|
}) => {
|
|
11
12
|
const charsRef = useRef([]);
|
|
12
13
|
const containerRef = useRef(null);
|
|
@@ -32,7 +33,7 @@ const HoverText = ({
|
|
|
32
33
|
gsap.to(el, {
|
|
33
34
|
scale: 1,
|
|
34
35
|
rotate: 0,
|
|
35
|
-
color:
|
|
36
|
+
color: textColor,
|
|
36
37
|
filter: `drop-shadow(0 0px 0px ${effectColor})`,
|
|
37
38
|
duration: 0.32,
|
|
38
39
|
ease: "expo.inOut",
|
|
@@ -61,7 +62,7 @@ const HoverText = ({
|
|
|
61
62
|
gsap.to(el, {
|
|
62
63
|
scale: 1,
|
|
63
64
|
rotate: 0,
|
|
64
|
-
color:
|
|
65
|
+
color: textColor,
|
|
65
66
|
filter: `drop-shadow(0 0px 0px ${effectColor})`,
|
|
66
67
|
duration: 0.38,
|
|
67
68
|
ease: "expo.inOut",
|
|
@@ -79,11 +80,11 @@ const HoverText = ({
|
|
|
79
80
|
x: 0,
|
|
80
81
|
y: 0,
|
|
81
82
|
filter: `drop-shadow(0 0px 0px ${effectColor})`,
|
|
82
|
-
color:
|
|
83
|
+
color: textColor,
|
|
83
84
|
});
|
|
84
85
|
}
|
|
85
86
|
});
|
|
86
|
-
}, [text, effectColor]);
|
|
87
|
+
}, [text, effectColor, textColor]);
|
|
87
88
|
|
|
88
89
|
// Magnetic effect
|
|
89
90
|
const handleMouseMove = (e) => {
|
|
@@ -126,7 +127,7 @@ const HoverText = ({
|
|
|
126
127
|
filter: isFocus
|
|
127
128
|
? `drop-shadow(0 4px 32px ${effectColor})`
|
|
128
129
|
: `drop-shadow(0 2px 16px ${effectColor}44)`,
|
|
129
|
-
color: isFocus ? effectColor :
|
|
130
|
+
color: isFocus ? effectColor : textColor,
|
|
130
131
|
zIndex: isFocus ? 10 : 2,
|
|
131
132
|
duration: 0.35,
|
|
132
133
|
ease: "expo.out",
|
|
@@ -156,7 +157,7 @@ const HoverText = ({
|
|
|
156
157
|
gsap.to(el, {
|
|
157
158
|
scaleX: 1,
|
|
158
159
|
scaleY: 1,
|
|
159
|
-
color:
|
|
160
|
+
color: textColor,
|
|
160
161
|
filter: `drop-shadow(0 0px 0px ${effectColor})`,
|
|
161
162
|
duration: 0.18,
|
|
162
163
|
ease: "expo.inOut",
|
|
@@ -203,7 +204,7 @@ const HoverText = ({
|
|
|
203
204
|
y: 0,
|
|
204
205
|
scale: 1,
|
|
205
206
|
rotate: 0,
|
|
206
|
-
color:
|
|
207
|
+
color: textColor,
|
|
207
208
|
filter: `drop-shadow(0 0px 0px ${effectColor})`,
|
|
208
209
|
duration: 0.32,
|
|
209
210
|
ease: "expo.inOut",
|
|
@@ -245,7 +246,7 @@ const HoverText = ({
|
|
|
245
246
|
gsap.to(el, {
|
|
246
247
|
scale: 1,
|
|
247
248
|
rotate: 0,
|
|
248
|
-
color:
|
|
249
|
+
color: textColor,
|
|
249
250
|
filter: `drop-shadow(0 0px 0px ${effectColor})`,
|
|
250
251
|
duration: 0.32,
|
|
251
252
|
ease: "expo.inOut",
|
|
@@ -279,7 +280,7 @@ const HoverText = ({
|
|
|
279
280
|
gsap.to(el, {
|
|
280
281
|
y: 0,
|
|
281
282
|
scale: 1,
|
|
282
|
-
color:
|
|
283
|
+
color: textColor,
|
|
283
284
|
filter: `drop-shadow(0 0px 0px ${effectColor})`,
|
|
284
285
|
duration: 0.38 + Math.abs(j) * 0.08,
|
|
285
286
|
ease: "expo.inOut",
|
|
@@ -297,7 +298,7 @@ const HoverText = ({
|
|
|
297
298
|
y: 0,
|
|
298
299
|
scale: 1,
|
|
299
300
|
rotate: 0,
|
|
300
|
-
color:
|
|
301
|
+
color: textColor,
|
|
301
302
|
filter: `drop-shadow(0 0px 0px ${effectColor})`,
|
|
302
303
|
duration: 0.6,
|
|
303
304
|
ease: "expo.inOut",
|
|
@@ -309,7 +310,7 @@ const HoverText = ({
|
|
|
309
310
|
if (el) {
|
|
310
311
|
gsap.to(el, {
|
|
311
312
|
rotateY: 0,
|
|
312
|
-
color:
|
|
313
|
+
color: textColor,
|
|
313
314
|
filter: `drop-shadow(0 0px 0px ${effectColor})`,
|
|
314
315
|
duration: 0.6,
|
|
315
316
|
ease: "expo.inOut",
|
|
@@ -356,7 +357,7 @@ const HoverText = ({
|
|
|
356
357
|
borderRadius: "0.18em",
|
|
357
358
|
position: "relative",
|
|
358
359
|
zIndex: 2,
|
|
359
|
-
color:
|
|
360
|
+
color: textColor,
|
|
360
361
|
background: "none",
|
|
361
362
|
transition: "filter 0.3s, color 0.3s, background 0.3s, opacity 0.3s",
|
|
362
363
|
}}
|
|
@@ -1,23 +1,22 @@
|
|
|
1
|
+
// ...existing code...
|
|
1
2
|
import React, { useRef, useEffect, useState } from "react";
|
|
2
3
|
import { gsap } from "gsap";
|
|
3
4
|
import {
|
|
4
5
|
faCube,
|
|
5
|
-
faChevronRight,
|
|
6
|
+
faChevronRight,
|
|
6
7
|
} from "@fortawesome/free-solid-svg-icons";
|
|
7
8
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
8
9
|
|
|
9
10
|
const HyperCard = ({
|
|
10
|
-
// Content props remain unchanged
|
|
11
11
|
text = "Introducing Hyper Card",
|
|
12
12
|
LastText = "Effect",
|
|
13
|
-
LastTextColor = "#9AE600",
|
|
13
|
+
LastTextColor = "#9AE600",
|
|
14
14
|
SubText = "Fully animated lightning effect card with customizable props, built with Apex UI.",
|
|
15
15
|
title = "Apex UI Hyper Card",
|
|
16
16
|
description = "Experience futuristic animations with customizable props like glow, beam, star count, and hover interactions.",
|
|
17
17
|
buttonText = "Get Started",
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
starColor = "#9AE600", // Green stars
|
|
19
|
+
starColor = "#9AE600",
|
|
21
20
|
starCount = 250,
|
|
22
21
|
glow = true,
|
|
23
22
|
baseSpeed = 0.5,
|
|
@@ -30,18 +29,21 @@ const HyperCard = ({
|
|
|
30
29
|
const stars = useRef([]);
|
|
31
30
|
const speedRef = useRef({ value: baseSpeed });
|
|
32
31
|
|
|
33
|
-
//
|
|
32
|
+
// Particle canvas (kept same logic; responsive CSS classes control visual size)
|
|
34
33
|
useEffect(() => {
|
|
35
34
|
const canvas = canvasRef.current;
|
|
35
|
+
if (!canvas) return;
|
|
36
36
|
const ctx = canvas.getContext("2d");
|
|
37
|
-
let animationFrame;
|
|
37
|
+
let animationFrame = 0;
|
|
38
38
|
let w, h;
|
|
39
|
+
// Only draw while the card is on screen.
|
|
40
|
+
let visible = true;
|
|
39
41
|
|
|
40
42
|
const init = () => {
|
|
41
43
|
w = canvas.width = canvas.offsetWidth;
|
|
42
44
|
h = canvas.height = canvas.offsetHeight;
|
|
43
45
|
stars.current = Array.from({ length: starCount }, () => {
|
|
44
|
-
const z = Math.random() * w;
|
|
46
|
+
const z = Math.random() * Math.max(w, 1);
|
|
45
47
|
return {
|
|
46
48
|
x: Math.random() * w, y: Math.random() * h,
|
|
47
49
|
z, baseZ: z, brightness: Math.random() * 0.5 + 0.5,
|
|
@@ -50,17 +52,17 @@ const HyperCard = ({
|
|
|
50
52
|
};
|
|
51
53
|
|
|
52
54
|
const draw = () => {
|
|
53
|
-
|
|
54
|
-
if (!ctx) return;
|
|
55
|
+
animationFrame = 0;
|
|
56
|
+
if (!ctx || !visible) return;
|
|
55
57
|
ctx.clearRect(0, 0, w, h);
|
|
56
58
|
ctx.fillStyle = starColor;
|
|
57
59
|
|
|
58
60
|
stars.current.forEach((star) => {
|
|
59
61
|
star.z -= speedRef.current.value;
|
|
60
|
-
if (star.z <= 1) star.z = star.baseZ;
|
|
62
|
+
if (star.z <= 1) star.z = star.baseZ || Math.max(w, 1);
|
|
61
63
|
const sx = (star.x - w / 2) * (w / star.z) + w / 2;
|
|
62
64
|
const sy = (star.y - h / 2) * (h / star.z) + h / 2;
|
|
63
|
-
const size = (1 - star.z / w) * 2;
|
|
65
|
+
const size = (1 - star.z / Math.max(w, 1)) * 2;
|
|
64
66
|
ctx.beginPath();
|
|
65
67
|
ctx.globalAlpha = star.brightness;
|
|
66
68
|
if (speedRef.current.value > baseSpeed + 0.3) {
|
|
@@ -85,29 +87,46 @@ const HyperCard = ({
|
|
|
85
87
|
init();
|
|
86
88
|
draw();
|
|
87
89
|
window.addEventListener("resize", init);
|
|
88
|
-
|
|
90
|
+
const visibilityObserver = typeof IntersectionObserver !== "undefined"
|
|
91
|
+
? new IntersectionObserver(([entry]) => {
|
|
92
|
+
visible = entry.isIntersecting;
|
|
93
|
+
if (visible && !animationFrame) animationFrame = requestAnimationFrame(draw);
|
|
94
|
+
})
|
|
95
|
+
: null;
|
|
96
|
+
visibilityObserver?.observe(canvas);
|
|
97
|
+
return () => {
|
|
98
|
+
cancelAnimationFrame(animationFrame);
|
|
99
|
+
visibilityObserver?.disconnect();
|
|
100
|
+
window.removeEventListener("resize", init);
|
|
101
|
+
};
|
|
89
102
|
}, [starColor, starCount, baseSpeed]);
|
|
90
103
|
|
|
91
|
-
// GSAP
|
|
104
|
+
// GSAP speed tween (cleaned up)
|
|
92
105
|
useEffect(() => {
|
|
93
|
-
gsap.to(speedRef.current, { value: hovered ? warpSpeed : baseSpeed, duration: 1.
|
|
106
|
+
const tween = gsap.to(speedRef.current, { value: hovered ? warpSpeed : baseSpeed, duration: 1.2, ease: "power4.inOut" });
|
|
107
|
+
return () => tween.kill();
|
|
94
108
|
}, [hovered, warpSpeed, baseSpeed]);
|
|
95
109
|
|
|
110
|
+
// Hover jiggle with cleanup
|
|
96
111
|
useEffect(() => {
|
|
97
112
|
const el = contentRef.current;
|
|
113
|
+
if (!el) return;
|
|
114
|
+
let jiggle;
|
|
98
115
|
if (hovered) {
|
|
99
|
-
gsap.to(el, { scale: 0.95, duration: 0.
|
|
100
|
-
gsap.to(el, { x: "+=1", y: "-=1", duration: 0.
|
|
116
|
+
gsap.to(el, { scale: 0.95, duration: 0.45, ease: "power1.out" });
|
|
117
|
+
jiggle = gsap.to(el, { x: "+=1", y: "-=1", duration: 0.06, repeat: -1, yoyo: true, ease: "sine.inOut" });
|
|
101
118
|
} else {
|
|
102
119
|
gsap.killTweensOf(el);
|
|
103
|
-
gsap.to(el, { x: 0, y: 0, scale: 1, duration: 0.
|
|
120
|
+
gsap.to(el, { x: 0, y: 0, scale: 1, duration: 0.35, ease: "power2.out" });
|
|
104
121
|
}
|
|
122
|
+
return () => jiggle?.kill();
|
|
105
123
|
}, [hovered]);
|
|
106
124
|
|
|
107
125
|
return (
|
|
108
126
|
<div
|
|
109
|
-
className="w-full
|
|
110
|
-
|
|
127
|
+
className="w-full mx-auto rounded-2xl overflow-hidden text-white
|
|
128
|
+
border border-slate-700 bg-slate-900 shadow-2xl shadow-black/50
|
|
129
|
+
max-w-[20rem]"
|
|
111
130
|
style={{
|
|
112
131
|
backgroundImage: 'linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px), linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px)',
|
|
113
132
|
backgroundSize: '2rem 2rem'
|
|
@@ -115,13 +134,13 @@ const HyperCard = ({
|
|
|
115
134
|
>
|
|
116
135
|
{/* Top animated HyperCard */}
|
|
117
136
|
<div
|
|
118
|
-
className="relative w-full
|
|
137
|
+
className="relative w-full overflow-hidden group cursor-pointer
|
|
138
|
+
h-52 md:h-56"
|
|
119
139
|
onMouseEnter={() => setHovered(true)}
|
|
120
140
|
onMouseLeave={() => setHovered(false)}
|
|
121
141
|
>
|
|
122
|
-
<canvas ref={canvasRef} className="absolute inset-0 w-full h-full z-0" />
|
|
142
|
+
<canvas ref={canvasRef} className="absolute inset-0 w-full h-full z-0 block" />
|
|
123
143
|
|
|
124
|
-
{/* Glow effect from your original code */}
|
|
125
144
|
{glow && (
|
|
126
145
|
<div
|
|
127
146
|
className="absolute inset-0 w-full h-full z-10 pointer-events-none"
|
|
@@ -133,44 +152,42 @@ const HyperCard = ({
|
|
|
133
152
|
/>
|
|
134
153
|
)}
|
|
135
154
|
|
|
136
|
-
|
|
137
|
-
<div className="absolute bottom-0 w-full h-[2px] bg-purple-500/50 blur-sm"></div>
|
|
155
|
+
<div className="absolute bottom-0 w-full h-[2px] bg-purple-500/50 blur-sm pointer-events-none"></div>
|
|
138
156
|
|
|
139
|
-
<div ref={contentRef} className="relative z-20 flex flex-col items-center justify-center w-full h-full px-
|
|
140
|
-
<h2 className="text-
|
|
157
|
+
<div ref={contentRef} className="relative z-20 flex flex-col items-center justify-center w-full h-full px-3 py-2">
|
|
158
|
+
<h2 className="text-sm sm:text-base md:text-lg lg:text-lg font-normal text-center text-white leading-tight">
|
|
141
159
|
{text}{" "}
|
|
142
|
-
{/* Fixed color issue by using inline style */}
|
|
143
160
|
<span className="font-bold ml-1" style={{ color: LastTextColor }}>
|
|
144
161
|
{LastText}
|
|
145
162
|
</span>
|
|
146
163
|
</h2>
|
|
147
|
-
<p className="text-sm text-gray-300 text-center max-w-
|
|
164
|
+
<p className="text-xs sm:text-sm text-gray-300 text-center max-w-xs mt-1 px-2">{SubText}</p>
|
|
148
165
|
</div>
|
|
149
166
|
</div>
|
|
150
167
|
|
|
151
168
|
{/* Bottom content section */}
|
|
152
|
-
<div className="px-
|
|
153
|
-
<div className="flex items-center gap-
|
|
154
|
-
<FontAwesomeIcon icon={faCube} style={{ color: starColor }} className="text-
|
|
155
|
-
<h3 className="text-2xl font-extrabold text-white tracking-tight">{title}</h3>
|
|
169
|
+
<div className="px-4 sm:px-5 pt-5 pb-6 w-full text-left flex flex-col items-start">
|
|
170
|
+
<div className="flex items-center gap-2 mb-2">
|
|
171
|
+
<FontAwesomeIcon icon={faCube} style={{ color: starColor }} className="text-lg sm:text-xl" />
|
|
172
|
+
<h3 className="text-xl md:text-2xl font-extrabold text-white tracking-tight leading-tight">{title}</h3>
|
|
156
173
|
</div>
|
|
157
|
-
<p className="text-
|
|
174
|
+
<p className="text-xs sm:text-sm text-gray-400 mb-4 max-w-full leading-relaxed">{description}</p>
|
|
158
175
|
|
|
159
|
-
{/* --- NEW BUTTON WITH ANIMATION --- */}
|
|
160
176
|
<button
|
|
161
|
-
className="group relative flex items-center gap-2 px-
|
|
177
|
+
className="group relative flex items-center justify-center gap-2 px-4 py-2
|
|
162
178
|
border-2 border-[#9AE600] rounded-lg
|
|
163
|
-
font-semibold text-white overflow-hidden transition-all duration-300
|
|
179
|
+
font-semibold text-white overflow-hidden transition-all duration-300
|
|
180
|
+
w-full sm:w-auto"
|
|
181
|
+
aria-label={buttonText}
|
|
164
182
|
>
|
|
165
|
-
{/* Fill-up background effect */}
|
|
166
183
|
<span
|
|
167
|
-
className="absolute inset-0 h-full w-0
|
|
184
|
+
className="absolute inset-0 h-full w-0 bg-[#4c6d0a]
|
|
168
185
|
transition-all duration-300 ease-in-out group-hover:w-full"
|
|
169
186
|
style={{ transformOrigin: 'bottom' }}
|
|
187
|
+
aria-hidden="true"
|
|
170
188
|
></span>
|
|
171
189
|
|
|
172
|
-
|
|
173
|
-
<span className="relative z-10 flex items-center gap-2">
|
|
190
|
+
<span className="relative z-10 flex items-center gap-2 text-sm sm:text-base">
|
|
174
191
|
{buttonText}
|
|
175
192
|
<FontAwesomeIcon icon={faChevronRight} className="transition-transform duration-300 group-hover:translate-x-1" />
|
|
176
193
|
</span>
|
|
@@ -180,4 +197,4 @@ const HyperCard = ({
|
|
|
180
197
|
);
|
|
181
198
|
};
|
|
182
199
|
|
|
183
|
-
export default HyperCard;
|
|
200
|
+
export default HyperCard;
|
|
@@ -190,7 +190,7 @@ const KineticThreadsBackground = ({
|
|
|
190
190
|
targetMouseRef.current.y = 0.5;
|
|
191
191
|
};
|
|
192
192
|
if (mouseInteraction) {
|
|
193
|
-
window.addEventListener('mousemove', handleMouseMove);
|
|
193
|
+
window.addEventListener('mousemove', handleMouseMove, { passive: true });
|
|
194
194
|
window.addEventListener('mouseleave', handleMouseLeave);
|
|
195
195
|
}
|
|
196
196
|
|
|
@@ -211,12 +211,19 @@ const KineticThreadsBackground = ({
|
|
|
211
211
|
uniforms.uResolution.value.set(clientWidth, clientHeight);
|
|
212
212
|
};
|
|
213
213
|
window.addEventListener('resize', handleResize);
|
|
214
|
+
// Also follow the container itself (it can change size without a window resize).
|
|
215
|
+
const resizeObserver = new ResizeObserver(() => handleResize());
|
|
216
|
+
resizeObserver.observe(mountNode);
|
|
214
217
|
handleResize(); // Initial call
|
|
215
218
|
|
|
216
219
|
// Animation loop
|
|
217
220
|
const clock = new THREE.Clock();
|
|
218
|
-
let animationFrameId;
|
|
221
|
+
let animationFrameId = 0;
|
|
222
|
+
// Only render while the background is on screen.
|
|
223
|
+
let visible = true;
|
|
219
224
|
const animate = () => {
|
|
225
|
+
animationFrameId = 0;
|
|
226
|
+
if (!visible) return;
|
|
220
227
|
uniforms.uTime.value = clock.getElapsedTime() * speed;
|
|
221
228
|
|
|
222
229
|
// Smoothly update mouse position (lerping)
|
|
@@ -228,10 +235,17 @@ const KineticThreadsBackground = ({
|
|
|
228
235
|
animationFrameId = requestAnimationFrame(animate);
|
|
229
236
|
};
|
|
230
237
|
animate();
|
|
238
|
+
const visibilityObserver = new IntersectionObserver(([entry]) => {
|
|
239
|
+
visible = entry.isIntersecting;
|
|
240
|
+
if (visible && !animationFrameId) animationFrameId = requestAnimationFrame(animate);
|
|
241
|
+
});
|
|
242
|
+
visibilityObserver.observe(mountNode);
|
|
231
243
|
|
|
232
244
|
// Cleanup function
|
|
233
245
|
return () => {
|
|
234
246
|
cancelAnimationFrame(animationFrameId);
|
|
247
|
+
visibilityObserver.disconnect();
|
|
248
|
+
resizeObserver.disconnect();
|
|
235
249
|
window.removeEventListener('resize', handleResize);
|
|
236
250
|
window.removeEventListener('mousemove', handleMouseMove);
|
|
237
251
|
window.removeEventListener('mouseleave', handleMouseLeave);
|
|
@@ -241,6 +255,7 @@ const KineticThreadsBackground = ({
|
|
|
241
255
|
geometry.dispose();
|
|
242
256
|
material.dispose();
|
|
243
257
|
renderer.dispose();
|
|
258
|
+
renderer.forceContextLoss();
|
|
244
259
|
};
|
|
245
260
|
}, [color, amplitude, distance, speed, mouseInteraction]);
|
|
246
261
|
|
|
@@ -207,6 +207,9 @@ const LensFlareBackground = ({
|
|
|
207
207
|
};
|
|
208
208
|
updatePlacement();
|
|
209
209
|
window.addEventListener("resize", updatePlacement);
|
|
210
|
+
// Also follow the container itself (it can change size without a window resize).
|
|
211
|
+
const resizeObserver = new ResizeObserver(() => updatePlacement());
|
|
212
|
+
if (mountRef.current) resizeObserver.observe(mountRef.current);
|
|
210
213
|
|
|
211
214
|
const handleMouseMove = (e) => {
|
|
212
215
|
if (!mountRef.current) return;
|
|
@@ -215,9 +218,13 @@ const LensFlareBackground = ({
|
|
|
215
218
|
const y = (e.clientY - rect.top) / rect.height;
|
|
216
219
|
mouseRef.current = { x, y };
|
|
217
220
|
};
|
|
218
|
-
window.addEventListener("mousemove", handleMouseMove);
|
|
221
|
+
window.addEventListener("mousemove", handleMouseMove, { passive: true });
|
|
219
222
|
|
|
223
|
+
// Only render while the background is on screen.
|
|
224
|
+
let visible = true;
|
|
220
225
|
const animate = (now) => {
|
|
226
|
+
frameId = 0;
|
|
227
|
+
if (!visible) return;
|
|
221
228
|
uniforms.iTime.value = now * 0.001;
|
|
222
229
|
const smoothing = 0.92;
|
|
223
230
|
smoothMouseRef.current.x =
|
|
@@ -229,14 +236,22 @@ const LensFlareBackground = ({
|
|
|
229
236
|
frameId = requestAnimationFrame(animate);
|
|
230
237
|
};
|
|
231
238
|
frameId = requestAnimationFrame(animate);
|
|
239
|
+
const visibilityObserver = new IntersectionObserver(([entry]) => {
|
|
240
|
+
visible = entry.isIntersecting;
|
|
241
|
+
if (visible && !frameId) frameId = requestAnimationFrame(animate);
|
|
242
|
+
});
|
|
243
|
+
if (mountRef.current) visibilityObserver.observe(mountRef.current);
|
|
232
244
|
|
|
233
245
|
return () => {
|
|
234
246
|
if (frameId) cancelAnimationFrame(frameId);
|
|
247
|
+
visibilityObserver.disconnect();
|
|
248
|
+
resizeObserver.disconnect();
|
|
235
249
|
window.removeEventListener("resize", updatePlacement);
|
|
236
250
|
window.removeEventListener("mousemove", handleMouseMove);
|
|
237
251
|
renderer.dispose();
|
|
238
|
-
|
|
239
|
-
|
|
252
|
+
renderer.forceContextLoss();
|
|
253
|
+
if (renderer.domElement.parentNode) {
|
|
254
|
+
renderer.domElement.parentNode.removeChild(renderer.domElement);
|
|
240
255
|
}
|
|
241
256
|
};
|
|
242
257
|
}, [flareColor, intensity, animationSpeed]);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useRef, useMemo } from 'react';
|
|
1
|
+
import React, { useRef, useMemo, useEffect, useState } from 'react';
|
|
2
2
|
import { Canvas, useFrame } from '@react-three/fiber';
|
|
3
3
|
import * as THREE from 'three';
|
|
4
4
|
// Helper function to convert hex color to rgba for CSS
|
|
@@ -102,8 +102,20 @@ const LuminousParticleOcean = ({
|
|
|
102
102
|
|
|
103
103
|
const lightColorRgba = useMemo(() => hexToRgba(sphereColor, 0.5), [sphereColor]);
|
|
104
104
|
|
|
105
|
+
// Pause the 3D scene while it is off screen.
|
|
106
|
+
const wrapperRef = useRef(null);
|
|
107
|
+
const [inView, setInView] = useState(true);
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
const el = wrapperRef.current;
|
|
110
|
+
if (!el || typeof IntersectionObserver === 'undefined') return undefined;
|
|
111
|
+
const observer = new IntersectionObserver(([entry]) => setInView(entry.isIntersecting));
|
|
112
|
+
observer.observe(el);
|
|
113
|
+
return () => observer.disconnect();
|
|
114
|
+
}, []);
|
|
115
|
+
|
|
105
116
|
return (
|
|
106
117
|
<div
|
|
118
|
+
ref={wrapperRef}
|
|
107
119
|
className="relative w-screen h-[60vh] lg:h-[80vh] overflow-hidden"
|
|
108
120
|
style={{
|
|
109
121
|
background: `linear-gradient(to bottom, ${bgColor1}, ${bgColor2})`
|
|
@@ -130,6 +142,8 @@ const LuminousParticleOcean = ({
|
|
|
130
142
|
|
|
131
143
|
<Canvas
|
|
132
144
|
camera={{ position: [0, 60, 150], fov: 55 }}
|
|
145
|
+
dpr={[1, 1.75]}
|
|
146
|
+
frameloop={inView ? 'always' : 'never'}
|
|
133
147
|
>
|
|
134
148
|
{/* Sunlight: Also uses the sphereColor prop */}
|
|
135
149
|
<pointLight
|