Is there a bug in HTML Canvas' fillText function? - javascript

I just came across some unexpected behaviour on the HTML Canvas element; I tried to strip the problem down as much as I could. In short, it appears the ctx.fillText fails to render the text in specific regions of the canvas.
This is the smallest script I could write to consistently reproduce the bug (I tested it on different machines, OSs and browsers). It creates a black canvas (1.25×1.25 in drawing space units, 1000×1000 in pixels, the drawing space origin is in the middle) and draws red dots as the mouse passes over it, but there are several horizontal stripes in which it fails to do so.
// define boundaries of drawing space
const left = -.625;
const tops = -.625;
const scale = 800;
const width = 1.25;
const height = 1.25;
// create canvas
let canvas = document.createElement("canvas");
canvas.width = scale * width;
canvas.height = scale * height;
canvas.style.backgroundColor = "black";
document.body.appendChild(canvas);
// create context
let ctx = canvas.getContext("2d");
ctx.scale(scale, scale);
ctx.translate(-left, -tops);
ctx.textBaseline = "middle";
ctx.textAlign = "center";
ctx.font = `.03px arial`;
ctx.fillStyle = "rgba(208, 64, 64, 1)";
// coordinates follow mouse
addEventListener("mousemove", event => {
let mouseX = left + event.layerX / scale;
let mouseY = tops + event.layerY / scale;
ctx.fillText(".", mouseX, mouseY);
});
You can paste it in your dev-tools in about:blank and see for yourself. In any case here's a gif too:
As you can see, there are several horizontal stripes left untouched, even though I pass over them with the mouse. Also, the entirety of the bottom 80% of the canvas is unaffected.
A few important notes:
This occurs with any text, but I used the dot because it takes up the least amount of space, while bigger symbols easily bridged the gaps in the upper portion and made it more difficult to see.
The mousemove event is not the culprit, mouseX and mouseY update properly and smoothly.
It is not due to my mouse being dragged too quickly, it leaves the gaps no matter how slow I move.
It is not due to how small the scale is, as the x dimension has the same scaling as the y dimension, but the former doesn't present this issue.
This does not occur with the ctx.strokeText method, which works fine.
It also does not occur with ctx.fillPath.
Am I doing something wrong? Or is this actually a bug?

The problem is the extremely small font size you're trying to render. You shouldn't use values like .03px - it makes sense for renderers to not be able to render something like that correctly, considering the typical smallest paintable size on a display is 1 pixel (a little smaller than that on high DPI displays, but probably not much less than .25px). It may work for painting simple lines, but rendering is more complicated than that (e.g. hinting).
Try the following values:
scale = 80
width = 12.5
height = 12.5
ctx.font = `.3px arial`;
Alternatively, try to paint a dot or a square rather than a "." string.
As a side note, I was able to reproduce the problem on Chrome, but on Firefox it actually renders fine.

Related

JS using wrong coordinates when drawing on canvas with margin

Hey guys I have problem with using canvas together with some basic CSS styling.
So I wanted to get myself familiar a little bit more with canvas element so I followed Dev Ed's (Youtube) tutorial on how to make a drawing app with canvas.
I wanted to upgrade it a little bit by adding some color options for lines and background, added a title and so on. I wanted to center canvas element as well as give it fixed size.
Now when I'm trying to use it, canvas uses wrong coordinates to draw lines.
Thanks in advance.
Code that controls mouse coorinates:
function draw(e) {
if (!painting) return;
ctx.lineWidth = 10;
ctx.lineCap = "round";
ctx.lineTo(e.clientX, e.clientY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(e.clientX, e.clientY);
}
Link to full code on codepen: https://codepen.io/Skafec/pen/NWKzxrg
I recently created a couple of functions for that. The real coordinate is a composition of pointer position, translated using the bounding rect position, and apply a factor ( real size vs display size)
function translatedX(x){
var rect = canvas.getBoundingClientRect();
var factor = canvas.width / rect.width;
return factor * (x - rect.left);
}
function translatedY(y){
var rect = canvas.getBoundingClientRect();
var factor = canvas.width / rect.width;
return factor * (y - rect.top);
}
See https://codepen.io/fraigo/pen/jONKWaz

How to scale my canvas in reason, with the zoom scale?

I am working on a multiple web game using JavaScript. My canvas is currently set to the width and the height of my screen.
html
<canvas id = "canvas"></canvas>
javascript
var c=document.getElementById("canvas");
var ctx = c.getContext("2d");
//Making canvas scale;
c.width = window.innerWidth;
c.height = window.innerHeight;
function resize(){
//Add some code
}
My problem is, I do not want my players to zoom out, well not by default. It will make the game look bad and give the players an edge over everyone else. So I need to add some code to go into the resize method, that regardless of scale, the canvas will not be zoomed out. If the end result is something blurry at 300%+ that is fine.
IMPORTANT: the resize function cannot remove or reset the canvas back to default.
There are various ways to scale a canvas.
First off, there are 2 main parameters for the canvas size:
-Canvas Pixel Count. Set via canvas.width = 1000
-Canvas Display Pixel Size. Set via canvas.style.width = '1000px'
If you want all players to see a 1000x1000 region but displaying it fullscreen:
canvas.width = 1000;
canvas.height = 1000;
canvas.style.width = window.innerWidth + 'px';
canvas.style.height = window.innerHeight + 'px';
There is also another option with canvas.style.transform = 'scale(2,2)'.
This method is the closest thing to the browser zoom done via Ctrl+ or Ctrl-.
The big advantage of transform is that the scaling is applied to all DOM children elements. If your game is using HTML for its interface, then this is the way to go. (By applying the scaling on the div containing the canvas + HTML interface.

How do I make text gradient in canvas?

I want to fill my text with a top from bottom gradient. I've followed four or five different tutorials on how to achieve this but it doesn't work.
I have two different dynamic text sources, something like this:
<div id="first-name" contentEditable="true">Olaf</div>
<div id="last-name" contentEditable="true">Smith</div>
I want to draw whatever the user writes on the canvas (after a button press). I know how to fetch the values, so let's ignore that part and focus on two things:
1) How to know how wide the content is to fill with a gradient.
2) How to fill anything with a gradient to begin with...
They say this is how you should do it:
var d_canvas = document.getElementById('canvas')
var context = d_canvas.getContext('2d')
var firstNameGradient = context.createLinearGradient(6,38,6,70) //no idea what values I should use
firstNameGradient.colorStop(0, '#eede85')
firstNameGradient.colorStop(1, '#fea700')
context.fillStyle = firstNameGradient
context.font = "bold 26px Tahoma"
context.fillText(firstName, 6, 38)
context.font = "bold 36px Tahoma"
context.fillText(lastName, whereFirstNameEnds, 38)
However, whatever values I try to fill with it doesn't work. In, fact nothing gets drawn in at all.
So I have no idea how to solve either 1) or 2). Does anyone know?
Edit: Just noticed an error:
TypeError: firstNameGradient.colorStop is not a function
context.createLinearGradient(6,38,6,70) //no idea what values I should use
This is defining a line from (x1, y1) to (x2, y2) which in turn describes the angle of the gradient. Seems OK here, might need some fine tuning.
Change these:
firstNameGradient.colorStop(0, '#eede85')
firstNameGradient.colorStop(1, '#fea700')
to use addColorStop() instead
firstNameGradient.addColorStop(0, '#eede85')
firstNameGradient.addColorStop(1, '#fea700')
To find the width (in pixels) you can use measureText() and the property width of the returned object:
var tw = firstNameGradient.measureText(firstName).width;
Text is by default drawn with y representing the baseline. You can change this by setting text-baseline to top - this makes it easier to provide the gradient line:
firstNameGradient.textBaseline = "top";
The specs do provide a way to get the height of a font as well, but no browers has yet implemented this part so you need to either guess or use a DOM element to measure height.
Sorry for being so naive as to use w3Schools, but If you reverse engineer this snippet you should get the job done:
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
ctx.font="20px Georgia";
ctx.fillText("Hello World!",10,50);
ctx.font="30px Verdana";
// Create gradient
var gradient=ctx.createLinearGradient(0,0,c.width,0);
gradient.addColorStop("0","magenta");
gradient.addColorStop("0.5","blue");
gradient.addColorStop("1.0","red");
// Fill with gradient
ctx.fillStyle=gradient;
ctx.fillText("Big smile!",10,90);
Should you need any more advice check it out here
NOTE: Please, for the love of all things holy, don't use w3Schools, please try to use MDN
NOTE 2: I have no experience with canvas, so if I am making myself out to be an idiot, I am very sorry.
You can use ctx.measureText(txt) to get the right width for the canvas. Here's an example that works as required:
var context = document
.querySelector('canvas')
.getContext('2d');
var pos = {x: 50, y: 50};
var txt = 'Hello!';
var txtHeight = 50;
context.font = txtHeight + 'px Verdana';
var txtWidth = context.measureText(txt).width;
var gradient = context.createLinearGradient(
pos.x, pos.y, txtWidth, txtHeight);
gradient.addColorStop(0,"red");
gradient.addColorStop(1,"blue");
context.fillStyle = gradient;
context.fillText(txt,pos.x,pos.y);
And here's the demo on JSFiddle

Antialiasing when drawing lines in Canvas in Firefox

I'm trying to draw a diagram with canvas and want to get crisp lines, not anti-aliased. I know about the 0.5 offset you need to use to make lines fall exactly on screen pixels, but even with that I get anti-aliased lines in Firefox, while both Chrome and IE render it fine.
Here's some example code:
JS:
var canvas = document.getElementsByTagName('canvas')[0];
var ctx = canvas.getContext('2d');
canvas.width = 100;
canvas.height = 100;
ctx.translate(-0.5, -0.5); //To get crisp lines
ctx.lineWidth = 1;
ctx.strokeStyle = 'black';
for (var x = 20; x < 100; x += 20){
ctx.moveTo(x, 20);
ctx.lineTo(x,100);
ctx.stroke();
}
See JsFiddle: http://jsfiddle.net/einaregilsson/9yrF6/8/
This is what it looks like in Chrome and IE:
This is what it looks like in Firefox:
This is Firefox 26 on Windows 7. I've tried turning off hardware acceleration, which someone suggested but that makes no difference. Any ideas how I can get crisp lines on Firefox?
Also, is there anyone on Firefox that doesn't get anti-aliased lines when they look at the Fiddle? I'm wondering if this is a general Firefox issue, or particular to my setup.
It looks like you're slightly zoomed-in on Firefox (notice how the lines are spaced slightly further apart)
Hit Ctrl+0 to reset the zoom level. This should fix your problem.
It should be crisp in Firefox too, you probably have zoomed in.
Reset the zoom (CTRL+0)

Tricky screen sizing and scaling (on Javascript)

I have a tricky question that might just have a simple solution, although I trully don't see it now.
So, I've been working around HTML5 element and, obviously, doing the interaction methodology in JavaScript.
One of the objectives of this work is to be able to use a mobile device [MD] (iOS or Android, phone or tablet) as a remote controller for an application that will be served by another machine (eg. a laptop or external display) and both will be showing the same thing on each of the screens on different scales.
So, I wanna have an event occur when the canvas is 80% filled (or in this case, "erased" (which I already have by calculating the total number of [initial] pixels) and each device has a different count since the screen sizes/resolutions are different.
This is the tricky part: How will I be able to "scale" the MD pixel count and mirror that to the bigger screen?
For concrete measures, how will I be able to implement the following example:
I draw a line on the MD that goes for 300px wide, and for simplicity, let's say that this represents 10% of the MD canvas (which on both the screens is in fullscreen).
I want the external monitor (which has a higher resolution) to mirror this event but on an appropriate scale so that those 10% on the MD represent the same (scaled) 10% of "canvas real estate"
Just in case the text is too confusing, I'll leave the code bellow:
function totalPix(x, y) {
var total = x * y;
var objective = (total * 80) / 100;
}
function canvasApp() {
//prevent from scrolling (no bouncing)
document.body.addEventListener('touchmove', function(event){
event.preventDefault();
}, false);
if(!canvasSupport()) {
alert("No canvas support on this device!");
return;
} else if(!socketSupport) {
alert("No websocket support on this device!");
} else {
//create canvas on every load (//TODO)
var elemDiv = document.getElementById("content");
var newElem = document.createElement("canvas");
newElem.setAttribute("id", "frontscreen");
elemDiv.appendChild(newElem);
drawScreen();
function drawScreen() {
//Setup canvas
var canvas = document.getElementById("frontscreen");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
totalPix(canvas.width, canvas.height);
ctx = canvas.getContext("2d");
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
//Foreach touchmove event, send position to server
canvas.addEventListener('touchmove', function(event) {
for (var i = 0; i<event.touches.length; i++) {
var touch = event.touches[i];
ctx.globalCompositeOperation = "destination-out";
ctx.fillStyle = "white";
ctx.beginPath();
ctx.arc(touch.pageX, touch.pageY, 30, 0, 2*Math.PI, false);
ctx.fill();
ctx.stroke();
}
}, false);
window.onresize = function resizeCanvas() {drawScreen();};
}
}
}
If I understand you correctly, it's as simple as changing the size property on the HTML style property of your <canvas> element. For example, let's say you fill in a 300 by 300 px. square on Monitor A, which occupies 10% of the screen real estate (I know, big monitor). Then you load the same page on Monitor B, which is twice the size of Monitor A. (Really really big monitor, just bear with me here. It's an example.) Naturally, it will only occupy 5% of the screen's real estate.
If you want that 300px to always occupy the same percentage of size on all screens (but still be 300px on the canvas), you can do something like this:
var canvas = document.getElementById("mycanvas");
var heightAsPercent = 10;
var widthAsPercent = 10;
canvas.style.height = (heightAsPercent / 100) * screen.height;
canvas.style.width = (widthAsPercent / 100) * screen.width;
That way, the canvas will always occupy 10% of the screen, whether the monitor width is 3000px or 6000px. I've obviously chosen very verbose variable names for clarity, so feel free to modify them as needed.
The reason this works is that you're only modifying the CSS properties of the canvas, which affect only how it's rendered, not the actual <canvas> data. I came across this little trick by accident, and it drove me nuts until I figured out why it was doing this. Now it actually comes in handy. :)

Categories

Resources