cards / index.html
naryvi's picture
使用vue、tailwindcss、daisyui实现一个组件: 若干图片按照卡片堆形式展示,默认整齐堆叠,并有缓缓浮动的动效;鼠标移入后,聚焦展示的图片居中完整展示并不静止,其他图片缓缓散开到四周遮挡在该卡片下面;点击后轮流居中展示图片,之前图片散开到四周。 整个组件动画过度平缓,简练,又富有诗意,动画效果可以使用gsap.js实现 - Initial Deployment
a02c814 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Poetic Image Stack</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/daisyui@3.9.4/dist/full.css" rel="stylesheet" type="text/css" />
<script src="https://cdn.jsdelivr.net/npm/vue@3.2.47/dist/vue.global.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.11.5/gsap.min.js"></script>
<style>
.card-stack {
perspective: 1000px;
}
.card {
transition: transform 0.5s ease, opacity 0.5s ease;
transform-origin: center;
will-change: transform, opacity;
}
.floating {
animation: float 6s ease-in-out infinite;
}
@keyframes float {
0%, 100% {
transform: translateY(0) rotate(0deg);
}
50% {
transform: translateY(-15px) rotate(1deg);
}
}
</style>
</head>
<body class="bg-gradient-to-br from-indigo-50 to-purple-50 min-h-screen flex items-center justify-center p-4">
<div id="app" class="w-full max-w-4xl">
<div class="text-center mb-12">
<h1 class="text-4xl font-bold text-indigo-900 mb-2">Poetic Image Stack</h1>
<p class="text-indigo-700 opacity-80">Hover to focus, click to cycle through memories</p>
</div>
<div class="card-stack relative h-96 w-full flex items-center justify-center">
<div
v-for="(image, index) in images"
:key="index"
:class="[
'card absolute rounded-xl shadow-xl overflow-hidden cursor-pointer transition-all duration-500',
'border-4 border-white',
index === activeIndex ? 'z-50' : 'z-' + (10 - index),
index === activeIndex ? 'w-full h-full max-w-md' : 'w-40 h-52',
{'floating': !hovered && index !== activeIndex}
]"
@mouseenter="hoverCard(index)"
@mouseleave="unhoverCard"
@click="cycleCard"
ref="cards"
>
<img
:src="image.src"
:alt="image.alt"
class="w-full h-full object-cover"
/>
<div
v-if="index === activeIndex"
class="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-4 text-white"
>
<h3 class="text-xl font-semibold">{{ image.title }}</h3>
<p class="text-sm opacity-90">{{ image.description }}</p>
</div>
</div>
</div>
<div class="mt-16 text-center">
<button
@click="cycleCard"
class="btn btn-primary btn-outline px-8 py-3 rounded-full transition-all hover:scale-105"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Cycle Images
</button>
</div>
</div>
<script>
const { createApp, ref, onMounted } = Vue;
createApp({
setup() {
const images = ref([
{
src: 'https://images.unsplash.com/photo-1506744038136-46273834b3fb',
alt: 'Mountain landscape',
title: 'Serene Peaks',
description: 'Where the earth touches the sky'
},
{
src: 'https://images.unsplash.com/photo-1519125323398-675f0ddb6308',
alt: 'Forest path',
title: 'Enchanted Woods',
description: 'A journey through whispering trees'
},
{
src: 'https://images.unsplash.com/photo-1429087969512-1e85aab2683d',
alt: 'Ocean waves',
title: 'Endless Blue',
description: 'The rhythm of the tides'
},
{
src: 'https://images.unsplash.com/photo-1501785888041-af3ef285b470',
alt: 'Lake sunset',
title: 'Golden Hour',
description: 'Nature\'s daily masterpiece'
},
{
src: 'https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05',
alt: 'Misty morning',
title: 'Veiled Dawn',
description: 'The world awakening in soft light'
}
]);
const activeIndex = ref(0);
const hovered = ref(false);
const cards = ref([]);
const positions = [
{ x: -100, y: -50, rotation: -5, scale: 0.8 },
{ x: 100, y: -30, rotation: 3, scale: 0.7 },
{ x: -80, y: 40, rotation: -2, scale: 0.6 },
{ x: 120, y: 60, rotation: 4, scale: 0.5 }
];
const animateCards = () => {
cards.value.forEach((card, index) => {
if (index === activeIndex.value) {
// Center the active card
gsap.to(card, {
x: 0,
y: 0,
rotation: 0,
scale: 1,
opacity: 1,
duration: 1,
ease: "back.out(1.2)"
});
} else {
// Position other cards around
const posIndex = index > activeIndex.value ? index - 1 : index;
const pos = positions[posIndex % positions.length];
gsap.to(card, {
x: pos.x,
y: pos.y,
rotation: pos.rotation,
scale: pos.scale,
opacity: 0.7,
duration: 1,
ease: "power2.out"
});
}
});
};
const hoverCard = (index) => {
hovered.value = true;
activeIndex.value = index;
animateCards();
};
const unhoverCard = () => {
hovered.value = false;
animateCards();
};
const cycleCard = () => {
activeIndex.value = (activeIndex.value + 1) % images.value.length;
animateCards();
};
onMounted(() => {
// Initial animation
setTimeout(() => {
animateCards();
}, 500);
// Add subtle floating animation to inactive cards
setInterval(() => {
if (!hovered.value) {
cards.value.forEach((card, index) => {
if (index !== activeIndex.value) {
gsap.to(card, {
y: `+=${Math.random() * 10 - 5}`,
rotation: `+=${Math.random() * 2 - 1}`,
duration: 5,
ease: "sine.inOut"
});
}
});
}
}, 3000);
});
return {
images,
activeIndex,
hovered,
cards,
hoverCard,
unhoverCard,
cycleCard
};
}
}).mount('#app');
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=naryvi/cards" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>