Splides keep rewinding even with 'Rewind : False' and 'Type : Loop'
Checks
- [X] Not a duplicate.
- [X] Not a question, feature request, or anything other than a bug report directly related to Splide. Use Discussions for these topics: https://github.com/Splidejs/splide/discussions
Version
v4.1.4
Description
Heres a video showing my current splide with the rewind issue:
I have a looping slider that has this weird wrap around after it reaches the last splide. I can confirm its not even a 'Rewind : True' somehow being set on it because trying to configure the wrap around time using 'rewindSpeed : 2000' does not affect the rewind time. In addition i explicitly have "type: "loop" meaning rewind option should be ignored.
The first time i had it, I fixed it by setting 'autoHeight: true' and 'autoWidth: true' to my base config because I suspected Splide was having an issue figuring out the width of each slide. While it did fix the wrap around issue it caused the same stutter described in this issue. I then somehow regressed and the splides started wrapping around again.
Heres my current Splide config
<script type="module">
import Splide from 'https://cdn.jsdelivr.net/npm/@splidejs/[email protected]/+esm';
const splide = new Splide(".splide", {
type: "loop",
updateOnMove: true,
perPage: 3,
focus: "center",
perMove: 1,
start: 0,
drag: false,
easing: "cubic-bezier(.645, .045, .355, 1)",
gap: "3rem",
pagination: false,
padding: "2rem",
direction: "ltr",
fixedWidth: '420px',
speed: 1000,
mediaQuery: 'max',
breakpoints: {
991: {
perPage: 3,
fixedWidth: '420px',
autoHeight: true,
},
478: {
gap: "3rem",
padding: "4rem",
perPage: 3,
fixedWidth: '70vw',
autoHeight: true,
},
},
});
// Mount the Splide instance
splide.mount();
</script>
Heres my older splide config
<script type="module">
import Splide from 'https://cdn.jsdelivr.net/npm/@splidejs/[email protected]/+esm';
document.addEventListener('DOMContentLoaded', () => {
const splide = new Splide(".splide", {
type: "loop",
updateOnMove: true,
perPage: 3,
focus: "center",
perMove: 1,
start: 0,
drag: false,
easing: "cubic-bezier(.645, .045, .355, 1)",
gap: "3rem",
pagination: false,
padding: "2rem",
direction: "ltr",
autoWidth: true,
autoHeight: true,
speed: 1000,
mediaQuery: 'max',
breakpoints: {
991: {
perPage: 3,
autoWidth: true,
autoHeight: true,
},
478: {
gap: "3rem",
padding: "4rem",
perPage: 3,
width: "70vw",
},
},
});
// Mount the Splide instance
splide.mount();
});
</script>
Reproduction Link
Heres a js fiddle with a more complete setup of my old version. Its really a standard splide setup (besides the animated bg gradients)
Steps to Reproduce
Use my older splide config on a standard splide setup and you should get the weird stutter issue im describing On my new splide config it just rewinds when reaching the last splide.
Expected Behaviour
The splides are supposed to infinitely loop smoothly with no stutter or wrapping. I can confirm Splide is still auto generating clones at runtime meaning the 'type:loop' option is working partially.
Same here.
Just a simple config.
new Splide('.splide', { type: 'loop' });
It just rewinds instead of going foward with new clones.
did you happen to have this css payload on your page as well?
For closure i ended up just creating my own splide js alternative. All the bugs are gone and it keeps splides core funcionalities like swiping and dragging.
Video Link: https://streamable.com/finbhn
Heres a sample implementation of the library:
- CSS File (infinite-carousel.css)
/* Core carousel transforms for performance */
.splide__slide,
.splide__list,
.splide__track {
-webkit-transform: translateZ(0);
-webkit-backface-visibility: hidden;
transform: translateZ(0);
}
.splide-image {
transform: translateZ(0);
}
/* Pagination styling */
.splide__pagination__page.centered {
background: #feffff;
border: 0;
border-radius: 50%;
display: inline-block;
height: 8px;
margin: 3px;
opacity: 1;
padding: 0;
position: relative;
transition: transform 1s cubic-bezier(0.645, 0.045, 0.355, 1);
width: 8px;
}
.splide__pagination__page.centered.is-active,
.splide__pagination__page.centered.is-activating {
background: #e5693b;
transform: scale(1.4);
}
/* Slide animations */
.splide__slide.centered.is-active,
.splide__slide.centered.is-activating {
filter: brightness(100%) saturate(100%);
transform: scale(1.1);
}
/* Gradient background for carousel container */
.why-ais-scrolling-cards {
background-image: linear-gradient(
var(--gradient-start),
var(--gradient-end)
);
}
- Main Library (infinite-carousel.js)
/**
* InfiniteCarousel Library
* A feature-rich, infinite-scrolling carousel with color extraction,
* gradient animations, and autoplay capabilities.
*
* @version 1.0.0
* @requires GSAP (for gradient animations)
*/
(function (global) {
"use strict";
// ==================== DEFAULT CONFIGURATION ====================
const DEFAULT_CONFIG = {
// Core carousel settings
carousel: {
selector: ".splide.centered",
gap: 48,
padding: 32,
slideWidth: 420,
speed: 1000,
easing: "cubic-bezier(.645, .045, .355, 1)",
focusCenter: true,
mobileOffset: -17.5,
cloneCount: null, // Auto-calculated if null
},
// Autoplay settings
autoplay: {
enabled: true,
interval: 2000,
pauseOnHover: false,
},
// Color extraction settings
colors: {
enabled: true,
sampleSize: 100,
cacheColors: true,
},
// Dynamic text color settings
dynamicText: {
enabled: true,
updateInterval: 3000,
contrastThreshold: 7,
safariWhiteText: true, // Force white text in Safari
},
// Gradient animation settings
gradients: {
enabled: true,
duration: 0.9,
colorPairs: {
start: "vibrant", // or 'dominant', 'muted', etc.
end: "dvibrant", // dark vibrant
},
},
// Card background settings
cardBackgrounds: {
enabled: true,
batchSize: 4,
darkenWeight: 0.67, // 0-1, blend between black and vibrant
},
// Responsive settings
responsive: {
mobile: {
breakpoint: 478,
slideWidth: "70vw", // Can be number or percentage string
gap: 48,
padding: 64,
},
},
// Debug mode
debug: false,
};
// ==================== UTILITY FUNCTIONS ====================
const Utils = {
// Color conversion utilities
hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: null;
},
rgbToHex(r, g, b) {
const toHex = (x) => {
const hex = x.toString(16);
return hex.length === 1 ? "0" + hex : hex;
};
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
},
rgbToHsl(r, g, b) {
r /= 255;
g /= 255;
b /= 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h,
s,
l = (max + min) / 2;
if (max === min) {
h = s = 0;
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
return [h * 360, s * 100, l * 100];
},
parseRgb(rgbString) {
const match = rgbString.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
return match
? {
r: parseInt(match[1]),
g: parseInt(match[2]),
b: parseInt(match[3]),
}
: null;
},
getLuminance(r, g, b) {
return [r, g, b]
.map((c) => {
c = c / 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
})
.reduce(
(acc, val, idx) => acc + val * [0.2126, 0.7152, 0.0722][idx],
0
);
},
getContrastRatio(l1, l2) {
const lighter = Math.max(l1, l2);
const darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
},
interpolateColor(color1, color2, weight) {
const c1 = color1.match(/\w\w/g).map((hex) => parseInt(hex, 16));
const c2 = color2.match(/\w\w/g).map((hex) => parseInt(hex, 16));
const interpolated = c1.map((comp, index) =>
Math.round(comp * (1 - weight) + c2[index] * weight)
);
return `#${interpolated.map((comp) => comp.toString(16).padStart(2, "0")).join("")}`;
},
isSafari() {
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
},
log(message, ...args) {
if (this.config && this.config.debug) {
console.log(`[InfiniteCarousel] ${message}`, ...args);
}
},
warn(message, ...args) {
console.warn(`[InfiniteCarousel] ${message}`, ...args);
},
error(message, ...args) {
console.error(`[InfiniteCarousel] ${message}`, ...args);
},
};
// ==================== COLOR EXTRACTOR ====================
class ColorExtractor {
constructor(config) {
this.config = config;
this.cache = new Map();
}
async extractFromUrl(imageUrl) {
if (this.config.cacheColors && this.cache.has(imageUrl)) {
return this.cache.get(imageUrl);
}
try {
const imageData = await this._getImageData(imageUrl);
const analyzedColors = this._analyzeColors(imageData);
const colors = this._findBestColors(analyzedColors);
if (this.config.cacheColors) {
this.cache.set(imageUrl, colors);
}
return colors;
} catch (error) {
Utils.error("Color extraction failed:", error);
return this._getFallbackColors();
}
}
async _getImageData(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = "anonymous";
img.onerror = () => reject(new Error("Failed to load image"));
img.onload = () => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d", { willReadFrequently: true });
const scale = Math.min(1, this.config.sampleSize / img.width);
canvas.width = img.width * scale;
canvas.height = img.height * scale;
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
const imageData = ctx.getImageData(
0,
0,
canvas.width,
canvas.height
);
resolve(imageData.data);
};
img.src = url;
});
}
_analyzeColors(pixels) {
const colorMap = new Map();
const colorProperties = new Map();
for (let i = 0; i < pixels.length; i += 4) {
const r = pixels[i];
const g = pixels[i + 1];
const b = pixels[i + 2];
const hex = Utils.rgbToHex(r, g, b);
colorMap.set(hex, (colorMap.get(hex) || 0) + 1);
if (!colorProperties.has(hex)) {
const [h, s, l] = Utils.rgbToHsl(r, g, b);
colorProperties.set(hex, {
hex,
hsl: [h, s, l],
rgb: [r, g, b],
saturation: s,
luminance: l,
popularity: 0,
});
}
}
const totalPixels = pixels.length / 4;
for (const [hex, count] of colorMap.entries()) {
const props = colorProperties.get(hex);
props.popularity = count / totalPixels;
}
return Array.from(colorProperties.values());
}
_findBestColors(colors) {
colors.sort((a, b) => b.popularity - a.popularity);
const result = {
dominant: colors[0].hex,
vibrant: null,
muted: null,
lvibrant: null,
lmuted: null,
dvibrant: null,
dmuted: null,
ldominant: null,
ddominant: null,
};
const findColor = (
targetSaturation,
targetLuminance,
saturationRange = 20,
luminanceRange = 20
) => {
return (
colors.find((color) => {
const [, s, l] = color.hsl;
return (
Math.abs(s - targetSaturation) <= saturationRange &&
Math.abs(l - targetLuminance) <= luminanceRange
);
})?.hex || result.dominant
);
};
result.vibrant = findColor(90, 50);
result.lvibrant = findColor(90, 75);
result.dvibrant = findColor(90, 25);
result.muted = findColor(40, 50);
result.lmuted = findColor(40, 75);
result.dmuted = findColor(40, 25);
result.ldominant =
colors.find((color) => color.luminance >= 70)?.hex || result.dominant;
result.ddominant =
colors.find((color) => color.luminance <= 30)?.hex || result.dominant;
for (const key in result) {
if (!result[key]) result[key] = result.dominant;
}
return result;
}
_getFallbackColors() {
const fallback = "#808080";
return {
dominant: fallback,
vibrant: fallback,
muted: fallback,
lvibrant: fallback,
lmuted: fallback,
dvibrant: fallback,
dmuted: fallback,
ldominant: fallback,
ddominant: fallback,
};
}
clearCache() {
this.cache.clear();
}
}
// ==================== CORE CAROUSEL ====================
class InfiniteCarousel {
constructor(selector, config) {
this._isFullyInitialized = false;
this._isInitializing = false;
this.container = document.querySelector(selector);
if (!this.container) {
throw new Error(
`InfiniteCarousel: Container not found for selector "${selector}"`
);
}
this.track = this.container.querySelector(".splide__track");
this.list = this.container.querySelector(".splide__list");
if (!this.track || !this.list) {
throw new Error("InfiniteCarousel: Required elements not found");
}
this.slides = Array.from(
this.list.querySelectorAll(
".splide__slide:not(.splide__slide--clone)"
)
);
if (this.slides.length === 0) {
throw new Error("InfiniteCarousel: No slides found");
}
// Store configuration
this.config = config;
this.gap = config.gap;
this.padding = config.padding;
this.slideWidth = config.slideWidth;
this.speed = config.speed;
this.easing = config.easing;
this.focusCenter = config.focusCenter;
this.mobileOffset = config.mobileOffset;
// Navigation elements
this.prevBtn = this.container.querySelector(".splide__arrow--prev");
this.nextBtn = this.container.querySelector(".splide__arrow--next");
this.paginationEls = Array.from(
this.container.querySelectorAll(".splide__pagination__page")
);
// State
this.originalSlideCount = this.slides.length;
this.CLONE_COUNT =
config.cloneCount ||
Math.max(6, Math.ceil(this.originalSlideCount / 2));
this.currentIndex = this.CLONE_COUNT;
this.targetIndex = null;
this.isAnimating = false;
this.isRepositioning = false;
this.realIndex = 0;
this.index = 0;
this.isInViewport = false;
// Interaction state
this._navigationLocked = false;
this._pointerActive = false;
this._isDragging = false;
this._touchActive = false;
this._hasTouch = false;
this._startX = 0;
this._lastX = 0;
this._startT = 0;
this._lastMoveT = 0;
this._velocityX = 0;
this._dragThreshold = 10;
this._swipeTrigger = 50;
// Event system
this.eventHandlers = {
mounted: [],
move: [],
moved: [],
active: [],
interactionStart: [],
interactionEnd: [],
visibilityChange: [],
};
// Components API
this.Components = {
Elements: { slides: [] },
Slides: {
getLength: () => this.originalSlideCount,
getAt: (i) => this.Components.Elements.slides[i],
},
};
// Observers
this._viewportObserver = null;
// Bind methods
this._onTransitionEnd = this._onTransitionEnd.bind(this);
this._onVisibilityChange = this._onVisibilityChange.bind(this);
this.init();
}
// [Rest of the InfiniteCarousel methods remain the same as in original code]
// I'll include the essential methods here for brevity
mod(a, n) {
return ((a % n) + n) % n;
}
_transitionCSS() {
return `transform ${this.speed}ms ${this.easing}, filter ${this.speed}ms ${this.easing}`;
}
_applySlideTransition(el) {
el.style.transition = this._transitionCSS();
}
_withSlidesNoTransition(fn) {
const slides = this.Components.Elements.slides;
const prev = new Array(slides.length);
for (let i = 0; i < slides.length; i++) {
prev[i] = slides[i].style.transition;
slides[i].style.transition = "none";
}
try {
fn();
} finally {
this.list.offsetHeight;
for (let i = 0; i < slides.length; i++) {
slides[i].style.transition = prev[i] || this._transitionCSS();
}
}
}
mapToReal(domIndex) {
const n = this.originalSlideCount;
const realStart = this.CLONE_COUNT;
if (domIndex < realStart) {
return (n - this.CLONE_COUNT + domIndex) % n;
}
if (domIndex >= realStart + n) {
return this.mod(domIndex - (realStart + n), n);
}
return domIndex - realStart;
}
isBusy() {
return (
this._isDragging ||
this._pointerActive ||
this.isAnimating ||
this.isRepositioning ||
!this._isFullyInitialized
);
}
init() {
if (this._isInitializing) return;
this._isInitializing = true;
Utils.log("Initializing carousel");
// Setup track and slides
this.track.style.paddingLeft = this.padding + "px";
this.track.style.paddingRight = this.padding + "px";
this.slides.forEach((slide, i) => {
slide.style.width = this.slideWidth + "px";
slide.style.marginRight = this.gap + "px";
slide.style.flexShrink = "0";
this._applySlideTransition(slide);
slide.classList.add("centered");
slide.setAttribute("data-real-index", String(i));
});
this.list.offsetHeight;
this.createClones();
this.Components.Elements.slides = Array.from(
this.list.querySelectorAll(".splide__slide")
);
this.Components.Elements.slides.forEach((el) => {
el.classList.add("centered");
this._applySlideTransition(el);
});
this.currentIndex = this.CLONE_COUNT;
this.realIndex = 0;
this.index = 0;
this.updatePosition(false);
this.updateActiveSlide();
this.updatePagination();
this.container.classList.add(
"is-initialized",
"is-active",
"splide--loop",
"splide--ltr",
"splide--draggable"
);
this.list.setAttribute("role", "presentation");
this.track.classList.add(
"splide__track--loop",
"splide__track--ltr",
"splide__track--draggable"
);
requestAnimationFrame(() => {
this.setupPagination();
this.setupEventListeners();
this.setupSwipeListeners();
this.setupVisibilityHandler();
this.setupViewportObserver();
this._isFullyInitialized = true;
this._isInitializing = false;
Utils.log("Carousel mounted");
this.emit("mounted");
});
}
createClones() {
const n = this.originalSlideCount;
// Clone at end
for (let i = 0; i < this.CLONE_COUNT; i++) {
const srcIdx = i % n;
const src = this.slides[srcIdx];
const clone = src.cloneNode(true);
clone.classList.add("splide__slide--clone", "centered");
clone.id = `${src.id || `slide-${srcIdx}`}-clone-end-${i}`;
clone.style.width = this.slideWidth + "px";
clone.style.marginRight = this.gap + "px";
clone.style.flexShrink = "0";
this._applySlideTransition(clone);
clone.setAttribute("data-real-index", String(srcIdx));
this.list.appendChild(clone);
}
// Clone at start
for (let i = this.CLONE_COUNT - 1; i >= 0; i--) {
const srcIdx = (n - this.CLONE_COUNT + i) % n;
const src = this.slides[srcIdx];
const clone = src.cloneNode(true);
clone.classList.add("splide__slide--clone", "centered");
clone.id = `${src.id || `slide-${srcIdx}`}-clone-start-${i}`;
clone.style.width = this.slideWidth + "px";
clone.style.marginRight = this.gap + "px";
clone.style.flexShrink = "0";
this._applySlideTransition(clone);
clone.setAttribute("data-real-index", String(srcIdx));
this.list.insertBefore(clone, this.list.firstChild);
}
}
setupPagination() {
this.paginationEls.forEach((dot, i) => {
dot.classList.add("centered");
dot.addEventListener("click", () => {
if (!this._isFullyInitialized || !this.isInViewport) return;
this.emit("interactionStart", { type: "pagination" });
this.go(i);
});
});
}
setupEventListeners() {
if (this.nextBtn) {
this.nextBtn.addEventListener("click", () => {
if (!this._isFullyInitialized || !this.isInViewport) return;
this.emit("interactionStart", { type: "button" });
this.next();
});
}
if (this.prevBtn) {
this.prevBtn.addEventListener("click", () => {
if (!this._isFullyInitialized || !this.isInViewport) return;
this.emit("interactionStart", { type: "button" });
this.prev();
});
}
document.addEventListener("keydown", (e) => {
if (!this._isFullyInitialized || !this.isInViewport) return;
if (e.key === "ArrowRight" && !e.repeat) {
this.emit("interactionStart", { type: "keyboard" });
this.next();
} else if (e.key === "ArrowLeft" && !e.repeat) {
this.emit("interactionStart", { type: "keyboard" });
this.prev();
} else if (
(e.key === " " || e.key === "Spacebar") &&
!e.repeat
) {
e.preventDefault();
this.emit("interactionStart", { type: "keyboard-spacebar" });
}
});
this.list.addEventListener("transitionend", this._onTransitionEnd);
}
setupViewportObserver() {
this._viewportObserver = new IntersectionObserver(
(entries) => {
if (!this._isFullyInitialized) return;
entries.forEach((entry) => {
this.isInViewport = entry.isIntersecting;
this.emit("visibilityChange", {
isVisible: this.isInViewport,
});
});
},
{ threshold: 0.01 }
);
this._viewportObserver.observe(this.container);
}
setupVisibilityHandler() {
document.addEventListener("visibilitychange", this._onVisibilityChange);
}
_onVisibilityChange() {
if (!this._isFullyInitialized) return;
if (document.hidden && this._pointerActive) {
this._cancelDrag();
}
}
_cancelDrag() {
this._pointerActive = false;
this._isDragging = false;
this._touchActive = false;
this.list.classList.remove("is-dragging");
if (this.targetIndex === null) {
this.list.style.transition = this._transitionCSS();
this.updatePosition(true);
}
this.emit("interactionEnd", { type: "drag-cancelled" });
}
setupSwipeListeners() {
// Touch events
this.list.addEventListener(
"touchstart",
(e) => {
if (
!this._isFullyInitialized ||
e.touches.length !== 1 ||
!this.isInViewport
)
return;
this._hasTouch = true;
this._touchActive = true;
this.emit("interactionStart", { type: "touch" });
const touch = e.touches[0];
this._pointerActive = true;
this._isDragging = false;
this._startX = touch.clientX;
this._lastX = touch.clientX;
this._startT = performance.now();
this._lastMoveT = this._startT;
this._velocityX = 0;
},
{ passive: true }
);
this.list.addEventListener(
"touchmove",
(e) => {
if (
!this._isFullyInitialized ||
!this._pointerActive ||
e.touches.length !== 1 ||
!this.isInViewport
)
return;
const touch = e.touches[0];
const x = touch.clientX;
const dx = x - this._startX;
const now = performance.now();
const dt = Math.max(1, now - this._lastMoveT);
this._velocityX = (x - this._lastX) / dt;
this._lastX = x;
this._lastMoveT = now;
if (!this._isDragging && Math.abs(dx) > this._dragThreshold) {
this._isDragging = true;
this.list.classList.add("is-dragging");
}
if (this._isDragging && !this.isRepositioning) {
const offset = this.focusCenter
? this.track.offsetWidth / 2 -
this.slideWidth / 2 -
this.padding
: 0;
const mobileAdjustment =
window.innerWidth <= 479 ? this.mobileOffset : 0;
const baseDom =
this.targetIndex !== null
? this.targetIndex
: this.currentIndex;
const baseTranslate =
-(baseDom * (this.slideWidth + this.gap)) +
offset +
mobileAdjustment;
this.list.style.transition = "none";
this.list.style.transform = `translateX(${baseTranslate + dx}px)`;
}
},
{ passive: true }
);
this.list.addEventListener("touchend", () => {
if (
!this._isFullyInitialized ||
!this._pointerActive ||
!this.isInViewport
)
return;
const totalDx = this._lastX - this._startX;
this._pointerActive = false;
this._touchActive = false;
this.list.classList.remove("is-dragging");
if (this._isDragging) {
const isSwipe =
Math.abs(this._velocityX) > 0.3 ||
Math.abs(totalDx) > this._swipeTrigger;
if (isSwipe) {
if (totalDx < 0) this.next();
else this.prev();
} else {
this.list.style.transition = this._transitionCSS();
this.updatePosition(true);
this.emit("interactionEnd", { type: "touch" });
}
} else {
this.list.style.transition = this._transitionCSS();
this.emit("interactionEnd", { type: "touch" });
}
this._isDragging = false;
});
this.list.addEventListener("touchcancel", () => {
if (!this._isFullyInitialized || !this.isInViewport) return;
this._cancelDrag();
});
// Mouse events
this.list.addEventListener("mousedown", (e) => {
if (
!this._isFullyInitialized ||
e.button !== 0 ||
!this.isInViewport ||
this._hasTouch
)
return;
this.emit("interactionStart", { type: "mouse" });
this._pointerActive = true;
this._isDragging = false;
this._startX = e.clientX;
this._lastX = e.clientX;
this._startT = performance.now();
this._lastMoveT = this._startT;
this._velocityX = 0;
e.preventDefault();
});
window.addEventListener("mousemove", (e) => {
if (
!this._isFullyInitialized ||
!this._pointerActive ||
!this.isInViewport ||
this._touchActive
)
return;
const x = e.clientX;
const dx = x - this._startX;
const now = performance.now();
const dt = Math.max(1, now - this._lastMoveT);
this._velocityX = (x - this._lastX) / dt;
this._lastX = x;
this._lastMoveT = now;
if (!this._isDragging && Math.abs(dx) > this._dragThreshold) {
this._isDragging = true;
this.list.classList.add("is-dragging");
}
if (this._isDragging && !this.isRepositioning) {
const offset = this.focusCenter
? this.track.offsetWidth / 2 - this.slideWidth / 2 - this.padding
: 0;
const mobileAdjustment =
window.innerWidth <= 479 ? this.mobileOffset : 0;
const baseDom =
this.targetIndex !== null ? this.targetIndex : this.currentIndex;
const baseTranslate =
-(baseDom * (this.slideWidth + this.gap)) +
offset +
mobileAdjustment;
this.list.style.transition = "none";
this.list.style.transform = `translateX(${baseTranslate + dx}px)`;
}
});
window.addEventListener("mouseup", () => {
if (
!this._isFullyInitialized ||
!this._pointerActive ||
!this.isInViewport ||
this._touchActive
)
return;
const totalDx = this._lastX - this._startX;
this._pointerActive = false;
this.list.classList.remove("is-dragging");
if (this._isDragging) {
const isSwipe =
Math.abs(this._velocityX) > 0.3 ||
Math.abs(totalDx) > this._swipeTrigger;
if (isSwipe) {
if (totalDx < 0) this.next();
else this.prev();
} else {
this.list.style.transition = this._transitionCSS();
this.updatePosition(true);
this.emit("interactionEnd", { type: "mouse" });
}
} else {
this.list.style.transition = this._transitionCSS();
this.emit("interactionEnd", { type: "mouse" });
}
this._isDragging = false;
});
this.list.addEventListener("dragstart", (e) => e.preventDefault());
}
next() {
if (
!this._isFullyInitialized ||
this._navigationLocked ||
this.isRepositioning
)
return;
const current =
this.targetIndex !== null ? this.targetIndex : this.currentIndex;
this._navigateTo(current + 1);
}
prev() {
if (
!this._isFullyInitialized ||
this._navigationLocked ||
this.isRepositioning
)
return;
const current =
this.targetIndex !== null ? this.targetIndex : this.currentIndex;
this._navigateTo(current - 1);
}
go(realIndex) {
if (
!this._isFullyInitialized ||
this._navigationLocked ||
this.isRepositioning
)
return;
const clamped = this.mod(realIndex, this.originalSlideCount);
const currentReal = this.mapToReal(
this.targetIndex !== null ? this.targetIndex : this.currentIndex
);
let delta = clamped - currentReal;
if (Math.abs(delta) > this.originalSlideCount / 2) {
delta =
delta > 0
? delta - this.originalSlideCount
: delta + this.originalSlideCount;
}
const current =
this.targetIndex !== null ? this.targetIndex : this.currentIndex;
this._navigateTo(current + delta);
}
_navigateTo(toDom) {
const total = this.Components.Elements.slides.length;
const n = this.originalSlideCount;
const current =
this.targetIndex !== null ? this.targetIndex : this.currentIndex;
let target = toDom;
const delta = target - current;
const clampedDelta = Math.max(-n, Math.min(n, delta));
target = current + clampedDelta;
if (target === current) return;
while (target >= total) target -= n;
while (target < 0) target += n;
const fromDom = this.currentIndex;
if (this.isAnimating) {
this.targetIndex = target;
this._updateActivatingClasses(target);
this.updatePosition(true);
} else {
this.targetIndex = target;
this.isAnimating = true;
this._updateActivatingClasses(target);
this.emit("move", {
from: {
domIndex: fromDom,
realIndex: this.mapToReal(fromDom),
},
to: {
domIndex: target,
realIndex: this.mapToReal(target),
},
});
this.updatePosition(true);
}
}
_updateActivatingClasses(toDom) {
const slides = this.Components.Elements.slides;
slides.forEach((el) =>
el.classList.remove(
"is-activating",
"is-deactivating",
"is-active",
"is-prev",
"is-next"
)
);
slides[toDom]?.classList.add("is-active", "is-activating");
const toReal = this.mapToReal(toDom);
this.paginationEls.forEach((dot, i) => {
dot.classList.remove("is-activating", "is-deactivating");
dot.classList.toggle("is-active", i === toReal);
if (i === toReal) dot.classList.add("is-activating");
});
this.emit("active", {
domIndex: toDom,
realIndex: toReal,
slideEl: slides[toDom],
early: true,
});
}
updatePosition(animate = true) {
const offset = this.focusCenter
? this.track.offsetWidth / 2 - this.slideWidth / 2 - this.padding
: 0;
const mobileAdjustment =
window.innerWidth <= 479 ? this.mobileOffset : 0;
const domIndex =
this.targetIndex !== null ? this.targetIndex : this.currentIndex;
const translateX =
-(domIndex * (this.slideWidth + this.gap)) + offset + mobileAdjustment;
if (animate) {
this.list.style.transition = `transform ${this.speed}ms ${this.easing}`;
} else {
this.list.style.transition = "none";
}
this.list.style.transform = `translateX(${translateX}px)`;
}
updateActiveSlide() {
const slides = this.Components.Elements.slides;
const total = slides.length;
const centerDom = this.currentIndex;
const prevIdx = this.mod(centerDom - 1, total);
const nextIdx = this.mod(centerDom + 1, total);
for (let i = 0; i < total; i++) {
slides[i].classList.remove(
"is-active",
"is-prev",
"is-next",
"is-activating",
"is-deactivating"
);
}
slides[centerDom]?.classList.add("is-active");
slides[prevIdx]?.classList.add("is-prev");
slides[nextIdx]?.classList.add("is-next");
}
updatePagination() {
if (!this.paginationEls.length) return;
const activeReal = this.realIndex;
this.paginationEls.forEach((dot, i) => {
dot.classList.remove("is-activating", "is-deactivating");
dot.classList.toggle("is-active", i === activeReal);
});
}
_onTransitionEnd(e) {
if (
!this._isFullyInitialized ||
e.target !== this.list ||
e.propertyName !== "transform" ||
this.targetIndex === null ||
!this.isAnimating
)
return;
this.currentIndex = this.targetIndex;
this.targetIndex = null;
this.isAnimating = false;
this.realIndex = this.mapToReal(this.currentIndex);
this.index = this.realIndex;
const didReposition = this.checkRepositionWithClassMirror();
const finalize = () => {
const slides = this.Components.Elements.slides;
slides.forEach((el) =>
el.classList.remove("is-activating", "is-deactivating")
);
this.updateActiveSlide();
this.updatePagination();
};
if (didReposition) {
this._withSlidesNoTransition(finalize);
} else {
finalize();
}
this.emit("active", {
domIndex: this.currentIndex,
realIndex: this.realIndex,
slideEl: this.Components.Elements.slides[this.currentIndex],
early: false,
repositioned: didReposition,
});
this.emit("moved", {
domIndex: this.currentIndex,
realIndex: this.realIndex,
});
this.emit("interactionEnd", { type: "transition-complete" });
}
checkRepositionWithClassMirror() {
const slides = this.Components.Elements.slides;
const total = slides.length;
let jumped = false;
this._navigationLocked = true;
const reposThreshold = Math.ceil(this.CLONE_COUNT / 2);
if (this.currentIndex >= total - reposThreshold) {
this.isRepositioning = true;
jumped = true;
const srcIndex = this.currentIndex;
const newIndex =
this.CLONE_COUNT + (this.currentIndex - (total - this.CLONE_COUNT));
this._mirrorActiveClasses(srcIndex, newIndex);
this.currentIndex = newIndex;
this.list.style.transition = "none";
this.updatePosition(false);
this.list.offsetHeight;
this.isRepositioning = false;
this.realIndex = this.mapToReal(this.currentIndex);
this.index = this.realIndex;
}
if (this.currentIndex <= reposThreshold - 1) {
this.isRepositioning = true;
jumped = true;
const srcIndex = this.currentIndex;
const newIndex =
total - this.CLONE_COUNT - (this.CLONE_COUNT - this.currentIndex);
this._mirrorActiveClasses(srcIndex, newIndex);
this.currentIndex = newIndex;
this.list.style.transition = "none";
this.updatePosition(false);
this.list.offsetHeight;
this.isRepositioning = false;
this.realIndex = this.mapToReal(this.currentIndex);
this.index = this.realIndex;
}
this._navigationLocked = false;
return jumped;
}
_mirrorActiveClasses(srcIndex, dstIndex) {
const slides = this.Components.Elements.slides;
const src = slides[srcIndex];
const dst = slides[dstIndex];
if (!src || !dst) return;
const copyFlag = (cls) => {
if (src.classList.contains(cls)) {
dst.classList.add(cls);
} else {
dst.classList.remove(cls);
}
};
copyFlag("is-active");
copyFlag("is-activating");
copyFlag("is-prev");
copyFlag("is-next");
}
resizeTo(width, gap, padding) {
if (!this._isFullyInitialized) return;
this.slideWidth = width;
this.gap = gap;
this.padding = padding;
const all = this.list.querySelectorAll(".splide__slide");
all.forEach((el) => {
el.style.width = this.slideWidth + "px";
el.style.marginRight = this.gap + "px";
el.style.flexShrink = "0";
this._applySlideTransition(el);
el.classList.add("centered");
});
this.track.style.paddingLeft = this.padding + "px";
this.track.style.paddingRight = this.padding + "px";
this._withSlidesNoTransition(() => {
this.updatePosition(false);
this.updateActiveSlide();
this.updatePagination();
});
}
destroy() {
if (this._viewportObserver) {
this._viewportObserver.disconnect();
this._viewportObserver = null;
}
document.removeEventListener(
"visibilitychange",
this._onVisibilityChange
);
this.list.removeEventListener("transitionend", this._onTransitionEnd);
this._isFullyInitialized = false;
Utils.log("Carousel destroyed");
}
on(event, handler) {
if (this.eventHandlers[event]) {
this.eventHandlers[event].push(handler);
}
}
emit(event, ...args) {
if (this.eventHandlers[event]) {
this.eventHandlers[event].forEach((h) => h(...args));
}
}
}
// ==================== DYNAMIC TEXT COLOR ====================
class DynamicTextColor {
constructor(config) {
this.config = config;
this.isSafari = Utils.isSafari();
this.intervalId = null;
}
init() {
if (!this.config.enabled) return;
this.update();
if (this.config.updateInterval > 0) {
this.intervalId = setInterval(
() => this.update(),
this.config.updateInterval
);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => this.update());
}
let resizeTimeout;
window.addEventListener(
"resize",
() => {
if (resizeTimeout) clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => this.update(), 150);
},
{ passive: true }
);
}
update() {
try {
const elements = document.querySelectorAll(
'[data-dynamic-color="true"]'
);
if (this.isSafari && this.config.safariWhiteText) {
elements.forEach((element) => {
element.style.color = "#ffffff";
});
return;
}
elements.forEach((element) => this.updateElement(element));
} catch (error) {
Utils.error("Error updating text colors:", error);
}
}
updateElement(element) {
const style = window.getComputedStyle(element);
const backgroundImage = style.backgroundImage;
let rgb;
const hexColorMatch = backgroundImage.match(
/#[a-f0-9]{6}|#[a-f0-9]{3}/i
);
if (hexColorMatch) {
rgb = Utils.hexToRgb(hexColorMatch[0]);
} else {
const rgbColorMatch = backgroundImage.match(/rgb\(\d+,\s*\d+,\s*\d+\)/);
if (rgbColorMatch) {
rgb = Utils.parseRgb(rgbColorMatch[0]);
}
}
if (!rgb) {
const gradientStart = style.getPropertyValue("--gradient-start").trim();
const gradientEnd = style.getPropertyValue("--gradient-end").trim();
if (gradientStart && gradientStart.startsWith("rgb")) {
rgb = Utils.parseRgb(gradientStart);
} else if (gradientEnd && gradientEnd.startsWith("rgb")) {
rgb = Utils.parseRgb(gradientEnd);
}
}
if (!rgb) return;
const bgLuminance = Utils.getLuminance(rgb.r, rgb.g, rgb.b);
const blackLuminance = Utils.getLuminance(0, 0, 0);
const whiteLuminance = Utils.getLuminance(255, 255, 255);
const blackContrast = Utils.getContrastRatio(bgLuminance, blackLuminance);
const whiteContrast = Utils.getContrastRatio(bgLuminance, whiteLuminance);
const optimalColor =
blackContrast - whiteContrast > this.config.contrastThreshold
? "#000000"
: "#ffffff";
element.style.color = optimalColor;
}
destroy() {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
}
}
// ==================== CARD BACKGROUNDS ====================
class CardBackgrounds {
constructor(carousel, colorExtractor, config) {
this.carousel = carousel;
this.colorExtractor = colorExtractor;
this.config = config;
this.observer = null;
}
async init() {
if (!this.config.enabled) return;
const cards = Array.from(
document.querySelectorAll(".splide__slide")
);
await this.processBatch(cards, 0);
this.observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.addedNodes.length) {
const newCards = Array.from(mutation.addedNodes).filter(
(node) => node.classList?.contains("splide__slide")
);
if (newCards.length) {
this.processBatch(newCards, 0);
}
}
});
});
this.observer.observe(document.body, {
childList: true,
subtree: true,
});
}
async processBatch(cards, startIndex) {
const batch = cards.slice(
startIndex,
startIndex + this.config.batchSize
);
if (batch.length === 0) return;
await Promise.all(
batch.map(async (card) => {
try {
const img = card.querySelector("img");
if (!img?.src) return;
const colors = await this.colorExtractor.extractFromUrl(img.src);
if (!colors) return;
const slideContents = card.querySelector(".splide-slide-text");
if (!slideContents) return;
const black = "#000000";
const vibrant = colors.vibrant;
const darkenedColor = Utils.interpolateColor(
black,
vibrant,
this.config.darkenWeight
);
slideContents.style.backgroundColor = darkenedColor;
// Optional: text color adjustment
const hexColor = colors.dmuted;
if (hexColor) {
const rgb = hexColor.match(/\w\w/g);
const brightness = Math.round(
(parseInt(rgb[0], 16) * 299 +
parseInt(rgb[1], 16) * 587 +
parseInt(rgb[2], 16) * 114) /
1000
);
const textColor = brightness > 128 ? "#000000" : "#FFFFFF";
const textElements = slideContents.querySelectorAll(
".splide-slide-heading, .splide-slide-sub-heading"
);
textElements.forEach((el) => (el.style.color = textColor));
}
} catch (error) {
Utils.warn("Error processing card:", error);
}
})
);
requestAnimationFrame(() => {
this.processBatch(cards, startIndex + this.config.batchSize);
});
}
destroy() {
if (this.observer) {
this.observer.disconnect();
this.observer = null;
}
}
}
// ==================== GRADIENT ANIMATOR ====================
class GradientAnimator {
constructor(carousel, colorExtractor, config) {
this.carousel = carousel;
this.colorExtractor = colorExtractor;
this.config = config;
this.currentAnimation = null;
this.lastImageSrc = null;
this.initializeCSS();
}
initializeCSS() {
const style = document.createElement("style");
document.head.appendChild(style);
try {
style.sheet.insertRule(
".why-ais-scrolling-cards{background-image:linear-gradient(var(--gradient-start),var(--gradient-end))}"
);
} catch (e) {
Utils.warn("Failed to insert gradient CSS rule");
}
document.documentElement.style.setProperty(
"--gradient-start",
"rgb(0,0,0)"
);
document.documentElement.style.setProperty("--gradient-end", "rgb(0,0,0)");
}
init() {
if (!this.config.enabled || !window.gsap) {
if (!window.gsap && this.config.enabled) {
Utils.warn("GSAP not found, gradient animations disabled");
}
return;
}
this.handleActive();
this.carousel.on("active", () => this.handleActive());
this.carousel.on("moved", () => this.handleActive());
}
async handleActive() {
const activeSlide =
this.carousel.list?.querySelector(".splide__slide.is-active") ||
this.carousel.Components?.Elements?.slides?.[
this.carousel.currentIndex
];
if (!activeSlide) return;
const img = activeSlide.querySelector("img");
let startHex = "#000000";
let endHex = "#000000";
if (img?.src) {
if (img.src === this.lastImageSrc) return;
this.lastImageSrc = img.src;
try {
const colors = await this.colorExtractor.extractFromUrl(img.src);
if (colors) {
startHex =
colors[this.config.colorPairs.start] || colors.dominant;
endHex = colors[this.config.colorPairs.end] || colors.dominant;
}
} catch (err) {
Utils.warn("Gradient color extraction error:", err);
}
} else {
const panel = activeSlide.querySelector(".splide-slide-text");
if (panel) {
const bg = getComputedStyle(panel).backgroundColor;
const m = bg && bg.match(/\d+/g);
if (m) {
const asHex = (r, g, b) =>
"#" +
[r, g, b]
.map((n) => Number(n).toString(16).padStart(2, "0"))
.join("");
const hex = asHex(m[0], m[1], m[2]);
startHex = hex;
endHex = hex;
}
}
this.lastImageSrc = null;
}
this.updateGradient(startHex, endHex);
}
updateGradient(startHex, endHex) {
const container = document.querySelector(".why-ais-scrolling-cards");
if (!container) return;
const cs = getComputedStyle(container);
const parseRGB = (val, fallback = { r: 0, g: 0, b: 0 }) => {
if (!val) return fallback;
const m = val.match(/\d+/g);
return m ? { r: +m[0], g: +m[1], b: +m[2] } : fallback;
};
const currentStartRGB = parseRGB(
cs.getPropertyValue("--gradient-start").trim()
);
const currentEndRGB = parseRGB(
cs.getPropertyValue("--gradient-end").trim()
);
const targetStart = Utils.hexToRgb(startHex);
const targetEnd = Utils.hexToRgb(endHex);
if (!targetStart || !targetEnd) return;
if (this.currentAnimation && typeof this.currentAnimation.kill === "function") {
this.currentAnimation.kill();
}
const tweenState = {
sR: currentStartRGB.r,
sG: currentStartRGB.g,
sB: currentStartRGB.b,
eR: currentEndRGB.r,
eG: currentEndRGB.g,
eB: currentEndRGB.b,
};
const rgbToString = (rgb) =>
`rgb(${Math.round(rgb.r)}, ${Math.round(rgb.g)}, ${Math.round(rgb.b)})`;
this.currentAnimation = gsap.to(tweenState, {
duration: this.config.duration,
sR: targetStart.r,
sG: targetStart.g,
sB: targetStart.b,
eR: targetEnd.r,
eG: targetEnd.g,
eB: targetEnd.b,
ease: "power2.inOut",
onUpdate: () => {
container.style.setProperty(
"--gradient-start",
rgbToString({
r: tweenState.sR,
g: tweenState.sG,
b: tweenState.sB,
})
);
container.style.setProperty(
"--gradient-end",
rgbToString({ r: tweenState.eR, g: tweenState.eG, b: tweenState.eB })
);
},
});
}
destroy() {
if (this.currentAnimation && typeof this.currentAnimation.kill === "function") {
this.currentAnimation.kill();
this.currentAnimation = null;
}
}
}
// ==================== AUTOPLAY ====================
class CarouselAutoplay {
constructor(carousel, config) {
this.carousel = carousel;
this.config = config;
this.intervalId = null;
this.isPlaying = false;
this.hasInitialAdvance = false;
this.handleVisibilityChangeBound =
this.handleVisibilityChange.bind(this);
}
init() {
if (!this.config.enabled || !this.carousel) return;
this.carousel.on("visibilityChange", this.handleVisibilityChangeBound);
this.carousel.on("interactionStart", (data) => {
if (data.type === "keyboard-spacebar") {
this.toggle();
}
});
if (this.carousel.isInViewport) {
this.play();
}
}
handleVisibilityChange(data) {
if (data.isVisible) {
this.play();
} else {
this.pause();
}
}
toggle() {
if (this.isPlaying) {
this.pause();
} else {
this.play();
}
}
play() {
if (this.intervalId || !this.carousel.isInViewport) return;
this.isPlaying = true;
if (!this.hasInitialAdvance) {
this.advance();
this.hasInitialAdvance = true;
}
this.intervalId = setInterval(() => {
this.advance();
}, this.config.interval);
Utils.log("Autoplay started");
}
pause() {
this.isPlaying = false;
clearInterval(this.intervalId);
this.intervalId = null;
Utils.log("Autoplay paused");
}
advance() {
if (!this.carousel || !this.carousel.isInViewport) return;
if (!this.carousel.isBusy()) {
this.carousel.next();
}
}
destroy() {
this.pause();
}
}
// ==================== MAIN LIBRARY CLASS ====================
class InfiniteCarouselLibrary {
constructor(userConfig = {}) {
// Deep merge configuration
this.config = this.mergeConfig(DEFAULT_CONFIG, userConfig);
Utils.config = this.config;
// Store instances
this.carousel = null;
this.colorExtractor = null;
this.dynamicTextColor = null;
this.cardBackgrounds = null;
this.gradientAnimator = null;
this.autoplay = null;
Utils.log("Library initialized with config:", this.config);
}
mergeConfig(defaults, user) {
const merged = { ...defaults };
for (const key in user) {
if (
typeof user[key] === "object" &&
!Array.isArray(user[key]) &&
user[key] !== null
) {
merged[key] = this.mergeConfig(defaults[key] || {}, user[key]);
} else {
merged[key] = user[key];
}
}
return merged;
}
async init() {
try {
// Initialize color extractor
if (this.config.colors.enabled) {
this.colorExtractor = new ColorExtractor(this.config.colors);
Utils.log("Color extractor initialized");
}
// Initialize carousel
await this.initCarousel();
// Initialize additional features
if (this.carousel) {
this.initFeatures();
}
Utils.log("Library fully initialized");
} catch (error) {
Utils.error("Initialization failed:", error);
throw error;
}
}
async initCarousel() {
const tryInit = () => {
const container = document.querySelector(this.config.carousel.selector);
if (!container) {
Utils.warn("Carousel container not found, retrying...");
return false;
}
try {
this.carousel = new InfiniteCarousel(
this.config.carousel.selector,
this.config.carousel
);
// Setup responsive handling
this.setupResponsive();
Utils.log("Carousel initialized");
return true;
} catch (error) {
Utils.error("Carousel initialization failed:", error);
return false;
}
};
return new Promise((resolve) => {
const attemptInit = (attempts = 0) => {
if (tryInit()) {
resolve();
return;
}
if (attempts >= 10) {
Utils.error("Failed to initialize carousel after 10 attempts");
resolve();
return;
}
const delay = Math.min(100 * Math.pow(1.5, attempts), 2000);
setTimeout(() => attemptInit(attempts + 1), delay);
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
requestAnimationFrame(() => attemptInit());
});
} else {
requestAnimationFrame(() => attemptInit());
}
});
}
initFeatures() {
// Dynamic text color
if (this.config.dynamicText.enabled) {
this.dynamicTextColor = new DynamicTextColor(this.config.dynamicText);
this.dynamicTextColor.init();
Utils.log("Dynamic text color initialized");
}
// Card backgrounds
if (this.config.cardBackgrounds.enabled && this.colorExtractor) {
this.cardBackgrounds = new CardBackgrounds(
this.carousel,
this.colorExtractor,
this.config.cardBackgrounds
);
this.carousel.on("mounted", () => {
this.cardBackgrounds.init();
Utils.log("Card backgrounds initialized");
});
}
// Gradient animator
if (this.config.gradients.enabled && this.colorExtractor) {
this.gradientAnimator = new GradientAnimator(
this.carousel,
this.colorExtractor,
this.config.gradients
);
this.carousel.on("mounted", () => {
this.gradientAnimator.init();
Utils.log("Gradient animator initialized");
});
}
// Autoplay
if (this.config.autoplay.enabled) {
this.autoplay = new CarouselAutoplay(
this.carousel,
this.config.autoplay
);
this.carousel.on("mounted", () => {
this.autoplay.init();
Utils.log("Autoplay initialized");
});
}
}
setupResponsive() {
let resizeTimeout;
window.addEventListener("resize", () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
if (!this.carousel || !this.carousel._isFullyInitialized) return;
const mobile = this.config.responsive.mobile;
if (window.innerWidth <= mobile.breakpoint) {
const width =
typeof mobile.slideWidth === "string" &&
mobile.slideWidth.includes("vw")
? Math.round(
(window.innerWidth *
parseFloat(mobile.slideWidth)) /
100
)
: mobile.slideWidth;
this.carousel.resizeTo(width, mobile.gap, mobile.padding);
} else {
this.carousel.resizeTo(
this.config.carousel.slideWidth,
this.config.carousel.gap,
this.config.carousel.padding
);
}
}, 100);
});
}
// Public API
getCarousel() {
return this.carousel;
}
getColorExtractor() {
return this.colorExtractor;
}
toggleAutoplay() {
if (this.autoplay) {
this.autoplay.toggle();
}
}
destroy() {
Utils.log("Destroying library");
if (this.autoplay) this.autoplay.destroy();
if (this.gradientAnimator) this.gradientAnimator.destroy();
if (this.cardBackgrounds) this.cardBackgrounds.destroy();
if (this.dynamicTextColor) this.dynamicTextColor.destroy();
if (this.carousel) this.carousel.destroy();
this.carousel = null;
this.colorExtractor = null;
this.dynamicTextColor = null;
this.cardBackgrounds = null;
this.gradientAnimator = null;
this.autoplay = null;
Utils.log("Library destroyed");
}
}
// ==================== EXPORT ====================
global.InfiniteCarouselLibrary = InfiniteCarouselLibrary;
global.InfiniteCarousel = InfiniteCarousel; // Export core class separately
})(typeof window !== "undefined" ? window : this);
- Usage Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Infinite Carousel Example</title>
<!-- CSS Dependency -->
<link rel="stylesheet" href="infinite-carousel.css" />
<!-- GSAP (required for gradient animations) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
</head>
<body>
<div class="why-ais-scrolling-cards">
<div class="splide centered">
<div class="splide__track">
<ul class="splide__list">
<li class="splide__slide">
<img src="image1.jpg" alt="Slide 1" />
<div class="splide-slide-text">
<h3 class="splide-slide-heading">Slide 1</h3>
<p class="splide-slide-sub-heading">Description</p>
</div>
</li>
<!-- More slides -->
</ul>
</div>
<!-- Navigation -->
<button class="splide__arrow splide__arrow--prev">Previous</button>
<button class="splide__arrow splide__arrow--next">Next</button>
<!-- Pagination -->
<ul class="splide__pagination">
<li>
<button class="splide__pagination__page"></button>
</li>
<!-- More pagination dots -->
</ul>
</div>
</div>
<!-- Library -->
<script src="infinite-carousel.js"></script>
<!-- Initialize -->
<script>
// Basic initialization with defaults
const carousel = new InfiniteCarouselLibrary();
carousel.init();
// Or with custom configuration
const customCarousel = new InfiniteCarouselLibrary({
carousel: {
selector: ".splide.centered",
gap: 32,
slideWidth: 380,
speed: 800,
},
autoplay: {
enabled: true,
interval: 3000,
},
colors: {
sampleSize: 150, // Higher quality color extraction
},
gradients: {
duration: 1.2,
colorPairs: {
start: "lvibrant", // Light vibrant
end: "dvibrant", // Dark vibrant
},
},
debug: true, // Enable console logging
});
customCarousel.init();
// Access carousel instance
const carouselInstance = customCarousel.getCarousel();
carouselInstance.on("moved", (data) => {
console.log("Moved to slide:", data.realIndex);
});
</script>
</body>
</html>
- Configuration Options Reference
{
// Core carousel settings
carousel: {
selector: ".splide.centered", // CSS selector for carousel container
gap: 48, // Gap between slides (px)
padding: 32, // Track padding (px)
slideWidth: 420, // Slide width (px)
speed: 1000, // Transition speed (ms)
easing: "cubic-bezier(...)", // CSS easing function
focusCenter: true, // Center active slide
mobileOffset: -17.5, // Mobile positioning offset
cloneCount: null // Number of clones (auto if null)
},
// Autoplay
autoplay: {
enabled: true, // Enable autoplay
interval: 2000, // Advance interval (ms)
pauseOnHover: false // Pause on hover (not yet implemented)
},
// Color extraction
colors: {
enabled: true, // Enable color extraction
sampleSize: 100, // Image downsample size
cacheColors: true // Cache extracted colors
},
// Dynamic text color
dynamicText: {
enabled: true, // Enable dynamic text color
updateInterval: 3000, // Update frequency (ms)
contrastThreshold: 7, // Contrast threshold for black text
safariWhiteText: true // Force white text in Safari
},
// Gradient animations
gradients: {
enabled: true, // Enable gradient animations
duration: 0.9, // Animation duration (seconds)
colorPairs: {
start: "vibrant", // Start color type
end: "dvibrant" // End color type
}
// Available color types:
// dominant, vibrant, muted, lvibrant, lmuted,
// dvibrant, dmuted, ldominant, ddominant
},
// Card backgrounds
cardBackgrounds: {
enabled: true, // Enable card backgrounds
batchSize: 4, // Process cards in batches
darkenWeight: 0.67 // Blend weight (0-1, 0=black, 1=vibrant)
},
// Responsive
responsive: {
mobile: {
breakpoint: 478, // Mobile breakpoint (px)
slideWidth: "70vw", // Slide width (number or vw string)
gap: 48,
padding: 64
}
},
// Debug
debug: false // Enable console logging
}