How can I draw a circle on a canvas? - javascript

I'm using javascript and a canvas to draw a mathematically designed scale (for measuring torque, it includes newton-meters and foot-pounds). I had been positioning my ticks using trigonometry, and drawing arcing lines using arc, naturally. The problem came when they needed to line up, but there was some strange distortion. I then drew a very large circle and compared it with a circular selection in GIMP and there was a distortion. The circles are consistent every 45 degrees around the circles, but between those nodes the canvas drawn circle deviates outwards, much like an octagon that can been rounded too far outwards to approximate a circle.
Why do these distortions occur? How can I draw a truly circular circle on a canvas?
This is the code I used to generate my distorted circle.
<!DOCTYPE html>
<html>
<body>
<canvas width=8000 height=8000 id='myCanvas'></canvas>
<script>
var canvas = document.getElementById('myCanvas');
var context = canvas.getContext('2d');
context.beginPath();
context.arc(4000, 4000, 3200, 0, Math.PI*2, false);
context.lineWidth = 1;
context.strokeStyle = '#ff0000';
context.lineCap = 'square';
context.stroke();
</script>
</body>
</html>
This may not be minimal, I know not of the relevance of context.lineCap, but it's a vestige of the code this is based on.
The following screenshot shows the difference between the circle drawn by GIMP and the circle drawn by Chrome

This is Chromium Bug #164241 (declared fixed, although the fix may not have made it out yet). The short answer is: Chrome is approximating circles with composite Bezier curves.
I was unable to replicate this myself on Chromium (43.0.2357.130, Ubuntu) but it does occur on Firefox 39, although I couldn't find a similar bug report for Firefox. The fact that it's correct every 90 degrees (not 45, at least not for me) indicates that circles are being approximated by 4 curves, as the curve endpoints are guaranteed to be correct.
Luckily there's a simple workaround: create a path consisting of more than 4 curves. 8 or even 6 should be sufficient for the radii you're using but you can use more if you want to play it safe. Or you could increase the number of curves as a function of the radius, although calculating the optimal function (the minimum n that will produce an accurate circle for a given radius r) is non-trivial.
context.beginPath();
var n=8; // 4 is the default behaviour. higher is better, also slower
for (var i=0; i<n; i++) {
context.arc(4000, 4000, 3200, Math.PI*2*i/n, Math.PI*2*(i+1)/n, false);
}
context.stroke();
See also this question.
(And yes, lineCap is a red herring.)

Related

Unpredictable behaviour of HTML canvas

I observe weird behaviour of HTML canvas both in chrome and in firefox. For some reason there is one-pixel wide palid line when I fill rect over the smaller rect of different color after clipping.
Here is JS fiddle snippet: https://jsfiddle.net/srkgbxw1/7/
var my_canvas = document.getElementById('canvas'),
context = my_canvas.getContext("2d");
context.translate(0.5, 0.5)
context.fillStyle = "orange";
context.fillRect(10,10,100,100);
context.beginPath();
context.rect(20,20,90,90);
context.clip();
context.fillStyle = "white";
context.fillRect(0,0,110,110);
context.fillStyle = "orange";
context.fillRect(0,0,190,190);
This is the result:
Please help me figure out what the reason for the pallid line after third fillRect with orange?
UPD: original question's changed after I was pointed out I had made a stupid mistake in its first edition
UPD 2 I got the answer, this happens due to color interpolation at edges of clipping region because context was translated for half a pixel, which was done to get thin lines (recommended technique). To avoid interpolation, clipping region should be adjusted for half a pixel as well, then the pallid line disappears.
If you look into the logs:
TypeError: context.setStrokeStyle is not a function
Therefore, when you comment it, everything after it is executed while when you uncomment it, it fails and everything after this line is not executed.
EDIT: For the second question issue:
The issue is equivalent to this, why the blue square does not fully overlap the red square:
context.translate(0.5, 0.5);
context.fillStyle = "red";
context.fillRect(10,10,100,100);
context.fillStyle = "blue";
context.fillRect(10,10,100,100);
Because of translate, you are now drawing in float pixels, everything moved by half a pixel.
I suggest you read this for more information about how it is handled:
fillRect() not overlapping exactly when float numbers are used
Removing context.translate(0.5, 0.5); removes the "palid line".

Make canvas fill include stroke in shapes

Ok, so i am making a 3D rendering engine in pure javascript, as a challenge of course - to test my linear algebra skills. I am not using webgl, so please do not say "use webgl".
Anyways, the software will take in triangles, a camera and local transformations, and render the data onto the screen (i even made it interactive)
There are only 6 lines of rendering code, however, which are:
// some shading and math calculations then this:
context.fillStyle = color;
context.strokeStyle = color;
context.beginPath();
context.moveTo(x0, y0);
context.lineTo(x1, y1);
context.lineTo(x2, y2);
context.lineTo(x0, y0);
context.closePath();
context.fill();
context.stroke();
And while that works, it drops to 10fps with 4k+ faces on my Chromebook. (60fps on a regular computer)
Anyways, that outputs this:
But to make it faster, and because canvas state changes are slow, i removed the stroke, making the rendering code:
// some shading and math calculations then this:
context.fillStyle = color;
//context.strokeStyle = color;
context.beginPath();
context.moveTo(x0, y0);
context.lineTo(x1, y1);
context.lineTo(x2, y2);
context.lineTo(x0, y0);
context.closePath();
context.fill();
//context.stroke();
which runs twice as fast, but the resulting thing that gets rendered to the screen is this: (different model)
which has ugly lines everywhere at the edges of the triangles (which get removed when I re-add the stroke)
However, the fps doubles and performance gains are great...
So i believe the lines are caused because the canvas fill doesnt include the area where it would have stroked (the outline, as you may say).
I have tried to fix it with math, and although it works there are some edge cases where it doesn't
So my question is as follows:
Is there a way to make the context fill include the stroke area without stroking, because it is very expensive?
Using both stroke and fill will force the rasterization twice which explains the approximate double time.
The reason why you get glitches between the triangles is because of rounding errors and anti-aliasing. There is not a straight-forward solution to this; the stroke will cover the glitches of course, but to do it without the stroke will require you to offset and expand at least every other triangle.
However, you could use a small trick to cover up the gap and that is to redraw the entire image (as bitmap) on top offset just a single pixel (you might get away with 0.5 pixel but then anti-aliasing is needed). This adds to the time, but far less than rasterization or recalculation of the paths.
Say that the result on the left is what you have (simulated here) with a clear gap. Redrawing it on top as shown in the right will cover the gap without too much distortion.
Simply use:
ctx.drawImage(sourceCanvas, 1, 1);
Tip: when only calling fill() you don't need closePath() as it is called implicit, saving one op. Microscopic gain perhaps but still (with more complex geometry it even might have an influence :) ).
Note: drawing to itself will cause an internal allocation of a temporary bitmap copy. However, you will only need to do one extra drawImage() operation. The option is to use off-canvas render but draw twice to a main displayed canvas. Either way...
var ctx = c.getContext("2d");
ctx.fillStyle = "#777";
tri(10,10, 72,17, 40.2, 100);
// simulates gap
ctx.fillStyle = "#222";
tri(72.5,17.5, 40.7,100.5, 90,25);
// fill entire image back again, drawn twice here for demo
ctx.drawImage(c, 100, 0);
ctx.drawImage(c, 0, 0, 100, 150, 101, 1, 100, 150);
ctx.fillText("Raster", 5, 8);
ctx.fillText("Offset self", 105, 8);
function tri(x0,y0,x1,y1,x2,y2) {
ctx.beginPath();
ctx.moveTo(x0, y0);
ctx.lineTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.fill();
}
<canvas id=c></canvas>

p5.js canvas drawing path with ellipses

I am working on a real time canvas drawing webapp using socket.io, node.js, and p5.js. I am having trouble creating a smooth line when the mouse is dragged. If the mouse is dragged too fast there is a trail of empty space in between each ellipse. The end goal here is to create a smooth path. Here are the things I have tried so far:
Attempt 1:
noStroke();
fill(lineColor[0],lineColor[1],lineColor[2]);
ellipse(mouseX, mouseY, lineThickness, lineThickness);
Attempt 2:
strokeWeight(lineThickness);
line(mouseX,mouseY);
stroke(lineColor[0],lineColor[1],lineColor[2]);
Here is a picture of what the issue looks like:
canvas drawing incomplete path image
any feedback is welcome; thanks!
Kevin's answer is great because it will be more efficient to draw lines instead of many ellipses. You should also look into:
beginShape()/endShape()
bezierVertex()
curveVertex()
curvePoint()
The above should help you draw a smooth path and setting a thicker stroke will looks as it many filled ellipses are connected forming the path.
If for some reason you do want to draw many ellipses, you can interpolate position when the mouse move faster and create gaps to fill those gaps.
For more information and p5.js example, check out this answer:
Processing: Draw vector instead of pixels
You could use the pmouseX and pmouseY variables, which hold the previous position of the cursor. Use that to draw a line from the previous position to the current position to fill in the blank space between mouse events.
From the reference:
// Move the mouse across the canvas to leave a trail
function setup() {
//slow down the frameRate to make it more visible
frameRate(10);
}
function draw() {
background(244, 248, 252);
line(mouseX, mouseY, pmouseX, pmouseY);
print(pmouseX + " -> " + mouseX);
}
<script src="https://github.com/processing/p5.js/releases/download/0.5.11/p5.js"></script>

Canvas drawImage - visible edges of tiles in firefox/opera/ie (not chrome)

I'm drawing a game map into canvas. The ground is made of tiles - simple 64x64 png images.
When I draw it in Chrome, it looks ok (left), but when I draw it in Firefox/Opera/IE (right), I get visible edges:
The problem disappears when I use rounded numbers:
ctx.drawImage(img, parseInt(x), parseInt(y), w, h);
But that doesn't help when I use scaling:
ctx.scale(scale); // anything from 0.1 to 2.0
I also tried these, but no change:
ctx.drawImage(img, 5, 5, 50, 50, x, y, w, h); // so not an issue of clamping
ctx.imageSmoothingEnabled = false;
image-rendering: -moz-crisp-edges; (css)
Is there any way to make it work in ff/op/ie?
Edit: Partial solution found
Adding 1 pixel to width/height and compensating it by scale (width+1/scale) seems to help:
ctx.drawImage(props.img, 0, 0, width + 1/scale, height + 1/scale);
It makes some artifacts, but I think it's acceptable. On this image, you can see green tiles without edges, and blue windows, which are not compensated, still with visible edges:
The simplest solution (and I'd argue most effective) is to use tiles that have a 1 pixel overlap (are either 1x1 or 2x2 larger) when drawing the background tiles of your game.
Nothing fancy, just draw slightly more than you would normally. This avoids complications and performance considerations of bringing extra transformations into the mix.
For example:
var img = new Image();
img.onload = function () {
for (var x = 0.3; x < 200; x += 15) {
for (var y = 0.3; y < 200; y += 15) {
ctx.drawImage(img, 0, 0, 15, 15, x, y, 15, 15);
// If we merely use 16x16 tiles instead,
// this will never happen:
//ctx.drawImage(img, 0, 0, 16, 16, x, y, 16, 16);
}
}
}
img.src = "http://upload.wikimedia.org/wikipedia/en/thumb/0/06/Neptune.jpg/100px-Neptune.jpg";
Before: http://jsfiddle.net/d9MSV
And after: http://jsfiddle.net/d9MSV/1/
Note as the asker pointed out, the extra pixel needs to account for scaling, so a more correct solution is his modification: http://jsfiddle.net/d9MSV/3/
Cause
This is caused by anti-aliasing.
Canvas is still work-in-progress and browser has different implementations for handling anti-aliasing.
Possible solutions
1
You can try turning off anti-aliasing for images in Firefox like this:
context.mozImageSmoothingEnabled = false;
In Chrome:
context.webkitImageSmoothingEnabled = false;
and add a class to the element like this (should work with Opera):
canvas {
image-rendering: optimizeSpeed; // Older versions of FF
image-rendering: -moz-crisp-edges; // FF 6.0+
image-rendering: -webkit-optimize-contrast; // Webkit
image-rendering: -o-crisp-edges; // OS X & Windows Opera (12.02+)
image-rendering: optimize-contrast; // Possible future browsers.
-ms-interpolation-mode: nearest-neighbor; // IE
}
Here's a browser test I made to see the effect of turning off anti-aliasing:
ANTI-ALIAS BROWSER TEST
2
Translate the whole canvas by 0.5 point.
ctx.translate(0.5, 0.5);
This doesn't always work and might come in conflict with other translations. However you can add a fixed offset each time:
ctx.translate(scrollX + 0.5, scrollY + 0.5);
3
Another option is to do a compromise that you either pad the tiles with one extra pixel which I don't recommend due to the extra work you'll get maintaining this.
4
This method draws the tiles a bit scaled so they overlap:
ctx.drawImage(tile, x, y, 65, 65); //source tile = 64x64
This might be enough to cover the glitch. Combined with turning anti-alias off (or using nearest neighbor) it won't affect much of the tile graphics, but it might reduce performance a tad due to the scaling.
If you turn off anti-aliasing (and that didn't work on its own) the overhead will be minimal as some goes to interpolate the image.
5
Simply draw everything offset -1 position (ie. grid = 63x63). Of course this will screw up everything else regarding checks so...
In every tile draw use Math.floor when there is division involved, like this:
ctx.drawImage(image,Math.floor(xpos/3),ypos+1)
Also, if you have a loop to draw, that calls itself, always use requestAnimationFrame. I don't know why, but since I moved from timer timeout to requestAnimationFrame I have no more artifacts.
I draw all of my tiles to a perfectly sized buffer and then draw that buffer to the display canvas with drawImage, which takes care of scaling. If you have 16x16 tiles, make your buffer some multiple of 16, like 256x128 or 64x96 or something along those lines. This eliminates spaces between tiles that arise due to drawing with scaled dimensions. The only downside is that you must draw the full background twice: once to draw the background in pixel perfect space, and once to draw the scaled image to the final display canvas. Remember to maintain aspect ratio between the buffer and display canvas to avoid skewing your final image.

Canvas - Fill a rectangle in all areas that are fully transparent

I'm writing a simple 2D game engine using the HTML5 canvas. I've come to adding a lighting engine. Each light source has a radius value and an intensity value (0-1, eg 1 would be very bright). There's also an ambient light value that is used to light everything else in the world that isn't near a light source (0-1, eg 0.1 would be moonlight). The process of lighting is done on a separate canvas above the main canvas:
For each light source, a radial gradient is drawn at that position with the same radius as the light source. The gradient is given two stops: the center is black with an alpha of 1-intensity of the light and the end/edge is black with alpha of 1-ambient light value. That all works fine.
This is where it goes wrong :/ I need to fill the whole canvas with black with and alpha of 1-ambient light value and at the moment I do this by setting the context.globalCompositeOperation to source-out then fillRecting the whole canvas.
My code for this stuff is:
var amb = 'rgba(0,0,0,' + (1-f.ambientLight) + ')';
for(i in f.entities) {
var e = f.entities[i], p = f.toScreenPoint(e.position.x, e.position.y), radius = e.light.radius;
if(radius > 0) {
var g = cxLighting.createRadialGradient(p.x, p.y, 0, p.x, p.y, radius);
g.addColorStop(0, 'rgba(0,0,0,' + (1-e.light.intensity) + ')');
g.addColorStop(1, amb);
cxLighting.fillStyle = g;
cxLighting.beginPath();
cxLighting.arc(p.x, p.y, radius, 0, Math.PI*2, true);
cxLighting.closePath();
cxLighting.fill();
}
}
//Ambient light
cxLighting.globalCompositeOperation = 'source-out';
cxLighting.fillStyle = amb;
cxLighting.fillRect(0, 0, f.width, f.height);
cxLighting.globalCompositeOperation = 'source-over';
However instead of getting what I wan't out of the engine (left) I get a kind of reversed gradient (right). I think this is because when I draw the rectangle with the source-out composite operation it affects the colours of the gradient itself because they are semi-transparent.
Is there a way to do this differently or better? Using clipping maybe, or drawing the rect over everything first?
Also, I modified the Mozila Dev Centre's example on composting to replicate what I need to do and none of the composite modes seemed to work, check that out if it would help.
Thanks very much, any answer would be great :)
One trivial way would be to use imageData but that would be painfully slow. It's an option, but not a good one for a game engine.
Another way would be to think of the ambient light and the light-source as if they were one path. That would make it very easy to do:
http://jsfiddle.net/HADky/
Or see it with an image behind: http://jsfiddle.net/HADky/10/
The thing you're taking advantage of here is the fact that any intersection of a path on canvas is always only unioned and never compounded. So you're using a single gradient brush to draw the whole thing.
But it gets a bit trickier than that if there's more than one light-source. I'm not too sure how to cover that in an efficient way, especially if you plan for two light-sources to intersect.
What you should probably do instead is devise an alpha channel instead of this overlay thing, but I can't currently think of a good way to get it to work. I'll revisit this if I think of anything else.
EDIT: Hey! So I've done a bit of thinking and came up with a good solution.
What you need to do is draw a sort of alpha channel, where the dark spots mark the places where you want light to be. So if you had three light sources it would look like this:
Then you want to set the fill style to your ambient color and set the globalCompositeOperation to xor and xor the whole thing.
ctx.fillStyle = amb;
ctx.globalCompositeOperation = 'xor';
ctx.fillRect(0,0,500,500);
That will leave you with the "opposite" image except the transparent parts will be correctly ambient:
Here's a working example of the code:
http://jsfiddle.net/a2Age/
Extra optimization: You can actually achieve the effect without using any paths at all, by simply drawing the exact same radial gradients onto rects instead of circular paths:
http://jsfiddle.net/a2Age/2/
Hope that helps!
Just an idea, but since you're getting the opposite effect you're going for from your gradient, have you tried reversing the gradient?

Categories

Resources