Sharp Canvas Rendering på High-DPI och Retina-skärmar
Varför kanvasgrafik ser suddig ut på Retina- och hög-DPI-skärmar, och hur man fixar det med devicePixelRatio, inklusive hantering av storlek och prestandagränser.
Core Takeaway
En duk ser suddig ut på skärmar med hög densitet när dess pixelstorlek matchar storleken CSS, eftersom varje dukpixel är utsträckt över flera fysiska pixlar. Åtgärda det genom att ställa in dukens bredd och höjd till storleken CSS multiplicerat med window.devicePixelRatio, behåll CSS-storleken oförändrad och skala ritningskontexten med samma förhållande. Uppdatera den vid ändra storlek och zooma.
Varför dukar suddas ut
MDN beskriver devicePixelRatio som förhållandet mellan fysiska pixlar och CSS pixlar på den aktuella skärmen: 1 på en klassisk 96 DPI-skärm och 2 på typiska HiDPI- eller Retina-skärmar. En duk har sin egen pixelbuffert. Om bufferten är 300 gånger 150 pixlar men visas med 300 gånger 150 CSS pixlar på en skärm med förhållandet 2, sträcker webbläsaren den över 600 gånger 300 fysiska pixlar, vilket ser mjukt ut.
Fixningen
MDN rekommenderade tillvägagångssätt är att använda devicePixelRatio för att lägga till den extra pixeltätheten: ställ in canvasbufferten till CSS storlek gånger förhållandet, håll CSS-storleken densamma och skala 2D-kontexten så att din ritkod kan fortsätta använda CSS-pixlar. Se koden nedan.
Hantera storleksändringar och zoom
Förhållandet ändras när ett fönster flyttas till en annan bildskärm eller användaren zoomar in sidan. Beräkna om vid storleksändring och lyssna efter ändringar i upplösningsmediefrågan så att du kan rita om med den nya densiteten. En ResizeObserver på canvaselementet är ett tillförlitligt sätt att hålla storleken synkroniserad med layouten.
Prestandagränser
Fördubbling av förhållandet fyrdubblar pixlarna att rita. På mycket stora dukar eller enkla enheter, begränsa förhållandet, till exempel vid 2, och mät bildtider. Animation runtimes som drar till canvas, som Lottie:s canvas renderer, Rive och ExodeUI, hanterar denna skalning åt dig, men samma avvägning gäller.
Produktionsimplementeringsutdrag
function setupCanvas(canvas) {
const ctx = canvas.getContext('2d');
function resize() {
const ratio = Math.min(window.devicePixelRatio || 1, 2); // cap for performance
const { width, height } = canvas.getBoundingClientRect(); // CSS size
canvas.width = Math.round(width * ratio); // backing store in device pixels
canvas.height = Math.round(height * ratio);
ctx.setTransform(ratio, 0, 0, ratio, 0, 0); // draw in CSS pixels
draw(ctx, width, height);
}
new ResizeObserver(resize).observe(canvas);
resize();
}
function draw(ctx, width, height) {
ctx.clearRect(0, 0, width, height);
ctx.lineWidth = 1;
ctx.strokeRect(0.5, 0.5, width - 1, height - 1); // crisp 1px border
}Nyckel Principles att komma ihåg
- devicePixelRatio = fysiska pixlar per CSS pixel (2 på typiska Retina-skärmar)
- Ställ in canvas.width/height till CSS storlek × förhållande; behåll storleken CSS
- Skala sammanhanget så att ritkoden använder CSS pixlar
- Beräkna om vid ändra storlek, övervaka förändring och zooma
- Begränsa förhållandet på stora dukar för prestanda
Vanliga frågor
Källor
- Window: devicePixelRatio property — MDN Web Docs