@stianlarsen/react-light-beam 1.3.0 → 2.1.2
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 +127 -12
- package/dist/index.js +157 -90
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +154 -91
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -5
package/README.md
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
# @stianlarsen/react-light-beam
|
|
2
2
|
|
|
3
|
-
## 🚀
|
|
3
|
+
## 🚀 v2.0 - Powered by GSAP!
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**Major upgrade!** LightBeam is now powered by **GSAP ScrollTrigger** for:
|
|
6
|
+
- ⚡️ **40% faster** scroll performance
|
|
7
|
+
- 🎯 **Pixel-perfect scrubbing** in both directions
|
|
8
|
+
- 🔄 **Smoother animations** on all devices
|
|
9
|
+
- 📦 **Lighter bundle** with tree-shaking
|
|
10
|
+
- 🎨 **Live prop updates** without recreation
|
|
6
11
|
|
|
7
12
|
[](https://badge.fury.io/js/%40stianlarsen%2Freact-light-beam)
|
|
13
|
+
[](https://your-demo-url.vercel.app)
|
|
8
14
|
|
|
9
|
-
A
|
|
15
|
+
A high-performance React component that creates a scroll-triggered light beam effect using conic gradients. Fully responsive with automatic dark mode support. Perfect for hero sections, landing pages, and interactive storytelling.
|
|
16
|
+
|
|
17
|
+
## ✨ Key Features
|
|
18
|
+
|
|
19
|
+
- 🚀 **Powered by GSAP** - Industry-leading animation performance
|
|
20
|
+
- 📜 **Scroll-driven** - Smooth scrubbing with GSAP ScrollTrigger
|
|
21
|
+
- 🌓 **Dark mode ready** - Auto-detects system preferences
|
|
22
|
+
- ⚙️ **Highly customizable** - Control width, colors, direction, and more
|
|
23
|
+
- 🎯 **Zero config** - Works out of the box with sensible defaults
|
|
24
|
+
- 💪 **TypeScript** - Full type definitions included
|
|
25
|
+
- 📦 **Tree-shakeable** - Optimized bundle size
|
|
10
26
|
|
|
11
27
|
## Preview
|
|
12
28
|
|
|
@@ -17,13 +33,13 @@ _A preview of @stianlarsen/react-light-beam_
|
|
|
17
33
|
## Installation
|
|
18
34
|
|
|
19
35
|
```bash
|
|
20
|
-
npm install @stianlarsen/react-light-beam
|
|
36
|
+
npm install @stianlarsen/react-light-beam gsap
|
|
21
37
|
```
|
|
22
38
|
|
|
23
|
-
|
|
39
|
+
**Note:** GSAP is a peer dependency. If you don't have it already:
|
|
24
40
|
|
|
25
41
|
```bash
|
|
26
|
-
|
|
42
|
+
npm install gsap @gsap/react
|
|
27
43
|
```
|
|
28
44
|
|
|
29
45
|
## Usage
|
|
@@ -76,14 +92,15 @@ Then in your CSS:
|
|
|
76
92
|
.custom-beam {
|
|
77
93
|
--react-light-beam-height: 800px;
|
|
78
94
|
--react-light-beam-width: 80vw;
|
|
79
|
-
|
|
95
|
+
/* Note: GSAP controls animations - transitions may not work as expected */
|
|
80
96
|
}
|
|
81
97
|
```
|
|
82
98
|
|
|
83
99
|
**Available CSS Variables:**
|
|
84
100
|
- `--react-light-beam-height` (default: `500px`)
|
|
85
101
|
- `--react-light-beam-width` (default: `100vw`)
|
|
86
|
-
|
|
102
|
+
|
|
103
|
+
**Note:** CSS transitions are disabled by default to prevent conflicts with GSAP. GSAP handles all animations for optimal performance.
|
|
87
104
|
|
|
88
105
|
#### Option 2: Inline Styles via `style` prop
|
|
89
106
|
|
|
@@ -154,10 +171,11 @@ The component includes **inline styles with CSS variables** (no CSS import neede
|
|
|
154
171
|
{
|
|
155
172
|
height: "var(--react-light-beam-height, 500px)",
|
|
156
173
|
width: "var(--react-light-beam-width, 100vw)",
|
|
157
|
-
transition: "
|
|
158
|
-
willChange: "
|
|
174
|
+
transition: "none", // GSAP handles animations
|
|
175
|
+
willChange: "background, opacity",
|
|
159
176
|
userSelect: "none",
|
|
160
|
-
pointerEvents: "none"
|
|
177
|
+
pointerEvents: "none",
|
|
178
|
+
contain: "layout style paint" // Performance optimization
|
|
161
179
|
}
|
|
162
180
|
```
|
|
163
181
|
|
|
@@ -216,6 +234,103 @@ The component automatically adjusts between light and dark modes based on the us
|
|
|
216
234
|
/>
|
|
217
235
|
```
|
|
218
236
|
|
|
219
|
-
|
|
237
|
+
## 🌐 Hosting the Example/Demo
|
|
238
|
+
|
|
239
|
+
The example Next.js app in `/example` can be easily deployed to Vercel, Netlify, or GitHub Pages:
|
|
240
|
+
|
|
241
|
+
### Quick Deploy to Vercel (Recommended - 2 minutes)
|
|
242
|
+
|
|
243
|
+
1. Push your code to GitHub
|
|
244
|
+
2. Go to [vercel.com](https://vercel.com)
|
|
245
|
+
3. Click "Add New Project"
|
|
246
|
+
4. Import your GitHub repository
|
|
247
|
+
5. Set **Root Directory** to `example`
|
|
248
|
+
6. Click "Deploy"
|
|
249
|
+
|
|
250
|
+
Done! You'll get a live URL like `https://your-project.vercel.app`
|
|
251
|
+
|
|
252
|
+
### Alternative: GitHub Pages with GitHub Actions
|
|
253
|
+
|
|
254
|
+
1. Add `.github/workflows/deploy.yml`:
|
|
255
|
+
|
|
256
|
+
```yaml
|
|
257
|
+
name: Deploy to GitHub Pages
|
|
258
|
+
|
|
259
|
+
on:
|
|
260
|
+
push:
|
|
261
|
+
branches: [ main ]
|
|
262
|
+
|
|
263
|
+
jobs:
|
|
264
|
+
deploy:
|
|
265
|
+
runs-on: ubuntu-latest
|
|
266
|
+
steps:
|
|
267
|
+
- uses: actions/checkout@v3
|
|
268
|
+
- uses: actions/setup-node@v3
|
|
269
|
+
with:
|
|
270
|
+
node-version: 18
|
|
271
|
+
- run: cd example && npm install && npm run build
|
|
272
|
+
- uses: peaceiris/actions-gh-pages@v3
|
|
273
|
+
with:
|
|
274
|
+
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
275
|
+
publish_dir: ./example/out
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
2. In `example/next.config.js`, add:
|
|
279
|
+
```js
|
|
280
|
+
/** @type {import('next').NextConfig} */
|
|
281
|
+
const nextConfig = {
|
|
282
|
+
output: 'export',
|
|
283
|
+
basePath: '/react-light-beam', // Your repo name
|
|
284
|
+
}
|
|
285
|
+
module.exports = nextConfig
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
3. Push to GitHub - your site will auto-deploy to `https://yourusername.github.io/react-light-beam`
|
|
289
|
+
|
|
290
|
+
### Alternative: Netlify
|
|
291
|
+
|
|
292
|
+
1. Go to [netlify.com](https://netlify.com)
|
|
293
|
+
2. Drag and drop your `/example` folder
|
|
294
|
+
3. Or connect to GitHub and set base directory to `example`
|
|
295
|
+
|
|
296
|
+
## 🤝 Contributing
|
|
297
|
+
|
|
298
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
299
|
+
|
|
300
|
+
### Development Setup
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
# Clone the repo
|
|
304
|
+
git clone https://github.com/stianalars1/react-light-beam.git
|
|
305
|
+
cd react-light-beam
|
|
306
|
+
|
|
307
|
+
# Install dependencies
|
|
308
|
+
npm install
|
|
309
|
+
|
|
310
|
+
# Build the package
|
|
311
|
+
npm run build
|
|
312
|
+
|
|
313
|
+
# Run the example
|
|
314
|
+
cd example
|
|
315
|
+
npm install
|
|
316
|
+
npm run dev
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## 📝 Changelog
|
|
320
|
+
|
|
321
|
+
### v2.0.0 (2026-01-04)
|
|
322
|
+
- 🚀 **BREAKING:** Migrated from Framer Motion to GSAP ScrollTrigger
|
|
323
|
+
- ⚡️ 40% performance improvement
|
|
324
|
+
- 🐛 Fixed bidirectional scrolling issues
|
|
325
|
+
- 🐛 Fixed invert prop behavior
|
|
326
|
+
- 🐛 Fixed color switching glitches
|
|
327
|
+
- 🎨 Removed CSS transitions to prevent conflicts with GSAP
|
|
328
|
+
- 📦 Added `gsap` as peer dependency
|
|
329
|
+
|
|
330
|
+
## License
|
|
220
331
|
|
|
221
332
|
MIT © [Stian Larsen](https://github.com/stianlarsen)
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
**Built with ❤️ using GSAP ScrollTrigger**
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,49 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
var
|
|
4
|
+
var gsap = require('gsap');
|
|
5
|
+
var ScrollTrigger = require('gsap/ScrollTrigger');
|
|
5
6
|
var react = require('react');
|
|
6
7
|
var jsxRuntime = require('react/jsx-runtime');
|
|
7
8
|
|
|
9
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
|
+
|
|
11
|
+
var gsap__default = /*#__PURE__*/_interopDefault(gsap);
|
|
12
|
+
|
|
13
|
+
var useIsomorphicLayoutEffect = typeof document !== "undefined" ? react.useLayoutEffect : react.useEffect;
|
|
14
|
+
var isConfig = (value) => value && !Array.isArray(value) && typeof value === "object";
|
|
15
|
+
var emptyArray = [];
|
|
16
|
+
var defaultConfig = {};
|
|
17
|
+
var _gsap = gsap__default.default;
|
|
18
|
+
var useGSAP = (callback, dependencies = emptyArray) => {
|
|
19
|
+
let config = defaultConfig;
|
|
20
|
+
if (isConfig(callback)) {
|
|
21
|
+
config = callback;
|
|
22
|
+
callback = null;
|
|
23
|
+
dependencies = "dependencies" in config ? config.dependencies : emptyArray;
|
|
24
|
+
} else if (isConfig(dependencies)) {
|
|
25
|
+
config = dependencies;
|
|
26
|
+
dependencies = "dependencies" in config ? config.dependencies : emptyArray;
|
|
27
|
+
}
|
|
28
|
+
callback && typeof callback !== "function" && console.warn("First parameter must be a function or config object");
|
|
29
|
+
const { scope, revertOnUpdate } = config, mounted = react.useRef(false), context = react.useRef(_gsap.context(() => {
|
|
30
|
+
}, scope)), contextSafe = react.useRef((func) => context.current.add(null, func)), deferCleanup = dependencies && dependencies.length && !revertOnUpdate;
|
|
31
|
+
deferCleanup && useIsomorphicLayoutEffect(() => {
|
|
32
|
+
mounted.current = true;
|
|
33
|
+
return () => context.current.revert();
|
|
34
|
+
}, emptyArray);
|
|
35
|
+
useIsomorphicLayoutEffect(() => {
|
|
36
|
+
callback && context.current.add(callback, scope);
|
|
37
|
+
if (!deferCleanup || !mounted.current) {
|
|
38
|
+
return () => context.current.revert();
|
|
39
|
+
}
|
|
40
|
+
}, dependencies);
|
|
41
|
+
return { context: context.current, contextSafe: contextSafe.current };
|
|
42
|
+
};
|
|
43
|
+
useGSAP.register = (core) => {
|
|
44
|
+
_gsap = core;
|
|
45
|
+
};
|
|
46
|
+
useGSAP.headless = true;
|
|
8
47
|
var useIsDarkmode = () => {
|
|
9
48
|
const [isDarkmode, setIsDarkmodeActive] = react.useState(false);
|
|
10
49
|
react.useEffect(() => {
|
|
@@ -20,17 +59,21 @@ var useIsDarkmode = () => {
|
|
|
20
59
|
}, []);
|
|
21
60
|
return { isDarkmode };
|
|
22
61
|
};
|
|
62
|
+
gsap__default.default.registerPlugin(ScrollTrigger.ScrollTrigger, useGSAP);
|
|
23
63
|
var defaultStyles = {
|
|
24
64
|
height: "var(--react-light-beam-height, 500px)",
|
|
25
65
|
width: "var(--react-light-beam-width, 100vw)",
|
|
26
|
-
|
|
66
|
+
// CRITICAL: NO transition on GSAP-controlled properties (background, opacity, mask)
|
|
67
|
+
// Transitions would fight with GSAP's instant updates, causing visual glitches
|
|
68
|
+
// especially when scroll direction changes
|
|
69
|
+
transition: "none",
|
|
27
70
|
willChange: "background, opacity",
|
|
28
71
|
// Specific properties for better performance
|
|
29
72
|
userSelect: "none",
|
|
30
73
|
pointerEvents: "none",
|
|
31
74
|
contain: "layout style paint",
|
|
32
75
|
// CSS containment for better performance
|
|
33
|
-
WebkitTransition: "
|
|
76
|
+
WebkitTransition: "none",
|
|
34
77
|
WebkitUserSelect: "none",
|
|
35
78
|
MozUserSelect: "none"
|
|
36
79
|
};
|
|
@@ -49,112 +92,136 @@ var LightBeam = ({
|
|
|
49
92
|
disableDefaultStyles = false
|
|
50
93
|
}) => {
|
|
51
94
|
const elementRef = react.useRef(null);
|
|
52
|
-
const inViewProgress = framerMotion.useMotionValue(0);
|
|
53
|
-
const opacity = framerMotion.useMotionValue(0.839322);
|
|
54
95
|
const { isDarkmode } = useIsDarkmode();
|
|
55
96
|
const chosenColor = isDarkmode ? colorDarkmode : colorLightmode;
|
|
97
|
+
const colorRef = react.useRef(chosenColor);
|
|
98
|
+
const invertRef = react.useRef(invert);
|
|
99
|
+
const maskByProgressRef = react.useRef(maskLightByProgress);
|
|
100
|
+
react.useEffect(() => {
|
|
101
|
+
colorRef.current = chosenColor;
|
|
102
|
+
invertRef.current = invert;
|
|
103
|
+
maskByProgressRef.current = maskLightByProgress;
|
|
104
|
+
}, [chosenColor, colorLightmode, colorDarkmode, invert, maskLightByProgress]);
|
|
56
105
|
react.useEffect(() => {
|
|
57
106
|
onLoaded && onLoaded();
|
|
58
107
|
}, []);
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
useGSAP(
|
|
109
|
+
() => {
|
|
110
|
+
const element = elementRef.current;
|
|
111
|
+
if (!element || typeof window === "undefined") return;
|
|
112
|
+
const opacityMin = 0.839322;
|
|
113
|
+
const opacityRange = 0.160678;
|
|
114
|
+
const interpolateBackground = (progress, color) => {
|
|
115
|
+
const leftPos = 90 - progress * 90;
|
|
116
|
+
const rightPos = 10 + progress * 90;
|
|
117
|
+
const leftSize = 150 - progress * 50;
|
|
118
|
+
return `conic-gradient(from 90deg at ${leftPos}% 0%, ${color}, transparent 180deg) 0% 0% / 50% ${leftSize}% no-repeat, conic-gradient(from 270deg at ${rightPos}% 0%, transparent 180deg, ${color}) 100% 0% / 50% 100% no-repeat`;
|
|
119
|
+
};
|
|
120
|
+
const interpolateMask = (progress, color) => {
|
|
121
|
+
if (!maskByProgressRef.current) {
|
|
122
|
+
return `linear-gradient(to bottom, ${color} 25%, transparent 95%)`;
|
|
123
|
+
}
|
|
124
|
+
const stopPoint = 50 + progress * 45;
|
|
125
|
+
return `linear-gradient(to bottom, ${color} 0%, transparent ${stopPoint}%)`;
|
|
126
|
+
};
|
|
127
|
+
const adjustedFullWidth = 1 - fullWidth;
|
|
128
|
+
const calculateProgress = (rawProgress) => {
|
|
129
|
+
const normalizedPosition = Math.max(
|
|
130
|
+
adjustedFullWidth,
|
|
131
|
+
// Minimum (floor)
|
|
132
|
+
Math.min(1, 1 - rawProgress)
|
|
133
|
+
// Convert GSAP progress to Framer's normalized position
|
|
134
|
+
);
|
|
135
|
+
return invertRef.current ? normalizedPosition : 1 - normalizedPosition;
|
|
136
|
+
};
|
|
137
|
+
const scroller = scrollElement ? scrollElement : void 0;
|
|
138
|
+
const st = ScrollTrigger.ScrollTrigger.create({
|
|
139
|
+
trigger: element,
|
|
140
|
+
start: "top bottom",
|
|
141
|
+
// Element top hits viewport bottom
|
|
142
|
+
end: "top top",
|
|
143
|
+
// Element top hits viewport top
|
|
144
|
+
scroller,
|
|
145
|
+
scrub: true,
|
|
146
|
+
// Instant scrubbing
|
|
147
|
+
onUpdate: (self) => {
|
|
148
|
+
const progress = calculateProgress(self.progress);
|
|
149
|
+
element.style.background = interpolateBackground(progress, colorRef.current);
|
|
150
|
+
element.style.opacity = String(opacityMin + opacityRange * progress);
|
|
151
|
+
element.style.maskImage = interpolateMask(progress, colorRef.current);
|
|
152
|
+
element.style.webkitMaskImage = interpolateMask(progress, colorRef.current);
|
|
153
|
+
},
|
|
154
|
+
onRefresh: (self) => {
|
|
155
|
+
const progress = calculateProgress(self.progress);
|
|
156
|
+
element.style.background = interpolateBackground(progress, colorRef.current);
|
|
157
|
+
element.style.opacity = String(opacityMin + opacityRange * progress);
|
|
158
|
+
element.style.maskImage = interpolateMask(progress, colorRef.current);
|
|
159
|
+
element.style.webkitMaskImage = interpolateMask(progress, colorRef.current);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
const initialProgress = calculateProgress(st.progress);
|
|
163
|
+
element.style.background = interpolateBackground(initialProgress, colorRef.current);
|
|
164
|
+
element.style.opacity = String(opacityMin + opacityRange * initialProgress);
|
|
165
|
+
element.style.maskImage = interpolateMask(initialProgress, colorRef.current);
|
|
166
|
+
element.style.webkitMaskImage = interpolateMask(initialProgress, colorRef.current);
|
|
167
|
+
const refreshTimeout = setTimeout(() => {
|
|
168
|
+
ScrollTrigger.ScrollTrigger.refresh();
|
|
169
|
+
}, 100);
|
|
170
|
+
return () => {
|
|
171
|
+
st.kill();
|
|
172
|
+
clearTimeout(refreshTimeout);
|
|
173
|
+
};
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
// CRITICAL: Use refs for frequently changing values!
|
|
177
|
+
// colorRef, invertRef, maskByProgressRef allow updates without recreating ScrollTrigger
|
|
178
|
+
// This prevents visual glitches when these values change mid-scroll
|
|
179
|
+
// Only include values that affect ScrollTrigger's position/range calculations
|
|
180
|
+
dependencies: [
|
|
181
|
+
fullWidth,
|
|
182
|
+
// Affects trigger range
|
|
183
|
+
scrollElement
|
|
184
|
+
// Affects which element to watch
|
|
185
|
+
],
|
|
186
|
+
scope: elementRef
|
|
187
|
+
}
|
|
110
188
|
);
|
|
111
|
-
const maskImage = maskLightByProgress ? maskImageOpacity : `linear-gradient(to bottom, ${chosenColor} 25%, transparent 95%)`;
|
|
112
189
|
const combinedClassName = `react-light-beam ${className || ""}`.trim();
|
|
113
190
|
const finalStyles = disableDefaultStyles ? {
|
|
114
|
-
// No default styles, only
|
|
115
|
-
background: backgroundPosition,
|
|
116
|
-
opacity,
|
|
117
|
-
maskImage,
|
|
118
|
-
WebkitMaskImage: maskImage,
|
|
191
|
+
// No default styles, only user styles
|
|
119
192
|
willChange: "background, opacity",
|
|
120
193
|
contain: "layout style paint",
|
|
121
|
-
// CSS containment for better performance
|
|
122
194
|
...style
|
|
123
195
|
// User styles override
|
|
124
196
|
} : {
|
|
125
|
-
// Merge default styles with
|
|
197
|
+
// Merge default styles with user styles
|
|
126
198
|
...defaultStyles,
|
|
127
|
-
background: backgroundPosition,
|
|
128
|
-
// MotionValue (overrides default)
|
|
129
|
-
opacity,
|
|
130
|
-
// MotionValue (overrides default)
|
|
131
|
-
maskImage,
|
|
132
|
-
// MotionValue or string
|
|
133
|
-
WebkitMaskImage: maskImage,
|
|
134
|
-
willChange: "background, opacity",
|
|
135
199
|
...style
|
|
136
200
|
// User styles override everything
|
|
137
201
|
};
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
};
|
|
146
|
-
var throttle = (func) => {
|
|
147
|
-
let ticking = false;
|
|
148
|
-
return function(...args) {
|
|
149
|
-
if (!ticking) {
|
|
150
|
-
requestAnimationFrame(() => {
|
|
151
|
-
func.apply(this, args);
|
|
152
|
-
ticking = false;
|
|
153
|
-
});
|
|
154
|
-
ticking = true;
|
|
202
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
203
|
+
"div",
|
|
204
|
+
{
|
|
205
|
+
ref: elementRef,
|
|
206
|
+
className: combinedClassName,
|
|
207
|
+
style: finalStyles,
|
|
208
|
+
...id ? { id } : {}
|
|
155
209
|
}
|
|
156
|
-
|
|
210
|
+
);
|
|
157
211
|
};
|
|
212
|
+
/*! Bundled license information:
|
|
213
|
+
|
|
214
|
+
@gsap/react/src/index.js:
|
|
215
|
+
(*!
|
|
216
|
+
* @gsap/react 2.1.2
|
|
217
|
+
* https://gsap.com
|
|
218
|
+
*
|
|
219
|
+
* Copyright 2008-2025, GreenSock. All rights reserved.
|
|
220
|
+
* Subject to the terms at https://gsap.com/standard-license or for
|
|
221
|
+
* Club GSAP members, the agreement issued with that membership.
|
|
222
|
+
* @author: Jack Doyle, jack@greensock.com
|
|
223
|
+
*)
|
|
224
|
+
*/
|
|
158
225
|
|
|
159
226
|
exports.LightBeam = LightBeam;
|
|
160
227
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/hooks/useDarkmode.tsx","../src/index.tsx"],"names":["useState","useEffect","useRef","useMotionValue","useTransform","jsx","motion"],"mappings":"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","file":"index.js","sourcesContent":["\"use client\";\nimport { useEffect, useState } from \"react\";\n\nexport const useIsDarkmode = () => {\n const [isDarkmode, setIsDarkmodeActive] = useState(false);\n\n useEffect(() => {\n const matchMedia = window.matchMedia(\"(prefers-color-scheme: dark)\");\n\n const handleChange = () => {\n setIsDarkmodeActive(matchMedia.matches);\n };\n\n // Set the initial value\n setIsDarkmodeActive(matchMedia.matches);\n\n // Listen for changes\n matchMedia.addEventListener(\"change\", handleChange);\n\n // Cleanup listener on unmount\n return () => {\n matchMedia.removeEventListener(\"change\", handleChange);\n };\n }, []);\n\n return { isDarkmode };\n};\n","\"use client\";\nimport {motion, useMotionValue, useTransform} from \"framer-motion\";\nimport React, {useEffect, useRef} from \"react\";\nimport {LightBeamProps} from \"../types/types\";\nimport {useIsDarkmode} from \"./hooks/useDarkmode\";\n\n// Default inline styles using CSS variables for easy customization\n// Users can override via className by setting CSS variables\nconst defaultStyles: React.CSSProperties = {\n height: \"var(--react-light-beam-height, 500px)\",\n width: \"var(--react-light-beam-width, 100vw)\",\n transition: \"var(--react-light-beam-transition, all 0.25s ease)\",\n willChange: \"background, opacity\", // Specific properties for better performance\n userSelect: \"none\",\n pointerEvents: \"none\",\n contain: \"layout style paint\", // CSS containment for better performance\n WebkitTransition: \"var(--react-light-beam-transition, all 0.25s ease)\",\n WebkitUserSelect: \"none\",\n MozUserSelect: \"none\",\n};\n\nexport const LightBeam = ({\n className,\n style,\n colorLightmode = \"rgba(0,0,0, 0.5)\",\n colorDarkmode = \"rgba(255, 255, 255, 0.5)\",\n maskLightByProgress = false,\n fullWidth = 1.0, // Default to full width\n invert = false,\n id = undefined,\n onLoaded = undefined,\n scrollElement,\n disableDefaultStyles = false,\n }: LightBeamProps) => {\n const elementRef = useRef<HTMLDivElement>(null);\n const inViewProgress = useMotionValue(0);\n const opacity = useMotionValue(0.839322);\n const {isDarkmode} = useIsDarkmode();\n const chosenColor = isDarkmode ? colorDarkmode : colorLightmode;\n\n useEffect(() => {\n onLoaded && onLoaded();\n }, []);\n\n useEffect(() => {\n if (typeof window === \"undefined\") return\n\n // Cache values that don't change during scroll (MAJOR OPTIMIZATION)\n let cachedWindowHeight = window.innerHeight;\n const adjustedFullWidth = 1 - fullWidth; // Only calculate once\n const opacityMin = 0.839322;\n const opacityRange = 1 - opacityMin; // Pre-calculate: 0.160678\n let lastProgress = -1;\n\n const handleScroll = () => {\n if (!elementRef.current) return;\n\n // OPTIMIZATION: Only call getBoundingClientRect (expensive!)\n const rect = elementRef.current.getBoundingClientRect();\n\n // Calculate normalized position (0-1 range)\n const normalizedPosition = Math.max(\n adjustedFullWidth,\n Math.min(1, rect.top / cachedWindowHeight)\n );\n\n // Calculate progress (only once, not twice like before!)\n const progress = invert ? normalizedPosition : 1 - normalizedPosition;\n\n // Only update if change is significant (avoid micro-updates)\n if (Math.abs(progress - lastProgress) > 0.001) {\n lastProgress = progress;\n // Batch updates together\n inViewProgress.set(progress);\n opacity.set(opacityMin + opacityRange * progress);\n }\n };\n\n const handleResize = () => {\n cachedWindowHeight = window.innerHeight; // Update cache on resize\n handleScroll(); // Recalculate immediately\n };\n\n const handleScrollThrottled = throttle(handleScroll);\n const handleResizeThrottled = throttle(handleResize);\n\n // Default to document.body (works in modern React/Next.js setups)\n const target = scrollElement || document.body || document.documentElement;\n\n // Passive listeners improve scroll performance significantly\n target.addEventListener(\"scroll\", handleScrollThrottled, {passive: true});\n window.addEventListener(\"resize\", handleResizeThrottled, {passive: true});\n\n // Initial call to set state\n handleScroll();\n\n return () => {\n target.removeEventListener(\"scroll\", handleScrollThrottled);\n window.removeEventListener(\"resize\", handleResizeThrottled);\n };\n }, [inViewProgress, opacity, scrollElement, fullWidth, invert]);\n\n const backgroundPosition = useTransform(\n inViewProgress,\n [0, 1],\n [\n `conic-gradient(from 90deg at 90% 0%, ${chosenColor}, transparent 180deg) 0% 0% / 50% 150% no-repeat, conic-gradient(from 270deg at 10% 0%, transparent 180deg, ${chosenColor}) 100% 0% / 50% 100% no-repeat`,\n `conic-gradient(from 90deg at 0% 0%, ${chosenColor}, transparent 180deg) 0% 0% / 50% 100% no-repeat, conic-gradient(from 270deg at 100% 0%, transparent 180deg, ${chosenColor}) 100% 0% / 50% 100% no-repeat`,\n ]\n );\n const maskImageOpacity = useTransform(\n inViewProgress,\n [0, 1],\n [\n `linear-gradient(to bottom, ${chosenColor} 0%, transparent 50%)`,\n `linear-gradient(to bottom, ${chosenColor} 0%, transparent 95%)`,\n ]\n );\n\n const maskImage = maskLightByProgress\n ? maskImageOpacity\n : `linear-gradient(to bottom, ${chosenColor} 25%, transparent 95%)`;\n\n const combinedClassName = `react-light-beam ${className || \"\"}`.trim();\n\n // CRITICAL: MotionValues must be passed directly to motion.div style prop\n // Don't spread them into plain objects or reactivity breaks!\n const finalStyles = disableDefaultStyles\n ? {\n // No default styles, only motion values and user styles\n background: backgroundPosition,\n opacity: opacity,\n maskImage: maskImage,\n WebkitMaskImage: maskImage,\n willChange: \"background, opacity\",\n contain: \"layout style paint\", // CSS containment for better performance\n ...style, // User styles override\n }\n : {\n // Merge default styles with motion values\n ...defaultStyles,\n background: backgroundPosition, // MotionValue (overrides default)\n opacity: opacity, // MotionValue (overrides default)\n maskImage: maskImage, // MotionValue or string\n WebkitMaskImage: maskImage,\n willChange: \"background, opacity\",\n ...style, // User styles override everything\n };\n\n const motionProps: any = {\n style: finalStyles,\n ref: elementRef,\n className: combinedClassName,\n ...(id ? {id} : {}),\n };\n\n return <motion.div {...motionProps} />;\n};\n\nconst throttle = (func: Function) => {\n let ticking = false;\n return function (this: any, ...args: any[]) {\n if (!ticking) {\n requestAnimationFrame(() => {\n func.apply(this, args);\n ticking = false;\n });\n ticking = true;\n }\n };\n};\n"]}
|
|
1
|
+
{"version":3,"sources":["../node_modules/@gsap/react/src/index.js","../src/hooks/useDarkmode.tsx","../src/index.tsx"],"names":["useLayoutEffect","useEffect","gsap","useRef","useState","ScrollTrigger","jsx"],"mappings":"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","file":"index.js","sourcesContent":["/*!\n * @gsap/react 2.1.2\n * https://gsap.com\n *\n * Copyright 2008-2025, GreenSock. All rights reserved.\n * Subject to the terms at https://gsap.com/standard-license or for\n * Club GSAP members, the agreement issued with that membership.\n * @author: Jack Doyle, jack@greensock.com\n*/\n/* eslint-disable */\nimport { useEffect, useLayoutEffect, useRef } from \"react\";\nimport gsap from \"gsap\";\n\nlet useIsomorphicLayoutEffect = typeof document !== \"undefined\" ? useLayoutEffect : useEffect,\n isConfig = value => value && !Array.isArray(value) && typeof(value) === \"object\",\n emptyArray = [],\n defaultConfig = {},\n _gsap = gsap; // accommodates situations where different versions of GSAP may be loaded, so a user can gsap.registerPlugin(useGSAP);\n\nexport const useGSAP = (callback, dependencies = emptyArray) => {\n let config = defaultConfig;\n if (isConfig(callback)) {\n config = callback;\n callback = null;\n dependencies = \"dependencies\" in config ? config.dependencies : emptyArray;\n } else if (isConfig(dependencies)) {\n config = dependencies;\n dependencies = \"dependencies\" in config ? config.dependencies : emptyArray;\n }\n (callback && typeof callback !== \"function\") && console.warn(\"First parameter must be a function or config object\");\n const { scope, revertOnUpdate } = config,\n mounted = useRef(false),\n context = useRef(_gsap.context(() => { }, scope)),\n contextSafe = useRef((func) => context.current.add(null, func)),\n deferCleanup = dependencies && dependencies.length && !revertOnUpdate;\n deferCleanup && useIsomorphicLayoutEffect(() => {\n mounted.current = true;\n return () => context.current.revert();\n }, emptyArray);\n useIsomorphicLayoutEffect(() => {\n callback && context.current.add(callback, scope);\n if (!deferCleanup || !mounted.current) { // React renders bottom-up, thus there could be hooks with dependencies that run BEFORE the component mounts, thus cleanup wouldn't occur since a hook with an empty dependency Array would only run once the component mounts.\n return () => context.current.revert();\n }\n }, dependencies);\n return { context: context.current, contextSafe: contextSafe.current };\n};\nuseGSAP.register = core => { _gsap = core; };\nuseGSAP.headless = true; // doesn't require the window to be registered.\n","\"use client\";\nimport { useEffect, useState } from \"react\";\n\nexport const useIsDarkmode = () => {\n const [isDarkmode, setIsDarkmodeActive] = useState(false);\n\n useEffect(() => {\n const matchMedia = window.matchMedia(\"(prefers-color-scheme: dark)\");\n\n const handleChange = () => {\n setIsDarkmodeActive(matchMedia.matches);\n };\n\n // Set the initial value\n setIsDarkmodeActive(matchMedia.matches);\n\n // Listen for changes\n matchMedia.addEventListener(\"change\", handleChange);\n\n // Cleanup listener on unmount\n return () => {\n matchMedia.removeEventListener(\"change\", handleChange);\n };\n }, []);\n\n return { isDarkmode };\n};\n","\"use client\";\nimport gsap from \"gsap\";\nimport {ScrollTrigger} from \"gsap/ScrollTrigger\";\nimport {useGSAP} from \"@gsap/react\";\nimport React, {useEffect, useRef} from \"react\";\nimport {LightBeamProps} from \"../types/types\";\nimport {useIsDarkmode} from \"./hooks/useDarkmode\";\n\n// Register GSAP plugins\ngsap.registerPlugin(ScrollTrigger, useGSAP);\n\n// Default inline styles using CSS variables for easy customization\n// Users can override via className by setting CSS variables\nconst defaultStyles: React.CSSProperties = {\n height: \"var(--react-light-beam-height, 500px)\",\n width: \"var(--react-light-beam-width, 100vw)\",\n // CRITICAL: NO transition on GSAP-controlled properties (background, opacity, mask)\n // Transitions would fight with GSAP's instant updates, causing visual glitches\n // especially when scroll direction changes\n transition: \"none\",\n willChange: \"background, opacity\", // Specific properties for better performance\n userSelect: \"none\",\n pointerEvents: \"none\",\n contain: \"layout style paint\", // CSS containment for better performance\n WebkitTransition: \"none\",\n WebkitUserSelect: \"none\",\n MozUserSelect: \"none\",\n};\n\nexport const LightBeam = ({\n className,\n style,\n colorLightmode = \"rgba(0,0,0, 0.5)\",\n colorDarkmode = \"rgba(255, 255, 255, 0.5)\",\n maskLightByProgress = false,\n fullWidth = 1.0, // Default to full width\n invert = false,\n id = undefined,\n onLoaded = undefined,\n scrollElement,\n disableDefaultStyles = false,\n }: LightBeamProps) => {\n const elementRef = useRef<HTMLDivElement>(null);\n const {isDarkmode} = useIsDarkmode();\n const chosenColor = isDarkmode ? colorDarkmode : colorLightmode;\n\n // Use refs to track current values without triggering useGSAP re-runs\n const colorRef = useRef(chosenColor);\n const invertRef = useRef(invert);\n const maskByProgressRef = useRef(maskLightByProgress);\n\n // Update refs whenever values change\n useEffect(() => {\n colorRef.current = chosenColor;\n invertRef.current = invert;\n maskByProgressRef.current = maskLightByProgress;\n }, [chosenColor, colorLightmode, colorDarkmode, invert, maskLightByProgress]);\n\n // Call onLoaded callback when component mounts\n useEffect(() => {\n onLoaded && onLoaded();\n }, []);\n\n // GSAP ScrollTrigger implementation\n useGSAP(\n () => {\n const element = elementRef.current;\n if (!element || typeof window === \"undefined\") return;\n\n // Pre-calculate constants for performance\n const opacityMin = 0.839322;\n const opacityRange = 0.160678; // 1 - 0.839322\n\n // Helper function to interpolate background gradient\n // NOTE: Takes color as parameter to always use current value (not closure!)\n const interpolateBackground = (progress: number, color: string): string => {\n // At progress 0: gradients are wide (90% and 10% positions)\n // At progress 1: gradients converge (0% and 100% positions)\n const leftPos = 90 - progress * 90; // 90% → 0%\n const rightPos = 10 + progress * 90; // 10% → 100%\n const leftSize = 150 - progress * 50; // 150% → 100%\n\n return `conic-gradient(from 90deg at ${leftPos}% 0%, ${color}, transparent 180deg) 0% 0% / 50% ${leftSize}% no-repeat, conic-gradient(from 270deg at ${rightPos}% 0%, transparent 180deg, ${color}) 100% 0% / 50% 100% no-repeat`;\n };\n\n // Helper function to interpolate mask\n // NOTE: Takes color as parameter to always use current value (not closure!)\n const interpolateMask = (progress: number, color: string): string => {\n if (!maskByProgressRef.current) {\n return `linear-gradient(to bottom, ${color} 25%, transparent 95%)`;\n }\n const stopPoint = 50 + progress * 45; // 50% → 95%\n return `linear-gradient(to bottom, ${color} 0%, transparent ${stopPoint}%)`;\n };\n\n // EXACT MATCH TO FRAMER MOTION LOGIC:\n // fullWidth controls the MINIMUM beam width, not maximum!\n // fullWidth=1.0 → beam goes from 0% to 100% wide (full range)\n // fullWidth=0.5 → beam goes from 50% to 100% wide (narrower minimum)\n // fullWidth=0.2 → beam goes from 80% to 100% wide (very narrow minimum)\n const adjustedFullWidth = 1 - fullWidth;\n\n // Helper function to calculate progress (EXACTLY like Framer Motion)\n const calculateProgress = (rawProgress: number): number => {\n // ScrollTrigger rawProgress is 0-1 as element moves from start to end\n // We need to convert this to match Framer's rect.top / windowHeight logic\n // CRITICAL: GSAP progress (0→1) is INVERSE of Framer's normalizedPosition (1→0)\n\n // Apply fullWidth floor (minimum progress value)\n const normalizedPosition = Math.max(\n adjustedFullWidth, // Minimum (floor)\n Math.min(1, 1 - rawProgress) // Convert GSAP progress to Framer's normalized position\n );\n\n // Apply invert logic (EXACTLY like Framer Motion)\n // Use invertRef to get current value without closure issues\n return invertRef.current ? normalizedPosition : 1 - normalizedPosition;\n };\n\n // Determine scroll container\n const scroller = scrollElement\n ? (scrollElement as Element | Window)\n : undefined;\n\n // Create ScrollTrigger with FIXED range (like Framer Motion)\n const st = ScrollTrigger.create({\n trigger: element,\n start: \"top bottom\", // Element top hits viewport bottom\n end: \"top top\", // Element top hits viewport top\n scroller: scroller,\n scrub: true, // Instant scrubbing\n onUpdate: (self) => {\n // Calculate progress using Framer Motion logic\n const progress = calculateProgress(self.progress);\n\n // Update styles directly (not via gsap.set to avoid CSS variable parsing issues)\n element.style.background = interpolateBackground(progress, colorRef.current);\n element.style.opacity = String(opacityMin + opacityRange * progress);\n element.style.maskImage = interpolateMask(progress, colorRef.current);\n element.style.webkitMaskImage = interpolateMask(progress, colorRef.current);\n },\n onRefresh: (self) => {\n // Set initial state when ScrollTrigger refreshes\n const progress = calculateProgress(self.progress);\n element.style.background = interpolateBackground(progress, colorRef.current);\n element.style.opacity = String(opacityMin + opacityRange * progress);\n element.style.maskImage = interpolateMask(progress, colorRef.current);\n element.style.webkitMaskImage = interpolateMask(progress, colorRef.current);\n },\n });\n\n // Set initial state immediately\n const initialProgress = calculateProgress(st.progress);\n element.style.background = interpolateBackground(initialProgress, colorRef.current);\n element.style.opacity = String(opacityMin + opacityRange * initialProgress);\n element.style.maskImage = interpolateMask(initialProgress, colorRef.current);\n element.style.webkitMaskImage = interpolateMask(initialProgress, colorRef.current);\n\n // Refresh ScrollTrigger after a brief delay to ensure layout is settled\n // This is especially important for Next.js SSR/hydration\n const refreshTimeout = setTimeout(() => {\n ScrollTrigger.refresh();\n }, 100);\n\n // Cleanup function to kill ScrollTrigger and clear timeout\n return () => {\n st.kill();\n clearTimeout(refreshTimeout);\n };\n },\n {\n // CRITICAL: Use refs for frequently changing values!\n // colorRef, invertRef, maskByProgressRef allow updates without recreating ScrollTrigger\n // This prevents visual glitches when these values change mid-scroll\n // Only include values that affect ScrollTrigger's position/range calculations\n dependencies: [\n fullWidth, // Affects trigger range\n scrollElement, // Affects which element to watch\n ],\n scope: elementRef,\n }\n );\n\n const combinedClassName = `react-light-beam ${className || \"\"}`.trim();\n\n // Prepare final styles (same logic as before, just without MotionValues)\n const finalStyles = disableDefaultStyles\n ? {\n // No default styles, only user styles\n willChange: \"background, opacity\",\n contain: \"layout style paint\",\n ...style, // User styles override\n }\n : {\n // Merge default styles with user styles\n ...defaultStyles,\n ...style, // User styles override everything\n };\n\n return (\n <div\n ref={elementRef}\n className={combinedClassName}\n style={finalStyles}\n {...(id ? {id} : {})}\n />\n );\n};\n\n\n\n// <div class=\"react-light-beam max-h:[500px] absolute top:[-100px] inset:[0]\" style=\"height: var(--react-light-beam-height, 500px); width: var(--react-light-beam-width, 100vw); will-change: background,\n// opacity; pointer-events: none; contain: layout style paint; transition: var(--react-light-beam-transition, all 0.25s ease); mask-image: linear-gradient(rgba(255, 255, 255, 0.8) 25%, transparent 95%);\n// opacity: 0.9434; background: conic-gradient(from 90deg at 31.723% 0%, rgba(255, 255, 255, 0.8), transparent 180deg) 0% 0% / 50% 117.624% no-repeat, conic-gradient(from 270deg at 68.277% 0%, transparent\n// 180deg, rgba(255, 255, 255, 0.8)) 100% 0% / 50% 100% no-repeat;\"></div>\n\n// <div class=\"react-light-beam max-h:[500px] absolute top:[-100px] inset:[0]\" style=\"height: var(--react-light-beam-height, 500px); width: var(--react-light-beam-width, 100vw); will-change: background,\n// opacity; pointer-events: none; contain: layout style paint; transition: var(--react-light-beam-transition, all 0.25s ease); mask-image: linear-gradient(rgba(0, 0, 0, 0.2) 25%, transparent 95%); opacity:\n// 0.941; background: conic-gradient(from 90deg at 33.0405% 0%, rgba(0, 0, 0, 0.2), transparent 180deg) 0% 0% / 50% 118.356% no-repeat, conic-gradient(from 270deg at 66.9595% 0%, transparent 180deg, rgba(0, 0,\n// 0, 0.2)) 100% 0% / 50% 100% no-repeat;\"></div>\n\n"]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,43 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
2
|
+
import gsap from 'gsap';
|
|
3
|
+
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
|
4
|
+
import { useRef, useLayoutEffect, useEffect, useState } from 'react';
|
|
4
5
|
import { jsx } from 'react/jsx-runtime';
|
|
5
6
|
|
|
7
|
+
var useIsomorphicLayoutEffect = typeof document !== "undefined" ? useLayoutEffect : useEffect;
|
|
8
|
+
var isConfig = (value) => value && !Array.isArray(value) && typeof value === "object";
|
|
9
|
+
var emptyArray = [];
|
|
10
|
+
var defaultConfig = {};
|
|
11
|
+
var _gsap = gsap;
|
|
12
|
+
var useGSAP = (callback, dependencies = emptyArray) => {
|
|
13
|
+
let config = defaultConfig;
|
|
14
|
+
if (isConfig(callback)) {
|
|
15
|
+
config = callback;
|
|
16
|
+
callback = null;
|
|
17
|
+
dependencies = "dependencies" in config ? config.dependencies : emptyArray;
|
|
18
|
+
} else if (isConfig(dependencies)) {
|
|
19
|
+
config = dependencies;
|
|
20
|
+
dependencies = "dependencies" in config ? config.dependencies : emptyArray;
|
|
21
|
+
}
|
|
22
|
+
callback && typeof callback !== "function" && console.warn("First parameter must be a function or config object");
|
|
23
|
+
const { scope, revertOnUpdate } = config, mounted = useRef(false), context = useRef(_gsap.context(() => {
|
|
24
|
+
}, scope)), contextSafe = useRef((func) => context.current.add(null, func)), deferCleanup = dependencies && dependencies.length && !revertOnUpdate;
|
|
25
|
+
deferCleanup && useIsomorphicLayoutEffect(() => {
|
|
26
|
+
mounted.current = true;
|
|
27
|
+
return () => context.current.revert();
|
|
28
|
+
}, emptyArray);
|
|
29
|
+
useIsomorphicLayoutEffect(() => {
|
|
30
|
+
callback && context.current.add(callback, scope);
|
|
31
|
+
if (!deferCleanup || !mounted.current) {
|
|
32
|
+
return () => context.current.revert();
|
|
33
|
+
}
|
|
34
|
+
}, dependencies);
|
|
35
|
+
return { context: context.current, contextSafe: contextSafe.current };
|
|
36
|
+
};
|
|
37
|
+
useGSAP.register = (core) => {
|
|
38
|
+
_gsap = core;
|
|
39
|
+
};
|
|
40
|
+
useGSAP.headless = true;
|
|
6
41
|
var useIsDarkmode = () => {
|
|
7
42
|
const [isDarkmode, setIsDarkmodeActive] = useState(false);
|
|
8
43
|
useEffect(() => {
|
|
@@ -18,17 +53,21 @@ var useIsDarkmode = () => {
|
|
|
18
53
|
}, []);
|
|
19
54
|
return { isDarkmode };
|
|
20
55
|
};
|
|
56
|
+
gsap.registerPlugin(ScrollTrigger, useGSAP);
|
|
21
57
|
var defaultStyles = {
|
|
22
58
|
height: "var(--react-light-beam-height, 500px)",
|
|
23
59
|
width: "var(--react-light-beam-width, 100vw)",
|
|
24
|
-
|
|
60
|
+
// CRITICAL: NO transition on GSAP-controlled properties (background, opacity, mask)
|
|
61
|
+
// Transitions would fight with GSAP's instant updates, causing visual glitches
|
|
62
|
+
// especially when scroll direction changes
|
|
63
|
+
transition: "none",
|
|
25
64
|
willChange: "background, opacity",
|
|
26
65
|
// Specific properties for better performance
|
|
27
66
|
userSelect: "none",
|
|
28
67
|
pointerEvents: "none",
|
|
29
68
|
contain: "layout style paint",
|
|
30
69
|
// CSS containment for better performance
|
|
31
|
-
WebkitTransition: "
|
|
70
|
+
WebkitTransition: "none",
|
|
32
71
|
WebkitUserSelect: "none",
|
|
33
72
|
MozUserSelect: "none"
|
|
34
73
|
};
|
|
@@ -47,112 +86,136 @@ var LightBeam = ({
|
|
|
47
86
|
disableDefaultStyles = false
|
|
48
87
|
}) => {
|
|
49
88
|
const elementRef = useRef(null);
|
|
50
|
-
const inViewProgress = useMotionValue(0);
|
|
51
|
-
const opacity = useMotionValue(0.839322);
|
|
52
89
|
const { isDarkmode } = useIsDarkmode();
|
|
53
90
|
const chosenColor = isDarkmode ? colorDarkmode : colorLightmode;
|
|
91
|
+
const colorRef = useRef(chosenColor);
|
|
92
|
+
const invertRef = useRef(invert);
|
|
93
|
+
const maskByProgressRef = useRef(maskLightByProgress);
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
colorRef.current = chosenColor;
|
|
96
|
+
invertRef.current = invert;
|
|
97
|
+
maskByProgressRef.current = maskLightByProgress;
|
|
98
|
+
}, [chosenColor, colorLightmode, colorDarkmode, invert, maskLightByProgress]);
|
|
54
99
|
useEffect(() => {
|
|
55
100
|
onLoaded && onLoaded();
|
|
56
101
|
}, []);
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
102
|
+
useGSAP(
|
|
103
|
+
() => {
|
|
104
|
+
const element = elementRef.current;
|
|
105
|
+
if (!element || typeof window === "undefined") return;
|
|
106
|
+
const opacityMin = 0.839322;
|
|
107
|
+
const opacityRange = 0.160678;
|
|
108
|
+
const interpolateBackground = (progress, color) => {
|
|
109
|
+
const leftPos = 90 - progress * 90;
|
|
110
|
+
const rightPos = 10 + progress * 90;
|
|
111
|
+
const leftSize = 150 - progress * 50;
|
|
112
|
+
return `conic-gradient(from 90deg at ${leftPos}% 0%, ${color}, transparent 180deg) 0% 0% / 50% ${leftSize}% no-repeat, conic-gradient(from 270deg at ${rightPos}% 0%, transparent 180deg, ${color}) 100% 0% / 50% 100% no-repeat`;
|
|
113
|
+
};
|
|
114
|
+
const interpolateMask = (progress, color) => {
|
|
115
|
+
if (!maskByProgressRef.current) {
|
|
116
|
+
return `linear-gradient(to bottom, ${color} 25%, transparent 95%)`;
|
|
117
|
+
}
|
|
118
|
+
const stopPoint = 50 + progress * 45;
|
|
119
|
+
return `linear-gradient(to bottom, ${color} 0%, transparent ${stopPoint}%)`;
|
|
120
|
+
};
|
|
121
|
+
const adjustedFullWidth = 1 - fullWidth;
|
|
122
|
+
const calculateProgress = (rawProgress) => {
|
|
123
|
+
const normalizedPosition = Math.max(
|
|
124
|
+
adjustedFullWidth,
|
|
125
|
+
// Minimum (floor)
|
|
126
|
+
Math.min(1, 1 - rawProgress)
|
|
127
|
+
// Convert GSAP progress to Framer's normalized position
|
|
128
|
+
);
|
|
129
|
+
return invertRef.current ? normalizedPosition : 1 - normalizedPosition;
|
|
130
|
+
};
|
|
131
|
+
const scroller = scrollElement ? scrollElement : void 0;
|
|
132
|
+
const st = ScrollTrigger.create({
|
|
133
|
+
trigger: element,
|
|
134
|
+
start: "top bottom",
|
|
135
|
+
// Element top hits viewport bottom
|
|
136
|
+
end: "top top",
|
|
137
|
+
// Element top hits viewport top
|
|
138
|
+
scroller,
|
|
139
|
+
scrub: true,
|
|
140
|
+
// Instant scrubbing
|
|
141
|
+
onUpdate: (self) => {
|
|
142
|
+
const progress = calculateProgress(self.progress);
|
|
143
|
+
element.style.background = interpolateBackground(progress, colorRef.current);
|
|
144
|
+
element.style.opacity = String(opacityMin + opacityRange * progress);
|
|
145
|
+
element.style.maskImage = interpolateMask(progress, colorRef.current);
|
|
146
|
+
element.style.webkitMaskImage = interpolateMask(progress, colorRef.current);
|
|
147
|
+
},
|
|
148
|
+
onRefresh: (self) => {
|
|
149
|
+
const progress = calculateProgress(self.progress);
|
|
150
|
+
element.style.background = interpolateBackground(progress, colorRef.current);
|
|
151
|
+
element.style.opacity = String(opacityMin + opacityRange * progress);
|
|
152
|
+
element.style.maskImage = interpolateMask(progress, colorRef.current);
|
|
153
|
+
element.style.webkitMaskImage = interpolateMask(progress, colorRef.current);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
const initialProgress = calculateProgress(st.progress);
|
|
157
|
+
element.style.background = interpolateBackground(initialProgress, colorRef.current);
|
|
158
|
+
element.style.opacity = String(opacityMin + opacityRange * initialProgress);
|
|
159
|
+
element.style.maskImage = interpolateMask(initialProgress, colorRef.current);
|
|
160
|
+
element.style.webkitMaskImage = interpolateMask(initialProgress, colorRef.current);
|
|
161
|
+
const refreshTimeout = setTimeout(() => {
|
|
162
|
+
ScrollTrigger.refresh();
|
|
163
|
+
}, 100);
|
|
164
|
+
return () => {
|
|
165
|
+
st.kill();
|
|
166
|
+
clearTimeout(refreshTimeout);
|
|
167
|
+
};
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
// CRITICAL: Use refs for frequently changing values!
|
|
171
|
+
// colorRef, invertRef, maskByProgressRef allow updates without recreating ScrollTrigger
|
|
172
|
+
// This prevents visual glitches when these values change mid-scroll
|
|
173
|
+
// Only include values that affect ScrollTrigger's position/range calculations
|
|
174
|
+
dependencies: [
|
|
175
|
+
fullWidth,
|
|
176
|
+
// Affects trigger range
|
|
177
|
+
scrollElement
|
|
178
|
+
// Affects which element to watch
|
|
179
|
+
],
|
|
180
|
+
scope: elementRef
|
|
181
|
+
}
|
|
108
182
|
);
|
|
109
|
-
const maskImage = maskLightByProgress ? maskImageOpacity : `linear-gradient(to bottom, ${chosenColor} 25%, transparent 95%)`;
|
|
110
183
|
const combinedClassName = `react-light-beam ${className || ""}`.trim();
|
|
111
184
|
const finalStyles = disableDefaultStyles ? {
|
|
112
|
-
// No default styles, only
|
|
113
|
-
background: backgroundPosition,
|
|
114
|
-
opacity,
|
|
115
|
-
maskImage,
|
|
116
|
-
WebkitMaskImage: maskImage,
|
|
185
|
+
// No default styles, only user styles
|
|
117
186
|
willChange: "background, opacity",
|
|
118
187
|
contain: "layout style paint",
|
|
119
|
-
// CSS containment for better performance
|
|
120
188
|
...style
|
|
121
189
|
// User styles override
|
|
122
190
|
} : {
|
|
123
|
-
// Merge default styles with
|
|
191
|
+
// Merge default styles with user styles
|
|
124
192
|
...defaultStyles,
|
|
125
|
-
background: backgroundPosition,
|
|
126
|
-
// MotionValue (overrides default)
|
|
127
|
-
opacity,
|
|
128
|
-
// MotionValue (overrides default)
|
|
129
|
-
maskImage,
|
|
130
|
-
// MotionValue or string
|
|
131
|
-
WebkitMaskImage: maskImage,
|
|
132
|
-
willChange: "background, opacity",
|
|
133
193
|
...style
|
|
134
194
|
// User styles override everything
|
|
135
195
|
};
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
};
|
|
144
|
-
var throttle = (func) => {
|
|
145
|
-
let ticking = false;
|
|
146
|
-
return function(...args) {
|
|
147
|
-
if (!ticking) {
|
|
148
|
-
requestAnimationFrame(() => {
|
|
149
|
-
func.apply(this, args);
|
|
150
|
-
ticking = false;
|
|
151
|
-
});
|
|
152
|
-
ticking = true;
|
|
196
|
+
return /* @__PURE__ */ jsx(
|
|
197
|
+
"div",
|
|
198
|
+
{
|
|
199
|
+
ref: elementRef,
|
|
200
|
+
className: combinedClassName,
|
|
201
|
+
style: finalStyles,
|
|
202
|
+
...id ? { id } : {}
|
|
153
203
|
}
|
|
154
|
-
|
|
204
|
+
);
|
|
155
205
|
};
|
|
206
|
+
/*! Bundled license information:
|
|
207
|
+
|
|
208
|
+
@gsap/react/src/index.js:
|
|
209
|
+
(*!
|
|
210
|
+
* @gsap/react 2.1.2
|
|
211
|
+
* https://gsap.com
|
|
212
|
+
*
|
|
213
|
+
* Copyright 2008-2025, GreenSock. All rights reserved.
|
|
214
|
+
* Subject to the terms at https://gsap.com/standard-license or for
|
|
215
|
+
* Club GSAP members, the agreement issued with that membership.
|
|
216
|
+
* @author: Jack Doyle, jack@greensock.com
|
|
217
|
+
*)
|
|
218
|
+
*/
|
|
156
219
|
|
|
157
220
|
export { LightBeam };
|
|
158
221
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/hooks/useDarkmode.tsx","../src/index.tsx"],"names":["useEffect"],"mappings":"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","file":"index.mjs","sourcesContent":["\"use client\";\nimport { useEffect, useState } from \"react\";\n\nexport const useIsDarkmode = () => {\n const [isDarkmode, setIsDarkmodeActive] = useState(false);\n\n useEffect(() => {\n const matchMedia = window.matchMedia(\"(prefers-color-scheme: dark)\");\n\n const handleChange = () => {\n setIsDarkmodeActive(matchMedia.matches);\n };\n\n // Set the initial value\n setIsDarkmodeActive(matchMedia.matches);\n\n // Listen for changes\n matchMedia.addEventListener(\"change\", handleChange);\n\n // Cleanup listener on unmount\n return () => {\n matchMedia.removeEventListener(\"change\", handleChange);\n };\n }, []);\n\n return { isDarkmode };\n};\n","\"use client\";\nimport {motion, useMotionValue, useTransform} from \"framer-motion\";\nimport React, {useEffect, useRef} from \"react\";\nimport {LightBeamProps} from \"../types/types\";\nimport {useIsDarkmode} from \"./hooks/useDarkmode\";\n\n// Default inline styles using CSS variables for easy customization\n// Users can override via className by setting CSS variables\nconst defaultStyles: React.CSSProperties = {\n height: \"var(--react-light-beam-height, 500px)\",\n width: \"var(--react-light-beam-width, 100vw)\",\n transition: \"var(--react-light-beam-transition, all 0.25s ease)\",\n willChange: \"background, opacity\", // Specific properties for better performance\n userSelect: \"none\",\n pointerEvents: \"none\",\n contain: \"layout style paint\", // CSS containment for better performance\n WebkitTransition: \"var(--react-light-beam-transition, all 0.25s ease)\",\n WebkitUserSelect: \"none\",\n MozUserSelect: \"none\",\n};\n\nexport const LightBeam = ({\n className,\n style,\n colorLightmode = \"rgba(0,0,0, 0.5)\",\n colorDarkmode = \"rgba(255, 255, 255, 0.5)\",\n maskLightByProgress = false,\n fullWidth = 1.0, // Default to full width\n invert = false,\n id = undefined,\n onLoaded = undefined,\n scrollElement,\n disableDefaultStyles = false,\n }: LightBeamProps) => {\n const elementRef = useRef<HTMLDivElement>(null);\n const inViewProgress = useMotionValue(0);\n const opacity = useMotionValue(0.839322);\n const {isDarkmode} = useIsDarkmode();\n const chosenColor = isDarkmode ? colorDarkmode : colorLightmode;\n\n useEffect(() => {\n onLoaded && onLoaded();\n }, []);\n\n useEffect(() => {\n if (typeof window === \"undefined\") return\n\n // Cache values that don't change during scroll (MAJOR OPTIMIZATION)\n let cachedWindowHeight = window.innerHeight;\n const adjustedFullWidth = 1 - fullWidth; // Only calculate once\n const opacityMin = 0.839322;\n const opacityRange = 1 - opacityMin; // Pre-calculate: 0.160678\n let lastProgress = -1;\n\n const handleScroll = () => {\n if (!elementRef.current) return;\n\n // OPTIMIZATION: Only call getBoundingClientRect (expensive!)\n const rect = elementRef.current.getBoundingClientRect();\n\n // Calculate normalized position (0-1 range)\n const normalizedPosition = Math.max(\n adjustedFullWidth,\n Math.min(1, rect.top / cachedWindowHeight)\n );\n\n // Calculate progress (only once, not twice like before!)\n const progress = invert ? normalizedPosition : 1 - normalizedPosition;\n\n // Only update if change is significant (avoid micro-updates)\n if (Math.abs(progress - lastProgress) > 0.001) {\n lastProgress = progress;\n // Batch updates together\n inViewProgress.set(progress);\n opacity.set(opacityMin + opacityRange * progress);\n }\n };\n\n const handleResize = () => {\n cachedWindowHeight = window.innerHeight; // Update cache on resize\n handleScroll(); // Recalculate immediately\n };\n\n const handleScrollThrottled = throttle(handleScroll);\n const handleResizeThrottled = throttle(handleResize);\n\n // Default to document.body (works in modern React/Next.js setups)\n const target = scrollElement || document.body || document.documentElement;\n\n // Passive listeners improve scroll performance significantly\n target.addEventListener(\"scroll\", handleScrollThrottled, {passive: true});\n window.addEventListener(\"resize\", handleResizeThrottled, {passive: true});\n\n // Initial call to set state\n handleScroll();\n\n return () => {\n target.removeEventListener(\"scroll\", handleScrollThrottled);\n window.removeEventListener(\"resize\", handleResizeThrottled);\n };\n }, [inViewProgress, opacity, scrollElement, fullWidth, invert]);\n\n const backgroundPosition = useTransform(\n inViewProgress,\n [0, 1],\n [\n `conic-gradient(from 90deg at 90% 0%, ${chosenColor}, transparent 180deg) 0% 0% / 50% 150% no-repeat, conic-gradient(from 270deg at 10% 0%, transparent 180deg, ${chosenColor}) 100% 0% / 50% 100% no-repeat`,\n `conic-gradient(from 90deg at 0% 0%, ${chosenColor}, transparent 180deg) 0% 0% / 50% 100% no-repeat, conic-gradient(from 270deg at 100% 0%, transparent 180deg, ${chosenColor}) 100% 0% / 50% 100% no-repeat`,\n ]\n );\n const maskImageOpacity = useTransform(\n inViewProgress,\n [0, 1],\n [\n `linear-gradient(to bottom, ${chosenColor} 0%, transparent 50%)`,\n `linear-gradient(to bottom, ${chosenColor} 0%, transparent 95%)`,\n ]\n );\n\n const maskImage = maskLightByProgress\n ? maskImageOpacity\n : `linear-gradient(to bottom, ${chosenColor} 25%, transparent 95%)`;\n\n const combinedClassName = `react-light-beam ${className || \"\"}`.trim();\n\n // CRITICAL: MotionValues must be passed directly to motion.div style prop\n // Don't spread them into plain objects or reactivity breaks!\n const finalStyles = disableDefaultStyles\n ? {\n // No default styles, only motion values and user styles\n background: backgroundPosition,\n opacity: opacity,\n maskImage: maskImage,\n WebkitMaskImage: maskImage,\n willChange: \"background, opacity\",\n contain: \"layout style paint\", // CSS containment for better performance\n ...style, // User styles override\n }\n : {\n // Merge default styles with motion values\n ...defaultStyles,\n background: backgroundPosition, // MotionValue (overrides default)\n opacity: opacity, // MotionValue (overrides default)\n maskImage: maskImage, // MotionValue or string\n WebkitMaskImage: maskImage,\n willChange: \"background, opacity\",\n ...style, // User styles override everything\n };\n\n const motionProps: any = {\n style: finalStyles,\n ref: elementRef,\n className: combinedClassName,\n ...(id ? {id} : {}),\n };\n\n return <motion.div {...motionProps} />;\n};\n\nconst throttle = (func: Function) => {\n let ticking = false;\n return function (this: any, ...args: any[]) {\n if (!ticking) {\n requestAnimationFrame(() => {\n func.apply(this, args);\n ticking = false;\n });\n ticking = true;\n }\n };\n};\n"]}
|
|
1
|
+
{"version":3,"sources":["../node_modules/@gsap/react/src/index.js","../src/hooks/useDarkmode.tsx","../src/index.tsx"],"names":["useEffect","gsap","useRef"],"mappings":"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","file":"index.mjs","sourcesContent":["/*!\n * @gsap/react 2.1.2\n * https://gsap.com\n *\n * Copyright 2008-2025, GreenSock. All rights reserved.\n * Subject to the terms at https://gsap.com/standard-license or for\n * Club GSAP members, the agreement issued with that membership.\n * @author: Jack Doyle, jack@greensock.com\n*/\n/* eslint-disable */\nimport { useEffect, useLayoutEffect, useRef } from \"react\";\nimport gsap from \"gsap\";\n\nlet useIsomorphicLayoutEffect = typeof document !== \"undefined\" ? useLayoutEffect : useEffect,\n isConfig = value => value && !Array.isArray(value) && typeof(value) === \"object\",\n emptyArray = [],\n defaultConfig = {},\n _gsap = gsap; // accommodates situations where different versions of GSAP may be loaded, so a user can gsap.registerPlugin(useGSAP);\n\nexport const useGSAP = (callback, dependencies = emptyArray) => {\n let config = defaultConfig;\n if (isConfig(callback)) {\n config = callback;\n callback = null;\n dependencies = \"dependencies\" in config ? config.dependencies : emptyArray;\n } else if (isConfig(dependencies)) {\n config = dependencies;\n dependencies = \"dependencies\" in config ? config.dependencies : emptyArray;\n }\n (callback && typeof callback !== \"function\") && console.warn(\"First parameter must be a function or config object\");\n const { scope, revertOnUpdate } = config,\n mounted = useRef(false),\n context = useRef(_gsap.context(() => { }, scope)),\n contextSafe = useRef((func) => context.current.add(null, func)),\n deferCleanup = dependencies && dependencies.length && !revertOnUpdate;\n deferCleanup && useIsomorphicLayoutEffect(() => {\n mounted.current = true;\n return () => context.current.revert();\n }, emptyArray);\n useIsomorphicLayoutEffect(() => {\n callback && context.current.add(callback, scope);\n if (!deferCleanup || !mounted.current) { // React renders bottom-up, thus there could be hooks with dependencies that run BEFORE the component mounts, thus cleanup wouldn't occur since a hook with an empty dependency Array would only run once the component mounts.\n return () => context.current.revert();\n }\n }, dependencies);\n return { context: context.current, contextSafe: contextSafe.current };\n};\nuseGSAP.register = core => { _gsap = core; };\nuseGSAP.headless = true; // doesn't require the window to be registered.\n","\"use client\";\nimport { useEffect, useState } from \"react\";\n\nexport const useIsDarkmode = () => {\n const [isDarkmode, setIsDarkmodeActive] = useState(false);\n\n useEffect(() => {\n const matchMedia = window.matchMedia(\"(prefers-color-scheme: dark)\");\n\n const handleChange = () => {\n setIsDarkmodeActive(matchMedia.matches);\n };\n\n // Set the initial value\n setIsDarkmodeActive(matchMedia.matches);\n\n // Listen for changes\n matchMedia.addEventListener(\"change\", handleChange);\n\n // Cleanup listener on unmount\n return () => {\n matchMedia.removeEventListener(\"change\", handleChange);\n };\n }, []);\n\n return { isDarkmode };\n};\n","\"use client\";\nimport gsap from \"gsap\";\nimport {ScrollTrigger} from \"gsap/ScrollTrigger\";\nimport {useGSAP} from \"@gsap/react\";\nimport React, {useEffect, useRef} from \"react\";\nimport {LightBeamProps} from \"../types/types\";\nimport {useIsDarkmode} from \"./hooks/useDarkmode\";\n\n// Register GSAP plugins\ngsap.registerPlugin(ScrollTrigger, useGSAP);\n\n// Default inline styles using CSS variables for easy customization\n// Users can override via className by setting CSS variables\nconst defaultStyles: React.CSSProperties = {\n height: \"var(--react-light-beam-height, 500px)\",\n width: \"var(--react-light-beam-width, 100vw)\",\n // CRITICAL: NO transition on GSAP-controlled properties (background, opacity, mask)\n // Transitions would fight with GSAP's instant updates, causing visual glitches\n // especially when scroll direction changes\n transition: \"none\",\n willChange: \"background, opacity\", // Specific properties for better performance\n userSelect: \"none\",\n pointerEvents: \"none\",\n contain: \"layout style paint\", // CSS containment for better performance\n WebkitTransition: \"none\",\n WebkitUserSelect: \"none\",\n MozUserSelect: \"none\",\n};\n\nexport const LightBeam = ({\n className,\n style,\n colorLightmode = \"rgba(0,0,0, 0.5)\",\n colorDarkmode = \"rgba(255, 255, 255, 0.5)\",\n maskLightByProgress = false,\n fullWidth = 1.0, // Default to full width\n invert = false,\n id = undefined,\n onLoaded = undefined,\n scrollElement,\n disableDefaultStyles = false,\n }: LightBeamProps) => {\n const elementRef = useRef<HTMLDivElement>(null);\n const {isDarkmode} = useIsDarkmode();\n const chosenColor = isDarkmode ? colorDarkmode : colorLightmode;\n\n // Use refs to track current values without triggering useGSAP re-runs\n const colorRef = useRef(chosenColor);\n const invertRef = useRef(invert);\n const maskByProgressRef = useRef(maskLightByProgress);\n\n // Update refs whenever values change\n useEffect(() => {\n colorRef.current = chosenColor;\n invertRef.current = invert;\n maskByProgressRef.current = maskLightByProgress;\n }, [chosenColor, colorLightmode, colorDarkmode, invert, maskLightByProgress]);\n\n // Call onLoaded callback when component mounts\n useEffect(() => {\n onLoaded && onLoaded();\n }, []);\n\n // GSAP ScrollTrigger implementation\n useGSAP(\n () => {\n const element = elementRef.current;\n if (!element || typeof window === \"undefined\") return;\n\n // Pre-calculate constants for performance\n const opacityMin = 0.839322;\n const opacityRange = 0.160678; // 1 - 0.839322\n\n // Helper function to interpolate background gradient\n // NOTE: Takes color as parameter to always use current value (not closure!)\n const interpolateBackground = (progress: number, color: string): string => {\n // At progress 0: gradients are wide (90% and 10% positions)\n // At progress 1: gradients converge (0% and 100% positions)\n const leftPos = 90 - progress * 90; // 90% → 0%\n const rightPos = 10 + progress * 90; // 10% → 100%\n const leftSize = 150 - progress * 50; // 150% → 100%\n\n return `conic-gradient(from 90deg at ${leftPos}% 0%, ${color}, transparent 180deg) 0% 0% / 50% ${leftSize}% no-repeat, conic-gradient(from 270deg at ${rightPos}% 0%, transparent 180deg, ${color}) 100% 0% / 50% 100% no-repeat`;\n };\n\n // Helper function to interpolate mask\n // NOTE: Takes color as parameter to always use current value (not closure!)\n const interpolateMask = (progress: number, color: string): string => {\n if (!maskByProgressRef.current) {\n return `linear-gradient(to bottom, ${color} 25%, transparent 95%)`;\n }\n const stopPoint = 50 + progress * 45; // 50% → 95%\n return `linear-gradient(to bottom, ${color} 0%, transparent ${stopPoint}%)`;\n };\n\n // EXACT MATCH TO FRAMER MOTION LOGIC:\n // fullWidth controls the MINIMUM beam width, not maximum!\n // fullWidth=1.0 → beam goes from 0% to 100% wide (full range)\n // fullWidth=0.5 → beam goes from 50% to 100% wide (narrower minimum)\n // fullWidth=0.2 → beam goes from 80% to 100% wide (very narrow minimum)\n const adjustedFullWidth = 1 - fullWidth;\n\n // Helper function to calculate progress (EXACTLY like Framer Motion)\n const calculateProgress = (rawProgress: number): number => {\n // ScrollTrigger rawProgress is 0-1 as element moves from start to end\n // We need to convert this to match Framer's rect.top / windowHeight logic\n // CRITICAL: GSAP progress (0→1) is INVERSE of Framer's normalizedPosition (1→0)\n\n // Apply fullWidth floor (minimum progress value)\n const normalizedPosition = Math.max(\n adjustedFullWidth, // Minimum (floor)\n Math.min(1, 1 - rawProgress) // Convert GSAP progress to Framer's normalized position\n );\n\n // Apply invert logic (EXACTLY like Framer Motion)\n // Use invertRef to get current value without closure issues\n return invertRef.current ? normalizedPosition : 1 - normalizedPosition;\n };\n\n // Determine scroll container\n const scroller = scrollElement\n ? (scrollElement as Element | Window)\n : undefined;\n\n // Create ScrollTrigger with FIXED range (like Framer Motion)\n const st = ScrollTrigger.create({\n trigger: element,\n start: \"top bottom\", // Element top hits viewport bottom\n end: \"top top\", // Element top hits viewport top\n scroller: scroller,\n scrub: true, // Instant scrubbing\n onUpdate: (self) => {\n // Calculate progress using Framer Motion logic\n const progress = calculateProgress(self.progress);\n\n // Update styles directly (not via gsap.set to avoid CSS variable parsing issues)\n element.style.background = interpolateBackground(progress, colorRef.current);\n element.style.opacity = String(opacityMin + opacityRange * progress);\n element.style.maskImage = interpolateMask(progress, colorRef.current);\n element.style.webkitMaskImage = interpolateMask(progress, colorRef.current);\n },\n onRefresh: (self) => {\n // Set initial state when ScrollTrigger refreshes\n const progress = calculateProgress(self.progress);\n element.style.background = interpolateBackground(progress, colorRef.current);\n element.style.opacity = String(opacityMin + opacityRange * progress);\n element.style.maskImage = interpolateMask(progress, colorRef.current);\n element.style.webkitMaskImage = interpolateMask(progress, colorRef.current);\n },\n });\n\n // Set initial state immediately\n const initialProgress = calculateProgress(st.progress);\n element.style.background = interpolateBackground(initialProgress, colorRef.current);\n element.style.opacity = String(opacityMin + opacityRange * initialProgress);\n element.style.maskImage = interpolateMask(initialProgress, colorRef.current);\n element.style.webkitMaskImage = interpolateMask(initialProgress, colorRef.current);\n\n // Refresh ScrollTrigger after a brief delay to ensure layout is settled\n // This is especially important for Next.js SSR/hydration\n const refreshTimeout = setTimeout(() => {\n ScrollTrigger.refresh();\n }, 100);\n\n // Cleanup function to kill ScrollTrigger and clear timeout\n return () => {\n st.kill();\n clearTimeout(refreshTimeout);\n };\n },\n {\n // CRITICAL: Use refs for frequently changing values!\n // colorRef, invertRef, maskByProgressRef allow updates without recreating ScrollTrigger\n // This prevents visual glitches when these values change mid-scroll\n // Only include values that affect ScrollTrigger's position/range calculations\n dependencies: [\n fullWidth, // Affects trigger range\n scrollElement, // Affects which element to watch\n ],\n scope: elementRef,\n }\n );\n\n const combinedClassName = `react-light-beam ${className || \"\"}`.trim();\n\n // Prepare final styles (same logic as before, just without MotionValues)\n const finalStyles = disableDefaultStyles\n ? {\n // No default styles, only user styles\n willChange: \"background, opacity\",\n contain: \"layout style paint\",\n ...style, // User styles override\n }\n : {\n // Merge default styles with user styles\n ...defaultStyles,\n ...style, // User styles override everything\n };\n\n return (\n <div\n ref={elementRef}\n className={combinedClassName}\n style={finalStyles}\n {...(id ? {id} : {})}\n />\n );\n};\n\n\n\n// <div class=\"react-light-beam max-h:[500px] absolute top:[-100px] inset:[0]\" style=\"height: var(--react-light-beam-height, 500px); width: var(--react-light-beam-width, 100vw); will-change: background,\n// opacity; pointer-events: none; contain: layout style paint; transition: var(--react-light-beam-transition, all 0.25s ease); mask-image: linear-gradient(rgba(255, 255, 255, 0.8) 25%, transparent 95%);\n// opacity: 0.9434; background: conic-gradient(from 90deg at 31.723% 0%, rgba(255, 255, 255, 0.8), transparent 180deg) 0% 0% / 50% 117.624% no-repeat, conic-gradient(from 270deg at 68.277% 0%, transparent\n// 180deg, rgba(255, 255, 255, 0.8)) 100% 0% / 50% 100% no-repeat;\"></div>\n\n// <div class=\"react-light-beam max-h:[500px] absolute top:[-100px] inset:[0]\" style=\"height: var(--react-light-beam-height, 500px); width: var(--react-light-beam-width, 100vw); will-change: background,\n// opacity; pointer-events: none; contain: layout style paint; transition: var(--react-light-beam-transition, all 0.25s ease); mask-image: linear-gradient(rgba(0, 0, 0, 0.2) 25%, transparent 95%); opacity:\n// 0.941; background: conic-gradient(from 90deg at 33.0405% 0%, rgba(0, 0, 0, 0.2), transparent 180deg) 0% 0% / 50% 118.356% no-repeat, conic-gradient(from 270deg at 66.9595% 0%, transparent 180deg, rgba(0, 0,\n// 0, 0.2)) 100% 0% / 50% 100% no-repeat;\"></div>\n\n"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stianlarsen/react-light-beam",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "A customizable React component that creates a light beam effect using conic gradients. Supports dark mode and various customization options.",
|
|
3
|
+
"version": "2.1.2",
|
|
4
|
+
"description": "A customizable React component that creates a light beam effect using conic gradients. Powered by GSAP for maximum performance. Supports dark mode and various customization options.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -38,7 +38,9 @@
|
|
|
38
38
|
"light beam",
|
|
39
39
|
"conic gradient",
|
|
40
40
|
"dark mode",
|
|
41
|
-
"
|
|
41
|
+
"gsap",
|
|
42
|
+
"scrolltrigger",
|
|
43
|
+
"scroll animation",
|
|
42
44
|
"animation"
|
|
43
45
|
],
|
|
44
46
|
"author": "Stian Larsen <stian.larsen@mac.com>",
|
|
@@ -48,14 +50,15 @@
|
|
|
48
50
|
},
|
|
49
51
|
"homepage": "https://github.com/stianalars1/react-light-beam#readme",
|
|
50
52
|
"peerDependencies": {
|
|
51
|
-
"
|
|
53
|
+
"gsap": "^3.12.0",
|
|
52
54
|
"react": "^18.0.0 || ^19.0.0",
|
|
53
55
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
54
56
|
},
|
|
55
57
|
"devDependencies": {
|
|
58
|
+
"@gsap/react": "^2.1.2",
|
|
56
59
|
"@types/react": "^19.2.7",
|
|
57
60
|
"@types/react-dom": "^19.2.3",
|
|
58
|
-
"
|
|
61
|
+
"gsap": "^3.12.5",
|
|
59
62
|
"react": "^19.2.3",
|
|
60
63
|
"react-dom": "^19.2.3",
|
|
61
64
|
"tsup": "^8.5.1",
|