Canvas Rendering (Blur)

I know that this question has been asked many times, but I tried almost everything I could find on the network, and still can’t get the text to display correctly on the canvas no matter what (and with any combinations) I tried.

For the problem with blurry lines and shapes, simply adding + 0.5px to the coordinates solves the problem: however, this solution does not seem to work for rendering text.

Note. I never use CSS to set the width and height of the canvas (I just tried once to check if the properties of the size parameter changed in both HTML and CSS). Also the problem is not related to the browser.

I tried:

  • creating a canvas using HTML, then with javascript instead of html
  • setting width and height in an HTML element, then using JS, and then with HTML and JS
  • add 0.5px to text coordinates with any possible combination
  • change font family and font size
  • change font size (px, pt, em)
  • open the file with different browsers to check if something has changed.
  • disable the alpha channel with canvas.getContext('2d', {alpha:false}) , which simply made most of my layers disappear without solving the problem.

See a comparison of canvas font rendering and html here: https://jsfiddle.net/balleronde/1e9a5xbf/

Is it even possible for text in the canvas to appear as text in the dom element? Any advice or suggestions would be greatly appreciated.

+6
source share
2 answers

DOM quality text on canvas.

Closer look

If you zoom in on the DOM text, you will see the following (the upper part of the canvas, the lower part of the DOM, the center hopes for the pixel size (not on the retina displays))

enter image description here

As you can see, there are colored sections in the lower text. This is because it was created using a method called a true type.

Note using true is an optional parameter in browsers and operating systems. If you turn off or use a device with a very low resolution, the enlarged text above will look the same (without color pixels in the lower image)

Pixels and auxiliary pixels

When you look closely at the LCD, you will see that each pixel consists of three sub pixels located in a row, one for red, green and blue. To set the pixel, you supply the RGB intensity for each color channel and set the corresponding RGB pixels. We usually accept that red is the first and blue is the last, but the reality is that it does not matter in which order the colors are, as long as they are close to each other, you will get the same result.

When you stop thinking about color and only about controlled image elements, you triple the horizontal resolution of your device. Since most texts are monochrome, you don’t have to worry too much about aligning the RGB subpixels, and you can display the text in the subpixel rather than the entire pixel, and thus get high quality text. The sub-pixels are so small that most people don't notice a slight color distortion, and this advantage is worth a bit of a dirty look.

Why there is no true type for canvas

When using auxiliary pixels, you need to have full control over each, including the alpha value. For display drivers, alpha applies to all auxiliary pixels of a pixel; you cannot have blue at alpha-0.2 and red at the same pixel at alpha-0.7. But if you know what subpixel values ​​are under each subpixel, you can do alpha calculations instead of letting the hardware do this. This gives you sub-pixel algha control.

Unfortunately (no ... lucky for 99.99% of cases), the canvas allows transparency, but you have no way of knowing what the subelements do under the canvas, they can be of any color, and therefore you cannot do alpha calculations it was necessary to effectively use auxiliary pixels.

Subpixel home text.

But you do not need to have a transparent canvas, and if you make all your pixels opaque (alpha = 1.0), you will restore alpha control of the subpixel.

The following function draws canvas text using subpixels. It's not very fast, but it gets better text.

It works by creating text 3 times the width. Then it uses extra pixels to calculate the values ​​of the subpixels, and when this is done, puts the subpixel data on the canvas.

Refresh . When I wrote this answer, I completely forgot about the zoom settings. The use of sub-pixel attributes corresponds to a match between the physical screen size and the DOM pixel size. If you zoomed in or out, it will not be so, and therefore finding sub-elements will be much more difficult.
I updated the demos to try to determine the zoom settings. Since there is no standard way to do this, I just used devicePixelRatio , which for FF and Chrome !== 1 when zoomed in (And since I don't have a retinal lattice, I can only guess if the bottom demo is working). If you want to see the demo correctly and you do not get a warning about scaling, although you are still zooming in, set the scale to 1.
Addistionaly, you can set the scale to 200% and use the bottom demo, since it seems that scaling significantly reduces the quality of the DOM text, and the canvas subpixel maintains high quality.

The upper text is normal. Canvas text, center (home), sub-pixel text on the canvas, and the bottom text is DOM text.

PLEASE note that if you have a Retina Display or a very high resolution display, you should view the snippet below this if you do not see a high quality canvas.

Standard demo from 1 to 1 pixel.

 var createCanvas =function(w,h){ var c = document.createElement("canvas"); c.width = w; c.height = h; c.ctx = c.getContext("2d"); // document.body.appendChild(c); return c; } // converts pixel data into sub pixel data var subPixelBitmap = function(imgData){ var spR,spG,spB; // sub pixels var id,id1; // pixel indexes var w = imgData.width; var h = imgData.height; var d = imgData.data; var x,y; var ww = w*4; var ww4 = ww+4; for(y = 0; y < h; y+=1){ for(x = 0; x < w; x+=3){ var id = y*ww+x*4; var id1 = Math.floor(y)*ww+Math.floor(x/3)*4; spR = Math.sqrt(d[id + 0] * d[id + 0] * 0.2126 + d[id + 1] * d[id + 1] * 0.7152 + d[id + 2] * d[id + 2] * 0.0722); id += 4; spG = Math.sqrt(d[id + 0] * d[id + 0] * 0.2126 + d[id + 1] * d[id + 1] * 0.7152 + d[id + 2] * d[id + 2] * 0.0722); id += 4; spB = Math.sqrt(d[id + 0] * d[id + 0] * 0.2126 + d[id + 1] * d[id + 1] * 0.7152 + d[id + 2] * d[id + 2] * 0.0722); d[id1++] = spR; d[id1++] = spG; d[id1++] = spB; d[id1++] = 255; // alpha always 255 } } return imgData; } // Assume default textBaseline and that text area is contained within the canvas (no bits hanging out) // Also this will not work is any pixels are at all transparent var subPixelText = function(ctx,text,x,y,fontHeight){ var width = ctx.measureText(text).width + 12; // add some extra pixels var hOffset = Math.floor(fontHeight *0.7); var c = createCanvas(width * 3,fontHeight); c.ctx.font = ctx.font; c.ctx.fillStyle = ctx.fillStyle; c.ctx.fontAlign = "left"; c.ctx.setTransform(3,0,0,1,0,0); // scale by 3 // turn of smoothing c.ctx.imageSmoothingEnabled = false; c.ctx.mozImageSmoothingEnabled = false; // copy existing pixels to new canvas c.ctx.drawImage(ctx.canvas,x -2, y - hOffset, width,fontHeight,0,0, width,fontHeight ); c.ctx.fillText(text,0,hOffset); // draw thw text 3 time the width // convert to sub pixel c.ctx.putImageData(subPixelBitmap(c.ctx.getImageData(0,0,width*3,fontHeight)),0,0); ctx.drawImage(c,0,0,width-1,fontHeight,x,y-hOffset,width-1,fontHeight); // done } var globalTime; // render loop does the drawing function update(timer) { // Main update loop globalTime = timer; ctx.setTransform(1,0,0,1,0,0); // set default ctx.globalAlpha= 1; ctx.fillStyle = "White"; ctx.fillRect(0,0,canvas.width,canvas.height) ctx.fillStyle = "black"; ctx.fillText("Canvas text is Oh hum "+ globalTime.toFixed(0),6,20); subPixelText(ctx,"Sub pixel text is best "+ globalTime.toFixed(0),6,45,25); div.textContent = "DOM is off course perfect "+ globalTime.toFixed(0); requestAnimationFrame(update); } function start(){ document.body.appendChild(canvas); document.body.appendChild(div); ctx.font = "20px Arial"; requestAnimationFrame(update); // start the render } var canvas = createCanvas(512,50); // create and add canvas var ctx = canvas.ctx; // get a global context var div = document.createElement("div"); div.style.font = "20px Arial"; div.style.background = "white"; div.style.color = "black"; if(devicePixelRatio !== 1){ var dir = "in" var more = ""; if(devicePixelRatio > 1){ dir = "out"; } if(devicePixelRatio === 2){ div.textContent = "Detected a zoom of 2. You may have a Retina display or zoomed in 200%. Please use the snippet below this one to view this demo correctly as it requiers a precise match between DOM pixel size and display physical pixel size. If you wish to see the demo anyways just click this text. "; more = "Use the demo below this one." }else{ div.textContent = "Sorry your browser is zoomed "+dir+".This will not work when DOM pixels and Display physical pixel sizes do not match. If you wish to see the demo anyways just click this text."; more = "Sub pixel display does not work."; } document.body.appendChild(div); div.style.cursor = "pointer"; div.title = "Click to start the demo."; div.addEventListener("click",function(){ start(); var divW = document.createElement("div"); divW.textContent = "Warning pixel sizes do not match. " + more; divW.style.color = "red"; document.body.appendChild(divW); }); }else{ start(); } 

1 to 2 pixels.

For retina, very high resolution or enhanced 200% of browsers.

 var createCanvas =function(w,h){ var c = document.createElement("canvas"); c.width = w; c.height = h; c.ctx = c.getContext("2d"); // document.body.appendChild(c); return c; } // converts pixel data into sub pixel data var subPixelBitmap = function(imgData){ var spR,spG,spB; // sub pixels var id,id1; // pixel indexes var w = imgData.width; var h = imgData.height; var d = imgData.data; var x,y; var ww = w*4; var ww4 = ww+4; for(y = 0; y < h; y+=1){ for(x = 0; x < w; x+=3){ var id = y*ww+x*4; var id1 = Math.floor(y)*ww+Math.floor(x/3)*4; spR = Math.sqrt(d[id + 0] * d[id + 0] * 0.2126 + d[id + 1] * d[id + 1] * 0.7152 + d[id + 2] * d[id + 2] * 0.0722); id += 4; spG = Math.sqrt(d[id + 0] * d[id + 0] * 0.2126 + d[id + 1] * d[id + 1] * 0.7152 + d[id + 2] * d[id + 2] * 0.0722); id += 4; spB = Math.sqrt(d[id + 0] * d[id + 0] * 0.2126 + d[id + 1] * d[id + 1] * 0.7152 + d[id + 2] * d[id + 2] * 0.0722); d[id1++] = spR; d[id1++] = spG; d[id1++] = spB; d[id1++] = 255; // alpha always 255 } } return imgData; } // Assume default textBaseline and that text area is contained within the canvas (no bits hanging out) // Also this will not work is any pixels are at all transparent var subPixelText = function(ctx,text,x,y,fontHeight){ var width = ctx.measureText(text).width + 12; // add some extra pixels var hOffset = Math.floor(fontHeight *0.7); var c = createCanvas(width * 3,fontHeight); c.ctx.font = ctx.font; c.ctx.fillStyle = ctx.fillStyle; c.ctx.fontAlign = "left"; c.ctx.setTransform(3,0,0,1,0,0); // scale by 3 // turn of smoothing c.ctx.imageSmoothingEnabled = false; c.ctx.mozImageSmoothingEnabled = false; // copy existing pixels to new canvas c.ctx.drawImage(ctx.canvas,x -2, y - hOffset, width,fontHeight,0,0, width,fontHeight ); c.ctx.fillText(text,0,hOffset); // draw thw text 3 time the width // convert to sub pixel c.ctx.putImageData(subPixelBitmap(c.ctx.getImageData(0,0,width*3,fontHeight)),0,0); ctx.drawImage(c,0,0,width-1,fontHeight,x,y-hOffset,width-1,fontHeight); // done } var globalTime; // render loop does the drawing function update(timer) { // Main update loop globalTime = timer; ctx.setTransform(1,0,0,1,0,0); // set default ctx.globalAlpha= 1; ctx.fillStyle = "White"; ctx.fillRect(0,0,canvas.width,canvas.height) ctx.fillStyle = "black"; ctx.fillText("Normal text is Oh hum "+ globalTime.toFixed(0),12,40); subPixelText(ctx,"Sub pixel text is best "+ globalTime.toFixed(0),12,90,50); div.textContent = "DOM is off course perfect "+ globalTime.toFixed(0); requestAnimationFrame(update); } var canvas = createCanvas(1024,100); // create and add canvas canvas.style.width = "512px"; canvas.style.height = "50px"; var ctx = canvas.ctx; // get a global context var div = document.createElement("div"); div.style.font = "20px Arial"; div.style.background = "white"; div.style.color = "black"; function start(){ document.body.appendChild(canvas); document.body.appendChild(div); ctx.font = "40px Arial"; requestAnimationFrame(update); // start the render } if(devicePixelRatio !== 2){ var dir = "in" var more = ""; div.textContent = "Incorrect pixel size detected. Requiers zoom of 2. See the answer for more information. If you wish to see the demo anyways just click this text. "; document.body.appendChild(div); div.style.cursor = "pointer"; div.title = "Click to start the demo."; div.addEventListener("click",function(){ start(); var divW = document.createElement("div"); divW.textContent = "Warning pixel sizes do not match. "; divW.style.color = "red"; document.body.appendChild(divW); }); }else{ start(); } 

For best results.

To get the best results, you will need to use webGL. This is a relatively simple modification from standard anti-aliasing to sub-pixel anti-aliasing. An example of standard vector text rendering using webGL can be found on WebGL PDF

The WebGL API will be happy to sit besides the 2D-canvas API, and copying the result of rendering webGl content to a 2D canvas is as simple as making a context.drawImage(canvasWebGL,0,0) image context.drawImage(canvasWebGL,0,0)

+9
source

Thanks so much for all these explanations!

It is completely unbelievable that displaying a “simple line” in a canvas is not neatly supported by default fillText() , and that we should do such tricks in order to have a proper display to say a display that is not a bit blurry or fuzzy. It’s kind of like a “1px line drawing problem” in the canvas (for which it helps +0.5 to the coordinates, but without a complete solution to the problem) ...

I modified the code above to support color text (not just black and white text). Hope this helps.

The subPixelBitmap() function has a small algorithm for the average values ​​of red / green / blue colors. This slightly improves the display of the string in the canvas (in Chrome), especially for small fonts. Maybe there are other algos that are even better: if you find him, I would be interested.

This picture shows the effect on the display: Improved display of a line in the canvas

Here is a working example that you can run online: a working example on jsfiddle.net

The linked code is one (check the working example above for the latest version):

  canvas = document.getElementById("my_canvas"); ctx = canvas.getContext("2d"); ... // Display a string: // - nice way: ctx.font = "12px Arial"; ctx.fillStyle = "red"; subPixelText(ctx,"Hello World",50,50,25); ctx.font = "bold 14px Arial"; ctx.fillStyle = "red"; subPixelText(ctx,"Hello World",50,75,25); // - blurry default way: ctx.font = "12px Arial"; ctx.fillStyle = "red"; ctx.fillText("Hello World", 50, 100); ctx.font = "bold 14px Arial"; ctx.fillStyle = "red"; ctx.fillText("Hello World", 50, 125); var subPixelBitmap = function(imgData){ var spR,spG,spB; // sub pixels var id,id1; // pixel indexes var w = imgData.width; var h = imgData.height; var d = imgData.data; var x,y; var ww = w*4; for(y = 0; y < h; y+=1){ // (go through all y pixels) for(x = 0; x < w-2; x+=3){ // (go through all groups of 3 x pixels) var id = y*ww+x*4; // (4 consecutive values: id->red, id+1->green, id+2->blue, id+3->alpha) var output_id = y*ww+Math.floor(x/3)*4; spR = Math.round((d[id + 0] + d[id + 4] + d[id + 8])/3); spG = Math.round((d[id + 1] + d[id + 5] + d[id + 9])/3); spB = Math.round((d[id + 2] + d[id + 6] + d[id + 10])/3); // console.log(d[id+0], d[id+1], d[id+2] + '|' + d[id+5], d[id+6], d[id+7] + '|' + d[id+9], d[id+10], d[id+11]); d[output_id] = spR; d[output_id+1] = spG; d[output_id+2] = spB; d[output_id+3] = 255; // alpha is always set to 255 } } return imgData; } var subPixelText = function(ctx,text,x,y,fontHeight){ var width = ctx.measureText(text).width + 12; // add some extra pixels var hOffset = Math.floor(fontHeight); var c = document.createElement("canvas"); c.width = width * 3; // scaling by 3 c.height = fontHeight; c.ctx = c.getContext("2d"); c.ctx.font = ctx.font; c.ctx.globalAlpha = ctx.globalAlpha; c.ctx.fillStyle = ctx.fillStyle; c.ctx.fontAlign = "left"; c.ctx.setTransform(3,0,0,1,0,0); // scaling by 3 c.ctx.imageSmoothingEnabled = false; c.ctx.mozImageSmoothingEnabled = false; // (obsolete) c.ctx.webkitImageSmoothingEnabled = false; c.ctx.msImageSmoothingEnabled = false; c.ctx.oImageSmoothingEnabled = false; // copy existing pixels to new canvas c.ctx.drawImage(ctx.canvas,x,y-hOffset,width,fontHeight,0,0,width,fontHeight); c.ctx.fillText(text,0,hOffset-3 /* (harcoded to -3 for letters like 'p', 'g', ..., could be improved) */); // draw the text 3 time the width // convert to sub pixels c.ctx.putImageData(subPixelBitmap(c.ctx.getImageData(0,0,width*3,fontHeight)), 0, 0); ctx.drawImage(c,0,0,width-1,fontHeight,x,y-hOffset,width-1,fontHeight); } 
0
source

All Articles