@stianlarsen/react-light-beam 2.1.0 → 3.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/index.js CHANGED
@@ -1,65 +1,249 @@
1
1
  "use client";
2
2
  'use strict';
3
3
 
4
- var gsap2 = require('gsap');
4
+ var gsap3 = require('gsap');
5
5
  var ScrollTrigger = require('gsap/ScrollTrigger');
6
- var react = require('react');
6
+ var react = require('@gsap/react');
7
+ var react$1 = require('react');
7
8
  var jsxRuntime = require('react/jsx-runtime');
8
9
 
9
10
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
10
11
 
11
- var gsap2__default = /*#__PURE__*/_interopDefault(gsap2);
12
+ var gsap3__default = /*#__PURE__*/_interopDefault(gsap3);
12
13
 
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 = gsap2__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;
47
14
  var useIsDarkmode = () => {
48
- const [isDarkmode, setIsDarkmodeActive] = react.useState(false);
49
- react.useEffect(() => {
15
+ const [isDarkmode, setIsDarkmodeActive] = react$1.useState(false);
16
+ react$1.useEffect(() => {
50
17
  const matchMedia = window.matchMedia("(prefers-color-scheme: dark)");
51
18
  const handleChange = () => {
19
+ console.log("Darkmode match?", matchMedia.matches);
52
20
  setIsDarkmodeActive(matchMedia.matches);
53
21
  };
54
22
  setIsDarkmodeActive(matchMedia.matches);
55
23
  matchMedia.addEventListener("change", handleChange);
24
+ handleChange();
56
25
  return () => {
57
26
  matchMedia.removeEventListener("change", handleChange);
58
27
  };
59
28
  }, []);
60
29
  return { isDarkmode };
61
30
  };
62
- gsap2__default.default.registerPlugin(ScrollTrigger.ScrollTrigger, useGSAP);
31
+ var DustParticles = ({ config, beamColor }) => {
32
+ const {
33
+ enabled = false,
34
+ count = 30,
35
+ speed = 1,
36
+ sizeRange = [1, 3],
37
+ opacityRange = [0.2, 0.6],
38
+ color
39
+ } = config;
40
+ const particles = react$1.useMemo(() => {
41
+ if (!enabled) return [];
42
+ return Array.from({ length: count }, (_, i) => {
43
+ const x = Math.random() * 100;
44
+ const y = Math.random() * 100;
45
+ const size = sizeRange[0] + Math.random() * (sizeRange[1] - sizeRange[0]);
46
+ const opacity = opacityRange[0] + Math.random() * (opacityRange[1] - opacityRange[0]);
47
+ const duration = (3 + Math.random() * 4) / speed;
48
+ const delay = Math.random() * duration;
49
+ return {
50
+ id: `dust-${i}`,
51
+ x,
52
+ y,
53
+ size,
54
+ opacity,
55
+ duration,
56
+ delay
57
+ };
58
+ });
59
+ }, [enabled, count, sizeRange, opacityRange, speed]);
60
+ react.useGSAP(
61
+ () => {
62
+ if (!enabled || particles.length === 0) return;
63
+ const timelines = [];
64
+ particles.forEach((particle) => {
65
+ const element = document.getElementById(particle.id);
66
+ if (!element) return;
67
+ const tl = gsap3__default.default.timeline({
68
+ repeat: -1,
69
+ yoyo: true,
70
+ delay: particle.delay
71
+ });
72
+ tl.to(element, {
73
+ y: `-=${20 + Math.random() * 30}`,
74
+ // Float upward 20-50px
75
+ x: `+=${Math.random() * 20 - 10}`,
76
+ // Slight horizontal drift ±10px
77
+ opacity: particle.opacity * 0.5,
78
+ // Fade slightly
79
+ duration: particle.duration,
80
+ ease: "sine.inOut"
81
+ });
82
+ timelines.push(tl);
83
+ });
84
+ return () => {
85
+ timelines.forEach((tl) => tl.kill());
86
+ };
87
+ },
88
+ {
89
+ dependencies: [particles, enabled]
90
+ }
91
+ );
92
+ if (!enabled) return null;
93
+ const particleColor = color || beamColor;
94
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: particles.map((particle) => /* @__PURE__ */ jsxRuntime.jsx(
95
+ "div",
96
+ {
97
+ id: particle.id,
98
+ style: {
99
+ position: "absolute",
100
+ left: `${particle.x}%`,
101
+ top: `${particle.y}%`,
102
+ width: `${particle.size}px`,
103
+ height: `${particle.size}px`,
104
+ borderRadius: "50%",
105
+ backgroundColor: particleColor,
106
+ opacity: particle.opacity,
107
+ pointerEvents: "none",
108
+ willChange: "transform, opacity"
109
+ }
110
+ },
111
+ particle.id
112
+ )) });
113
+ };
114
+ var MistEffect = ({ config, beamColor }) => {
115
+ const {
116
+ enabled = false,
117
+ intensity = 0.3,
118
+ speed = 1,
119
+ layers = 2
120
+ } = config;
121
+ const mistLayers = react$1.useMemo(() => {
122
+ if (!enabled) return [];
123
+ return Array.from({ length: layers }, (_, i) => {
124
+ const layerOpacity = intensity * 0.6 / (i + 1);
125
+ const duration = (8 + i * 3) / speed;
126
+ const delay = i * 1.5 / speed;
127
+ const scale = 1 + i * 0.2;
128
+ return {
129
+ id: `mist-layer-${i}`,
130
+ opacity: layerOpacity,
131
+ duration,
132
+ delay,
133
+ scale
134
+ };
135
+ });
136
+ }, [enabled, intensity, speed, layers]);
137
+ react.useGSAP(
138
+ () => {
139
+ if (!enabled || mistLayers.length === 0) return;
140
+ const timelines = [];
141
+ mistLayers.forEach((layer) => {
142
+ const element = document.getElementById(layer.id);
143
+ if (!element) return;
144
+ const tl = gsap3__default.default.timeline({
145
+ repeat: -1,
146
+ yoyo: false
147
+ });
148
+ tl.fromTo(
149
+ element,
150
+ {
151
+ x: "-100%",
152
+ opacity: 0
153
+ },
154
+ {
155
+ x: "100%",
156
+ opacity: layer.opacity,
157
+ duration: layer.duration,
158
+ ease: "none",
159
+ delay: layer.delay
160
+ }
161
+ ).to(element, {
162
+ opacity: 0,
163
+ duration: layer.duration * 0.2,
164
+ ease: "power1.in"
165
+ });
166
+ timelines.push(tl);
167
+ });
168
+ return () => {
169
+ timelines.forEach((tl) => tl.kill());
170
+ };
171
+ },
172
+ {
173
+ dependencies: [mistLayers, enabled]
174
+ }
175
+ );
176
+ if (!enabled) return null;
177
+ const mistColor = beamColor.replace(/[\d.]+\)$/g, `${intensity})`);
178
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: mistLayers.map((layer) => /* @__PURE__ */ jsxRuntime.jsx(
179
+ "div",
180
+ {
181
+ id: layer.id,
182
+ style: {
183
+ position: "absolute",
184
+ top: 0,
185
+ left: 0,
186
+ width: "100%",
187
+ height: "100%",
188
+ background: `radial-gradient(ellipse 120% 80% at 50% 20%, ${mistColor}, transparent 70%)`,
189
+ opacity: 0,
190
+ pointerEvents: "none",
191
+ willChange: "transform, opacity",
192
+ transform: `scale(${layer.scale})`,
193
+ filter: "blur(40px)"
194
+ }
195
+ },
196
+ layer.id
197
+ )) });
198
+ };
199
+ var PulseEffect = ({ config, containerRef }) => {
200
+ const {
201
+ enabled = false,
202
+ duration = 2,
203
+ intensity = 0.2,
204
+ easing = "sine.inOut"
205
+ } = config;
206
+ react.useGSAP(
207
+ () => {
208
+ if (!enabled || !containerRef.current) return;
209
+ const element = containerRef.current;
210
+ const timeline = gsap3__default.default.timeline({
211
+ repeat: -1,
212
+ // Infinite loop
213
+ yoyo: true
214
+ // Reverse on each iteration
215
+ });
216
+ const maxMultiplier = Math.min(2, 1 + intensity);
217
+ timeline.fromTo(
218
+ element,
219
+ {
220
+ "--pulse-multiplier": 1
221
+ },
222
+ {
223
+ "--pulse-multiplier": maxMultiplier,
224
+ duration,
225
+ ease: easing
226
+ }
227
+ );
228
+ const updateOpacity = () => {
229
+ const baseOpacity = getComputedStyle(element).getPropertyValue("--base-opacity") || "1";
230
+ const pulseMultiplier = getComputedStyle(element).getPropertyValue("--pulse-multiplier") || "1";
231
+ element.style.opacity = `calc(${baseOpacity} * ${pulseMultiplier})`;
232
+ };
233
+ const ticker = gsap3__default.default.ticker.add(updateOpacity);
234
+ return () => {
235
+ timeline.kill();
236
+ gsap3__default.default.ticker.remove(ticker);
237
+ };
238
+ },
239
+ {
240
+ dependencies: [enabled, duration, intensity, easing],
241
+ scope: containerRef
242
+ }
243
+ );
244
+ return null;
245
+ };
246
+ gsap3__default.default.registerPlugin(ScrollTrigger.ScrollTrigger, react.useGSAP);
63
247
  var defaultStyles = {
64
248
  height: "var(--react-light-beam-height, 500px)",
65
249
  width: "var(--react-light-beam-width, 100vw)",
@@ -84,92 +268,135 @@ var LightBeam = ({
84
268
  colorDarkmode = "rgba(255, 255, 255, 0.5)",
85
269
  maskLightByProgress = false,
86
270
  fullWidth = 1,
87
- // Default to full width
271
+ // Default to full width range
88
272
  invert = false,
89
273
  id = void 0,
90
274
  onLoaded = void 0,
91
275
  scrollElement,
92
- disableDefaultStyles = false
276
+ disableDefaultStyles = false,
277
+ scrollStart = "top bottom",
278
+ scrollEnd = "top top",
279
+ dustParticles = { enabled: false },
280
+ mist = { enabled: false },
281
+ pulse = { enabled: false }
93
282
  }) => {
94
- const elementRef = react.useRef(null);
283
+ const elementRef = react$1.useRef(null);
95
284
  const { isDarkmode } = useIsDarkmode();
96
285
  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(() => {
286
+ const colorRef = react$1.useRef(chosenColor);
287
+ const invertRef = react$1.useRef(invert);
288
+ const maskByProgressRef = react$1.useRef(maskLightByProgress);
289
+ const scrollTriggerRef = react$1.useRef(null);
290
+ react$1.useEffect(() => {
101
291
  colorRef.current = chosenColor;
292
+ if (elementRef.current) {
293
+ elementRef.current.style.setProperty("--beam-color", chosenColor);
294
+ }
295
+ }, [chosenColor, colorLightmode, colorDarkmode]);
296
+ react$1.useEffect(() => {
297
+ const prevInvert = invertRef.current;
102
298
  invertRef.current = invert;
299
+ if (prevInvert !== invert && scrollTriggerRef.current && elementRef.current) {
300
+ const st = scrollTriggerRef.current;
301
+ elementRef.current;
302
+ st.refresh();
303
+ }
304
+ }, [invert]);
305
+ react$1.useEffect(() => {
306
+ const prevMaskByProgress = maskByProgressRef.current;
103
307
  maskByProgressRef.current = maskLightByProgress;
104
- }, [chosenColor, colorLightmode, colorDarkmode, invert, maskLightByProgress]);
105
- react.useEffect(() => {
308
+ if (prevMaskByProgress !== maskLightByProgress && elementRef.current) {
309
+ const element = elementRef.current;
310
+ if (maskLightByProgress) {
311
+ element.style.setProperty("--beam-mask-stop", "50%");
312
+ element.style.maskImage = `linear-gradient(to bottom, var(--beam-color) 0%, transparent var(--beam-mask-stop))`;
313
+ element.style.webkitMaskImage = `linear-gradient(to bottom, var(--beam-color) 0%, transparent var(--beam-mask-stop))`;
314
+ } else {
315
+ element.style.maskImage = `linear-gradient(to bottom, var(--beam-color) 25%, transparent 95%)`;
316
+ element.style.webkitMaskImage = `linear-gradient(to bottom, var(--beam-color) 25%, transparent 95%)`;
317
+ }
318
+ if (scrollTriggerRef.current) {
319
+ scrollTriggerRef.current.refresh();
320
+ }
321
+ }
322
+ }, [maskLightByProgress]);
323
+ react$1.useEffect(() => {
106
324
  onLoaded && onLoaded();
107
325
  }, []);
108
- useGSAP(
326
+ react.useGSAP(
109
327
  () => {
110
328
  const element = elementRef.current;
111
329
  if (!element || typeof window === "undefined") return;
112
330
  const opacityMin = 0.839322;
113
331
  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`;
332
+ const updateColorVar = (color) => {
333
+ element.style.setProperty("--beam-color", color);
119
334
  };
120
- const interpolateMask = (progress, color) => {
121
- if (!maskByProgressRef.current) {
122
- return `linear-gradient(to bottom, ${color} 25%, transparent 95%)`;
335
+ const initGradientStructure = (color) => {
336
+ updateColorVar(color);
337
+ const baseGradient = `conic-gradient(from 90deg at var(--beam-left-pos) 0%, var(--beam-color), transparent 180deg) 0% 0% / 50% var(--beam-left-size) no-repeat, conic-gradient(from 270deg at var(--beam-right-pos) 0%, transparent 180deg, var(--beam-color)) 100% 0% / 50% 100% no-repeat`;
338
+ element.style.background = baseGradient;
339
+ if (maskByProgressRef.current) {
340
+ element.style.maskImage = `linear-gradient(to bottom, var(--beam-color) 0%, transparent var(--beam-mask-stop))`;
341
+ element.style.webkitMaskImage = `linear-gradient(to bottom, var(--beam-color) 0%, transparent var(--beam-mask-stop))`;
342
+ } else {
343
+ element.style.maskImage = `linear-gradient(to bottom, var(--beam-color) 25%, transparent 95%)`;
344
+ element.style.webkitMaskImage = `linear-gradient(to bottom, var(--beam-color) 25%, transparent 95%)`;
123
345
  }
124
- const stopPoint = 50 + progress * 45;
125
- return `linear-gradient(to bottom, ${color} 0%, transparent ${stopPoint}%)`;
126
346
  };
127
347
  const adjustedFullWidth = 1 - fullWidth;
128
348
  const calculateProgress = (rawProgress) => {
129
349
  const normalizedPosition = Math.max(
130
350
  adjustedFullWidth,
131
- // Minimum (floor)
351
+ // Floor value (1 - fullWidth)
132
352
  Math.min(1, 1 - rawProgress)
133
- // Convert GSAP progress to Framer's normalized position
353
+ // Inverted GSAP progress
134
354
  );
135
355
  return invertRef.current ? normalizedPosition : 1 - normalizedPosition;
136
356
  };
137
357
  const scroller = scrollElement ? scrollElement : void 0;
358
+ const applyProgressState = (progress) => {
359
+ const leftPos = 90 - progress * 90;
360
+ const rightPos = 10 + progress * 90;
361
+ const leftSize = 150 - progress * 50;
362
+ const baseOpacity = opacityMin + opacityRange * progress;
363
+ const maskStop = maskByProgressRef.current ? 50 + progress * 45 : void 0;
364
+ const cssProps = {
365
+ "--beam-left-pos": `${leftPos}%`,
366
+ "--beam-right-pos": `${rightPos}%`,
367
+ "--beam-left-size": `${leftSize}%`,
368
+ "--base-opacity": baseOpacity
369
+ };
370
+ if (maskStop !== void 0) {
371
+ cssProps["--beam-mask-stop"] = `${maskStop}%`;
372
+ }
373
+ if (!pulse.enabled) {
374
+ cssProps.opacity = baseOpacity;
375
+ }
376
+ gsap3__default.default.set(element, cssProps);
377
+ };
378
+ initGradientStructure(colorRef.current);
138
379
  const st = ScrollTrigger.ScrollTrigger.create({
139
380
  trigger: element,
140
- start: "top bottom",
141
- // Element top hits viewport bottom
142
- end: "top top",
143
- // Element top hits viewport top
381
+ start: scrollStart,
382
+ // When to start the animation
383
+ end: scrollEnd,
384
+ // When to end the animation
144
385
  scroller,
145
- scrub: true,
146
- // Instant scrubbing
386
+ scrub: 0.15,
387
+ // Fast catch-up (300ms) for responsive scroll without jitter
147
388
  onUpdate: (self) => {
148
389
  const progress = calculateProgress(self.progress);
149
- gsap2__default.default.set(element, {
150
- background: interpolateBackground(progress, colorRef.current),
151
- opacity: opacityMin + opacityRange * progress,
152
- maskImage: interpolateMask(progress, colorRef.current),
153
- webkitMaskImage: interpolateMask(progress, colorRef.current)
154
- });
390
+ applyProgressState(progress);
155
391
  },
156
392
  onRefresh: (self) => {
157
393
  const progress = calculateProgress(self.progress);
158
- gsap2__default.default.set(element, {
159
- background: interpolateBackground(progress, colorRef.current),
160
- opacity: opacityMin + opacityRange * progress,
161
- maskImage: interpolateMask(progress, colorRef.current),
162
- webkitMaskImage: interpolateMask(progress, colorRef.current)
163
- });
394
+ applyProgressState(progress);
164
395
  }
165
396
  });
397
+ scrollTriggerRef.current = st;
166
398
  const initialProgress = calculateProgress(st.progress);
167
- gsap2__default.default.set(element, {
168
- background: interpolateBackground(initialProgress, colorRef.current),
169
- opacity: opacityMin + opacityRange * initialProgress,
170
- maskImage: interpolateMask(initialProgress, colorRef.current),
171
- webkitMaskImage: interpolateMask(initialProgress, colorRef.current)
172
- });
399
+ applyProgressState(initialProgress);
173
400
  const refreshTimeout = setTimeout(() => {
174
401
  ScrollTrigger.ScrollTrigger.refresh();
175
402
  }, 100);
@@ -185,9 +412,13 @@ var LightBeam = ({
185
412
  // Only include values that affect ScrollTrigger's position/range calculations
186
413
  dependencies: [
187
414
  fullWidth,
188
- // Affects trigger range
189
- scrollElement
415
+ // Affects progress range calculation
416
+ scrollElement,
190
417
  // Affects which element to watch
418
+ scrollStart,
419
+ // Affects when animation starts
420
+ scrollEnd
421
+ // Affects when animation ends
191
422
  ],
192
423
  scope: elementRef
193
424
  }
@@ -205,29 +436,21 @@ var LightBeam = ({
205
436
  ...style
206
437
  // User styles override everything
207
438
  };
208
- return /* @__PURE__ */ jsxRuntime.jsx(
439
+ return /* @__PURE__ */ jsxRuntime.jsxs(
209
440
  "div",
210
441
  {
211
442
  ref: elementRef,
212
443
  className: combinedClassName,
213
444
  style: finalStyles,
214
- ...id ? { id } : {}
445
+ ...id ? { id } : {},
446
+ children: [
447
+ dustParticles.enabled && /* @__PURE__ */ jsxRuntime.jsx(DustParticles, { config: dustParticles, beamColor: chosenColor }),
448
+ mist.enabled && /* @__PURE__ */ jsxRuntime.jsx(MistEffect, { config: mist, beamColor: chosenColor }),
449
+ pulse.enabled && /* @__PURE__ */ jsxRuntime.jsx(PulseEffect, { config: pulse, containerRef: elementRef })
450
+ ]
215
451
  }
216
452
  );
217
453
  };
218
- /*! Bundled license information:
219
-
220
- @gsap/react/src/index.js:
221
- (*!
222
- * @gsap/react 2.1.2
223
- * https://gsap.com
224
- *
225
- * Copyright 2008-2025, GreenSock. All rights reserved.
226
- * Subject to the terms at https://gsap.com/standard-license or for
227
- * Club GSAP members, the agreement issued with that membership.
228
- * @author: Jack Doyle, jack@greensock.com
229
- *)
230
- */
231
454
 
232
455
  exports.LightBeam = LightBeam;
233
456
  //# sourceMappingURL=index.js.map