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.