【问题标题】:Canvas text rendering (blurry)画布文本渲染(模糊)
【发布时间】:2023-03-20 01:36:01
【问题描述】:

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

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

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

我试过了:

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

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

是否有可能让画布中的文本像 dom 元素中的文本一样呈现?任何意见或建议将不胜感激

【问题讨论】:

  • 这是一个已经在高清画布元素上的线程。 stackoverflow.com/questions/15661339/…
  • 谢谢,这段代码太棒了!文本渲染仍然有点模糊,但现在完全可读。他们是一种让它像 dom 文本元素一样清晰的方法,还是这是使用画布可以获得的最好的方法。?这是我使用 Paul Lewis 代码得到的结果:jsfiddle.net/balleronde/m7xuuv09

标签: javascript html canvas text rendering


【解决方案1】:

最简单的解决方案是使用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

  2. 比例为 1x2

【讨论】:

    【解决方案2】:

    不过,还有一个更简单的解决方案。

        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)
    

    在这里我缩小了画布上下文,这让我可以使用更高的字体像素,从而获得更好的文本质量。

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

    【讨论】:

      【解决方案3】:

      非常感谢所有这些解释!

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

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

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

      下图显示效果:改进了canvas中的字符串显示

      这是一个可以在线运行的工作示例: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);
      }
      

      【讨论】:

        【解决方案4】:

        画布上的 DOM 质量文本。

        仔细观察

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

        如您所见,底部文本上有彩色部分。这是因为它是使用一种称为真实类型的技术进行渲染的

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

        像素和子像素

        当您仔细观察 LCD 显示器时,您会发现每个像素由 3 个排列成一行的子像素组成,红色、绿色和蓝色各一个。要设置像素,您需要为每个颜色通道提供 RGB 强度,并设置适当的 RGB 子像素。我们通常接受红色在前,蓝色在后,但实际上颜色的顺序并不重要,只要它们彼此接近,您就会得到相同的结果。

        当您不再考虑颜色而只考虑可控的图像元素时,您的设备水平分辨率就会提高三倍。由于大多数文本是单色的,因此您不必过多担心 RGB 子像素的对齐方式,您可以将文本渲染到子像素而不是整个像素,从而获得高质量的文本。子像素太小了,大多数人都不会注意到轻微的颜色失真,而且它的好处是值得看起来有点脏。

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

        使用子像素时,您需要完全控制每个像素,包括 alpha 值。对于显示驱动程序,alpha 适用于像素的所有子像素,您不能在 alpha 0.2 处有蓝色,而在 alpha 0.7 处的同一像素上不能有红色。但是,如果您知道每个子像素下的子像素值是多少,您就可以进行 alpha 计算,而不是让硬件来做。这使您可以在亚像素级别上控制藻类。

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

        自制亚像素文本。

        但您不必拥有透明画布,如果您将所有像素设为非透明 (alpha = 1.0),您将重新获得亚像素 alpha 控制。

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

        它通过以正常宽度的 3 倍呈现文本来工作。然后它使用额外的像素来计算子像素值,完成后将子像素数据放到画布上。

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

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

        请注意,如果您有 Retina 显示屏或非常高分辨率的显示屏,如果您看不到高质量的画布文本,则应查看此显示屏下方的 sn-p。

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

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

        为了更好的结果。

        要获得最佳结果,您需要使用 webGL。从标准抗锯齿到亚像素抗锯齿是一个比较简单的修改。使用 webGL 进行标准矢量文本渲染的示例可以在 WebGL PDF

        找到

        WebGL API 将很高兴地坐在 2D 画布 API 旁边,将 webGl 渲染内容的结果复制到 2D 画布就像渲染图像一样简单context.drawImage(canvasWebGL,0,0)

        【讨论】:

        • 感谢您的回答,我学到了很多关于像素渲染的工作原理!您的解决方案确实提高了文本渲染质量,但速度也很慢(特别是考虑到我需要在我的网页上显示多个画布)。一些字母出现颜色故障(蓝色使用铬),这说明了您的观点,即“无法知道画布下的子像素在做什么”(我没有检查这个问题是否与浏览器相关然而)。我会花时间深入研究您的代码,尽管我仍在寻找性能方面不那么贪婪的解决方案。
        • @Nora 我没有透明像素,因此看到彩色条是因为 (A) 因为您的视力非常好,或者 (B) 显示器的分辨率非常低,或者 (C)非标准缩放(!== 1.0)。我进行了一些更改以尝试检测缩放设置。 True Type 是一项 MS 专利技术(因此此演示是亚像素方法而不是真实类型,(真实类型抑制颜色变化))对于速度,您可以使用 webGL,链接显示速度wdobbie.com/pdf 和答案有你需要做的一切亚像素渲染的模组。
        猜你喜欢
        • 1970-01-01
        • 2020-07-30
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-02-03
        • 1970-01-01
        • 1970-01-01
        • 2017-08-10
        相关资源
        最近更新 更多