代码之家  ›  专栏  ›  技术社区  ›  Dan Durnev

如何在js画布上只绘制tilemap的可见部分?

  •  0
  • Dan Durnev  · 技术社区  · 7 年前

    我使用平铺(3200 x 3200像素)创建了简单的平铺贴图。我用 this library

    我每个seocnd绘制整个tilemap 3200 x 3200 60次。 ctx。翻译 . 我包括了这个 in my own function

    但当我用平铺(32000 x 32000像素)创建更大的地图时,我得到了一个非常冻结的页面。我不能快速移动,我想大概有10帧

    drawTiles()

    非常感谢。

    1 回复  |  直到 7 年前
        1
  •  5
  •   Blindman67    3 年前

    ##绘制大平铺集

    如果您有一个大的平铺集,并且只在画布中看到它的一部分,那么您只需要计算画布左上角的平铺,以及适合画布的横下平铺数。

    然后绘制适合画布的方形瓷砖阵列。

    worldTileCount = 1024 ),每个图块为64 x 64像素 tileSize = 64 ,使整个操场65536像素为正方形

    左上角平铺的位置由变量设置 worldX , worldY

    ###用于绘制平铺的函数

    // val | 0 is the same as Math.floor(val)
    
    var worldX = 512 * tileSize;  // pixel position of playfield
    var worldY = 512 * tileSize;
    
    function drawWorld(){
      const c = worldTileCount; // get the width of the tile array
      const s = tileSize;       // get the tile size in pixels
    
      // get the tile position
      const tx = worldX / s | 0;  // get the top left tile
      const ty = worldY / s | 0;
    
      // get the number of tiles that will fit the canvas
      const tW = (canvas.width / s | 0) + 2;  
      const tH = (canvas.height / s | 0) + 2;
    
      // set the location. Must floor to pixel boundary or you get holes
      ctx.setTransform(1,0,0,1,-worldX | 0,-worldY | 0);  
    
      // Draw the tiles across and down
      for(var y = 0; y < tH; y += 1){
         for(var x = 0; x < tW; x += 1){
             // get the index into the tile array for the tile at x,y plus the topleft tile
             const i = tx + x + (ty + y) * c;
    
             // get the tile id from the tileMap. If outside map default to tile 6
             const tindx = tileMap[i] === undefined ? 6 : tileMap[i];
    
             // draw the tile at its location. last 2 args are x,y pixel location
             imageTools.drawSpriteQuick(tileSet, tindx, (tx + x) * s, (ty + y) * s);
         }
      }
    
    }
    

    ###setTransform和绝对坐标。

    使用画布上下文 setTransform 设置世界位置,然后可以在其自己的坐标处绘制每个瓷砖。

       // set the world location. The | 0 floors the values and ensures no holes
       ctx.setTransform(1,0,0,1,-worldX | 0,-worldY | 0);  
    

    这样,如果你在51023、34256位置有一个角色,你可以在那个位置画它。

       playerX = 51023;
       playerY = 34256;
       ctx.drawImage(myPlayerImage,playerX,playerY);
    

    如果您想要相对于玩家的平铺贴图,则只需将世界位置设置为画布大小的一半,并向左加一个平铺,以确保重叠

       playerX = 51023;
       playerY = 34256;
    
       worldX = playerX - canvas.width / 2 - tileWidth;
       worldY = playerY - canvas.height / 2 - tileHeight;
    

    ###演示大65536×65536像素的瓷砖地图。

    如果你有马匹,可以以60fps的速度处理更大的数据,而不会损失任何帧速率。(使用此方法的地图大小限制约为4000000000x 4000000000像素(32位整数坐标))


    #2019年5月15日更新 抖动

    评论指出,有一些 抖动 当地图滚动时。

    我做了一些改变,以平滑随机路径,每240帧(60fps时4秒)进行一次轻松的输入输出,还添加了一个帧速率降低器,如果在画布上单击并按住鼠标按钮,帧速率将降低到正常的1/8,以便 抖动 更容易看到。

    这有两个原因 抖动

    ###时间误差

    第一个也是最短的一个是通过 requestAnimationFrame ,间隔不是完美的,时间导致的舍入误差加剧了对齐问题。

    为了减少时间误差,我将移动速度设置为恒定间隔,以最小化帧之间的舍入误差漂移。

    ###将瓷砖与像素对齐

    主要原因是 抖动 瓷砖必须在像素边界上渲染。否则,锯齿错误将在瓷砖之间创建可见接缝。

    要获得平滑滚动(子像素定位),请将地图绘制到与像素对齐的屏幕外画布,然后将该画布渲染到显示画布,添加子像素偏移。这将提供使用画布的最佳结果。为了更好,您需要使用webGL

    ###更新结束

    var refereshSkip = false; // when true drops frame rate by 4
    var dontAlignToPixel = false;
    var ctx = canvas.getContext("2d");
    function mouseEvent(e) {
       if(e.type === "click") {
           dontAlignToPixel = !dontAlignToPixel;
           pixAlignInfo.textContent = dontAlignToPixel ? "Pixel Align is OFF" : "Pixel Align is ON";
       } else {
           refereshSkip = e.type === "mousedown";
       }
    }
    pixAlignInfo.addEventListener("click",mouseEvent);
    canvas.addEventListener("mousedown",mouseEvent);
    canvas.addEventListener("mouseup",mouseEvent);
    
    
    // wait for code under this to setup
    setTimeout(() => {
    
    
      var w = canvas.width;
      var h = canvas.height;
      var cw = w / 2; // center 
      var ch = h / 2;
    
    
    
      // create tile map
      const worldTileCount = 1024;
      const tileMap = new Uint8Array(worldTileCount * worldTileCount);
      
      // add random tiles
      doFor(worldTileCount * worldTileCount, i => {
        tileMap[i] = randI(1, tileCount);
      });
      
      // this is the movement direction of the map
      var worldDir = Math.PI / 4;
    
    
    
    /* =======================================================================
       Drawing the tileMap 
    ========================================================================*/
    
    
      var worldX = 512 * tileSize;
      var worldY = 512 * tileSize;
    
      function drawWorld() {
        const c = worldTileCount; // get the width of the tile array
        const s = tileSize; // get the tile size in pixels
        const tx = worldX / s | 0; // get the top left tile
        const ty = worldY / s | 0;
        const tW = (canvas.width / s | 0) + 2; // get the number of tiles to fit canvas
        const tH = (canvas.height / s | 0) + 2;
        // set the location
        if(dontAlignToPixel) {
            ctx.setTransform(1, 0, 0, 1, -worldX,-worldY);
            
        } else {
            ctx.setTransform(1, 0, 0, 1, Math.floor(-worldX),Math.floor(-worldY));
        }
        // Draw the tiles
        for (var y = 0; y < tH; y += 1) {
          for (var x = 0; x < tW; x += 1) {
            const i = tx + x + (ty + y) * c;
            const tindx = tileMap[i] === undefined ? 6 : tileMap[i];
            imageTools.drawSpriteQuick(tileSet, tindx, (tx + x) * s, (ty + y) * s);
          }
        }
    
      }
      var timer = 0;
      var refreshFrames = 0;
      const dirChangeMax = 3.5;
      const framesBetweenDirChange = 240;
      var dirChangeDelay = 1;
      var dirChange = 0;
      var prevDir = worldDir;
      const eCurve   = (v, p = 2) =>  v < 0 ? 0 : v > 1 ? 1 : v ** p / (v ** p + (1 - v) ** p); 
     
      //==============================================================
      // main render function
      function update() {
        refreshFrames ++;
        if(!refereshSkip || (refereshSkip && refreshFrames % 8 === 0)){
          timer += 1000 / 60;
          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;
          } else {
            ctx.clearRect(0, 0, w, h);
          }
        
          // Move the map
          var speed = Math.sin(timer / 10000) * 8;
          worldX += Math.cos(worldDir) * speed;
          worldY += Math.sin(worldDir) * speed;
          if(dirChangeDelay-- <= 0) {
            dirChangeDelay = framesBetweenDirChange;
            prevDir = worldDir = prevDir + dirChange;
            dirChange = rand(-dirChangeMax , dirChangeMax);
    
          }
          worldDir = prevDir + (1-eCurve(dirChangeDelay / framesBetweenDirChange,3)) * dirChange;
        
          // Draw the map
          drawWorld();
        }
        requestAnimationFrame(update);
      }
      requestAnimationFrame(update);
    }, 0);
    
    
    
    
    /*===========================================================================
      CODE FROM HERE DOWN UNRELATED TO THE ANSWER
      
      ===========================================================================*/
    
    
    
    
    
    
    const imageTools = (function() {
      // This interface is as is. No warenties no garenties, and NOT to be used comercialy
      var workImg, workImg1, keep; // for internal use
      keep = false;
      var tools = {
        canvas(width, height) { // create a blank image (canvas)
          var c = document.createElement("canvas");
          c.width = width;
          c.height = height;
          return c;
        },
        createImage: function(width, height) {
          var i = this.canvas(width, height);
          i.ctx = i.getContext("2d");
          return i;
        },
        drawSpriteQuick: function(image, spriteIndex, x, y) {
          var w, h, spr;
          spr = image.sprites[spriteIndex];
          w = spr.w;
          h = spr.h;
          ctx.drawImage(image, spr.x, spr.y, w, h, x, y, w, h);
        },
        line(x1, y1, x2, y2) {
          ctx.moveTo(x1, y1);
          ctx.lineTo(x2, y2);
        },
        circle(x, y, r) {
          ctx.moveTo(x + r, y);
          ctx.arc(x, y, r, 0, Math.PI * 2);
        },
      };
      return tools;
    })();
    
    const doFor = (count, cb) => {
      var i = 0;
      while (i < count && cb(i++) !== true);
    }; // the ; after while loop is important don't remove
    const randI = (min, max = min + (min = 0)) => (Math.random() * (max - min) + min) | 0;
    const rand = (min = 1, max = min + (min = 0)) => Math.random() * (max - min) + min;
    const seededRandom = (() => {
      var seed = 1;
      return {
        max: 2576436549074795,
        reseed(s) {
          seed = s
        },
        random() {
          return seed = ((8765432352450986 * seed) + 8507698654323524) % this.max
        }
      }
    })();
    const randSeed = (seed) => seededRandom.reseed(seed | 0);
    const randSI = (min, max = min + (min = 0)) => (seededRandom.random() % (max - min)) + min;
    const randS = (min = 1, max = min + (min = 0)) => (seededRandom.random() / seededRandom.max) * (max - min) + min;
    const tileSize = 64;
    const tileCount = 7;
    
    function drawGrass(ctx, c1, c2, c3) {
      const s = tileSize;
      const gs = s / (8 * c3);
      ctx.fillStyle = c1;
      ctx.fillRect(0, 0, s, s);
    
      ctx.strokeStyle = c2;
      ctx.lineWidth = 2;
      ctx.lineCap = "round";
      ctx.beginPath();
      doFor(s, i => {
        const x = rand(-gs, s + gs);
        const y = rand(-gs, s + gs);
        const x1 = rand(x - gs, x + gs);
        const y1 = rand(y - gs, y + gs);
        imageTools.line(x, y, x1, y1);
        imageTools.line(x + s, y, x1 + s, y1);
        imageTools.line(x - s, y, x1 - s, y1);
        imageTools.line(x, y + s, x1, y1 + s);
        imageTools.line(x, y - s, x1, y1 - s);
      })
      ctx.stroke();
    }
    
    function drawTree(ctx, c1, c2, c3) {
    
      const seed = Date.now();
      const s = tileSize;
      const gs = s / 2;
      const gh = gs / 2;
      ctx.fillStyle = c1;
      ctx.strokeStyle = "#000";
      ctx.lineWidth = 2;
      ctx.save();
      ctx.shadowColor = "rgba(0,0,0,0.5)";
      ctx.shadowBlur = 4;
      ctx.shadowOffsetX = 8;
      ctx.shadowOffsetY = 8;
      randSeed(seed);
      ctx.beginPath();
      doFor(18, i => {
        const ss = 1 - i / 18;
        imageTools.circle(randS(gs - gh * ss, gs + gh * ss), randS(gs - gh * ss, gs + gh * ss), randS(gh / 4, gh / 2));
      })
      ctx.stroke();
      ctx.fill();
      ctx.restore();
      ctx.fillStyle = c2;
      ctx.strokeStyle = c3;
      ctx.lineWidth = 2;
      ctx.save();
    
      randSeed(seed);
      ctx.beginPath();
      doFor(18, i => {
        const ss = 1 - i / 18;
        imageTools.circle(randS(gs - gh * ss, gs + gh * ss) - 2, randS(gs - gh * ss, gs + gh * ss) - 2, randS(gh / 4, gh / 2) / 1.6);
      })
      ctx.stroke();
      ctx.fill();
      ctx.restore();
    
    
    }
    
    
    const tileRenders = [
      (ctx) => {
        drawGrass(ctx, "#4C4", "#4F4", 1)
      },
      (ctx) => {
        drawGrass(ctx, "#644", "#844", 2)
      },
      (ctx) => {
        tileRenders[0](ctx);
        drawTree(ctx, "#480", "#8E0", "#7C0")
      },
      (ctx) => {
        tileRenders[1](ctx);
        drawTree(ctx, "#680", "#AE0", "#8C0")
      },
      (ctx) => {
        drawGrass(ctx, "#008", "#00A", 4)
      },
      (ctx) => {
        drawGrass(ctx, "#009", "#00C", 4)
      },
      (ctx) => {
        drawGrass(ctx, "#00B", "#00D", 4)
      },
    ]
    const tileSet = imageTools.createImage(tileSize * tileCount, tileSize);
    const ctxMain = ctx;
    ctx = tileSet.ctx;
    tileSet.sprites = [];
    doFor(tileCount, i => {
      x = i * tileSize;
      ctx.save();
      ctx.setTransform(1, 0, 0, 1, x, 0);
      ctx.beginPath();
      ctx.rect(0, 0, tileSize, tileSize);
      ctx.clip()
      if (tileRenders[i]) {
        tileRenders[i](ctx)
      }
      tileSet.sprites.push({
        x,
        y: 0,
        w: tileSize,
        h: tileSize
      });
      ctx.restore();
    });
    ctx = ctxMain;
    canvas {
      position: absolute;
      top: 0px;
      left: 0px;
    }
    div {
      position: absolute;
      top: 8px;
      left: 8px;
      color: white;
    }
    #pixAlignInfo {
      color: yellow;
      cursor: pointer;
      border: 2px solid green;
      margin: 4px;
    }
    #pixAlignInfo:hover {
      color: white;
      background: #0008;
      cursor: pointer;
    }
    body {
      background: #49c;
    }
    <canvas id="canvas"></canvas>
    <div>Hold left button to slow to 1/8th<br>
      <span id="pixAlignInfo">Click this button to toggle pixel alignment. Alignment is ON</span></div>