Javascript Canvas : Apply zoom at given coordinate

2020-03-15 04:08发布

问题:

I'm struggling with an implementation of canvas zooming (on mouse wheel), using Vanilla Javascript, without success.

The zoom must be applied on the coordinates of the mouse, when the wheel is rolled. It's been asked here before, but my situation is quite different, considering I can't use canvasContext.translate , and the values of offsetX and offsetY must be kept in its absolute representation (not scaled)

I would really appreciate some light.

In the following snippet, I provide my current and dysfunctional implementation. You will see how it works when you zoom in and out keeping the same zoom coordinates, but as soon as you move the mouse to new coordinates (when scale != 1)and continue zooming, the new offsets become wrong.

// initiate variabks : canvas ref, offsets, scale...
const context   = document.getElementById('c').getContext('2d');
let scale       = 1;
let scaleFactor = 0.2;
let offsetX=0;
let offsetY=0;

// Handle mousenwheel zoom
context.canvas.onwheel= function(e){
  e.preventDefault();
  // calculate scale direction 6 new value
  let direction = e.deltaY > 0 ? 1 : -1;
  scale += scaleFactor * direction;
  // calculatethe new offsets (unscaled values)
  offsetX = e.offsetX - (e.offsetX  / scale);
  offsetY = e.offsetY - (e.offsetY / scale);  
  // apply new scale
  context.setTransform(1, 0, 0, 1, 0, 0);
  context.scale(scale, scale);
}

// clear canvas and draw two boxes
// NO CHANGES CAN BE DONE IN THIS FUNCTION
function render(){
  context.beginPath();
  context.clearRect(0,0,context.canvas.width/scale,context.canvas.height/scale);
  context.rect(100-offsetX,50-offsetY,50,50);
  context.rect(200-offsetX,50-offsetY,50,50);
  context.stroke();
  
  document.getElementById("info").innerHTML=`
    Scale : ${scale} <br>
    Offets : ${ Math.round(offsetX) + ' , ' + Math.round(offsetY)} <br>
  `;
  requestAnimationFrame( render );
}
render()
<canvas id="c" width="350" height="150" style="border: 1px solid red;"></canvas>
<div id="info"></div>

回答1:

I finally got it... canvas arbitrary coordinates scaling without using context.translate()

I attach my solution, with a little bonus : canvas panning (cursor keys scrolling). I hope becomes useful to somebody.

// initiate variables : canvas ref, offsets, scale...
const context   = document.getElementById('c').getContext('2d');
let scale       = 1;
let scaleFactor = 0.2;
let scrollX     = 0;
let scrollY     = 0;

let info        = document.getElementById("info");


// Handle mousenwheel zoom
context.canvas.onwheel =  function(e){
  e.preventDefault();
  let previousScale= scale;
  
  // calculate scale direction 6 new scale value
  let direction = e.deltaY > 0 ? 1 : -1;
  scale += scaleFactor * direction;

  // calculate the new scroll values
  scrollX += ( e.offsetX / previousScale )  - (e.offsetX  / scale);
  scrollY += ( e.offsetY / previousScale ) - ( e.offsetY / scale);
  
  // apply new scale in a non acumulative way
  context.setTransform(1, 0, 0, 1, 0, 0);
  context.scale(scale, scale);
}


// clear canvas and draw two boxes
function render(){
  context.beginPath();
  context.clearRect(0,0,context.canvas.width/scale, context.canvas.height/scale);
  context.rect(100-scrollX,50-scrollY,50,50);
  context.rect(200-scrollX,50-scrollY,50,50);
  context.stroke();
  
  info.innerHTML=`
    Scale : ${scale} <br>
    Scroll: ${scrollX},${scrollY} <br>
  `
  requestAnimationFrame( render );
}

// handlencursor keys to move scroll
window.onkeydown = function(event){
    event.preventDefault();
    if(event.keyCode == 37)      scrollX -=10;
    else if(event.keyCode == 39) scrollX +=10;
    else if(event.keyCode == 38) scrollY -=10;
    else if(event.keyCode == 40) scrollY +=10;
};

context.canvas.focus()
render()
<canvas id="c" width="400" height="150" style="border: 1px solid red;" tabindex="1"></canvas>
<div id="info"></div>



回答2:

The main idea is:

  context.translate(cx, cy);
  context.scale(scale, scale);
  context.translate(-cx, -cy);

where cx and cy are the coords of the center of the group of objects.

Observation: negative scale flip objects. This is the reason why I've used fill red and black.

// global vars : canvas, scroll & scale
let info        = document.getElementById("info");
const canvas    = document.getElementById('c');
let cw = canvas.width = 350,cx = cw/2;
let ch = canvas.height = 150,cy = 75;
const context   = c.getContext('2d');
let scrollX     = 0;
let scrollY     = 0;
let scale       = 1;
let scaleFactor = 0.02;


// clear canvas and draw two boxes
function render(){
  //context.clearRect(0, 0, canvas.width/scale, canvas.height/scale);
 
  
  context.beginPath();
  context.rect(100,50,50,50);
  context.fillStyle = "red";
  context.fill();
  
  context.beginPath();
  context.rect(200,50,50,50);
  context.fillStyle = "black";
  context.fill();
  
  info.innerHTML=`Scroll: ${scrollX},${scrollY} - Scale : ${scale}`
  requestAnimationFrame( render );
}


// handlencursor keys to move scroll
window.onkeydown = function(event){
  event.preventDefault();
  if(event.keyCode == 37)      scrollX -=10;
  else if(event.keyCode == 39) scrollX +=10;
  else if(event.keyCode == 38) scrollY -=10;
  else if(event.keyCode == 40) scrollY +=10;
};

// Handl mousenwheel zoom
canvas.onwheel= function(e){
  e.preventDefault();
  context.clearRect(-cw, -ch, 2 * cw, 2 * ch)
  

  let direction = e.deltaY > 0 ? 1 : -1;
  
  
  scrollX += Math.round(e.offsetX * scaleFactor * direction);
  scrollY += Math.round(e.offsetY * scaleFactor * direction);
  scale += scaleFactor * direction;
  
  context.setTransform(1, 0, 0, 1, 0, 0);
  
  context.translate(cx, cy);
  context.scale(scale, scale);
  context.translate(-cx, -cy);
  //render()
}

render()
<canvas id="c" width="350" height="150" style="border: 1px solid red;"></canvas>
<div id="info"></div>