Agar.io style ripple effect for canvas arcs - javascript

I really love how they created the online game agario. I have been thinking: "How did they created this ripple effect for the edges?"
There are a few things I could think of:
1) The border is made of many vector points, therefore allowing flexible animation of the border.
2) The border is a predefined gif like animation.
3) There are many invisible pixels around the edge. They loop around the arc and activate a few groups of those pixels, therefore creating an illusion that the border is "contracting" and "retracting".
How can something like this be done in HTML5 canvas? Do you think one of my 3 ideas for a solution apply or is it more complex than that?

What you can do is repeatedly draw a sine wave around the circumference of a circle.
The equations to get the sine wave [x,y] point at any angle around a circle are:
var x = centerX+(radius+amplitude*Math.sin(sineCount*angle))*Math.cos(angle);
var y = centerY+(radius+amplitude*Math.sin(sineCount*angle))*Math.sin(angle);
The centerX, centerY and radius define the circle.
The amplitude determines how far from the circle's circumference the sine wave will travel.
The sineCount is the number of complete sine waves that will be drawn around the circle.
The angle is the current angle around the circle which you desire the "sined" [x,y].
Here's an example and a Demo:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cx=150;
var cy=150;
var radius=100;
var amp=10;
var sineCount=10;
ctx.beginPath();
for(var i=0;i<360;i++){
var angle=i*Math.PI/180;
var pt=sineCircleXYatAngle(cx,cy,radius,amp,angle,sineCount);
ctx.lineTo(pt.x,pt.y);
}
ctx.closePath();
ctx.stroke();
function sineCircleXYatAngle(cx,cy,radius,amplitude,angle,sineCount){
var x = cx+(radius+amplitude*Math.sin(sineCount*angle))*Math.cos(angle);
var y = cy+(radius+amplitude*Math.sin(sineCount*angle))*Math.sin(angle);
return({x:x,y:y});
}
body{ background-color: ivory; }
#canvas{border:1px solid red; margin:0 auto; }
<canvas id="canvas" width=300 height=300></canvas>

(late reply but probably more accurate than the accepted answer)
I was also wondering how this effect was done and since I couldn't find any informations about it I decided to dive into the obfuscated code.
Firstly, one should note that the cells are not circles but rather polygons. Every point of the polygon is constrained to keep the same distance from the center, which makes the calculations much easier. In addition each point has a velocity which is represented by a scalar (a positive velocity tends to move the point away from the center while a negative one will bring it closer). Whenever a point is out of the map or touches another point of a different cell, its velocity is decreased. At each iteration the velocity is added to the point and then increased by a small amount (natural decay).
With this set of rules and some (minor) additional constraints you should be able to reproduce this effect. I tried myself and ended up with a pretty good result.
Edit: I also made a Scala.js fiddle: https://scalafiddle.io/sf/FMoNM7c/0

Related

EaselJS triangle with rounded corners?

How can I create a triangle with rounded corners in EaselJS? I'm using drawPolyStar to create the triangle,
var polystar = new createjs.Shape();
polystar.graphics.drawPolyStar(100, 100, 60, 3, 0, -90);
This is an image of what I want the triangle to look like:
EDIT: Image link doesn't seem to work. This is what the triangle should look like :
But actual triangle has sharp corners.
There are no canvas APIs for rounding corners of polygons that works like the roundRect API.
There are a few approaches I can think of:
Do the round corners yourself using arcTo. This would take some math to figure out, and there may even be some libraries or examples floating around.
[EDIT] I spent a little time making a sample https://jsfiddle.net/lannymcnie/cga60tsf/1/
Using rounded stroke edges, you can sort of fake it. This fiddle shows how a thick line with round ends can make the outer edges look round. You could draw another smaller triangle on top to give the desired effect.
Sample
// Line outer radius
context.lineJoin = "round";
context.lineWidth = cornerRadius;

Pixelated Lines (HTML Canvas) [duplicate]

I have been writing a little javascript plugin, and i am having a little trouble with improving the canvas overall quality of the render. I have searched over the web here and there but can not find anything that makes sense.
The lines created from my curves are NOT smooth, if you look at the jsfiddle below you will understand what I mean. It kind of looks pixelated. Is there a way to improve the quality? Or is there a Canvas Framework that already uses some method to auto improve its quality that I can use in my project?
My Canvas Render
Not sure if this helps but i am using this code at the start of my script:
var c = document.getElementsByClassName("canvas");
for (i = 0; i < c.length; i++) {
var canvas = c[i];
var ctx = canvas.getContext("2d");
ctx.clearRect(0,0, canvas.width, canvas.height);
ctx.lineWidth=1;
}
}
Thanks in advance
Example of my Curve Code:
var data = {
diameter: 250,
slant: 20,
height: 290
};
for (i = 0; i < c.length; i++) {
var canvas = c[i];
var ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.moveTo( 150 + ((data.diameter / 2) + data.slant ), (data.height - 3) );
ctx.quadraticCurveTo( 150 , (data.height - 15), 150 - ((data.diameter / 2) + data.slant ), (data.height - 3));
ctx.lineTo( 150 - ((data.diameter / 2) + data.slant ), data.height );
ctx.quadraticCurveTo( 150 , (data.height + 5), 150 + ((data.diameter / 2) + data.slant ), data.height);
ctx.closePath();
ctx.stroke();
}
The Problem
This is one of those cases where it's almost impossible to get a smooth result without manually tweaking it.
The cause has to do with the minimal space to distribute smoothing pixels. In this case we only have a single pixel height between each section in the quadratic curve.
If we look at a curve with no smoothing we can more clearly see this limitation (without smoothing each pixel sits on an integer position):
The red line indicates a single section and we can see that the transition between the previous and next section has to be distributed over the height one pixel. See my answer here for how this works.
Smoothing is based on the remaining fraction for the point's coordinate conversion to integer. Since smoothing then uses this fraction to determine the color and alpha based on stroke main color and background color to add a shaded pixel, we will quickly run into limitations as each pixel used for smoothing occupies a whole pixel itself and due to the lack of space as here, the shades will be very rough and therefor revealing.
When a long line goes from y to y+/-1 (or x to x+/-1) there is not a single pixel between the end points that would land on a perfect bound which means every pixel between is instead a shade.
If we take a closer look at a couple of segments from the current line we can see the shades more clearly and how it affects the result :
Additionally
Though this explains the principle in general - Other problems are (as I barely hinted about in revision 1 (last paragraph) of this answer a few days ago, but removed and forgot about going deeper into it) is that lines drawn on top of each other in general, will contribute to contrast as the alpha pixels will blend and in some parts introduce higher contrast.
You will have to go over the code to remove unneeded strokes so you get a single stroke in each location. You have for instance some closePaths() that will connect end of path with the beginning and draw double lines and so forth.
A combination of these two should give a nice balance between smooth and sharp.
Smoothing test-bench
This demo allows you to see the effect for how smoothing is distributed based on available space.
The more bent the curve is, the shorter each section becomes and would require less smoothing. The result: smoother line.
var ctx = c.getContext("2d");
ctx.imageSmoothingEnabled =
ctx.mozImageSmoothingEnabled = ctx.webkitImageSmoothingEnabled = false; // for zoom!
function render() {
ctx.clearRect(0, 0, c.width, c.height);
!!t.checked ? ctx.setTransform(1,0,0,1,0.5,0.5):ctx.setTransform(1,0,0,1,0,0);
ctx.beginPath();
ctx.moveTo(0,1);
ctx.quadraticCurveTo(150, +v.value, 300, 1);
ctx.lineWidth = +lw.value;
ctx.strokeStyle = "hsl(0,0%," + l.value + "%)";
ctx.stroke();
vv.innerHTML = v.value;
lv.innerHTML = l.value;
lwv.innerHTML = lw.value;
ctx.drawImage(c, 0, 0, 300, 300, 304, 0, 1200, 1200); // zoom
}
render();
v.oninput=v.onchange=l.oninput=l.onchange=t.onchange=lw.oninput=render;
html, body {margin:0;font:12px sans-serif}; #c {margin-top:5px}
<label>Bend: <input id=v type=range min=1 max=290 value=1></label>
<span id=vv></span><br>
<label>Lightness: <input id=l type=range min=0 max=60 value=0></label>
<span id=lv></span><br>
<label>Translate 1/2 pixel: <input id=t type=checkbox></label><br>
<label>Line width: <input id=lw type=range min=0.25 max=2 step=0.25 value=1></label>
<span id=lwv></span><br>
<canvas id=c width=580></canvas>
Solution
There is no good solution unless the resolution could have been increased. So we are stuck with tweaking the colors and geometry to give a more smooth result.
We can use a few of tricks to get around:
We can reduce the line width to 0.5 - 0.75 so we get a less visible color gradient used for shading.
We can dim the color to decrease the contrast
We can translate half pixel. This will work in some cases, others not.
If sharpness is not essential, increasing the line width instead may help balancing out shades. Example value could be 1.5 combined with a lighter color/gray.
We could use shadow too but this is an performance hungry approach as it uses more memory as well as Gaussian blur, together with two extra composition steps and is relatively slow. I would recommend using 4) instead.
1) and 2) are somewhat related as using a line width < 1 will force sub-pixeling on the whole line which means no pixel is pure black. The goal of both techniques is to reduce the contrast to camouflage the shade gradients giving the illusion of being a sharper/thinner line.
Note that 3) will only improve pixels that as a result lands on a exact pixel bound. All other cases will still be blurry. In this case this trick will have little to no effect on the curve, but serves well for the rectangles and vertical and horizontal lines.
If we apply these tricks by using the test-bench above, we'll get some usable values:
Variation 1
ctx.lineWidth = 1.25; // gives some space for lightness
ctx.strokeStyle = "hsl(0,0%,50%)"; // reduces contrast
ctx.setTransform(1,0,0,1,0.5,0.5); // not so useful for the curve itself
Variation 2:
ctx.lineWidth = 0.5; // sub-pixels all points
ctx.setTransform(1,0,0,1,0.5,0.5); // not so useful for the curve itself
We can fine-tune further by experimenting with line width and the stroke color/lightness.
An alternative is to produce a more accurate result for the curves using Photoshop or something similar which has better smoothing algorithms, and use that as image instead of using native curve.
This question struck me as a little odd. Canvas rendering, though not the best when compared to high end renderers is still very good. So why is there such a problem with this example. I was about to leave it, but 500 points is worth another look. From that I can give two bits of advice, a solution, and an alternative.
First, designers and their designs must incorporate the limits of the media. It may sound a little presumptuous but you are trying to reduce the irreducible, you can not get rid of aliasing on a bitmap display.
Second, Always write neat well commented code. There are 4 answers here and no-one picked out the flaw. That is because the presented code is rather messy and hard to understand. I am guessing (almost like me) the others skipped your code altogether rather than work out what it was doing wrong.
Please Note
The quality of images in all the answers for this question may be scaled (thus resampled) by the browser. To make a true comparison it is best to view the images on a separate page so that they are not scaled.
Results of study of problem in order of quality (in my view)
Genetic algorithm
The best method I found to improve the quality, a method not normally associated to computer graphics, is to use a very simple form of a genetic algorithm (GA) to search for the best solution by making subtle changes to the rendering process.
Sub pixel positioning, line width, filter selection, compositing, resampling and colour changes can make marked changes to the final result. This present billions of possible combinations, any one of which could be the best. GAs are well suited to finding solutions to these types of searches, though in this case the fitness test was problematic, because the quality is subjective the fitness test has to be also, and thus requires human input.
After many iterations and taking up rather a bit more of my time than I wanted I found a method very well suited to this type of image (many thin closely spaced lines) The GA is not suitable for public release. Fortunately we are only interested in the fittest solution and the GA created a sequence of steps that are repeatable and consistent for the particular style it was run to solve.
The result of the GA search is.
see Note 1 for processing steps
The results is far better than I expected so I had to have a closed look and noticed 2 distinct features that set this image apart from all the others presented in this and other answers.
Anti-aliasing is non uniform. Where you would normally expect a uniform change in intensity this method produces a stepped gradient (why this makes it look better I do not know)
Dark nodes. Just where the transition from one row to the next is almost complete the line below or above is rendered noticeably darker for a few pixels then reverts back to the lighter shade when the line is fitting the row. This seams to compensate lightening of the overall line as it shares its intencity across two rows.
This has given me some food for thought and I will see if these features can be incorporated directly into the line and curve scan line rendering.
Note 1
The methods used to render the above image. Off screen canvas size 1200 by 1200. Rendered at scale 4 ctx.setTransform(4,0,0,4,0,0), pixel y offset 3 ctx.translate(0,3), Line width 0.9pixels rendered twice on white background, 1 pixel photon count blur repeated 3 times (similar to convolution 3*3 gaussian blur but a little less weight along the diagonals), 4 times down samples via 2 step downsample using photon count means (each pixel channel is the square root of the mean of the squares of the 4 (2 by 2) sampled pixels). Sharpen one pass (unfortunately that is a very complex custom sharpen filter (like some pin/pixel sharpen filters)) and then layered once with ctx.globalCompositeOperation = "multiply" and ctx.globalAlpha = 0.433 then captured on canvas. All processing done on Firefox
Code fix
The awful rendering result was actually caused by some minor rendering inconsistencies in you code.
Below is the before and after the code fix. As you can see there is a marked improvement.
So what did you do wrong?
Not to much, the problem is that you where rendering lines over the top of existing lines. This has the effect of increasing the contrast of those lines, the render does not know you don't want the existing colours and thus adds to the existing anti aliasing doubling the opacity and destroying the effect.
Bellow your code with only the rendering of the shape. Comments show the changes.
ctx.beginPath();
// removed the top and bottom lines of the rectangle
ctx.moveTo(150, 0);
ctx.lineTo(150, 75);
ctx.moveTo(153, 0);
ctx.lineTo(153, 75);
// dont need close path
ctx.stroke();
ctx.beginPath();
ctx.moveTo((150 - (data.diameter / 2)), 80);
ctx.quadraticCurveTo(150, 70, 150 + (data.diameter / 2), 80);
ctx.lineTo(150 + (data.diameter / 2), 83);
ctx.quadraticCurveTo(150, 73, 150 - (data.diameter / 2), 83);
ctx.closePath();
ctx.stroke();
ctx.beginPath();
// removed the two quadratic curves that where drawing over the top of existing ones
ctx.moveTo(150 + (data.diameter / 2), 83);
ctx.lineTo(150 + ((data.diameter / 2) + data.slant), data.height);
ctx.moveTo(150 - ((data.diameter / 2) + data.slant), data.height);
ctx.lineTo(150 - (data.diameter / 2), 83);
// dont need close path
ctx.stroke();
ctx.beginPath();
// removed a curve
ctx.moveTo(150 + ((data.diameter / 2) + data.slant), (data.height - 3));
ctx.quadraticCurveTo(150, (data.height - 15), 150 - ((data.diameter / 2) + data.slant), (data.height - 3));
// dont need close path
ctx.stroke();
ctx.beginPath();
ctx.moveTo(150 + ((data.diameter / 2) + data.slant), data.height);
ctx.quadraticCurveTo(150, (data.height - 10), 150 - ((data.diameter / 2) + data.slant), data.height);
ctx.quadraticCurveTo(150, (data.height + 5), 150 + ((data.diameter / 2) + data.slant), data.height);
ctx.closePath();
ctx.stroke();
So now the render is much better.
Subjective eye
The code fix in my opinion is the best solution that can be achieved with the minimum of effort. As quality is subjective below I present several more methods that may or may not improve the quality, dependent on the eye of the judge.
DOWN SAMPLING
Another why of improving render quality is to down sample.This involves simply rendering the image at a higher resolution and then re rendering the image at a lower resolution. Each pixel is then an average of 2 or more pixels from the original.
There are many down sampling methods, but many are not of any practical use due to the time they take to process the image.
The quickest down sampling can be done via the GPU and native canvas render calls. Simply create an offscreen canvas at a resolution 2 time or 4 time greater than required, then use the transform to scale the image rendering up (so you don't need to change the rendering code). Then you render that image at the required resolution for the result.
Example of downsampling using 2D API and JS
var canvas = document.getElementById("myCanvas"); // get onscreen canvas
// set up the up scales offscreen canvas
var display = {};
display.width = canvas.width;
display.height = canvas.height;
var downSampleSize = 2;
var canvasUp = document.createElement("canvas");
canvasUp.width = display.width * downSampleSize;
canvasUp.height = display.height * downSampleSize;
var ctx = canvasUp.getContext("2D");
ctx.setTransform(downSampleSize,0,0,downSampleSize,0,0);
// call the render function and render to the offscreen canvas
Once you have the image just render it to you onscreen canvas
ctx = canvas.getContext("2d");
ctx.drawImage(canvasUp,0,0,canvas.width,canvas.height);
The following images shows the result of 4* down sampling and varying the line width from 1.2 pixels down to 0.9 pixels (Note the upsampled line width is 4 * that. 4.8, 4.4, 4, & 3.6)
Next image 4* down sample using Lanczos resampling a reasonably quick resample (better suited to pictures)
Down sampling is quick and requires very little modification to the original code to work. The resulting image will improve the look of fine detail and create a slightly better antialiased line.
Down sampling also allows for much finer control of the (apparent) line width. rendering at display resolution gives poor results under 1/4 pixel changes in line width. Using downsampling you double and quadruple that 1/8th and 1/16th fine detail (keep in mind there are other types of aliasing effect that come into play when rendering at sub pixels resolutions)
Dynamic Range
Dynamic range in digital media refers to the range of values that the media can handle. For the canvas that range is 256 (8bits) per color channel. The human eye has a hard time picking the difference between to concurrent values, say 128 and 129 so this range is almost ubiquitous in the realm of computer graphics. Modern GPU though can render at much higher dynamic ranges 16bit, 24bit, 32bit per channel and even double precision floats 64bit.
The adequate 8bit range is good for 95% of cases but suffers when the image being rendered is forced into a lower dynamic range. This happens when you render a line on top of a colour that is close to the line colour. In the questio the image is rendered on not a very bright background (example #888), the result is that the anti aliasing only has a range of 7 bits halving the dynamic range. The problem is compounded by the fact that if the image is rendered onto a transparent background where the anti aliasing is achieved by varying the alpha channel, resulting in the introduction of a second level of artifacts.
When you keep dynamic range in mind you can design your imagery to get the best result (within the design constraints). When rendering and the background is known, don't render onto a transparent canvas letting the hardware composite the final screen output, render the background onto the canvas, then render the design. Try to keep the dynamic range as large as possible, the greater the difference in colour the better the antialiasing algorithm can deal with the intermediate colour.
Below is an example of rendering to various background intensities, they are rendered using 2* down sampling on pre rendered background. BG denotes the background intensity .
Please note that this image is to wide too fit the page and is down sampled by the browser thus adding extra artifacts.
TRUE TYPE like
While here there is another method. If you consider the screen made up of pixels, each pixel has 3 parts red, green, blue and we group them always starting at red.
But it does not matter where a pixels starts, all that matters is that the pixel has the 3 colours rgb, it could be gbr or brg. When you look at the screen like this you effectively get 3 times the horizontal resolution in regard to the edges of the pixels. The pixel size is still the same but offset. This is how microsoft does its special font rendering (true type) Unfortunately Microsoft have many patents on the use of this method so all I can do is show you what it looks like when you render ignoring pixel boundaries.
The effect is most pronounced in the horizontal resolution, and does not improve the vertical much (Note this is my own implementation of the canvas rendering and it's still being refined) This method also does not work for transparent images
What is a slanted line on a pixel matrix is what you should understand. If you need to draw a slanted line of a single pixel width, there is no way you can prevent it from having jagged edges on it since slanting is achieved via a progressing vertical pattern of horizontal lines.
The solution is to have some blur effect around the lines and make the line joining smoother.
You need to use shadowColor, shadowBlur, lineCap and lineJoin properties of the canvas context to achieve this.
Put the following setup and try drawing you lines.
for (i = 0; i < c.length; i++) {
var canvas = c[i];
var ctx = canvas.getContext("2d");
ctx.shadowColor = "rgba(0,0,0,1)";
ctx.shadowBlur = 2;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.lineWidth = 1;
ctx.strokeStyle = 'black';
ctx.clearRect(0,0, canvas.width, canvas.height);
}
Here is the result
Try playing with the shadowColor opacity and the blur size together with the line width and color. You can get pretty amazing results.
On a side note, your project sounds more SVG to me than Canvas. Probably you should think of moving to SVG to get better drawing support and performance.
Update
Here is a fine adjustment
ctx.shadowColor = "rgba(128,128,128,.2)";
ctx.shadowBlur = 1;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.lineWidth = 1;
ctx.strokeStyle = 'gray';
Sorry I'm late to the party, but all of the answers here are overcomplicating things.
What you are actually seeing is the absence of gamma correction. Look at the Antialias 1&2 examples here: http://bourt.com/2014/ (you'll need to calibrate the gamma value for your monitor first), and this short explanation: https://medium.com/#alexbourt/use-gamma-everywhere-da027d9dc82f
The vectors are drawn as if in a linear color space, while the pixels exist in a gamma-corrected space. It's that simple. Unfortunately, Canvas has no gamma support, so you're kind of stuck.
There is a way to fix this, but you have to draw your stuff, then access the pixels directly and correct them for gamma yourself, like I did in those examples. Naturally, this is most easily done with simple graphics. For anything more complicated you need your own rendering pipeline which takes gamma into account.
(Because this argument invariably comes up, I'll address it now: it's better to err on the side of gamma than not. If you say "well, I don't know what the user monitor's gamma will be", and leave it at 1.0, the result WILL BE WRONG in almost all cases. But if you take an educated guess, say 1.8, then for a substantial percentage of users you will have guessed something close to what's correct for their monitor.)
One reason for blury lines is drawing in-between pixels. This answer gives a good overview of the canvas coordinate system:
https://stackoverflow.com/a/3657831/4602079
One way of keeping integer coordinates but still getting crisp lines is to translate the context by 0.5 pixel:
context.translate(0.5,0.5);
Take a look at the snippet below. The second canvas is translated by (0.5, 0.5) making the line drawn with integer coordinates look crisp.
That should get your straight lines fixed. Curves, diagonal lines, etc. will be anti-aliased (gray pixels around the strokes). Not much you can do about it. The higher the resolution less visible they are and all lines except for the straight ones look better anti-aliased anyways.
function draw(ctx){
ctx.beginPath();
ctx.moveTo(25, 30);
ctx.lineTo(75, 30);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(25, 50.5);
ctx.lineTo(75, 50.5);
ctx.stroke();
}
draw(document.getElementById("c1").getContext("2d"))
var ctx = document.getElementById("c2").getContext("2d");
ctx.translate(0.5, 0.5);
draw(ctx);
<canvas id="c1" width="100" height="100" style="width: 100px; height: 100px"></canvas>
<canvas id="c2" width="100" height="100" style="width: 100px; height: 100px"></canvas>
Anti-aliasing helps a lot. But when you have angled lines that are close to horizontal, or vertical, or are gently curving, the anti-aliasing is going to be a lot more noticeable. Especially for thin lines with widths of less than a couple of a pixels or so.
As Maciej points out, if you have a line that's around 1px width and it passed directly between two pixels, anti-aliasing will result in a line that's two pixels wide and half-grey.
You may have to just learn to live with it. There is only so much that anti-aliasing can do.

What is the best way to programatically draw the border(s) of a group of squares?

I am building a tool which will ultimately leverage the Google Maps API v3 to build up an area on a map constructed of squares of a fixed edge length (e.g. 10 metres) on a fixed “grid” system (e.g., co-ordinates spaced out every 0.0001 latlong units starting at earth’s 0,0 point).
I have written code where users can click on an area on the map and the code draws an outline and fill of the square where it's found. Users can click on other adjacent locations to that square to build up a larger and larger “blocky” polygon, and can click on individual squares to delete them. I have tested all this myself in both HTML5 canvas/JavaScript as well as the Google Maps API.
Now I want to write code that removes any internal edges/vertices so that it only draws the outermost boundaries of this polygon so that it is actually drawn as one large polygon, rather than a collection of squares. Think of it this way: even though we know countries like Australia, USA etc., are comprised of a number of states, when we draw the border around the country we are not usually interested in the borders of all the states and can delete those lines in between and just draw the outer boundary. This is the same thing I want to accomplish, just using a grid of squares rather than complex polygons.
My current code is here:
https://jsfiddle.net/wyxcvmdf/14/
HTML:
<canvas id="myCanvas" width="500" height="250" style="border:1px solid #000000;"></canvas>
<!--etc.-->
JavaScript:
// don't forget to set load type in jsfiddle to no wrap in <body>
// define the global variable and some helper variables to make the code shorter
var gv = {};
gv.o = function(id) {
return document.getElementById(id)
};
gv.i = 'innerHTML';
// etc.
A couple of explanatory notes about my code:
• The “origin point” for every square is the vertex at the bottom left corner of that square. No particular reason for this.
• The “drawing direction” in terms of how HTML5 canvas draws the outline is counter-clockwise from the origin point. Again, no particular reason for this.
• You can’t “click” to add squares yet as it’s just a proof of concept, so you add squares by entering the x and y co-ordinates in the relevant text entry boxes
The use cases/tests required to prove my code which I have thought of are:
Square added to polygon with 1 duplicate vertex (working)
Square added to polygon with 2 and 3 duplicate vertices in all cases: adjacent edges, non-adjacent edges, non-sequential vertices (currently working for first case only)
Square added to polygon with 4 duplicate vertices in all cases: plugging a hole, plugging part of a hole, joining multiple polygons (currently working for first case only)
Square removed from polygon with 1 duplicate vertex in cases described above (not developed yet, but should effectively be “reverse” of addition code)
Square removed from polygon with 2 and 3 duplicate vertices in cases described above (not developed yet, but should effectively be “reverse” of addition code)
Square removed from polygon with 4 duplicate vertices in cases described above (not developed yet, but should effectively be “reverse” of addition code)
Square addition/removal on outside of polygon with multiple inner borders, i.e., holes (not developed yet, may be tricky)
Square addition/removal on inside of polygon with multiple inner borders, i.e., holes (not developed yet, may be tricky)
Note 1: My use of “squares”, “edge” etc., instead of "polygons", etc., is just for simplicity of explanation.
Note 2: I have performed quite a bit of research on similar problems and possible solutions but haven’t really found anything which will meet my needs. The research I’ve done is on:
Travelling Salesman Problem. However, this is not about optimising a path – it is about making sure a path is “drawable” and hence heading in one direction. Overlapping vertices are totally fine as long as the resulting shape looks like what a user would expect it to.
Convex hull algorithm. Not really applicable as the hull could be convex, concave or even non-contiguous! Also, I think that by simplifying to a grid system I have removed the problem of having many scattered vertices where you need to determine how far they are from a centre point, use trigonometry etc.
Concave hull solutions. This gets closer to solving my problem, however what I have seen is that there are many plug-ins for commercial tools (e.g. ArcGIS) to do this, but no generic code (irrespective of programming language) which covers all of my use cases.
Tile-based games. You would think that any tile-based game which requires drawing boundaries around tiles (e.g. A player’s territory in a real-time strategy game) would have solved this problem, but not from what I can see.
You say "draw" rather than calculate the outside vertices, so ...
You can use clipping plus compositing to "hollow out" your set of squares.
Assume you have determined that these squares are inside your desired boundary (either partially or fully inside):
var aInside=[ {x:60,y:60},{x:80,y:60},{x:40,y:60},{x:60,y:40},{x:60,y:80} ];
An illustration of squares that are inside your desired boundary.
Then, to draw just the boundary of the set of squares, you can:
Stroke (not fill) each of your inside squares: context.rect
Restrict futher drawing to the stroked rects: context.clip
Cause all new drawing to erase existing pixels: context.globalCompositeOperation = 'destination-out'
Fill the entire canvas with a solid color: context.fillRect(0,0,canvas.width,canvas.height).
The trick: Stroking a rectangle actually draws a stroke half-inside & half-outside the rectangle, so step#4 will erase the inside of the set of rectangles but (importantly!) will leave the half outside stroke.
So you end up with this:
Here's example code and a Demo:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var aInside=[ {x:60,y:60},{x:80,y:60},{x:40,y:60},{x:60,y:40},{x:60,y:80} ];
// stroke all inside squares
ctx.save();
ctx.beginPath();
for(var i=0;i<aInside.length;i++){
var s=aInside[i];
ctx.rect(s.x,s.y,20,20);
}
ctx.stroke();
// clip to cause all new drawing to be inside the stroked squares
ctx.clip();
// set compositing to use new drawings to "erase" existing drawings
ctx.globalCompositeOperation='destination-out';
// Fill (===erase!) the entire canvas
// Clipping causes only the clipping area to be erased
// so the inside of the rects set is "hollowed out"
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.restore();
body{ background-color: ivory; }
#canvas{border:1px solid red; }
<canvas id="canvas" width=150 height=150></canvas>
An Algorithmic Note: If you want a set of the surviving vertices rather than a drawing, you can modify the Marching Squares Algorithm to return only the inflection points. Those inflection points are the vertices of your outside boundary.
This method addresses only drawing/appearance - it does not produce any new polygons. But it allow you to use a collection of polygons (any shape, here rectangles) and merge them visually to produce a merged outline. I base this answer on one of my earlier answers, but modified and adopted to fit the scenario here:
Draw all the rectangles as solids
Re-draw them offset around all edges and corners extruded to the thickness you want
Redraw the original rectangles but with global composite mode set to destination-outand centered on top
There are a few steps, but it works pretty fast.
A couple of notes:
If you have an existing background it would be necessary to use an off-screen canvas as a temporary stage. Not shown here, though the steps would be the same except that you would do these steps on the off-screen context and at the end you would copy the content from the off-screen canvas on top of the existing content of your display canvas.
If you have a lot of rectangles it can be optimized by drawing each single rectangle to a separate off-screen canvas without redrawing anything else. Then you just use this off-screen canvas as a source when you do the extrusion process shown below (see link above for example, just replace image with off-screen canvas itself as source).
It can be further optimized by checking if a rectangle is embedded and if so remove it from the collection.
Demo
var ctx = c.getContext("2d"),
rw = 50, rh = 50, // some demo size
rectangles = []; // rectangle collection
function render(ctx) {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.fillStyle = "#a00";
ctx.globalCompositeOperation = "source-over"; // draw using standard mode3
// we will draw the same rects on top of each other eight times
// this will extrude the edge so we can in the next step punch a
// hole in the drawing and leave only the extrusion -
// offset array (x,y) pairs
var i, d = 2, // d = number of pixels to offset
offsets = [-d, -d, 0, -d, d, -d, d, 0, d, d, 0, d, -d, d, -d, 0];
for(i = 0; i < offsets.length; i += 2) {
ctx.setTransform(1,0,0,1, offsets[i], offsets[i+1]);
drawRects()
}
// punch hole in the center
ctx.setTransform(1,0,0,1,0,0); // reset transformatons
ctx.globalCompositeOperation = "destination-out"; // "erase" mode
drawRects(); // draw a final time, wo/ extrusion
function drawRects() {
ctx.beginPath();
rectangles.forEach(function(r) {
ctx.rect(r.x, r.y, r.w, r.h)
}); // loop through collection and draw
ctx.fill()
}
}
// demo stuff --
c.onclick = function(e) {
var r = this.getBoundingClientRect(), // for demo, get mouse position
x = e.clientX - r.left,
y = e.clientY - r.top;
// add rectangle to list
rectangles.push({ // generate a rect. from center
x: x - rw*0.5,
y: y - rh*0.5,
w: rw,
h: rh
});
render(ctx); // the key process
};
canvas {border:1px solid #000}
Click on the canvas below to place rectangles:<br>
<canvas width=600 height=600 id=c></canvas>

Drawing a point to point ellipse to allow painting in GUI

I'm implementing some basic annotation draw features, such as arrows. Now I'm a little bit stuck with ellipse.
The methods to draw an ellipse usually address using it's two diameters and eventually a rotation:
However I want to display the ellipse between the point user clicked and the one he's hovering, therefore I need a function that calculates diameters and rotation based on two points:
How would I do that? Can it be achieved with sufficient performance (as it renders during mouse-hovering)?
the steps you shoul follow:
get the angle of the line (from this post: get angle of a line from horizon)
rotate the canvas or at least the part you currently drawing (live demo here: http://www.html5canvastutorials.com/advanced/html5-canvas-transform-rotate-tutorial)
draw an ellipse in canvas (http://www.scienceprimer.com/draw-oval-html5-canvas)
the resulted ellipse will be transformed as described
It can be done in the same way that it is normally done, just using different math to calculate the shape. Without writing the entire code for you, you can start by having an event trigger when the user clicks the mouse button down. The function will copy the users x and y position based on the screen. Then there is a second function which will handle mouse movement. This function will keep track of the x and y coords of the mouse while it is in motion. The final function will be a mouse up event, when a user lifts their finger from the mouse button (assuming this is when the event should be finished). Using the initial and final position of the x and y coordinates, you can calculate the length of the line the user created. That line is the long diameter of the ellipse. Half this number for the large radius. Then use whatever ratio you are using to calculate the smaller radius from the larger one. Then create an ellipse based on these numbers.
For the math: Suppose your first point is x1,y1 and the end point is x2,y2
I'm also assuming that we have a line going from bottom-left to top-right
Distance between two points = sqrt((x2-x1)^2 + (y2-y1)^2) ---> (we will call this d1)
half of this is the length of the large radius ---> (we will call this r1)
Midpoint formula = ((x1+x2)/2 , (y1+y2)/2) ---> axis of rotation (we will call it (m1, m2))
distance from midpoint to end is just the radius
radius is now the hypotenuse of constructed plane, y2-m2 is height of right triangle.
Find the angles between midpoint and one end of larger radius - sin((y2-m2)/r1).
Angle of smaller radius is this angle + pi/4 radians.
calculate length of smaller radius based on ratio.

Calculate new width when skewing in canvas

I'm using canvas for a project and I have a number of elements that I'm skewing. I'm only skewing on the y value and just want to know what the new width of the image is after skewing (so I can align it with another canvas element). Check out the code below to see what I mean
ctx.save();
//skew the context
ctx.transform(1,0,1.3,0,0,0);
//draw two images with different heights/widths
ctx.drawImage(image,0,0,42,60);
ctx.drawImage(image,0,0,32,25);
The goal would be to know that the 42 by 60 image was now a X by 60 image so I could do some translating before drawing it at 0,0. It's easy enough to measure each image individually, but I have different skew values and heights/widths throughout the project that need to be align. Currently I use this code (works decently for images between 25 and 42 widths):
var skewModifier = imageWidth*(8/6)+(19/3);
var skewAmount = 1.3; //this is dynamic in my app
var width = (skewModifier*skewAmount)+imageWidth;
As images get wider though this formula quickly falls apart (I think it's a sloping formula not a straight value like this one). Any ideas on what canvas does for skews?
You should be able to derive it mathematically. I believe:
Math.atan(skewAmount) is the angle, in radians, that something is skewed with respect to the origin.
So 1.3 would skew the object by 0.915 radians or 52 degrees.
So here's a red unskewed object next to the same object skewed (painted green). So you have a right triangle:
We know the origin angle (0.915 rads) and we know the adjacent side length, which is 60 and 25 for your two images. (red's height).
The hypotenuse is the long side thats being skewed.
And the opposite side is the triangle bottom - how much its been skewed!
Tangent gets us opposite / adjacent if I recall, so for the first one:
tan(0.915) = opposite / 60, solving for the opposite in JavaScript code we have:
opposite = Math.tan(0.915)*60
So the bottom side of the skewed object starts about 77 pixels away from the origin. Lets check our work in the canvas:
http://jsfiddle.net/LBzUt/
Looks good to me!
The triangle in question of course is the canvas origin, that black dot I painted, and the bottom-left of the red rectangle, which is the original position that we're searching for before skewing.
That was a bit of a haphazard explanation. Any questions?
Taking Simon's fiddle example one step further, so you can simply enter the degrees:
Here's the fiddle
http://jsfiddle.net/LBzUt/33/

Categories