mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 14:39:30 -07:00
refactor(style): utilize tailwind instead of CSS
This commit is contained in:
@@ -1,76 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
:class="['bounceCardsContainer', className]"
|
:class="['relative flex items-center justify-center', className]"
|
||||||
:style="{
|
:style="{
|
||||||
position: 'relative',
|
|
||||||
width: typeof containerWidth === 'number' ? `${containerWidth}px` : containerWidth,
|
width: typeof containerWidth === 'number' ? `${containerWidth}px` : containerWidth,
|
||||||
height: typeof containerHeight === 'number' ? `${containerHeight}px` : containerHeight,
|
height: typeof containerHeight === 'number' ? `${containerHeight}px` : containerHeight
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center'
|
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(src, idx) in images"
|
v-for="(src, idx) in images"
|
||||||
:key="idx"
|
:key="idx"
|
||||||
:class="`card card-${idx}`"
|
ref="cardRefs"
|
||||||
:style="{
|
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"
|
||||||
position: 'absolute',
|
:style="{ transform: transformStyles[idx] ?? 'none' }"
|
||||||
width: '200px',
|
|
||||||
aspectRatio: '1',
|
|
||||||
border: '5px solid #fff',
|
|
||||||
borderRadius: '25px',
|
|
||||||
overflow: 'hidden',
|
|
||||||
boxShadow: '0 4px 10px rgba(0, 0, 0, 0.2)',
|
|
||||||
transform: transformStyles[idx] ?? 'none',
|
|
||||||
backgroundColor: '#f8f9fa'
|
|
||||||
}"
|
|
||||||
@mouseenter="() => pushSiblings(idx)"
|
@mouseenter="() => pushSiblings(idx)"
|
||||||
@mouseleave="resetSiblings"
|
@mouseleave="resetSiblings"
|
||||||
>
|
>
|
||||||
<div
|
<div v-if="!imageLoaded[idx]" class="absolute inset-0 z-[1] flex items-center justify-center bg-black/80">
|
||||||
v-if="!imageLoaded[idx]"
|
<div class="w-[75px] h-[75px] border-[3px] border-gray-400 border-t-[#27FF64] rounded-full animate-spin"></div>
|
||||||
class="placeholder"
|
|
||||||
:style="{
|
|
||||||
position: 'absolute',
|
|
||||||
top: '0',
|
|
||||||
left: '0',
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
zIndex: 1
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="loading-spinner"
|
|
||||||
:style="{
|
|
||||||
width: '75px',
|
|
||||||
height: '75px',
|
|
||||||
border: '3px solid #a3a3a3',
|
|
||||||
borderTop: '3px solid #27FF64',
|
|
||||||
borderRadius: '50%',
|
|
||||||
}"
|
|
||||||
></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
class="image"
|
class="absolute inset-0 w-full h-full object-cover transition-opacity duration-300 ease-in-out z-[2]"
|
||||||
:src="src"
|
:src="src"
|
||||||
:alt="`card-${idx}`"
|
:alt="`card-${idx}`"
|
||||||
:style="{
|
:style="{ opacity: imageLoaded[idx] ? 1 : 0 }"
|
||||||
position: 'absolute',
|
|
||||||
top: '0',
|
|
||||||
left: '0',
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
objectFit: 'cover',
|
|
||||||
opacity: imageLoaded[idx] ? 1 : 0,
|
|
||||||
transition: 'opacity 0.3s ease',
|
|
||||||
zIndex: 2
|
|
||||||
}"
|
|
||||||
@load="() => onImageLoad(idx)"
|
@load="() => onImageLoad(idx)"
|
||||||
@error="() => onImageError(idx)"
|
@error="() => onImageError(idx)"
|
||||||
/>
|
/>
|
||||||
@@ -79,7 +32,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, onUnmounted, ref } from 'vue';
|
import { onMounted, onUnmounted, ref, watch, nextTick } from 'vue';
|
||||||
import { gsap } from 'gsap';
|
import { gsap } from 'gsap';
|
||||||
|
|
||||||
export interface BounceCardsProps {
|
export interface BounceCardsProps {
|
||||||
@@ -113,6 +66,7 @@ const props = withDefaults(defineProps<BounceCardsProps>(), {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const imageLoaded = ref(new Array(props.images.length).fill(false));
|
const imageLoaded = ref(new Array(props.images.length).fill(false));
|
||||||
|
const cardRefs = ref<HTMLElement[]>([]);
|
||||||
|
|
||||||
const getNoRotationTransform = (transformStr: string): string => {
|
const getNoRotationTransform = (transformStr: string): string => {
|
||||||
const hasRotate = /rotate\([\s\S]*?\)/.test(transformStr);
|
const hasRotate = /rotate\([\s\S]*?\)/.test(transformStr);
|
||||||
@@ -133,9 +87,7 @@ const getPushedTransform = (baseTransform: string, offsetX: number): string => {
|
|||||||
const newX = currentX + offsetX;
|
const newX = currentX + offsetX;
|
||||||
return baseTransform.replace(translateRegex, `translate(${newX}px)`);
|
return baseTransform.replace(translateRegex, `translate(${newX}px)`);
|
||||||
} else {
|
} else {
|
||||||
return baseTransform === 'none'
|
return baseTransform === 'none' ? `translate(${offsetX}px)` : `${baseTransform} translate(${offsetX}px)`;
|
||||||
? `translate(${offsetX}px)`
|
|
||||||
: `${baseTransform} translate(${offsetX}px)`;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -143,13 +95,13 @@ const pushSiblings = (hoveredIdx: number) => {
|
|||||||
if (!props.enableHover) return;
|
if (!props.enableHover) return;
|
||||||
|
|
||||||
props.images.forEach((_, i) => {
|
props.images.forEach((_, i) => {
|
||||||
gsap.killTweensOf(`.card-${i}`);
|
gsap.killTweensOf(cardRefs.value[i]);
|
||||||
|
|
||||||
const baseTransform = props.transformStyles[i] || 'none';
|
const baseTransform = props.transformStyles[i] || 'none';
|
||||||
|
|
||||||
if (i === hoveredIdx) {
|
if (i === hoveredIdx) {
|
||||||
const noRotationTransform = getNoRotationTransform(baseTransform);
|
const noRotationTransform = getNoRotationTransform(baseTransform);
|
||||||
gsap.to(`.card-${i}`, {
|
gsap.to(cardRefs.value[i], {
|
||||||
transform: noRotationTransform,
|
transform: noRotationTransform,
|
||||||
duration: 0.4,
|
duration: 0.4,
|
||||||
ease: 'back.out(1.4)',
|
ease: 'back.out(1.4)',
|
||||||
@@ -158,11 +110,10 @@ const pushSiblings = (hoveredIdx: number) => {
|
|||||||
} else {
|
} else {
|
||||||
const offsetX = i < hoveredIdx ? -160 : 160;
|
const offsetX = i < hoveredIdx ? -160 : 160;
|
||||||
const pushedTransform = getPushedTransform(baseTransform, offsetX);
|
const pushedTransform = getPushedTransform(baseTransform, offsetX);
|
||||||
|
|
||||||
const distance = Math.abs(hoveredIdx - i);
|
const distance = Math.abs(hoveredIdx - i);
|
||||||
const delay = distance * 0.05;
|
const delay = distance * 0.05;
|
||||||
|
|
||||||
gsap.to(`.card-${i}`, {
|
gsap.to(cardRefs.value[i], {
|
||||||
transform: pushedTransform,
|
transform: pushedTransform,
|
||||||
duration: 0.4,
|
duration: 0.4,
|
||||||
ease: 'back.out(1.4)',
|
ease: 'back.out(1.4)',
|
||||||
@@ -177,9 +128,9 @@ const resetSiblings = () => {
|
|||||||
if (!props.enableHover) return;
|
if (!props.enableHover) return;
|
||||||
|
|
||||||
props.images.forEach((_, i) => {
|
props.images.forEach((_, i) => {
|
||||||
gsap.killTweensOf(`.card-${i}`);
|
gsap.killTweensOf(cardRefs.value[i]);
|
||||||
const baseTransform = props.transformStyles[i] || 'none';
|
const baseTransform = props.transformStyles[i] || 'none';
|
||||||
gsap.to(`.card-${i}`, {
|
gsap.to(cardRefs.value[i], {
|
||||||
transform: baseTransform,
|
transform: baseTransform,
|
||||||
duration: 0.4,
|
duration: 0.4,
|
||||||
ease: 'back.out(1.4)',
|
ease: 'back.out(1.4)',
|
||||||
@@ -196,59 +147,31 @@ const onImageError = (idx: number) => {
|
|||||||
imageLoaded.value[idx] = true;
|
imageLoaded.value[idx] = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
const playEntranceAnimation = () => {
|
||||||
|
gsap.killTweensOf(cardRefs.value);
|
||||||
|
gsap.set(cardRefs.value, { opacity: 0, scale: 0 });
|
||||||
|
|
||||||
gsap.fromTo(
|
gsap.fromTo(
|
||||||
'.card',
|
cardRefs.value,
|
||||||
{ scale: 0 },
|
{ scale: 0, opacity: 0 },
|
||||||
{
|
{
|
||||||
scale: 1,
|
scale: 1,
|
||||||
|
opacity: 1,
|
||||||
stagger: props.animationStagger,
|
stagger: props.animationStagger,
|
||||||
ease: props.easeType,
|
ease: props.easeType,
|
||||||
delay: props.animationDelay
|
delay: props.animationDelay
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(playEntranceAnimation);
|
||||||
|
watch(() => props.images, async () => {
|
||||||
|
await nextTick();
|
||||||
|
gsap.set(cardRefs.value, { opacity: 0, scale: 0 });
|
||||||
|
playEntranceAnimation();
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
gsap.killTweensOf('.card');
|
gsap.killTweensOf(cardRefs.value);
|
||||||
props.images.forEach((_, i) => {
|
|
||||||
gsap.killTweensOf(`.card-${i}`);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.bounceCardsContainer {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: 400px;
|
|
||||||
height: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
position: absolute;
|
|
||||||
width: 200px;
|
|
||||||
aspect-ratio: 1;
|
|
||||||
border: 5px solid #fff;
|
|
||||||
border-radius: 25px;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
|
|
||||||
background-color: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card .image {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-spinner {
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
@keyframes spin {
|
|
||||||
0% { transform: rotate(0deg);}
|
|
||||||
100% { transform: rotate(360deg);}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user