HTML5等距投影疑问:无视口变换绘网格及Sprite失真解决
嘿,我来帮你搞定这个等距投影的问题!咱们分两部分来聊,先解决不依赖视口变换的等距网格绘制,再搞定视口变换方案下的Sprite失真问题。
一、不缩放旋转视口的等距网格绘制方法
你用的cartesianToIsometric坐标转换公式是完全正确的,但问题出在绘制方式上:你直接用strokeRect绘制轴对齐的矩形,而等距网格的单元是菱形,不是轴对齐的正方形。所以我们需要把笛卡尔坐标系的正方形转换成等距的菱形,用路径来绘制。
修改后的坐标转换实现代码
window.addEventListener('DOMContentLoaded', (event) => { class Point { constructor(x = 0, y = 0) { this.x = x; this.y = y; } } let canvas = document.getElementById('canvas'); let ctx = canvas.getContext('2d'); canvas.width = window.innerWidth; canvas.height = window.innerHeight; const tileSize = 32; // 笛卡尔网格的边长 function cartesianToIsometric(cartPt) { var tempPt = new Point(); tempPt.x = cartPt.x - cartPt.y; tempPt.y = (cartPt.x + cartPt.y) / 2; return tempPt; } function tick(time) { ctx.clearRect(0, 0, canvas.width, canvas.height); // 清空画布避免重复绘制 // 平移整个网格到画布中心,方便查看 ctx.save(); ctx.translate(canvas.width/2 - tileSize*10, canvas.height/2 - tileSize*5); for (var x = 0; x < 10; ++x) { for (var y = 0; y < 10; ++y) { let cartPt = new Point(x * tileSize, y * tileSize); let isoPt = cartesianToIsometric(cartPt); ctx.strokeStyle = 'black'; // 绘制等距菱形(替代原来的strokeRect) ctx.beginPath(); ctx.moveTo(isoPt.x, isoPt.y); // 左上角 ctx.lineTo(isoPt.x + tileSize, isoPt.y + tileSize/2); // 右上角 ctx.lineTo(isoPt.x, isoPt.y + tileSize); // 右下角 ctx.lineTo(isoPt.x - tileSize, isoPt.y + tileSize/2); // 左下角 ctx.closePath(); ctx.stroke(); } } ctx.restore(); requestAnimationFrame(tick); } requestAnimationFrame(tick); });
关键说明
- 笛卡尔坐标系的每个
32x32正方形,在等距投影下会变成菱形,四个顶点的坐标需要单独计算,用路径绘制才能得到正确的网格。 - 我给
Point类加了带参数的构造函数简化代码,还添加了画布清空和网格平移,避免重复绘制导致线条变粗,同时让网格居中显示更直观。
二、视口变换方案下避免Sprite失真的方法
你原来的视口变换(缩放+旋转)会导致Sprite失真,核心原因是上下文的变换会直接作用于所有绘制内容,包括图片的像素拉伸。这里提供两种解决思路:
方法1:对Sprite单独应用逆变换
在绘制Sprite时,先保存当前上下文状态,然后撤销视口的缩放和旋转,再绘制Sprite,最后恢复状态。这样Sprite就不会被拉伸变形:
window.addEventListener('DOMContentLoaded', (event) => { class Point { constructor() { this.x = 0; this.y = 0; } } let canvas = document.getElementById('canvas'); let ctx = canvas.getContext('2d'); canvas.width = window.innerWidth; canvas.height = window.innerHeight; const tileSize = 32; function tick(time) { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.save(); // 视口变换:平移+缩放+旋转 ctx.translate(250, 0); ctx.scale(1, 0.5); ctx.rotate(45 * Math.PI / 180); // 绘制网格 for (var x = 0; x < 10; ++x) { for (var y = 0; y < 10; ++y) { ctx.strokeStyle = 'black'; ctx.strokeRect(x*tileSize, y*tileSize, tileSize, tileSize); } } // 绘制Sprite:先撤销视口变换 ctx.save(); ctx.rotate(-45 * Math.PI / 180); // 逆旋转 ctx.scale(1, 2); // 逆缩放(还原y轴比例) // 绘制Sprite(这里用绿色矩形模拟,实际替换为drawImage即可) let p = new Point(); p.x = 4*tileSize; p.y = 2*tileSize; ctx.fillStyle = 'green'; ctx.fillRect(p.x, p.y, tileSize, tileSize); ctx.restore(); ctx.restore(); requestAnimationFrame(tick); } requestAnimationFrame(tick); });
方法2:使用预先制作的等距Sprite
更彻底的方法是直接让美术资源导出成菱形的等距Sprite,然后用坐标转换函数计算位置后直接绘制,不需要依赖视口变换,从根源上避免失真。如果是像素风格的Sprite,还可以开启ctx.imageSmoothingEnabled = false;,防止Canvas自动插值导致的模糊:
ctx.imageSmoothingEnabled = false; ctx.drawImage(yourIsometricSprite, isoPt.x - tileSize/2, isoPt.y - tileSize/2);
内容的提问来源于stack exchange,提问作者Tofu
相关产品推荐
相关产品推荐

