diff --git a/packages/embla-carousel/src/__tests__/loop-ltr.test.ts b/packages/embla-carousel/src/__tests__/loop-ltr.test.ts index 1e90d4777..3b0a771cf 100644 --- a/packages/embla-carousel/src/__tests__/loop-ltr.test.ts +++ b/packages/embla-carousel/src/__tests__/loop-ltr.test.ts @@ -50,7 +50,7 @@ describe('➡️ Loop - Horizontal LTR', () => { ) expect(emblaApi.containerNode().style.transform).toBe( - 'translate3d(0.09000000000000001px,0px,0px)' + 'translate3d(0.09px,0px,0px)' ) }) }) @@ -274,7 +274,7 @@ describe('➡️ Loop - Horizontal LTR', () => { ) expect(emblaApi.containerNode().style.transform).toBe( - 'translate3d(-1209.8899999999999px,0px,0px)' + 'translate3d(-1209.89px,0px,0px)' ) }) @@ -285,7 +285,7 @@ describe('➡️ Loop - Horizontal LTR', () => { ) expect(emblaApi.containerNode().style.transform).toBe( - 'translate3d(450.09000000000003px,0px,0px)' + 'translate3d(450.09px,0px,0px)' ) }) }) @@ -940,7 +940,7 @@ describe('➡️ Loop - Horizontal LTR', () => { ) expect(emblaApi.containerNode().style.transform).toBe( - 'translate3d(-1419.8899999999999px,0px,0px)' + 'translate3d(-1419.89px,0px,0px)' ) }) @@ -951,7 +951,7 @@ describe('➡️ Loop - Horizontal LTR', () => { ) expect(emblaApi.containerNode().style.transform).toBe( - 'translate3d(440.09000000000003px,0px,0px)' + 'translate3d(440.09px,0px,0px)' ) }) }) diff --git a/packages/embla-carousel/src/__tests__/loop-rtl.test.ts b/packages/embla-carousel/src/__tests__/loop-rtl.test.ts index e58eae181..62c30fdf2 100644 --- a/packages/embla-carousel/src/__tests__/loop-rtl.test.ts +++ b/packages/embla-carousel/src/__tests__/loop-rtl.test.ts @@ -41,7 +41,7 @@ describe('➡️ Loop - Horizontal RTL', () => { ) expect(emblaApi.containerNode().style.transform).toBe( - 'translate3d(-0.09000000000000001px,0px,0px)' + 'translate3d(-0.09px,0px,0px)' ) }) }) @@ -267,7 +267,7 @@ describe('➡️ Loop - Horizontal RTL', () => { ) expect(emblaApi.containerNode().style.transform).toBe( - 'translate3d(1209.8899999999999px,0px,0px)' + 'translate3d(1209.89px,0px,0px)' ) }) @@ -278,7 +278,7 @@ describe('➡️ Loop - Horizontal RTL', () => { ) expect(emblaApi.containerNode().style.transform).toBe( - 'translate3d(-450.09000000000003px,0px,0px)' + 'translate3d(-450.09px,0px,0px)' ) }) }) @@ -939,7 +939,7 @@ describe('➡️ Loop - Horizontal RTL', () => { ) expect(emblaApi.containerNode().style.transform).toBe( - 'translate3d(1419.8899999999999px,0px,0px)' + 'translate3d(1419.89px,0px,0px)' ) }) @@ -950,7 +950,7 @@ describe('➡️ Loop - Horizontal RTL', () => { ) expect(emblaApi.containerNode().style.transform).toBe( - 'translate3d(-440.09000000000003px,0px,0px)' + 'translate3d(-440.09px,0px,0px)' ) }) }) diff --git a/packages/embla-carousel/src/__tests__/loop-vertical.test.ts b/packages/embla-carousel/src/__tests__/loop-vertical.test.ts index d6c72b406..81e274f5a 100644 --- a/packages/embla-carousel/src/__tests__/loop-vertical.test.ts +++ b/packages/embla-carousel/src/__tests__/loop-vertical.test.ts @@ -41,7 +41,7 @@ describe('➡️ Loop - Vertical', () => { ) expect(emblaApi.containerNode().style.transform).toBe( - 'translate3d(0px,0.09000000000000001px,0px)' + 'translate3d(0px,0.09px,0px)' ) }) }) @@ -267,7 +267,7 @@ describe('➡️ Loop - Vertical', () => { ) expect(emblaApi.containerNode().style.transform).toBe( - 'translate3d(0px,-1209.8899999999999px,0px)' + 'translate3d(0px,-1209.89px,0px)' ) }) @@ -278,7 +278,7 @@ describe('➡️ Loop - Vertical', () => { ) expect(emblaApi.containerNode().style.transform).toBe( - 'translate3d(0px,450.09000000000003px,0px)' + 'translate3d(0px,450.09px,0px)' ) }) }) @@ -939,7 +939,7 @@ describe('➡️ Loop - Vertical', () => { ) expect(emblaApi.containerNode().style.transform).toBe( - 'translate3d(0px,-1419.8899999999999px,0px)' + 'translate3d(0px,-1419.89px,0px)' ) }) @@ -950,7 +950,7 @@ describe('➡️ Loop - Vertical', () => { ) expect(emblaApi.containerNode().style.transform).toBe( - 'translate3d(0px,440.09000000000003px,0px)' + 'translate3d(0px,440.09px,0px)' ) }) }) diff --git a/packages/embla-carousel/src/components/Animations.ts b/packages/embla-carousel/src/components/Animations.ts index 7980a2ca1..53a2451cf 100644 --- a/packages/embla-carousel/src/components/Animations.ts +++ b/packages/embla-carousel/src/components/Animations.ts @@ -2,10 +2,7 @@ import { EngineType } from './Engine' import { EventStore } from './EventStore' import { WindowType } from './utils' -export type AnimationsUpdateType = ( - engine: EngineType, - timeStep: number -) => void +export type AnimationsUpdateType = (engine: EngineType) => void export type AnimationsRenderType = ( engine: EngineType, lagOffset: number @@ -23,14 +20,14 @@ export type AnimationsType = { export function Animations( ownerDocument: Document, ownerWindow: WindowType, - update: (timeStep: number) => void, + update: () => void, render: (lagOffset: number) => void ): AnimationsType { const documentVisibleHandler = EventStore() - const timeStep = 1000 / 60 + const fixedTimeStep = 1000 / 60 let lastTimeStamp: number | null = null - let lag = 0 - let animationFrame = 0 + let accumulatedTime = 0 + let animationId = 0 function init(): void { documentVisibleHandler.add(ownerDocument, 'visibilitychange', () => { @@ -43,41 +40,57 @@ export function Animations( documentVisibleHandler.clear() } - function animate(timeStamp: DOMHighResTimeStamp): void { - if (!animationFrame) return - if (!lastTimeStamp) lastTimeStamp = timeStamp + function shouldUpdate(): boolean { + return accumulatedTime >= fixedTimeStep + } - const elapsed = timeStamp - lastTimeStamp - lastTimeStamp = timeStamp - lag += elapsed + function updateAndRemoveAccumulatedTime(): void { + update() + accumulatedTime -= fixedTimeStep + if (shouldUpdate()) updateAndRemoveAccumulatedTime() + } + + function renderWithAlpha(): void { + const alpha = accumulatedTime / fixedTimeStep + render(alpha) + } + + function animate(timeStamp: DOMHighResTimeStamp): void { + if (!animationId) return - while (lag >= timeStep) { - update(timeStep) - lag -= timeStep + if (!lastTimeStamp) { + lastTimeStamp = timeStamp + update() + renderWithAlpha() } - const lagOffset = lag / timeStep - render(lagOffset) + const timeElapsed = timeStamp - lastTimeStamp + lastTimeStamp = timeStamp + accumulatedTime += timeElapsed - if (animationFrame) ownerWindow.requestAnimationFrame(animate) + if (shouldUpdate()) updateAndRemoveAccumulatedTime() + renderWithAlpha() + + if (animationId) { + animationId = ownerWindow.requestAnimationFrame(animate) + } } function start(): void { - if (animationFrame) return - - animationFrame = ownerWindow.requestAnimationFrame(animate) + if (animationId) return + animationId = ownerWindow.requestAnimationFrame(animate) } function stop(): void { - ownerWindow.cancelAnimationFrame(animationFrame) + ownerWindow.cancelAnimationFrame(animationId) lastTimeStamp = null - lag = 0 - animationFrame = 0 + accumulatedTime = 0 + animationId = 0 } function reset(): void { lastTimeStamp = null - lag = 0 + accumulatedTime = 0 } const self: AnimationsType = { @@ -85,7 +98,7 @@ export function Animations( destroy, start, stop, - update: () => update(timeStep), + update, render } return self diff --git a/packages/embla-carousel/src/components/Engine.ts b/packages/embla-carousel/src/components/Engine.ts index c00e2d53f..705ca04ac 100644 --- a/packages/embla-carousel/src/components/Engine.ts +++ b/packages/embla-carousel/src/components/Engine.ts @@ -157,12 +157,14 @@ export function Engine( const slideIndexes = arrayKeys(slides) // Animation - const update: AnimationsUpdateType = ( - { dragHandler, scrollBody, scrollBounds, options: { loop } }, - timeStep - ) => { + const update: AnimationsUpdateType = ({ + dragHandler, + scrollBody, + scrollBounds, + options: { loop } + }) => { if (!loop) scrollBounds.constrain(dragHandler.pointerDown()) - scrollBody.seek(timeStep) + scrollBody.seek() } const render: AnimationsRenderType = ( @@ -171,6 +173,7 @@ export function Engine( translate, location, offsetLocation, + previousLocation, scrollLooper, slideLooper, dragHandler, @@ -179,7 +182,7 @@ export function Engine( scrollBounds, options: { loop } }, - lagOffset + alpha ) => { const shouldSettle = scrollBody.settled() const withinBounds = !scrollBounds.shouldConstrain() @@ -192,7 +195,7 @@ export function Engine( if (!hasSettled) eventHandler.emit('scroll') const interpolatedLocation = - location.get() * lagOffset + previousLocation.get() * (1 - lagOffset) + location.get() * alpha + previousLocation.get() * (1 - alpha) offsetLocation.set(interpolatedLocation) @@ -203,11 +206,12 @@ export function Engine( translate.to(offsetLocation.get()) } + const animation = Animations( ownerDocument, ownerWindow, - (timeStep) => update(engine, timeStep), - (lagOffset: number) => render(engine, lagOffset) + () => update(engine), + (alpha: number) => render(engine, alpha) ) // Shared diff --git a/packages/embla-carousel/src/components/ScrollBody.ts b/packages/embla-carousel/src/components/ScrollBody.ts index 2eb117ac6..fbed72d3a 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: (timeStep: number) => ScrollBodyType + seek: () => ScrollBodyType settled: () => boolean useBaseFriction: () => ScrollBodyType useBaseDuration: () => ScrollBodyType @@ -21,38 +21,36 @@ export function ScrollBody( baseDuration: number, baseFriction: number ): ScrollBodyType { - let bodyVelocity = 0 + let scrollVelocity = 0 let scrollDirection = 0 let scrollDuration = baseDuration let scrollFriction = baseFriction let rawLocation = location.get() let rawLocationPrevious = 0 - function seek(timeStep: number): ScrollBodyType { - const fixedDeltaTimeSeconds = timeStep / 1000 - const duration = scrollDuration * fixedDeltaTimeSeconds - const diff = target.get() - location.get() + function seek(): ScrollBodyType { + const displacement = target.get() - location.get() const isInstant = !scrollDuration - let directionDiff = 0 + let scrollDistance = 0 if (isInstant) { - bodyVelocity = 0 + scrollVelocity = 0 previousLocation.set(target) location.set(target) - directionDiff = diff + scrollDistance = displacement } else { previousLocation.set(location) - bodyVelocity += diff / duration - bodyVelocity *= scrollFriction - rawLocation += bodyVelocity - location.add(bodyVelocity * fixedDeltaTimeSeconds) + scrollVelocity += displacement / scrollDuration + scrollVelocity *= scrollFriction + rawLocation += scrollVelocity + location.add(scrollVelocity) - directionDiff = rawLocation - rawLocationPrevious + scrollDistance = rawLocation - rawLocationPrevious } - scrollDirection = mathSign(directionDiff) + scrollDirection = mathSign(scrollDistance) rawLocationPrevious = rawLocation return self } @@ -71,7 +69,7 @@ export function ScrollBody( } function velocity(): number { - return bodyVelocity + return scrollVelocity } function useBaseDuration(): ScrollBodyType { diff --git a/packages/embla-carousel/src/components/Translate.ts b/packages/embla-carousel/src/components/Translate.ts index 7191e8575..4ccda61b4 100644 --- a/packages/embla-carousel/src/components/Translate.ts +++ b/packages/embla-carousel/src/components/Translate.ts @@ -1,4 +1,5 @@ import { AxisType } from './Axis' +import { roundToTwoDecimals } from './utils' export type TranslateType = { clear: () => void @@ -12,6 +13,7 @@ export function Translate( ): TranslateType { const translate = axis.scroll === 'x' ? x : y const containerStyle = container.style + let previousTarget: number | null = null let disabled = false function x(n: number): string { @@ -24,7 +26,12 @@ export function Translate( function to(target: number): void { if (disabled) return - containerStyle.transform = translate(axis.direction(target)) + + const newTarget = roundToTwoDecimals(axis.direction(target)) + if (newTarget === previousTarget) return + + containerStyle.transform = translate(newTarget) + previousTarget = newTarget } function toggleActive(active: boolean): void { diff --git a/packages/embla-carousel/src/components/utils.ts b/packages/embla-carousel/src/components/utils.ts index 149db1ef9..42bc3e11c 100644 --- a/packages/embla-carousel/src/components/utils.ts +++ b/packages/embla-carousel/src/components/utils.ts @@ -37,6 +37,10 @@ export function factorAbs(valueB: number, valueA: number): number { return mathAbs(diff / valueB) } +export function roundToTwoDecimals(num: number): number { + return Math.round(num * 100) / 100 +} + export function arrayKeys(array: Type[]): number[] { return objectKeys(array).map(Number) }