p5.js:如何缩放到 canvas 上的某个点?

p5.js: How do I zoom to a point on the canvas?

在下面的 p5.js 代码中,我试图创建 2 个独立的方法。

centerInWindow() 是为了在用户单击 canvas.

后缩小图像时,将图像保持在 canvas 的中心

centerToClick() 是为了让图像在用户点击的点上居中,同时它正在被放大。

None 他们的工作,我有问题得到正确的逻辑。

function centerInWindow(img) {
  let currentSize = img.width * currentScale
  imgX = (windowWidth / 2) - (currentSize / 2) 
  imgY = (windowHeight / 2) - (currentSize / 2)
}

function centerToClick() {
  imgX = clickX * currentScale
  imgY = clickY * currentScale
}

let minScale = 1
let maxScale = 5
let targetScale = minScale
let currentScale = targetScale

let clickX, clickY, imgX, imgY

let idx = 0

function setup() {
  pixelDensity(1)
  createCanvas(windowWidth, windowHeight)
  preload(IMG_PATHS, IMGS)
  frameRate(12)  
}

function draw() { 
  clear()
  
  if (currentScale < targetScale) {
    currentScale += 0.05
    if (currentScale > targetScale) {
      currentScale = targetScale
    }
    centerToClick()
  } else if (currentScale > targetScale) {
    currentScale -= 0.05
    if (currentScale < targetScale) {
      currentScale = targetScale
    }
    centerInWindow(IMGS[idx])
  } else {
    centerInWindow(IMGS[idx])
  }
    
  scale(currentScale)
    
  image(IMGS[idx], imgX, imgY)
      
  idx++
  
  if (idx === IMGS.length) {
    idx = 0
  }  
}

window.addEventListener('click', function({ clientX, clientY }) {
  targetScale = targetScale === maxScale ? minScale : maxScale
  
  clickX = clientX
  clickY = clientY
})

查看实际效果 here

如有任何帮助,我们将不胜感激。

可能有多种方法可以解决这个问题,但这里有一个:

假设您的视口是一个 NxM 矩形,并且您正在该视口内绘制场景的一部分。为了放大和缩小,您可以移动绘制该场景的原点并增大或减小比例。棘手的部分是能够以场景当前可见部分内的任意点为中心进行放大和缩小,使场景中的该点锁定到视口中的当前点。

给定一些中心点和所需的比例因子,可以确定场景偏移的必要变化以在缩放后保持中心点的位置。

关于如何计算这个可能有一些复杂的三角证明,但方便的是根据鼠标与当前场景左上角的偏移[=58]的比率进行简单计算=],到场景的缩放高度

x_offset -= (x_center - x_offset) / (N * current_scale) * (N * new_scale - N * current_scale)
y_offset -= (y_center - y_offset) / (M * current_scale) * (M * new_scale - M * current_scale)

方便的是,无论比例是增加还是减少,都可以在比例变化时重复应用此方法。

这是演示此内容的示例草图:

const viewport = { width: 400, height: 300 };
let scaledView = { ...viewport, x: 0, y: 0 };

function setup() {
  createCanvas(windowWidth, windowHeight);
  viewport.x = (width - viewport.width) / 2;
  viewport.y = (height - viewport.height) / 2;
}

function draw() {
  background(255);
  translate(viewport.x, viewport.y);
  
  push();
  translate(scaledView.x, scaledView.y);
  scale(scaledView.width / viewport.width, scaledView.height / viewport.height);
  // Draw scene
  ellipseMode(CENTER);
  noStroke();
  fill(200);
  rect(0, 0, viewport.width, viewport.height);
  
  stroke('blue');
  noFill();
  strokeWeight(1);
  translate(viewport.width / 2, viewport.height / 2);
  circle(0, 0, 200);
  arc(0, 0, 120, 120, PI * 0.25, PI * 0.75);
  strokeWeight(4)
  point(-40, -40);
  point(40, -40);
  pop();
  
  noFill();
  stroke(0);
  rect(0, 0, viewport.width, viewport.height);
  
  // viewport relative mouse position
  let mousePos = { x: mouseX - viewport.x, y: mouseY - viewport.y };
  
  if (mousePos.x >= 0 && mousePos.x <= viewport.width &&
      mousePos.y >= 0 && mousePos.y <= viewport.height) {
    
    line(scaledView.x, scaledView.y, mousePos.x, mousePos.y);
    
    let updatedView = keyIsDown(SHIFT) ? getUnZoomedView(mousePos) : getZoomedView(mousePos);
    
    line(scaledView.x, scaledView.y, updatedView.x, updatedView.y);
    stroke('red');
    rect(updatedView.x, updatedView.y, updatedView.width, updatedView.height);
  }
}

function getZoomedView(center) {
  return getScaledView(center, 1.1);
}

function getUnZoomedView(center) {
  return getScaledView(center, 0.9);
}

function getScaledView(center, factor) {
  // the center position relative to the scaled/shifted scene
  let viewCenterPos = {
    x: center.x - scaledView.x,
    y: center.y - scaledView.y
  };

  // determine how much we will have to shift to keep the position centered
  let shift = {
    x: map(viewCenterPos.x, 0, scaledView.width, 0, 1),
    y: map(viewCenterPos.y, 0, scaledView.height, 0, 1)
  };

  // calculate the new view dimensions
  let updatedView = {
    width: scaledView.width * factor,
    height: scaledView.height * factor
  };

  // adjust the x and y offsets according to the shift
  updatedView.x = scaledView.x + (updatedView.width - scaledView.width) * -shift.x;
  updatedView.y = scaledView.y + (updatedView.height - scaledView.height) * -shift.y;
  
  return updatedView;
}

function mouseClicked() {
  // viewport relative mouse position
  let mousePos = { x: mouseX - viewport.x, y: mouseY - viewport.y };
  
  if (mousePos.x >= 0 && mousePos.x <= viewport.width &&
      mousePos.y >= 0 && mousePos.y <= viewport.height) {
    scaledView = keyIsDown(SHIFT) ? getUnZoomedView(mousePos) : getZoomedView(mousePos);
  }
}
html, body {
margin: 0;
padding: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>

我们实际上可以简化这些方程式,因为可以分解出 N 和 M。之前的伪代码变为:

x_offset -= ((x_center - x_offset) * (new_scale - current_scale) * N) / (current_scale * N)

并且因为顶部和底部都乘以 N 这变成:

x_offset -= ((x_center - x_offset) * (new_scale - current_scale)) / (current_scale)

这是一个使用您的图像绘制代码的示例:

const IMG_PATHS = [
  'https://storage.googleapis.com/www.paulwheeler.us/files/windows-95-desktop-background.jpg'
];

let IMGS = [];

const minScale = 1;
const maxScale = 5;

let targetScale = minScale;
let currentScale = targetScale;
let targetOrigin = {
  x: 0,
  y: 0
};
let currentOrigin = {
  x: 0,
  y: 0
};

let idx = 0;

function preloadHelper(pathsToImgs, imgs) {
  for (let pathToImg of pathsToImgs) {
    loadImage(pathToImg, img => {
      imgs.push(img);
    })
  }
}

// Make sure load images happen from the actual preload() function. p5.js has special logic when these calls happen here to have the sketch wait to start until all the loadXXX class are complete
function preload() {
  preloadHelper(IMG_PATHS, IMGS);
}

function setup() {
  pixelDensity(1)
  createCanvas(windowWidth, windowHeight)

  frameRate(12)
}

function draw() {
  clear();

  /*
  if (currentScale < targetScale) {
    currentScale += 0.01
  } else if (currentScale > targetScale) {
    currentScale -= 0.01
  } */

  // By making all of the changing components part of a vector and normalizing it we can ensure that the we reach our target origin and scale at the same point
  let offset = createVector(
    targetOrigin.x - currentOrigin.x,
    targetOrigin.y - currentOrigin.y,
    // Give the change in scale more weight so that it happens at a similar rate to the translation. This is especially noticable when there is little to no offset required
    (targetScale - currentScale) * 500
  );
  if (offset.magSq() > 0.01) {
    // Multiplying by a larger number will move faster
    offset.normalize().mult(8);
    currentOrigin.x += offset.x;
    currentOrigin.y += offset.y;
    currentScale += offset.z / 500;

    // We need to make sure we do not over shoot or targets
    if (offset.x > 0 && currentOrigin.x > targetOrigin.x) {
      currentOrigin.x = targetOrigin.x;
    }
    if (offset.x < 0 && currentOrigin.x < targetOrigin.x) {
      currentOrigin.x = targetOrigin.x;
    }
    if (offset.y > 0 && currentOrigin.y > targetOrigin.y) {
      currentOrigin.y = targetOrigin.y;
    }
    if (offset.y < 0 && currentOrigin.y < targetOrigin.y) {
      currentOrigin.y = targetOrigin.y;
    }
    if (offset.z > 0 && currentScale > targetScale) {
      currentScale = targetScale;
    }
    if (offset.z < 0 && currentScale < targetScale) {
      currentScale = targetScale;
    }
  }

  translate(currentOrigin.x, currentOrigin.y);
  scale(currentScale);

  image(IMGS[idx], 0, 0);
}

function mouseClicked() {
  targetScale = constrain(
    keyIsDown(SHIFT) ? currentScale * 0.9 : currentScale * 1.1,
    minScale,
    maxScale
  );

  targetOrigin = getScaledOrigin({
      x: mouseX,
      y: mouseY
    },
    currentScale,
    targetScale
  );
}

function getScaledOrigin(center, currentScale, newScale) {
  // the center position relative to the scaled/shifted scene
  let viewCenterPos = {
    x: center.x - currentOrigin.x,
    y: center.y - currentOrigin.y
  };

  // determine the new origin
  let originShift = {
    x: viewCenterPos.x / currentScale * (newScale - currentScale),
    y: viewCenterPos.y / currentScale * (newScale - currentScale)
  };

  return {
    x: currentOrigin.x - originShift.x,
    y: currentOrigin.y - originShift.y
  };
}
html,
body {
  margin: 0;
  padding: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>