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?
In CSS, we use the font-family property to specify the font of a text.
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>
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.
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 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.
HTMLCanvasElement.width
, and HTMLCanvasElement.height
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.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)
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.
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.
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
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>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With