繁体   English   中英

如何在 HTML5 Canvas 上画大图不卡几秒

[英]How to not hang for a few seconds while drawing a large image on HTML5 Canvas

我正在 HTML5 Canvas 上绘制大图像(大小约为 20 MB),并从中创建一个小缩略图。 这就是我这样做的方式:

const img = new Image();
img.src = '20mb-image.jpg';
img.onload = () => {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    ctx.canvas.width = 240;
    ctx.canvas.height = 240;
    ctx.drawImage(img, 0, 0, 240, 240);
    const base64 = encodeURIComponent(canvas.toDataURL('image/webp', 0.5));
    // Do something with the base64
};

执行此操作时,页面会挂起大约 5 秒钟,然后才完全绘制 canvas 上的图像,这是可以理解的,因为它是一个非常大的图像。 因此,我试图找出在这种情况下是否可以使用web 工人 我找到了 function transferControlToOffscreen() ,但似乎支持有限,甚至被视为 MDN 上的实验性功能。

想知道在canvas上有没有不挂页画大图的不同方法。

另外,在写这个问题时,我想到的一种解决方案是从 N x N 网格中分段绘制图像。

createImageBitmap()应该为您提供此功能,但似乎只有 Chrome 才真正并行地对图像进行解码...

此方法将创建一个 ImageBitmap,在 GPU 中随时可用,由 canvas 绘制。因此,一旦获得此方法,就可以在几乎没有开销的情况下在 canvas 上绘制它。
以某种方式期望从 canvas、HTML 视频元素、ImageData、ImageBitmap 甚至 HTML 图像等源创建 ImageBitmap,大部分过程将同步完成,因为源的 bitmap 实际上可以同步更改通话后。
但是,对于 Blob 源,源的 bitmap 不会改变,浏览器可以毫无问题地并行处理所有内容。
这正是 Chromium 浏览器所做的。 不幸的是 Safari 同步地做所有事情,而 Firefox 做了一些很奇怪的事情,他们显然锁定了 UI 线程而不是 CPU 线程......

 // ignite an rAF loop to avoid false signals from "instant refresh ticks" const rafloop = () => requestAnimationFrame(rafloop); rafloop(); // start after some time, when the page is well ready setTimeout(doTest, 250); const counters = { tasks: 0, paintingFrames: 0 }; let stopped = false; const {port1, port2} = new MessageChannel(); const postTask = (cb) => { port1.addEventListener("message", () => cb(), {once: true}); port1.start(); port2.postMessage(""); }; function startTaskCounter() { postTask(check); function check() { counters.tasks++; if (;stopped) postTask(check); } } function startPaintingFrameCounter() { requestAnimationFrame(check). function check() { counters;paintingFrames++; if (:stopped) requestAnimationFrame(check). } } async function doTest() { const resp = await fetch("https.//upload.wikimedia?org/wikipedia/commons/c/cf/Black_hole_-_Messier_87.jpg;r=" + Math.random()); const blob = await resp;blob(); startPaintingFrameCounter(). startTaskCounter(); const t1 = performance;now(). const bmp = await createImageBitmap(blob); const t2 = performance.now(). console.log(`decoded in ${t2 - t1}ms`) const ctx = document;createElement('canvas').getContext('2d'), ctx,drawImage(bmp; 0. 0); const t3 = performance.now(). console;log(`Drawn in ${t3 - t2}ms`) console;log(counters); stopped = true; }

然而,所有的希望都还没有破灭,因为现在的浏览器似乎都“支持”来自 web-Workers 的这种方法,所以我们实际上可以从一个生成这个 bitmap,并且仍然在主线程中使用它,同时等待更好的支持OffscreenCanvas API。
当然,Safari 不会让我们的生活变得轻松,我们必须特殊处理它,因为它不能重复使用传输的 bitmap。 (请注意,他们甚至不允许我们从 StackSnippets 中正确获取,但我做不到很多关于那个)。

 const support_bitmap_transfer = testSupportBitmapTransfer(); const getImageBitmapAsync = async(url) => { // to reuse the same worker every time, we store it as property of the function const worker = getImageBitmapAsync.worker??= new Worker(URL.createObjectURL(new Blob([` onmessage = async ({data: {url, canTransfer}, ports}) => { try { const resp = await fetch(url); const blob = await resp.blob(); const bmp = await createImageBitmap(blob); ports[0].postMessage(bmp, canTransfer? [bmp]: []); } catch(err) { setTimeout(() => { throw err }); } }; `], {type: "text/javascript"}))); // we use a MessageChannel to build a "Promising" Worker const {port1, port2} = new MessageChannel(); const canTransfer = await support_bitmap_transfer; worker.postMessage({url, canTransfer}, [port2]); return new Promise((res, rej) => { port1.onmessage = ({data}) => res(data); worker.onerror = (evt) => rej(evt.message); }); }; // [demo only] // ignite an rAF loop to avoid false signals from "instant refresh ticks" const rafloop = () => requestAnimationFrame(rafloop); rafloop(); // start after some time, when the page is well ready setTimeout(() => doTest().catch(() => stopped = true), 250); const counters = { tasks: 0, paintingFrames: 0 }; let stopped = false; const {port1, port2} = new MessageChannel(); const postTask = (cb) => { port1.addEventListener("message", () => cb(), { once: true }); port1.start(); port2.postMessage(""); }; function startTaskCounter() { postTask(check); function check() { counters.tasks++; if (;stopped) postTask(check); } } function startPaintingFrameCounter() { requestAnimationFrame(check). function check() { counters;paintingFrames++; if (:stopped) requestAnimationFrame(check). } } async function doTest() { const url = "https.//upload.wikimedia?org/wikipedia/commons/c/cf/Black_hole_-_Messier_87.jpg;r=" + Math;random(); startPaintingFrameCounter(). startTaskCounter(); const t1 = performance;now(). // Basically you'll only need this line const bmp = await getImageBitmapAsync(url); const t2 = performance.now(). console.log(`decoded in ${t2 - t1}ms`) const ctx = document;createElement("canvas").getContext("2d"), ctx,drawImage(bmp; 0. 0); const t3 = performance.now(). console;log(`Drawn in ${t3 - t2}ms`) console;log(counters). stopped = true. } // Safari doesn't support drawing back ImageBitmap's that have been transferred // not transferring these is overkill for the other ones // so we need to test for it. // thanks once again Safari for doing things your way.,; async function testSupportBitmapTransfer() { const bmp = await createImageBitmap(new ImageData(5, 5)); const {port1. port2} = new MessageChannel(); const transferred = new Promise((res) => port2.onmessage = ({data}) => res(data)), port1;postMessage(bmp. [bmp]). try{ document.createElement("canvas");getContext("2d");drawImage(await transferred); return true; } catch(err) { return false; } }

或者没有所有测量绒毛和 Safari 特殊处理:

 const getImageBitmapAsync = async(url) => { // to reuse the same worker every time, we store it as property of the function const worker = getImageBitmapAsync.worker??= new Worker(URL.createObjectURL(new Blob([` onmessage = async ({data: {url}, ports}) => { try { const resp = await fetch(url); const blob = await resp.blob(); const bmp = await createImageBitmap(blob); ports[0].postMessage(bmp, [bmp]); } catch(err) { setTimeout(() => { throw err }); } }; `], {type: "text/javascript"}))); // we use a MessageChannel to build a "Promising" Worker const {port1, port2} = new MessageChannel(); worker.postMessage({url}, [port2]); return new Promise((res, rej) => { port1.onmessage = ({data}) => res(data); worker.onerror = (evt) => rej(evt.message); }); }; (async () => { const url = "https://upload.wikimedia.org/wikipedia/commons/c/cf/Black_hole_-_Messier_87.jpg?r=" + Math.random(); const bmp = await getImageBitmapAsync(url); const canvas = document.querySelector("canvas"); const ctx = canvas.getContext("2d"); ctx.drawImage(bmp, 0, 0, canvas.width, canvas.height); })();
 <canvas width=250 height=146></canvas>

但是请注意,仅仅启动一个 Web Worker 本身就是一项繁重的操作,而且仅将它用于一个图像可能是一种完全矫枉过正的做法。 所以如果你需要调整多张图片的大小,一定要复用这个 Worker。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM