Chrome canvas 2d context measureText giving me weird results - javascript

Here's a compact version of my problem
let canvas = document.createElement('canvas')
let ctx = canvas.getContext('2d')
ctx.font = '11pt Calibri'
ctx.fillStyle = '#000000'
let temp = ctx.font
console.log(ctx.font)
console.log(ctx.measureText('M').width)
ctx.font = 'bold ' + ctx.font
console.log(ctx.font)
console.log(ctx.measureText('M').width)
ctx.font = 'italic ' + ctx.font
console.log(ctx.font)
console.log(ctx.measureText('M').width)
ctx.font = temp
console.log(ctx.font)
console.log(ctx.measureText('M').width)
Running this code in chrome produces incorrect numbers, at least at the end. First I'm setting the font to '11pt Calibri', but chrome immediately changes it to '15px Calibri' for some reason, and because of that it's producing text that's slightly bigger than correct. I read that the canvas runs at 96dpi so the correct px should be 14.6.
After that, I'm measuring the width of a text M, which comes out at 12.53401184 for me, this number is important.
After that, I've modified the font to add bold and italic, and then I roll it back to what the font originally was. Now when I measure it, it gives me 12.824707, which is a massive 0.3px off. I'm drawing text on a canvas with anywhere from 600px to 800px width, and I need it to wrap correctly, so I need it to be precise to 1px over the line, so individual letters need to have at least 0.02px accuracy, which worked decently until I started using bolds and italics.
None of the above problems exist on firefox, and disabling canvas hardware acceleration on chrome doesn't seem to have any effect. I'm using chrome 52.0 which is the current latest version.
Edit: I figured out you don't even need to do any of that to get the incorrect number, simply doing this is enough.
let canvas = document.createElement('canvas')
let ctx = canvas.getContext('2d')
ctx.font = '11pt Calibri'
ctx.fillStyle = '#000000'
console.log(ctx.font)
console.log(ctx.measureText('M').width)
let temp = ctx.font
ctx.font = temp
console.log(ctx.font)
console.log(ctx.measureText('M').width)

Don't use "pt" for font sizing on canvas.
CSS Absolute and Magic Units
Using pt for font sizing is not recommended as it has no real meaning for media that represent visual information in terms of pixels (discrete indivisible image units) and is displayed on screens which have no fixed pixel density.
pt is an absolute measuring unit, the same as cm while px is a "magic unit" and only ever has an absolute meaning when the media type is print.
OP: "I read that the canvas runs at 96dpi so the correct px should be 14.6."
This is not correct the canvas does not have a absolute measuring unit. A pixel as a CSS unit only has an absolute dimension when the media type is print in which case 1px = 1/96 of an inch. The canvas is not considered as a printed media.
Why does the Width change?
The apparent problem
ctx.font = '11pt Calibri'
console.log(ctx.font); // 15px Calibri
console.log(ctx.measureText('M').width); // 12.534011840820312
ctx.font = ctx.font
console.log(ctx.font); // 15px Calibri
console.log(ctx.measureText('M').width); // 12.82470703125
Though the ctx.font values are the same the measured font widths are different
The simple solution
ctx.font = ctx.font = '11pt Calibri';
Will avoid the measured size discrepancies, but i am sure that nobody would consider this as anything but an ugly work around to an "apparent" browser specific bug.
The solution
Don't use pt units when setting canvas fonts.
What is going on.
The issue is a misunderstanding of what the ctx.font property actually is. It does not represent the current font's actual internal representation but rather an abstract human readable form.
W3C 2D Canvas: "On getting, the font attribute must return the serialized form of the current font of the context."
The process of serialization will lose precision. Serialising CSS values.The W3C standard specifies that the font-size be in px units which in this case further amplifies the apparent "bug"
The font properties set function takes the CSS font string, parses it. If valid then sets the canvas internal font and writes the serialized CSS font value to context.font The two do not have to match and the standard does not specify that they should.
Summary
The behaviour as described in the question is not a "bug". Inconsistency between browsers though (as always) is a concern. If we are to follow the standards, one could consider that browsers not showing the measurement inconsistency have interpreted the standard incorrectly and filled a ambiguity with their own interpretation (though this is speculative on my part).
The simple solution to the problem is to follow the standard's guidelines and not use pt when setting font-size values for anything but printed media.
As with all computer media "dpi" only ever has meaning when printing and is not defined until then. Nor do pixels necessarily equate to dots when printing. Always use resolution when referring to pixels rather than dp1 (my pet hate)

I realized why it's broken. Chrome does something internally to compensate for pt values, even though the font gets highjacked to 15px. So when I get the font value from the ctx.font to modify it, I'm getting the modified px value, instead of the original pt so I'm actually giving it a raw 15px value, so when this happens chrome doesn't compensate. A workaround is to keep the original font somewhere else, like ctx.originalFont and then use it for modifying instead of ctx.font
For example this works
let canvas = document.createElement('canvas')
let ctx = canvas.getContext('2d')
ctx.font = '11pt Calibri'
ctx.originalFont = '11pt Calibri'
ctx.fillStyle = '#000000'
console.log(ctx.font)
console.log(ctx.measureText('M').width)
let temp = ctx.originalFont
ctx.font = temp
console.log(ctx.font)
console.log(ctx.measureText('M').width)

Related

Why there's white line appears in HTML canvas between two shape?

Why there's white line appears in JS canvas between two shape?
I'm making a game with JS / TS (I'm using MacBook Pro), with HTML5 canvas, and there's a unexpected white line appear between two shapes in safari browser:
but I run exactly same code in chrome, everything is fine:
So why this is happened? And how can I fix it?
code I'm using to render
CONTEXT.drawImage(
CACHES.get(this.materialURL),
(this.rect.x - camera.location.x) * GRID_W,
(this.rect.y - camera.location.y) * GRID_H,
GRID_W,
GRID_H,
);
Render artifacts
More info?
There are many reasons this can happen. Most are the result of rounding errors. Sometimes the error is in JavaScript, other times it occurs in the rendering.
There are subtle differences in the JS engines (resulting from hardware, OS, driver and or engine implementations) that can result in rendering artifact that differ across devices.
There are major differences in rendering implementations even on the same browser, same OS, and using the same hardware, depending on setup (flags).
Where your artifacts are coming from I can only guess at without a lot more information. Even how you captured the example images can change the solution.
Things to try
Try using nearest pixel lookup by setting 2D context smoothing off
ctx.imageSmoothingEnabled = false;
To turn back on use
ctx.imageSmoothingEnabled = true;
Use software rendering (CPU) by setting the willReadFrequently flag when getting the context.
const ctx = canvas.getContext("2d", {willReadFrequently: true});
Note this can slow things down a lot
Turn off canvas alpha (to stop BG appearing at seams) using context option alpha
const ctx = canvas.getContext("2d", {alpha: false});
Ensure that the source image resolution matches the render size.
In other words does
const img = CACHES.get(this.materialURL);
const isSameRes = img.width === GRID_W && img.height === GRID_H;
isSameRes should equal true?
Note use naturalWidth and naturalHeight if img is an instance of Image
Extend the source image by 1 px on each edge copying the edge pixels as shown in next image. This will prevent transparent edge pixels bleeding into rendering result.
Then render the inner original image as shown below
const img = CACHES.get(this.materialURL);
ctx.drawImage(
img,
1, 1, img.width - 2, img.height - 2,
(this.rect.x - camera.location.x) * GRID_W,
(this.rect.y - camera.location.y) * GRID_H,
GRID_W,
GRID_H,
);
Note this will add in tiny bit of overhead.
Ensure integer coordinates by flooring coordinates and forcing constants to be integers.
// When defining GRID_W and GRID_H (assuming positive integer values)
// Force internal type to int32 by using bitwise operation on values
// Note this may not do anything
const GRID_W = 32 | 0;
const GRID_H = 32 | 0;
// Render using floored coordinates.
ctx.drawImage(
CACHES.get(this.materialURL),
Math.floor((this.rect.x - camera.location.x) * GRID_W),
Math.floor((this.rect.y - camera.location.y) * GRID_H),
GRID_W,
GRID_H,
);
More
There are many more options but without the needed information I would be wasting your time.

How to calculate pixel width of given letter

I'm attempting to implement the ability to draw text to the screen on a basic 2D plane, using only HTML Canvas and JavaScript.
I start by setting up the canvas.
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
I have a keydown event listener that appends to an array whatever characters I enter.
let characters = []
document.onkeydown = function(event) {
if (event.key.length === 1) {
characters.append(event.key)
}
}
I want to draw each character onto the canvas.
let x,y = 0;
characters.forEach(char => {
ctx.beginPath()
ctx.font = "20px Arial"
ctx.fillText(char, x, y)
x += someValue // this needs to be calculated based on character pixel width
})
To avoid drawing all the characters at the same spot and overlapping, I have to increment the x coordinate by some value each iteration. This can't be a constant value, because characters differ in their pixel width ('O' is wider than 'I').
I need a way to know how much pixel space a character will take up, to determine what x coodinate to draw the subsequent character at.
I'm not looking for an HTML/JS specific answer. I want to know what the general computer science approaches are to rendering characters with correct spacing to a graphical context. i.e, how does HTML itself do it under the hood?

Modifying in memory canvases before drawing to screen drastically reduces Javascript performance

I've noticed if I have a large number of canvases in memory, modifying each canvas before drawing them to the screen drastically reduces performance on my machine. This occurs even when the canvases are small and the modifications are minor.
Here is the most contrived example I could come up with:
var { canvas, ctx } = generateCanvas();
ctx.strokeStyle = "#000";
var images = [];
for (var i = 0; i < 500; i++) {
images.push(generateCanvas(50, "red"));
}
var fps = 0,
lastFps = new Date().getTime();
requestAnimationFrame(draw);
function draw() {
requestAnimationFrame(draw);
var modRects = document.getElementById("mod-rects").checked;
var drawRects = document.getElementById("draw-rects").checked;
ctx.clearRect(0, 0, 500, 500);
ctx.strokeRect(0, 0, 500, 500);
fps++;
if (new Date().getTime() - lastFps > 1000) {
console.clear();
console.log(fps);
fps = 0;
lastFps = new Date().getTime();
}
images.forEach(img => {
img.ctx.fillStyle = "yellow";
if (modRects) img.ctx.fillRect(20, 20, 10, 10);
if (drawRects) ctx.drawImage(img.canvas, 225, 225);
});
}
function generateCanvas(size = 500, color = "black") {
var canvas = document.createElement("canvas");
canvas.width = canvas.height = size;
var ctx = canvas.getContext("2d");
ctx.fillStyle = color;
ctx.fillRect(0, 0, size, size);
return {
canvas,
ctx
};
}
function generateCheckbox(name) {
var div = document.createElement("div");
var check = document.createElement("input");
check.type = "checkbox";
check.id = name;
var label = document.createElement("label");
label.for = name;
label.innerHTML = name;
div.appendChild(check);
div.appendChild(label);
return div;
}
document.body.appendChild(canvas);
document.body.appendChild(generateCheckbox("mod-rects"));
document.body.appendChild(generateCheckbox("draw-rects"));
canvas+div+div { margin-bottom: 20px; }
In this example we create 500 canvases of size 50x50. There are two checkboxes underneath the larger onscreen canvas. The first causes a small yellow square to be drawn on each of those 500 canvases. The 2nd causes the canvases to be drawn to the larger canvas. FPS is posted to the console once per second. I see no performance issues when one or the other checkbox is checked, but when both are checked, performance drops drastically.
My first thought is that it has something to do with sending in-memory canvas to the gfx card every frame when they are modified.
Here's the actual effect I'm trying to create.
Video: https://youtu.be/Vr6v2oF3G-8
Code: https://github.com/awhipple/base-command-dev/blob/e2c38946cdaf573abff5ded5399c90687ffa76a5/engine/gfx/shapes/Particle.js
My ultimate goal is to be able to smoothly transition the colors of the canvas. I'm using globalCompositeOperation = "source-in" and fillRect() to do this in the code link above.
As has been stated before, this is an issue with the overhead of sending hundreds of canvases to the GPU every single frame. When a canvas is modified in CPU it gets marked as "dirty" and is re sent to the GPU next time it's used.
The workaround I found was to create a large canvas containing a grid of my particle images. Every particle object makes its modification to its assigned section of the grid. Then once all modifications are made, we begin making draw image calls, cutting up the larger canvas as needed
I also needed to switch to globalCompositeOperation = "source-atop" to prevent all other particles from getting trashed each time I tried to change one.
Code: https://github.com/awhipple/base-command-dev/blob/2514327c6c30cb9914962d2c8d604f04bfbdbed5/engine/gfx/shapes/Particle.js
Examples: http://avocado.whipple.life/
You can see here, when this.newRender === true in draw, it queues up to be drawn later.
Then static drawQueuedParticles is called once every particle has had a chance to queue itself up.
The end result is that this larger canvas is only sent to the GPU once per frame. I saw a performance increase from 15 FPS to 60 FPS on my Razorblade Pro running a 2700 RTX GPU with 1500 on screen particles.
I expect browsers are optimized to display 1, or at most a few canvases at a time. I'm betting each canvas is uploaded to the GPU individually, which would have way more overhead than a single canvas. The GPU has a limited number of resources, and using a lot of canvases could cause a lot of churn if textures and buffers are repeatedly cleared for each canvas. This answer WebGL VS Canvas 2D hardware acceleration also claims that Chrome didn't hardware accelerate canvases under 256px.
Since you're trying to do a particle effect with sprites, you'd be better off using a webgl library that's built for this kind of thing. I've had a good experience with https://www.pixijs.com/. If you're doing 3d, https://threejs.org/ is also popular. It is possible to build your own webgl engine, but it's very complicated and a lot of work. You have to worry about things like vector math, vertex buffers, supporting mobile GPU's, batching draw calls, etc. You'd be better off using an existing library unless you really have a strong need for something unique.

HTML5 Canvas blendmode

I'm new to HTML5 canvas and I want to reproduce the result of BlendMode.ADD in ActionScript 3.
According to the documentation, here's what BlendMode.ADD does:
Adds the values of the constituent colors of the display object to the
colors of its background, applying a ceiling of 0xFF. This setting is
commonly used for animating a lightening dissolve between two objects.
For example, if the display object has a pixel with an RGB value of
0xAAA633, and the background pixel has an RGB value of 0xDD2200, the
resulting RGB value for the displayed pixel is 0xFFC833 (because 0xAA
+ 0xDD > 0xFF, 0xA6 + 0x22 = 0xC8, and 0x33 + 0x00 = 0x33).
Source: http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/display/BlendMode.html#ADD
How can I do the same thing to an image in HTML5 Canvas?
The specification of the 2D canvas has implemented the blending mode with the name "lighter" (not to be confused with "lighten" which is a different mode) that will do "add".
var ctx = c.getContext("2d");
ctx.fillStyle = "#037";
ctx.fillRect(0, 0, 130, 130);
ctx.globalCompositeOperation = "lighter"; // AKA add / linear-dodge
ctx.fillStyle = "#777";
ctx.fillRect(90, 20, 130, 130);
<canvas id=c></canvas>
(update: I was remembering (incorrectly) lighten as the name for it, so sorry for the extra manual step in the original version of the answer).

Merge canvas image and canvas alpha mask into dataurl generated png

given two canvas with the same pixel size, where canvas1 contains an arbitrary image (jpg, png or such) and canvas2 contains black and non-black pixels.
what i want to achive: using a third canvas3 i want to clone canvas1 and have every black canvas2 pixel (may including a threshold of blackness) be transparent in canvas3
i already have a working solution like this:
canvas3context.drawImage(canvas1,0,0);
var c3img = canvas3context.getImageData(0,0,canvas3.width,canvas3.height);
var c2img = canvas2context.getImageData(0,0,canvas2.width,canvas2.height);
loop(){
if(c2img i-th,i+1-th,i+2-th pixel is lower than threshold)
set c3img.data[i]=c3img.data[i+1]=c3img.data[i+2]=c3img.data[i+3]=0
}
the problem with above (pseudo) code is, that it is slow
so my question is: anyone can share an idea how to speed this up significantly?
i thought about webgl but i never worked with it - so i have no idea about shaders or the tools or terms needed for this. another idea was that maybe i could convert canvas2 to black&white somehow very fast (not just modifieng every pixel in a loop like above) and work with blend modes to generate the transparent pixels
any help is highly appreciated
answering my own question, i provide a solution for merging an arbitrary image with a black&white image. what im still missing is how to set the alpha channel for just one color of a canvas.
I seperate the question in pieces and answer them each.
Question 1: How to convert a canvas into grayscale without iterating every pixel?
Answer: draw the image on to a white canvas with blend mode 'luminosity'
function convertCanvasToGrayscale(canvas){
var tmp = document.createElement('canvas');
tmp.width = canvas.width;
tmp.height = canvas.height;
var tmpctx = tmp.getContext('2d');
// conversion
tmpctx.globalCompositeOperation="source-over"; // default composite value
tmpctx.fillStyle="#FFFFFF";
tmpctx.fillRect(0,0,canvas.width,canvas.height);
tmpctx.globalCompositeOperation="luminosity";
tmpctx.drawImage(canvas,0,0);
// write converted back to canvas
ctx = canvas.getContext('2d');
ctx.globalCompositeOperation="source-over";
ctx.drawImage(tmp, 0, 0);
}
Question 2: How to convert a grayscale canvas into black&white without iterating every pixel?
Answer: two times color-dodge blend mode with color #FEFEFE will do the job
function convertGrayscaleCanvasToBlackNWhite(canvas){
var ctx = canvas.getContext('2d');
// in case the grayscale conversion is to bulky for ya
// darken the canvas bevore further black'nwhite conversion
//for(var i=0;i<3;i++){
// ctx.globalCompositeOperation = 'multiply';
// ctx.drawImage(canvas, 0, 0);
//}
ctx.globalCompositeOperation = 'color-dodge';
ctx.fillStyle = "rgba(253, 253, 253, 1)";
ctx.beginPath();
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fill();
ctx.globalCompositeOperation = 'color-dodge';
ctx.fillStyle = "rgba(253, 253, 253, 1)";
ctx.beginPath();
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fill();
}
Note: this function assumes that you want black areas left black and every non-black pixel become white! thus a grayscale image which has no black pixel will become completely white
the reason i choose this operation is that it worked better in my case and using only two blend operations means its pretty fast - if you want that more dark pixel be left black and more white pixel become white you can use the commented for loop to darken the image beforehand. thus dark pixel will become black and brighter pixel become darker. as you increase the amount of black pixel's using color-dodge will again do the rest of the job
Question 3: How to merge a Black&White canvas with another canvas without iterating every pixel?
Answer: use 'multiply' blend mode
function getBlendedImageWithBlackNWhite(canvasimage, canvasbw){
var tmp = document.createElement('canvas');
tmp.width = canvasimage.width;
tmp.height = canvasimage.height;
var tmpctx = tmp.getContext('2d');
tmpctx.globalCompositeOperation = 'source-over';
tmpctx.drawImage(canvasimage, 0, 0);
// multiply means, that every white pixel gets replaced by canvasimage pixel
// and every black pixel will be left black
tmpctx.globalCompositeOperation = 'multiply';
tmpctx.drawImage(canvasbw, 0, 0);
return tmp;
}
Question 4: How to invert a Black&White canvas without iterating every pixel?
Answer: use 'difference' blend mode
function invertCanvas(canvas){
var ctx = canvas.getContext("2d");
ctx.globalCompositeOperation = 'difference';
ctx.fillStyle = "rgba(255, 255, 255, 1)";
ctx.beginPath();
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fill();
}
now to 'merge' an canvasimage with a canvasmask one can do
convertCanvasToGrayscale(canvasmask);
convertGrayscaleCanvasToBlackNWhite(canvasmask);
result = getBlendedImageWithBlackNWhite(canvasimage, canvasmask);
regarding performance: obviously those blend modes are much faster than modifieng every pixel and to get a bit faster one can pack all functions together as needed into one function and recycle only one tmpcanvas - but thats left to the reader ^^
as a sidenote: i tested how the size of the resulting png differs when you compare above's getBlendedImageWithBlackNWhite result with the same image but the black areas are made transparent by iterating every pixel and setting the alpha channel
the difference in size is nearly nothing and thus if you dont really need the alpha-mask the information that every black pixel is meant to be transparent may be enough for futher processing
note: one can invert the meaning of black and white using the invertCanvas() function
if you want to know more of why i use those blend modes or how blend modes really work
you should check the math behind them - imho you're not able to develop such functions if ya dont know how they really work:
math behind blend modes
canvas composition standard including a bit math
need an example - spot the difference: http://jsfiddle.net/C3fp4/1/

Categories

Resources