画布文字渲染(模糊)

时间:2016-10-16 02:18:57

标签: javascript html5 canvas text rendering

我知道这个问题已被多次询问过,但我尝试了几乎所有我能在网上找到的东西,但仍无法在画布中正确渲染文本,无论我尝试过什么(以及任何组合)。

对于模糊的线条和形状问题,只需在坐标上添加+ 0.5px即可解决问题:但是,此解决方案似乎不适用于文本渲染。

注意:我从不使用CSS来设置画布宽度和高度(只需尝试一次以检查HTML和CSS中的设置大小属性是否会改变任何内容)。此外,问题似乎与浏览器无关。

我试过了:

  • 使用HTML创建画布,然后使用javascript而不是html
  • 在HTML元素中设置宽度和高度,然后使用JS,然后使用HTML和JS
  • 将0.5px添加到每个可能组合的文本坐标
  • 更改font-family和font-size
  • 更改字体大小单位(px,pt,em)
  • 使用不同浏览器打开文件以检查是否有任何更改
  • 使用canvas.getContext('2d', {alpha:false})禁用Alpha通道,这只会让我的大部分图层消失而不解决问题

在此处查看canvas和html字体呈现之间的比较:https://jsfiddle.net/balleronde/1e9a5xbf/

甚至可以将画布中的文本渲染为dom元素中的文本吗?任何建议或建议将不胜感激

4 个答案:

答案 0 :(得分:13)

Canvas上的DOM质量文本。

仔细看看

如果您放大DOM文本,您将看到以下内容(顶部是画布,底部是DOM,中心有望像素大小(不在视网膜显示屏上))

enter image description here

正如您所看到的,底部文字上有彩色部分。这是因为它已使用名为true type

的技术进行渲染
  

注意使用true type是浏览器和操作系统上的可选设置。如果关闭它或设备的分辨率非常低,则上面的缩放文本看起来会相同(底部图像中没有彩色像素)

像素和子像素

当您仔细观察液晶显示器时,您会看到每个像素由排成一排的3个子像素组成,每个像素分别用于红色,绿色和蓝色。要设置像素,请为每个颜色通道提供RGB强度,并设置相应的RGB子像素。我们通常认为红色是第一个,最后是蓝色,但实际情况是,只要它们彼此接近就会得到相同的结果,颜色的顺序并不重要。

当您停止考虑颜色和几乎可控的图像元素时,您的设备的水平分辨率会增加三倍。由于大多数文本都是单色的,因此您不必过于担心RGB子像素的对齐,您可以将文本渲染到子像素而不是整个像素,从而获得高质量的文本。子像素是如此之小,大多数人都没有注意到轻微的颜色扭曲,这样的好处值得稍微肮脏的外观。

为什么没有真正的画布类型

使用子像素时,您需要完全控制每个子像素,包括alpha值。对于显示驱动程序alpha应用于像素的所有子像素,您不能在alpha为0.2时为蓝色,在alpha 0.7为相同像素时为红色。但是如果你知道每个子像素下的子像素值是什么,你可以进行alpha计算,而不是让硬件去做。这可以让你在亚像素级别进行algha控制。

不幸的是(没有... 99.99%的情况幸运)画布允许透明度,但你无法知道画布下的子像素在做什么,它们可以是任何颜色,因此你不能做有效使用子像素所需的alpha计算。

自制的亚像素文字。

但是你不必拥有透明画布,如果你使所有像素都不透明(alpha = 1.0),你就会重新获得亚像素alpha控制。

以下函数使用子像素绘制画布文本。它不是很快,但确实可以获得质量更好的文本。

它的工作原理是将文本渲染为正常宽度的3倍。然后它使用额外的像素来计算子像素值,并在完成时将子像素数据放到画布上。

  

更新当我写这个答案时,我完全忘记了缩放设置。使用子像素需要显示物理像素大小和DOM像素大小之间的预设匹配。如果放大或缩小,则不会如此,因此定位子像素变得更加困难。   
我已更新演示以尝试检测缩放设置。由于没有标准的方法来执行此操作,我刚刚使用了devicePixelRatio用于FF和Chrome !== 1缩放时(并且由于我没有视网膜装置,我只猜测底部演示是否有效) 。如果您希望正确地看到演示并且仍然没有得到缩放警告但仍然进行了缩放,请将缩放设置为1。   
Addistionaly您可能希望将缩放设置为200%并使用底部演示,因为放大似乎会显着降低DOM文本质量,而画布子像素保持高质量。

顶部文字是普通画布文字,中心是(自制)画布上的子像素文字,底部是DOM文字

请注意,如果您有Retina Display或非常高分辨率的显示器,如果您没有看到高质量的画布文本,则应该查看此片段下方的片段。

标准的1对1像素演示。



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();
}






          
&#13;
&#13;
&#13;

1到2像素比率演示。

适用于视网膜,非常高分辨率或200%浏览器缩放。

&#13;
&#13;
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();
}





          
&#13;
&#13;
&#13;

获得更好的结果。

要获得最佳效果,您需要使用webGL。这是从标准抗锯齿到子像素抗锯齿的相对简单的修改。可以在WebGL PDF

找到使用webGL的标准矢量文本渲染示例 除了2D画布API之外,WebGL API很乐意将其复制并将webGl渲染内容的结果复制到2D画布就像渲染图像一样简单context.drawImage(canvasWebGL,0,0)

答案 1 :(得分:1)

但是,有一个更简单的解决方案。

    context.scale(0.3, 0.3)
    context.fillText("Hello there", canvas.width / 2 * 1 / 0.3, canvas.height * 2.8 / 4 * 1 / 0.3, canvas.width * 0.9 * 1 / 0.3);
    context.font = canvas.width / 15 + "px Arial";
    context.fillText("Want to talk? Mail me at mher@movsisyan.info", canvas.width / 2 * 1 / 0.3, canvas.height * 3.6 / 4 * 1 / 0.3, canvas.width * 0.9 * 1 / 0.3);
    context.fillText("Want to see my code? Find me on GitHub as MovsisyanM", canvas.width / 2 * 1 / 0.3, canvas.height * 3.8 / 4 * 1 / 0.3, canvas.width * 0.9 * 1 / 0.3);
    context.scale(1, 1)

在这里我按比例缩小了画布上下文,这使我可以使用较高的字体px从而获得更好的文本质量。

我实际上发现了这种技术,因为photoshop或某些类似的图像编辑程序存在相同的问题。我希望这会有所帮助!

答案 2 :(得分:0)

非常感谢所有这些解释!

默认fillText()不支持在画布中以简洁的方式显示“简单字符串”,这是非常不可思议的,我们必须做这样的技巧才能有正确的显示,也就是说显示这不是有点模糊或模糊。它在某种程度上就像画布中的“1px线条绘制问题”(对于坐标有+0.5有帮助,但没有完全解决问题)......

我修改了您上面提供的代码,使其支持彩色文本(不仅是黑白文本)。我希望它可以提供帮助。

在函数subPixelBitmap()中,有一些算法来平均红色/绿色/蓝色。它改进了画布(在Chrome上)的字符串显示,特别是对于小字体。也许还有其他更好的算法:如果你找到一个,我会感兴趣。

此图显示了对显示的影响:改进了画布中的字符串显示

以下是一个可以在线运行的工作示例:working example on jsfiddle.net

相关代码就是这个(请查看上一个版本的上述工作示例):

  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);
}

答案 3 :(得分:0)

最简单的解决方案是使用window.devicePixelRatio来缩放画布分辨率(与其实际大小相比)。

例如具有画布元素:

<canvas id="canvas"></canvas>

将画布大小设置为其物理大小的2倍(window.devicePixelRatio的值):

const canvas = document.getElementById('canvas')
canvas.width = canvas.clientWidth * 2
canvas.height = canvas.clientHeight * 2

并将画布上下文缩放为2:

const context = canvas.getContext('2d')
context.scale(2, 2)

演示:

  1. 比例为1x1 Resolution 1x1

  2. 比例为1x2 Resolution 1x2