画布文字渲染(模糊)

Nor*_*ora 9 javascript html5 text rendering canvas

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

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

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

我试过了 :

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

请参阅canvas和html字体渲染之间的比较:https://jsfiddle.net/balleronde/1e9a5xbf/

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

Bli*_*n67 14

画布上的DOM质量文本.

仔细看看

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

在此输入图像描述

如您所见,底部文本上有彩色部分.这是因为它使用了一种名为true type的技术进行渲染

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

像素和子像素

当您仔细观察LCD显示器时,您会看到每个像素由连续排列的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文本质量相当高,而画布子像素保持高品质.

顶部文本是普通的Canvas文本,center是(自制)子画面文本,底部是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();
}






          
Run Code Online (Sandbox Code Playgroud)

1到2像素比例演示.

对于视网膜,非常高的分辨率,或缩放200%的浏览器.

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





          
Run Code Online (Sandbox Code Playgroud)

为了更好的结果.

要获得最佳效果,您需要使用webGL.这是从标准抗锯齿到子像素抗锯齿的相对简单的修改.使用webGL的标准矢量文本渲染的示例可以在WebGL PDF中找到

除了2D canvas API之外,WebGL API也很乐意将webGl渲染内容的结果复制到2D画布就像渲染图像一样简单context.drawImage(canvasWebGL,0,0)