如何将 x->n y->n 对象顺时针旋转 90°?

How to rotate an x->n y->n object clockwise by 90°?

我正在实现一个简单的俄罗斯方块游戏,其中有多个形状:

在典型的游戏中,当您在键盘上按UP时,下落的形状会顺时针旋转。

我在 JavaScript 实现中将形状定义为点坐标数组。比如S形是:

[
  {x:0, y:2},
  {x:0, y:1},
  {x:1, y:1},
  {x:1, y:0}
]

这将转换为以下形状:

往上按,应该把上面的数组转换成下面的数组:

[
  {x:0, y:0},
  {x:1, y:0},
  {x:1, y:1},
  {x:2, y:1}
]

又名,这个形状:

为了实现这一点,我...无耻地将坐标硬编码到 shapeRotationMap 对象中:

let shapeRotationMap = {
  "line0": [
    {x:0, y:0},      
    {x:0, y:1},
    {x:0, y:2},
    {x:0, y:3},
   ],
   "line1": [
    {x:0, y:0},      
    {x:1, y:0},
    {x:2, y:0},
    {x:3, y:0},
   ],
   "leftS0": [
      {x:0, y:0},      
      {x:1, y:0},
      {x:1, y:1},
      {x:2, y:1},
    ],
   "leftS1": [
      {x:0, y:1},      
      {x:0, y:2},
      {x:1, y:0},
      {x:1, y:1},
    ],
   "rightS0": [
      {x:0, y:1},      
      {x:1, y:1},
      {x:1, y:0},
      {x:2, y:0},
    ],
   "rightS1": [
      {x:0, y:0},      
      {x:0, y:1},
      {x:1, y:1},
      {x:1, y:2},
    ],
    "podium0": [
      {x:0, y:1},      
      {x:1, y:0},
      {x:1, y:1},
      {x:1, y:2},
    ],
    "podium1": [
      {x:0, y:0},      
      {x:1, y:0},
      {x:1, y:1},
      {x:2, y:0},
    ],
    "podium2": [
      {x:0, y:0},      
      {x:0, y:1},
      {x:0, y:2},
      {x:1, y:1},
    ],
    "podium3": [
      {x:0, y:1},      
      {x:1, y:0},
      {x:1, y:1},
      {x:2, y:1},
    ]
}

shapeRotationMap["line2"] = shapeRotationMap["line0"];
shapeRotationMap["line3"] = shapeRotationMap["line1"];
shapeRotationMap["leftS2"] = shapeRotationMap["leftS0"];
shapeRotationMap["leftS3"] = shapeRotationMap["leftS1"];
shapeRotationMap["rightS2"] = shapeRotationMap["rightS0"];
shapeRotationMap["rightS3"] = shapeRotationMap["rightS1"];

["square0", "square1", "square2","square3"].forEach(function(key){
  shapeRotationMap[key] = [
        {x:0, y:0},      
        {x:0, y:1},
        {x:1, y:0},
        {x:1, y:1},
      ];
});

我有一个形状字符串 ("line") 和一个旋转 (0123),这就是我知道的要拿哪个对象。

但是,这会使代码复杂化,很难添加新形状,只是将其张贴在这里我觉得我在侮辱一些程序员。

但是我找不到旋转这样一个对象的算法。

我找到了这个算法:How to rotate a matrix in an array in javascript。但是这里OP有一个二维数组(矩阵),在我的例子中我有一个坐标对象。

有谁知道如何将我的对象旋转 90°?

如果不是,我想我会切换我的逻辑并改用矩阵。

对于简单的形状,只需交换 x 和 y 坐标即可:

function rotateShape(s) {
    return s.map(function (p) {
        return { x: p.y, y: p.x };
    });
}
//TEST
var shape = [
    { x: 0, y: 1 },
    { x: 0, y: 0 },
    { x: 1, y: 0 },
    { x: 1, y: -1 }
];
var tileSize = 20;
var canvas = document.body.appendChild(document.createElement("canvas"));
canvas.width = canvas.height = tileSize * 6;
var ctx = canvas.getContext("2d");
ctx.strokeStyle = "red";
ctx.fillStyle = "green";
function draw(s, offset) {
    if (offset === void 0) { offset = { x: 2, y: 2 }; }
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    s.forEach(function (p) {
        ctx.fillRect((p.x + offset.x) * tileSize, (p.y + offset.y) * tileSize, tileSize, tileSize);
        ctx.strokeRect((p.x + offset.x) * tileSize, (p.y + offset.y) * tileSize, tileSize, tileSize);
    });
}
setInterval(function () {
    shape = rotateShape(shape);
    draw(shape);
}, 1000);

以上代码段使用坐标 x:0,y:0 作为轴心点。偏移形状坐标将有效地 "move" 枢轴点。

感谢@NinaScholz in ,我成功地实现了我的俄罗斯方块游戏,并适当地旋转了我的方块

let _game;
var gameLoopHandle;
let possibleShapes = ["square","line","leftS","rightS","podium","lShapeR","lShapeL","zShapeL","zShapeR","fourSquares"];
function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.rotateCW = function (c) {
  // x' =  x cos phi + y sin phi \ formula with pivot at (0, 0)
  // y' = -x sin phi + y cos phi /
  // phi = 90°                   insert phi
  // cos 90° = 0   sin 90° = 1   calculate cos and sin 
  // x' =  y                     \ formula with pivot at (0, 0)
  // y' = -x                     /
  // x' =  (cy - y) + cx         \ formula with different pivot needs correction 
  // y' = -(cx - x) + cy         /
  // y' = -cx + x + cy          /
  return new Point(
  c.x + c.y - this.y,
  c.y - c.x + this.x
  );
}

Point.prototype.rotateCCW = function (c) {
  // source: https://en.wikipedia.org/wiki/Rotation_(mathematics)#Two_dimensions
  // x' =  x cos phi + y sin phi  \ formula with pivot at (0, 0)
  // y' = -x sin phi + y cos phi  /
  // phi = -90°
  // cos -90° = 0   sin -90° = -1
  // x' = -y                      \ formula with pivot at (0, 0)
  // y' =  x                      /
  // x' = -(cy - y) + cx          \ formula with different pivot needs correction 
  // x' = -cy + y + cx            /
  // y' =  (cx - x) + cy         /
  return new Point(
  c.x - c.y + this.y,
  c.y + c.x - this.x
  );
}
let Shape = function(shapeStr){
  this.shapeStr = shapeStr;
  switch(shapeStr){
case "lShapeR":
  this.body = [
    new Point(0,0),
    new Point(1,0),
    new Point(2,0),
    new Point(0,1)
  ];
  this.pivotPointIndex = 0;
  this.canRotate = true;
  this.color = "pink";
  break;
case "lShapeL":
  this.body = [
    new Point(0,0),
    new Point(1,0),
    new Point(2,0),
    new Point(2,1)
  ];
  this.pivotPointIndex = 3;
  this.canRotate = true;
  this.color = "gray";
  break;
case "zShapeR":
  this.body = [
    new Point(0,0),
    new Point(1,0),
    new Point(1,1),
    new Point(1,2),
    new Point(2,2)
  ];
  this.pivotPointIndex = 2;
  this.canRotate = true;
  this.color = "#73C6B6";
  break;
case "fourSquares":
  this.body = [
    new Point(0,0),
    new Point(2,0),
    new Point(2,2),
    new Point(0,2),
  ];
  this.pivotPointIndex = 2;
  this.canRotate = false;
  this.color = "#E67E22";
  break;
case "line":
  this.body = [
    new Point(0,0),
    new Point(0,1),
    new Point(0,2),
    new Point(0,3)
  ];
  this.pivotPointIndex = 1;
  this.canRotate = true;
  this.color = "green";
  break;
case "leftS":
  this.body = [
    new Point(0,2),
    new Point(0,1),
    new Point(1,1),
    new Point(1,0)
  ];
  this.pivotPointIndex = 2;
  this.canRotate = true;
  this.color = "lime";
  break;
case "rightS":
  this.body = [
    new Point(1,2),
    new Point(1,1),
    new Point(0,1),
    new Point(0,0)
  ];
  this.pivotPointIndex = 2;
  this.canRotate = true;
  this.color = "yellow";
  break;
case "podium":
  this.body = [
    new Point(0,0),
    new Point(1,0),
    new Point(2,0),
    new Point(1,1)
  ];
  this.pivotPointIndex = 1;
  this.canRotate = true;
  this.color = "brown";
  break;
case "square":
default:
  this.body = [
    new Point(0,0),
    new Point(0,1),
    new Point(1,1),
    new Point(1,0)
  ];
  this.canRotate = false;
  this.color = "red";
  break;
  }
  this.rotate = function(){
if (!this.canRotate) return;

for(let i=0; i<this.body.length; i++){
  this.body[i] = this.body[i].rotateCW(this.body[this.pivotPointIndex])
}

  }
}



let TetrisBlock = function(shapeStr){
  this.stepsDown = 0;
  this.stepsRight = 0;
  this.shape = new Shape(shapeStr);
  this.getLowestSquare = function(){
let lowestSquare;
for (let i=0; i<this.shape.body.length;i++){
  let thisSquare = this.shape.body[i];
  if (!lowestSquare) {
    lowestSquare = thisSquare;
  } else {
    if (thisSquare.y > lowestSquare.y){
      lowestSquare = thisSquare;
    }
  }
  
}

return lowestSquare;
  }
  this.containsPoint = function(point){
for (let i = 0; i<this.shape.body.length; i++){
  if (this.shape.body[i].x === point.x && this.shape.body[i].y === point.y) return true;
}
return false;
  }
  this.cloneShape = function(){
let clonedShape = new Shape(this.shape.shapeStr);
let nonPoints = JSON.parse(JSON.stringify(this.shape.body));
let actualPoints = [];
for (let i = 0; i<nonPoints.length; i++){
  actualPoints.push(new Point(nonPoints[i].x, nonPoints[i].y));
}
clonedShape.body = actualPoints;
return clonedShape;
  }

  


  this.rotate = function(){
this.shape.rotate();

  }
  
  this.moveDown = function(){

for (let i=0; i<this.shape.body.length;i++){
  this.shape.body[i].y++;
}
this.stepsDown++;

  }
  
  this.moveLeft = function(){

for (let i=0; i<this.shape.body.length;i++){
  this.shape.body[i].x--;
}
this.stepsRight--;

  }
  
  this.moveRight = function(){

for (let i=0; i<this.shape.body.length;i++){
  this.shape.body[i].x++;
}
this.stepsRight++;

  }
  
}
let getLeftMostSquare = function(body){
  let leftMostsquare;
  for (let i=0; i<body.length;i++){
let thisSquare = body[i];
if (!leftMostsquare) {
  leftMostsquare = thisSquare;
} else {
  if (leftMostsquare.x > thisSquare.x){
    leftMostsquare = thisSquare;
  }
}

  }
  
  return leftMostsquare;
}

let getRightMostSquare = function(body){
  let rightMostSquare;
  for (let i=0; i<body.length;i++){
let thisSquare = body[i];
if (!rightMostSquare) {
  rightMostSquare = thisSquare;
} else {
  if (rightMostSquare.x < thisSquare.x){
    rightMostSquare = thisSquare;
  }
}

  }
  return rightMostSquare;
}

let TetrisGame = function(){
  this.board = [];
  this.score = 0;
  this.speed = 1000;
  this.ended = false;
  this.boardSizeY = 40;
  this.boardSizeX = 20;
  this.manager = {};
  this.fallingBlock = new TetrisBlock(possibleShapes[getRandomInt(0,possibleShapes.length-1)]);
  this.blocksFallen = [];
}

TetrisGame.prototype.init = function(options){
  options = options || {};
  this.boardSizeY = options.boardSizeY || 20;
  this.boardSizeX = options.boardSizeX || 20;

  
}

TetrisGame.prototype.generateBoard = function(){
  this.board = [];
  for (let i=0;i<this.boardSizeY;i++){
let boardRow=[];
for (let j = 0; j < this.boardSizeX; j++) {
  let hadBlock = false;
  for (let k = 0; k<this.fallingBlock.shape.body.length;k++){
    if(this.fallingBlock.shape.body[k].y == i &&
      this.fallingBlock.shape.body[k].x == j) {
        boardRow.push(this.fallingBlock.shape.color);
        hadBlock = true;
      }
  }
  for (let l = 0; l<this.blocksFallen.length;l++){
    for (let k = 0; k<this.blocksFallen[l].shape.body.length;k++){
    if(this.blocksFallen[l].shape.body[k].y == i &&
      this.blocksFallen[l].shape.body[k].x == j) {
        boardRow.push(this.blocksFallen[l].shape.color);
        hadBlock = true;
      }
    }
  }
  
  if (!hadBlock)
    boardRow.push(0);
}
this.board.push(boardRow);
  }
}

TetrisGame.prototype.setSpeed = function(speed){
  this.speed = speed;
}

TetrisGame.prototype.setScore = function(score){
  this.score = score;
}
function outOfBoundsDownOrIsBlock(game,tetrisBody){
  game.generateBoard();
  let gameBoard = game.board;
  for (let i = 0; i<tetrisBody.length;i++){
let tetrisBlock = tetrisBody[i];
for(let j = 0; j<gameBoard.length;j++){
  for(let k = 0; k<gameBoard[j].length;k++){
    let gameBlock = gameBoard[j][k];
    if (gameBlock) {
      let blockPoint = {x:k,y:j}
      if(!game.fallingBlock.containsPoint(blockPoint)){
        if (tetrisBlock.x == blockPoint.x && tetrisBlock.y == blockPoint.y) return true;
      }
    }
  }
}
  };
  return game.fallingBlock.getLowestSquare().y == game.boardSizeY - 1;

}
function haveCollision(game,tetrisBody){
  game.generateBoard();
  let fallenBlocks = game.blocksFallen;
  // left edge
  if (getLeftMostSquare(tetrisBody).x < 0) return true;
  // right edge
  if (getRightMostSquare(tetrisBody).x == game.boardSizeX) return true;

  // fallenBlocks
  for (let i = 0; i<tetrisBody.length;i++){
let tetrisBlock = tetrisBody[i];
for(let j = 0; j<fallenBlocks.length;j++){
  let thisFallenBlock = fallenBlocks[j];
  for (let k=0;k<thisFallenBlock.shape.body.length;k++){
    let fallenBlockPoint = thisFallenBlock.shape.body[k];
    if (tetrisBlock.x == fallenBlockPoint.x && tetrisBlock.y == fallenBlockPoint.y) return true;
  }
  
}
  }

  return false;
}
TetrisGame.prototype.rotateIfCan = function(){
  if (!this.fallingBlock.shape.canRotate) return;
  let rawBlock = new TetrisBlock(this.fallingBlock.shape.shapeStr);
  rawBlock.shape = this.fallingBlock.cloneShape();
  rawBlock.rotate();
  if (!haveCollision(this,rawBlock.shape.body)) this.fallingBlock.rotate();

}

TetrisGame.prototype.moveLeftIfCan = function(){
  let rawBlock = new TetrisBlock(this.fallingBlock.shape.shapeStr);
  rawBlock.shape = this.fallingBlock.cloneShape();
  rawBlock.moveLeft();

  if (!haveCollision(this,rawBlock.shape.body)) this.fallingBlock.moveLeft();

}
TetrisGame.prototype.generateNewBlockOrGameOver = function(){
  

  this.fallingBlock = new TetrisBlock(possibleShapes[getRandomInt(0,possibleShapes.length-1)]);

  if (haveCollision(this,this.fallingBlock.shape.body)) {
this.ended = true;
  } else {
this.score++;
this.generateBoard();
let lineCount = 0;
for (let i=0; i<this.board.length;i++){
  let boardLine = this.board[i];
  let pointCountPerLine = 0;

  for (let j=0;j<boardLine.length;j++) {
    let blockPoint = boardLine[j];
    if (blockPoint !== 0 && typeof blockPoint === "string") pointCountPerLine++;
  }
  
  if (pointCountPerLine === boardLine.length) {
    lineCount++;
    for (let k=0;k<this.blocksFallen.length;k++){
      let thisFallenBlock = this.blocksFallen[k];
      for (let p = 0; p<thisFallenBlock.shape.body.length;p++){
        let thisFallenBlockPoint = thisFallenBlock.shape.body[p];
        if(thisFallenBlockPoint.y === i) {
          thisFallenBlock.shape.body.splice(p,1,{dummy:true});
        }
      }
    }

    for (let k=0;k<this.blocksFallen.length;k++){
      let thisFallenBlock = this.blocksFallen[k];
      for (let p = 0; p<thisFallenBlock.shape.body.length;p++){
        let thisFallenBlockPoint = thisFallenBlock.shape.body[p];
        if(thisFallenBlockPoint.y < i) {
          thisFallenBlockPoint.y++;
        }
      }
    }
  }
}

if (lineCount) this.score += lineCount * 10;
if (lineCount == 4) this.score += 40;
this.generateBoard();
  }
 
}

TetrisGame.prototype.teleportDown = function(){
  let movedDown = this.moveDownOrNewBlock();
  while (movedDown) {
movedDown = this.moveDownOrNewBlock();
  }
}

TetrisGame.prototype.moveDownOrNewBlock = function(){
  
  let rawBlock = new TetrisBlock(this.fallingBlock.shape.shapeStr);
  rawBlock.shape = this.fallingBlock.cloneShape();
  rawBlock.moveDown();

  if (outOfBoundsDownOrIsBlock(this,rawBlock.shape.body)) {
rawBlock = new TetrisBlock(this.fallingBlock.shape.shapeStr);
rawBlock.shape = this.fallingBlock.cloneShape();
this.blocksFallen.push(rawBlock);
return this.generateNewBlockOrGameOver();
  }

  if (!haveCollision(this,rawBlock.shape.body)) {
this.fallingBlock.moveDown();
return true;
  }

  
  
}

TetrisGame.prototype.moveRightIfCan = function(){
  let rawBlock = new TetrisBlock(this.fallingBlock.shape.shapeStr);
  rawBlock.shape = this.fallingBlock.cloneShape();
  rawBlock.moveRight();

  if (!haveCollision(this,rawBlock.shape.body)) this.fallingBlock.moveRight();

}

function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

let genericDiv = function(color){
  let returnDiv = document.createElement("div");
  returnDiv.style.height = "10px";
  returnDiv.style.width = "10px";
  returnDiv.style.background = color;

  return returnDiv;
}



let emptyDiv = function(){
  return genericDiv("black");
}

function updateDOM(game) {
  var el = document.getElementById("gameboard");
  el.innerHTML = "";
  el.style.position = "relative";
  var scoreEl = document.getElementById("score");
  scoreEl.innerText = game.score;


  for (let i =0;i<game.board.length;i++){
let rowDiv = document.createElement("div");
//snakeRowDiv.style.position = "absolute";
for (let j=0;j<game.board[i].length;j++){
  if (game.board[i][j]){
    whichDiv = genericDiv(game.board[i][j]);
  } else {
    whichDiv = emptyDiv();
  }
   
  whichDiv.style.position = "absolute";
  whichDiv.style.left = j * (parseInt(whichDiv.style.width)) + "px";
  whichDiv.style.top = (i * (parseInt(whichDiv.style.height)) + 100)  + "px";
  rowDiv.appendChild(whichDiv);

}

el.appendChild(rowDiv);
  }
}

function generateDomListener(game){
  return function(event){
switch (event.key) {
  case "ArrowUp":
    game.rotateIfCan();
    game.generateBoard();
    updateDOM(game);
    break;
  case "ArrowDown":
    game.teleportDown();
    game.generateBoard();
    updateDOM(game);
    break;
  case "ArrowLeft":
    game.moveLeftIfCan();
    game.generateBoard();
    updateDOM(game);
    break;
  case "ArrowRight":
    game.moveRightIfCan();
    game.generateBoard();
    updateDOM(game);
    break;
}
  }
}
function refreshInterval(game){
  clearInterval(gameLoopHandle);
  gameLoopHandle = setInterval(gameLoop(game), game.speed);
}
function decreaseDifficulty(game){
  if (game.speed <= 900) {
game.speed += 50;
  }
  clearInterval(gameLoopHandle);
  gameLoopHandle = setInterval(gameLoop(game), game.speed);
}
function restart(game){
  game.ended = false;
  game.genApple = true;
  game.score = 0;
  game.speed = 500;
  game.apple = {x:null,y:null}

  game.snake.body = [
{x:9,y:8},
{x:9,y:9},
{x:9,y:10},
{x:9,y:11},
  ]
  game.snake.going = "RIGHT";

  clearInterval(gameLoopHandle);
  gameLoopHandle = setInterval(gameLoop(game), game.speed);
  
}
function increaseDifficulty(game){
  if (game.speed >= 100) {
game.speed -= 50;
  }
  clearInterval(gameLoopHandle);
  gameLoopHandle = setInterval(gameLoop(game), game.speed);
}

function gameLoop(game){
  return function(){
if (!game.ended) {
  game.moveDownOrNewBlock();
  if (!game.ended) {
    game.generateBoard();
    updateDOM(game);
  }
} else {
  clearInterval(gameLoopHandle);
  alert ("GAME OVER");
}
  }
}

document.addEventListener("DOMContentLoaded", function(event) {
  var game = new TetrisGame();
  _game =game;
  game.init();
  game.generateBoard()
  updateDOM(game);
  document.addEventListener("keydown", generateDomListener(game));

  gameLoopHandle = setInterval(gameLoop(game), game.speed);
})
<div id="gameboard"></div>
<div>
  <h1>Score: <span id="score">0</span></h1>
  <button onclick="increaseDifficulty(_game)">Increase difficulty</button>
  <button onclick="decreaseDifficulty(_game)">Decrease difficulty</button>
</div>