477 lines
10 KiB
Vue
477 lines
10 KiB
Vue
<template>
|
|
<section
|
|
class="carousel-container"
|
|
aria-label="Featured content"
|
|
@mouseenter="pauseAutoplay"
|
|
@mouseleave="resumeAutoplay"
|
|
>
|
|
<KagolCarousel
|
|
v-model="activePage"
|
|
class="home-carousel"
|
|
:autoplay="false"
|
|
:interval="1000"
|
|
>
|
|
<article
|
|
v-for="slide in slides"
|
|
:key="slide.id"
|
|
ref="slideEls"
|
|
class="carousel-slide"
|
|
>
|
|
<div class="mask"></div>
|
|
<div class="banner-title" v-if="slide.title">{{ slide.title }}</div>
|
|
<img
|
|
v-if="!slide.video"
|
|
class="carousel-banner image"
|
|
:src="slide.image"
|
|
:alt="slide.alt"
|
|
/>
|
|
<video
|
|
v-else
|
|
class="carousel-banner video"
|
|
:alt="slide.alt"
|
|
:controls="false"
|
|
autoplay
|
|
muted
|
|
loop
|
|
>
|
|
<source :src="slide.video" type="video/mp4" />
|
|
</video>
|
|
|
|
<div class="desc flex flex-center" v-if="slide.description">
|
|
<span class="desc-fill" aria-hidden="true"></span>
|
|
<span class="desc-index-group">
|
|
<span class="desc-line" aria-hidden="true"></span>
|
|
<span class="desc-index-frame">
|
|
<span class="desc-index">{{ slide.number }}</span>
|
|
<span class="desc-index-cover" aria-hidden="true"></span>
|
|
</span>
|
|
</span>
|
|
<p class="desc-copy">{{ slide.description }}</p>
|
|
</div>
|
|
</article>
|
|
|
|
<template #pagination="{ prevPage, nextPage }">
|
|
<nav class="carousel-pagination" aria-label="Carousel pagination">
|
|
<button
|
|
class="carousel-pagination-button carousel-pagination-button-prev"
|
|
type="button"
|
|
aria-label="Previous slide"
|
|
@click="prevPage"
|
|
>
|
|
<span
|
|
class="carousel-pagination-arrow carousel-pagination-arrow-prev"
|
|
></span>
|
|
</button>
|
|
<button
|
|
class="carousel-pagination-button carousel-pagination-button-next"
|
|
type="button"
|
|
aria-label="Next slide"
|
|
@click="nextPage"
|
|
>
|
|
<span
|
|
class="carousel-pagination-arrow carousel-pagination-arrow-next"
|
|
></span>
|
|
</button>
|
|
</nav>
|
|
</template>
|
|
|
|
<template #indicator></template>
|
|
</KagolCarousel>
|
|
</section>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { Carousel as KagolCarousel } from '@kagol/vue-carousel'
|
|
import '@kagol/vue-carousel/dist/style.css'
|
|
import { gsap } from 'gsap'
|
|
import {
|
|
nextTick,
|
|
onBeforeUnmount,
|
|
onMounted,
|
|
shallowRef,
|
|
useTemplateRef,
|
|
watch
|
|
} from 'vue'
|
|
import mainBanner01 from '../../../assets/images/home/mainbanner01.jpg'
|
|
import mainBanner02 from '../../../assets/images/home/mainbanner02.jpg'
|
|
import Video from '@/assets/images/home/hero-desktop.mp4'
|
|
|
|
type HomeSlide = {
|
|
id: string
|
|
image: string
|
|
video: string
|
|
alt: string
|
|
title?: string
|
|
number?: string
|
|
description?: string
|
|
}
|
|
|
|
const activePage = shallowRef(1)
|
|
const isAutoplayEnabled = shallowRef(false)
|
|
const slideEls = useTemplateRef<HTMLElement[]>('slideEls')
|
|
const slides: readonly HomeSlide[] = [
|
|
{
|
|
id: 'aida',
|
|
image: mainBanner01,
|
|
video: '',
|
|
alt: 'Code Create product banner',
|
|
title: 'Shaping the future\nof fashion design',
|
|
number: '01',
|
|
description:
|
|
"World's first and only designer-led AI system that streamlines ideation from hours to seconds"
|
|
},
|
|
{
|
|
id: 'mixi',
|
|
image: mainBanner02,
|
|
video: '',
|
|
alt: 'Code Create product banner',
|
|
title: 'Be the game changer,\n subscribe now!',
|
|
number: '02',
|
|
description: 'Make the first move to streamline and facilitate your inspiration process'
|
|
},
|
|
{
|
|
id: 'video',
|
|
image: '',
|
|
video: Video,
|
|
alt: 'Code Create product video banner'
|
|
}
|
|
]
|
|
const descAnimationDelay = 1
|
|
let activeSlideIndex: number | null = null
|
|
let descAnimationFrame = 0
|
|
let descTimeline: ReturnType<typeof gsap.timeline> | null = null
|
|
|
|
function getActiveSlideIndex() {
|
|
const slideCount = slides.length
|
|
|
|
return ((activePage.value - 1) % slideCount + slideCount) % slideCount
|
|
}
|
|
|
|
function prefersReducedMotion() {
|
|
return window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false
|
|
}
|
|
|
|
function playDescAnimation(slideIndex: number) {
|
|
const activeSlide = slideEls.value?.[slideIndex]
|
|
const desc = activeSlide?.querySelector<HTMLElement>('.desc')
|
|
const fill = desc?.querySelector<HTMLElement>('.desc-fill')
|
|
const line = desc?.querySelector<HTMLElement>('.desc-line')
|
|
const index = desc?.querySelector<HTMLElement>('.desc-index')
|
|
const cover = desc?.querySelector<HTMLElement>('.desc-index-cover')
|
|
const copy = desc?.querySelector<HTMLElement>('.desc-copy')
|
|
|
|
descTimeline?.kill()
|
|
descTimeline = null
|
|
|
|
if (!fill || !line || !index || !cover || !copy) {
|
|
return
|
|
}
|
|
|
|
gsap.killTweensOf([fill, line, index, cover, copy])
|
|
|
|
if (prefersReducedMotion()) {
|
|
gsap.set(fill, { width: '100%' })
|
|
gsap.set(line, { autoAlpha: 1, width: 1, x: 0 })
|
|
gsap.set(index, { autoAlpha: 1, x: 0 })
|
|
gsap.set(cover, { autoAlpha: 0, xPercent: 110 })
|
|
gsap.set(copy, { autoAlpha: 1, x: 0 })
|
|
return
|
|
}
|
|
|
|
gsap.set(fill, { width: 0 })
|
|
gsap.set(line, { autoAlpha: 0, width: 5, x: 18 })
|
|
gsap.set(index, { autoAlpha: 0, x: -34 })
|
|
gsap.set(cover, { autoAlpha: 0, xPercent: 0 })
|
|
gsap.set(copy, { autoAlpha: 0, x: 22 })
|
|
|
|
descTimeline = gsap
|
|
.timeline({
|
|
delay: descAnimationDelay,
|
|
defaults: {
|
|
ease: 'power3.out'
|
|
}
|
|
})
|
|
.addLabel('panel', 0)
|
|
.to(fill, { duration: 1.05, ease: 'power2.out', width: '100%' }, 'panel')
|
|
.to(line, { autoAlpha: 1, duration: 0.9, width: 1, x: 0 }, 'panel+=0.12')
|
|
.addLabel('number', 1.18)
|
|
.set(index, { autoAlpha: 1 }, 'number')
|
|
.set(cover, { autoAlpha: 1 }, 'number')
|
|
.to(index, { duration: 0.7, x: 0 }, 'number')
|
|
.to(cover, { duration: 0.72, ease: 'power2.inOut', xPercent: 110 }, 'number+=0.08')
|
|
.to(copy, { autoAlpha: 1, duration: 0.72, x: 0 }, '>')
|
|
}
|
|
|
|
function queueDescAnimation() {
|
|
const slideIndex = getActiveSlideIndex()
|
|
|
|
if (slideIndex === activeSlideIndex) {
|
|
return
|
|
}
|
|
|
|
activeSlideIndex = slideIndex
|
|
nextTick(() => {
|
|
if (descAnimationFrame) {
|
|
window.cancelAnimationFrame(descAnimationFrame)
|
|
}
|
|
|
|
descAnimationFrame = window.requestAnimationFrame(() => {
|
|
descAnimationFrame = 0
|
|
playDescAnimation(slideIndex)
|
|
})
|
|
})
|
|
}
|
|
|
|
function pauseAutoplay() {
|
|
isAutoplayEnabled.value = false
|
|
}
|
|
|
|
function resumeAutoplay() {
|
|
isAutoplayEnabled.value = true
|
|
}
|
|
|
|
onMounted(() => {
|
|
resumeAutoplay()
|
|
queueDescAnimation()
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
if (descAnimationFrame) {
|
|
window.cancelAnimationFrame(descAnimationFrame)
|
|
}
|
|
|
|
descTimeline?.kill()
|
|
})
|
|
|
|
watch(activePage, queueDescAnimation)
|
|
</script>
|
|
|
|
<style scoped lang="less">
|
|
.carousel-container {
|
|
width: 100%;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.home-carousel {
|
|
.carousel-slide {
|
|
position: relative;
|
|
width: 100%;
|
|
aspect-ratio: 16 / 9;
|
|
height: 960px;
|
|
overflow: hidden;
|
|
.mask {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-color: rgba(0, 0, 0, 0.35);
|
|
z-index: 2;
|
|
}
|
|
.carousel-banner {
|
|
display: block;
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
position: absolute;
|
|
}
|
|
.banner-title {
|
|
position: absolute;
|
|
right: 50%;
|
|
top: 50%;
|
|
white-space: pre-line;
|
|
color: #fff;
|
|
font-size: 64px;
|
|
font-family: 'Poppins';
|
|
transform: translateY(-70%);
|
|
z-index: 3;
|
|
}
|
|
.desc {
|
|
position: absolute;
|
|
left: 0;
|
|
bottom: 0;
|
|
z-index: 3;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 24px;
|
|
height: 140px;
|
|
width: 656px;
|
|
color: #ffffff;
|
|
background: transparent;
|
|
isolation: isolate;
|
|
}
|
|
.desc-fill {
|
|
position: absolute;
|
|
inset: 0 auto 0 0;
|
|
z-index: 0;
|
|
display: block;
|
|
width: 0;
|
|
background: #a51f24;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.desc-index-group {
|
|
position: relative;
|
|
z-index: 1;
|
|
flex: 0 0 auto;
|
|
}
|
|
.desc-line {
|
|
position: absolute;
|
|
top: -160%;
|
|
left: 50%;
|
|
display: block;
|
|
width: 1px;
|
|
height: 65px;
|
|
background: rgba(255, 255, 255, 0.72);
|
|
opacity: 0;
|
|
transform: translateX(-50%);
|
|
will-change: transform, width, opacity;
|
|
}
|
|
.desc-index-frame {
|
|
position: relative;
|
|
display: block;
|
|
overflow: hidden;
|
|
}
|
|
.desc-index {
|
|
flex: 0 0 auto;
|
|
position: relative;
|
|
z-index: 1;
|
|
display: block;
|
|
font-family: 'Poppins';
|
|
font-size: 52px;
|
|
font-weight: 700;
|
|
line-height: 1;
|
|
opacity: 0;
|
|
will-change: transform, opacity;
|
|
}
|
|
.desc-index-cover {
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: 2;
|
|
display: block;
|
|
background: #ffffff;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
will-change: transform;
|
|
}
|
|
.desc-copy {
|
|
position: relative;
|
|
z-index: 1;
|
|
max-width: 440px;
|
|
margin: 0;
|
|
font-family: 'Poppins';
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
line-height: 1.45;
|
|
opacity: 0;
|
|
will-change: transform, opacity;
|
|
}
|
|
}
|
|
.carousel-pagination {
|
|
position: absolute;
|
|
right: 0;
|
|
bottom: 0;
|
|
z-index: 2;
|
|
display: flex;
|
|
flex-direction: column;
|
|
width: 70px;
|
|
height: 140px;
|
|
transform: translateX(100%);
|
|
transition: transform 0.28s ease;
|
|
will-change: transform;
|
|
}
|
|
.carousel-pagination-button {
|
|
position: relative;
|
|
display: flex;
|
|
flex: 1 1 0;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 100%;
|
|
padding: 0;
|
|
border: 0;
|
|
color: #ffffff;
|
|
background: #65090c;
|
|
cursor: pointer;
|
|
appearance: none;
|
|
transition: background-color 0.2s ease;
|
|
}
|
|
|
|
.carousel-pagination-button:hover {
|
|
background: rgba(255, 255, 255, 0.75);
|
|
}
|
|
|
|
.carousel-pagination-button:focus-visible {
|
|
position: relative;
|
|
z-index: 1;
|
|
outline: 2px solid #ffffff;
|
|
outline-offset: -6px;
|
|
}
|
|
|
|
.carousel-pagination-arrow {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
display: block;
|
|
box-sizing: border-box;
|
|
width: 20px;
|
|
height: 20px;
|
|
border-top: 2.5px solid currentColor;
|
|
border-right: 2.5px solid currentColor;
|
|
}
|
|
|
|
.carousel-pagination-arrow-prev {
|
|
transform: translate(-50%, -50%) rotate(225deg);
|
|
}
|
|
|
|
.carousel-pagination-arrow-next {
|
|
transform: translate(-50%, -50%) rotate(45deg);
|
|
}
|
|
}
|
|
|
|
.carousel-container:hover .carousel-pagination {
|
|
transform: translateX(0);
|
|
}
|
|
|
|
:deep(.xui-carousel) {
|
|
width: 100%;
|
|
}
|
|
|
|
:deep(.xui-carousel-item-container) {
|
|
height: 100%;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.carousel-slide {
|
|
min-height: 320px;
|
|
}
|
|
|
|
.home-carousel {
|
|
.carousel-slide {
|
|
.desc {
|
|
width: 100%;
|
|
min-width: 0;
|
|
height: auto;
|
|
min-height: 92px;
|
|
padding: 16px 20px;
|
|
}
|
|
.desc-line {
|
|
top: -28px;
|
|
height: 42px;
|
|
}
|
|
.desc-index {
|
|
font-size: 40px;
|
|
}
|
|
.desc-copy {
|
|
max-width: none;
|
|
font-size: 12px;
|
|
}
|
|
}
|
|
}
|
|
|
|
.carousel-pagination-arrow {
|
|
width: 18px;
|
|
height: 18px;
|
|
}
|
|
}
|
|
</style>
|