Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Fractional font sizes html5 canvas?

I am having an issue with how different browsers render fonts sizes on the HTML5 Canvas. Specificaly where the font sizes are not whole numbers. For example "8.5px Arial", "2.23em Arial" and so on.

For canvas.font= FontSize + "px Arial"; where FontSize is (8.1, 8.2, 8.3, ...) I would expect a linear result, however the only browser that consistently archives this is IE ! ( I know ). Firefox rounds below 11.2px but is linear beyond that. Chrome and Safari only scale fonts to integer values.

The rounding rule seems to be set to pixels and rounds down to the nearest integer below .5

My canvas app is performing some programmatic animated transforms ( scale and translate ) Trying to avoid canvas transforms for the sake of efficiency, although I suspect it would solve the problem, I have also not tested this by setting html text size with just CSS3 ( that the Canvas.font is suppose to be based on according to the spec)

The browsers are at least consistent in behavior between pt, px, em, and % font sizes. [edit: except Safari does not render % sizes]

[ an aside : all fonts look a little bold once blown up a bit. Safari takes that cake as we go from 17.4px to 17.5px .. BAMB!]

I have attached some example images below but would really like some ideas on how to make all browsers more like IE ( again never thought I would say that ) - Is this a bug or a standard for rendering fonts?

here is my test code that can be replicated for the PX font size case.

<!DOCTYPE html><html><head><script>
function display() {
    var canvas = document.getElementById('test');
    // For EM and % cases....
    //canvas.style.font="5px Arial";
    canvas.height = 1000;
    var context = canvas.getContext('2d');
    var minSize = 10;
    var lineHeight = 100;
    for(var a=minSize; a< 20; a+=0.1)
    {
        var font_size =  Math.round(a*10000)/10000;
        context.font = a + "px Arial";
        context.fillText("A: EXAMPLE TEXT > " +  font_size, 20, (font_size-minSize)*lineHeight);
    }
}
</script></head><body onload="display()"><canvas id="test"></canvas></body></html>

Pixel scale pixel scale Point scale point scale em scale - 1em = 5px Arial em scale percent scale - 100% = 5px Arial percent scale

like image 218
dave.zap Avatar asked Mar 15 '13 11:03

dave.zap


1 Answers

I thought you lot were supposed to do my job for me ;) Anyway here is how I solved the problem, although it's a bit of a hack and still has some issues that I will describe.

To recap ( because it was not clear in the question perhaps ) I was trying multiply font sizes by some floating point number to enlarge and shrink text. But this resulted in jittery results due to the fact that most round the font size to the nearest integer value.

So for instance.

var zoom = 1.02;
var font_size = 12;
context.font = (font_size * zoom) + "px Arial"; // 12.24px

zoom = 1.04 (12.48px) would render the same as the above code, while zoom =1.06 (12.75px) would be rounded up to 13px.

This resulted in a very undesirable jumping of the font size between values, while all other elements on the canvas were scaled correctly.

The problem was compounded by the fact that all browsers render a given string of text like "EXAMPLE TEXT" and slightly different lengths.

The solution

During initialization render the text at the standard font size (zoom=1) and measure the line length, let this value be called iniLineLength

During scene render I used a hidden canvas to render the text onto then used drawImage to the main canvas with a multiplier on the width.

tmpContext.font = (font_size * zoom) + "px Arial";
tmpContext.fillText(MyLineOfText,0, 0); 
var s = (iniLineLength * zoom) / tmpContext.measureText(MyLineOfText) ;
mainContext.drawImage(tmpCanvas,x, y, pw * s, ph);

The resulting line of text is stretched or shrunk on the main canvas according to how far off the line length is from the expected line length.

In an ideal world s would always equal one as the expected line length would always be the same as the actual line length. The expected line length is our sample line length iniLineLength multiplied by the same zoom factor that our font size was in this render pass.

I bonus to all this is that it has the effect of normalizing all differences in browser behavior... nearly.

Issues

Even with the line length normalized the exact position of each character in the line is not the same. Giving the effect in the image below.

enter image description here

However this is already a vast improvement on what I had.

In addition you are going to want to clip your tmpCanvas to mainCanvas or else your browser will grind to a halt as it tries to create and copy your mega-pixels worth of text, just for the sake of having one character full screen.

With that in mind, you can also set your tmp canvas width and height as needed to suit the actual line of text your going to render, this will save memory, time and so on.

like image 93
dave.zap Avatar answered Oct 22 '22 07:10

dave.zap