card gasp
This commit is contained in:
parent
9e5cc5ab1a
commit
1504c841e0
BIN
public/hero-tarot.jpg
Normal file
BIN
public/hero-tarot.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 162 KiB |
186
resources/js/components/landing/BackgroundCards.vue
Normal file
186
resources/js/components/landing/BackgroundCards.vue
Normal file
@ -0,0 +1,186 @@
|
||||
<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>
|
||||
@ -1,96 +1,180 @@
|
||||
<script lang="ts" setup>
|
||||
import { router } from '@inertiajs/vue3';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
const isMounted = ref(false);
|
||||
const buttonHover = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
isMounted.value = true;
|
||||
}, 100);
|
||||
});
|
||||
|
||||
const goToShuffle = () => {
|
||||
router.visit('/tirage');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="relative flex min-h-[60vh] items-center justify-center overflow-hidden px-4 py-20 text-center md:min-h-[80vh]">
|
||||
<!-- Animated background elements -->
|
||||
<div class="absolute inset-0 bg-black/50"></div>
|
||||
<div class="absolute top-0 left-0 h-full w-full">
|
||||
<div
|
||||
class="absolute top-1/4 left-1/4 h-72 w-72 animate-pulse rounded-full bg-purple-500 opacity-30 mix-blend-soft-light blur-xl filter"
|
||||
></div>
|
||||
<div
|
||||
class="absolute right-1/4 bottom-1/3 h-72 w-72 animate-bounce rounded-full bg-yellow-300 opacity-30 mix-blend-soft-light blur-xl filter delay-1000"
|
||||
style="animation-duration: 15s"
|
||||
></div>
|
||||
</div>
|
||||
<section
|
||||
class="relative flex min-h-screen w-full items-center justify-center overflow-hidden bg-cover bg-center bg-no-repeat px-4 py-24 text-center"
|
||||
>
|
||||
<div class="pointer-events-none absolute inset-0 bg-gradient-to-b from-[var(--c-purple)]/60 via-[var(--c-deep-navy)]/60 to-black/80"></div>
|
||||
|
||||
<div class="relative z-10 mx-auto max-w-3xl">
|
||||
<!-- Animated heading with hover effect -->
|
||||
<div class="relative z-10 mx-auto flex max-w-5xl flex-col items-center justify-between lg:flex-row">
|
||||
<!-- Text Content -->
|
||||
<div class="mb-10 lg:mb-0 lg:w-1/2">
|
||||
<h1
|
||||
class="mb-4 transform text-4xl font-bold text-white transition-all duration-700 md:text-6xl"
|
||||
class="mb-4 transform text-4xl font-black text-white transition-all duration-700 md:text-6xl"
|
||||
:class="isMounted ? 'translate-y-0 opacity-100' : 'translate-y-10 opacity-0'"
|
||||
>
|
||||
<span class="inline-block cursor-default transition-all duration-300 hover:scale-105 hover:text-purple-200">Libérez</span>
|
||||
<span class="mx-2 inline-block cursor-default transition-all duration-300 hover:scale-105 hover:text-purple-200">votre</span>
|
||||
<span class="inline-block cursor-default transition-all duration-300 hover:scale-105 hover:text-yellow-300">potentiel</span>
|
||||
<span class="mt-2 block"></span>
|
||||
<span class="inline-block cursor-default transition-all duration-300 hover:scale-105 hover:text-purple-200">avec Toutes</span>
|
||||
<span class="mx-2 inline-block cursor-default transition-all duration-300 hover:scale-105 hover:text-purple-200">les</span>
|
||||
<span class="inline-block cursor-default transition-all duration-300 hover:scale-105 hover:text-yellow-300">Clés du Succès</span>
|
||||
Révélez Votre Voyage Intérieur
|
||||
</h1>
|
||||
|
||||
<!-- Animated paragraph -->
|
||||
<p
|
||||
class="mb-8 transform text-lg text-white/80 transition-all delay-200 duration-1000 md:text-xl"
|
||||
:class="isMounted ? 'translate-y-0 opacity-100' : 'translate-y-10 opacity-0'"
|
||||
>
|
||||
Gagnez en clarté et en direction avec un coaching personnalisé et des tirages d'oracle.
|
||||
<span class="mt-2 block md:inline-block">Découvrez votre chemin vers le succès et l'épanouissement.</span>
|
||||
Embrassez la sagesse intemporelle de l'Oracle de Kris Saint Ange, un guide stratégique pour naviguer votre destin avec clarté et
|
||||
confiance.
|
||||
</p>
|
||||
|
||||
<!-- Animated buttons with enhanced hover effects -->
|
||||
<!-- Buttons -->
|
||||
<div
|
||||
class="flex transform flex-wrap justify-center gap-4 transition-all delay-500 duration-1000"
|
||||
class="flex transform flex-wrap justify-center gap-6 transition-all delay-500 duration-1000"
|
||||
:class="isMounted ? 'translate-y-0 opacity-100' : 'translate-y-10 opacity-0'"
|
||||
>
|
||||
<button
|
||||
@mouseenter="buttonHover = true"
|
||||
@mouseleave="buttonHover = false"
|
||||
@click="goToShuffle"
|
||||
class="group relative flex h-14 max-w-[480px] min-w-[180px] cursor-pointer items-center justify-center overflow-hidden rounded-xl bg-gradient-to-r from-purple-600 to-purple-800 px-8 text-base font-bold text-white shadow-lg shadow-purple-500/30 transition-all duration-500 hover:from-purple-700 hover:to-purple-900 hover:shadow-xl hover:shadow-purple-500/50 focus:ring-2 focus:ring-purple-400 focus:ring-offset-2 focus:ring-offset-black focus:outline-none"
|
||||
class="group relative inline-flex h-14 min-w-[180px] items-center justify-center overflow-hidden rounded-full bg-gradient-to-r from-[var(--c-gold)] to-yellow-400 px-8 font-bold tracking-wide text-[var(--c-purple)] shadow-lg transition-all duration-300 hover:-translate-y-0.5 hover:shadow-[var(--c-gold)]/40"
|
||||
>
|
||||
<span class="relative z-10 truncate transition-all duration-300 group-hover:scale-105">Essayez 1 carte gratuite</span>
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-r from-yellow-400 to-purple-500 opacity-0 transition-opacity duration-500 group-hover:opacity-30"
|
||||
></div>
|
||||
<span class="relative z-10 truncate">Essayez 1 carte gratuite</span>
|
||||
<span
|
||||
class="pointer-events-none absolute inset-0 translate-x-[-120%] -skew-x-12 bg-gradient-to-r from-transparent via-white/30 to-transparent transition-transform duration-700 group-hover:translate-x-[120%]"
|
||||
></span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@mouseenter="buttonHover = true"
|
||||
@mouseleave="buttonHover = false"
|
||||
@click="goToShuffle"
|
||||
class="group relative flex h-14 max-w-[480px] min-w-[180px] cursor-pointer items-center justify-center overflow-hidden rounded-xl border-2 border-purple-500 bg-transparent px-8 text-base font-bold text-white transition-all duration-500 hover:border-transparent hover:bg-gradient-to-r hover:from-purple-600 hover:to-purple-800 hover:text-white focus:ring-2 focus:ring-purple-400 focus:ring-offset-2 focus:ring-offset-black focus:outline-none"
|
||||
class="inline-flex h-14 min-w-[180px] items-center justify-center rounded-full border-2 border-[var(--c-purple)] bg-transparent px-8 font-bold text-white transition-all duration-300 hover:-translate-y-0.5 hover:bg-[var(--c-purple)]"
|
||||
>
|
||||
<span class="relative z-10 truncate transition-all duration-300 group-hover:scale-105">Découvrir les tirages</span>
|
||||
<div class="absolute inset-0 bg-white opacity-0 transition-opacity duration-500 group-hover:opacity-10"></div>
|
||||
<span class="truncate">Découvrir les tirages</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card Stack -->
|
||||
<div class="relative flex h-96 items-center justify-center lg:w-1/2">
|
||||
<div class="perspective-1000 relative h-96 w-64" @click.self="randomizeStack" title="Cliquez pour mélanger">
|
||||
<!-- Card Stack -->
|
||||
<div
|
||||
v-for="(card, index) in cardStack"
|
||||
:key="card.id"
|
||||
class="absolute h-full w-full cursor-pointer transition-all duration-500"
|
||||
:style="{
|
||||
transform: `translateY(${index * -4}px) rotate(${index * -2}deg)`,
|
||||
zIndex: cardStack.length - index,
|
||||
}"
|
||||
@click="flipCard(card)"
|
||||
>
|
||||
<div
|
||||
class="preserve-3d h-full w-full rounded-xl shadow-xl transition-transform duration-700"
|
||||
:class="{ 'rotate-y-180': card.isFlipped }"
|
||||
>
|
||||
<!-- Card Back -->
|
||||
<div
|
||||
class="border-gold/50 absolute h-full w-full overflow-hidden rounded-xl border-2 bg-gradient-to-br from-purple-900 via-blue-900 to-indigo-900 backface-hidden"
|
||||
>
|
||||
<img src="/cards/1.png" alt="Card Back" class="card-fill" />
|
||||
</div>
|
||||
|
||||
<!-- Card Front -->
|
||||
<div class="absolute h-full w-full rotate-y-180 overflow-hidden rounded-xl backface-hidden">
|
||||
<img :src="card.image" class="card-fill" :alt="'Card ' + card.id" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.gradient-bg {
|
||||
background: linear-gradient(135deg, #1a202c 0%, #2d3748 50%, #4a5568 100%);
|
||||
<script setup lang="ts">
|
||||
import { router } from '@inertiajs/vue3';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
const isMounted = ref(false);
|
||||
const container = ref<HTMLDivElement | null>(null);
|
||||
|
||||
// Sample card data
|
||||
const cardStack = ref([
|
||||
{ id: 2, title: "L'Intuition", image: '/cards/2.png', isFlipped: false },
|
||||
{ id: 3, title: 'La Sagesse', image: '/cards/3.png', isFlipped: false },
|
||||
{ id: 4, title: 'Le Destin', image: '/cards/4.png', isFlipped: false },
|
||||
]);
|
||||
|
||||
// Background images
|
||||
const backgroundImages = Array.from({ length: 12 }, (_, i) => `/cards/${(i % 4) + 1}.png`);
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
isMounted.value = true;
|
||||
}, 100);
|
||||
|
||||
// Initialize GSAP animations for background
|
||||
if (container.value) {
|
||||
const cards = container.value.querySelectorAll('.bg-card');
|
||||
cards.forEach((el: Element, idx: number) => {
|
||||
const startY = Math.random() * 300 - 150;
|
||||
const rot = Math.random() * 30 - 15;
|
||||
const drift = Math.random() * 160 + 120;
|
||||
|
||||
// Set initial state
|
||||
(el as HTMLElement).style.transform = `translate(-50%, -50%) rotate(${rot}deg) translateY(${startY}px)`;
|
||||
(el as HTMLElement).style.opacity = '0.5';
|
||||
|
||||
// Animate with requestAnimationFrame for simplicity
|
||||
// In a real implementation, you would use GSAP here
|
||||
let startTime: number | null = null;
|
||||
const duration = 20000 + Math.random() * 10000;
|
||||
|
||||
function animate(timestamp: number) {
|
||||
if (!startTime) startTime = timestamp;
|
||||
const elapsed = timestamp - startTime;
|
||||
const progress = (elapsed % duration) / duration;
|
||||
|
||||
const yPos = startY + drift * progress;
|
||||
const rotation = rot + 10 * Math.sin(progress * Math.PI * 2);
|
||||
|
||||
(el as HTMLElement).style.transform = `translate(-50%, -50%) rotate(${rotation}deg) translateY(${yPos}px)`;
|
||||
(el as HTMLElement).style.opacity = `${0.5 + 0.35 * Math.sin(progress * Math.PI)}`;
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const goToShuffle = () => {
|
||||
router.visit('/tirage');
|
||||
};
|
||||
|
||||
function randomInt(min: number, max: number) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
function randomizeStack() {
|
||||
const picked = new Set<number>();
|
||||
while (picked.size < 3) picked.add(randomInt(1, 22));
|
||||
const ids = Array.from(picked);
|
||||
cardStack.value = ids.map((id, idx) => ({ id, title: `Carte ${id}`, image: `/cards/${id}.png`, isFlipped: false }));
|
||||
}
|
||||
|
||||
const flipCard = (card: any) => {
|
||||
card.isFlipped = !card.isFlipped;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@keyframes float {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0) rotate(0deg);
|
||||
}
|
||||
33% {
|
||||
transform: translateY(-20px) rotate(5deg);
|
||||
}
|
||||
66% {
|
||||
transform: translateY(10px) rotate(-5deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom animation for text elements */
|
||||
@keyframes textShine {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
@ -100,12 +184,54 @@ const goToShuffle = () => {
|
||||
}
|
||||
}
|
||||
|
||||
.animate-float {
|
||||
animation: float 10s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-text-shine {
|
||||
background: linear-gradient(to right, #ffffff 20%, #d8b4fe 30%, #fef08a 70%, #ffffff 80%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-size: 500% auto;
|
||||
animation: textShine 5s ease-in-out infinite alternate;
|
||||
animation: textShine 2s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.card-fill {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.perspective-1000 {
|
||||
perspective: 1000px;
|
||||
}
|
||||
|
||||
.preserve-3d {
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.rotate-y-180 {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
.backface-hidden {
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.bg-card {
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
filter: drop-shadow(0 12px 30px rgba(0, 0, 0, 0.35));
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
|
||||
/* Custom color variables */
|
||||
:root {
|
||||
--c-purple: #4c1d95;
|
||||
--c-deep-navy: #1e1b4b;
|
||||
--c-gold: rgba(245, 158, 11, 0.7);
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.bg-card {
|
||||
width: 10rem;
|
||||
height: 15rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -5,6 +5,8 @@
|
||||
<main class="flex flex-col items-center">
|
||||
<div class="w-full max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<HeroSection primaryButtonLink="/tirage" secondaryButtonLink="/tirage" minHeight="70vh" />
|
||||
<div id="after-hero-sentinel" class="h-px w-full"></div>
|
||||
<BackgroundCards />
|
||||
<ManuscritSection />
|
||||
<OfferSection />
|
||||
<HowSection />
|
||||
@ -23,5 +25,6 @@ import HowSection from '@/components/landing/HowSection.vue';
|
||||
import ManuscritSection from '@/components/landing/ManuscritSection.vue';
|
||||
import OfferSection from '@/components/landing/OfferSection.vue';
|
||||
import TestimonialsSection from '@/components/landing/TestimonialsSection.vue';
|
||||
import BackgroundCards from '@/components/landing/BackgroundCards.vue';
|
||||
import LandingLayout from '@/layouts/app/LandingLayout.vue';
|
||||
</script>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user