以HTML5画布中的画布对象为中心

时间:2017-07-11 16:08:20

标签: html5 html5-canvas

我有一个Html5画布,我正在绘制正方形。 画布本身大致与窗口大小相同。 当我在方块上检测到点击时,我想翻译画布,使广场大致位于窗口的中心。任何见解,提示或直截了当的回复都是受欢迎的。

这是我到目前为止所尝试的内容:

如果一个正方形位于点(1000,1000),我只需翻译画布(-1000,-1000)。我知道我需要添加一个偏移量,以便它在窗口中居中。但是,画布总是从可见窗口结束(在某处的左上角太远)。

更复杂的情况:

最终,我希望能够以转换(旋转和倾斜)的画布上的单击对象为中心。我正在寻找一种效果非常好的等长效果。 我想知道这种转变是否会影响居中的逻辑/数学?

1 个答案:

答案 0 :(得分:0)

从屏幕转换为世界并返回

使用非标准轴(或投影)(如isometrix)时,最好使用变换矩阵。它将涵盖具有相同简单功能的每个可能的2D投影。

iso世界的坐标称为世界坐标。所有对象都存储为世界坐标。渲染它们时,使用变换矩阵将这些坐标投影到屏幕坐标。

矩阵,而不是电影。

矩阵表示世界屏幕坐标的方向和大小  x和y轴以及世界原点的屏幕位置(0,0)

对于

的iso
  • x轴横跨1向下0.5
  • y轴横跨-1下降0.5
  • z轴向上1(-1向上是向下的反向)但此示例不使用z

所以矩阵作为数组

const isoMat = [1,0.5,-1,0.5,0,0];  // ISO (pixel art) dimorphic projection

前两个是x轴,接下来两个是y轴,后两个值是原点的屏幕坐标。

使用矩阵转换点

将矩阵应用于点,这会将点从一个坐标系转换为另一个坐标系。您也可以通过逆变换转换回来。

屏幕世界

您需要将世界坐标转换为屏幕坐标。

function worldToScreen(pos,retPos){
    retPos.x = pos.x * isoMat[0] + pos.y * isoMat[2] + isoMat[4];
    retPos.y = pos.x * isoMat[1] + pos.y * isoMat[3] + isoMat[5];  
}

在演示中,我忽略了原点,因为我始终将它设置在画布的中心。因此从该函数中删除原点

function worldToScreen(pos,retPos){
    retPos.x = pos.x * isoMat[0] + pos.y * isoMat[2];
    retPos.y = pos.x * isoMat[1] + pos.y * isoMat[3];  
}

屏幕世界。

您还需要将屏幕坐标转换为世界。为此,您需要使用逆变换。它有点像乘法的倒数a * 2 = bb / 2 = a

的倒数

有一种计算逆矩阵的标准方法如下

const invMatrix = [];  // inverse matrix
// I call the next line cross, most call it the determinant which I 
// think is stupid as it is effectively a cross product and is used
// like you would use a cross product. Anyways I digress
const cross = isoMat[0] * isoMat[3] - isoMat[1] * isoMat[2];
invMatrix[0] =  isoMat[3] / cross;
invMatrix[1] = -isoMat[1] / cross;
invMatrix[2] = -isoMat[2] / cross;
invMatrix[3] =  isoMat[0] / cross;

然后我们有一个从屏幕x,y转换到世界位置的函数

function screenToWorld(pos,retPos){
    const x = pos.x - isoMat[4];
    const y = pos.y - isoMat[5];
    retPos.x = x * invMatrix[0] + y * invMatrix[2];
    retPos.y = x * invMatrix[1] + y * invMatrix[3];  
}

因此,您将鼠标坐标作为屏幕像素,使用上述功能转换为世界坐标。然后你可以使用世界坐标找到你想要的对象。

要将世界对象移动到屏幕中心,您可以将其坐标转换为屏幕坐标,在屏幕上添加位置(画布中心)并将变换矩阵原点设置为该位置。

演示

该演示在世界坐标中创建一组框。它通过isoMat

将2D上下文变换设置为ctx.setTransform((等轴测投影)

每一帧我将鼠标屏幕转换为世界坐标,然后用它来检查鼠标在哪个框上。

如果鼠标按钮关闭,我将该框从世界坐标转换为屏幕并添加屏幕中心。为了平滑步骤,新的屏幕中心被追逐(平滑)..

那么你应该能够在代码中解决它,在评论中提出任何问题。

const ctx = canvas.getContext("2d");
const moveSpeed = 0.4;
const boxMin = 20;
const boxMax = 50;
const boxCount = 100;
const boxArea = 2000;
// some canvas vals
var w = canvas.width;
var h = canvas.height;
var cw = w / 2;  // center 
var ch = h / 2;
var globalTime;




const U = undefined;
// Helper function 
const doFor = (count, cb) => { var i = 0; while (i < count && cb(i++) !== true); };
const eachOf = (array, cb) => { var i = 0; const len = array.length; while (i < len && cb(array[i], i++, len) !== true ); };
const setOf = (count, cb) => {var a = [],i = 0; while (i < count) { a.push(cb(i ++)) } return a };
const randI = (min, max = min + (min = 0)) => (Math.random() * (max - min) + min) | 0;
const rand = (min, max = min + (min = 0)) => Math.random() * (max - min) + min;
// mouse function and object
const mouse  = {x : 0, y : 0, button : false, world : {x : 0, y : 0}}
function mouseEvents(e){
	mouse.x = e.pageX;
	mouse.y = e.pageY;
	mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down","up","move"].forEach(name => document.addEventListener("mouse"+name,mouseEvents));


// boxes in world coordinates.
const boxes = [];
function draw(){
   if(this.dead){
      ctx.fillStyle = "rgba(0,0,0,0.5)"; 
      ctx.fillRect(this.x,this.y,this.w,this.h);
   }
   ctx.strokeStyle = this.col;
   ctx.globalAlpha = 1;
   ctx.strokeRect(this.x,this.y,this.w,this.h);

   // the rest is just overkill
   if(this.col === "red"){
      this.mr = 10;
   }else{
      this.mr = 1;
   }
   this.mc += (this.mr-this.m) * 0.45;
   this.mc *= 0.05;
   this.m += this.mc;
   for(var i = 0; i < this.m; i ++){
      const m = this.m * (i + 1);
      ctx.globalAlpha = 1-(m / 100);
      ctx.strokeRect(this.x-m,this.y-m,this.w,this.h);
   }
}


// make random boxes.
function createBoxes(){
  boxes.length = 0;
  boxes.push(...setOf(boxCount,()=>{
      return {
        x : randI(cw- boxArea/ 2, cw + boxArea/2), 
        y : randI(ch- boxArea/ 2, ch + boxArea/2), 
        w : randI(boxMin,boxMax), 
        h : randI(boxMin,boxMax),
        m : 5,
        mc : 0,
        mr : 5,
        col : "black",
        dead : false,
        draw : draw,
        isOver : isOver,
     }
   }));
}	
// use mouse world coordinates to find box under mouse
function isOver(x,y){
   return x > this.x && x < this.x + this.w && y > this.y && y < this.y + this.h;
}
var overBox;
function findBox(x,y){
   if(overBox){
       overBox.col = "black";
   }
   overBox = undefined;
   eachOf(boxes,box=>{
     if(box.isOver(x,y)){
        overBox = box;
        box.col = "red";
        return true;
     }
   })
}
function drawBoxes(){
   boxes.forEach(box=>box.draw());
}
// next 3 values control the movement of the origin
// rather than move instantly the currentPos chases the new pos.        
const currentPos = {x :0, y : 0};
const newPos = {x :0, y : 0};
const chasePos = {x :0, y : 0};
// this function does the chasing
function updatePos(){
  chasePos.x += (newPos.x - currentPos.x) * moveSpeed;
  chasePos.y += (newPos.y - currentPos.y) * moveSpeed;
  chasePos.x *= moveSpeed;
  chasePos.y *= moveSpeed;
  currentPos.x += chasePos.x;
  currentPos.y += chasePos.y;
}

// ISO matrix and inverse matrix plus 2world and 2 screen
const isoMat = [1,0.5,-1,0.5,0,0];
const invMatrix = [];
const cross = isoMat[0] * isoMat[3] - isoMat[1] * isoMat[2];
invMatrix[0] =  isoMat[3] / cross;
invMatrix[1] = -isoMat[1] / cross;
invMatrix[2] = -isoMat[2] / cross;
invMatrix[3] =  isoMat[0] / cross;

function screenToWorld(pos,retPos){
    const x = pos.x - isoMat[4];
    const y = pos.y - isoMat[5];
    retPos.x = x * invMatrix[0] + y * invMatrix[2];
    retPos.y = x * invMatrix[1] + y * invMatrix[3];  
}
function worldToScreen(pos,retPos){
    retPos.x = pos.x * isoMat[0] + pos.y * isoMat[2];// + isoMat[4];
    retPos.y = pos.x * isoMat[1] + pos.y * isoMat[3];// + isoMat[5];  
}

// main update function
function update(timer){
    // standard frame setup
    globalTime = timer;
    ctx.setTransform(1,0,0,1,0,0); // reset transform
    ctx.globalAlpha = 1;           // reset alpha
    if(w !== innerWidth || h !== innerHeight){
      cw = (w = canvas.width = innerWidth) / 2;
      ch = (h = canvas.height = innerHeight) / 2;
      createBoxes();
    }else{
      ctx.clearRect(0,0,w,h);
    }
    ctx.fillStyle = "black";
    ctx.font = "28px arial";
    ctx.textAlign = "center";
    ctx.fillText("Click on a box to center it.",cw,28);
    
    // update position      
    updatePos();
    isoMat[4] = currentPos.x;
    isoMat[5] = currentPos.y;
    // set the screen transform to the iso matrix
    // all drawing can now be done in world coordinates.
    ctx.setTransform(isoMat[0], isoMat[1], isoMat[2], isoMat[3], isoMat[4], isoMat[5]);
    // convert the mouse to world coordinates
    screenToWorld(mouse,mouse.world);
    // find box under mouse
	findBox(mouse.world.x, mouse.world.y);
    // if mouse down and over a box
    if(mouse.button && overBox){
       mouse.button = false;
       overBox.dead = true;  // make it gray
       // get the screen coordinates of the box
       worldToScreen({
            x:-(overBox.x + overBox.w/2),
            y:-(overBox.y + overBox.h/2),
          },newPos
       );
       // move it to the screen center
       newPos.x += cw;
       newPos.y += ch;

    }
    // forget what the following function does, think it does something like draw boxes, but I am guessing.. :P
	drawBoxes();
    requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas { position : absolute; top : 0px; left : 0px; }
<canvas id="canvas"></canvas>