web-dev-qa-db-ja.com

キャンバステキストレンダリング(ぼやけ)

この質問が何度も聞かれたことは知っていますが、ネット上で見つけたほとんどすべてのことを試しましたが、何を試しても(そしてどのような組み合わせでも)テキストをキャンバスで正しくレンダリングできませんでした。

ぼやけた線や形の問題の場合、座標に+ 0.5pxを追加するだけで問題は解決しました。ただし、この解決策はテキストレンダリングでは機能しないようです。

注:CSSを使用してキャンバスの幅と高さを設定することはありません(HTMLとCSSの両方でサイズプロパティを設定すると何かが変わるかどうかを確認するために一度試してみました)。また、問題はブラウザに関連していないようです。

私は試した :

  • hTMLを使用してキャンバスを作成し、次にhtmlの代わりにjavascriptを使用して
  • hTML要素、次にJS、次にHTMLとJSの両方で幅と高さを設定する
  • 可能なすべての組み合わせでテキスト座標に0.5pxを追加します
  • フォントファミリーとフォントサイズの変更
  • フォントサイズの単位(px、pt、em)の変更
  • 別のブラウザでファイルを開いて、何か変更がないか確認します
  • canvas.getContext('2d', {alpha:false})を使用してアルファチャネルを無効にすると、問題を解決せずにほとんどのレイヤーが消えてしまいます

ここでキャンバスとhtmlフォントのレンダリングの比較を参照してください: https://jsfiddle.net/balleronde/1e9a5xbf/

キャンバスのテキストをdom要素のテキストのようにレンダリングすることも可能ですか?アドバイスや提案をいただければ幸いです

10
Nora

キャンバス上のDOM品質のテキスト。

詳しく見る

DOMテキストを拡大すると、次のように表示されます(上はキャンバス、下はDOM、中央はピクセルサイズであることが望ましい(Retinaディスプレイではない))

enter image description here

ご覧のとおり、下部のテキストには色付きのセクションがあります。これは、TrueTypeと呼ばれる手法を使用してレンダリングされているためです。

True Typeの使用は、ブラウザーおよびオペレーティングシステムのオプション設定です。オフにしている場合、または非常に低解像度のデバイスを使用している場合、上のズームテキストは同じように見えます(下の画像に色付きのピクセルはありません)

ピクセルとサブピクセル

LCDディスプレイをよく見ると、各ピクセルが赤、緑、青に1つずつ、連続して配置された3つのサブピクセルで構成されていることがわかります。ピクセルを設定するには各カラーチャネルにRGB強度を指定し、適切なRGBサブピクセルを設定します。通常、赤が最初で青が最後であると認められていますが、実際には、色がそれぞれに近い限り、色の順序は関係ありません。他の場合も同じ結果が得られます。

色や制御可能な画像要素について考えるのをやめると、デバイスの水平解像度が3倍になります。ほとんどのテキストは単色であるため、RGBサブピクセルの配置についてあまり心配する必要はなく、テキストをピクセル全体ではなくサブピクセルにレンダリングして、高品質のテキストを取得できます。サブピクセルは非常に小さいので、ほとんどの人はわずかな色の歪みに気づきません。その利点は、わずかに汚れた外観の価値が十分にあります。

キャンバスにTrueTypeがない理由

サブピクセルを使用する場合は、アルファ値を含め、それぞれを完全に制御する必要があります。ディスプレイドライバの場合、アルファはピクセルのすべてのサブピクセルに適用されます。アルファ0.2で青を、アルファ0.7で同じピクセルに赤を使用することはできません。ただし、各サブピクセルの下のサブピクセル値がわかっている場合は、ハードウェアにアルファ計算を行わせる代わりに、アルファ計算を行うことができます。これにより、サブピクセルレベルでアルガコントロールが可能になります。

残念ながら(いいえ...ケースの99.99%は幸運です)、キャンバスは透明度を許可しますが、キャンバスの下のサブピクセルが何をしているのかを知る方法はありません。それらは任意の色である可能性があるため、アルファ計算を行うことはできません。サブピクセルを効果的に使用するために必要です。

自家製のサブピクセルテキスト。

ただし、透明なキャンバスを用意する必要はありません。すべてのピクセルを非透明(alpha = 1.0)にすると、サブピクセルのアルファ制御が回復します。

次の関数は、サブピクセルを使用してキャンバステキストを描画します。それほど高速ではありませんが、より高品質のテキストが得られます。

通常の幅の3倍でテキストをレンダリングすることで機能します。次に、余分なピクセルを使用してサブピクセル値を計算し、完了するとサブピクセルデータをキャンバスに配置します。

更新この回答を書いたとき、ズーム設定を完全に忘れていました。サブピクセルを使用するには、ディスプレイの物理ピクセルサイズとDOMピクセルサイズが正確に一致している必要があります。ズームインまたはズームアウトした場合、これはそうではないため、サブピクセルの検索ははるかに困難になります。
ズーム設定を検出するようにデモを更新しました。これを行う標準的な方法がないので、FFとChromeはズームすると_!== 1_になります(そして網膜デバイスがないので)devicePixelRatioを使用しました一番下のデモが機能するかどうかを推測しているだけです。デモを正しく表示したいのにズーム警告が表示されない場合は、ズームを1に設定してください。
さらに、ズームを200%に設定し、下のデモを使用することもできます。ズームインするとDOMテキストの品質が大幅に低下し、キャンバスのサブピクセルは高品質を維持するようです。

上のテキストは通常​​のキャンバステキスト、中央はキャンバス上の(自家製の)サブピクセルテキスト、下はDOMテキストです

Retinaディスプレイまたは非常に高解像度のディスプレイを使用している場合、高品質のキャンバステキストが表示されない場合は、この下のスニペットを表示する必要があることに注意してください。

標準の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キャンバスにコピーするのは、imagecontext.drawImage(canvasWebGL,0,0)をレンダリングするのと同じくらい簡単です。

14
Blindman67

これらすべての説明に感謝します!

キャンバスに「単純な文字列」をきちんと表示することがデフォルトのfillText()でサポートされていないこと、そして適切な表示を行うためにそのようなトリックを行う必要があること、つまり、少しぼやけたりぼやけたりすることはありません。それはどういうわけかキャンバスの「1px線画の問題」のようなものです(座標に+0.5を作成すると問題は解決しませんが、問題は解決しません)...

上記で提供したコードを変更して、(白黒のテキストだけでなく)色付きのテキストをサポートするようにしました。お役に立てば幸いです。

関数subPixelBitmap()には、赤/緑/青の色を平均化するための小さなアルゴリズムがあります。特に小さいフォントの場合、キャンバス(Chrome)での文字列の表示が少し改善されます。たぶん、もっと良い他のアルゴがあります:もしあなたがそれを見つけたら、私は興味があるでしょう。

この図は、表示への影響を示しています。キャンバスでの文字列表示の改善

オンラインで実行できる実例を次に示します。 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);
}
0
Ghislain