mirror of
https://github.com/LukeHagar/website.git
synced 2025-12-09 12:57:48 +00:00
169 lines
5.4 KiB
Svelte
169 lines
5.4 KiB
Svelte
<script lang="ts">
|
|
import embla from 'embla-carousel-svelte';
|
|
import {
|
|
type EmblaCarouselType,
|
|
type EmblaEventType,
|
|
type EmblaOptionsType,
|
|
type EmblaPluginType
|
|
} from 'embla-carousel';
|
|
import { WheelGesturesPlugin } from 'embla-carousel-wheel-gestures';
|
|
|
|
let emblaApi: EmblaCarouselType;
|
|
|
|
let options: EmblaOptionsType = {
|
|
align: 'center',
|
|
skipSnaps: true,
|
|
loop: true
|
|
};
|
|
|
|
let hasPrev: boolean = false;
|
|
let hasNext: boolean = true;
|
|
|
|
const togglePrevNextBtnsState = () => {
|
|
if (emblaApi.canScrollPrev()) hasPrev = true;
|
|
else hasPrev = false;
|
|
|
|
if (emblaApi.canScrollNext()) hasNext = true;
|
|
else hasNext = false;
|
|
};
|
|
|
|
let plugins: EmblaPluginType[] = [WheelGesturesPlugin()];
|
|
|
|
let selectedScrollIndex = 0;
|
|
const onSelect = (index: number) => {
|
|
emblaApi.scrollTo(index);
|
|
selectedScrollIndex = emblaApi.selectedScrollSnap();
|
|
};
|
|
|
|
const onPrev = () => {
|
|
emblaApi.scrollPrev();
|
|
selectedScrollIndex = emblaApi.selectedScrollSnap();
|
|
};
|
|
|
|
const onNext = () => {
|
|
emblaApi.scrollNext();
|
|
selectedScrollIndex = emblaApi.selectedScrollSnap();
|
|
};
|
|
|
|
const TWEEN_FACTOR_BASE = 0.52;
|
|
let tweenFactor = 0;
|
|
let tweenNodes: HTMLElement[] = [];
|
|
|
|
const numberWithinRange = (number: number, min: number, max: number) =>
|
|
Math.min(Math.max(number, min), max);
|
|
|
|
const setTweenNodes = (emblaApi: EmblaCarouselType): void => {
|
|
tweenNodes = emblaApi.slideNodes().map((slideNode) => {
|
|
return slideNode.querySelector('.embla__slide__number') as HTMLElement;
|
|
});
|
|
};
|
|
|
|
const setTweenFactor = (emblaApi: EmblaCarouselType): void => {
|
|
tweenFactor = TWEEN_FACTOR_BASE * emblaApi.scrollSnapList().length;
|
|
};
|
|
|
|
const tweenScale = (emblaApi: EmblaCarouselType, eventName?: EmblaEventType): void => {
|
|
const engine = emblaApi.internalEngine();
|
|
const scrollProgress = emblaApi.scrollProgress();
|
|
const slidesInView = emblaApi.slidesInView();
|
|
const isScrollEvent = eventName === 'scroll';
|
|
|
|
emblaApi.scrollSnapList().forEach((scrollSnap, snapIndex) => {
|
|
let diffToTarget = scrollSnap - scrollProgress;
|
|
const slidesInSnap = engine.slideRegistry[snapIndex];
|
|
|
|
slidesInSnap.forEach((slideIndex) => {
|
|
if (isScrollEvent && !slidesInView.includes(slideIndex)) return;
|
|
|
|
if (engine.options.loop) {
|
|
engine.slideLooper.loopPoints.forEach((loopItem) => {
|
|
const target = loopItem.target();
|
|
|
|
if (slideIndex === loopItem.index && target !== 0) {
|
|
const sign = Math.sign(target);
|
|
|
|
if (sign === -1) {
|
|
diffToTarget = scrollSnap - (1 + scrollProgress);
|
|
}
|
|
if (sign === 1) {
|
|
diffToTarget = scrollSnap + (1 - scrollProgress);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
const tweenValue = 1 - Math.abs(diffToTarget * tweenFactor);
|
|
const scale = numberWithinRange(tweenValue, 0.8, 1).toString();
|
|
const tweenNode = tweenNodes[slideIndex];
|
|
tweenNode.style.transform = `scale(${scale})`;
|
|
});
|
|
});
|
|
};
|
|
|
|
const onEmblaInit = (event: CustomEvent<EmblaCarouselType>) => {
|
|
emblaApi = event.detail;
|
|
|
|
setTweenNodes(emblaApi);
|
|
setTweenFactor(emblaApi);
|
|
tweenScale(emblaApi);
|
|
|
|
emblaApi
|
|
.on('scroll', () => {
|
|
selectedScrollIndex = emblaApi.selectedScrollSnap();
|
|
})
|
|
.on('init', togglePrevNextBtnsState)
|
|
.on('select', togglePrevNextBtnsState)
|
|
.on('reInit', togglePrevNextBtnsState)
|
|
.on('reInit', setTweenNodes)
|
|
.on('reInit', setTweenFactor)
|
|
.on('reInit', tweenScale)
|
|
.on('scroll', tweenScale)
|
|
.on('slideFocus', tweenScale);
|
|
};
|
|
</script>
|
|
|
|
<div class="embla web-carousel">
|
|
{#if hasPrev}
|
|
<button class="web-carousel-button web-carousel-button-start" on:click={onPrev}>
|
|
<span class="web-icon-arrow-left" aria-hidden="true"></span>
|
|
</button>
|
|
{/if}
|
|
{#if hasNext}
|
|
<button class="web-carousel-button web-carousel-button-end" on:click={onNext}>
|
|
<span class="web-icon-arrow-right" aria-hidden="true"></span>
|
|
</button>
|
|
{/if}
|
|
|
|
<div class="embla__viewport" use:embla={{ options, plugins }} on:emblaInit={onEmblaInit}>
|
|
<ul class="embla__container">
|
|
<slot />
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="web-carousel-bullets">
|
|
<ul class="web-carousel-bullets-list">
|
|
{#each Array.from({ length: emblaApi?.scrollSnapList().length }) as _, i}
|
|
<li class="web-carousel-bullets-item">
|
|
<button
|
|
class="web-carousel-bullets-button"
|
|
class:is-selected={selectedScrollIndex === i}
|
|
aria-label={`gallery item ${i + 1}`}
|
|
on:click={() => onSelect(i)}
|
|
></button>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</div>
|
|
|
|
<style>
|
|
.embla {
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
|
|
.embla__container {
|
|
display: flex;
|
|
}
|
|
</style>
|