187 lines
5.6 KiB
Vue
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>
|