简体   繁体   中英

HTML Canvas blurry in Safari but not Chrome after translate3d

Running into an interesting canvas bug: after translating a canvas, the pixels appear blurry on Safari but not Chrome.

I've tried just about every image-rendering and imageSmoothing trick to no avail.

Here's a codepen where I've been able to reproduce the issue: https://codepen.io/plillian/pen/RwQegyR

Is this a just Safari bug? Or is there a way to force nearest neighbor in Safari as well?

Yes this is a Safari bug, you may want to let them know about it. For what it's worth, it's still an issue in the latest Technology Preview (Safari 15.4, WebKit 17614.1.14.10.6) where it's not even able to render every frame on time and will just "blink".

As for a workaround, the only one I can think of would be to do this all on the canvas directly, you can easily make this resizing of an ImageData by first converting it to an ImageBitmap and use drawImage() .

Though to implement the scrolling behavior we'll have a bit of work to do.
One way is to use a placeholder <div> and make it act as-if we did transform our <canvas>. This way we can still use the native scrolling behavior and simply update the arguments to drawImage() .
We then can stick the canvas on the top left corner of the viewport, and set it to the size of the viewport, overcoming the issue of possibly having a too big canvas.

 (async () => { const SIZE = 1024; const X = -208.97398878415459; const Y = 47.03519866364394; const scale = 80; const viewport = document.getElementById('viewport'); const wrapper = document.getElementById('wrapper'); const placeholder = document.getElementById('placeholder'); const canvas = document.getElementById('cvs'); placeholder.style.width = SIZE + 'px'; placeholder.style.height = SIZE + 'px'; const c = canvas.getContext('2d'); const pixels = new Uint8ClampedArray(4 * SIZE * SIZE); for (let xi = 0; xi < SIZE; xi++) { for (let yi = 0; yi < SIZE; yi++) { const idx = (xi + yi * SIZE) * 4; pixels[idx] = (xi << 6) % 255; pixels[idx + 1] = (yi << 6) % 255; pixels[idx + 3] = 255; } } const pixelData = new ImageData(pixels, SIZE, SIZE); // Convert to an ImageBitmap for ease of resizing and cropping const bmp = await createImageBitmap(pixelData); // We resize the canvas bitmap based on the size of the viewport // While respecting the actual dPR (gimme crisp pixels!) // Thanks to gman for the reminder of how to suppport all early impl. // https://stackoverflow.com/a/65435847/3702797 const observer = new ResizeObserver(([entry]) => { let width; let height; const dPR = devicePixelRatio; if (entry.devicePixelContentBoxSize) { width = entry.devicePixelContentBoxSize[0].inlineSize; height = entry.devicePixelContentBoxSize[0].blockSize; } else if (entry.contentBoxSize) { if ( entry.contentBoxSize[0]) { width = entry.contentBoxSize[0].inlineSize * dPR; height = entry.contentBoxSize[0].blockSize * dPR; } else { width = entry.contentBoxSize.inlineSize * dPR; height = entry.contentBoxSize.blockSize * dPR; } } else { width = entry.contentRect.width * dPR; height = entry.contentRect.height * dPR; } canvas.width = width; canvas.height = height; canvas.style.width = (width / dPR) + 'px'; canvas.style.height = (height / dPR) + 'px'; c.scale(dPR, dPR); c.imageSmoothingEnabled = false; }); // observe the scrollbox size changes try { observer.observe(viewport, { box: 'device-pixel-content-box' }); } catch(err) { observer.observe(viewport, { box: 'content-box' }); } function getDrawImageArgs(nodetranslate) { const { width, height } = canvas; const { scrollLeft, scrollTop } = viewport; const mat = new DOMMatrix(nodetranslate).inverse(); const source = mat.transformPoint({ x: scrollLeft, y: scrollTop }); const sourceWidth = canvas.width; const sourceHeight = canvas.height; return [source.x, source.y, sourceWidth, sourceHeight, 0, 0, canvas.width * scale, canvas.height * scale]; } function animate() { const nodetranslate = `translate3D(${X}px, ${Y}px, 0px) scale(${scale})`; wrapper.style.transform = nodetranslate; c.clearRect(0, 0, canvas.width, canvas.height); c.drawImage(bmp, ...getDrawImageArgs(nodetranslate)); requestAnimationFrame(animate); } animate(); })().catch(console.error)
 body { margin: 0 } #viewport { position: absolute; top: 0; left: 0; width: 100vw; height: 100vh; overflow: auto; } .sticker { position: sticky; top: 0; left: 0; height: 0px; width: 0px; overflow: visible; line-height: 0; z-index: 1; } canvas { position: absolute; } #wrapper { transform-origin: 0 0; position: absolute; } #placeholder { display: inline-block; }
 <script> // Because Safari wouldn't be Safari without all its little bugs... // See https://stackoverflow.com/a/35503829/3702797 (()=>{if(function(){const e=document.createElement("canvas").getContext("2d");e.fillRect(0,0,40,40),e.drawImage(e.canvas,-40,-40,80,80,50,50,20,20);var a=e.getImageData(50,50,30,30),r=new Uint32Array(a.data.buffer),n=(e,t)=>r[t*a.width+e];return[[9,9],[20,9],[9,20],[20,20]].some(([e,t])=>0!==n(e,t))||[[10,10],[19,10],[10,19],[19,19]].some(([e,t])=>0===n(e,t))}()){const e=CanvasRenderingContext2D.prototype,i=e.drawImage;i?e.drawImage=function(e,t,a){if(!(9===arguments.length))return i.apply(this,[...arguments]);var r,n=function(e,t,a,r,n,i,o,h,m){var{width:s,height:d}=function(t){var e=e=>{e=globalThis[e];return e&&t instanceof e};{if(e("HTMLImageElement"))return{width:t.naturalWidth,height:t.naturalHeight};if(e("HTMLVideoElement"))return{width:t.videoWidth,height:t.videoHeight};if(e("SVGImageElement"))throw new TypeError("SVGImageElement isn't yet supported as source image.","UnsupportedError");return e("HTMLCanvasElement")||e("ImageBitmap")?t:void 0}}(e);r<0&&(t+=r,r=Math.abs(r));n<0&&(a+=n,n=Math.abs(n));h<0&&(i+=h,h=Math.abs(h));m<0&&(o+=m,m=Math.abs(m));var g=Math.max(t,0),u=Math.min(t+r,s),s=Math.max(a,0),d=Math.min(a+n,d),r=h/r,n=m/n;return[e,g,s,ug,ds,t<0?it*r:i,a<0?oa*n:o,(ug)*r,(ds)*n]}(...arguments);return r=n,[3,4,7,8].some(e=>!r[e])?void 0:i.apply(this,n)}:console.error("This script requires a basic implementation of drawImage")}})(); </script> <div id="viewport"> <div class="sticker"> <!-- <canvas> isn't a void element, it must have a closing tag --> <!-- We place it in a "sticky" element, outside of the one that gets transformed --> <canvas id="cvs"></canvas> </div> <div id="wrapper"> <div id="placeholder"><!-- We'll use it as an easy way to measure what part of the canvas we should draw based on the current scroll position. --></div> <div> </div>

You can inspect the <canvas> element and see it's actually only as big as the viewport and not some 81920x81920px.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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