diff --git a/package.json b/package.json
index ee087c506..6ddfe134d 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
"packages/embla-carousel-auto-scroll",
"packages/embla-carousel-auto-height",
"packages/embla-carousel-class-names",
+ "packages/embla-carousel-fade",
"packages/embla-carousel-reactive-utils",
"playgrounds/embla-carousel-playground-vanilla",
"playgrounds/embla-carousel-playground-react",
diff --git a/packages/embla-carousel-docs/src/components/Examples/examplesCarouselStyles.ts b/packages/embla-carousel-docs/src/components/Examples/examplesCarouselStyles.ts
index b889add62..af2c6d19b 100644
--- a/packages/embla-carousel-docs/src/components/Examples/examplesCarouselStyles.ts
+++ b/packages/embla-carousel-docs/src/components/Examples/examplesCarouselStyles.ts
@@ -332,7 +332,7 @@ export const THUMBS_STYLES = css`
}
`
-const PROGRESS_STYLES = css`
+export const PROGRESS_STYLES = css`
.embla__progress {
${CAROUSEL_SLIDE_RADIUS_STYLES};
${CAROUSEL_BORDER_SHADOW_STYLES};
diff --git a/packages/embla-carousel-fade/.eslintignore b/packages/embla-carousel-fade/.eslintignore
new file mode 100644
index 000000000..f5efc98ab
--- /dev/null
+++ b/packages/embla-carousel-fade/.eslintignore
@@ -0,0 +1,5 @@
+docs
+package.json
+package-lock.json
+yarn.lock
+node_modules
diff --git a/packages/embla-carousel-fade/.eslintrc.js b/packages/embla-carousel-fade/.eslintrc.js
new file mode 100644
index 000000000..b17ac0422
--- /dev/null
+++ b/packages/embla-carousel-fade/.eslintrc.js
@@ -0,0 +1,28 @@
+module.exports = {
+ parser: '@typescript-eslint/parser',
+ parserOptions: {
+ ecmaVersion: 2018,
+ sourceType: 'module'
+ },
+ extends: [
+ 'eslint:recommended',
+ 'plugin:prettier/recommended',
+ 'plugin:@typescript-eslint/eslint-recommended',
+ 'plugin:@typescript-eslint/recommended'
+ ],
+ rules: {
+ 'no-debugger': 2,
+ 'no-console': 2,
+ '@typescript-eslint/no-inferrable-types': 'off',
+ '@typescript-eslint/no-explicit-any': 'off',
+ '@typescript-eslint/ban-types': [
+ 'error',
+ {
+ types: {
+ '{}': false
+ },
+ extendDefaults: true
+ }
+ ]
+ }
+}
diff --git a/packages/embla-carousel-fade/.prettierrc.js b/packages/embla-carousel-fade/.prettierrc.js
new file mode 100644
index 000000000..5100fc98a
--- /dev/null
+++ b/packages/embla-carousel-fade/.prettierrc.js
@@ -0,0 +1 @@
+module.exports = require('../../.prettierrc.js')
diff --git a/packages/embla-carousel-fade/README.md b/packages/embla-carousel-fade/README.md
new file mode 100644
index 000000000..7155d61e3
--- /dev/null
+++ b/packages/embla-carousel-fade/README.md
@@ -0,0 +1,209 @@
+
+
+ + +
+ ++ + + + + + +
+ + ++ Embla Carousel is a bare bones carousel library with great fluid motion and awesome swipe precision. It's library agnostic, dependency free and 100% open source. +
+ +
+
+ Examples
+
+
+
+ Generator
+
+
+
+ Installation
+
+
+ Thank you to all contributors for making Embla Carousel awesome! Contributions are welcome. +
++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ gunnarx2 - React wrapper useEmblaCarousel.
+
+
+
+ LiamMartens - Solid wrapper createEmblaCarousel.
+
+
+
+ donaldxdonald, zip-fa, JeanMeche - Angular wrapper EmblaCarouselDirective.
+
+
+
+ xiel - Plugin Embla Carousel Wheel Gestures.
+
+
+
+ zaaakher - Contributing guidelines.
+
+
+ Embla is MIT licensed 💖.
+ Embla Carousel - Copyright © 2019-present.
+ Package created by David Jerleke.
+
+ · · · +
+ ++ Thanks BrowserStack. +
+ ++ + + +
diff --git a/packages/embla-carousel-fade/jest.config.js b/packages/embla-carousel-fade/jest.config.js new file mode 100644 index 000000000..04a0181f9 --- /dev/null +++ b/packages/embla-carousel-fade/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + transform: { + '^.+\\.(t|j)sx?$': 'ts-jest' + }, + testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$', + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], + testEnvironment: 'jsdom' +} diff --git a/packages/embla-carousel-fade/package.json b/packages/embla-carousel-fade/package.json new file mode 100644 index 000000000..165c4dc02 --- /dev/null +++ b/packages/embla-carousel-fade/package.json @@ -0,0 +1,77 @@ +{ + "name": "embla-carousel-fade", + "version": "8.0.4", + "author": "David Jerleke", + "description": "A fade plugin for Embla Carousel", + "repository": { + "type": "git", + "url": "git+https://github.com/davidjerleke/embla-carousel" + }, + "bugs": { + "url": "https://github.com/davidjerleke/embla-carousel/issues" + }, + "homepage": "https://www.embla-carousel.com", + "license": "MIT", + "keywords": [ + "slider", + "carousel", + "slideshow", + "gallery", + "lightweight", + "touch", + "javascript", + "typescript", + "react", + "vue", + "svelte", + "solid" + ], + "main": "embla-carousel-fade.umd.js", + "unpkg": "embla-carousel-fade.umd.js", + "module": "./esm/embla-carousel-fade.esm.js", + "types": "index.d.ts", + "sideEffects": false, + "files": [ + "embla-carousel-fade*", + "components/**/*", + "index.d.ts", + "esm/**/*", + "cjs/**/*" + ], + "scripts": { + "test": "echo \"Info: no tests specified\" && exit 0", + "build": "rollup --bundleConfigAsCjs -c", + "start": "rollup --bundleConfigAsCjs -c --watch --environment BUILD:development", + "eslint:report": "eslint \"src/**/*.{js,tsx,ts}\"" + }, + "devDependencies": { + "@types/jest": "^29.5.6", + "@typescript-eslint/eslint-plugin": "^6.9.0", + "@typescript-eslint/parser": "^6.9.0", + "eslint": "^8.52.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^4.0.0", + "jest": "^29.5.0", + "jest-environment-jsdom": "^29.5.0", + "prettier": "2.8.8", + "rollup": "^4.1.5", + "ts-jest": "^29.1.1", + "typescript": "^5.2.2" + }, + "peerDependencies": { + "embla-carousel": "8.0.2" + }, + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "types": "./esm/index.d.ts", + "default": "./esm/embla-carousel-fade.esm.js" + }, + "require": { + "types": "./cjs/index.d.ts", + "default": "./cjs/embla-carousel-fade.cjs.js" + } + } + } +} diff --git a/packages/embla-carousel-fade/rollup.config.js b/packages/embla-carousel-fade/rollup.config.js new file mode 100644 index 000000000..b9fda5710 --- /dev/null +++ b/packages/embla-carousel-fade/rollup.config.js @@ -0,0 +1,53 @@ +import packageJson from './package.json' +import { + FOLDERS, + CONFIG_BABEL, + CONFIG_TYPESCRIPT, + CONFIG_GLOBALS, + babel, + typescript, + resolve, + terser, + createBuildPath, + kebabToPascalCase, + createNodeNextSupport +} from '../../rollup.config' + +export default [ + { + input: 'src/index.ts', + output: [ + { + file: createBuildPath(packageJson, FOLDERS.CJS), + format: FOLDERS.CJS, + globals: CONFIG_GLOBALS, + strict: true, + sourcemap: true, + exports: 'auto' + }, + { + file: createBuildPath(packageJson, FOLDERS.ESM), + format: FOLDERS.ESM, + globals: CONFIG_GLOBALS, + strict: true, + sourcemap: true + }, + { + file: createBuildPath(packageJson, FOLDERS.UMD), + format: FOLDERS.UMD, + globals: CONFIG_GLOBALS, + strict: true, + sourcemap: false, + name: kebabToPascalCase(packageJson.name), + plugins: [terser()] + } + ], + external: Object.keys(CONFIG_GLOBALS), + plugins: [ + resolve(), + typescript(CONFIG_TYPESCRIPT), + babel(CONFIG_BABEL), + createNodeNextSupport() + ] + } +] diff --git a/packages/embla-carousel-fade/src/components/Fade.ts b/packages/embla-carousel-fade/src/components/Fade.ts new file mode 100644 index 000000000..d99cd907b --- /dev/null +++ b/packages/embla-carousel-fade/src/components/Fade.ts @@ -0,0 +1,254 @@ +import { OptionsType } from './Options' +import { CreatePluginType } from 'embla-carousel/components/Plugins' +import { EmblaCarouselType } from 'embla-carousel' +import { isNumber, clampNumber } from './utils' +import { ScrollBodyType } from 'embla-carousel/components/ScrollBody' + +declare module 'embla-carousel/components/Plugins' { + interface EmblaPluginsType { + fade?: FadeType + } +} + +export type FadeType = CreatePluginType<{}, OptionsType> + +export type FadeOptionsType = FadeType['options'] + +function Fade(userOptions: FadeOptionsType = {}): FadeType { + const fullOpacity = 1 + const noOpacity = 0 + const fadeFriction = 0.68 + + let emblaApi: EmblaCarouselType + let opacities: number[] = [] + let fadeToNextDistance: number + let distanceFromPointerDown = 0 + let fadeVelocity = 0 + let progress = 0 + let defaultSettledBehaviour: ScrollBodyType['settled'] + let defaultProgressBehaviour: EmblaCarouselType['scrollProgress'] + + function init(emblaApiInstance: EmblaCarouselType): void { + emblaApi = emblaApiInstance + + const selectedSnap = emblaApi.selectedScrollSnap() + const { scrollBody, containerRect, axis } = emblaApi.internalEngine() + const containerSize = axis.measureSize(containerRect) + fadeToNextDistance = clampNumber(containerSize * 0.75, 200, 500) + + opacities = emblaApi + .scrollSnapList() + .map((_, index) => (index === selectedSnap ? fullOpacity : noOpacity)) + + disableScroll() + fadeToSelectedSnapInstantly() + + defaultSettledBehaviour = scrollBody.settled + defaultProgressBehaviour = emblaApi.scrollProgress + + scrollBody.settled = settled + emblaApi.scrollProgress = scrollProgress + + emblaApi + .on('select', select) + .on('slideFocus', fadeToSelectedSnapInstantly) + .on('pointerDown', pointerDown) + } + + function destroy(): void { + const { scrollBody } = emblaApi.internalEngine() + scrollBody.settled = defaultSettledBehaviour + emblaApi.scrollProgress = defaultProgressBehaviour + + emblaApi + .off('select', select) + .off('slideFocus', fadeToSelectedSnapInstantly) + .off('pointerDown', pointerDown) + + emblaApi.slideNodes().forEach((slideNode) => { + const slideStyle = slideNode.style + slideStyle.opacity = '' + slideStyle.transform = '' + slideStyle.pointerEvents = '' + if (!slideNode.getAttribute('style')) slideNode.removeAttribute('style') + }) + } + + function fadeToSelectedSnapInstantly(): void { + const selectedSnap = emblaApi.selectedScrollSnap() + setOpacities(selectedSnap, fullOpacity) + } + + function pointerDown(): void { + distanceFromPointerDown = 0 + fadeVelocity = 0 + } + + function select(): void { + const duration = emblaApi.internalEngine().scrollBody.duration() + fadeVelocity = duration ? 0 : fullOpacity + if (!duration) fadeToSelectedSnapInstantly() + } + + function getSlideTransform(position: number): string { + const { axis } = emblaApi.internalEngine() + const translateAxis = axis.scroll.toUpperCase() + return `translate${translateAxis}(${axis.direction(position)}px)` + } + + function disableScroll(): void { + const { translate, slideLooper } = emblaApi.internalEngine() + + translate.clear() + translate.toggleActive(false) + + slideLooper.loopPoints.forEach(({ translate }) => { + translate.clear() + translate.toggleActive(false) + }) + } + + function lockExcessiveScroll(fadeIndex: number | null): void { + const { scrollSnaps, location, target } = emblaApi.internalEngine() + if (!isNumber(fadeIndex) || opacities[fadeIndex] < 0.5) return + + location.set(scrollSnaps[fadeIndex]) + target.set(location) + } + + function setOpacities(fadeIndex: number, velocity: number): void { + const scrollSnaps = emblaApi.scrollSnapList() + + scrollSnaps.forEach((_, index) => { + const absVelocity = Math.abs(velocity) + const currentOpacity = opacities[index] + const isFadeIndex = index === fadeIndex + + const nextOpacity = isFadeIndex + ? currentOpacity + absVelocity + : currentOpacity - absVelocity + + const clampedOpacity = clampNumber(nextOpacity, noOpacity, fullOpacity) + opacities[index] = clampedOpacity + + if (isFadeIndex) setProgress(fadeIndex, clampedOpacity) + setOpacity(index) + }) + } + + function setOpacity(index: number): void { + const slidesInSnap = emblaApi.internalEngine().slideRegistry[index] + const { scrollSnaps, containerRect } = emblaApi.internalEngine() + const opacity = opacities[index] + + slidesInSnap.forEach((slideIndex) => { + const slideStyle = emblaApi.slideNodes()[slideIndex].style + const roundedOpacity = parseFloat(opacity.toFixed(2)) + const hasOpacity = roundedOpacity > noOpacity + const position = hasOpacity ? scrollSnaps[index] : containerRect.width + 2 + const transform = getSlideTransform(position) + + if (hasOpacity) slideStyle.transform = transform + + slideStyle.opacity = roundedOpacity.toString() + slideStyle.pointerEvents = opacity > 0.5 ? 'auto' : 'none' + + if (!hasOpacity) slideStyle.transform = transform + }) + } + + function setProgress(fadeIndex: number, opacity: number): void { + const { index, dragHandler, scrollSnaps } = emblaApi.internalEngine() + const pointerDown = dragHandler.pointerDown() + const snapFraction = 1 / (scrollSnaps.length - 1) + + let indexA = fadeIndex + let indexB = pointerDown + ? emblaApi.selectedScrollSnap() + : emblaApi.previousScrollSnap() + + if (pointerDown && indexA === indexB) { + const reverseSign = Math.sign(distanceFromPointerDown) * -1 + indexA = indexB + indexB = index.clone().set(indexB).add(reverseSign).get() + } + + const currentPosition = indexB * snapFraction + const diffPosition = (indexA - indexB) * snapFraction + progress = currentPosition + diffPosition * opacity + } + + function getFadeIndex(): number | null { + const { dragHandler, index } = emblaApi.internalEngine() + const selectedSnap = emblaApi.selectedScrollSnap() + + if (!dragHandler.pointerDown()) return selectedSnap + + const directionSign = Math.sign(dragHandler.velocity()) + const distanceSign = Math.sign(distanceFromPointerDown) + const nextSnap = index + .clone() + .set(selectedSnap) + .add(directionSign * -1) + .get() + + if (!directionSign || !distanceSign) return null + return distanceSign === directionSign ? nextSnap : selectedSnap + } + + const fade = (emblaApi: EmblaCarouselType): void => { + const { dragHandler, scrollBody } = emblaApi.internalEngine() + const pointerDown = dragHandler.pointerDown() + const dragVelocity = dragHandler.velocity() + const duration = scrollBody.duration() + const fadeIndex = getFadeIndex() + const noFadeIndex = !isNumber(fadeIndex) + + if (pointerDown) { + if (!dragVelocity) return + + distanceFromPointerDown += dragVelocity + fadeVelocity = Math.abs(dragVelocity / fadeToNextDistance) + lockExcessiveScroll(fadeIndex) + } + + if (!pointerDown) { + if (!duration || noFadeIndex) return + + fadeVelocity += (fullOpacity - opacities[fadeIndex]) / duration + fadeVelocity *= fadeFriction + } + + if (noFadeIndex) return + setOpacities(fadeIndex, fadeVelocity) + } + + function settled(): boolean { + const { target, location } = emblaApi.internalEngine() + const diffToTarget = target.get() - location.get() + const notReachedTarget = Math.abs(diffToTarget) >= 1 + const fadeIndex = getFadeIndex() + const noFadeIndex = !isNumber(fadeIndex) + + fade(emblaApi) + + if (noFadeIndex || notReachedTarget) return false + return opacities[fadeIndex] > 0.999 + } + + function scrollProgress(): number { + return progress + } + + const self: FadeType = { + name: 'fade', + options: userOptions, + init, + destroy + } + return self +} + +Fade.globalOptions =