@thinkpixellab-public/px-vue 4.1.30 → 4.1.32
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/components/PxQrCode.vue +84 -64
- package/composables/loadGsap.js +6 -2
- package/composables/loadLenis.js +12 -12
- package/composables/useGsap.js +12 -25
- package/composables/useLenis.js +21 -21
- package/composables/useScrollAnimations.js +106 -101
- package/package.json +1 -1
- package/utils/noiseGenerator.js +98 -0
package/components/PxQrCode.vue
CHANGED
|
@@ -45,14 +45,14 @@ export default {
|
|
|
45
45
|
const rectEls = svg?.querySelectorAll(':scope > rect');
|
|
46
46
|
const imageEls = svg?.querySelectorAll(':scope > image');
|
|
47
47
|
|
|
48
|
-
const marginPx = (this.percentageMargin / 100) * svg
|
|
49
|
-
const scale = (svg
|
|
48
|
+
const marginPx = (this.percentageMargin / 100) * svg?.clientWidth;
|
|
49
|
+
const scale = (svg?.clientWidth - marginPx * 2) / svg?.clientWidth;
|
|
50
50
|
|
|
51
51
|
// the second rect is the qrcode
|
|
52
52
|
const qrRect = rectEls?.[1];
|
|
53
53
|
const qrImage = imageEls?.[0];
|
|
54
54
|
|
|
55
|
-
if (!qrRect || !qrImage) {
|
|
55
|
+
if (!qrRect || !qrImage || !svg) {
|
|
56
56
|
if (retryCount < MAX_RETRIES) {
|
|
57
57
|
setTimeout(() => {
|
|
58
58
|
this.setScale(retryCount + 1);
|
|
@@ -69,86 +69,106 @@ export default {
|
|
|
69
69
|
qrRect.style.transform = `scale(${scale})`;
|
|
70
70
|
qrImage.style.transform = `scale(${scale})`;
|
|
71
71
|
},
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
imageOptions: {
|
|
86
|
-
hideBackgroundDots: false,
|
|
87
|
-
imageSize: this.imageSettings?.size || 0.1,
|
|
88
|
-
margin: 0,
|
|
89
|
-
crossOrigin: 'anonymous',
|
|
90
|
-
},
|
|
91
|
-
data: this.content,
|
|
92
|
-
...rest,
|
|
93
|
-
type: 'svg',
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
if (!this.percentageMargin) {
|
|
97
|
-
this.isRendered = true;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
this.qrCode = new QRCodeStyling(options);
|
|
72
|
+
setupQRCode() {
|
|
73
|
+
this.size = Math.min(this.$el.clientWidth, this.$el.clientHeight);
|
|
74
|
+
|
|
75
|
+
if (this.size <= 0) {
|
|
76
|
+
// setup a size observer to wait for a size, if size is 0 the qr code throws an exception
|
|
77
|
+
this.setupSizeObserver = new ResizeObserver(entries => {
|
|
78
|
+
const size = Math.min(this.$el.clientWidth, this.$el.clientHeight);
|
|
79
|
+
if (size > 0) {
|
|
80
|
+
this.setupSizeObserver.disconnect();
|
|
81
|
+
this.setupSizeObserver = null;
|
|
82
|
+
this.setupQRCode();
|
|
83
|
+
}
|
|
84
|
+
});
|
|
101
85
|
|
|
102
|
-
|
|
103
|
-
const containerSize = Math.min(this.$el.clientWidth, this.$el.clientHeight);
|
|
104
|
-
this.size = containerSize;
|
|
86
|
+
this.setupSizeObserver.observe(this.$el);
|
|
105
87
|
|
|
106
|
-
if (!this.qrCode._qr) {
|
|
107
88
|
return;
|
|
108
89
|
}
|
|
109
90
|
|
|
110
|
-
|
|
111
|
-
|
|
91
|
+
const { margin, ...rest } = this.qrCodeConfig || {};
|
|
92
|
+
|
|
93
|
+
// margin is a percentage of the svg width
|
|
94
|
+
this.percentageMargin = margin ? Math.min(Math.max(0, margin), 100) : 0;
|
|
95
|
+
|
|
96
|
+
const options = {
|
|
97
|
+
width: this.size,
|
|
98
|
+
height: this.size,
|
|
99
|
+
image: this.imageSettings?.src || null,
|
|
100
|
+
imageOptions: {
|
|
101
|
+
hideBackgroundDots: false,
|
|
102
|
+
imageSize: this.imageSettings?.size || 0.1,
|
|
103
|
+
margin: 0,
|
|
104
|
+
crossOrigin: 'anonymous',
|
|
105
|
+
},
|
|
106
|
+
data: this.content,
|
|
107
|
+
...rest,
|
|
108
|
+
type: 'svg',
|
|
109
|
+
};
|
|
112
110
|
|
|
113
|
-
|
|
111
|
+
if (!this.percentageMargin) {
|
|
112
|
+
this.isRendered = true;
|
|
113
|
+
}
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
const moduleSize = Math.floor(naturalModSize);
|
|
115
|
+
this.qrCode = new QRCodeStyling(options);
|
|
117
116
|
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
this.onSizeChange = () => {
|
|
118
|
+
const containerSize = Math.min(this.$el.clientWidth, this.$el.clientHeight);
|
|
119
|
+
this.size = containerSize;
|
|
120
120
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
if (!this.qrCode._qr) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
// 1. Get module count
|
|
126
|
+
const moduleCount = this.qrCode._qr.getModuleCount();
|
|
126
127
|
|
|
127
|
-
|
|
128
|
-
this.setScale();
|
|
129
|
-
});
|
|
130
|
-
};
|
|
128
|
+
const naturalModSize = containerSize / moduleCount;
|
|
131
129
|
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
// 2. the qr code lib uses integer pixel sizes, so we need to round down
|
|
131
|
+
const moduleSize = Math.floor(naturalModSize);
|
|
134
132
|
|
|
135
|
-
|
|
136
|
-
|
|
133
|
+
// 3. recalculate actual size to position the qr code in the top left corner
|
|
134
|
+
const qrSize = moduleCount * moduleSize;
|
|
137
135
|
|
|
138
|
-
|
|
136
|
+
// 4. Update QR options
|
|
137
|
+
options.width = qrSize;
|
|
138
|
+
options.height = qrSize;
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
this
|
|
144
|
-
this.setScale();
|
|
145
|
-
});
|
|
140
|
+
this.qrCode.update(options);
|
|
141
|
+
|
|
142
|
+
this.$nextTick(() => {
|
|
143
|
+
this.setScale();
|
|
146
144
|
});
|
|
147
|
-
}
|
|
148
|
-
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
this.resizeObserver = new ResizeObserver(this.onSizeChange);
|
|
148
|
+
this.resizeObserver.observe(this.$el);
|
|
149
|
+
|
|
150
|
+
this.$nextTick(() => {
|
|
151
|
+
this.qrCode.append(this.$refs.qrCode);
|
|
152
|
+
|
|
153
|
+
this.onSizeChange();
|
|
154
|
+
|
|
155
|
+
if (this.percentageMargin) {
|
|
156
|
+
this.qrCode.getRawData('svg').then(() => {
|
|
157
|
+
// QR code has been rendered
|
|
158
|
+
this.$nextTick(() => {
|
|
159
|
+
this.setScale();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
mounted() {
|
|
167
|
+
this.setupQRCode();
|
|
149
168
|
},
|
|
150
169
|
beforeUnmount() {
|
|
151
170
|
this.resizeObserver?.disconnect();
|
|
171
|
+
this.setupSizeObserver?.disconnect();
|
|
152
172
|
},
|
|
153
173
|
};
|
|
154
174
|
</script>
|
package/composables/loadGsap.js
CHANGED
|
@@ -51,8 +51,12 @@ export const loadGsap = async () => {
|
|
|
51
51
|
export const loadScrollTrigger = async () => {
|
|
52
52
|
if (!scrollTriggerPromise) {
|
|
53
53
|
scrollTriggerPromise = (async () => {
|
|
54
|
-
const [gsap, { ScrollTrigger }] = await Promise.all([
|
|
55
|
-
|
|
54
|
+
const [gsap, { ScrollTrigger }, { CSSPlugin }] = await Promise.all([
|
|
55
|
+
loadGsap(),
|
|
56
|
+
import('gsap/ScrollTrigger'),
|
|
57
|
+
import('gsap/CSSPlugin')
|
|
58
|
+
]);
|
|
59
|
+
gsap.registerPlugin(ScrollTrigger, CSSPlugin);
|
|
56
60
|
return { gsap, ScrollTrigger };
|
|
57
61
|
})();
|
|
58
62
|
}
|
package/composables/loadLenis.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Lazy loading utilities for Lenis smooth scrolling
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
6
|
* These functions provide race-condition-safe loading and initialization of Lenis.
|
|
7
7
|
* The instance is shared across all components for consistent scroll behavior.
|
|
8
8
|
*/
|
|
@@ -12,13 +12,13 @@ let lenisInstancePromise = null;
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Loads the Lenis class lazily
|
|
15
|
-
*
|
|
15
|
+
*
|
|
16
16
|
* @returns {Promise} Promise that resolves to the Lenis constructor
|
|
17
|
-
*
|
|
17
|
+
*
|
|
18
18
|
* @example
|
|
19
19
|
* // Direct usage (not recommended - use useLenis composable instead)
|
|
20
20
|
* import { loadLenis } from '@thinkpixellab-public/px-vue/composables/loadLenis';
|
|
21
|
-
*
|
|
21
|
+
*
|
|
22
22
|
* const Lenis = await loadLenis();
|
|
23
23
|
* const customLenis = new Lenis({
|
|
24
24
|
* duration: 2,
|
|
@@ -34,32 +34,32 @@ export const loadLenis = async () => {
|
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* Loads a shared Lenis instance with default configuration
|
|
37
|
-
*
|
|
37
|
+
*
|
|
38
38
|
* Returns the same instance across all calls for consistent scroll behavior.
|
|
39
39
|
* Starts with autoRaf enabled - useScrollAnimation will take over RAF control when needed.
|
|
40
|
-
*
|
|
40
|
+
*
|
|
41
41
|
* @returns {Promise} Promise that resolves to the shared Lenis instance
|
|
42
|
-
*
|
|
42
|
+
*
|
|
43
43
|
* @example
|
|
44
44
|
* // Direct usage (not recommended - use useLenis composable instead)
|
|
45
45
|
* import { loadLenisInstance } from '@thinkpixellab-public/px-vue/composables/loadLenis';
|
|
46
|
-
*
|
|
46
|
+
*
|
|
47
47
|
* const lenis = await loadLenisInstance();
|
|
48
|
-
*
|
|
48
|
+
*
|
|
49
49
|
* // smooth scroll to element
|
|
50
50
|
* lenis.scrollTo('#section-2', { duration: 1.5 });
|
|
51
|
-
*
|
|
51
|
+
*
|
|
52
52
|
* // listen to scroll events
|
|
53
53
|
* lenis.on('scroll', (e) => {
|
|
54
54
|
* console.log('Scroll position:', e.scroll);
|
|
55
55
|
* });
|
|
56
|
-
*
|
|
56
|
+
*
|
|
57
57
|
* @example
|
|
58
58
|
* // Configuration details
|
|
59
59
|
* // The shared instance is created with:
|
|
60
60
|
* // {
|
|
61
61
|
* // smoothWheel: false, // disabled for better performance
|
|
62
|
-
* // smoothTouch: false, // disabled for mobile compatibility
|
|
62
|
+
* // smoothTouch: false, // disabled for mobile compatibility
|
|
63
63
|
* // autoRaf: true // auto RAF until ScrollTrigger takes over
|
|
64
64
|
* // }
|
|
65
65
|
*/
|
package/composables/useGsap.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import { loadGsap } from './loadGsap';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Provides access to the GSAP library with lazy loading.
|
|
6
|
+
* Components should handle their own animation cleanup.
|
|
7
7
|
*
|
|
8
|
-
* @returns {Object} Object with
|
|
8
|
+
* @returns {Object} Object with getGsap loader function
|
|
9
9
|
*
|
|
10
10
|
* @example
|
|
11
11
|
* // Basic usage in a Vue component
|
|
@@ -13,12 +13,12 @@ import { loadGsap } from './loadGsap';
|
|
|
13
13
|
*
|
|
14
14
|
* export default {
|
|
15
15
|
* setup() {
|
|
16
|
-
* const {
|
|
16
|
+
* const { getGsap } = useGsap();
|
|
17
17
|
*
|
|
18
18
|
* onMounted(async () => {
|
|
19
|
-
* await
|
|
19
|
+
* const gsap = await getGsap();
|
|
20
20
|
*
|
|
21
|
-
* //
|
|
21
|
+
* // animate an element
|
|
22
22
|
* gsap.to('.my-element', {
|
|
23
23
|
* duration: 1,
|
|
24
24
|
* x: 100,
|
|
@@ -35,12 +35,13 @@ import { loadGsap } from './loadGsap';
|
|
|
35
35
|
*
|
|
36
36
|
* export default {
|
|
37
37
|
* setup() {
|
|
38
|
-
* useGsap();
|
|
38
|
+
* const { getGsap } = useGsap();
|
|
39
39
|
* const myElement = ref(null);
|
|
40
40
|
* let animation = null;
|
|
41
41
|
*
|
|
42
|
-
* onMounted(() => {
|
|
43
|
-
*
|
|
42
|
+
* onMounted(async () => {
|
|
43
|
+
* const gsap = await getGsap();
|
|
44
|
+
*
|
|
44
45
|
* animation = gsap.to(myElement.value, {
|
|
45
46
|
* duration: 2,
|
|
46
47
|
* rotation: 360,
|
|
@@ -57,25 +58,11 @@ import { loadGsap } from './loadGsap';
|
|
|
57
58
|
* return { myElement };
|
|
58
59
|
* }
|
|
59
60
|
* }
|
|
60
|
-
*
|
|
61
|
-
* @example
|
|
62
|
-
* // Immediate usage if you know GSAP is already loaded
|
|
63
|
-
* import { useGsap } from '@thinkpixellab-public/px-vue/composables/useGsap';
|
|
64
|
-
*
|
|
65
|
-
* export default {
|
|
66
|
-
* setup() {
|
|
67
|
-
* useGsap(); // triggers load if needed
|
|
68
|
-
*
|
|
69
|
-
* // can use gsap immediately in most cases
|
|
70
|
-
* const tl = gsap.timeline();
|
|
71
|
-
* }
|
|
72
|
-
* }
|
|
73
61
|
*/
|
|
74
62
|
export const useGsap = () => {
|
|
75
63
|
if (process.server) {
|
|
76
|
-
return {
|
|
64
|
+
return { getGsap: () => null };
|
|
77
65
|
}
|
|
78
66
|
|
|
79
|
-
|
|
80
|
-
return { ready };
|
|
67
|
+
return { getGsap: loadGsap };
|
|
81
68
|
};
|
package/composables/useLenis.js
CHANGED
|
@@ -3,74 +3,74 @@ import { loadLenisInstance } from './loadLenis';
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Vue composable for using Lenis smooth scrolling
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
7
|
* Provides access to a shared Lenis instance with autoRaf enabled.
|
|
8
8
|
* Perfect for components that need smooth scrolling without ScrollTrigger integration.
|
|
9
|
-
*
|
|
9
|
+
*
|
|
10
10
|
* @returns {Object} Object with getLenis loader function
|
|
11
|
-
*
|
|
11
|
+
*
|
|
12
12
|
* @example
|
|
13
13
|
* // Basic usage for smooth scrolling
|
|
14
14
|
* import { useLenis } from '@thinkpixellab-public/px-vue/composables/useLenis';
|
|
15
|
-
*
|
|
15
|
+
*
|
|
16
16
|
* export default {
|
|
17
17
|
* setup() {
|
|
18
18
|
* const { getLenis } = useLenis();
|
|
19
|
-
*
|
|
19
|
+
*
|
|
20
20
|
* onMounted(async () => {
|
|
21
21
|
* const lenis = await getLenis();
|
|
22
|
-
*
|
|
22
|
+
*
|
|
23
23
|
* // scroll to top smoothly
|
|
24
24
|
* lenis.scrollTo(0);
|
|
25
|
-
*
|
|
25
|
+
*
|
|
26
26
|
* // scroll to element
|
|
27
27
|
* lenis.scrollTo('#my-section');
|
|
28
|
-
*
|
|
28
|
+
*
|
|
29
29
|
* // scroll to specific position
|
|
30
30
|
* lenis.scrollTo(500, { duration: 2 });
|
|
31
31
|
* });
|
|
32
32
|
* }
|
|
33
33
|
* }
|
|
34
|
-
*
|
|
34
|
+
*
|
|
35
35
|
* @example
|
|
36
36
|
* // Listen to scroll events for parallax
|
|
37
37
|
* import { ref, onMounted } from 'vue';
|
|
38
38
|
* import { useLenis } from '@thinkpixellab-public/px-vue/composables/useLenis';
|
|
39
|
-
*
|
|
39
|
+
*
|
|
40
40
|
* export default {
|
|
41
41
|
* setup() {
|
|
42
42
|
* const { getLenis } = useLenis();
|
|
43
43
|
* const parallaxOffset = ref(0);
|
|
44
|
-
*
|
|
44
|
+
*
|
|
45
45
|
* onMounted(async () => {
|
|
46
46
|
* const lenis = await getLenis();
|
|
47
|
-
*
|
|
47
|
+
*
|
|
48
48
|
* lenis.on('scroll', (e) => {
|
|
49
49
|
* parallaxOffset.value = e.scroll * 0.5;
|
|
50
50
|
* });
|
|
51
51
|
* });
|
|
52
|
-
*
|
|
52
|
+
*
|
|
53
53
|
* return { parallaxOffset };
|
|
54
54
|
* }
|
|
55
55
|
* }
|
|
56
|
-
*
|
|
56
|
+
*
|
|
57
57
|
* @example
|
|
58
58
|
* // Custom scroll behavior
|
|
59
59
|
* import { useLenis } from '@thinkpixellab-public/px-vue/composables/useLenis';
|
|
60
|
-
*
|
|
60
|
+
*
|
|
61
61
|
* export default {
|
|
62
62
|
* setup() {
|
|
63
63
|
* const { getLenis } = useLenis();
|
|
64
|
-
*
|
|
64
|
+
*
|
|
65
65
|
* const scrollToSection = async (selector) => {
|
|
66
66
|
* const lenis = await getLenis();
|
|
67
|
-
*
|
|
67
|
+
*
|
|
68
68
|
* lenis.scrollTo(selector, {
|
|
69
69
|
* duration: 1.5,
|
|
70
70
|
* easing: (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
|
|
71
71
|
* });
|
|
72
72
|
* };
|
|
73
|
-
*
|
|
73
|
+
*
|
|
74
74
|
* return { scrollToSection };
|
|
75
75
|
* }
|
|
76
76
|
* }
|
|
@@ -80,7 +80,7 @@ export const useLenis = () => {
|
|
|
80
80
|
return { getLenis: () => null };
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
return {
|
|
84
|
-
getLenis: loadLenisInstance
|
|
83
|
+
return {
|
|
84
|
+
getLenis: loadLenisInstance,
|
|
85
85
|
};
|
|
86
|
-
};
|
|
86
|
+
};
|
|
@@ -5,107 +5,104 @@ import { loadLenisInstance } from './loadLenis';
|
|
|
5
5
|
let scrollTriggerPromise = null;
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
* return { container };
|
|
102
|
-
* }
|
|
103
|
-
* }
|
|
104
|
-
*/
|
|
8
|
+
Vue composable for scroll-triggered animations with GSAP ScrollTrigger and Lenis
|
|
9
|
+
|
|
10
|
+
Provides integrated GSAP, ScrollTrigger, and Lenis for complex scroll animations.
|
|
11
|
+
Automatically manages RAF control transfer from Lenis to GSAP for optimal performance.
|
|
12
|
+
|
|
13
|
+
@returns {Object} Object with getGsap, getScrollTrigger, and getLenis loader functions
|
|
14
|
+
|
|
15
|
+
@example
|
|
16
|
+
// Basic scroll-triggered animation
|
|
17
|
+
import { useScrollAnimation } from '@thinkpixellab-public/px-vue/composables/useScrollAnimations';
|
|
18
|
+
|
|
19
|
+
export default {
|
|
20
|
+
setup() {
|
|
21
|
+
const { getGsap, getScrollTrigger } = useScrollAnimation();
|
|
22
|
+
|
|
23
|
+
onMounted(async () => {
|
|
24
|
+
const [gsap, ScrollTrigger] = await Promise.all([getGsap(), getScrollTrigger()]);
|
|
25
|
+
|
|
26
|
+
gsap.to('.fade-in', {
|
|
27
|
+
opacity: 1,
|
|
28
|
+
y: 0,
|
|
29
|
+
scrollTrigger: {
|
|
30
|
+
trigger: '.fade-in',
|
|
31
|
+
start: 'top 80%',
|
|
32
|
+
end: 'bottom 20%',
|
|
33
|
+
scrub: true
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@example
|
|
41
|
+
// Parallax scrolling with Lenis integration
|
|
42
|
+
import { useScrollAnimation } from '@thinkpixellab-public/px-vue/composables/useScrollAnimations';
|
|
43
|
+
|
|
44
|
+
export default {
|
|
45
|
+
setup() {
|
|
46
|
+
const { getGsap, getLenis } = useScrollAnimation();
|
|
47
|
+
|
|
48
|
+
onMounted(async () => {
|
|
49
|
+
const [gsap, lenis] = await Promise.all([getGsap(), getLenis()]);
|
|
50
|
+
|
|
51
|
+
// smooth scroll to section
|
|
52
|
+
lenis.scrollTo('#section-2', { duration: 2 });
|
|
53
|
+
|
|
54
|
+
// parallax background
|
|
55
|
+
gsap.to('.bg-parallax', {
|
|
56
|
+
yPercent: -50,
|
|
57
|
+
ease: 'none',
|
|
58
|
+
scrollTrigger: {
|
|
59
|
+
trigger: '.parallax-section',
|
|
60
|
+
start: 'top bottom',
|
|
61
|
+
end: 'bottom top',
|
|
62
|
+
scrub: true
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@example
|
|
70
|
+
// Complex timeline with ScrollTrigger
|
|
71
|
+
import { ref, onMounted } from 'vue';
|
|
72
|
+
import { useScrollAnimation } from '@thinkpixellab-public/px-vue/composables/useScrollAnimations';
|
|
73
|
+
|
|
74
|
+
export default {
|
|
75
|
+
setup() {
|
|
76
|
+
const { getGsap } = useScrollAnimation();
|
|
77
|
+
const container = ref(null);
|
|
78
|
+
|
|
79
|
+
onMounted(async () => {
|
|
80
|
+
const gsap = await getGsap();
|
|
81
|
+
|
|
82
|
+
const tl = gsap.timeline({
|
|
83
|
+
scrollTrigger: {
|
|
84
|
+
trigger: container.value,
|
|
85
|
+
start: 'top center',
|
|
86
|
+
end: 'bottom center',
|
|
87
|
+
scrub: 1,
|
|
88
|
+
pin: true
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
tl.from('.item-1', { x: -100, opacity: 0 })
|
|
93
|
+
.from('.item-2', { x: 100, opacity: 0 }, '-=0.5')
|
|
94
|
+
.from('.item-3', { y: 100, opacity: 0 }, '-=0.3');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return { container };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
**/
|
|
105
101
|
export const useScrollAnimation = () => {
|
|
106
102
|
if (process.server) {
|
|
107
103
|
return {
|
|
108
|
-
|
|
104
|
+
getGsap: () => null,
|
|
105
|
+
getScrollTrigger: () => null,
|
|
109
106
|
getLenis: () => null,
|
|
110
107
|
};
|
|
111
108
|
}
|
|
@@ -121,14 +118,21 @@ export const useScrollAnimation = () => {
|
|
|
121
118
|
// Take over RAF control from Lenis
|
|
122
119
|
lenis.stop();
|
|
123
120
|
|
|
121
|
+
// Disable autoRaf permanently - we'll drive it with GSAP
|
|
122
|
+
lenis.options.autoRaf = false;
|
|
123
|
+
|
|
124
124
|
lenis.on('scroll', ScrollTrigger.update);
|
|
125
|
+
|
|
125
126
|
gsap.ticker.add(time => {
|
|
126
127
|
lenis.raf(time * 1000);
|
|
127
128
|
});
|
|
129
|
+
|
|
128
130
|
gsap.ticker.lagSmoothing(0);
|
|
129
131
|
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
+
// Re-enable scroll processing (but without autoRaf)
|
|
133
|
+
lenis.start();
|
|
134
|
+
|
|
135
|
+
return { gsap, ScrollTrigger, lenis };
|
|
132
136
|
})();
|
|
133
137
|
}
|
|
134
138
|
|
|
@@ -136,7 +140,8 @@ export const useScrollAnimation = () => {
|
|
|
136
140
|
};
|
|
137
141
|
|
|
138
142
|
return {
|
|
139
|
-
|
|
143
|
+
getGsap: async () => (await init()).gsap,
|
|
144
|
+
getScrollTrigger: async () => (await init()).ScrollTrigger,
|
|
140
145
|
getLenis: async () => (await init()).lenis,
|
|
141
146
|
};
|
|
142
147
|
};
|
package/package.json
CHANGED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a Base64 PNG noise texture (transparent background).
|
|
3
|
+
*
|
|
4
|
+
* @param {number} width
|
|
5
|
+
* @param {number} height
|
|
6
|
+
* @param {Object} [opts]
|
|
7
|
+
* @param {number} [opts.seed=Date.now()] - RNG seed
|
|
8
|
+
* @param {number} [opts.coverage=0.35] - 0..1 overall density of speckles
|
|
9
|
+
* @param {number} [opts.darkAmount=0.5] - relative weight of dark specks
|
|
10
|
+
* @param {number} [opts.lightAmount=0.5] - relative weight of light specks
|
|
11
|
+
* @param {Array} [opts.darkColor=[0,0,0]] - [r,g,b] for dark specks (0–255)
|
|
12
|
+
* @param {Array} [opts.lightColor=[255,255,255]] - [r,g,b] for light specks
|
|
13
|
+
* @param {number} [opts.darkAlpha=0.22] - 0..1 alpha for dark specks
|
|
14
|
+
* @param {number} [opts.lightAlpha=0.18] - 0..1 alpha for light specks
|
|
15
|
+
* @param {number} [opts.jitter=0.08] - alpha variation per pixel
|
|
16
|
+
* @param {function} [opts.weightFn] - (x, y, r) => 0..1 density modulator
|
|
17
|
+
* @returns {string} data URL: "data:image/png;base64,..."
|
|
18
|
+
*/
|
|
19
|
+
function createNoiseBase64(width, height, opts = {}) {
|
|
20
|
+
// Seeded RNG
|
|
21
|
+
function mulberry32(a) {
|
|
22
|
+
return function () {
|
|
23
|
+
let t = (a += 0x6d2b79f5);
|
|
24
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
25
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
26
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const clamp255 = v => Math.max(0, Math.min(255, v | 0));
|
|
31
|
+
|
|
32
|
+
const {
|
|
33
|
+
seed = Date.now(),
|
|
34
|
+
coverage = 0.35,
|
|
35
|
+
darkAmount = 0.5,
|
|
36
|
+
lightAmount = 0.5,
|
|
37
|
+
darkColor = [0, 0, 0],
|
|
38
|
+
lightColor = [255, 255, 255],
|
|
39
|
+
darkAlpha = 0.22,
|
|
40
|
+
lightAlpha = 0.18,
|
|
41
|
+
jitter = 0.08,
|
|
42
|
+
weightFn,
|
|
43
|
+
} = opts;
|
|
44
|
+
|
|
45
|
+
const rng = mulberry32(seed >>> 0 || 1);
|
|
46
|
+
|
|
47
|
+
const canvas = document.createElement('canvas');
|
|
48
|
+
canvas.width = width;
|
|
49
|
+
canvas.height = height;
|
|
50
|
+
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
51
|
+
|
|
52
|
+
const img = ctx.createImageData(width, height);
|
|
53
|
+
const data = img.data;
|
|
54
|
+
|
|
55
|
+
// Transparent background (already zeroed out by default, but explicit for clarity)
|
|
56
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
57
|
+
data[i + 0] = 0;
|
|
58
|
+
data[i + 1] = 0;
|
|
59
|
+
data[i + 2] = 0;
|
|
60
|
+
data[i + 3] = 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Draw speckles
|
|
64
|
+
for (let y = 0; y < height; y++) {
|
|
65
|
+
for (let x = 0; x < width; x++) {
|
|
66
|
+
const idx = (y * width + x) * 4;
|
|
67
|
+
const r = rng();
|
|
68
|
+
const weight =
|
|
69
|
+
typeof weightFn === 'function' ? Math.max(0, Math.min(1, weightFn(x, y, r))) : 1;
|
|
70
|
+
|
|
71
|
+
// Should this pixel be noise at all?
|
|
72
|
+
if (r >= coverage * weight) continue;
|
|
73
|
+
|
|
74
|
+
// Decide dark vs light by relative weights
|
|
75
|
+
const total = Math.max(1e-6, darkAmount + lightAmount);
|
|
76
|
+
const darkShare = darkAmount / total;
|
|
77
|
+
const draw = rng() < darkShare ? 1 : 2;
|
|
78
|
+
|
|
79
|
+
const jitterSign = rng() < 0.5 ? -1 : 1;
|
|
80
|
+
const baseAlpha = draw === 1 ? darkAlpha : lightAlpha;
|
|
81
|
+
const alpha = Math.max(0, Math.min(1, baseAlpha + jitterSign * jitter * rng()));
|
|
82
|
+
|
|
83
|
+
const [srcR, srcG, srcB] = draw === 1 ? darkColor : lightColor;
|
|
84
|
+
const srcA = alpha;
|
|
85
|
+
|
|
86
|
+
// Direct write (since background is transparent)
|
|
87
|
+
data[idx + 0] = clamp255(srcR);
|
|
88
|
+
data[idx + 1] = clamp255(srcG);
|
|
89
|
+
data[idx + 2] = clamp255(srcB);
|
|
90
|
+
data[idx + 3] = clamp255(srcA * 255);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
ctx.putImageData(img, 0, 0);
|
|
95
|
+
return canvas.toDataURL('image/png');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export { createNoiseBase64 };
|