1. ホーム
  2. Web制作
  3. html5

モバイルドローイングブラー問題解決におけるキャンバスの深い理解

2022-02-06 11:07:31

モバイルとの互換性の問題から、あるプロジェクトでは、フロントエンドでpdfをモバイルページで直接表示できるようなインターフェイスに変換する必要があります。この解決策を容易にするために、pdf.js プラグインを使用し、ページ上で pdf をキャンバスに変換することができました。しかし、テストの過程で、それはモバイルブラウザでは、ディスプレイの内容が非常にぼやけていることが判明した(下図)、分析の後、モバイルHD画面が原因であることがわかった。問題を解決した後、私はテキストに理由と結果を書き留め

問題を説明する前に、まずはモバイルディスプレイとキャバ嬢の豆知識を知っておくと、後の探索に役立ちます。もし結果を直接見たい場合は、最後に引っ張ってくることができます。

画面に関するいくつかの基本事項

物理画素(DP)

物理画素はデバイスピクセルとも呼ばれ、スマホの解像度も物理画素としてよく聞きますが、例えばiPhone 7の物理解像度は750 * 1334です。画面はピクセルドットで構成されており、画面上には水平方向に750ピクセル、垂直方向に1334ピクセルがあることになります

デバイス・インディペンデント・ピクセル(DIP)

論理ピクセルとも呼ばれ、例えば、Iphone4とIphone3GSのサイズは共に3.5インチで、iphone4の物理解像度は640 * 980ですが、3gsは320 * 480しかなく、実際のレイアウトに従って幅320pxの画像を取ると、iphone4では半分しか内容がなく、残りの半分は空白になってしまうのです。この問題を回避するために、論理的なピクセルを導入し、両機種とも320pxとすることで、描画しやすくしています

デバイスピクセル比(DPR)

上記のデバイス独立画素は、計算しやすいようにデバイスの論理画素を統一すると言っていますが、各論理画素が表す物理画素は確定しておらず、スケーリングされていない場合の物理画素と論理画素の関係を決めるために、デバイス画素比率(DPR)という概念を導入しているのです

typedef long long ll;
 
/********************************** 
 template of lucas theorem for modulo large combinatorial numbers,1<=n<=m<=1e9,1<p<=1e6, p must be prime 
 Input: C(n,m)%p call lucas(n,m,p) 
 Complexity: min(m,p)*log(m) 
 ***********************************/
 

//ax + by = gcd(a,b) 
//pass in fixed values a,b. put back d=gcd(a,b), x , y 
void extendgcd(ll a,ll b,ll &d,ll &x,ll &y) 
{ 
    if(b==0){d=a;x=1;y=0;return;} 
    extendgcd(b,a%b,d,y,x); 
    y-=x*(a/b); 
}
 
//Ax=1(mod M), gcd(A,M)==1 
//input: 10^18>=A,M>=1 
//output: return x in the range [1,M-1] 
ll GetNi(ll A,ll M) 
{ 
    ll rex=0,rey=0; 
    ll td=0; 
    extendgcd(A,M,td,rex,rey); 
    return (rex%M+M)%M; 
}
 
ll C(ll n,ll m,ll p) 
{ 
    if(m>n) return 0; 
    ll up=1,dn=1; 
    for(int i=0;i<m;i++) 
    { 
        up = up*(n-i)%p; 
        dn = dn*(i+1)%p; 
    } 
    return up*GetNi(dn, p)%p; 
}
 
ll lucas(ll n,ll m,ll p) 
{ 
    if(m==0) return 1; 
    return C(n%p,m%p,p)*lucas(n/p,m/p,p) % p; 
}

上記には様々な理論がありますが、ここでは図解で説明します。

上のグラフからわかるように、同じ大きさの論理画素に対して、HD画面では物理画素の数が多くなっています。通常の画面では、1論理画素が1物理画素に対応しますが、dpr=2のHD画面では、1論理画素が4物理画素で構成されます。このため、HD画面はより精細に見えるのです。

キャンバスについての基礎知識

キャンバスはビットマップを描画する

これは、canvasについて知っている人なら誰でも知っていることであり、次に分析する問題の核心でもあります。

ビットマップについては後で説明しますが、今はcanvasで描かれる画像がビットマップであることだけを知っていればよいのです。

キャンバスの幅と高さのプロパティ

canvasのwidthとheightのプロパティは、初心者が非常に間違えやすいものです。この2つのプロパティは、CSSのwidthとheightのプロパティと混同されることが多いようです。

例えば、次のようなコード(1)があるとします。

<canvas width="600" height="300" style="width: 300px; height: 150px"></canvas>

  • style の width と height は、canvas 要素がインターフェース上で占める幅と高さをそれぞれ表します。 というのは、スタイルの幅と高さ
  • 属性にある幅と高さ は、キャンバスの実際のピクセル幅と高さを表します。

意味がわからない場合は、次のコード(2)を想像してみてください。

<! -- logo.png has 600 * 300 pixels -->
<img style="width: 300px; height: 150px" src="logo.png" /> 

キャンバスのデフォルトの幅と高さは300 * 150で、これにcssを設定した後 キャンバスは、設定されたCSSの幅と高さに従って拡大縮小されます(クロップはされません)。 これはimgタグと同じです

上記のコード(1)は、実際には、より一般的な方法で、1つの論理的なピクセルが実際には2つのキャンバスピクセルで埋められると解釈することができます。

ブレの原因の予備知識

上記は必要な基礎知識を簡単に紹介したもので、正式な探求は以下からとなります。

まず、canvasを使って描かれた画像はビットマップであると述べましたが、私たちが普段使っているjpgやpngもビットマップです。では、ビットマップとは何でしょうか?

ビットマップはピクセルマップ、ラスターマップとも呼ばれ、画像の各点に色や深度、透明度などの情報を記録して保存、表示します。具体的には、無数のピースからなる巨大なジグソーパズルのようなもので、各ピースは無色のピクセルドットを表しています。 理論上、ビットマップの1画素は物理的な1画素に相当する . しかし、AppleのRetinaスクリーンのようなHDスクリーンを使って写真を見るとしたら、どのように見えるでしょうか。

例えば、以下のようなコードがあったとします。これをiphone 4の網膜スクリーンで表示させたとします。

<canvas width="320" height="150" style="width: 320px; height: 150px"></canvas>

iphone4本体の物理的なピクセルが640 * 980、デバイスに依存しないピクセルが320 * 480なので、実際には1cssピクセルが4物理ピクセルで構成されており、キャンバスのピクセルが320 * 150、そのcssピクセルが320 * 150ということになります。ということは、1cssピクセルが1canvas要素で構成されることになるので、変換を行うために、Under retina画面では、1canvasピクセル(または1ビットマップピクセル)が4物理ピクセルに充満し、1ビットマップピクセルがさらに分割できないので、近くしか色分けできず、ボケた画像になってしまうのです。

以下の画像は、Retinaスクリーン上でビットマップがどのように充填されるかを示しています。

上の画像の左側は通常の画面下での表示ルールで、ビットマップのピクセルドットが4つあることがわかりますが、右側はHD画面下で16個のピクセルドットがあることが確認できます。カットできない画素のドットによって色が変化しています。

しかし、もう一つ説明されていないのは、なぜ画像が直接ではなく、オリジナルに近い色を取るのかということで、これもぼかしの背景になっています。

裏技 - 平滑化技術

ここで、私の仲間の達人が説明してくれたのですが、先ほど、ビットマップの各要素は、実際にはソリッドカラーのピクセルのドットであると言いました。ここで、通常の画面上に、cssサイズが4px * 4px、dprが1の数字 "0"を描く必要があるとします。

dprを小さくしても、quot;0"のパターンは見えています。では、cssのサイズはそのままで、代わりに画像をRetinaスクリーンに描くと、どのように見えるでしょうか。

cssの1ピクセルは、Retinaの画面では物理的に4ピクセルに相当することが分かっています。もし、何も加工せずに上記のように直接マトリックスを配置して拡大すると、このパターンはRetinaの画面では非常にギザギザで、画像には明らかに滑らかさが欠けていることが分かると思います。

画像を少し変えて、次のようにします。

これは実は画像スムージングと呼ばれるもので、ギザギザを解消するために元の色を変え、より色の濃い画像でより自然になるように、画像のつながりが近似色になるため、上記の塗り色が元の色ではなく、近似色になっていることが説明できるのです。

原因のまとめ

以上の説明を踏まえて、次に結論をまとめましょう。モバイルが普及し、HD画面がほぼ全世界で利用できるようになったことで、CSSの1pxは実際には4つ以上の物理的なピクセルに相当する。しかし、コードの都合上、css の 1px のピクセルと canvas の 1 ピクセルは等しく描画されるため、実際には canvas の 1 ピクセルに 4 個以上の物理ピクセルを埋める必要があり、画像処理をスムーズに行うために、残りの物理ピクセルに元の色の近似値を埋めて、ぼやけた画像になってしまう。

ソリューションのアイデア

原因がわかれば、解決するのは簡単です。最も重要なことは、キャンバスの1ピクセルと物理的な1ピクセルを同じにすることです

上位バージョンのブラウザには、windowオブジェクトの下にdevicePixelRatio属性があり、これが上記のdprにあたります。

canvas 要素の css の幅と高さが決まっている場合は、次のようにします。

let canvas = document.getElementById('canvas');
let ctx = canvas.getContext('2d');
let dpr = window.devicePixelRatio; // Assume dpr is 2
// Get the width and height of the css
let { width: cssWidth, height: cssHeight } = canvas.getBoundingClientRect();
// expand the canvas canvas by pixels according to dpr, so that 1 canvas pixel is equal to 1 physical pixel
canvas.width = dpr * cssWidth;
canvas.height = dpr * cssHeight;
// As the canvas expands, the canvas coordinate system also expands, so if we draw according to the original coordinate system, the content will shrink
// so you need to scale up the drawing
ctx.scale(dpr,dpr);

経験の概要

多くの場合、問題を見つけたとき、それを解決することに集中するのではなく、なぜそうなったのかを深く理解することで、その過程でより良い歩みができるようになるはずです。

この記事がお役に立てれば幸いです。そして、スクリプトハウスを応援していただければ幸いです。