Nyavokevin 1504c841e0
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
card gasp
2025-09-19 15:32:25 +03:00

187 lines
5.6 KiB
Vue

<template>
<!-- Fixed, pointer-events none so it stays behind the content -->
<div ref="container" class="pointer-events-none fixed inset-0 z-0 overflow-hidden opacity-0 transition-opacity duration-500">
<div
v-for="(src, i) in images"
:key="i"
class="absolute rounded-lg bg-card opacity-70 shadow-2xl"
:style="{
left: `${(i * 83) % 100}%`,
top: `${(i * 37) % 100}%`,
transform: `translate(-50%, -50%) rotate(${(i % 2 ? 1 : -1) * 8}deg)`,
backgroundImage: `url(${src})`,
}"
></div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
const container = ref<HTMLDivElement | null>(null);
function loadScript(src: string) {
return new Promise<void>((resolve, reject) => {
const s = document.createElement('script');
s.src = src;
s.async = true;
s.onload = () => resolve();
s.onerror = () => reject(new Error(`Failed to load ${src}`));
document.head.appendChild(s);
});
}
async function loadGsap() {
const w = window as any;
if (!w.gsap) {
await loadScript('https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js');
}
if (!w.ScrollTrigger) {
await loadScript('https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js');
}
if (w.gsap && w.ScrollTrigger) {
w.gsap.registerPlugin(w.ScrollTrigger);
}
return w.gsap as typeof import('gsap');
}
const images = Array.from({ length: 5 }, (_, i) => `/cards/${i + 1}.png`);
onMounted(async () => {
try {
const gsap = await loadGsap();
const w = window as any;
const ScrollTrigger = w.ScrollTrigger;
const cards = container.value?.querySelectorAll('.bg-card');
cards?.forEach((el: Element, idx: number) => {
// Random starting positions and properties
const startY = gsap.utils.random(-150, 150);
const startX = gsap.utils.random(-100, 100);
const rot = gsap.utils.random(-15, 15);
const drift = gsap.utils.random(120, 280);
const scale = gsap.utils.random(0.8, 1.2);
const duration = gsap.utils.random(15, 25);
// Set initial state
gsap.set(el, {
y: startY,
x: startX,
rotation: rot,
opacity: 0.5,
scale: scale,
});
// Create floating animation
gsap.to(el, {
y: startY + drift,
x: startX + gsap.utils.random(-40, 40),
rotation: rot + gsap.utils.random(-20, 20),
opacity: gsap.utils.random(0.6, 0.9),
scale: scale + gsap.utils.random(-0.1, 0.1),
ease: 'sine.inOut',
duration: duration,
yoyo: true,
repeat: -1,
delay: idx * 0.3,
});
// Scroll-based animation
gsap.to(el, {
y: startY + drift * 1.5,
rotation: rot + gsap.utils.random(-10, 10),
opacity: 0.85,
ease: 'none',
scrollTrigger: {
trigger: document.body,
start: 'top top',
end: 'bottom bottom',
scrub: true,
},
});
});
// Subtle parallax drift for the whole layer
gsap.to(container.value, {
y: 60,
ease: 'none',
scrollTrigger: {
trigger: document.body,
start: 'top top',
end: 'bottom bottom',
scrub: true,
},
});
// Add pulsing glow effect to container (will be visible only when activated)
gsap.to(container.value, {
duration: 4,
opacity: 0.9,
repeat: -1,
yoyo: true,
ease: 'sine.inOut',
});
// Show only after hero: observe the sentinel placed after hero
const sentinel = document.getElementById('after-hero-sentinel');
if (sentinel) {
const ob = new IntersectionObserver(
(entries) => {
entries.forEach((e) => {
if (e.isIntersecting) {
container.value?.classList.remove('opacity-0');
container.value?.classList.add('opacity-100');
} else {
container.value?.classList.add('opacity-0');
container.value?.classList.remove('opacity-100');
}
});
},
{ threshold: 0.1 }
);
ob.observe(sentinel);
}
} catch (e) {
console.error('GSAP load/animation failed:', e);
}
});
</script>
<style scoped>
.bg-card {
background-size: cover;
background-position: center;
filter: drop-shadow(0 12px 30px rgba(0, 0, 0, 0.35));
mix-blend-mode: screen;
width: 8rem;
height: 12rem;
transition: filter 0.5s ease;
}
.bg-card:hover {
filter: drop-shadow(0 15px 35px rgba(100, 100, 255, 0.5)) brightness(1.1);
}
@media (min-width: 1024px) {
.bg-card {
width: 10rem;
height: 15rem;
}
}
/* Subtle glow animation for the container */
::v-deep(.fixed) {
animation: subtleGlow 8s ease-in-out infinite;
}
@keyframes subtleGlow {
0%,
100% {
opacity: 0.8;
}
50% {
opacity: 1;
}
}
</style>