Image Is Not Panning Properly with JS Touch Events

The below code works great, there is only one problem. If the user is panning at a boundary, i.e. the user dragged the image all the way to the bottom and they swipe in a diagonally down/left of down/right direction, the image fails to pan on the x-axis. It’s sticking.

I have no idea how to solve this, I’ve tried everything and I’ve never been so stuck.

To test this just swipe in a large circular motion and it becomes clear how the image sticks.

const image = {
  startX: undefined,
  startY: undefined,
  currentX: undefined,
  currentY: undefined,
  scrollX: undefined,
  scrollY: undefined,
  currentOffsetX: undefined,
  currentOffsetY: undefined,
  maxScrollX: undefined,
  maxScrollY: undefined
}


const imageContainer = document.querySelector('.image-container')
const zoomImageWrapper = document.querySelector('.zoom-image-wrapper')
let isAlreadySwiped = false

const { x, y } = computeInitialOffset()
document.querySelector('.image-container').style.transform = `translate3d(-${x}px, -${y}px, 0)` 


image.maxScrollX = Math.abs(imageContainer.offsetWidth - zoomImageWrapper.offsetWidth)
image.maxScrollY = Math.abs(imageContainer.offsetHeight - zoomImageWrapper.offsetHeight)


function computeInitialOffset() {
  return {
    x: (imageContainer.offsetWidth - zoomImageWrapper.offsetWidth) / 2,
    y: (imageContainer.offsetHeight - zoomImageWrapper.offsetHeight) / 2
  }
}

function getImageOffsets(img) {
  return {
    imageOffsetX: Math.abs((img.getBoundingClientRect().left)),
    imageOffsetY: Math.abs((img.getBoundingClientRect().top))
  }
}

// zoom out on double tap
function resetOffset(img) {
  const { x, y } = computeInitialOffset()
  image.currentOffsetX = x
  image.currentOffsetY = y
}


function handleDragMove(e) {
  image.currentX = e.touches[0].pageX
  image.currentY = e.touches[0].pageY

  const swipingLeft = image.startX > image.currentX
  const swipingUp = image.startY > image.currentY

  if (isAlreadySwiped) {
    image.scrollX = Math.min(
      (image.currentOffsetX - (e.touches[0].pageX - image.startX)), 
      image.maxScrollX
      )

    image.scrollY = Math.min(
      (image.currentOffsetY - (e.touches[0].pageY - image.startY)),
      image.maxScrollY
    ) 
  } 
  else {
    const { x, y } = computeInitialOffset()

    image.scrollX = Math.min(
      (Math.abs(x) - (e.touches[0].pageX - image.startX)), 
      image.maxScrollX
    )
    
    image.scrollY = Math.min(
      (Math.abs(y) - (e.touches[0].pageY - image.startY)),
      image.maxScrollY
    )
  }
  
  imageContainer.style.transform = `translate3d(-${image.scrollX}px, -${image.scrollY}px, 0)`

}

function handleDragStart(e) {
  image.startX = e.touches[0].pageX
  image.startY = e.touches[0].pageY
}

function handleDragEnd(e) {
  image.currentOffsetX = getImageOffsets(imageContainer).imageOffsetX
  image.currentOffsetY = getImageOffsets(imageContainer).imageOffsetY
  isAlreadySwiped = true
}


imageContainer.addEventListener('touchstart', handleDragStart)
imageContainer.addEventListener('touchmove', handleDragMove)
imageContainer.addEventListener('touchend', handleDragEnd)
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Home</title>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <style>
      * {
        box-sizing: border-box;
      }

      .modal {
        height: 100%;
        width: 100%;
        z-index: 100000;
        position: fixed;
        display: flex;
        top: 0;
        left: 0;
        justify-content: center;
      }

      .modal__wrapper {
        display: flex;
        position: relative;
        background: #fff;
        flex-direction: column;
        height: 100%;
        width: 100%;
        align-items: center;
      }

      .modal__content {
        display: flex;
        flex-direction: column;
        overflow-x: hidden;
        overflow-y: scroll;
        padding: 0;
        margin: 0;
        height: 100%;
        white-space: nowrap; 
      }

      .zoom-container {
        position: relative;
        height: 100%;
      }

      .zoom-container-inner {
        position: absolute;
        z-index: 9;
        opacity: 1;
        top: 0;
        left: 0;
        height: 100%;
        width: 100%;

      }

      .image-container {
        position: relative;
        overflow: hidden;
      }

      .controls {
        display: flex;
        background: #fff;
        border: #ccc;
        width: 100%;
        flex: 0 0 auto;
        margin-top: auto;
        max-height: 73px;
        height: 73px;
      }

      .controls__inner {
        display: flex;
        justify-content: space-between;
        align-items: center;
        width: 100%;
        padding: 24px;
      }
    </style>
  </head>
  <body>
    
    <div class="modal">
      <div class="modal__wrapper">
        <div class="modal__content">
          <div class="zoom-container" style="margin: 0 auto; width: 375px;">
            <div class="zoom-container-inner">
              <div style="height: 100%; width: 100%;">
                  <div style="touch-action: none; height: 100%; width: 100%; overflow: hidden;" class="zoom-image-wrapper">
                    <div class="image-container"
                      style="overflow: hidden; will-change: transform; user-select: none; transform-origin: 0px 0px; transform: translate3d(0, 0, 0); padding-top: 144%; width: 610px; height: 880px;"
                    >
                      <div style="position: absolute; top: 0; left: 0; height: 100%; width: 100%;">
                        <img src="https://img01.ztat.net/article/spp-media-p1/3a301a3d8a274a18821af76f9a21bfe4/137fd73e910c45db8be381e6d8c72fee.jpg?imwidth=1800&filter=packshot" style="max-width: 100%; position: relative; text-align: center; width: 100%; height: auto; display: block;">
                      </div>
                    </div>
                  </div>
                </div>
              </div>
            </div>
        </div>
        

        <div style="max-height: 73px;" class="controls">
          <div class="controls__inner">
            <div>1 of 5</div>
            <div>x</div>
          </div>
        </div>

      </div>
    </div>

    <script src="./index.js" defer></script>
  </body>
</html>