When I transform an HTML5 canvas context and draw an ellipse, as the transform becomes larger the ellipse outline becomes completely distorted. Below is some sample code and the results I see in both Firefox and Chrome.
<body>
<canvas id="myCanvas" width=900 height=900></canvas>
<script>
var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
ctx.fillStyle = "rgba(0, 51, 255, 0.5)";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.transform(1, 0, 0, 1, -16776544, -16776916);
ctx.ellipse(16776994, 16777316, 400, 400, 0, 0, 2 * Math.PI);
ctx.stroke();
ctx.fill();
</script>
</body>
Chrome Results
Firefox Results
Is there a way to fix the distortion, or is it somewhere documented that there are limits on canvas transformation?
This is due to the limited precision of floating point numbers. There is not much you can do about it but to avoid really big and really small numbers.
With a pixel size of 0.1mm you are addressing a pixel that is near one mile from the origin (that's a very big screen).
I am not sure if the canvas rendering uses 32bit floats (thinking it does from your example and I know that webGL definitely uses floats) you need to do the transformations in javascript as it uses doubles 64bit which will give you a screen 9 orders (approx) greater (screen now size of the sun)
eg
var x = 16776544;
var y = 16776916;
var x1 = 16776234;
var y1 = 16776446;
ctx.setTransform(1,0,0,1,0,0);
x -= x1; // apply transform using doubles
y -= y1;
ctx.ellipse(x,y,100,100,0,0,Math.PI*2);
Related
I have some code in my game that rotates an icon within its canvas as follows:
if (rotate > 0) {
context.translate(canvasWidth / 2, canvasHeight / 2);
context.rotate(rotate);
context.translate(-canvasWidth / 2, -canvasHeight / 2);
}
Nothing you haven't seen before. I've also added a function that tiles the icons within a larger canvas like so:
var x = 0;
var y = 0;
for (var i = 0; i < totalUnits; i++) {
context.drawImage(img, x, y);
if (i != 0 && (i + 1) % level == 0) {
x = 0;
y += 72;
}
else {
x += 72;
}
}
Note that the variable level can be any integer, and totalUnits is its square if it's greater than 1, so for example if I specify level as 2, then 4 images are drawn on my canvas, 2 across and 2 down. Note also that my images are always 72x72 pixels, hence the 72 above. Again, nothing particularly exciting.
My difficulty is trying to rotate the images within the canvas such that the individual image is rotated by the value passed in rotate, but not the whole canvas itself. I have tried adding the following code with many permutations replacing the above call to context.drawImage in the for loop, with no luck so far:
context.translate(72 / 2, 72 / 2);
context.rotate(rotate);
context.drawImage(img, x, y);
context.rotate(0);
context.translate(-72 / 2, -72 / 2);
To help visualise the effect I am trying to achieve, here is what is drawn when rotate is set to 0:
And here is what I'd like my tiled images on the canvas to look like when rotated by 45 degrees (for example):
I'd like to point out that I am not trying to rotate the entire canvas - I know how to do that, but it is not the effect I'm trying to achieve as I need the icons to stay in their individual x, y positions. Also, rotating the entire canvas presents cutoff corner challenges.
Any help would be much appreciated!
Easiest way is to overwrite the current transform
ctx.setTransform(scale, 0, 0, scale, x, y);
ctx.rotate(rotate);
ctx.drawImage(img, -img.width / 2, -img.height/ 2);
Draw image center at x, y, rotated by rotate around img center, and scales by scale. Scale of 1 is no scale.
To reset to default transform for example before clearing the canvas
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
Because under the hood the ctx.rotate requires at least 1 sin and 1 cos, 12 temp numbers, then 12 multiplications and 8 additions. If the scaling is uniform (same scale for x and y), it is quicker to use a simplified method to creating the matrix. Note if the scale is always 1 you don't need the scale`
Also when the image loads you can set the point of rotation as properties of the image.
When the image loads set the offset
img.offset_x = - img.naturalWidth / 2; // NOTE I use snake_case for the property names
img.offset_y = - img.naturalHeight / 2; // so that the names will never clash with
// future changes to the standard
To render that image
const ax = Math.cos(rotate) * scale;
const ay = Math.sin(rotate) * scale;
ctx.setTranform(ax, ay, -ay, ax, x, y);
ctx.drawImage(img, img.offset_x, img.offset_y);
There are thousands of moving particles on an HTML5 canvas, and my goal is to draw a short fading trail behind each one. A nice and fast way to do this is to not completely clear the canvas each frame, but overlay it with semi-transparent color. Here is an example with just one particle:
var canvas = document.getElementById('display');
var ctx = canvas.getContext('2d');
var displayHeight = canvas.height;
var backgroundColor = '#000000';
var overlayOpacity = 0.05;
var testParticle = {
pos: 0,
size: 3
};
function render(ctx, particle) {
ctx.globalAlpha = overlayOpacity;
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.globalAlpha = 1.0;
ctx.fillStyle = '#FFF';
ctx.fillRect(particle.pos, displayHeight / 2, particle.size, particle.size);
}
function update(particle) {
particle.pos += 1;
}
// Fill with initial color
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
function mainLoop() {
update(testParticle);
render(ctx, testParticle);
requestAnimationFrame(mainLoop);
}
mainLoop();
<canvas id="display" width="320" height="240"></canvas>
There is an apparent problem: with low opacity values, the trail never fades away completely. You can see the horizontal line that (almost) does not fade in my single-particle example. I understand why this happens. ColorA overlayed by semi-transparent ColorB is basically a linear interpolation, and ColorA never fully converges to ColorB if we repeatedly do the following:
ColorA = lerp(ColorA, ColorB, opacityOfB)
My question is, what can I do to make it converge to the background color, so that trails don't remain there forever? Using WebGL or drawing trails manually are not valid options (because of compatibility and performance reasons respectively). One possibility is to loop over all canvas pixels and manually set pixels with low brightness to background color, although it may get expensive for large canvases. I wonder if there are better solutions.
As a workaround which could work in some cases is to set the overlayOpacity up to 0.1 (this value converges) but draw it only every x times and not in every render call.
So when drawn only every other time it keeps more or less the same trail length.
var renderCount = 0;
var overlayOpacity = 0.1;
function render(ctx, particle) {
if((renderCount++)%2 == 0) {
ctx.globalAlpha = overlayOpacity;
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
ctx.globalAlpha = 1.0;
ctx.fillStyle = '#FFF';
ctx.fillRect(particle.pos, displayHeight / 2, particle.size, particle.size);
}
Obviously the disadvantage is that it looks more jerked and perhaps this may not be acceptable in your case.
Best solution is to use the composite operation "destination-out" and fade to a transparent background. Works well for fade rates down to globalAlpha = 0.01 and event a little lower 0.006 but it can be troublesome below that. Then if you need even slower fade just doe the fade every 2nd or 3rd frame.
ctx.globalAlpha = 0.01; // fade rate
ctx.globalCompositeOperation = "destination-out" // fade out destination pixels
ctx.fillRect(0,0,w,h)
ctx.globalCompositeOperation = "source-over"
ctx.globalAlpha = 1; // reset alpha
If you want a coloured background you will need to render the animation on an offscreen canvas and render it over the onscreen canvas each frame. Or make the canvas background the colour you want.
If someone struggles with this, here is a workaround that worked for me:
// Do this instead of ctx.fillStyle some alpha value and ctx.fillRect
if(Math.random() > 0.8){
ctx.fillStyle = 'rgba(255, 255, 255, '+getRandomNumber(0.1,0.001)+')';
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
// Define this helper function somewhere in your code
function getRandomNumber(minValue, maxValue) {
return Math.random() * (maxValue - minValue) + minValue;
}
It also works for different colored backgrounds. Adjust trail length by playing around with Math.random() > 0.8 and getRandomNumber(0.1,0.001).
I am trying to draw a graph in a simple (x, y) plane.
I am using canvas transform to transform my coordinate system.
I am looking for a method to draw the 'linewidth' as a fixed pixel width independent of the x/y scale.
The problem is when x/y is large, the line width along x or y disappears or it is deformed because the line width is scaled relative to the x and y scale.
See example:
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
var y_scale=10
ctx.transform(1, 0, 0, 1/y_scale, 0, 0);
ctx.lineWidth = 10;
ctx.strokeRect(20, 20, 80, 100*y_scale);
Is there a way to fix the width independent of the transformation?
Of course I can do my own transformation matrix but I would rather not.
Same question: html5 canvas prevent linewidth scaling
Solution:
Apply transformation
Define path
Restore transformation
Stroke path
Code:
var y_scale=10;
ctx.beginPath();
ctx.save(); //save context
ctx.transform(1, 0, 0, 1 / y_scale, 0, 0);
ctx.rect(20, 20, 80, 100 * y_scale); // define rect
ctx.restore(); //restore context
ctx.lineWidth = 10;
ctx.stroke(); //stroke path
See example: https://jsfiddle.net/aj3sn7yv/2/
Instead of scaling the context you could scale your point series - no need for a complete matrix solution. This way you are just altering the "path" and not the rendering result.
var scale = 10;
var newPoints = scalePoints(points, scale); // scale or inverse: 1/scale
... plot new points here at the line-width you wish
// example function
function scalePoints(points, scale) {
var newPoints = [];
points.forEach(function(p) { newPoints.push(p * scale) });
return newPoints
}
Modify as needed.
I'm attempting to make a program that takes the information gathered from some calculations and plots it on a canvas graph. I need to scale the graph, however, so that it can accommodate larger numbers. But every time I put ctx.scale(); the whole canvas blanks out! I thought I could stop this by scaling the canvas first, but nothing is drawn on the canvas after I scale it.
Here's the coding for my canvas:
var c=document.getElementById("graph_");
var ctx=c.getContext("2d");
graph_.style.backgroundColor="white";
var z0=Math.max(Math.abs(a),Math.abs(b));
var z=Math.round(z0);
var z1=Math.round(z);
var z2=z*2
// alert(z1);
// alert(z2);
ctx.scale(3200/z,3200/z)
var xnew=360/2+360/2*a
var ynew=360/2-360/2*b
alert(xnew);
alert(ynew);
ctx.font = '10px Calibri';
ctx.fillText("( 0 , 0 )", 125, 85);
ctx.fillText(z1, 210, 87);
ctx.fillText(z2, 270, 87);
ctx.fillText(z1*-1, 75, 87);
ctx.fillText(z2*-1, 0, 87);
ctx.fillText(z1, 120, 43.5);
ctx.fillText(z2, 120, 10);
ctx.fillText(z1*-1, 120, 120);
ctx.fillText(z2*-1, 120, 145);
ctx.lineWidth = 1;
ctx.beginPath()
ctx.moveTo(150, 0);
ctx.lineTo(150, 400);
ctx.closePath();
ctx.lineWidth = .2;
ctx.moveTo(0, 75);
ctx.lineTo(400, 75);
ctx.strokeStyle = "#8B8682";
ctx.stroke();
ctx.closePath();
ctx.beginPath();
ctx.lineWidth = 2;
ctx.moveTo(xnew, 180);
ctx.lineTo(180, ynew);
ctx.strokeStyle = "red";
ctx.stroke();
Actually, the stuff is being drawn to the canvas, you just can't see it because you're both too far zoomed in and still in the upper left corner of the graph since the default origin points for drawing are in the top left as 0,0.
So if you want to zoom in that far (even though you probably want to zoom out to show bigger numbers, i.e. larger drawings on the graph) you need to translate the canvas origin point to your new origin point (the top left of what you want to see) before you scale the context.
You can use the translate method like
ctx.translate(newX,newY);
But before you do you're going to what to save the context's state so you can revert back to it.
Say you wanted to zoom in on the center of the graph you would translate to the point that is:
ctx.translate((-c.width /2 * scale) + offsetX,(-c.height / 2 * scale) + offsetY);
where the offsetX is the canvas width / 2 and offsetY is the canvas height / 2 and the scale is by the amount that you're scaling in you ctx.scale call.
What is the value of 3200/z, exactly?
I'm guessing that you are scaling your canvas by an enormous amount, so much so that the only thing visible on your screen would be the first few pixels of the canvas. Since you don't draw anything in the top-left 5 pixels of the screen, you don't see anything.
So i'm trying to map color space in HTML and my knowledge is pretty much limited to CSS HTML and Javascript. I am looking for a way to construct a 2 dimensional gradient, with 2 variable along 2 vectors. My research has indicated that CSS and SVG tech only has capacity for single dimension grdaients. Or rather Linear Grads can only have a single vector. So to make up for this limitation I am using JS to iterate over the 256 changes I need so that I can get a gradient on 2 RGB color channels. So picture if you will an x-axis that is relative to for example purposes - Red and Grads from 0 to 255 and y-axis that is likewise relative - Green and Grads from 0 to 255 but with a JS iteration instead of a CSS linear-grad.
What I end up with is a beautiful representation of RGB color space !BUT! changes to the z-axis -blue channel in this example- means that I have to call on a JS function that iterates through 256 loops updating the background of 256 DOM elements with new CSS linear grads.
I am making this web-app because of the limitations that I see in current web-based color pickers a 256 step loop for each change of the Z-axis will place an unacceptable amount of computation overhead into the program.
Any Idea's for a better way to make a dual vector gradient? Perhaps I could make an app specific library for the HTML 5 canvas element??? Where I would be operating on a bitmap instead of DOM elements maybe significantly lower the processor cost-per-call?
You can use the canvas element for that. Here are some examples of colorpickers.
Basically you want to create two linear gradients, one horizontal one vertical, moving from transparent to whatever rgba colors you want. Then draw one gradient over the other on the canvas. There's kind of a catch though, I've found that canvas doesn't make very clean rgba gradients, but you can uses half transparent colors, draw the first one once, the second one twice, then the first one again and it seems to give pretty good results. You can play with it though, here's some code to work off of.
var Draw = function(clr1, clr2){
clr1 = clr1 || 'rgba(255, 0, 0, 0.5)';
clr2 = clr2 || 'rgba(0, 0, 255, 0.5)';
var bg1 = document.getElementById('canvas').getContext('2d'),
grad1 = bg1.createLinearGradient(0, 128, 256, 128),
grad2 = bg1.createLinearGradient(128, 0, 128, 256);
grad1.addColorStop(0, 'rgba(255, 0, 0, 0)');
grad1.addColorStop(1, clr1);
grad2.addColorStop(0, 'rgba(0, 0, 255, 0)');
grad2.addColorStop(1, clr2);
bg1.fillStyle = grad1;
bg1.fillRect(0, 0, 256, 256);
bg1.fillStyle = grad2;
bg1.fillRect(0, 0, 256, 256);
bg1.fillRect(0, 0, 256, 256);
bg1.fillStyle = grad1;
bg1.fillRect(0, 0, 256, 256);
}
Here's a simple example showing how to create an arbitrary gradient on a canvas, with per-pixel control: http://jsfiddle.net/j85FQ/3/
colorField( myCanvas, 500, 500, pretty );
function colorField(canvas,width,height,colorLookup){
var w = width-1, h = height-1;
canvas.width = width;
canvas.height = height;
var ctx = canvas.getContext('2d'),
idata = ctx.getImageData(0,0,width,height),
data = idata.data;
for (var x=0;x<width;++x){
for (var y=0;y<height;++y){
var rgba = colorLookup(x/w,y/h);
var o = (width*y+x)*4;
for (var i=0;i<4;++i) data[o+i] = rgba[i]*255;
}
}
ctx.putImageData(idata,0,0);
}
function pretty(xPct,yPct){
return [ xPct, yPct, xPct*(1-yPct), 1];
}
Thanks guys I was able to work it out with the canvas element. I used a bucket fill for the z channel value and horizontal & vertical linear gradients from 0 to 255 for x and y channels. Setting context.globalCompositeOperation = "lighter" was the key I was missing. That was the simple additive mode I needed much easier then trying to find a suitable alpha compositing method. The following is the canvas init function I wrote.
function init() {
var c = document.getElementById('myCanvas');
var ctx = c.getContext('2d');
ctx.globalCompositeOperation = "lighter";
var grd = ctx.createLinearGradient(0, 0, 512, 0);
grd.addColorStop(0, "#000000");
grd.addColorStop(1, "#FF0000");
var grd2 = ctx.createLinearGradient(0, 0, 0, 512);
grd2.addColorStop(0, "#000000");
grd2.addColorStop(1, "#00FF00");
ctx.fillStyle = "#0000FF";
ctx.fillRect(0, 0, 512, 512);
ctx.fillStyle = grd;
ctx.fillRect(0, 0, 512, 512);
ctx.fillStyle = grd2
ctx.fillRect(0, 0, 512, 512)
}