3

I'm trying to make a simple draggable background using background-position and percentage values. I managed to get the drag working so far but I can't seem to find the right calculation for the image to follow the cursor at the same speed (if it makes sense).

Here is a simple example (using only the x axis):

const container = document.querySelector('div');
const containerSize = container.getBoundingClientRect();

let imagePosition = { x: 50, y: 50 };
let cursorPosBefore = { x: 0, y: 0 };
let imagePosBefore = null;
let imagePosAfter = imagePosition;

container.addEventListener('mousedown', function(event) {
  cursorPosBefore = { x: event.clientX, y: event.clientY };
  imagePosBefore = imagePosAfter; // Get current image position
});

container.addEventListener('mousemove', function(event) {
  if (event.buttons === 0) return;

  let newXPos = imagePosBefore.x + ((cursorPosBefore.x - event.clientX) * 100 / containerSize.width);
  newXPos = (newXPos < 0) ? 0 : (newXPos > 100) ? 100 : newXPos; // Stop at the end of the image
  
  imagePosAfter = { x: newXPos, y: imagePosition.y }; // Save position
  container.style.backgroundPosition = `${newXPos}% ${imagePosition.y}%`;
});
div {
  width: 400px;
  height: 400px;
  background-position: 50% 50%;
  background-size: cover;
  background-repeat: no-repeat;
  background-image: url('https://i.stack.imgur.com/5yqL8.png');
  cursor: move;
  border: 2px solid transparent;
}

div:active {
  border-color: red;
}
<div></div>

If I click on one of the white cross on the background and move the mouse then the cross should always remains under the cursor until I reach either the end of the image or the end of the container.

It's probably just a math problem but I'm a bit confused because of how percentages work with background-position. Any idea?

Arkellys
  • 5,562
  • 2
  • 16
  • 40

1 Answers1

5

I dont know how the formula is exactly, but you would have to include the size of the css-background too.

Your formula would work if the background size is 100% of the container divs size, but it is not. You would need to know the zoom level (which could be calculated from the size in relation to the divs size).

Here is the formula for when you have your zoom level:

container.addEventListener('mousemove', function(event) {
    event.preventDefault();

    const zoomAffector = (currentZoomLevel - 100) / 100; // 100% zoom = image as big as container
    if (zoomAffector <= 0) return; // Cant drag a image that is zoomed out

    let newXPos = imagePosBefore.x + ((imagePosBefore.x - event.pageX) / zoomAffector * 100;);
    newXPos = (newXPos < 0) ? 0 : (newXPos > 100) ? 100 : newXPos;
  
    imagePosAfter = { x: newXPos, y: imagePosition.y };
    container.style.backgroundPosition = `${newXPos}% ${imagePosition.y}%`;
});

To get the size of the css-background image, maybe checkout this question: Get the Size of a CSS Background Image Using JavaScript?

Here is my try on your problem using one of the answers from the linked question to get the image width (also did it for y-axis, to be complete):

const container = document.querySelector('div');
const containerSize = container.getBoundingClientRect();

let imagePosition = { x: 50, y: 50 };
let cursorPosBefore = { x: 0, y: 0 };
let imagePosBefore = null;
let imagePosAfter = imagePosition;


var actualImage = new Image();
actualImage.src = $('#img').css('background-image').replace(/"/g,"").replace(/url\(|\)$/ig, "");
actualImage.onload = function() {
    const zoomX = this.width / containerSize.width - 1;
    const zoomY = this.height / containerSize.height - 1;
    
    container.addEventListener('mousedown', function(event) {
      cursorPosBefore = { x: event.clientX, y: event.clientY };
      imagePosBefore = imagePosAfter; // Get current image position
    });

    container.addEventListener('mousemove', function(event) {
        event.preventDefault();

        if (event.buttons === 0) return;

        let newXPos = imagePosBefore.x + ((cursorPosBefore.x - event.clientX) / containerSize.width * 100 / zoomX);
        newXPos = (newXPos < 0) ? 0 : (newXPos > 100) ? 100 : newXPos;
        let newYPos = imagePosBefore.y + ((cursorPosBefore.y - event.clientY) / containerSize.height * 100 / zoomY);
        newYPos = (newYPos < 0) ? 0 : (newYPos > 100) ? 100 : newYPos;

        imagePosAfter = { x: newXPos, y: newYPos };
        container.style.backgroundPosition = `${newXPos}% ${newYPos}%`;
    });
}
#img {
  width: 400px;
  height: 200px;
  background-position: 50% 50%;
  background-image: url('https://i.stack.imgur.com/5yqL8.png');
  cursor: move;
  border: 2px solid transparent;
}

#img:active {
  border-color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="img"></div>

Or a bit more cleaned up:

const container = document.querySelector('div');
const containerSize = container.getBoundingClientRect();

let imagePosition = { x: 50, y: 50 };
let cursorPosBefore = { x: 0, y: 0 };
let imagePosBefore = null;
let imagePosAfter = imagePosition;

// Helpers
const minMax = (pos) => (pos < 0) ? 0 : (pos > 100) ? 100 : pos;
const setNewCenter = (x, y) => {
  imagePosAfter = { x: x, y: y }; 
  container.style.backgroundPosition = `${x}% ${y}%`;
};

const getImageZoom = () => {
  return new Promise((resolve, reject) => {
    let actualImage = new Image();
    actualImage.src = $('#img').css('background-image').replace(/"/g,"").replace(/url\(|\)$/ig, "");
    actualImage.onload = function() {
      resolve({
        x: zoomX = this.width / containerSize.width - 1,
        y: zoomY = this.height / containerSize.height - 1
      });
    }
  });
}
    
const addEventListeners = (zoomLevels) => {container.addEventListener('mousedown', function(event) {
      cursorPosBefore = { x: event.clientX, y: event.clientY };
      imagePosBefore = imagePosAfter; // Get current image position
    });

    container.addEventListener('mousemove', function(event) {
        event.preventDefault();

        if (event.buttons === 0) return;

        let newXPos = imagePosBefore.x + ((cursorPosBefore.x - event.clientX) / containerSize.width * 100 / zoomLevels.x);
        let newYPos = imagePosBefore.y + ((cursorPosBefore.y - event.clientY) / containerSize.height * 100 / zoomLevels.y);

        setNewCenter(minMax(newXPos), minMax(newYPos));
    });
};

getImageZoom().then(zoom => addEventListeners(zoom));
    
#img {
  width: 400px;
  height: 200px;
  background-position: 50% 50%;
  background-image: url('https://i.stack.imgur.com/5yqL8.png');
  cursor: move;
  border: 2px solid transparent;
}

#img:active {
  border-color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="img"></div>

Or to answer your follow-up question:

const container = document.querySelector("div");
const containerSize = container.getBoundingClientRect();

let imagePosition = { x: 50, y: 50 };
let cursorPosBefore = { x: 0, y: 0 };
let imagePosBefore = null;
let imagePosAfter = imagePosition;

// Helpers
const minMax = (pos) => (pos < 0 ? 0 : pos > 100 ? 100 : pos);
const setNewCenter = (x, y) => {
  imagePosAfter = { x: x, y: y };
  container.style.backgroundPosition = `${x}% ${y}%`;
};

const getImageZoom = () => {
  return new Promise((resolve, reject) => {
    let actualImage = new Image();

    actualImage.src = $("#img")
      .css("background-image")
      .replace(/"/g, "")
      .replace(/url\(|\)$/gi, "");
    actualImage.onload = function () {
      const imgW = this.width,
        imgH = this.height,
        conW = containerSize.width,
        conH = containerSize.height,
        ratioW = imgW / conW,
        ratioH = imgH / conH;

      // Stretched to Height
      if (ratioH < ratioW) {
        resolve({
          x: imgW / (conW * ratioH) - 1,
          y: imgH / (conH * ratioH) - 1,
        });
      } else {
        // Stretched to Width
        resolve({
          x: imgW / (conW * ratioW) - 1,
          y: imgH / (conH * ratioW) - 1,
        });
      }
    };
  });
};

const addEventListeners = (zoomLevels) => {
  container.addEventListener("mousedown", function (event) {
    cursorPosBefore = { x: event.clientX, y: event.clientY };
    imagePosBefore = imagePosAfter; // Get current image position
  });

  container.addEventListener("mousemove", function (event) {
    event.preventDefault();

    if (event.buttons === 0) return;

    let newXPos =
      imagePosBefore.x +
      (((cursorPosBefore.x - event.clientX) / containerSize.width) * 100) /
        zoomLevels.x;
    let newYPos =
      imagePosBefore.y +
      (((cursorPosBefore.y - event.clientY) / containerSize.height) * 100) /
        zoomLevels.y;

    setNewCenter(minMax(newXPos), minMax(newYPos));
  });
};

getImageZoom().then((zoom) => addEventListeners(zoom));
#img {
  width: 400px;
  height: 200px;
  background-size: cover;
  background-position: 50% 50%;
  background-image: url('https://i.stack.imgur.com/5yqL8.png');
  cursor: move;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="img"></div>
MauriceNino
  • 6,214
  • 1
  • 23
  • 60
  • Oops I forgot to add the `background-size` property on the example (I edited the question to add it), the image takes the size of the container. I will check your code, thank you. (: – Arkellys Sep 28 '20 at 14:40
  • 1
    background size: conver makes no difference. Example will still work. @Arkellys You still need to take care of the zoom. – MauriceNino Sep 28 '20 at 14:48
  • Yes, that definitively the parameter I forgot! I can't test your code right know so I will come back later to accept your anwser if it works, thanks again! – Arkellys Sep 28 '20 at 14:54
  • No problem. @Arkellys – MauriceNino Sep 29 '20 at 07:56
  • Humm.. I might have accepted it too soon, it doesn't work with `background-size` set as `cover`. – Arkellys Sep 29 '20 at 12:27
  • I was talking about the principle - it still works. Just the zoom is calculated differently now, because there is a second parameter in effect - the size property. `cover` means that either the width of the image equals the width of the container or the height of the image equals the height of the container. With that information and some simple math, you could calculate the actual height and width of the image. If you can't do that, please ask a new question, because your question is answered correctly and follow-up questions should not change anything about the answer. – MauriceNino Sep 29 '20 at 12:41
  • Sorry for the misunderstanding, but I assumed that your solution was working for the example I gave, which is not the case. If you were only talking "about the principle" then maybe the complex examples you added are not necessary since they do not really solve the problem I have and are confusing. I also need to add that I only made this code snippet for SO, it is not my actual code. As for your last comment, I never said that getting the image/background size was a problem so I don't see why you think telling me how to do it is relevant here. – Arkellys Sep 29 '20 at 13:14
  • Furthermore, to the " I never said that getting the image/background size was a problem" - yes you never said that, but this exact thing is the only problem you really have. You need to determine the background size and calculate the zoom level from that. So I gave you links to find a solution for the `cover` problem. Therefore its pretty relevant here – MauriceNino Sep 29 '20 at 13:21
  • btw I added a snippet with a working solution for `cover` which I have created using a little research from the provided link. Hope this works for you, but in the future please open a new question if you decide to follow-up @Arkellys – MauriceNino Sep 29 '20 at 13:23
  • Yeah it has been a misunderstanding, what I meant is that the example provided still applies to your problem. It is still not cool to alter a question after a working solution was provided to it. But do what you want, there is no point in arguing any further. The exact solution for your updated problem is in my answer at the bottom - please check it out, so everything should be resolved for this question. @Arkellys – MauriceNino Sep 29 '20 at 13:49
  • big thanks for your solution it's perfect for my problem. – Cadot.eu Jan 18 '23 at 09:52