PageSourceSearch

https://veefriends.com/dist/GestureCarousel-DA3TDT3H.p39y5031qh.js

js veefriends.com collected 2026-09-25 14:35:32 UTC 21,945 bytes, 579 lines download raw bytes

1import {
2  animate,
3  spring
4} from "./chunk-CTQMBWFQ.js";
5import "./chunk-5WRI5ZAA.js";
6
7// ../VeeFriends.Web.Www.Host/wwwroot/utils/GestureCarousel.js
8var GestureCarousel = class {
9  constructor(elementOrSelector) {
10    this.container = typeof elementOrSelector === "string" ? document.querySelector(elementOrSelector) : elementOrSelector;
11    this.wrapper = this.container.firstElementChild;
12    this.items = Array.from(this.wrapper.children ?? []);
13    if (this.items.length < 1) {
14      throw "Carousel must have at least one child element";
15    }
16    this.itemWidth = this.calculateItemWidth();
17    this.isDragging = false;
18    this.startX = 0;
19    this.currentX = 0;
20    this.animationInstance = null;
21    this.autoPlaySpeed = -1;
22    this.lastTimestamp = 0;
23    this.lastDragX = 0;
24    this.lastDragTime = 0;
25    this.velocity = 0;
26    this.momentumMultiplier = 50;
27    this.originalItems = [...this.items];
28    this.nextItemIndex = 0;
29    this.setupDOM();
30    this.setupGestures();
31    this.setupResizeHandling();
32    this.startAutoPlay();
33  }
34  calculateItemWidth() {
35    const item = this.items[0];
36    const style = window.getComputedStyle(item);
37    const width = item.offsetWidth;
38    const marginLeft = parseFloat(style.marginLeft || "0");
39    const marginRight = parseFloat(style.marginRight || "0");
40    return width + marginLeft + marginRight;
41  }
42  /**
43   * Calculates and updates the boundaries to center characters within the container.
44   */
45  calculateBounds() {
46    const containerWidth = this.container.offsetWidth;
47    const totalContentWidth = this.originalItems.length * this.itemWidth;
48    const containerCenterX = containerWidth / 2;
49    const firstItemCenter = 0 * this.itemWidth + this.itemWidth / 2;
50    this.maxX = containerCenterX - firstItemCenter;
51    const lastItemCenter = (this.originalItems.length - 1) * this.itemWidth + this.itemWidth / 2;
52    this.minX = containerCenterX - lastItemCenter;
53    this.updateArrowStates();
54  }
55  /**
56   * Constrains the position within specified boundaries with elastic resistance.
57   *
58   * This function checks if the input position `x` exceeds the defined maximum (`maxX`) or falls below the minimum (`minX`).
59   * If `x` is greater than `maxX`, it applies a 30% resistance, pulling back towards `maxX`.
60   * Similarly, if `x` is less than `minX`, it applies a 30% resistance, pushing away from `minX`.
61   * If `x` is within the boundaries, it returns `x` unchanged.
62   *
63   * @param {number} x - The current position to be constrained.
64   */
65  constrainPosition(x) {
66    if (x > this.maxX) {
67      const excess = x - this.maxX;
68      return this.maxX + excess * 0.3;
69    }
70    if (x < this.minX) {
71      const excess = this.minX - x;
72      return this.minX - excess * 0.3;
73    }
74    return x;
75  }
76  /**
77   * Ensures a value is within specified bounds.
78   */
79  snapToBounds(x) {
80    return Math.max(this.minX, Math.min(this.maxX, x));
81  }
82  /**
83   * Resets and reinitializes the DOM by clearing, storing original items, and adding clones.
84   */
85  setupDOM() {
86    const originalItems = [...this.items];
87    this.wrapper.innerHTML = "";
88    originalItems.forEach((item) => {
89      this.wrapper.appendChild(item);
90    });
91    for (let i = 0; i < originalItems.length * 3; i++) {
92      const clone = originalItems[i % originalItems.length].cloneNode(true);
93      clone.setAttribute("aria-hidden", "true");
94      this.wrapper.appendChild(clone);
95    }
96    this.calculateBounds();
97    const initialCenterIndex = Math.min(9, this.originalItems.length - 1);
98    const containerCenterX = this.container.offsetWidth / 2;
99    const initialItemCenter = initialCenterIndex * this.itemWidth + this.itemWidth / 2;
100    this.currentX = containerCenterX - initialItemCenter;
101    this.currentX = this.snapToBounds(this.currentX);
102    this.wrapper.style.transform = `translateX(${this.currentX}px)`;
103    requestAnimationFrame(() => {
104      this.updateCenterFocus();
105    });
106  }
107  setupResizeHandling() {
108    let resizeTimeout;
109    const handleResize = () => {
110      clearTimeout(resizeTimeout);
111      resizeTimeout = setTimeout(() => {
112        this.itemWidth = this.calculateItemWidth();
113        this.checkAndRecycleItems();
114      }, 250);
115    };
116    this.resizeObserver = new ResizeObserver(handleResize);
117    this.resizeObserver.observe(this.container);
118    window.addEventListener("resize", handleResize);
119    this.handleResize = handleResize;
120  }
121  startAutoPlay() {
122    if (this.animationFrame) {
123      cancelAnimationFrame(this.animationFrame);
124    }
125    if (this.momentumAnimation) {
126      this.momentumAnimation.stop();
127    }
128    const animate2 = () => {
129      if (this.isDragging) return;
130      const currentTime = performance.now();
131      const deltaTime = currentTime - this.lastTimestamp;
132      this.lastTimestamp = currentTime;
133      this.currentX += this.autoPlaySpeed;
134      this.wrapper.style.transform = `translateX(${this.currentX}px)`;
135      this.checkAndRecycleItems();
136      this.animationFrame = requestAnimationFrame(animate2);
137    };
138    this.lastTimestamp = performance.now();
139    this.animationFrame = requestAnimationFrame(animate2);
140  }
141  checkAndRecycleItems() {
142    const containerRect = this.container.getBoundingClientRect();
143    const items = Array.from(this.wrapper.children);
144    items.forEach((item, index) => {
145      const itemRect = item.getBoundingClientRect();
146      if (itemRect.right < containerRect.left) {
147        const viewportWidth = containerRect.width;
148        const itemsNeeded = Math.ceil(viewportWidth / this.itemWidth) + 2;
149        const remainingItems = items.length - index;
150        if (remainingItems > itemsNeeded) {
151          this.wrapper.removeChild(item);
152          const nextClone = this.originalItems[this.nextItemIndex].cloneNode(true);
153          nextClone.setAttribute("aria-hidden", "true");
154          this.wrapper.appendChild(nextClone);
155          this.nextItemIndex = (this.nextItemIndex + 1) % this.originalItems.length;
156          this.currentX += this.itemWidth;
157          this.wrapper.style.transform = `translateX(${this.currentX}px)`;
158        }
159      }
160    });
161    const lastItemRect = this.wrapper.lastElementChild.getBoundingClientRect();
162    if (lastItemRect.right < containerRect.right + containerRect.width) {
163      const nextClone = this.originalItems[this.nextItemIndex].cloneNode(true);
164      nextClone.setAttribute("aria-hidden", "true");
165      this.wrapper.appendChild(nextClone);
166      this.nextItemIndex = (this.nextItemIndex + 1) % this.originalItems.length;
167    }
168  }
169  pauseAutoPlay() {
170    if (this.animationFrame) {
171      cancelAnimationFrame(this.animationFrame);
172      this.animationFrame = null;
173    }
174  }
175  /**
176   * Sets up event listeners for gesture controls and navigation.
177   */
178  setupGestures() {
179    this.wrapper.addEventListener("mousedown", this.dragStart.bind(this));
180    this.wrapper.addEventListener("touchstart", this.dragStart.bind(this));
181    document.addEventListener("mousemove", this.dragMove.bind(this));
182    document.addEventListener("touchmove", this.dragMove.bind(this));
183    document.addEventListener("mouseup", this.dragEnd.bind(this));
184    document.addEventListener("touchend", this.dragEnd.bind(this));
185    document.addEventListener("mouseleave", this.dragEnd.bind(this));
186    this.container.addEventListener("mouseenter", () => {
187      this.pauseAutoPlay();
188    });
189    this.setupArrowNavigation();
190    this.updateCenterFocus();
191    this.container.addEventListener("mouseleave", () => {
192      if (!this.isDragging) {
193        this.startAutoPlay();
194      }
195    });
196  }
197  /**
198   * Sets up event listeners and updates arrow states for navigation.
199   *
200   * This function locates the previous and next buttons within a container,
201   * binds click events to them, and ensures that the arrow states are updated accordingly.
202   * It checks if the necessary elements exist before setting up the listeners.
203   */
204  setupArrowNavigation() {
205    const containerId = this.container.id;
206    if (!containerId) return;
207    this.prevButton = document.getElementById(containerId + "_prev");
208    this.nextButton = document.getElementById(containerId + "_next");
209    if (this.prevButton) {
210      this.prevButton.addEventListener(
211        "click",
212        this.scrollToPrevious.bind(this)
213      );
214    }
215    if (this.nextButton) {
216      this.nextButton.addEventListener("click", this.scrollToNext.bind(this));
217    }
218    this.updateArrowStates();
219  }
220  /**
221   * Scrolls to the previous item in the carousel.
222   */
223  scrollToPrevious() {
224    if (this.isDragging) return;
225    const containerRect = this.container.getBoundingClientRect();
226    const containerCenterX = containerRect.left + containerRect.width / 2;
227    const items = Array.from(this.wrapper.children);
228    let currentCenterIndex = 0;
229    let closestDistance = Infinity;
230    items.forEach((item, index) => {
231      const itemRect = item.getBoundingClientRect();
232      const itemCenterX = itemRect.left + itemRect.width / 2;
233      const distance = Math.abs(itemCenterX - containerCenterX);
234      if (distance < closestDistance) {
235        closestDistance = distance;
236        currentCenterIndex = index;
237      }
238    });
239    const currentOriginalIndex = currentCenterIndex % this.originalItems.length;
240    const targetOriginalIndex = Math.max(0, currentOriginalIndex - 1);
241    const containerCenterXPos = this.container.offsetWidth / 2;
242    const targetItemCenter = targetOriginalIndex * this.itemWidth + this.itemWidth / 2;
243    const targetX = containerCenterXPos - targetItemCenter;
244    const snappedX = this.snapToBounds(targetX);
245    this.animateToPosition(snappedX);
246  }
247  /**
248   * Scrolls to the next item in the carousel.
249   */
250  scrollToNext() {
251    if (this.isDragging) return;
252    const containerRect = this.container.getBoundingClientRect();
253    const containerCenterX = containerRect.left + containerRect.width / 2;
254    const items = Array.from(this.wrapper.children);
255    let currentCenterIndex = 0;
256    let closestDistance = Infinity;
257    items.forEach((item, index) => {
258      const itemRect = item.getBoundingClientRect();
259      const itemCenterX = itemRect.left + itemRect.width / 2;
260      const distance = Math.abs(itemCenterX - containerCenterX);
261      if (distance < closestDistance) {
262        closestDistance = distance;
263        currentCenterIndex = index;
264      }
265    });
266    const currentOriginalIndex = currentCenterIndex % this.originalItems.length;
267    const targetOriginalIndex = Math.min(
268      this.originalItems.length - 1,
269      currentOriginalIndex + 1
270    );
271    const containerCenterXPos = this.container.offsetWidth / 2;
272    const targetItemCenter = targetOriginalIndex * this.itemWidth + this.itemWidth / 2;
273    const targetX = containerCenterXPos - targetItemCenter;
274    const snappedX = this.snapToBounds(targetX);
275    this.animateToPosition(snappedX);
276  }
277  /**
278   * Animates the wrapper element to a new horizontal position with an ease-out effect.
279   *
280   * This function cancels any ongoing animations, calculates the distance and duration,
281   * and then animates the movement using `requestAnimationFrame`. The animation uses an ease-out
282   * cubic timing function to smooth the transition. After the animation completes, it updates
283   * the arrow states and center focus.
284   */
285  animateToPosition(targetX) {
286    if (!isFinite(targetX) || !isFinite(this.currentX)) {
287      console.warn(
288        "GestureCarousel: Invalid position values detected in animateToPosition"
289      );
290      return;
291    }
292    if (this.momentumAnimationId) {
293      cancelAnimationFrame(this.momentumAnimationId);
294    }
295    const startX = this.currentX;
296    const distance = Math.abs(targetX - startX);
297    if (distance < 1) return;
298    const duration = 400;
299    const startTime = performance.now();
300    const animateFrame = (currentTime) => {
301      try {
302        const elapsed = currentTime - startTime;
303        const progress = Math.min(elapsed / duration, 1);
304        const easeOut = 1 - Math.pow(1 - progress, 3);
305        const x = startX + (targetX - startX) * easeOut;
306        if (isFinite(x)) {
307          this.currentX = x;
308          this.wrapper.style.transform = `translateX(${x}px)`;
309          this.updateCenterFocus();
310        }
311        if (progress < 1) {
312          this.momentumAnimationId = requestAnimationFrame(animateFrame);
313        } else {
314          this.currentX = targetX;
315          this.wrapper.style.transform = `translateX(${this.currentX}px)`;
316          this.momentumAnimationId = null;
317          this.updateArrowStates();
318          this.updateCenterFocus();
319        }
320      } catch (error) {
321        console.warn("GestureCarousel: Animation frame error:", error);
322        this.currentX = targetX;
323        this.wrapper.style.transform = `translateX(${this.currentX}px)`;
324        this.momentumAnimationId = null;
325        this.updateArrowStates();
326        this.updateCenterFocus();
327      }
328    };
329    this.momentumAnimationId = requestAnimationFrame(animateFrame);
330  }
331  /**
332   * Updates the states of the arrow buttons based on scroll position bounds.
333   */
334  updateArrowStates() {
335    if (this.prevButton) {
336      this.prevButton.disabled = this.currentX >= this.maxX - 5;
337    }
338    if (this.nextButton) {
339      this.nextButton.disabled = this.currentX <= this.minX + 5;
340    }
341  }
342  /**
343   * Updates the focus styling of items within a container to ensure uniform size and opacity.
344   */
345  updateCenterFocus() {
346    const containerRect = this.container.getBoundingClientRect();
347    const containerCenterX = containerRect.left + containerRect.width / 2;
348    const items = Array.from(this.wrapper.children);
349    items.forEach((item) => {
350      item.classList.remove(
351        "scale-[1.15]",
352        "opacity-100",
353        "z-20",
354        "scale-75",
355        "opacity-60",
356        "z-10"
357      );
358      item.classList.add("opacity-100", "z-10");
359      const characterImage = item.querySelector(".character-image");
360      const characterName = item.querySelector(".character-name");
361      if (characterImage) {
362        characterImage.classList.remove("scale-[1.15]");
363      }
364      if (characterName) {
365        characterName.classList.remove("text-lg", "font-bold", "px-4", "py-2");
366      }
367    });
368  }
369  // Calculate the position needed to center a specific original item
370  /**
371   * Calculates the center position of an item within a container.
372   */
373  getItemCenterPosition(originalItemIndex) {
374    const containerCenterX = this.container.offsetWidth / 2;
375    const itemCenterOffset = originalItemIndex * this.itemWidth + this.itemWidth / 2;
376    return containerCenterX - itemCenterOffset;
377  }
378  // Find the closest item to center and snap to it
379  /**
380   * Snaps the current position to the nearest center of an item within the container.
381   */
382  snapToNearestCenter() {
383    const containerRect = this.container.getBoundingClientRect();
384    const containerCenterX = containerRect.left + containerRect.width / 2;
385    const items = Array.from(this.wrapper.children);
386    let closestIndex = 0;
387    let closestDistance = Infinity;
388    items.forEach((item, index) => {
389      const itemRect = item.getBoundingClientRect();
390      const itemCenterX = itemRect.left + itemRect.width / 2;
391      const distance = Math.abs(itemCenterX - containerCenterX);
392      if (distance < closestDistance) {
393        closestDistance = distance;
394        closestIndex = index;
395      }
396    });
397    const originalIndex = closestIndex % this.originalItems.length;
398    const containerCenterXPos = this.container.offsetWidth / 2;
399    const targetItemCenter = originalIndex * this.itemWidth + this.itemWidth / 2;
400    const targetX = containerCenterXPos - targetItemCenter;
401    const snappedX = this.snapToBounds(targetX);
402    if (Math.abs(this.currentX - snappedX) > 5) {
403      this.animateToPosition(snappedX);
404    }
405  }
406  /**
407   * Handles the start of a drag event.
408   */
409  dragStart(event) {
410    if (event.target.closest("button") || event.target.closest("a")) {
411      this.isDragging = false;
412      return;
413    }
414    const startPosition = this.getPositionX(event);
415    if (!isFinite(startPosition)) {
416      console.warn("GestureCarousel: Invalid start position detected");
417      return;
418    }
419    this.isDragging = true;
420    this.startX = startPosition;
421    this.lastDragX = this.startX;
422    this.lastDragTime = performance.now();
423    this.velocity = 0;
424    this.wrapper.classList.add("cursor-grabbing");
425    this.wrapper.classList.remove("cursor-grab");
426    this.pauseAutoPlay();
427    try {
428      const transform = getComputedStyle(this.wrapper).transform;
429      const matrix = new DOMMatrix(transform);
430      const transformX = matrix.m41;
431      if (isFinite(transformX)) {
432        this.currentX = transformX;
433      }
434    } catch (error) {
435      console.warn(
436        "GestureCarousel: Failed to get current transform, using existing currentX:",
437        error
438      );
439    }
440  }
441  dragMove(event) {
442    if (!this.isDragging) return;
443    event.preventDefault();
444    const currentPosition = this.getPositionX(event);
445    const currentTime = performance.now();
446    if (!isFinite(currentPosition) || !isFinite(currentTime)) {
447      console.warn("GestureCarousel: Invalid drag position values detected");
448      return;
449    }
450    const dt = Math.max(1, currentTime - this.lastDragTime);
451    const dx = currentPosition - this.lastDragX;
452    if (!isFinite(dx) || !isFinite(dt)) {
453      return;
454    }
455    const newVelocity = dx / dt;
456    if (isFinite(newVelocity)) {
457      this.velocity = this.velocity * 0.7 + newVelocity * 0.3;
458    }
459    this.lastDragX = currentPosition;
460    this.lastDragTime = currentTime;
461    const diff = currentPosition - this.startX;
462    if (isFinite(diff)) {
463      this.currentX = this.currentX + diff;
464      this.startX = currentPosition;
465      if (isFinite(this.currentX)) {
466        this.wrapper.style.transform = `translateX(${this.currentX}px)`;
467        this.checkAndRecycleItems();
468      }
469    }
470  }
471  dragEnd() {
472    if (!this.isDragging) return;
473    this.isDragging = false;
474    this.wrapper.classList.add("cursor-grab");
475    this.wrapper.classList.remove("cursor-grabbing");
476    const momentumDistance = this.velocity * this.momentumMultiplier * (Math.abs(this.velocity) < 0.5 ? 0 : 1);
477    const targetX = this.currentX + momentumDistance;
478    if (this.momentumAnimation) {
479      this.momentumAnimation.stop();
480    }
481    const startX = this.currentX;
482    if (!isFinite(startX) || !isFinite(targetX) || !isFinite(this.velocity)) {
483      console.warn(
484        "GestureCarousel: Invalid animation values detected, falling back to auto-play"
485      );
486      requestAnimationFrame(() => {
487        if (!this.isDragging) {
488          this.startAutoPlay();
489        }
490      });
491      return;
492    }
493    if (Math.abs(targetX - startX) < 1) {
494      requestAnimationFrame(() => {
495        if (!this.isDragging) {
496          this.startAutoPlay();
497        }
498      });
499      return;
500    }
501    try {
502      this.momentumAnimation = animate(
503        this.wrapper,
504        {
505          x: [startX, targetX]
506          // Explicitly define start and end points
507        },
508        {
509          duration: Math.min(Math.abs(momentumDistance) * 2, 1e3),
510          // Duration based on distance
511          easing: spring({
512            stiffness: 100,
513            damping: 25,
514            mass: 1,
515            restSpeed: 1e-3,
516            restDelta: 0.01
517          }),
518          onUpdate: ({ x }) => {
519            if (isFinite(x)) {
520              this.currentX = x;
521              this.wrapper.style.transform = `translateX(${x}px)`;
522              this.checkAndRecycleItems();
523            }
524          },
525          onComplete: () => {
526            this.currentX = targetX;
527            this.wrapper.style.transform = `translateX(${this.currentX}px)`;
528            requestAnimationFrame(() => {
529              if (!this.isDragging) {
530                this.startAutoPlay();
531              }
532            });
533          }
534        }
535      );
536    } catch (error) {
537      console.warn(
538        "GestureCarousel: Animation failed, falling back to direct positioning:",
539        error
540      );
541      this.currentX = targetX;
542      this.wrapper.style.transform = `translateX(${this.currentX}px)`;
543      requestAnimationFrame(() => {
544        if (!this.isDragging) {
545          this.startAutoPlay();
546        }
547      });
548    }
549  }
550  getPositionX(event) {
551    return event.type.includes("mouse") ? event.pageX : event.touches[0].clientX;
552  }
553  destroy() {
554    this.pauseAutoPlay();
555    if (this.momentumAnimation) {
556      this.momentumAnimation.stop();
557    }
558    if (this.resizeObserver) {
559      this.resizeObserver.disconnect();
560    }
561    window.removeEventListener("resize", this.handleResize);
562    this.wrapper.removeEventListener("mousedown", this.dragStart);
563    this.wrapper.removeEventListener("touchstart", this.dragStart);
564    document.removeEventListener("mousemove", this.dragMove);
565    document.removeEventListener("touchmove", this.dragMove);
566    document.removeEventListener("mouseup", this.dragEnd);
567    document.removeEventListener("touchend", this.dragEnd);
568    document.removeEventListener("mouseleave", this.dragEnd);
569    this.wrapper.innerHTML = "";
570    this.originalItems.forEach((item) => this.wrapper.appendChild(item));
571    this.wrapper.style.transform = "";
572    this.wrapper.classList.remove("cursor-grabbing");
573    this.wrapper.classList.remove("cursor-grab");
574  }
575};
576export {
577  GestureCarousel
578};
579//# sourceMappingURL=GestureCarousel-DA3TDT3H.js.map

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.