From 6ed3a5113bdbd50108c14ed2ad87d83380bec16b Mon Sep 17 00:00:00 2001 From: David Jerleke Date: Thu, 4 Jul 2024 21:43:08 +0200 Subject: [PATCH] Bug fix for #928. --- .../src/components/AutoScroll.ts | 2 + .../src/components/Fade.ts | 4 +- .../src/__tests__/loop-ltr.test.ts | 1 + .../src/__tests__/scrollBounds-ltr.test.ts | 5 +- .../src/__tests__/scrollBounds-rtl.test.ts | 5 +- .../__tests__/scrollBounds-vertical.test.ts | 5 +- .../src/components/Animations.ts | 27 +++++-- .../embla-carousel/src/components/Engine.ts | 79 ++++++++++++++----- .../src/components/ScrollBody.ts | 17 ++-- .../embla-carousel/src/components/ScrollTo.ts | 12 ++- .../src/main.ts | 2 +- 11 files changed, 117 insertions(+), 42 deletions(-) diff --git a/packages/embla-carousel-auto-scroll/src/components/AutoScroll.ts b/packages/embla-carousel-auto-scroll/src/components/AutoScroll.ts index 587ae5c12..5029c579d 100644 --- a/packages/embla-carousel-auto-scroll/src/components/AutoScroll.ts +++ b/packages/embla-carousel-auto-scroll/src/components/AutoScroll.ts @@ -158,6 +158,8 @@ function AutoScroll(userOptions: AutoScrollOptionsType = {}): AutoScrollType { const directionSign = options.direction === 'forward' ? -1 : 1 const noop = (): ScrollBodyType => self + // TODO: Make it work with interpolation + let bodyVelocity = 0 let scrollDirection = 0 let rawLocation = location.get() diff --git a/packages/embla-carousel-fade/src/components/Fade.ts b/packages/embla-carousel-fade/src/components/Fade.ts index 98d5b4dea..63eabb4a7 100644 --- a/packages/embla-carousel-fade/src/components/Fade.ts +++ b/packages/embla-carousel-fade/src/components/Fade.ts @@ -217,7 +217,9 @@ function Fade(userOptions: FadeOptionsType = {}): FadeType { const fade = (emblaApi: EmblaCarouselType): void => { const { dragHandler, scrollBody } = emblaApi.internalEngine() const pointerDown = dragHandler.pointerDown() - const velocity = scrollBody.velocity() + // TODO: There's probably a better way to solve velocity below + const timeStep = 1000 / 60 + const velocity = (scrollBody.velocity() * timeStep) / 1000 const duration = scrollBody.duration() const fadeIndex = getFadeIndex() const noFadeIndex = !isNumber(fadeIndex) diff --git a/packages/embla-carousel/src/__tests__/loop-ltr.test.ts b/packages/embla-carousel/src/__tests__/loop-ltr.test.ts index 4f9cfcd67..1e90d4777 100644 --- a/packages/embla-carousel/src/__tests__/loop-ltr.test.ts +++ b/packages/embla-carousel/src/__tests__/loop-ltr.test.ts @@ -13,6 +13,7 @@ export const scrollToLocationInstant = ( engine.target.set(location) engine.scrollBody.useDuration(0) engine.animation.update() + engine.animation.render(1) } describe('➡️ Loop - Horizontal LTR', () => { diff --git a/packages/embla-carousel/src/__tests__/scrollBounds-ltr.test.ts b/packages/embla-carousel/src/__tests__/scrollBounds-ltr.test.ts index 18d3cd4e5..10ee47c09 100644 --- a/packages/embla-carousel/src/__tests__/scrollBounds-ltr.test.ts +++ b/packages/embla-carousel/src/__tests__/scrollBounds-ltr.test.ts @@ -7,6 +7,7 @@ export const setLocationOutOfBounds = ( engine: EngineType, outOfBoundsLocation: number ): void => { + engine.offsetLocation.set(outOfBoundsLocation) engine.location.set(outOfBoundsLocation) engine.target.set(outOfBoundsLocation) } @@ -21,7 +22,7 @@ describe('➡️ ScrollBounds - Horizontal LTR', () => { emblaApi.on('settle', settleCallback) setLocationOutOfBounds(engine, engine.limit.max + 1) - engine.animation.update() + engine.animation.render(1) expect(settleCallback).toHaveBeenCalledTimes(0) }) @@ -46,7 +47,7 @@ describe('➡️ ScrollBounds - Horizontal LTR', () => { emblaApi.on('settle', settleCallback) setLocationOutOfBounds(engine, engine.limit.min - 1) - engine.animation.update() + engine.animation.render(1) expect(settleCallback).toHaveBeenCalledTimes(0) }) diff --git a/packages/embla-carousel/src/__tests__/scrollBounds-rtl.test.ts b/packages/embla-carousel/src/__tests__/scrollBounds-rtl.test.ts index 4dcdd1b0b..f268c77eb 100644 --- a/packages/embla-carousel/src/__tests__/scrollBounds-rtl.test.ts +++ b/packages/embla-carousel/src/__tests__/scrollBounds-rtl.test.ts @@ -7,6 +7,7 @@ export const setLocationOutOfBounds = ( engine: EngineType, outOfBoundsLocation: number ): void => { + engine.offsetLocation.set(outOfBoundsLocation) engine.location.set(outOfBoundsLocation) engine.target.set(outOfBoundsLocation) } @@ -23,7 +24,7 @@ describe('➡️ ScrollBounds - Horizontal RTL', () => { emblaApi.on('settle', settleCallback) setLocationOutOfBounds(engine, engine.limit.max + 1) - engine.animation.update() + engine.animation.render(1) expect(settleCallback).toHaveBeenCalledTimes(0) }) @@ -52,7 +53,7 @@ describe('➡️ ScrollBounds - Horizontal RTL', () => { emblaApi.on('settle', settleCallback) setLocationOutOfBounds(engine, engine.limit.min - 1) - engine.animation.update() + engine.animation.render(1) expect(settleCallback).toHaveBeenCalledTimes(0) }) diff --git a/packages/embla-carousel/src/__tests__/scrollBounds-vertical.test.ts b/packages/embla-carousel/src/__tests__/scrollBounds-vertical.test.ts index 05ac692d1..1daddfaa0 100644 --- a/packages/embla-carousel/src/__tests__/scrollBounds-vertical.test.ts +++ b/packages/embla-carousel/src/__tests__/scrollBounds-vertical.test.ts @@ -7,6 +7,7 @@ export const setLocationOutOfBounds = ( engine: EngineType, outOfBoundsLocation: number ): void => { + engine.offsetLocation.set(outOfBoundsLocation) engine.location.set(outOfBoundsLocation) engine.target.set(outOfBoundsLocation) } @@ -23,7 +24,7 @@ describe('➡️ ScrollBounds - Vertical', () => { emblaApi.on('settle', settleCallback) setLocationOutOfBounds(engine, engine.limit.max + 1) - engine.animation.update() + engine.animation.render(1) expect(settleCallback).toHaveBeenCalledTimes(0) }) @@ -52,7 +53,7 @@ describe('➡️ ScrollBounds - Vertical', () => { emblaApi.on('settle', settleCallback) setLocationOutOfBounds(engine, engine.limit.min - 1) - engine.animation.update() + engine.animation.render(1) expect(settleCallback).toHaveBeenCalledTimes(0) }) diff --git a/packages/embla-carousel/src/components/Animations.ts b/packages/embla-carousel/src/components/Animations.ts index 5097f7c54..7980a2ca1 100644 --- a/packages/embla-carousel/src/components/Animations.ts +++ b/packages/embla-carousel/src/components/Animations.ts @@ -2,7 +2,14 @@ import { EngineType } from './Engine' import { EventStore } from './EventStore' import { WindowType } from './utils' -export type AnimationsUpdateType = (engine: EngineType) => void +export type AnimationsUpdateType = ( + engine: EngineType, + timeStep: number +) => void +export type AnimationsRenderType = ( + engine: EngineType, + lagOffset: number +) => void export type AnimationsType = { init: () => void @@ -10,18 +17,20 @@ export type AnimationsType = { start: () => void stop: () => void update: () => void + render: (lagOffset: number) => void } export function Animations( ownerDocument: Document, ownerWindow: WindowType, - update: AnimationsType['update'] + update: (timeStep: number) => void, + render: (lagOffset: number) => void ): AnimationsType { const documentVisibleHandler = EventStore() const timeStep = 1000 / 60 let lastTimeStamp: number | null = null - let animationFrame = 0 let lag = 0 + let animationFrame = 0 function init(): void { documentVisibleHandler.add(ownerDocument, 'visibilitychange', () => { @@ -38,15 +47,18 @@ export function Animations( if (!animationFrame) return if (!lastTimeStamp) lastTimeStamp = timeStamp - const timeElapsed = timeStamp - lastTimeStamp + const elapsed = timeStamp - lastTimeStamp lastTimeStamp = timeStamp - lag += timeElapsed + lag += elapsed while (lag >= timeStep) { - update() + update(timeStep) lag -= timeStep } + const lagOffset = lag / timeStep + render(lagOffset) + if (animationFrame) ownerWindow.requestAnimationFrame(animate) } @@ -73,7 +85,8 @@ export function Animations( destroy, start, stop, - update + update: () => update(timeStep), + render } return self } diff --git a/packages/embla-carousel/src/components/Engine.ts b/packages/embla-carousel/src/components/Engine.ts index aeff9c682..b82991618 100644 --- a/packages/embla-carousel/src/components/Engine.ts +++ b/packages/embla-carousel/src/components/Engine.ts @@ -1,5 +1,10 @@ import { Alignment } from './Alignment' -import { Animations, AnimationsType, AnimationsUpdateType } from './Animations' +import { + Animations, + AnimationsType, + AnimationsUpdateType, + AnimationsRenderType +} from './Animations' import { Axis, AxisType } from './Axis' import { Counter, CounterType } from './Counter' import { DragHandler, DragHandlerType } from './DragHandler' @@ -44,6 +49,7 @@ export type EngineType = { indexPrevious: CounterType limit: LimitType location: Vector1DType + offsetLocation: Vector1DType options: OptionsType percentOfView: PercentOfViewType scrollBody: ScrollBodyType @@ -149,21 +155,30 @@ export function Engine( const slideIndexes = arrayKeys(slides) // Animation - const update: AnimationsUpdateType = ({ - dragHandler, - eventHandler, - scrollBody, - scrollBounds, - scrollLooper, - slideLooper, - translate, - location, - animation, - options: { loop } - }) => { + const update: AnimationsUpdateType = ( + { dragHandler, scrollBody, scrollBounds, options: { loop } }, + timeStep + ) => { if (!loop) scrollBounds.constrain(dragHandler.pointerDown()) - scrollBody.seek() + scrollBody.seek(timeStep) + } + const render: AnimationsRenderType = ( + { + scrollBody, + translate, + location, + offsetLocation, + scrollLooper, + slideLooper, + dragHandler, + animation, + eventHandler, + scrollBounds, + options: { loop } + }, + lagOffset + ) => { const shouldSettle = scrollBody.settled() const withinBounds = !scrollBounds.shouldConstrain() const hasSettled = loop ? shouldSettle : shouldSettle && withinBounds @@ -174,22 +189,40 @@ export function Engine( } if (!hasSettled) eventHandler.emit('scroll') + const interpolatedLocation = + location.get() * lagOffset + prevLocation.get() * (1 - lagOffset) + + offsetLocation.set(interpolatedLocation) + if (loop) { scrollLooper.loop(scrollBody.direction()) slideLooper.loop() } - translate.to(location.get()) + translate.to(offsetLocation.get()) } - - const animation = Animations(ownerDocument, ownerWindow, () => update(engine)) + const animation = Animations( + ownerDocument, + ownerWindow, + (timeStep) => update(engine, timeStep), + (lagOffset: number) => render(engine, lagOffset) + ) // Shared const friction = 0.68 const startLocation = scrollSnaps[index.get()] const location = Vector1D(startLocation) + const prevLocation = Vector1D(startLocation) + const offsetLocation = Vector1D(startLocation) const target = Vector1D(startLocation) - const scrollBody = ScrollBody(location, target, duration, friction) + const scrollBody = ScrollBody( + location, + offsetLocation, + prevLocation, + target, + duration, + friction + ) const scrollTarget = ScrollTarget( loop, scrollSnaps, @@ -201,6 +234,7 @@ export function Engine( animation, index, indexPrevious, + scrollBody, scrollTarget, target, eventHandler @@ -267,6 +301,7 @@ export function Engine( indexPrevious, limit, location, + offsetLocation, options, resizeHandler: ResizeHandler( container, @@ -280,13 +315,15 @@ export function Engine( scrollBody, scrollBounds: ScrollBounds( limit, - location, + offsetLocation, target, scrollBody, percentOfView ), - scrollLooper: ScrollLooper(contentSize, limit, location, [ + scrollLooper: ScrollLooper(contentSize, limit, offsetLocation, [ location, + offsetLocation, + prevLocation, target ]), scrollProgress, @@ -302,7 +339,7 @@ export function Engine( slideSizesWithGaps, snaps, scrollSnaps, - location, + offsetLocation, slides ), slideFocus, diff --git a/packages/embla-carousel/src/components/ScrollBody.ts b/packages/embla-carousel/src/components/ScrollBody.ts index d14d23a5b..885b8bda5 100644 --- a/packages/embla-carousel/src/components/ScrollBody.ts +++ b/packages/embla-carousel/src/components/ScrollBody.ts @@ -5,7 +5,7 @@ export type ScrollBodyType = { direction: () => number duration: () => number velocity: () => number - seek: () => ScrollBodyType + seek: (timeStep: number) => ScrollBodyType settled: () => boolean useBaseFriction: () => ScrollBodyType useBaseDuration: () => ScrollBodyType @@ -15,6 +15,8 @@ export type ScrollBodyType = { export function ScrollBody( location: Vector1DType, + offsetLocation: Vector1DType, + prevLocation: Vector1DType, target: Vector1DType, baseDuration: number, baseFriction: number @@ -26,21 +28,26 @@ export function ScrollBody( let rawLocation = location.get() let rawLocationPrevious = 0 - function seek(): ScrollBodyType { + function seek(timeStep: number): ScrollBodyType { + const fixedDeltaTimeSeconds = timeStep / 1000 + const duration = scrollDuration * 0.016 const diff = target.get() - location.get() const isInstant = !scrollDuration let directionDiff = 0 if (isInstant) { bodyVelocity = 0 + prevLocation.set(target) location.set(target) directionDiff = diff } else { - bodyVelocity += diff / scrollDuration + prevLocation.set(location) + + bodyVelocity += diff / duration bodyVelocity *= scrollFriction rawLocation += bodyVelocity - location.add(bodyVelocity) + location.add(bodyVelocity * fixedDeltaTimeSeconds) directionDiff = rawLocation - rawLocationPrevious } @@ -51,7 +58,7 @@ export function ScrollBody( } function settled(): boolean { - const diff = target.get() - location.get() + const diff = target.get() - offsetLocation.get() return mathAbs(diff) < 0.001 } diff --git a/packages/embla-carousel/src/components/ScrollTo.ts b/packages/embla-carousel/src/components/ScrollTo.ts index 0f03e801d..bdd6e406e 100644 --- a/packages/embla-carousel/src/components/ScrollTo.ts +++ b/packages/embla-carousel/src/components/ScrollTo.ts @@ -1,6 +1,7 @@ import { AnimationsType } from './Animations' import { CounterType } from './Counter' import { EventHandlerType } from './EventHandler' +import { ScrollBodyType } from './ScrollBody' import { ScrollTargetType, TargetType } from './ScrollTarget' import { Vector1DType } from './Vector1d' @@ -13,6 +14,7 @@ export function ScrollTo( animation: AnimationsType, indexCurrent: CounterType, indexPrevious: CounterType, + scrollBody: ScrollBodyType, scrollTarget: ScrollTargetType, targetVector: Vector1DType, eventHandler: EventHandlerType @@ -23,7 +25,15 @@ export function ScrollTo( targetVector.add(distanceDiff) - if (distanceDiff) animation.start() + if (distanceDiff) { + if (scrollBody.duration()) { + animation.start() + } else { + animation.update() + animation.render(1) + animation.update() + } + } if (indexDiff) { indexPrevious.set(indexCurrent.get()) diff --git a/playgrounds/embla-carousel-playground-vanilla/src/main.ts b/playgrounds/embla-carousel-playground-vanilla/src/main.ts index fd51359d1..a611f14ec 100644 --- a/playgrounds/embla-carousel-playground-vanilla/src/main.ts +++ b/playgrounds/embla-carousel-playground-vanilla/src/main.ts @@ -24,7 +24,7 @@ import './main.css' const injectBaseStyles = (): void => { const styleElement = document.createElement('style') const carouselStyles = examplesCarouselDefaultStyles( - '100%', + '70%', '1rem', 'x', styledComponentsStylesToString(