1
1
<template >
2
2
<div
3
- :class =" ['bounceCardsContainer ', className]"
3
+ :class =" ['relative flex items-center justify-center ', className]"
4
4
:style =" {
5
- position: 'relative',
6
5
width: typeof containerWidth === 'number' ? `${containerWidth}px` : containerWidth,
7
- height: typeof containerHeight === 'number' ? `${containerHeight}px` : containerHeight,
8
- display: 'flex',
9
- justifyContent: 'center',
10
- alignItems: 'center'
6
+ height: typeof containerHeight === 'number' ? `${containerHeight}px` : containerHeight
11
7
}"
12
8
>
13
9
<div
14
10
v-for =" (src, idx) in images"
15
11
:key =" idx"
16
- :class =" `card card-${idx}`"
17
- :style =" {
18
- position: 'absolute',
19
- width: '200px',
20
- aspectRatio: '1',
21
- border: '5px solid #fff',
22
- borderRadius: '25px',
23
- overflow: 'hidden',
24
- boxShadow: '0 4px 10px rgba(0, 0, 0, 0.2)',
25
- transform: transformStyles[idx] ?? 'none',
26
- backgroundColor: '#f8f9fa'
27
- }"
12
+ ref =" cardRefs"
13
+ class =" absolute w-[200px] aspect-square border-[5px] border-white rounded-[25px] overflow-hidden shadow-[0_4px_10px_rgba(0,0,0,0.2)] bg-[#f8f9fa] opacity-0"
14
+ :style =" { transform: transformStyles[idx] ?? 'none' }"
28
15
@mouseenter =" () => pushSiblings(idx)"
29
16
@mouseleave =" resetSiblings"
30
17
>
31
- <div
32
- v-if =" !imageLoaded[idx]"
33
- class =" placeholder"
34
- :style =" {
35
- position: 'absolute',
36
- top: '0',
37
- left: '0',
38
- width: '100%',
39
- height: '100%',
40
- backgroundColor: 'rgba(0, 0, 0, 0.8)',
41
- display: 'flex',
42
- alignItems: 'center',
43
- justifyContent: 'center',
44
- zIndex: 1
45
- }"
46
- >
47
- <div
48
- class =" loading-spinner"
49
- :style =" {
50
- width: '75px',
51
- height: '75px',
52
- border: '3px solid #a3a3a3',
53
- borderTop: '3px solid #27FF64',
54
- borderRadius: '50%',
55
- }"
56
- ></div >
18
+ <div v-if =" !imageLoaded[idx]" class =" absolute inset-0 z-[1] flex items-center justify-center bg-black/80" >
19
+ <div class =" w-[75px] h-[75px] border-[3px] border-gray-400 border-t-[#27FF64] rounded-full animate-spin" ></div >
57
20
</div >
58
21
59
22
<img
60
- class =" image "
23
+ class =" absolute inset-0 w-full h-full object-cover transition-opacity duration-300 ease-in-out z-[2] "
61
24
:src =" src"
62
25
:alt =" `card-${idx}`"
63
- :style =" {
64
- position: 'absolute',
65
- top: '0',
66
- left: '0',
67
- width: '100%',
68
- height: '100%',
69
- objectFit: 'cover',
70
- opacity: imageLoaded[idx] ? 1 : 0,
71
- transition: 'opacity 0.3s ease',
72
- zIndex: 2
73
- }"
26
+ :style =" { opacity: imageLoaded[idx] ? 1 : 0 }"
74
27
@load =" () => onImageLoad(idx)"
75
28
@error =" () => onImageError(idx)"
76
29
/>
79
32
</template >
80
33
81
34
<script setup lang="ts">
82
- import { onMounted , onUnmounted , ref } from ' vue' ;
35
+ import { onMounted , onUnmounted , ref , watch , nextTick } from ' vue' ;
83
36
import { gsap } from ' gsap' ;
84
37
85
38
export interface BounceCardsProps {
@@ -113,6 +66,7 @@ const props = withDefaults(defineProps<BounceCardsProps>(), {
113
66
});
114
67
115
68
const imageLoaded = ref (new Array (props .images .length ).fill (false ));
69
+ const cardRefs = ref <HTMLElement []>([]);
116
70
117
71
const getNoRotationTransform = (transformStr : string ): string => {
118
72
const hasRotate = / rotate\( [\s\S ] *? \) / .test (transformStr );
@@ -133,23 +87,21 @@ const getPushedTransform = (baseTransform: string, offsetX: number): string => {
133
87
const newX = currentX + offsetX ;
134
88
return baseTransform .replace (translateRegex , ` translate(${newX }px) ` );
135
89
} else {
136
- return baseTransform === ' none'
137
- ? ` translate(${offsetX }px) `
138
- : ` ${baseTransform } translate(${offsetX }px) ` ;
90
+ return baseTransform === ' none' ? ` translate(${offsetX }px) ` : ` ${baseTransform } translate(${offsetX }px) ` ;
139
91
}
140
92
};
141
93
142
94
const pushSiblings = (hoveredIdx : number ) => {
143
95
if (! props .enableHover ) return ;
144
96
145
97
props .images .forEach ((_ , i ) => {
146
- gsap .killTweensOf (` .card-${ i } ` );
98
+ gsap .killTweensOf (cardRefs . value [ i ] );
147
99
148
100
const baseTransform = props .transformStyles [i ] || ' none' ;
149
101
150
102
if (i === hoveredIdx ) {
151
103
const noRotationTransform = getNoRotationTransform (baseTransform );
152
- gsap .to (` .card-${ i } ` , {
104
+ gsap .to (cardRefs . value [ i ] , {
153
105
transform: noRotationTransform ,
154
106
duration: 0.4 ,
155
107
ease: ' back.out(1.4)' ,
@@ -158,11 +110,10 @@ const pushSiblings = (hoveredIdx: number) => {
158
110
} else {
159
111
const offsetX = i < hoveredIdx ? - 160 : 160 ;
160
112
const pushedTransform = getPushedTransform (baseTransform , offsetX );
161
-
162
113
const distance = Math .abs (hoveredIdx - i );
163
114
const delay = distance * 0.05 ;
164
115
165
- gsap .to (` .card-${ i } ` , {
116
+ gsap .to (cardRefs . value [ i ] , {
166
117
transform: pushedTransform ,
167
118
duration: 0.4 ,
168
119
ease: ' back.out(1.4)' ,
@@ -177,9 +128,9 @@ const resetSiblings = () => {
177
128
if (! props .enableHover ) return ;
178
129
179
130
props .images .forEach ((_ , i ) => {
180
- gsap .killTweensOf (` .card-${ i } ` );
131
+ gsap .killTweensOf (cardRefs . value [ i ] );
181
132
const baseTransform = props .transformStyles [i ] || ' none' ;
182
- gsap .to (` .card-${ i } ` , {
133
+ gsap .to (cardRefs . value [ i ] , {
183
134
transform: baseTransform ,
184
135
duration: 0.4 ,
185
136
ease: ' back.out(1.4)' ,
@@ -196,59 +147,31 @@ const onImageError = (idx: number) => {
196
147
imageLoaded .value [idx ] = true ;
197
148
};
198
149
199
- onMounted (() => {
150
+ const playEntranceAnimation = () => {
151
+ gsap .killTweensOf (cardRefs .value );
152
+ gsap .set (cardRefs .value , { opacity: 0 , scale: 0 });
153
+
200
154
gsap .fromTo (
201
- ' .card ' ,
202
- { scale: 0 },
155
+ cardRefs . value ,
156
+ { scale: 0 , opacity: 0 },
203
157
{
204
158
scale: 1 ,
159
+ opacity: 1 ,
205
160
stagger: props .animationStagger ,
206
161
ease: props .easeType ,
207
162
delay: props .animationDelay
208
163
}
209
164
);
165
+ };
166
+
167
+ onMounted (playEntranceAnimation );
168
+ watch (() => props .images , async () => {
169
+ await nextTick ();
170
+ gsap .set (cardRefs .value , { opacity: 0 , scale: 0 });
171
+ playEntranceAnimation ();
210
172
});
211
173
212
174
onUnmounted (() => {
213
- gsap .killTweensOf (' .card' );
214
- props .images .forEach ((_ , i ) => {
215
- gsap .killTweensOf (` .card-${i } ` );
216
- });
175
+ gsap .killTweensOf (cardRefs .value );
217
176
});
218
177
</script >
219
-
220
- <style scoped>
221
- .bounceCardsContainer {
222
- position : relative ;
223
- display : flex ;
224
- justify-content : center ;
225
- align-items : center ;
226
- width : 400px ;
227
- height : 400px ;
228
- }
229
-
230
- .card {
231
- position : absolute ;
232
- width : 200px ;
233
- aspect-ratio : 1 ;
234
- border : 5px solid #fff ;
235
- border-radius : 25px ;
236
- overflow : hidden ;
237
- box-shadow : 0 4px 10px rgba (0 , 0 , 0 , 0.2 );
238
- background-color : transparent !important ;
239
- }
240
-
241
- .card .image {
242
- width : 100% ;
243
- height : 100% ;
244
- object-fit : cover ;
245
- }
246
-
247
- .loading-spinner {
248
- animation : spin 1s linear infinite ;
249
- }
250
- @keyframes spin {
251
- 0% { transform : rotate (0deg );}
252
- 100% { transform : rotate (360deg );}
253
- }
254
- </style >
0 commit comments