CSSで記述されたフォントを持つテキスト要素をキャンバスで使用する方法


8

これは、Bismonプロジェクト(H2020ヨーロッパのプロジェクトが資金を提供するGPLv3 +ソフトウェア)、git commit内にあり0e9a8eccc2976fます。このドラフトレポートでは、ソフトウェアについて説明します。この質問は、より多くの背景と動機を与えます。これは、(手書きの)webroot / jscript / bismon-hwroot.jsファイルに関するもので、Bismon(libonionの上の特殊なWebサーバー)によってコードが生成されるHTMLページで使用されます。

たとえば、スパンのCSSクラスを追加しましたspan.bmcl_evalprompt(たとえば、ファイルfirst-theme.cssに

JavaScriptをコーディングして、同じスタイル(同じフォント、色など...)を持つキャンバスに(できればjqueryでjcanvasを使用して)テキスト部分を追加するにはどうすればよいspan.bmcl_evalpromptですか?DOMにそのようなスパン要素を作成する必要がありますか?それは単に可能ですか?

私はLinux上の最近のFirefox(少なくとも68)のみに関心があります。jQueryは3.4です。私はJquery UI 1.12.1 も使用しています

私は私の心の中に持っていたアイデア1つの作成することでした<span class='bmcl_evalprompt'>遠くのブラウザビューポート(またはX11ウィンドウ)からの座標を持つ要素を、例えば時x= -10000y= -10000 (ピクセル単位)、そしてその後、伝統を使用し、ドキュメントのDOMにその単一ひどく位置付け要素を追加フォントファミリー、フォントサイズ、要素サイズを取得するためのjqueryテクニック。しかし、もっと良い方法はありますか?またはそれを行ういくつかのjquery互換ライブラリ?

回答:


5

キャンバスでDOMフォントを一致させますか?

簡単な答えは、「ハードへの道!!」そして「それは完璧になることは決してないだろう」。

あなたができる最善のことは、回答の下部にある例にある近似です。これは、表示スタイルとの一致が表示品質とは無関係であることも示します。

CSSルールだけからの拡張。

フォントを要素にできるだけ一致させるには、Spark Fountainの回答で指摘されているようにCSSを取得するだけでなく、いくつかの追加の懸念事項があります

フォントサイズとCSSピクセルサイズ

  • フォントサイズはCSSピクセルサイズに関連しています。HTMLCanvasElement
  • CSSピクセルサイズは、デバイスの表示ピクセルと常に一致するとは限りません。たとえば、HiDPI / Retinaディスプレイ。次の方法でデバイスのCSSピクセル比率にアクセスできますdevicePixelRatio
  • CSSピクセルサイズは一定ではなく、さまざまな理由で変化する可能性があります。変更はMediaQueryListEventchangeイベントを介して監視し、イベントを聞くことができます
  • 要素は変換できます。CanvasRenderingContext2Dしたがって、3D変換を行うことができない要素やキャンバスが3Dあなたは、フォントレンダリングされた要素を持つキャンバスレンダリングのフォントを照合することができません変換持っています。

  • キャンバスの解像度と表示サイズは独立しています。

    • あなたは、プロパティを介したキャンバスの解像度を得ることができHTMLCanvasElement.width、かつHTMLCanvasElement.height
    • キャンバスの表示サイズは、スタイルプロパティの幅と高さ、または他のさまざまな方法で取得できます(例を参照)。
    • キャンバスピクセルアスペクトはCSSピクセルアスペクトと一致しない場合があり、キャンバスでフォントをレンダリングするときに計算する必要があります。
    • 小さなフォントサイズでのCanvasフォントのレンダリングはひどいです。たとえば、サイズが16pxにレンダリングされた4pxフォントは読み取り不可能です。ctx.font = "4px arial"; ctx.scale(4,4); ctx.fillText("Hello pixels");小さなフォントを使用する場合は、高品質のキャンバスレンダリング結果を持つ固定フォントサイズを使用し、レンダリングを縮小する必要があります。

フォントカラー

要素の色スタイルは、レンダリングされた色のみを表します。ユーザーから見た実際の色を表すも​​のではありません。

これは、キャンバスと、色を取得する要素とその上または下にある要素の両方に当てはまるため、色を視覚的に一致させるために必要な作業量は膨大であり、スタックオーバーフローの回答の範囲をはるかに超えています(回答には最大30Kの長さ)

フォントレンダリング

キャンバスのフォントレンダリングエンジンは、DOMのそれとは異なります。DOMは、さまざまなレンダリング技術を使用して、デバイスの物理的なRGBサブピクセルの配置方法を利用することにより、フォントの見かけの品質を向上させることができます。たとえば、レンダラーで使用されるTrueTypeフォントと関連するヒンティング、およびヒンティングレンダリングによる派生ClearTypeのサブピクセル。

これらのフォントレンダリングメソッドはキャンバス上で一致させることができますが、リアルタイムで一致させるにはWebGLを使用する必要があります。

問題は、DOMフォントのレンダリングが、ブラウザーの設定を含む多くの要因によって決定されることです。JavaScriptは、フォントのレンダリング方法を決定するために必要な情報にアクセスできません。せいぜい、知識に基づいて推測することができます。

さらなる合併症

フォントに影響を与える他の要因と、CSSフォントスタイルルールが、表示されたフォントの視覚的な結果にどのように関係するかも示します。たとえば、CSSユニット、アニメーション、配置、方向、フォント変換、および癖モード。

個人的にはレンダリングと色については気にしません。イベントすべてのフォントに一致するようにWebGLを使用してフルフォントエンジンを作成し、すべてのフォント、フィルタリング、合成、レンダリングバリアントを作成した場合、それらは標準の一部ではないため、予告なしに変更されることがあります。したがって、プロジェクトは常にオープンであり、いつでも読めない結果のレベルに失敗する可能性があります。努力するだけの価値はありません。


この例では、左側にレンダリングキャンバスがあります。テキストとフォントの中央上部。左側のキャンバスcanvasの拡大ビューを示す、右側の拡大ビュー

最初に使用されるスタイルはページのデフォルトです。キャンバスの解像度は300 x 150ですが、500 x 500 CSSピクセルに合わせて拡大縮小されています。その結果、キャンバステキストの品質が非常に低下します。キャンバスの解像度を循環させると、キャンバスの解像度が品質にどのように影響するかがわかります。

機能

  • drawText(text, x, y, fontCSS, sizeCSSpx, colorStyleCSS)CSSプロパティ値を使用してテキストを描画します。DOMの視覚的なサイズとアスペクト比にできるだけ一致するようにフォントをスケーリングします。

  • getFontStyle(element) 必要なフォントスタイルをオブジェクトとして返します element

UIの使用

  • フォントスタイルを切り替えるには、中央のフォントをクリックします。

  • 左のキャンバスをクリックして、キャンバスの解像度を切り替えます。

  • 下部には、キャンバスでテキストをレンダリングするために使用される設定があります。

テキストの品質がキャンバスの解像度に依存していることがわかります。

DOMズームがレンダリングにどのように影響するかを確認するには、ページをズームインまたはズームアウトする必要があります。HiDPIとRetinaディスプレイでは、キャンバスがCSSピクセルの解像度の半分であるため、キャンバステキストの品質がはるかに低くなります。

const ZOOM_SIZE = 16;
canvas1.width = ZOOM_SIZE;
canvas1.height = ZOOM_SIZE;
const ctx = canvas.getContext("2d");
const ctx1 = canvas1.getContext("2d");
const mouse = {x:0, y:0};
const CANVAS_FONT_BASE_SIZE = 32; // the size used to render the canvas font.
const TEXT_ROWS = 12;
var currentFontClass = 0;
const fontClasses = "fontA,fontB,fontC,fontD".split(",");

const canvasResolutions = [[canvas.scrollWidth, canvas.scrollHeight],[300,150],[200,600],[600,600],[1200,1200],[canvas.scrollWidth * devicePixelRatio, canvas.scrollHeight * devicePixelRatio]];
var currentCanvasRes = canvasResolutions.length - 1;
var updateText = true;
var updating = false;
setTimeout(updateDisplay, 0, true);

function drawText(text, x, y, fontCSS, sizeCSSpx, colorStyleCSS) { // Using px as the CSS size unit
    ctx.save();
    
    // Set canvas state to default
    ctx.globalAlpha = 1;
    ctx.filter = "none";
    ctx.globalCompositeOperation = "source-over";
    
    const pxSize = Number(sizeCSSpx.toString().trim().replace(/[a-z]/gi,"")) * devicePixelRatio;
    const canvasDisplayWidthCSSpx = ctx.canvas.scrollWidth; // these are integers
    const canvasDisplayHeightCSSpx = ctx.canvas.scrollHeight;
    
    const canvasResWidth = ctx.canvas.width;
    const canvasResHeight = ctx.canvas.height;
    
    const scaleX = canvasResWidth / (canvasDisplayWidthCSSpx * devicePixelRatio);
    const scaleY = canvasResHeight / (canvasDisplayHeightCSSpx * devicePixelRatio);
    const fontScale = pxSize / CANVAS_FONT_BASE_SIZE
    
    ctx.setTransform(scaleX * fontScale, 0, 0, scaleY * fontScale, x, y); // scale and position rendering
    
    ctx.font = CANVAS_FONT_BASE_SIZE + "px " + fontCSS;
    ctx.textBaseline = "hanging";
    ctx.fillStyle = colorStyleCSS;
    ctx.fillText(text, 0, 0);
    
    ctx.restore();
}
    
function getFontStyle(element) {
    const style = getComputedStyle(element);    
    const color = style.color;
    const family = style.fontFamily;
    const size = style.fontSize;    
    styleView.textContent = `Family: ${family} Size: ${size} Color: ${color} Canvas Resolution: ${canvas.width}px by ${canvas.height}px Canvas CSS size 500px by 500px CSS pixel: ${devicePixelRatio} to 1 device pixels`
    
    return {color, family, size};
}

function drawZoomView(x, y) {
    ctx1.clearRect(0, 0, ctx1.canvas.width, ctx1.canvas.height);
    //x -= ZOOM_SIZE / 2;
    //y -= ZOOM_SIZE / 2;
    const canvasDisplayWidthCSSpx = ctx.canvas.scrollWidth; // these are integers
    const canvasDisplayHeightCSSpx = ctx.canvas.scrollHeight;
    
    const canvasResWidth = ctx.canvas.width;
    const canvasResHeight = ctx.canvas.height;
    
    const scaleX = canvasResWidth / (canvasDisplayWidthCSSpx * devicePixelRatio);
    const scaleY = canvasResHeight / (canvasDisplayHeightCSSpx * devicePixelRatio);
    
    x *= scaleX;
    y *= scaleY;
    x -= ZOOM_SIZE / 2;
    y -= ZOOM_SIZE / 2;
    
    ctx1.drawImage(ctx.canvas, -x, -y);
}

displayFont.addEventListener("click", changeFontClass);
function changeFontClass() {
   currentFontClass ++;
   myFontText.className = fontClasses[currentFontClass % fontClasses.length];
   updateDisplay(true);
}
canvas.addEventListener("click", changeCanvasRes);
function changeCanvasRes() {
   currentCanvasRes ++;
   if (devicePixelRatio === 1 && currentCanvasRes === canvasResolutions.length - 1) {
       currentCanvasRes ++;
   }
   updateDisplay(true);
}
   
   

addEventListener("mousemove", mouseEvent);
function mouseEvent(event) {
    const bounds = canvas.getBoundingClientRect();
    mouse.x = event.pageX - scrollX - bounds.left;
    mouse.y = event.pageY - scrollY - bounds.top;    
    updateDisplay();
}

function updateDisplay(andRender = false) {
    if(updating === false) {
        updating = true;
        requestAnimationFrame(render);
    }
    updateText = andRender;
}

function drawTextExamples(text, textStyle) {
    
    var i = TEXT_ROWS;
    const yStep = ctx.canvas.height / (i + 2);
    while (i--) {
        drawText(text, 20, 4 + i * yStep, textStyle.family, textStyle.size, textStyle.color);
    }
}



function render() {
    updating = false;

    const res = canvasResolutions[currentCanvasRes % canvasResolutions.length];
    if (res[0] !== canvas.width || res[1] !== canvas.height) {
        canvas.width = res[0];
        canvas.height = res[1];
        updateText = true;
    }
    if (updateText) {
        ctx.setTransform(1,0,0,1,0,0);
        ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
        updateText = false;
        const textStyle = getFontStyle(myFontText);
        const text = myFontText.textContent;
        drawTextExamples(text, textStyle);
        
    }
    
    
    
    drawZoomView(mouse.x, mouse.y)


}
.fontContainer {
  position: absolute;
  top: 8px;
  left: 35%;
  background: white;
  border: 1px solid black;
  width: 30%;   
  cursor: pointer;
  text-align: center;
}
#styleView {
}
  

.fontA {}
.fontB {
  font-family: arial;
  font-size: 12px;
  color: #F008;
}
.fontC {
  font-family: cursive;
  font-size: 32px;
  color: #0808;
}
.fontD {
  font-family: monospace;
  font-size: 26px;
  color: #000;
}

.layout {
   display: flex;
   width: 100%;
   height: 128px;
}
#container {
   border: 1px solid black;
   width: 49%;
   height: 100%;
   overflow-y: scroll;
}
#container canvas {
   width: 500px;
   height: 500px;
}

#magViewContainer {
   border: 1px solid black;
   display: flex;
   width: 49%;
   height: 100%; 
}

#magViewContainer canvas {
   width: 100%;
   height: 100%;
   image-rendering: pixelated;
}
<div class="fontContainer" id="displayFont"> 
   <span class="fontA" id="myFontText" title="Click to cycle font styles">Hello Pixels</span>
</div>


<div class="layout">
  <div id="container">
      <canvas id="canvas" title="Click to cycle canvas resolution"></canvas>
  </div>
  <div id="magViewContainer">
      <canvas id="canvas1"></canvas>
  </div>
</div>
<code id="styleView"></code>


3

スパンのテキストをキャンバスにレンダリングするだけの場合は、関数window.getComputedStyleを使用してスタイリング属性にアクセスできます。元のスパンを非表示にするには、そのスタイルをに設定しdisplay: noneます。

// get the span element
const span = document.getElementsByClassName('bmcl_evalprompt')[0];

// get the relevant style properties
const font = window.getComputedStyle(span).font;
const color = window.getComputedStyle(span).color;

// get the element's text (if necessary)
const text = span.innerHTML;

// get the canvas element
const canvas = document.getElementById('canvas');

// set the canvas styling
const ctx = canvas.getContext('2d');
ctx.font = font;
ctx.fillStyle = color;

// print the span's content with correct styling
ctx.fillText(text, 35, 110);
#canvas {
  width: 300px;
  height: 200px;
  background: lightgrey;
}

span.bmcl_evalprompt {
  display: none;           // makes the span invisible
  font-family: monospace;  // change this value to see the difference
  font-size: 32px;         // change this value to see the difference
  color: rebeccapurple;    // change this value to see the difference
}
<span class="bmcl_evalprompt">Hello World!</span>
<canvas id="canvas" width="300" height="200"></canvas>

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.