You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.07 16:52:59