如何在JavaScript中实现俄罗斯方块图形的90°顺时针旋转?
嘿,我懂你这种硬编码旋转映射的痛苦——每次加新形状都要写一堆重复代码,简直是折磨!其实咱们完全可以用坐标变换的思路,实现一个通用的90°顺时针旋转函数,再也不用依赖硬编码了,也没必要切换到二维矩阵。
核心思路
俄罗斯方块的旋转本质是围绕一个锚点做坐标变换:先把所有点转换为相对于锚点的局部坐标,应用旋转公式后再转回到全局坐标。关键是选对旋转锚点(比如官方俄块里不同形状有固定的锚点),以及适配屏幕坐标系(y轴向下)的旋转公式。
通用实现方案
1. 带指定锚点的旋转函数(精准匹配需求)
先给你一个最直接的实现,支持手动指定旋转锚点——刚好能完美匹配你给出的S形旋转例子:
// 传入形状坐标数组和旋转锚点,返回旋转后的坐标数组 function rotateShapeClockwise(shape, anchor) { return shape.map(point => { // 第一步:转成相对于锚点的局部坐标 const localX = point.x - anchor.x; const localY = point.y - anchor.y; // 第二步:应用屏幕坐标系下的90°顺时针旋转公式 // (数学坐标系顺时针公式是(x,y)→(y,-x),屏幕y轴向下,公式调整为(x,y)→(-y,x)) const newLocalX = -localY; const newLocalY = localX; // 第三步:转回全局坐标 return { x: newLocalX + anchor.x, y: newLocalY + anchor.y }; }); }
用你的S形测试一下:
const sShape = [ {x:0, y:2}, {x:0, y:1}, {x:1, y:1}, {x:1, y:0} ]; // S形的旋转锚点选中间的(1,1) const rotatedSShape = rotateShapeClockwise(sShape, {x:1, y:1}); console.log(rotatedSShape); // 输出完全符合你的需求: // [ {x:0,y:0}, {x:1,y:0}, {x:1,y:1}, {x:2,y:1} ]
2. 自动计算锚点的通用函数(适配所有形状)
如果不想手动给每个形状指定锚点,可以通过包围盒自动计算锚点,再结合位置调整保证旋转后形状不偏移:
// 自动计算形状的旋转锚点(默认取包围盒中心) function getRotationAnchor(shape) { const xs = shape.map(p => p.x); const ys = shape.map(p => p.y); const minX = Math.min(...xs); const maxX = Math.max(...xs); const minY = Math.min(...ys); const maxY = Math.max(...ys); // 返回包围盒中心(整数坐标,适配网格) return { x: Math.floor((minX + maxX) / 2), y: Math.floor((minY + maxY) / 2) }; } // 带自动锚点和位置校准的通用旋转函数 function rotateShapeClockwiseAuto(shape) { const anchor = getRotationAnchor(shape); let rotated = rotateShapeClockwise(shape, anchor); // 校准位置:让旋转前后的形状中心保持一致,避免偏移 const oldCenter = { x: (Math.min(...shape.map(p=>p.x)) + Math.max(...shape.map(p=>p.x))) / 2, y: (Math.min(...shape.map(p=>p.y)) + Math.max(...shape.map(p=>p.y))) / 2 }; const newCenter = { x: (Math.min(...rotated.map(p=>p.x)) + Math.max(...rotated.map(p=>p.x))) / 2, y: (Math.min(...rotated.map(p=>p.y)) + Math.max(...rotated.map(p=>p.y))) / 2 }; // 计算偏移量并取整,保证坐标是网格上的整数 const offsetX = Math.round(oldCenter.x - newCenter.x); const offsetY = Math.round(oldCenter.y - newCenter.y); return rotated.map(p => ({ x: p.x + offsetX, y: p.y + offsetY })); }
比如测试直线形:
const lineShape = [ {x:0,y:0}, {x:0,y:1}, {x:0,y:2}, {x:0,y:3} ]; const rotatedLine = rotateShapeClockwiseAuto(lineShape); // 旋转后会得到横向的直线:[ {x:0,y:1}, {x:1,y:1}, {x:2,y:1}, {x:3,y:1} ]
扩展优化
如果要完全贴合官方俄罗斯方块的旋转规则,可以给每个形状的定义加上专属锚点:
const shapes = { S: { coords: [ {x:0, y:2}, {x:0, y:1}, {x:1, y:1}, {x:1, y:0} ], anchor: {x:1, y:1} }, Line: { coords: [ {x:0,y:0}, {x:0,y:1}, {x:0,y:2}, {x:0,y:3} ], anchor: {x:0, y:1.5} // 用浮点锚点,最后取整即可 }, Square: { coords: [ {x:0,y:0}, {x:0,y:1}, {x:1,y:0}, {x:1,y:1} ], anchor: {x:0.5, y:0.5} } };
旋转时直接调用rotateShapeClockwise(shapes.S.coords, shapes.S.anchor)就行,完全不用硬编码旋转映射。
为什么这个方案更好?
- 无冗余代码:新增形状只需要定义初始坐标和锚点,不用写旋转后的坐标
- 灵活可控:可以随时调整锚点和位置校准规则,适配不同的游戏逻辑
- 逻辑清晰:基于坐标变换的数学原理,比硬编码更容易维护和调试
内容的提问来源于stack exchange,提问作者Adelin
相关产品推荐
相关产品推荐

