Why is the distance between first and last element decreasing?

Viewed 129

I'm trying to make an image slider. But as you can see the distance between the first and last element is not consistent. If you keep on dragging to left, the distance decreases and if you keep on dragging to right, the distance increases. Looks like the code is behaving differently on different zoom levels (sometimes?) and hence distance between every elements is changing at times.

//project refers to placeholder rectangular divs

projectContainer = document.querySelector(".project-container")
projects = document.querySelectorAll(".project")

elementAOffset = projects[0].offsetLeft;
elementBOffset = projects[1].offsetLeft;
elementAWidth = parseInt(getComputedStyle(projects[0]).width)
margin = (elementBOffset - (elementAOffset + elementAWidth))

LeftSideBoundary = -(elementAWidth)
RightSideBoundary = (elementAWidth * (projects.length)) + (margin * (projects.length))
RightSidePosition = RightSideBoundary - elementAWidth;

initialPosition = 0; //referring to mouse 
mouseIsDown = false

projectContainer.addEventListener("mousedown", e => {
    mouseIsDown = true
    initialPosition = e.clientX;
})

projectContainer.addEventListener("mouseup", e => {
    mouseExit(e)
})

projectContainer.addEventListener("mouseleave", e => {
    mouseExit(e);
})

function mouseExit(e) {
    mouseIsDown = false

    //updates translateX value of transform
    projects.forEach(project => {
        var style = window.getComputedStyle(project)
        project.currentTranslationX = (new WebKitCSSMatrix(style.webkitTransform)).m41
        project.style.transform = 'translateX(' + (project.currentTranslationX) + 'px)'
    })
}

projectContainer.addEventListener("mousemove", e => {
    if (!mouseIsDown) { return };

    // adds mousemovement to translateX
    projects.forEach(project => {
        project.style.transform = 'translateX(' + ((project.currentTranslationX ?? 0) + (e.clientX - initialPosition)) + 'px)'
        shiftPosition(e, project)
    })
})

//teleports div if it hits left or right boundary to make an infinite loop
function shiftPosition(e, project) {
    projectStyle = window.getComputedStyle(project)
    projectTranslateX = (new WebKitCSSMatrix(projectStyle.webkitTransform)).m41
    //projectVisualPosition is relative to the left border of container div
    projectVisualPosition = project.offsetLeft + projectTranslateX

    if (projectVisualPosition <= LeftSideBoundary) {
        project.style.transform = "translateX(" + ((RightSidePosition - project.offsetLeft)) + "px)"
        updateTranslateX(e);
    }
    if (projectVisualPosition >= RightSidePosition) {
        newPosition = -1 * (project.offsetLeft + elementAWidth)
        project.style.transform = "translateX(" + newPosition + "px)"
        updateTranslateX(e);
    }
}

function updateTranslateX(e) {
    projects.forEach(project => {
        style = window.getComputedStyle(project)
        project.currentTranslationX = (new WebKitCSSMatrix(style.webkitTransform)).m41

        project.style.transform = 'translateX(' + (project.currentTranslationX) + 'px)'
        initialPosition = e.clientX
    })
}
 *, *::before, *::after{
        margin:0px;
        padding:0px;
        box-sizing: border-box;
        font-size:0px;
        user-select: none;
    }
    
    .project-container{
        font-size: 0px;
        position: relative;
        width:1500px;
        height:400px;
        background-color: rgb(15, 207, 224);
        margin:auto;
        margin-top:60px;
        white-space: nowrap;
        overflow: hidden;
        padding-left:40px;
        padding-right:40px;
    }
    
    .project{
        font-size:100px;
        margin:40px;
        display: inline-block;
        height:300px;
        width:350px;
        background-color:red;
        border: black 3px solid;
        user-select: none;
    }
        <div class="project-container">
            <div class="project">1</div>
            <div class="project">2</div>
            <div class="project">3</div>
            <div class="project">4</div>
            <div class="project">5</div>
            <div class="project">6</div>
            <div class="project">7</div>
            <div class="project">8</div>
        </div>

1 Answers

I'm not sure exactly how you would go about fixing your implementation. I played around with it for a while and discovered a few things; dragging more quickly makes the displacement worse, and the displacement seems to happen mainly when the elements are teleported at each end of the container.

I would guess that the main reason for this is that you are looping over all the elements and spacing them individually. Mouse move events generally happen under 20ms apart, and you are relying on all the DOM elements being repainted with their new transform positions before the next move is registered.

I did come up with a different approach using absolutely placed elements and the IntersectionObserver API, which is now supported in all modern browsers. The idea here is basically that when each element intersects with the edge of the container, it triggers an array lookup to see if the next element in the sequence is on the correct end and moves it there if not. Elements are only ever spaced by a static variable, while the job of sliding them is passed up to a new parent wrapper .project-slider.

window.addEventListener('DOMContentLoaded', () => {
  // Style variables
  const styles = {
    width: 350,
    margin: 40
  };
  const space = styles.margin*2 + styles.width;

  // Document variables
  const projectContainer = document.querySelector(".project-container");
  const projectSlider = document.querySelector(".project-slider");
  const projects = Array.from(document.querySelectorAll(".project"));

  // Mouse interactions
  let dragActive = false;
  let prevPos = 0;

  projectContainer.addEventListener('mousedown', e => {
    dragActive = true;
    prevPos = e.clientX;
  });

  projectContainer.addEventListener('mouseup', () => dragActive = false);

  projectContainer.addEventListener('mouseleave', () => dragActive = false);

  projectContainer.addEventListener('mousemove', e => {
    if (!dragActive) return;

    const newTrans = projectSlider.currentTransX + e.clientX - prevPos;

    projectSlider.style.transform = `translateX(${newTrans}px)`;
    projectSlider.currentTransX = newTrans;
    prevPos = e.clientX;
  });
  
  // Generate initial layout
  function init() {
    let workingLeft = styles.margin;

    projects.forEach((project, i) => {
      if (i === projects.length - 1) {
        project.style.left = `-${space - styles.margin}px`;
      } else {
        i !== 0 && (workingLeft += space);
        project.style.left = `${workingLeft}px`;
      };
    });

    projectSlider.currentTransX = 0;
  };

  // Intersection observer
  function observe() {

    const callback = (entries, observer) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {

          // Find intersecting edge
          const { left } = entry.boundingClientRect;
          const isLeftEdge = left < projectContainer.clientWidth - left;

          // Test and reposition next element
          const targetIdx = projects.findIndex(project => project === entry.target);
          let nextIdx = null;
          const nextEl = () => projects[nextIdx];

          const targetLeft = parseInt(entry.target.style.left);
          const nextLeft = () => parseInt(nextEl().style.left);

          if (isLeftEdge) {
            nextIdx = targetIdx === 0 ? projects.length-1 : targetIdx - 1;
            nextLeft() > targetLeft && (nextEl().style.left = `${targetLeft - space}px`);
          } else {
            nextIdx = targetIdx === projects.length-1 ? 0 : targetIdx + 1;
            nextLeft() < targetLeft && (nextEl().style.left = `${targetLeft + space}px`);
          };
        };
      });
    };

    const observer = new IntersectionObserver(callback, {root: projectContainer});

    projects.forEach(project => observer.observe(project));
  };

  init();
  observe();
});
*, *::before, *::after{
    margin:0px;
    padding:0px;
    box-sizing: border-box;
    font-size:0px;
    user-select: none;
}
    
.project-container {
    font-size: 0px;
    width: 100%;
    height: 400px;
    background-color: rgb(15, 207, 224);
    margin:auto;
    margin-top:60px;
    white-space: nowrap;
    overflow: hidden;
}

.project-slider {
  position: relative;
}
    
.project {
    font-size:100px;
    display: block;
    position: absolute;
    top: 40px;
    height:300px;
    width:350px;
    background-color:red;
    border: black 3px solid;
    user-select: none;
}
<div class="project-container">
  <div class="project-slider">
    <div class="project">1</div>
    <div class="project">2</div>
    <div class="project">3</div>
    <div class="project">4</div>
    <div class="project">5</div>
    <div class="project">6</div>
    <div class="project">7</div>
    <div class="project">8</div>
  </div>
</div>

There is still an issue here which is how to resize the elements for smaller screens, and on browser resizes. You would have to add another event listener for window resizes which resets the positions and styles at certain breakpoints, and also determine the style variables programmatically when the page first loads. I believe this would still have been a partial issue with the original implementation so you'd have to address it at some point either way.

Related