peek-carousel 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +238 -0
  3. package/README.md +234 -0
  4. package/dist/peek-carousel.css +1 -0
  5. package/dist/peek-carousel.esm.js +1368 -0
  6. package/dist/peek-carousel.esm.js.map +1 -0
  7. package/dist/peek-carousel.esm.min.js +8 -0
  8. package/dist/peek-carousel.esm.min.js.map +1 -0
  9. package/dist/peek-carousel.js +1376 -0
  10. package/dist/peek-carousel.js.map +1 -0
  11. package/dist/peek-carousel.min.css +1 -0
  12. package/dist/peek-carousel.min.js +8 -0
  13. package/dist/peek-carousel.min.js.map +1 -0
  14. package/examples/example-built.html +367 -0
  15. package/examples/example.css +2216 -0
  16. package/examples/example.js +404 -0
  17. package/examples/example.min.css +1 -0
  18. package/examples/example.min.js +1 -0
  19. package/package.json +92 -0
  20. package/src/core/PeekCarousel.js +294 -0
  21. package/src/core/config.js +49 -0
  22. package/src/core/constants.js +73 -0
  23. package/src/modules/Animator.js +244 -0
  24. package/src/modules/AutoRotate.js +43 -0
  25. package/src/modules/EventHandler.js +390 -0
  26. package/src/modules/Navigator.js +116 -0
  27. package/src/modules/UIManager.js +170 -0
  28. package/src/peek-carousel.d.ts +34 -0
  29. package/src/styles/base/_accessibility.scss +16 -0
  30. package/src/styles/base/_mixins.scss +7 -0
  31. package/src/styles/base/_variables.scss +75 -0
  32. package/src/styles/components/_carousel.scss +5 -0
  33. package/src/styles/components/_counter.scss +109 -0
  34. package/src/styles/components/_indicators.scss +154 -0
  35. package/src/styles/components/_navigation.scss +193 -0
  36. package/src/styles/components/carousel/_carousel-base.scss +99 -0
  37. package/src/styles/components/carousel/_carousel-classic.scss +76 -0
  38. package/src/styles/components/carousel/_carousel-mixins.scss +18 -0
  39. package/src/styles/components/carousel/_carousel-radial.scss +72 -0
  40. package/src/styles/components/carousel/_carousel-stack.scss +84 -0
  41. package/src/styles/components/carousel/_carousel-variables.scss +118 -0
  42. package/src/styles/peek-carousel.scss +11 -0
  43. package/src/utils/dom.js +53 -0
  44. package/src/utils/helpers.js +46 -0
  45. package/src/utils/icons.js +92 -0
  46. package/src/utils/preloader.js +69 -0
  47. package/types/index.d.ts +34 -0
@@ -0,0 +1,294 @@
1
+ import { validateOptions } from './config.js';
2
+ import { SELECTORS, CLASS_NAMES } from './constants.js';
3
+ import { getElement, getElements } from '../utils/dom.js';
4
+ import { preloadImagesInRange } from '../utils/preloader.js';
5
+ import { injectIcon, injectAutoRotateIcons } from '../utils/icons.js';
6
+ import { Navigator } from '../modules/Navigator.js';
7
+ import { Animator } from '../modules/Animator.js';
8
+ import { AutoRotate } from '../modules/AutoRotate.js';
9
+ import { EventHandler } from '../modules/EventHandler.js';
10
+ import { UIManager } from '../modules/UIManager.js';
11
+
12
+ const FULL_CIRCLE_DEGREES = 360;
13
+
14
+ class PeekCarousel {
15
+ constructor(selector, options = {}) {
16
+ this.container = getElement(selector);
17
+ if (!this.container) {
18
+ throw new Error(`PeekCarousel: 셀렉터 "${selector}"에 해당하는 컨테이너를 찾을 수 없습니다`);
19
+ }
20
+
21
+ this.options = validateOptions(options);
22
+ this.initElements();
23
+
24
+ if (this.items.length === 0) {
25
+ throw new Error('PeekCarousel: 캐러셀 아이템을 찾을 수 없습니다');
26
+ }
27
+
28
+ this.state = {
29
+ currentIndex: this.options.startIndex,
30
+ angleUnit: FULL_CIRCLE_DEGREES / this.totalItems,
31
+ };
32
+
33
+ this.initModules();
34
+ this.init();
35
+ }
36
+
37
+ initElements() {
38
+ this.elements = {
39
+ carousel: this.container.querySelector(SELECTORS.carousel),
40
+ prevBtn: null,
41
+ nextBtn: null,
42
+ autoRotateBtn: null,
43
+ controls: null,
44
+ nav: null,
45
+ };
46
+
47
+ this.items = getElements(SELECTORS.item, this.container);
48
+ this.totalItems = this.items.length;
49
+ this.indicators = [];
50
+ }
51
+
52
+ initModules() {
53
+ this.navigator = new Navigator(this);
54
+ this.animator = new Animator(this);
55
+ this.autoRotate = new AutoRotate(this);
56
+ this.eventHandler = new EventHandler(this);
57
+ this.ui = new UIManager(this);
58
+ }
59
+
60
+ init() {
61
+ this.updateLayoutClass();
62
+ this.createNavigation();
63
+ this.createControls();
64
+ this.injectIcons();
65
+ this.createCounter();
66
+ this.setImageLoadingAttributes();
67
+ this.initCSSVariables();
68
+
69
+ this.eventHandler.init();
70
+ this.animator.updateCarousel();
71
+
72
+ if (this.options.autoRotate) {
73
+ this.autoRotate.start();
74
+ }
75
+
76
+ if (this.options.preloadRange > 0) {
77
+ this.preloadImages();
78
+ }
79
+ }
80
+
81
+ initCSSVariables() {
82
+ this.container.style.setProperty('--carousel-rotation', '0deg');
83
+ this.container.style.setProperty('--drag-offset', '0px');
84
+ this.container.style.setProperty('--drag-rotation', '0deg');
85
+ this.container.style.setProperty('--drag-rotation-y', '0deg');
86
+ }
87
+
88
+ createNavigation() {
89
+ if (!this.options.showNavigation) return;
90
+
91
+ const existingNav = this.container.querySelector(`.${CLASS_NAMES.nav}`);
92
+ if (existingNav) {
93
+ this.elements.nav = existingNav;
94
+ this.elements.prevBtn = existingNav.querySelector(SELECTORS.prevBtn);
95
+ this.elements.nextBtn = existingNav.querySelector(SELECTORS.nextBtn);
96
+ return;
97
+ }
98
+
99
+ const nav = document.createElement('div');
100
+ nav.className = CLASS_NAMES.nav;
101
+
102
+ const prevBtn = document.createElement('button');
103
+ prevBtn.className = `${CLASS_NAMES.navBtn} ${CLASS_NAMES.btn} ${CLASS_NAMES.prevBtn}`;
104
+ prevBtn.setAttribute('aria-label', 'Previous');
105
+
106
+ const nextBtn = document.createElement('button');
107
+ nextBtn.className = `${CLASS_NAMES.navBtn} ${CLASS_NAMES.btn} ${CLASS_NAMES.nextBtn}`;
108
+ nextBtn.setAttribute('aria-label', 'Next');
109
+
110
+ nav.appendChild(prevBtn);
111
+ nav.appendChild(nextBtn);
112
+ this.container.appendChild(nav);
113
+
114
+ this.elements.nav = nav;
115
+ this.elements.prevBtn = prevBtn;
116
+ this.elements.nextBtn = nextBtn;
117
+ }
118
+
119
+ createControls() {
120
+ if (!this.options.showIndicators && !this.options.showAutoRotateButton) return;
121
+
122
+ const existingControls = this.container.querySelector(`.${CLASS_NAMES.controls}`);
123
+ if (existingControls) {
124
+ this.elements.controls = existingControls;
125
+ const indicatorsWrapper = existingControls.querySelector(`.${CLASS_NAMES.indicators}`);
126
+ if (indicatorsWrapper && this.options.showIndicators) {
127
+ indicatorsWrapper.innerHTML = '';
128
+ this.createIndicators(indicatorsWrapper);
129
+ }
130
+ this.elements.autoRotateBtn = existingControls.querySelector(SELECTORS.autoRotateBtn);
131
+ return;
132
+ }
133
+
134
+ const controls = document.createElement('div');
135
+ controls.className = CLASS_NAMES.controls;
136
+
137
+ if (this.options.showIndicators) {
138
+ const indicatorsWrapper = document.createElement('div');
139
+ indicatorsWrapper.className = CLASS_NAMES.indicators;
140
+ this.createIndicators(indicatorsWrapper);
141
+ controls.appendChild(indicatorsWrapper);
142
+ }
143
+
144
+ if (this.options.showAutoRotateButton) {
145
+ const autoRotateBtn = document.createElement('button');
146
+ autoRotateBtn.className = `${CLASS_NAMES.autoRotateBtn} ${CLASS_NAMES.btn} ${CLASS_NAMES.btnAutoRotate}`;
147
+ autoRotateBtn.setAttribute('aria-label', 'Toggle auto-rotate');
148
+ autoRotateBtn.setAttribute('aria-pressed', 'false');
149
+ controls.appendChild(autoRotateBtn);
150
+ this.elements.autoRotateBtn = autoRotateBtn;
151
+ }
152
+
153
+ this.container.appendChild(controls);
154
+ this.elements.controls = controls;
155
+ }
156
+
157
+ createIndicators(wrapper) {
158
+ this.indicators = [];
159
+
160
+ for (let i = 0; i < this.totalItems; i++) {
161
+ const indicator = document.createElement('button');
162
+ const isActive = i === this.state.currentIndex;
163
+
164
+ indicator.className = CLASS_NAMES.indicator;
165
+ indicator.classList.add(CLASS_NAMES.indicatorPeek);
166
+ indicator.setAttribute('role', 'tab');
167
+ indicator.setAttribute('aria-label', `Image ${i + 1}`);
168
+ indicator.setAttribute('aria-selected', isActive ? 'true' : 'false');
169
+ indicator.setAttribute('tabindex', isActive ? '0' : '-1');
170
+
171
+ if (isActive) {
172
+ indicator.classList.add(CLASS_NAMES.indicatorActive);
173
+ }
174
+
175
+ wrapper.appendChild(indicator);
176
+ this.indicators.push(indicator);
177
+ }
178
+ }
179
+
180
+ injectIcons() {
181
+ const { prevBtn, nextBtn, autoRotateBtn } = this.elements;
182
+
183
+ if (prevBtn) injectIcon(prevBtn, 'prev');
184
+ if (nextBtn) injectIcon(nextBtn, 'next');
185
+ if (autoRotateBtn) injectAutoRotateIcons(autoRotateBtn);
186
+ }
187
+
188
+ createCounter() {
189
+ if (!this.options.showCounter) return;
190
+
191
+ const existingCounter = this.container.querySelector(`.${CLASS_NAMES.counter}`);
192
+ if (existingCounter) {
193
+ this.counterElement = existingCounter;
194
+ this.updateCounter();
195
+ return;
196
+ }
197
+
198
+ const counter = document.createElement('div');
199
+ counter.className = CLASS_NAMES.counter;
200
+ counter.setAttribute('aria-live', 'polite');
201
+ counter.setAttribute('aria-atomic', 'true');
202
+
203
+ counter.innerHTML = `
204
+ <span class="${CLASS_NAMES.counterCurrent}">${this.state.currentIndex + 1}</span>
205
+ <span class="${CLASS_NAMES.counterSeparator}">/</span>
206
+ <span class="${CLASS_NAMES.counterTotal}">${this.totalItems}</span>
207
+ `;
208
+
209
+ this.container.appendChild(counter);
210
+ this.counterElement = counter;
211
+ }
212
+
213
+ updateCounter() {
214
+ if (!this.counterElement) return;
215
+
216
+ const currentSpan = this.counterElement.querySelector(`.${CLASS_NAMES.counterCurrent}`);
217
+ if (currentSpan) {
218
+ currentSpan.textContent = this.state.currentIndex + 1;
219
+ }
220
+ }
221
+
222
+ setImageLoadingAttributes() {
223
+ const { startIndex } = this.options;
224
+ const preloadRange = this.options.preloadRange || 1;
225
+
226
+ for (let index = 0; index < this.items.length; index++) {
227
+ const item = this.items[index];
228
+ const img = item.querySelector(`.${CLASS_NAMES.image}`);
229
+ if (!img || img.hasAttribute('loading')) continue;
230
+
231
+ const distance = Math.abs(index - startIndex);
232
+ const isNearby = distance <= preloadRange;
233
+ img.setAttribute('loading', isNearby ? 'eager' : 'lazy');
234
+ }
235
+ }
236
+
237
+ updateLayoutClass() {
238
+ const currentMode = this.currentLayoutMode;
239
+ const newMode = this.options.layoutMode;
240
+
241
+ if (currentMode && currentMode !== newMode) {
242
+ this.container.classList.remove(`peek-carousel--${currentMode}`);
243
+ }
244
+
245
+ this.container.classList.add(`peek-carousel--${newMode}`);
246
+ this.currentLayoutMode = newMode;
247
+ }
248
+
249
+ preloadImages() {
250
+ preloadImagesInRange(this.items, this.state.currentIndex, this.options.preloadRange);
251
+ }
252
+
253
+ // [개발참고] Public API
254
+ next() {
255
+ this.navigator.next();
256
+ }
257
+
258
+ prev() {
259
+ this.navigator.prev();
260
+ }
261
+
262
+ goTo(index) {
263
+ this.navigator.goTo(index);
264
+ }
265
+
266
+ startAutoRotate() {
267
+ this.autoRotate.start();
268
+ }
269
+
270
+ stopAutoRotate() {
271
+ this.autoRotate.stop();
272
+ }
273
+
274
+ toggleAutoRotate() {
275
+ this.autoRotate.toggle();
276
+ }
277
+
278
+ destroy() {
279
+ this.autoRotate.destroy();
280
+ this.animator.stopMomentum();
281
+ this.eventHandler.destroy();
282
+ this.ui.destroy();
283
+ }
284
+
285
+ get currentIndex() {
286
+ return this.state.currentIndex;
287
+ }
288
+
289
+ get isAutoRotating() {
290
+ return this.autoRotate.isActive;
291
+ }
292
+ }
293
+
294
+ export default PeekCarousel;
@@ -0,0 +1,49 @@
1
+ export const LAYOUT_MODES = Object.freeze({
2
+ STACK: 'stack',
3
+ RADIAL: 'radial',
4
+ CLASSIC: 'classic',
5
+ });
6
+
7
+ export const DEFAULT_OPTIONS = Object.freeze({
8
+ startIndex: 1,
9
+ layoutMode: LAYOUT_MODES.STACK,
10
+ autoRotate: false,
11
+ autoRotateInterval: 2500,
12
+ preloadRange: 2,
13
+ swipeThreshold: 50,
14
+ dragThreshold: 80,
15
+ enableKeyboard: true,
16
+ enableWheel: true,
17
+ enableTouch: true,
18
+ enableMouse: true,
19
+ showNavigation: true,
20
+ showCounter: true,
21
+ showIndicators: true,
22
+ showAutoRotateButton: true,
23
+ });
24
+
25
+ export function validateOptions(options) {
26
+ const validated = { ...DEFAULT_OPTIONS, ...options };
27
+
28
+ if (validated.startIndex < 0) {
29
+ console.warn('PeekCarousel: startIndex는 0 이상이어야 합니다. 기본값 1 사용');
30
+ validated.startIndex = 1;
31
+ }
32
+
33
+ if (!Object.values(LAYOUT_MODES).includes(validated.layoutMode)) {
34
+ console.warn(`PeekCarousel: 유효하지 않은 layoutMode "${validated.layoutMode}". 기본값 "stack" 사용`);
35
+ validated.layoutMode = LAYOUT_MODES.STACK;
36
+ }
37
+
38
+ if (validated.autoRotateInterval < 100) {
39
+ console.warn('PeekCarousel: autoRotateInterval은 100ms 이상이어야 합니다. 기본값 2500 사용');
40
+ validated.autoRotateInterval = 2500;
41
+ }
42
+
43
+ if (validated.preloadRange < 0) {
44
+ console.warn('PeekCarousel: preloadRange는 0 이상이어야 합니다. 기본값 2 사용');
45
+ validated.preloadRange = 2;
46
+ }
47
+
48
+ return validated;
49
+ }
@@ -0,0 +1,73 @@
1
+ export const CLASS_NAMES = Object.freeze({
2
+ carousel: 'peek-carousel',
3
+ track: 'peek-carousel__track',
4
+ item: 'peek-carousel__item',
5
+ itemActive: 'peek-carousel__item--active',
6
+ itemPrev: 'peek-carousel__item--prev',
7
+ itemNext: 'peek-carousel__item--next',
8
+ itemCenter: 'peek-carousel__item--center',
9
+ itemHidden: 'peek-carousel__item--hidden',
10
+ itemDraggingLeft: 'peek-carousel__item--dragging-left',
11
+ itemDraggingRight: 'peek-carousel__item--dragging-right',
12
+ figure: 'peek-carousel__figure',
13
+ image: 'peek-carousel__image',
14
+ caption: 'peek-carousel__caption',
15
+ nav: 'peek-carousel__nav',
16
+ navBtn: 'nav-btn',
17
+ btn: 'peek-carousel__btn',
18
+ prevBtn: 'prev-btn',
19
+ nextBtn: 'next-btn',
20
+ autoRotateBtn: 'auto-rotate-btn',
21
+ btnAutoRotate: 'peek-carousel__btn--auto-rotate',
22
+ btnActive: 'peek-carousel__btn--active',
23
+ controls: 'peek-carousel__controls',
24
+ indicators: 'peek-carousel__indicators',
25
+ indicator: 'indicator',
26
+ indicatorPeek: 'peek-carousel__indicator',
27
+ indicatorActive: 'peek-carousel__indicator--active',
28
+ indicatorProgress: 'peek-carousel__indicator--progress',
29
+ indicatorCompleted: 'peek-carousel__indicator--completed',
30
+ counter: 'peek-carousel__counter',
31
+ counterCurrent: 'peek-carousel__counter-current',
32
+ counterSeparator: 'peek-carousel__counter-separator',
33
+ counterTotal: 'peek-carousel__counter-total',
34
+ playIcon: 'play-icon',
35
+ pauseIcon: 'pause-icon',
36
+ });
37
+
38
+ export const SELECTORS = Object.freeze({
39
+ carousel: '.peek-carousel__track',
40
+ item: '.peek-carousel__item',
41
+ indicator: '.indicator',
42
+ prevBtn: '.prev-btn',
43
+ nextBtn: '.next-btn',
44
+ autoRotateBtn: '.auto-rotate-btn',
45
+ playIcon: '.play-icon',
46
+ pauseIcon: '.pause-icon',
47
+ image: 'img',
48
+ });
49
+
50
+ export const ARIA = Object.freeze({
51
+ current: 'aria-current',
52
+ selected: 'aria-selected',
53
+ pressed: 'aria-pressed',
54
+ label: 'aria-label',
55
+ tabindex: 'tabindex',
56
+ });
57
+
58
+ export const BREAKPOINTS = Object.freeze({
59
+ mobile: 768, // [개발참고] px
60
+ });
61
+
62
+ export const DURATIONS = Object.freeze({
63
+ transition: 500, // [개발참고] ms
64
+ progressReset: 10, // [개발참고] ms
65
+ });
66
+
67
+ export const KEYS = Object.freeze({
68
+ arrowLeft: 'ArrowLeft',
69
+ arrowRight: 'ArrowRight',
70
+ home: 'Home',
71
+ end: 'End',
72
+ space: ' ',
73
+ });
@@ -0,0 +1,244 @@
1
+ import { normalizeIndex } from '../utils/helpers.js';
2
+ import { CLASS_NAMES } from '../core/constants.js';
3
+
4
+ const RADIAL_RADIUS = 400;
5
+
6
+ const CLASSIC_POSITIONS = Object.freeze({
7
+ center: { x: 50, scale: 1 },
8
+ peek: { scale: 1 },
9
+ hidden: { scale: 0.85 },
10
+ });
11
+
12
+ const CLASSIC_SPACING = Object.freeze({
13
+ gapPercent: 5,
14
+ additionalMobile: 40,
15
+ additionalDesktop: 15,
16
+ mobileBreakpoint: 768,
17
+ });
18
+
19
+ const MOMENTUM_CONFIG = Object.freeze({
20
+ friction: 0.92,
21
+ minVelocity: 0.05,
22
+ navigationThreshold: 1.5,
23
+ dampingFactor: 0.6,
24
+ });
25
+
26
+ const STACK_POSITION_CLASSES = [
27
+ CLASS_NAMES.itemCenter,
28
+ CLASS_NAMES.itemPrev,
29
+ CLASS_NAMES.itemNext,
30
+ CLASS_NAMES.itemHidden,
31
+ ];
32
+
33
+ export class Animator {
34
+ constructor(carousel) {
35
+ this.carousel = carousel;
36
+ this.momentumAnimation = null;
37
+ this.isAnimating = false;
38
+ }
39
+
40
+ normalizeAngleDiff(diff) {
41
+ return ((diff + 180) % 360) - 180;
42
+ }
43
+
44
+ round(value, decimals = 2) {
45
+ return Math.round(value * 10 ** decimals) / 10 ** decimals;
46
+ }
47
+
48
+ getAdjacentIndices(currentIndex) {
49
+ return {
50
+ prev: normalizeIndex(currentIndex - 1, this.carousel.totalItems),
51
+ next: normalizeIndex(currentIndex + 1, this.carousel.totalItems),
52
+ };
53
+ }
54
+
55
+ setCarouselRotation(angle) {
56
+ const rounded = this.round(angle, 2);
57
+ this.carousel.container.style.setProperty('--carousel-rotation', `${rounded}deg`);
58
+ }
59
+
60
+ setCSSVariables(element, variables) {
61
+ for (const [key, value] of Object.entries(variables)) {
62
+ element.style.setProperty(key, value);
63
+ }
64
+ }
65
+
66
+ updateRadialRotation(currentIndex) {
67
+ const targetAngle = -this.carousel.state.angleUnit * currentIndex;
68
+ const currentRotation = this.carousel.container.style.getPropertyValue('--carousel-rotation');
69
+
70
+ if (!currentRotation || currentRotation === '0deg') {
71
+ this.setCarouselRotation(targetAngle);
72
+ return;
73
+ }
74
+
75
+ // [개발참고] 최단 경로 계산: -180 ~ 180 범위로 정규화
76
+ const currentAngle = parseFloat(currentRotation);
77
+ const diff = this.normalizeAngleDiff(targetAngle - currentAngle);
78
+ const finalAngle = currentAngle + diff;
79
+ this.setCarouselRotation(finalAngle);
80
+ }
81
+
82
+ updateCarousel() {
83
+ const { currentIndex } = this.carousel.state;
84
+ const { layoutMode } = this.carousel.options;
85
+
86
+ if (layoutMode === 'stack' || layoutMode === 'classic') {
87
+ this.setCarouselRotation(0);
88
+ } else if (layoutMode === 'radial') {
89
+ this.updateRadialRotation(currentIndex);
90
+ }
91
+
92
+ this.updateActiveItem();
93
+ }
94
+
95
+ updateActiveItem() {
96
+ const { currentIndex } = this.carousel.state;
97
+ const { layoutMode } = this.carousel.options;
98
+
99
+ this.carousel.ui.updateActiveStates(currentIndex);
100
+
101
+ if (layoutMode === 'radial') {
102
+ this.updateRadialPositions(currentIndex);
103
+ } else if (layoutMode === 'classic') {
104
+ this.updateClassicPositions(currentIndex);
105
+ } else {
106
+ this.updateStackPositions(currentIndex);
107
+ }
108
+ }
109
+
110
+ updateRadialPositions(currentIndex) {
111
+ const { angleUnit } = this.carousel.state;
112
+
113
+ for (let i = 0; i < this.carousel.items.length; i++) {
114
+ const item = this.carousel.items[i];
115
+ const angle = angleUnit * i;
116
+
117
+ this.setCSSVariables(item, {
118
+ '--item-angle': `${this.round(angle, 2)}deg`,
119
+ '--item-radius': `${RADIAL_RADIUS}px`,
120
+ });
121
+ }
122
+
123
+ const { prev, next } = this.getAdjacentIndices(currentIndex);
124
+ this.carousel.ui.setPeekItems(prev, next);
125
+ }
126
+
127
+ updateStackPositions(currentIndex) {
128
+ const { prev, next } = this.getAdjacentIndices(currentIndex);
129
+
130
+ for (let i = 0; i < this.carousel.items.length; i++) {
131
+ const item = this.carousel.items[i];
132
+
133
+ item.classList.remove(...STACK_POSITION_CLASSES);
134
+
135
+ if (i === currentIndex) {
136
+ item.classList.add(CLASS_NAMES.itemCenter);
137
+ } else if (i === prev) {
138
+ item.classList.add(CLASS_NAMES.itemPrev);
139
+ } else if (i === next) {
140
+ item.classList.add(CLASS_NAMES.itemNext);
141
+ } else {
142
+ item.classList.add(CLASS_NAMES.itemHidden);
143
+ }
144
+ }
145
+ }
146
+
147
+ calculateClassicSpacing(containerWidth) {
148
+ const itemWidth = Math.max(300, Math.min(containerWidth * 0.35, 500));
149
+ const isMobile = containerWidth <= CLASSIC_SPACING.mobileBreakpoint;
150
+
151
+ const itemHalfPercent = (itemWidth / containerWidth) * 50;
152
+ const baseSpacing = itemHalfPercent + CLASSIC_SPACING.gapPercent;
153
+ const additionalSpacing = isMobile
154
+ ? CLASSIC_SPACING.additionalMobile
155
+ : CLASSIC_SPACING.additionalDesktop;
156
+
157
+ return baseSpacing + additionalSpacing;
158
+ }
159
+
160
+ getClassicItemPosition(itemIndex, currentIndex, itemSpacing) {
161
+ const { prev, next } = this.getAdjacentIndices(currentIndex);
162
+
163
+ if (itemIndex === currentIndex) {
164
+ return {
165
+ x: CLASSIC_POSITIONS.center.x,
166
+ scale: CLASSIC_POSITIONS.center.scale,
167
+ };
168
+ }
169
+
170
+ if (itemIndex === prev) {
171
+ return {
172
+ x: CLASSIC_POSITIONS.center.x - itemSpacing,
173
+ scale: CLASSIC_POSITIONS.peek.scale,
174
+ };
175
+ }
176
+
177
+ if (itemIndex === next) {
178
+ return {
179
+ x: CLASSIC_POSITIONS.center.x + itemSpacing,
180
+ scale: CLASSIC_POSITIONS.peek.scale,
181
+ };
182
+ }
183
+
184
+ const distanceFromCurrent = itemIndex - currentIndex;
185
+ return {
186
+ x: distanceFromCurrent < 0
187
+ ? CLASSIC_POSITIONS.center.x - itemSpacing * 2
188
+ : CLASSIC_POSITIONS.center.x + itemSpacing * 2,
189
+ scale: CLASSIC_POSITIONS.hidden.scale,
190
+ };
191
+ }
192
+
193
+ updateClassicPositions(currentIndex) {
194
+ const { prev, next } = this.getAdjacentIndices(currentIndex);
195
+ const containerWidth = this.carousel.container.offsetWidth;
196
+ const itemSpacing = this.calculateClassicSpacing(containerWidth);
197
+
198
+ for (let i = 0; i < this.carousel.items.length; i++) {
199
+ const item = this.carousel.items[i];
200
+ const { x, scale } = this.getClassicItemPosition(i, currentIndex, itemSpacing);
201
+
202
+ this.setCSSVariables(item, {
203
+ '--item-x': `${this.round(x, 2)}%`,
204
+ '--item-scale': String(scale),
205
+ });
206
+ }
207
+
208
+ this.carousel.ui.setPeekItems(prev, next);
209
+ }
210
+
211
+ startMomentum(velocity) {
212
+ this.stopMomentum();
213
+
214
+ let currentVelocity = velocity;
215
+
216
+ const momentumStep = () => {
217
+ currentVelocity *= MOMENTUM_CONFIG.friction;
218
+
219
+ if (Math.abs(currentVelocity) < MOMENTUM_CONFIG.minVelocity) {
220
+ this.stopMomentum();
221
+ return;
222
+ }
223
+
224
+ if (Math.abs(currentVelocity) > MOMENTUM_CONFIG.navigationThreshold) {
225
+ const direction = currentVelocity > 0 ? -1 : 1;
226
+ this.carousel.navigator.rotate(direction);
227
+ currentVelocity *= MOMENTUM_CONFIG.dampingFactor;
228
+ }
229
+
230
+ this.momentumAnimation = requestAnimationFrame(momentumStep);
231
+ };
232
+
233
+ this.isAnimating = true;
234
+ this.momentumAnimation = requestAnimationFrame(momentumStep);
235
+ }
236
+
237
+ stopMomentum() {
238
+ if (this.momentumAnimation) {
239
+ cancelAnimationFrame(this.momentumAnimation);
240
+ this.momentumAnimation = null;
241
+ }
242
+ this.isAnimating = false;
243
+ }
244
+ }
@@ -0,0 +1,43 @@
1
+ export class AutoRotate {
2
+ constructor(carousel) {
3
+ this.carousel = carousel;
4
+ this.interval = null;
5
+ this.isActive = false;
6
+ }
7
+
8
+ setActiveState(isActive) {
9
+ this.isActive = isActive;
10
+ this.carousel.ui.updateAutoRotateButton(isActive);
11
+ }
12
+
13
+ toggle() {
14
+ this.isActive ? this.stop() : this.start();
15
+ }
16
+
17
+ start() {
18
+ if (this.isActive) return;
19
+
20
+ this.setActiveState(true);
21
+
22
+ const rotateInterval = this.carousel.options.autoRotateInterval;
23
+ this.interval = setInterval(() => {
24
+ this.carousel.navigator.next();
25
+ }, rotateInterval);
26
+ }
27
+
28
+ stop() {
29
+ if (!this.isActive) return;
30
+
31
+ this.setActiveState(false);
32
+
33
+ if (this.interval) {
34
+ clearInterval(this.interval);
35
+ this.interval = null;
36
+ }
37
+ }
38
+
39
+ destroy() {
40
+ this.stop();
41
+ this.carousel = null;
42
+ }
43
+ }