【问题标题】:Is there a better way to repeat display multiple images in the same spot?有没有更好的方法可以在同一个位置重复显示多个图像?
【发布时间】:2019-09-24 21:16:18
【问题描述】:

我正在使用 Canvas HTML5 创建一个小游戏,并尝试在另一个图像上显示 LED,目前它的工作方式如下:Image displayed

目前,图像确实按预期显示和更改,但函数并未立即初始化(我猜这是由于 Javascript 在一个内核上运行以及 setInterval 函数的工作原理)代码也看起来非常笨重且冗长-缠绕。

有没有更好的方法来实现这些图像的循环形成动画?

我打算添加更多动画,因为小游戏处于“空闲”状态,理想情况下,控制图像循环的功能应该很容易被破坏。


var canvas = document.querySelector('canvas');
var context = canvas.getContext('2d');

function drawSafeBuster(imageSources, callback) {
    var images = {};
    var loadedImages = 0;
    var numImages = 0;
    // get number of images
    for (var src in imageSources) {
        numImages++;
    }
    for (var src in imageSources) {
        images[src] = new Image();
        images[src].onload = function () {
            if (++loadedImages >= numImages) {
                callback(images);
            }
        };
        images[src].src = imageSources[src];
    }
}

//Image path variables.
var imageSources = {
    ledPath: './graphics/leds_safe_dial_minigame.png'
};

drawSafeBuster(imageSources, function (images) {

    //Draw initial LED images.
    context.drawImage(images.ledPath, 2, 0, 115, 100, 850, 300, 120, 100);
    context.drawImage(images.ledPath, 2, 0, 115, 100, 1015, 300, 120, 100);

    //LED Animation Loop
    var ledRepeat = setInterval(function () {
        context.fillStyle = '#999999';

        var ledRepeat1 = setInterval(function () {
            context.fillRect(850, 300, 120, 45);
            context.fillRect(1015, 300, 120, 45);
            context.drawImage(images.ledPath, 2, 0, 115, 100, 850, 300, 120, 100);
            context.drawImage(images.ledPath, 2, 0, 115, 100, 1015, 300, 120, 100);
        }, 500);

        var ledRepeat2 = setInterval(function () {
            context.fillRect(850, 300, 120, 45);
            context.fillRect(1015, 300, 120, 45);
            context.drawImage(images.ledPath, 120, 0, 115, 100, 850, 300, 120, 100);
            context.drawImage(images.ledPath, 120, 0, 115, 100, 1015, 300, 120, 100);
        }, 1500);

        var ledRepeat3 = setInterval(function () {
            context.fillRect(850, 300, 120, 45);
            context.fillRect(1015, 300, 120, 45);
            context.drawImage(images.ledPath, 238, 0, 115, 100, 850, 300, 120, 100);
            context.drawImage(images.ledPath, 238, 0, 115, 100, 1015, 300, 120, 100);
        }, 2500);

        var clearInterval = setInterval(function () {

            clearInterval(ledRepeat1);
            clearInterval(ledRepeat2);
            clearInterval(ledRepeat3);
        }, 3500);

    }, 4500);

});
}

【问题讨论】:

    标签: javascript html html5-canvas


    【解决方案1】:

    我建议让每个 LED 成为具有自己状态(开/关)的对象。创建一个游戏对象来跟踪游戏状态和当前时间/滴答声。 (这里有一些关于game loops 的简单读物)

    不确定您的确切要求,但这里有一个示例,说明我将如何处理类似问题。在理想的世界中,每个 requestAnimationFrame() 是 1/60 秒 ~ 60 帧/秒...请参阅上面的链接了解为什么这可能不是案例以及如何纠正它。

    我没有为 LED 使用图像,但这可以添加到 LED 对象并用于绘图功能。

    let canvas, c, w, h,
      TWOPI = Math.PI * 2;
    
    canvas = document.getElementById('canvas');
    c = canvas.getContext('2d');
    w = canvas.width = 600;
    h = canvas.height = 400;
    
    let game = {
      state: "RUNNING",
      tick: 0,
      actors: []
    };
    
    //LED object.
    let LED = function(x, y, hue, radius, on, toggleRate) {
      this.position = {
        x: x,
        y: y
      };
      this.hue = hue;
      this.radius = radius;
      this.on = on;
      this.toggleRate = toggleRate;
      this.update = function(tick) {
        if (tick % this.toggleRate === 0) {
          this.on = !this.on;
        }
      };
      this.draw = function(ctx) {
        ctx.beginPath();
        ctx.arc(this.position.x, this.position.y, this.radius, 0, TWOPI, false);
        ctx.fillStyle = `hsl(${this.hue}, ${this.on ? 80 : 20}%, ${this.on ? 70 : 30}%)`;
        ctx.fill();
        ctx.beginPath();
        ctx.arc(this.position.x + this.radius / 5, this.position.y - this.radius / 5, this.radius / 3, 0, TWOPI, false);
        ctx.fillStyle = `hsl(${this.hue}, ${this.on ? 80 : 20}%, ${this.on ? 90 : 50}%)`;
        ctx.fill();
      };
    }
    
    //create LEDs
    for (let i = 0; i < 10; i++) {
      game.actors.push(
        new LED(
          100 + i * 25,
          100,
          i * 360 / 10,
          8,
          Math.random() * 1 > 0.5 ? true : false,
          Math.floor(Math.random() * 240) + 60
        )
      );
    }
    
    function update() {
      if (game.state === "RUNNING") {
        //increase game counter
        game.tick++;
    
        //update actors
        for (let a = 0; a < game.actors.length; a++) {
          game.actors[a].update(game.tick);
        }
      } else {
        //noop.
      }
    }
    
    function clear() {
      c.clearRect(0, 0, w, h);
    }
    
    function draw() {
      //draw all actors
      for (let a = 0; a < game.actors.length; a++) {
        game.actors[a].draw(c);
      }
    }
    
    function loop() {
      update();
      clear();
      draw();
      requestAnimationFrame(loop);
    }
    
    canvas.addEventListener('click', function() {
      if (game.state === "RUNNING") {
        game.state = "PAUSED";
      } else {
        game.state = "RUNNING";
      }
      console.log(game.state);
    });
    
    requestAnimationFrame(loop);
    body {
      background: #222;
    }
    <!doctype html>
    <html>
    
    <head>
      <meta charset="utf-8">
    </head>
    
    <body>
      <canvas id="canvas"></canvas>
    </body>
    
    </html>

    【讨论】:

      【解决方案2】:

      为了获得最佳质量,请始终通过requestAnimationFrame 的回调渲染到画布。使用计时器进行渲染可能会导致动画闪烁和/或剪切。

      setTimeoutsetInterval 在页面不在焦点或不可见时被大多数浏览器限制。回调也不总是准时

      如果时间对于 1/60 秒很重要,请使用 performance.now 并将时间参数传递给 requestAnimationFrame 回调。由于动画每 1/60 秒 (16.66666...ms) 仅显示一次,因此您永远不会准时显示,因此请在所需时间后尽快绘制动画的下一帧(参见示例)

      根据你在问题中提供的信息,我无法确定你的动画是什么样的,所以我做了一些例子。

      示例

      • 在加载媒体时使用 promise 而不是回调
      • 图像添加了表示子图像(精灵)位置的属性。函数drawSprite 使用imageNamespriteIdxlocationName 在画布上的某个位置绘制子图像。
      • 主渲染循环是函数renderLoop,它等待媒体加载完毕,然后使用timing 来制作动画。
      • 数组timing 包含每个动画事件的对象。该对象具有事件的时间偏移量、要在该事件上调用的函数以及传递给函数的参数。
      • 本例中的最后一个计时对象,只是重置开始时间以重复动画。

      请注意,此示例不检查浏览器是否停止动画,而是会循环播放所有动画以赶上。

      requestAnimationFrame(renderLoop);
      canvas.width = 64;
      canvas.height = 16;
      const ctx = canvas.getContext("2d");
      var mediaLoaded = false;
      const images = {};
      var currentStage = 0;
      var startTime;   // do not assign a value to this here or the animation may not start
      loadMedia({ 
              leds: {
                  src: "https://i.stack.imgur.com/g4Iev.png",
                  sprites: [
                      {x: 0,  y: 0,  w: 16, h: 16}, // idx 0 red off
                      {x: 16, y: 0,  w: 16, h: 16}, // idx 1 red on
                      {x: 0,  y: 16, w: 16, h: 16}, // idx 2 orange off
                      {x: 16, y: 16, w: 16, h: 16}, // idx 3 orange on
                      {x: 0,  y: 32, w: 16, h: 16}, // idx 4 green off
                      {x: 16, y: 32, w: 16, h: 16}, // idx 5 green on
                      {x: 0,  y: 48, w: 16, h: 16}, // idx 6 cyan off
                      {x: 16, y: 48, w: 16, h: 16}, // idx 7 cyan on
                  ]
              },
          }, images)
          .then(() => mediaLoaded = true);
      
      const renderLocations = {
          a: {x: 0,  y: 0, w: 16, h: 16},
          b: {x: 16, y: 0, w: 16, h: 16},
          c: {x: 32, y: 0, w: 16, h: 16},
          d: {x: 48, y: 0, w: 16, h: 16},
      };
      
      
      function loadMedia(imageSources, images = {}) {
          return new Promise(allLoaded => {
              var count = 0;
              for (const [name, desc] of Object.entries(imageSources)) {
                  const image = new Image;
                  image.src = desc.src;
                  image.addEventListener("load",() => {
                      images[name] = image;
                      if (desc.sprites) { image.sprites = desc.sprites }
                      count --;
                      if (!count) { allLoaded(images) }
                  });
                  count ++;
              }
          });
      }
      
      function drawSprite(imageName, spriteIdx, locName) {
          const loc = renderLocations[locName];
          const spr = images[imageName].sprites[spriteIdx];
          ctx.drawImage(images[imageName], spr.x, spr.y, spr.w, spr.h, loc.x, loc.y, loc.w, loc.h);
      }
      function drawLeds(sprites) {
          for(const spr of sprites) { drawSprite(...spr) }
      }
      function resetAnimation() {
          currentStage = 0;
          startTime += 4500;
      }
      
      
      const timing = [
          {time: 0,    func: drawLeds, args: [[["leds", 0, "a"], ["leds", 2, "b"], ["leds", 4, "c"], ["leds", 6, "d"]]]},
          {time: 500,  func: drawLeds, args: [[["leds", 1, "a"]]]},
          {time: 1500, func: drawLeds, args: [[["leds", 0, "a"], ["leds", 3, "b"]]]},
          {time: 2000, func: drawLeds, args: [[["leds", 1, "a"]]]},
          {time: 3000, func: drawLeds, args: [[["leds", 0, "a"], ["leds", 2, "b"], ["leds", 5, "c"], ["leds", 7, "d"]]]},
          {time: 3250, func: drawLeds, args: [[["leds", 1, "d"]]]},
          {time: 3500, func: drawLeds, args: [[["leds", 3, "d"]]]},
          {time: 3750, func: drawLeds, args: [[["leds", 5, "d"]]]},
          {time: 4000, func: drawLeds, args: [[["leds", 7, "d"]]]},
          {time: 4250, func: drawLeds, args: [[["leds", 1, "d"]]]},
          {time: 4500 - 17, func: resetAnimation, args: []},
      ];
         
      function renderLoop(time) {
          if (mediaLoaded) {
              if (startTime === undefined) { startTime = time }
              var offsetTime = time - startTime;
              const stage = timing[currentStage];
              if(offsetTime > stage.time) {
                  currentStage++;
                  stage.func(...stage.args);
              }
          }
          requestAnimationFrame(renderLoop);
      }
      canvas {
        border:1px solid black;
      }
      &lt;canvas id="canvas"&gt;&lt;/canvas&gt;

      示例中使用的图片

      【讨论】:

        猜你喜欢
        • 2022-01-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-12-05
        • 1970-01-01
        • 2015-07-27
        • 2018-03-16
        • 1970-01-01
        相关资源
        最近更新 更多