Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use in a canvas a text element with a font described in CSS

This is within the Bismon project (a GPLv3+ software funded by H2020 European projects), git commit 0e9a8eccc2976f. This draft report describes the software. This question gives more context and motivations. It is about the (hand-written) webroot/jscript/bismon-hwroot.js file, used in some HTML page whose code is generated by Bismon (a specialized web server above libonion).

I added some CSS class for span, e.g. span.bmcl_evalprompt (e.g. in my file first-theme.css).

How do I code the JavaScript to add a text piece in a canvas (preferably using jcanvas with jquery) having the same style (same font, color, etc...) as that span.bmcl_evalprompt ? Do I need to create such a span element in my DOM? Is that even simply possible?

I only care about a recent Firefox (68 at least) on Linux. JQuery is 3.4. I am also using Jquery UI 1.12.1

The idea I had in my mind was to create one single <span class='bmcl_evalprompt'> element with coordinates far away from the browser viewport (or X11 window), e.g. at x= -10000 and y= -10000 (in pixels), then add that single badly positioned element into the document DOM, then use traditional Jquery techniques to get the font family, font size, and element size. But is there any better way? Or some Jquery compatible library doing that?

like image 458
Basile Starynkevitch Avatar asked Jan 09 '20 15:01

Basile Starynkevitch


People also ask

What CSS property is used for text fonts?

In CSS, we use the font-family property to specify the font of a text.


2 Answers

If you simply want to render the text from your span in a canvas, you can access the styling attributes using the function window.getComputedStyle. To make the original span invisible, set its style to 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>
like image 79
Spark Fountain Avatar answered Sep 23 '22 14:09

Spark Fountain


Matching DOMs font in canvas?

Simple answer is, "Way to hard!!" and "It will never be perfect."

The best you can do is an approximation which is in the example at bottom of answer which will also show matching the visible style is unrelated to the visible quality.

Extending from just CSS rules.

If you want the font to match as closely as possible to the element there are some additional concerns than just getting the CSS as pointed out in Spark Fountain's answer.

Font size & CSS pixels size

  • Font size is related to CSS pixel size. The HTMLCanvasElement

  • CSS pixel size does not always match the device's display pixels. Eg HiDPI/Retina displays. You can access the device CSS pixel ration via devicePixelRatio

  • CSS pixel size is not a constant and can change for many reasons. Changes can be monitored via MediaQueryListEvent and listening to the change event

  • Elements can be transformed. The CanvasRenderingContext2D can not do 3D transformations thus is the element or the canvas has a 3D transform you will not be able to match the canvas rendered font with elements rendered font.

  • The canvas resolution and display size are independent.

    • You can get the canvas resolution via the properties HTMLCanvasElement.width, and HTMLCanvasElement.height
    • You can get the canvas display size via the style properties width and height, or via a variety of other methods see example.
    • The canvas pixel aspect may not match the CSS pixel aspect and must be computed when rendering the font on the canvas.
    • Canvas font rendering at small font sizes is terrible. For example a 4px font rendered to be 16px in size is unreadable. ctx.font = "4px arial"; ctx.scale(4,4); ctx.fillText("Hello pixels"); You should use a fixed font size that has good quality canvas rendering results and scale the rendering down when using small fonts.

Font Color

The elements color style only represent the rendered color. It does not represent the actual color as seen by the user.

As this applies to both the canvas and the element from which you are getting the color and any over or under laying elements, the amount of work required to visually match color is huge and well beyond the scope of a stack overflow answer (answers have a max 30K length)

Font Rendering

The font rendering engine of the canvas is different than that of the DOM. The DOM can use a variety of rendering techniques to improve the fonts apparent quality by taking advantage of how the devices physical RGB sub-pixels are arranged. For example TrueType fonts and related hinting used by the renderer, and the derived ClearType's sub pixel with hinting rendering.

These font rendering methods CAN be matched on the canvas, though for real-time matching you will have to use WebGL.

The problem is that the DOMs font rendering is determined by many factors, including the browsers settings. JavaScript can not access any of the information needed to determine how the font is rendered. At best you can make an educated guess.

Further complications

There are also other factors that effect the font and how the CSS font style rules relate to the visual result of the displayed font. For example CSS units, animation, alignment, direction, font transformations, and quirks mode.

Personally for rendering and color I don't bother. Event if I wrote a full font engine using WebGL to match every font, filtering, compositing and rendering variant, they are not part of the standard and thus subject to change without notice. The project would thus always be open and could at any point fail to the level of unreadable results. Just not worth the effort.


Example

The example has a render canvas on the left. The text and font center top. A zoomed view on right, that shows a zoomed in view of the left canvas canvas

The first style used is the pages default. The canvas resolution is 300by150 but scaled to fit 500 by 500 CSS pixels. This results in VERY poor quality canvas text. Cycling the canvas resolution will show how the canvas res effect the quality.

The functions

  • drawText(text, x, y, fontCSS, sizeCSSpx, colorStyleCSS) draws the text using CSS property values. Scaling the font to match the DOM visual size and aspect ratio as close as possible.

  • getFontStyle(element) returns the needed font styles as an object from element

UI Usage

  • CLICK center font to cycles font styles.

  • CLICK left canvas to cycle canvas resolutions.

  • At the bottom are the setting used to render the text in the canvas.

You will see that the quality of the text is dependent on the resolution of the canvas.

To see how DOM zoom effects the rendering you must zoom in or out of the page. HiDPI and retina displays will have a far lower quality canvas text due to the fact that the canvas is half the res of the CSS pixels.

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>
like image 35
Blindman67 Avatar answered Sep 22 '22 14:09

Blindman67