Reputation: 950
I'm using Embla Carousels in a project and want to have a nice slide scaling effect as you scroll through. Slides should get bigger the more they reach the left edge of the carousel container, and scale down relative to their distance to that left edge.
I found this example on their website: https://www.embla-carousel.com/examples/predefined/#scale
The core logic goes like this:
const [embla, setEmbla] = useState<Embla | null>(null);
const [scaleValues, setScaleValues] = useState<number[]>([]);
useEffect(() => {
if (!embla) return;
const onScroll = () => {
const engine = embla.internalEngine();
const scrollProgress = embla.scrollProgress();
const styles = embla.scrollSnapList().map((scrollSnap, index) => {
let diffToTarget = scrollSnap - scrollProgress;
if (engine.options.loop) {
engine.slideLooper.loopPoints.forEach(loopItem => {
const target = loopItem.target().get();
if (index === loopItem.index && target !== 0) {
const sign = Math.sign(target);
if (sign === -1) diffToTarget = scrollSnap - (1 + scrollProgress);
if (sign === 1) diffToTarget = scrollSnap + (1 - scrollProgress);
}
});
}
const scaleValue = 1 - Math.abs(diffToTarget * scaleFactor);
return clamp(scaleValue, 0, 1);
});
setScaleValues(styles);
};
onScroll();
const syncScroll = () => flushSync(onScroll);
embla.on("scroll", syncScroll);
embla.on("reInit", onScroll);
return () => {
embla.off("scroll", syncScroll);
embla.off("reInit", onScroll);
};
}, [embla, scaleFactor]);
scaleValues
gets then mapped onto the style property of the slides.
But they are several problems with this:
embla.scrollSnapList()
is the list of "snap" anchors the carousel has, not the list of slides. Its length is the length of the list of slides only if the carousel is small enough to only show one full slide at a time. Otherwise it's smaller than the list of slides. So depending on how many slides can fit into the view, some slides towards the end may not get any scaling at all.Is it possible to implement this feature while fixing all of the above?
The scaling difference between two slides should only be a function of their distance in px to the left edge of the Carousel, regardless of the Carousel's width or number of slides.
Upvotes: 1
Views: 2866
Reputation: 577
I've updated the tween examples including the scale example you mention. The following things have been fixed in the new example (see code snippet below):
import React, { useCallback, useEffect, useRef } from 'react'
import {
EmblaCarouselType,
EmblaEventType,
EmblaOptionsType
} from 'embla-carousel'
import useEmblaCarousel from 'embla-carousel-react'
const TWEEN_FACTOR_BASE = 0.52
const numberWithinRange = (number: number, min: number, max: number): number =>
Math.min(Math.max(number, min), max)
type PropType = {
slides: number[]
options?: EmblaOptionsType
}
const EmblaCarousel: React.FC<PropType> = (props) => {
const { slides, options } = props
const [emblaRef, emblaApi] = useEmblaCarousel(options)
const tweenFactor = useRef(0)
const tweenNodes = useRef<HTMLElement[]>([])
const setTweenNodes = useCallback((emblaApi: EmblaCarouselType): void => {
tweenNodes.current = emblaApi.slideNodes().map((slideNode) => {
return slideNode.querySelector('.embla__slide__number') as HTMLElement
})
}, [])
// Make tween factor slide count agnostic
const setTweenFactor = useCallback((emblaApi: EmblaCarouselType) => {
tweenFactor.current = TWEEN_FACTOR_BASE * emblaApi.scrollSnapList().length
}, [])
const tweenScale = useCallback(
(emblaApi: EmblaCarouselType, eventName?: EmblaEventType) => {
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]
// Include all slides when tweening
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.current)
const scale = numberWithinRange(tweenValue, 0, 1).toString()
const tweenNode = tweenNodes.current[slideIndex]
tweenNode.style.transform = `scale(${scale})`
})
})
},
[]
)
useEffect(() => {
if (!emblaApi) return
setTweenNodes(emblaApi)
setTweenFactor(emblaApi)
tweenScale(emblaApi)
emblaApi
.on('reInit', setTweenNodes)
.on('reInit', setTweenFactor)
.on('reInit', tweenScale)
.on('scroll', tweenScale)
}, [emblaApi, tweenScale])
return (
<div className="embla">
<div className="embla__viewport" ref={emblaRef}>
<div className="embla__container">
{slides.map((index) => (
<div className="embla__slide" key={index}>
<div className="embla__slide__number">{index + 1}</div>
</div>
))}
</div>
</div>
</div>
)
}
export default EmblaCarousel
Here's a link to the updated example in the docs. I hope this helps.
I'm not sure what you mean regarding this:
The scaling effect is dependent on the width of the Carousel, and the screen if the Carousel resizes according to it
Because there's no explicit correlation between them?
Upvotes: 1