How to fix blurry text in my HTML5 canvas?

I am full n00b with HTML5 and working with canvas to render shapes, colors and text. In my application, I have a view adapter that creates a canvas dynamically and fills it with content. This works really nicely, except that my text looks very fuzzy / blurry / stretched. I have seen many other posts about why determining width and height in CSS will cause this problem, but I define all of this in javascript .

Relevant code (view Fiddle ):

HTML

 <div id="layout-content"></div> 

Javascript

 var width = 500;//FIXME:size.w; var height = 500;//FIXME:size.h; var canvas = document.createElement("canvas"); //canvas.className="singleUserCanvas"; canvas.width=width; canvas.height=height; canvas.border = "3px solid #999999"; canvas.bgcolor = "#999999"; canvas.margin = "(0, 2%, 0, 2%)"; var context = canvas.getContext("2d"); ////////////////// //// SHAPES //// ////////////////// var left = 0; //draw zone 1 rect context.fillStyle = "#8bacbe"; context.fillRect(0, (canvas.height*5/6)+1, canvas.width*1.5/8.5, canvas.height*1/6); left = left + canvas.width*1.5/8.5; //draw zone 2 rect context.fillStyle = "#ffe381"; context.fillRect(left+1, (canvas.height*5/6)+1, canvas.width*2.75/8.5, canvas.height*1/6); left = left + canvas.width*2.75/8.5 + 1; //draw zone 3 rect context.fillStyle = "#fbbd36"; context.fillRect(left+1, (canvas.height*5/6)+1, canvas.width*1.25/8.5, canvas.height*1/6); left = left + canvas.width*1.25/8.5; //draw target zone rect context.fillStyle = "#004880"; context.fillRect(left+1, (canvas.height*5/6)+1, canvas.width*0.25/8.5, canvas.height*1/6); left = left + canvas.width*0.25/8.5; //draw zone 4 rect context.fillStyle = "#f8961d"; context.fillRect(left+1, (canvas.height*5/6)+1, canvas.width*1.25/8.5, canvas.height*1/6); left = left + canvas.width*1.25/8.5 + 1; //draw zone 5 rect context.fillStyle = "#8a1002"; context.fillRect(left+1, (canvas.height*5/6)+1, canvas.width-left, canvas.height*1/6); //////////////// //// TEXT //// //////////////// //user name context.fillStyle = "black"; context.font = "bold 18px sans-serif"; context.textAlign = 'right'; context.fillText("User Name", canvas.width, canvas.height*.05); //AT: context.font = "bold 12px sans-serif"; context.fillText("AT: 140", canvas.width, canvas.height*.1); //AB: context.fillText("AB: 94", canvas.width, canvas.height*.15); //this part is done after the callback from the view adapter, but is relevant here to add the view back into the layout. var parent = document.getElementById("layout-content"); parent.appendChild(canvas); 

The results that I see (in Safari) are much more distorted than shown in Fiddle:

Mine

Rendered output in safari

Fiddle

Rendered output on JSFiddle

What am I doing wrong? Do I need a separate canvas for each text element? Is this a font? Should I first define the canvas in the HTML5 layout? Is there a typo? I am lost.

+76
javascript html5 html5-canvas
Mar 27 '13 at 14:25
source share
9 answers

The canvas element works regardless of the pixel ratio of the device or monitor.

On iPad 3+, this ratio is 2. This essentially means that now a width of 1000 pixels wide will fill 2000 pixels to fit the specified width on the iPad display. Fortunately for us, this is done automatically by the browser. On the other hand, this is also the reason that you see fewer definitions on the images and canvas elements that were made to directly correspond to their visible area. Since your canvas only knows how to fill 1000px, but it will be asked to make 2000px, the browser should now intelligently fill in the spaces between the pixels in order to display the element in the correct size.

I highly recommend you read this article from HTML5Rocks , which explains in more detail how to create high-definition elements.

TL, others? Here is an example (based on the aunt above) that I use in my own projects to spill the canvas with the correct resolution:

 var PIXEL_RATIO = (function () { var ctx = document.createElement("canvas").getContext("2d"), dpr = window.devicePixelRatio || 1, bsr = ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1; return dpr / bsr; })(); createHiDPICanvas = function(w, h, ratio) { if (!ratio) { ratio = PIXEL_RATIO; } var can = document.createElement("canvas"); can.width = w * ratio; can.height = h * ratio; can.style.width = w + "px"; can.style.height = h + "px"; can.getContext("2d").setTransform(ratio, 0, 0, ratio, 0, 0); return can; } //Create canvas with the device resolution. var myCanvas = createHiDPICanvas(500, 250); //Create canvas with a custom resolution. var myCustomCanvas = createHiDPICanvas(500, 200, 4); 

Hope this helps!

+132
Mar 27 '13 at 18:03
source share
— -

Solved!

I decided to see what changes in the width and height attributes I set in javascript to see how this affected the size of the canvas - and it is not. He changes the resolution.

To get the result that I wanted, I also had to set the canvas.style.width attribute, which changes the physical size of the canvas :

 canvas.width=1000;//horizontal resolution (?) - increase for better looking text canvas.height=500;//vertical resolution (?) - increase for better looking text canvas.style.width=width;//actual width of canvas canvas.style.height=height;//actual height of canvas 
+24
Mar 27 '13 at 14:44
source share

I am resizing the canvas element via css to get the full width of the parent element. I noticed that the width and height of the element do not scale. I was looking for the best way to set the size that should be.

 canvas.width = canvas.clientWidth; canvas.height = canvas.clientHeight; 

This is a simple way that your canvas will be set perfectly, no matter what screen you use.

+5
Aug 27 '16 at 13:30
source share

I adapted MyNameIsKo code a bit under canvg (SVG for Canvas js library). I was a little embarrassed and spent some time on this. Hope this helps someone.

HTML

 <div id="chart"><canvas></canvas><svg>Your SVG here</svg></div> 

Javascript

 window.onload = function() { var PIXEL_RATIO = (function () { var ctx = document.createElement("canvas").getContext("2d"), dpr = window.devicePixelRatio || 1, bsr = ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1; return dpr / bsr; })(); setHiDPICanvas = function(canvas, w, h, ratio) { if (!ratio) { ratio = PIXEL_RATIO; } var can = canvas; can.width = w * ratio; can.height = h * ratio; can.style.width = w + "px"; can.style.height = h + "px"; can.getContext("2d").setTransform(ratio, 0, 0, ratio, 0, 0); } var svg = document.querySelector('#chart svg'), canvas = document.querySelector('#chart canvas'); var svgSize = svg.getBoundingClientRect(); var w = svgSize.width, h = svgSize.height; setHiDPICanvas(canvas, w, h); var svgString = (new XMLSerializer).serializeToString(svg); var ctx = canvas.getContext('2d'); ctx.drawSvg(svgString, 0, 0, w, h); } 
+3
Nov 12 '16 at 4:01
source share

This 100% solved it for me:

 var canvas = document.getElementById('canvas'); canvas.width = canvas.getBoundingClientRect().width; canvas.height = canvas.getBoundingClientRect().height; 

(this is close to the decision of Adam Mankovsky).

+3
Jan 17 '18 at 19:56
source share

For those of you who work in actjs, I adapted MyNameIsKo's answer and it works great. Here is the code

 import React from 'react' export default class CanvasComponent extends React.Component { constructor(props) { this.calcRatio = this.calcRatio.bind(this); } // Use componentDidMount to draw on the canvas componentDidMount() { this.updateChart(); } calcRatio() { let ctx = document.createElement("canvas").getContext("2d"), dpr = window.devicePixelRatio || 1, bsr = ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1; return dpr / bsr; } // Draw on the canvas updateChart() { // Adjust resolution const ratio = this.calcRatio(); this.canvas.width = this.props.width * ratio; this.canvas.height = this.props.height * ratio; this.canvas.style.width = this.props.width + "px"; this.canvas.style.height = this.props.height + "px"; this.canvas.getContext("2d").setTransform(ratio, 0, 0, ratio, 0, 0); const ctx = this.canvas.getContext('2d'); // now use ctx to draw on the canvas } render() { return ( <canvas ref={el=>this.canvas=el} width={this.props.width} height {this.props.height}/> ) } } 

In this example, I pass the width and height of the canvas as a props.

+2
Apr 23 '18 at 15:24
source share

Try this CSS line on your canvas: image-rendering: pixelated

According to MDN :

When enlarging an image, it is necessary to use the nearest neighbor algorithm so that the image is composed of large pixels.

Thus, it completely prevents anti-aliasing.

+1
May 31 '19 at 11:20
source share

I noticed a detail not mentioned in other answers. Canvas resolution is truncated to integer values.

The default canvas resolution sizes are: canvas.width: 300 and canvas.height: 150 .

On my screen window.devicePixelRatio: 1.75 .

Therefore, when I set canvas.height = 1.75 * 150 value is truncated from the desired 262.5 to 262 .

The solution is to select the CSS layout size for the given window.devicePixelRatio so that no truncation occurs when scaling the resolution.

For example, I could use width: 300px and height: 152px which would allow height: 152px to get integers when multiplied by 1.75 .

Edit : Another solution is to take advantage of the fact that CSS pixels can be fractional to counteract the truncation of canvas pixel scaling.

The following is a demonstration of the use of this strategy.

Edit : Here is an OP script updated to use this strategy: http://jsfiddle.net/65maD/83/ .

 main(); // Rerun on window resize. window.addEventListener('resize', main); function main() { // Prepare canvas with properly scaled dimensions. scaleCanvas(); // Test scaling calculations by rendering some text. testRender(); } function scaleCanvas() { const container = document.querySelector('#container'); const canvas = document.querySelector('#canvas'); // Get desired dimensions for canvas from container. let {width, height} = container.getBoundingClientRect(); // Get pixel ratio. const dpr = window.devicePixelRatio; // (Optional) Report the dpr. document.querySelector('#dpr').innerHTML = dpr.toFixed(4); // Size the canvas a bit bigger than desired. // Use exaggeration = 0 in real code. const exaggeration = 20; width = Math.ceil (width * dpr + exaggeration); height = Math.ceil (height * dpr + exaggeration); // Set the canvas resolution dimensions (integer values). canvas.width = width; canvas.height = height; /*----------------------------------------------------------- - KEY STEP - Set the canvas layout dimensions with respect to the canvas resolution dimensions. (Not necessarily integer values!) -----------------------------------------------------------*/ canvas.style.width = '${width / dpr}px'; canvas.style.height = '${height / dpr}px'; // Adjust canvas coordinates to use CSS pixel coordinates. const ctx = canvas.getContext('2d'); ctx.scale(dpr, dpr); } function testRender() { const canvas = document.querySelector('#canvas'); const ctx = canvas.getContext('2d'); // fontBaseline is the location of the baseline of the serif font // written as a fraction of line-height and calculated from the top // of the line downwards. (Measured by trial and error.) const fontBaseline = 0.83; // Start at the top of the box. let baseline = 0; // 50px font text ctx.font = '50px serif'; ctx.fillText("Hello World", 0, baseline + fontBaseline * 50); baseline += 50; // 25px font text ctx.font = '25px serif'; ctx.fillText("Hello World", 0, baseline + fontBaseline * 25); baseline += 25; // 12.5px font text ctx.font = '12.5px serif'; ctx.fillText("Hello World", 0, baseline + fontBaseline * 12.5); } 
 /* HTML is red */ #container { background-color: red; position: relative; /* Setting a border will mess up scaling calculations. */ /* Hide canvas overflow (if any) in real code. */ /* overflow: hidden; */ } /* Canvas is green */ #canvas { background-color: rgba(0,255,0,.8); animation: 2s ease-in-out infinite alternate both comparison; } /* animate to compare HTML and Canvas renderings */ @keyframes comparison { 33% {opacity:1; transform: translate(0,0);} 100% {opacity:.7; transform: translate(7.5%,15%);} } /* hover to pause */ #canvas:hover, #container:hover > #canvas { animation-play-state: paused; } /* click to translate Canvas by (1px, 1px) */ #canvas:active { transform: translate(1px,1px) !important; animation: none; } /* HTML text */ .text { position: absolute; color: white; } .text:nth-child(1) { top: 0px; font-size: 50px; line-height: 50px; } .text:nth-child(2) { top: 50px; font-size: 25px; line-height: 25px; } .text:nth-child(3) { top: 75px; font-size: 12.5px; line-height: 12.5px; } 
 <!-- Make the desired dimensions strange to guarantee truncation. --> <div id="container" style="width: 313.235px; height: 157.122px"> <!-- Render text in HTML. --> <div class="text">Hello World</div> <div class="text">Hello World</div> <div class="text">Hello World</div> <!-- Render text in Canvas. --> <canvas id="canvas"></canvas> </div> <!-- Interaction instructions. --> <p>Hover to pause the animation.<br> Click to translate the green box by (1px, 1px).</p> <!-- Color key. --> <p><em style="color:red">red</em> = HTML rendered<br> <em style="color:green">green</em> = Canvas rendered</p> <!-- Report pixel ratio. --> <p>Device pixel ratio: <code id="dpr"></code> <em>(physical pixels per CSS pixel)</em></p> <!-- Info. --> <p>Zoom your browser to re-run the scaling calculations. (<code>Ctrl+</code> or <code>Ctrl-</code>)</p> 
0
Jan 03 '19 at 17:48
source share

For me, only a combination of various “pixel perfect” techniques helped archive the results:

  1. Get and scale with pixel ratio as suggested by @MyNameIsKo.

    pixelRatio = window.devicePixelRatio / ctx.backingStorePixelRatio

  2. Scale the canvas when resizing (avoid stretching the canvas by default).

  3. multiply lineWidth by pixelRatio to find the correct "real" pixel line thickness:

    context.lineWidth = thickness * pixelRatio;

  4. Check if the line thickness is odd or even. add half pixelRatio to the line position for odd thickness values.

    x = x + pixelRatio / 2;

The odd line will be placed in the middle of the pixel. The line above is used to move it a bit.

 function getPixelRatio(context) { dpr = window.devicePixelRatio || 1, bsr = context.webkitBackingStorePixelRatio || context.mozBackingStorePixelRatio || context.msBackingStorePixelRatio || context.oBackingStorePixelRatio || context.backingStorePixelRatio || 1; return dpr / bsr; } var canvas = document.getElementById('canvas'); var context = canvas.getContext("2d"); var pixelRatio = getPixelRatio(context); var initialWidth = canvas.clientWidth * pixelRatio; var initialHeight = canvas.clientHeight * pixelRatio; window.addEventListener('resize', function(args) { rescale(); redraw(); }, false); function rescale() { var width = initialWidth * pixelRatio; var height = initialHeight * pixelRatio; if (width != context.canvas.width) context.canvas.width = width; if (height != context.canvas.height) context.canvas.height = height; context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); } function pixelPerfectLine(x) { context.save(); context.beginPath(); thickness = 1; // Multiple your stroke thickness by a pixel ratio! context.lineWidth = thickness * pixelRatio; context.strokeStyle = "Black"; context.moveTo(getSharpPixel(thickness, x), getSharpPixel(thickness, 0)); context.lineTo(getSharpPixel(thickness, x), getSharpPixel(thickness, 200)); context.stroke(); context.restore(); } function pixelPerfectRectangle(x, y, w, h, thickness, useDash) { context.save(); // Pixel perfect rectange: context.beginPath(); // Multiple your stroke thickness by a pixel ratio! context.lineWidth = thickness * pixelRatio; context.strokeStyle = "Red"; if (useDash) { context.setLineDash([4]); } // use sharp x,y and integer w,h! context.strokeRect( getSharpPixel(thickness, x), getSharpPixel(thickness, y), Math.floor(w), Math.floor(h)); context.restore(); } function redraw() { context.clearRect(0, 0, canvas.width, canvas.height); pixelPerfectLine(50); pixelPerfectLine(120); pixelPerfectLine(122); pixelPerfectLine(130); pixelPerfectLine(132); pixelPerfectRectangle(); pixelPerfectRectangle(10, 11, 200.3, 443.2, 1, false); pixelPerfectRectangle(41, 42, 150.3, 443.2, 1, true); pixelPerfectRectangle(102, 100, 150.3, 243.2, 2, true); } function getSharpPixel(thickness, pos) { if (thickness % 2 == 0) { return pos; } return pos + pixelRatio / 2; } rescale(); redraw(); 
 canvas { image-rendering: -moz-crisp-edges; image-rendering: -webkit-crisp-edges; image-rendering: pixelated; image-rendering: crisp-edges; width: 100vh; height: 100vh; } 
 <canvas id="canvas"></canvas> 

The Resize event does not fire, so you can try the file on GitHub

0
Jul 11 '19 at 21:36
source share



All Articles