p5js - Clamp Image in Pan/Zoom

Viewed 153

Using p5js, I'm working on a pan/zoom feature that would allow you to pan/zoom a image and then clamp the edges of the image. The main issue I'm having is with clamping the edges of the image so you can never drag the image past the edge of the container. For example:

Unclamped Clamped
enter image description here enter image description here

Notice the black background in the unclamped image. When I'm dragging the image, it should never allow me to drag the image corners past the edges of the canvas.

Here's how I'm currently handling the various events and p5js setup:

 /* eslint-disable no-undef, no-unused-vars */

var img;
var w, h, currentImageWidth, currentImageHeight;
var x, y, tox, toy;
var zoom = 0.01; //zoom step per mouse tick

function preload() {
  img = loadImage("https://i.imgur.com/MBFJhju.jpeg", () => {});
}

function setup() {
  createCanvas(600, 600);
  w = currentImageWidth = 600;
  h = currentImageHeight = 600;
  x = tox = w / 2;
  y = toy = h / 2;
}

function draw() {
  background(0);

  //tween/smooth motion
  x = lerp(x, tox, 0.1);
  y = lerp(y, toy, 0.1);
  w = lerp(w, currentImageWidth, 0.1);
  h = lerp(h, currentImageHeight, 0.1);

  image(img, x - w / 2, y - h / 2, w, h);
}

function mouseDragged() {
  let maxX = currentImageWidth;
  let minX = -1 * maxX;
  let maxY = currentImageHeight;
  let minY = -1 * maxY;
  let nextXposition = tox + mouseX - pmouseX;
  let nextYposition = toy + mouseY - pmouseY;
  if (
    nextXposition - currentImageWidth * 0.5 < maxX &&
    nextXposition - currentImageWidth * 0.5 > minX
  ) {
    tox += mouseX - pmouseX;
  }
  if (
    nextYposition - currentImageHeight * 0.5 < maxY &&
    nextYposition - currentImageHeight * 0.5 > minY
  ) {
    toy += mouseY - pmouseY;
  }
}

function mouseWheel(event) {
  var e = event.wheelDeltaY;
  console.log(event);
  if (e > 0) {
    //zoom in
    for (let i = 0; i < e; i++) {
      if (currentImageWidth > 3 * width) return; //max zoom
      tox -= zoom * (mouseX - tox);
      toy -= zoom * (mouseY - toy);
      currentImageWidth *= zoom + 1;
      currentImageHeight *= zoom + 1;
    }
  }

  if (e < 0) {
    //zoom out
    for (let i = 0; i < -e; i++) {
      if (currentImageWidth < width) return; //min zoom
      tox += (zoom / (zoom + 1)) * (mouseX - tox);
      toy += (zoom / (zoom + 1)) * (mouseY - toy);
      currentImageHeight /= zoom + 1;
      currentImageWidth /= zoom + 1;
    }
  }
  //adjust x and y if out of bounds
  let maxX = width * 0.5;
  let minX = -1 * maxX;
  let maxY = height * 0.5;
  let minY = -1 * maxY;
  let diff = 0;
  if (currentImageWidth + currentImageWidth * 0.5 <= maxX) {
    diff = maxX - (tox + currentImageWidth * 0.5);
    tox += diff;
  }
  if (currentImageWidth - currentImageWidth * 0.5 >= minX) {
    diff = currentImageWidth - currentImageWidth * 0.5 - minX;
    tox -= diff;
  }
  if (toy + currentImageHeight * 0.5 <= maxY) {
    diff = maxY - (toy + currentImageHeight * 0.5);
    toy += diff;
  }
  if (toy - currentImageHeight * 0.5 >= minY) {
    diff = toy - currentImageHeight * 0.5 - minY;
    toy -= diff;
  }

  return false;
}

Currently, the clamp doesn't actually work. But you can drag the image and zoom. My intentions are for this to work like pan/zoom on a map in a game.

I believe the main pain point would be the mouseDragged event handler, and I think the way I'll handle clamping with the mouseWheel event handler will work for zoom events.

Please let me know if you need any additional info!

Full demo:

https://codesandbox.io/embed/great-dirac-yvd0s?fontsize=14&hidenavigation=1&theme=dark

1 Answers

I recently answered a similar question on the processing.org forum.

  • In order to ensure that the left edge of the image is no further to the right than the left edge of the view area, the x position where the image is displayed cannot be any greater than the width of the image divided by two
  • In order to ensure that the right edge of the image is no further to the left than the right edge of the view area, the x position where the image is displayed cannot be any less than the width of the view area minus half the width of the image.
  • The logic for the vertical axis is the same. The y position of the image must be between the view height minus half the height of the image and half the width of the image.

var img;
var w, h, currentImageWidth, currentImageHeight;
var x, y, tox, toy;
var zoom = 0.01; //zoom step per mouse tick

function preload() {
  img = loadImage("https://i.imgur.com/MBFJhju.jpeg");
}

function setup() {
  createCanvas(300, 300);
  w = currentImageWidth = 300;
  h = currentImageHeight = 300;
  x = tox = w / 2;
  y = toy = h / 2;
}

function draw() {
  background(0);

  //tween/smooth motion
  x = lerp(x, tox, 0.1);
  y = lerp(y, toy, 0.1);
  w = lerp(w, currentImageWidth, 0.1);
  h = lerp(h, currentImageHeight, 0.1);

  image(img, x - w / 2, y - h / 2, w, h);
}

function mouseDragged() {
  let maxX = currentImageWidth / 2;
  let minX = width - maxX;
  let maxY = currentImageHeight / 2;
  let minY = height - maxY;
  
  console.log(JSON.stringify({minX, maxX,minY,maxY}));
  tox = constrain(tox + mouseX - pmouseX, minX, maxX);
  toy = constrain(toy + mouseY - pmouseY, minY, maxY);
}

function mouseWheel(event) {
  var delta = event.wheelDeltaY;
  // console.log(event);
  
  // TODO: figure out where on the image the mouse cursor is
  // Figure how how much that point will shift based on the scale
  // Adjust the tox and toy to compensate (thus keeping the part of the image where the mouse is stationary.
  
  currentImageWidth *= delta * zoom + 1;
  currentImageHeight *= delta * zoom + 1;
  
  // Check constraints
  if (delta > 0) {
    //zoom in
    if (currentImageWidth > 3 * width) {
      currentImageWidth = 3 * width;
      currentImageHeight = 3 * height;
      //max zoom
    }
  } else if (delta < 0) {
    //zoom out
    if (currentImageWidth < width) {
      //min zoom
      currentImageWidth = width;
      currentImageHeight = height;
    }
  }
  
  //adjust x and y if out of bounds
  let maxX = currentImageWidth / 2;
  let minX = width - maxX;
  let maxY = currentImageHeight / 2;
  let minY = height - maxY;
  
  tox = constrain(tox, minX, maxX);
  toy = constrain(toy, minY, maxY);

  return false;
}
html,
body {
  height: 100%;
  overflow: hidden;
  margin: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>

Related