27

我正在将图像绘制到画布元素。然后,我有依赖于要完成的过程的代码。我的代码如下所示:

var myContext = myCanvasElement.getContext('2d'),
    myImg = new Image();

myImg.onload = function() {
    myContext.drawImage(containerImg, 0, 0, 300, 300);
};

myImg.src = "someImage.png";

所以现在,我想在 drawImage 完成时收到通知。我检查了规范,但找不到事件或传递回调函数的可能性。到目前为止,我只是设置了一个超时,但这显然不是很可持续。你怎么解决这个问题?

4

5 回答 5

23

像几乎所有 Javascript 函数一样,它drawImage同步的,即它只会在它实际完成它应该做的事情后才返回。

也就是说,与大多数其他 DOM 调用一样,它应该做的是排队等待下次浏览器进入事件循环时重新绘制的事物列表。

没有可以专门注册的事件来告诉您什么时候发生,因为当可以调用任何此类事件处理程序时,重绘已经发生了。

于 2012-06-26T12:45:52.240 回答
9

Jef Claes在他的网站上很好地解释了这一点:

当脚本已经被解释和执行时,浏览器会异步加载图像。如果图像未完全加载,画布将无法呈现它。

幸运的是,这并不难解决。我们只需要等待开始绘制,直到我们收到来自图像的回调,通知加载已完成。

<script type="text/javascript">        
window.addEventListener("load", draw, true);

function draw(){                                    
    var img = new Image();
    img.src = "http://3.bp.blogspot.com/_0sKGHtXHSes/TPt5KD-xQDI/AAAAAAAAA0s/udx3iWAzUeo/s1600/aspnethomepageplusdevtools.PNG";                
    img.onload = function(){
        var canvas = document.getElementById('canvas');
        var context = canvas.getContext('2d');    

        context.drawImage(img, 0, 0);        
    };            
}                    

于 2013-08-13T15:31:45.767 回答
3

当图像加载时,您已经有一个事件,并且您做一件事(绘制)。为什么不做另一个并调用将做任何你想做的事情的函数drawImage呢?从字面上看:

myImg.onload = function() {
    myContext.drawImage(containerImg, 0, 0, 300, 300);
    notify(); // guaranteed to be called after drawImage
};
于 2012-06-26T13:32:42.547 回答
0

drawImage()因为 2D 画布上的任何绘图方法本身“大部分”是同步的。
您可以假设任何需要回读像素的代码都将具有更新的像素。此外,drawImage特别是,您甚至可以假设图像将“同步”完全解码,这可能需要一些时间来处理大图像。

从技术上讲,在大多数现代配置中,实际的绘画工作将推迟到 GPU,这意味着一些并行化和一些异步性,但回读将等待 GPU 完成它的工作并在这段时间内锁定 CPU。

然而,在画布上绘图只是将画布完全渲染到监视器的第一步。
然后画布需要通过 CSS 合成器,在那里它将沿着页面的其余部分绘制。这是推迟到下一个渲染步骤的内容。
alert()Chrome 中的当前确实阻止了 CSS 合成器,因此,即使画布缓冲区的实际像素已经更新,这些更改还没有被 CSS 合成器反映。(在 Firefoxalert()中触发一种“旋转事件循环”,它允许 CSS 合成器仍然启动,即使事件循环的全局任务被暂停)。

为了与 CSS 合成器挂钩,有一种requestPostAnimationFrame方法正在酝酿中,但显然最近被 Chrome 实验放弃了。

我们可以同时使用它requestAnimationFrame和一个 MessageEvent 来填充它以尽快挂钩到下一个任务(setTimeout通常优先级较低)。

现在,即使这requestPostAnimationFrame只是浏览器合成器启动时的一个事件,该图像仍然需要一段时间才能到达操作系统合成器和监视器(大约是一个完整的垂直同步帧)。

Windows 上的某些 Chrome 配置可以访问允许浏览器直接与 OS 合成器对话并绕过 CSS 合成器的快捷方式。要启用此选项,您可以使用desynchhronized设置为 true 的选项创建 2D 上下文。但是,此选项仅在少数配置中受支持。

下面是几乎所有这些的演示:

// requestPostAnimationFrame polyfill
if (typeof requestPostAnimationFrame !== "function") {
  (() => {
    const channel = new MessageChannel();
    const callbacks = [];
    let timestamp = 0;
    let called = false;
    let scheduled = false; // to make it work from rAF
    let inRAF = false; // to make it work from rAF
    channel.port2.onmessage = e => {
      called = false;
      const toCall = callbacks.slice();
      callbacks.length = 0;
      toCall.forEach(fn => {
        try {
          fn(timestamp);
        } catch (e) {}
      });
    }
    // We need to overwrite rAF to let us know we are inside an rAF callback
    // as to avoid scheduling yet an other rAF, which would be one painting frame late
    // We could have hooked an infinite loop on rAF, but this means
    // forcing the document to be animated all the time
    // which is bad for perfs
    const rAF = globalThis.requestAnimationFrame;
    globalThis.requestAnimationFrame = function(...args) {
      if (!scheduled) {
        scheduled = true;
        rAF.call(globalThis, (time) => inRAF = time);
        globalThis.requestPostAnimationFrame(() => {
          scheduled = false;
          inRAF = false;
        });
      }
      rAF.apply(globalThis, args);
    };
    globalThis.requestPostAnimationFrame = function(callback) {
      if (typeof callback !== "function") {
        throw new TypeError("Argument 1 is not callable");
      }
      callbacks.push(callback);
      if (!called) {
        if (inRAF) {
          timestamp = inRAF;
          channel.port1.postMessage("");
        } else {
          requestAnimationFrame((time) => {
            timestamp = time;
            channel.port1.postMessage("");
          });
        }
        called = true;
      }
    };
  })();
}

// now the demo

// if the current browser can use desync 2D context
// let's try it there too
// (I couldn't test it myself, so let me know in comments)
const supportsDesyncContext = CanvasRenderingContext2D.prototype.getContextAttributes && 
  document.createElement("canvas")
    .getContext("2d", { desynchronized: true })
    .getContextAttributes().desynchronized;

test(false);
if (supportsDesyncContext) {
  setTimeout(() => test(true), 1000);
}

async function test(desync) {
  const canvas = document.createElement("canvas");
  document.body.append(canvas);
  const ctx = canvas.getContext("2d", { desynchronized: desync });
  const blob = await fetch("https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png")
    .then((resp) => resp.ok && resp.blob());
  const bitmap = await createImageBitmap(blob);
  ctx.drawImage(bitmap, 0, 0, 300, 150);
  // schedule our callback after rendering
  requestPostAnimationFrame(() => {
    alert("Right after CSS compositing");
  });
  // prove that we actually already painted on the canvas
  // even if the CSS compositor hasn't kicked in yet
  const pixelOnCanvas = ctx.getImageData(120,120,1,1).data;
  alert("Before CSS compositing." + (desync ? " (desynchronized)": "") + "\nPixel on canvas: " + pixelOnCanvas);
}

于 2021-10-19T02:57:30.617 回答
-1

@MikeGledhill 的答案(已被删除)本质上是答案的开始,尽管它可以更好地解释它,并且浏览器当时可能并不都有requestAnimationFrame可用的 API:

像素的绘制发生在下一个动画帧中。这意味着如果您调用drawImage,屏幕像素实际上不会在那个时候更新,而是在下一个动画帧中更新。

这没有什么活动。

但!我们可以使用requestAnimationFrame在绘制(显示更新)发生之前为下一帧安排回调:

myImg.onload = function() {
    myContext.drawImage(containerImg, 0, 0, 300, 300);

    requestAnimationFrame(() => {
      // This function will run in the next animation frame, *right before*
      // the browser will update the pixels on the display (paint).

        // To ensure that we run logic *after* the display has been
        // updated, an option is to queue yet one more callback
        // using setTimeout.
        setTimeout(() => {
            // At this point, the page rendering has been updated with the
            // `drawImage` result (or a later frame's result, see below).
        }, 0)
    })
};

这里发生了什么:

requestAnimtionFrame调用安排了一个函数,该函数将在浏览器更新显示像素之前被调用。本次回调完成后,浏览器会在后续的tick中继续同步更新显示像素,非常类似于微任务。

类似于浏览器更新显示的“微任务”,发生在您的requestAnimationFrame回调之后,并且发生Promise.resolve().then()用户使用或await语句在回调中创建的所有用户创建的微任务之后。这意味着不能在绘制任务发生后立即(同步)触发延迟代码。

保证在下一个绘制任务之后触发逻辑的唯一方法是使用setTimeoutpostMessage技巧)从动画帧回调中将宏任务(而不是微任务)排队。从requestAnimationFrame回调排队的宏任务将在所有微任务和类似微任务的任务之后触发,包括更新像素的任务。setTimeout(或 postMessage)宏任务不会在动画帧微任务之后同步触发。

这种方法虽然并不完美。大多数情况下,从setTimeout(更可能是)排队的宏任务将在下一个动画帧和绘制周期之前postMessage触发。但是,由于(and ) 的规范,不能保证延迟将与我们指定的完全相同(在此示例中),并且浏览器可以自由使用启发式和/或硬编码值(如 2ms)来确定何时是运行(宏任务)回调的最快时间。setTimeoutpostMessage0setTimeout

由于宏任务调度的这种非保证的非同步性质,虽然实际上不太可能,但您的setTimeout(或postMessage)回调可能不仅在当前动画帧(以及更新显示的绘制周期)之后触发,而且在下一个动画帧(及其绘制任务)之后,这意味着宏任务回调对于您所针对的帧而言触发的可能性很小。使用postMessage代替时,此几率会降低setTimeout

话虽如此,除非您尝试编写捕获绘制像素并将它们与预期结果或类似结果进行比较的测试,否则您可能不应该做这种事情。

通常,您应该使用 来安排任何绘图逻辑(fe ctx.drawImage()requestAnimationFrame,永远不要依赖绘制更新的实际时间,并假设用户将看到浏览器 API 保证您指定他们看到的内容(浏览器有自己的自己的测试以确保其 API 工作)。

最后,我们不知道您的实际目标是什么。这个答案很可能与该目标无关。


postMessage这是使用技巧的相同示例:

let messageKey = 0

myImg.onload = function() {
    myContext.drawImage(containerImg, 0, 0, 300, 300);

    requestAnimationFrame(() => {
      // This function will run in the next animation frame, *right before*
      // the browser will update the pixels on the display (paint).

        const key = "Unique message key for after paint callback: "+ messageKey++

        // To ensure that we run logic *after* the display has been
        // updated, an option is to queue yet one more callback
        // using postMessage.

        const afterPaint = (event) => {
            // Ignore interference from any other messaging in the app.
            if (event.data != key) return

            removeEventListener('message', afterPaint)

            // At this point, the page rendering has been updated with the
            // `drawImage` result (or a later frame's result, but
            // more unlikely than with setTimeout, as per above).
        }

        addEventListener('message', afterPaint)

         // Hack: send a message which arrives back to us in a
         // following macrotask, more likely sooner than with
         // setTimeout.
        postMessage(key, '*')
    })
};
于 2021-10-18T22:40:28.007 回答