Thanks a lot for all those explanations!
It's quite incredible that displaying a "simple string" in canvas in a neat manner is not supported by the default fillText()
, and that we have to do such tricks to have a proper display, that's to say a display which is not a bit blurry or fuzzy. It's somehow like the "1px line drawing issue" in canvas (for which making +0.5 to coordinates helps but without solving completely the issue)...
I modified the code you provided above to make it support colored text (not only black and white text). I hope it can help.
In the function subPixelBitmap()
, there is a little algorithm to average red/green/blue colors. It improves a little the string display in canvas (on Chrome), especially for small fonts. Maybe there are other algos which are even better: if you find one, I would be interested.
This figure shows the effect on display: Improved string display in canvas

Here is a working example that can be run online: working example on jsfiddle.net
The associated code is this one (check the above working example for the last 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);
}