如何在画布中使用具有 CSS 中描述的字体的文本元素

Bas*_*tch 10 html css linux jquery html5-canvas

这在Bismon项目(由 H2020 欧洲项目资助的 GPLv3+ 软件)中, git commit0e9a8eccc2976f。该报告草案描述了软件。这个问题提供了更多的背景和动机。它是关于(手写)的webroot / JScript中/ bismon-hwroot.js文件,在其代码被Bismon产生一些HTML页面中使用的(上面有专门的web服务器libonion)。

我为 span 添加了一些 CSS 类,例如span.bmcl_evalprompt(例如在我的文件first-theme.css 中)。

我如何编写 JavaScript 代码以在画布中添加具有相同样式(相同字体、颜色等)的文本片段(最好将jcanvas与 jquery结合使用)span.bmcl_evalprompt?我需要在我的 DOM 中创建这样的 span 元素吗?这甚至可能吗?

我只关心 Linux 上最近的 Firefox(至少 68)。JQuery是 3.4。我也在使用Jquery UI 1.12.1

我的想法是创建一个<span class='bmcl_evalprompt'>坐标远离浏览器视口(或 X11 窗口)的单个元素,例如 atx= -10000y= -10000 (以像素为单位),然后将该单个定位错误的元素添加到文档 DOM 中,然后使用传统的获取字体系列、字体大小和元素大小的 Jquery 技术。但是有没有更好的方法呢?或者一些 Jquery 兼容库这样做?

Bli*_*n67 7

在画布中匹配 DOM 字体?

简单的回答是,“太难了!!” “它永远不会是完美的。”

您可以做的最好的事情是在答案底部的示例中进行近似,这也将显示匹配可见样式与可见质量无关。

仅从 CSS 规则扩展。

如果您希望字体尽可能地与元素匹配,那么除了像Spark Fountain's answer 中指出的那样获取 CSS 之外,还有一些额外的问题

字体大小和 CSS 像素大小

  • 字体大小与 CSS 像素大小有关。HTMLCanvas 元素

  • CSS 像素大小并不总是与设备的显示像素匹配。例如 HiDPI/Retina 显示器。您可以通过以下方式访问设备 CSS 像素比率devicePixelRatio

  • CSS 像素大小不是一个常数,可以因多种原因而改变。可以通过MediaQueryListEvent监听change事件来监听变化

  • 元素可以转换。该CanvasRenderingContext2D不能做3D变换因而是元素或画布拥有3D转换,你将无法匹配渲染字体的元素的字体呈现在画布上。

  • 画布分辨率和显示大小是独立的。

    • 您可以通过属性获取画布分辨率HTMLCanvasElement.width,以及HTMLCanvasElement.height
    • 您可以通过样式属性宽度和高度获取画布显示大小,或通过各种其他方法查看示例。
    • 画布像素方面可能与 CSS 像素方面不匹配,并且必须在画布上渲染字体时计算。
    • 小字体的画布字体渲染很糟糕。例如,呈现为 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 编写了一个完整的字体引擎来匹配每个字体、过滤、合成和渲染变体,则它们不是标准的一部分,因此如有更改,恕不另行通知。因此,该项目将始终处于开放状态,并且在任何时候都可能导致无法读取的结果。只是不值得努力。


例子

该示例的左侧有一个渲染画布。文本和字体居中顶部。右侧的缩放视图,显示左侧画布画布的放大视图

使用的第一种样式是页面默认样式。画布分辨率为 300 x 150,但已缩放以适合 500 x 500 CSS 像素。这导致质量非常差的画布文本。循环画布分辨率将显示画布分辨率如何影响质量。

功能

  • drawText(text, x, y, fontCSS, sizeCSSpx, colorStyleCSS)使用 CSS 属性值绘制文本。缩放字体以尽可能匹配 DOM 视觉大小和纵横比。

  • getFontStyle(element) 将所需的字体样式作为对象从 element

用户界面使用

  • 单击中心字体以循环字体样式。

  • 单击左画布以循环画布分辨率。

  • 底部是用于在画布中呈现文本的设置。

您将看到文本的质量取决于画布的分辨率。

要查看 DOM 缩放如何影响渲染,您必须放大或缩小页面。由于画布是 CSS 像素分辨率的一半,HiDPI 和 Retina 显示器的画布文本质量会低得多。

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)


}
Run Code Online (Sandbox Code Playgroud)
.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;
}
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)