as mentioned I am trying to fit a whole circle of rectangle inside a canvas, but as seen, I can only show a fourth of the circle. I made a basic html document with a canvas element. Relative to the size of the canvas element I made a single rectangel and centered it in the middle of the canvas. With that, I tried to make a for loop which should rotate the rectangle while making a full circle. But it didn't work.
body{
background-color: #000000;
}
canvas {
padding: 0;
margin: auto;
display: block;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: white;
border:1px solid
}
<html>
<body>
<link rel="stylesheet" href="canvas.css">
<canvas id="myCanvas" width="900" height="900" ></canvas>
<script>
// Get id from canvas element
var canvas = document.getElementById("myCanvas");
var context = canvas.getContext("2d");
// Change size of rectangle
var recWidth = 40
var recHeight = 40
// Position rectangle in the middle of the canvas
var xPos = (document.getElementById("myCanvas").width/2) - (recWidth/2);
var yPos = (document.getElementById("myCanvas").height/2) - (recHeight/2);
// Convert degree to radian
const degToRad = (degrees) => {
return degrees / 180 * Math.PI;
}
// Number of rectangles
const num = 36;
for (let i = 0; i<num; i++){
const slice = degToRad (360 / num);
const angle = slice * i;
context.fillStyle = 'green';
context.save();
context.rotate (angle);
context.beginPath();
context.rect(xPos,yPos,recWidth,recHeight);
context.fill();
context.restore();
}
</script>
</body>
</html>
I hope I'm not violating SO protocols by creating a separate answer,
but I have done so to avoid some clutter with the new code.
Here is a cleaned up version of your code.
Explanation:
Move the center of rotation to the center of the canvas:
context.translate(width/2, height/2)
Then specify the radius for drawing as a quarter of canvas dimension.
rad = xPos/2
Do the rotation one slice at a time, leaving it in place (no save/restore).
(incorrect statement removed)
<script>
// Get id from canvas element
var canvas = document.getElementById("myCanvas");
var context = canvas.getContext("2d");
context.font = "14pt Arial";
context.fillStyle = "green";
// Change size of rectangle
var recWidth = 30
var recHeight = 30
// Position rectangle in the middle of the canvas
var canvW = document.getElementById("myCanvas").width;
var canvH = document.getElementById("myCanvas").height;
var xPos = canvW/2;
var yPos = canvH/2;
var rad = xPos/2; // radius = 1/4 of canvas dimensions
context.translate(xPos,yPos); // rotate around the center
// Convert degree to radian
const degToRad = (degrees) => {
return degrees / 180 * Math.PI;
}
// Number of rectangles
const num = 36;
const slice = degToRad (360 / num);
const angle = slice;
// animates the drawing to help see what is going on
// by introducing 1/2 second delay between individual draws
function draw1Rect(i)
{
i++;
if (i < num) setTimeout("draw1Rect("+i+")", 500);
context.rotate (angle);
context.fillRect(rad,rad,recWidth,recHeight);
context.fillText(i, rad-30, rad-30);
}
draw1Rect(0);
</script>
The problem is that most of your squares are being drawn outside the boundaries of the canvas.
It appears the canvas rotates around (0, 0) rather than around the center.
Outside the loop:
// Use a different center and a smaller radius
var xPos = (document.getElementById("myCanvas").width/4) - (recWidth/4);
var yPos = (document.getElementById("myCanvas").height/4) - (recHeight/4);
// Move the center
context.translate(xPos * 2, yPos * 9/4);
context.font = "14pt Arial";
Inside the loop:
context.fill(); // existing line of code
context.fillText(i, xPos-30, yPos-30); // added
context.restore(); // existing
Also: instead of context.beginPath(), context.rect() and context.fill() you can use context.fillRect().
Related
I'm working on a fun little simulation environment for circles. I cannot find an accurate way to combine two circles and find their center coordinate.
I set up an html canvas, then generate random coords on the plane along with a random sized radius. After every generation, I check for an intersection between every circle and every other circle. When circles intersect I want them to merge - making a circle with the combined surface area. Finding the coordinates of the new center is my issue.
I don't want to simply find the midpoint of the centers because that doesn't factor in the size of the circles. A humongous circle could be swayed by a tiny one, which doesn't make for a realistic simulation.
I've thought up what I think is a bad solution: multiplying the change in distance created by the midpoint formula by the ratio of the two circles radii, getting the angle of the resulting triangle, using trig to get the x and y difference, then adding that to the center of the larger circle and calling it a day.
Really have no clue if that is the right way to do it, so I wanted to ask people smarter than me.
Oh also here's a link to the repo on github:
Circle Simulator
This is my first stackOverflow question so bear with me if I did something completely stupid. Thanks everyone!
var dataForm = document.getElementById('dataForm');
var type = document.getElementById('type');
var dataMinRad = document.getElementById('dataMinRad');
var dataMaxRad = document.getElementById('dataMaxRad');
var phaseInterval = document.getElementById('phaseInterval');
//form on submit
const onDataSubmit = (e) => {
if (e) e.preventDefault();
//updates min and max radius
minRadius = parseInt(dataMinRad.value);
maxRadius = parseInt(dataMaxRad.value);
//clears canvas
c.clearRect(0, 0, canvas.width, canvas.height);
//clears circles
circles = [];
//clears any previous interval
clearInterval(phase);
let generator = eval(type.value), data;
//every one second this code is repeated
phase = setInterval(() => {
//gets the circle data from whatever generator is selected
data = generator();
//adds the new circle and draws it on the canvas if the data is good
if (data) {
circles.push(new Circle(data.x, data.y, data.rad));
circles[circles.length - 1].draw();
}
}, parseInt(phaseInterval.value));
}
dataForm.addEventListener('submit', onDataSubmit);
</script>
<script>
//initializes global elements
var stage = document.getElementById('stage');
var canvas = document.getElementById('myCanvas');
var c = canvas.getContext('2d');
//sets width and height of canvas to that of the stage
canvas.setAttribute('width', stage.clientWidth);
canvas.setAttribute('height', stage.clientHeight);
class Circle {
constructor (x, y, rad) {
this.x = x;
this.y = y;
this.rad = rad;
}
draw() {
c.fillStyle = 'black';
c.beginPath();
c.arc(this.x, this.y, this.rad, 0, 2 * Math.PI, true);
c.stroke();
}
}
//variables
var circles = [];
var maxRadius = 100;
var minRadius = 1;
var phase;
const random = () => {
//random coords and radius
let x, y, rad;
do {
[x, y, rad] = [Math.round(Math.random() * canvas.width), Math.round(Math.random() * canvas.height), Math.ceil(Math.random() * (maxRadius - minRadius)) + minRadius];
} while ((() => {
for (let i in circles) {
if (Math.sqrt(Math.pow(x - circles[i].x, 2) + Math.pow(y - circles[i].y, 2)) < rad + circles[i].rad) {
return true;
}
}
return false;
})()) //end while
return { x: x, y: y, rad: rad};
}
const order = () => {
//gets some random coords and sets the radius to max
let [x, y, rad] = [Math.round(Math.random() * canvas.width), Math.round(Math.random() * canvas.height), maxRadius];
//decreases the radius while the resulting circle still intercects any other circle
while (rad >= minRadius && (() => {
for (let i in circles) {
if (Math.sqrt(Math.pow(x - circles[i].x, 2) + Math.pow(y - circles[i].y, 2)) < rad + circles[i].rad) {
return true;
}
}
return false;
})()) {
rad--;
}
//only sends the radii that are greater than the minimum radius
if (rad >= minRadius) return { x: x, y: y, rad: rad};
}
//the position changes must be weighted somehow
const agar = () => {
//some looping control variables
let i = 0, j = 1, noChange = true;
//loops through the circles array in every circle until the noChange variable is false
while (i < circles.length && noChange) {
while (j < circles.length && noChange) {
//checks if each circle is inside each other circle
if (Math.sqrt(Math.pow(circles[i].x - circles[j].x, 2) + Math.pow(circles[i].y - circles[j].y, 2)) < circles[i].rad + circles[j].rad) {
//copies the two circles
let tempCircles = [circles[i], circles[j]];
//splices the item closest to the end of the array first so that the position of the other doesn't shift after the splice
if (i > j) {
circles.splice(i, 1);
circles.splice(j, 1);
} else {
circles.splice(j, 1);
circles.splice(i, 1);
}
//radius of the two circles' surface area combined
let rad = Math.sqrt(tempCircles[0].rad * tempCircles[0].rad + tempCircles[1].rad * tempCircles[1].rad);
/*
// method 1: the midpoint of the centers //
let x = (tempCircles[0].x + tempCircles[1].x) / 2;
let y = (tempCircles[0].y + tempCircles[1].y) / 2;
*/
// method 2: the radius ratio weighted //
let bigCircle, smallCircle;
if (tempCircles[0].rad > tempCircles[1].rad) {
bigCircle = tempCircles[0];
smallCircle = tempCircles[1];
} else {
bigCircle = tempCircles[1];
smallCircle = tempCircles[0];
}
//get the distance between the two circles
let dist = Math.sqrt(Math.pow(bigCircle.x - smallCircle.x, 2) + Math.pow(bigCircle.y - smallCircle.y, 2));
//gets the ratio of the two circles radius size
let radRatio = smallCircle.rad / bigCircle.rad;
//the adjusted hypot for the ratio
dist = dist * radRatio;
//the angle
let theta = Math.atan2(smallCircle.y - bigCircle.y, smallCircle.x - bigCircle.x); // all hail atan2!
//the new center coords
let x = bigCircle.x + dist * Math.cos(theta);
let y = bigCircle.y + dist * Math.sin(theta);
circles.push(new Circle(x, y, rad));
//change happened so the variable should be false
noChange = false;
/*
-find the middle of the point
-weigh it in the direction of teh biggest circle
radius as the magnitude and [angle of the triangle created when the centers are connected] as the direction for both radii.
find the point on each circle closest to the center of the other circle
find those two points midpoint
find the distance from that point to each of the centers
those two distances are the magnitude of two new vectors with the same angels as before
add those two vectors
is there really not a freaking easier way?
*/
/*
try this:
-get the distance between the centers.
-multiply that by the ratio
-get the angle
-use that angle and that hypot to find the x and y
-add the x and y to the bigger circles centerr
*/
}
j++;
}
i++;
j = i + 1;
}
//if there was no change
if (noChange) {
//random coords and radius size
let x = Math.round(Math.random() * canvas.width),
y = Math.round(Math.random() * canvas.height),
rad = Math.ceil(Math.random() * (maxRadius - minRadius)) + minRadius;
//adds the random circle to the array
circles.push(new Circle(x, y, rad));
}
//clears canvas
c.clearRect(0, 0, canvas.width, canvas.height);
//redraws ALL circles
for (let i in circles) {
circles[i].draw();
}
}
onDataSubmit();
* {
margin: 0;
box-sizing: border-box;
}
#wrapper {
width: 100%;
max-width: 1280px;
margin: auto;
margin-right: 0;
display: flex;
flex-flow: row nowrap;
}
#dataContainer {
height: 100%;
width: 20%;
padding: 5px;
}
#dataContainer>* {
padding: 15px;
}
#dataForm {
max-width: 200px;
display: grid;
}
#dataForm>* {
margin-top: 5px;
width: 100%;
}
.center {
margin: auto;
}
#stage {
margin: 5px;
width: 80%;
height: 97vh;
}
<div id='wrapper'>
<!-- form containter -->
<div id='dataContainer'>
<h3>Data</h3>
<form id='dataForm' method='post'>
<label for='type'>Type:</label>
<select id='type' name='type'>
<option value='random' selected>Random</option>
<option value='order'>Order</option>
<option value='agar'>Agario</option>
</select>
<label for='min'>Min-Radius:</label>
<input id='dataMinRad' name='min' type='number' value='1' min='0'>
<label for='max'>Max-Radius:</label>
<input id='dataMaxRad' name='max' type='number' value='100'>
<label for='interval'>Phase Interval:</label>
<input id='phaseInterval' name='interval' type='number' value='1' min='1'>
<button type='submit' id='dataSubmit' class='center'>Load</submit>
</form>
</div>
<!-- canvas container-->
<div id='stage'>
<canvas id='myCanvas'></canvas>
</div>
</div>
So the question is given two overlapping circles find a new circle that represents the merged circles and has an area equal to the sum of the original circles.
For the center of this new circle, one choice is to find the center of mass of the two original circles. If you have two masses of masses m1, m2 and positions (x1,y1), (x2,y2) then the center of mass of the whole system would be
m1/(m1+m2) (x1,y1) + m2/(m1+m2) (x2,y2)
In this case, the center of mass will be
r1^2/(r1^2+r2^2) (x1,y1) + r2^2/(r1^2+r2^2) (x2,y2)
For circles with radii r1, r2 then the masses will be proportional to pi r1^2 + pi r2^2. Hence, the radius of the circle will be sqrt(r1^2+r2^2).
If you can get the center point coordinates and radius, you can draw this new circle
like this:
And about extract a root √ you can use this:
var xxxx = Math.pow(your target here,2);
Update my answer:
I've been working on a game that's sort of a Worms clone. In it, the player rotates a cannon with the up up and down keys (it's a 2D game) to fire at enemies coming from above. I use the context.rotate() and context.translate() methods when drawing the cannon, then immediately context.restore() back to the default canvas.The cannon is the only thing (for now) that's rotated.
The problem is, I want to accurately show projectiles coming from the top of the cannon. For this, I need to know the top of the cannon's coordinates at all times. Normally, this is something I could easily calculate. However, because the canvas is rotated only before the cannon is drawn, it's not as simple.
Just use simple trigonometry to track the top:
var canonTopX = pivotX + Math.cos(angleInRadians) * canonLength;
var canonTopY = pivotY + Math.sin(angleInRadians) * canonLength;
You can choose to render the canon using transformations of course, or share the math.
ctx.translate(pivotX, pivotY);
ctx.rotate(angleInRadians);
//render canon from (0,0) pointing right (0°)
ctx.setTransform(1,0,0,1,0,0); // instead of save/restore
// calc canon top for projectiles here
var ctx = c.getContext("2d");
var canonLength = 70;
var angleInRadians = 0;
var angleStep = 0.03;
var pivotX = ctx.canvas.width>>1;
var pivotY = ctx.canvas.height>>1;
ctx.fillStyle = "#000";
ctx.strokeStyle = "#c00";
(function loop() {
angleInRadians += angleStep;
render();
requestAnimationFrame(loop);
})();
function render() {
ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height);
ctx.translate(pivotX, pivotY);
ctx.rotate(angleInRadians);
ctx.fillRect(0, -5, canonLength, 10);
ctx.setTransform(1,0,0,1,0,0); // instead of save/restore
var canonTopX = pivotX + Math.cos(angleInRadians) * canonLength;
var canonTopY = pivotY + Math.sin(angleInRadians) * canonLength;
ctx.beginPath();
ctx.arc(canonTopX, canonTopY, 9, 0, 6.3);
ctx.stroke();
}
<canvas id=c width=600 height=180></canvas>
Concerns to be addressed:
looks like the straight line on the left is higher than the one on the right. I don't want that. Why is that happening and how could I fix it?
Are all the lines(recs) the same length. not sure but it looks like the ones that go outwards at 0.7853981633974483 radians are smaller
window.onload = function(){
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
context.translate(200,200);
for(var i = 0; i < 5; i++){
context.save()
context.rotate(Math.PI / 4 * i);
context.fillStyle = "red";
context.fillRect(0,0,70,3 )
context.restore()
}
}
<canvas id="canvas" width="400" height="400"></canvas>
EDIT: Also I want to ask you how you would go about labeling these slices that make up the angle. for example slice 1 gets "1" and so on 1 2 3 4 5. it should be positioned by the vertex(angle)
Firstly, the lengths are all the same. If the appear different it is likely due to an illusion from the slight overlapping at the origin.
Now the reason why things aren't lining up is due to the nature of rotation and rectangles. When you rotate a rect the rotational origin is at the top left corner of the rectangle. So when rotated 180 degrees, the origin of the rectangle will be the bottom right. This can be seen more obviously if you widen your rectangles, and change their colours. For example:
var cols = ['red', 'green', 'blue', 'yellow', 'purple'];
window.onload = function(){
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
context.translate(200,200);
for(var i = 0; i < 5; i++){
context.save();
context.rotate(Math.PI / 4 * i);
context.fillStyle = cols[i];
context.fillRect(0,0,70,30);
context.restore();
}
}
<canvas id="canvas" width="400" height="400"></canvas>
Sow how do you fix this? One way to fix is to translate each rect on the y axis, half of its width after rotation. For example:
var cols = ['red', 'green', 'blue', 'black', 'purple'];
// the line width (technically rect height)
var width = 3;
var length = 70;
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
context.translate(200,200);
for(var i = 0; i < 5; i++){
var rotAmount = Math.PI / 4 * i;
context.save();
context.rotate(rotAmount);
context.translate(0, -(width / 2));
context.fillStyle = cols[i];
context.fillRect(0,0,length,width);
context.translate(length + 20, 0);
context.rotate(-rotAmount);
context.font="18px Verdana";
context.fillText(i+1,-5,5);
context.restore();
}
<canvas id="canvas" width="400" height="400"></canvas>
Also added fillText as per #markE's comment, to get you started with drawing the numbers.
So how does this work? Basically after drawing the line, you move the axis to where you want each number to be (translate), and then rotate the axis in reverse, the same amount that you originally rotated the line (rotate). This will rotate the numbers to their original coordinate system.
I've written a loop in JavaScript that will render rings of concentric hexagons around a central hexagon on the HTML canvas.
I start with the innermost ring, draw the hex at 3 o'clock, then continue around in a circle until all hexes are rendered. Then I move on to the next ring and repeat.
When you draw hexagons this way (instead of tiling them using solely x and y offsets) any hexagon that is not divisible by 60 is not the same distance to the center hex as those that are divisible by 60 (because these hexes comprise the flat edges, not the vertices, of the larger hex).
The problem I'm having is these hexes (those not divisible by 60 degrees) are rendering in a slightly off position. I'm not sure if it is a floating point math problem, the problem with my algorithm, the problem with my rusty trig, or just plain stupidity. I'm betting 3 out of 4. To cut to the chase, look at the line if (alpha % 60 !== 0) in the code below.
As a point of information, I decided to draw the grid this way because I needed an easy way to map the coordinates of each hex into a data structure, with each hex being identified by its ring # and ID# within that ring. If there is a better way to do it I'm all ears, however, I'd still like to know why my rendering is off.
Here is my very amateur code, so bear with me.
<script type="text/javascript">
window.addEventListener('load', eventWindowLoaded, false);
function eventWindowLoaded() {
canvasApp();
}
function canvasApp(){
var xOrigin;
var yOrigin;
var scaleFactor = 30;
var theCanvas = document.getElementById("canvas");
var context;
if (canvas.getContext) {
context = theCanvas.getContext("2d");
window.addEventListener('resize', resizeCanvas, false);
window.addEventListener('orientationchange', resizeCanvas, false);
resizeCanvas();
}
drawScreen();
function resizeCanvas() {
var imgData = context.getImageData(0,0, theCanvas.width, theCanvas.height);
theCanvas.width = window.innerWidth;
theCanvas.height = window.innerHeight;
context.putImageData(imgData,0,0);
xOrigin = theCanvas.width / 2;
yOrigin = theCanvas.height / 2;
}
function drawScreen() {
var rings = 3;
var alpha = 0;
var modifier = 1;
context.clearRect(0, 0, theCanvas.width, theCanvas.height);
drawHex(0,0);
for (var i = 1; i<=rings; i++) {
for (var j = 1; j<=i*6; j++) {
if (alpha % 60 !== 0) {
var h = modifier * scaleFactor / Math.cos(dtr(360 / (6 * i)));
drawHex(h * (Math.cos(dtr(alpha))), h * Math.sin(dtr(alpha)));
}
else {
drawHex(2 * scaleFactor * i * Math.cos(dtr(alpha)), 2 * scaleFactor * i * Math.sin(dtr(alpha)));
}
alpha += 360 / (i*6);
}
modifier+=2;
}
}
function drawHex(xOff, yOff) {
context.fillStyle = '#aaaaaa';
context.strokeStyle = 'black';
context.lineWidth = 2;
context.lineCap = 'square';
context.beginPath();
context.moveTo(xOrigin+xOff-scaleFactor,yOrigin+yOff-Math.tan(dtr(30))*scaleFactor);
context.lineTo(xOrigin+xOff,yOrigin+yOff-scaleFactor/Math.cos(dtr(30)));
context.lineTo(xOrigin+xOff+scaleFactor,yOrigin+yOff-Math.tan(dtr(30))*scaleFactor);
context.lineTo(xOrigin+xOff+scaleFactor,yOrigin+yOff+Math.tan(dtr(30))*scaleFactor);
context.lineTo(xOrigin+xOff,yOrigin+yOff+scaleFactor/Math.cos(dtr(30)));
context.lineTo(xOrigin+xOff-scaleFactor,yOrigin+yOff+Math.tan(dtr(30))*scaleFactor);
context.closePath();
context.stroke();
}
function dtr(ang) {
return ang * Math.PI / 180;
}
function rtd(ang) {
return ang * 180 / Math.PI;
}
}
</script>
Man it took me longer than I'd like to admit to find the pattern for the hexagonal circles. I'm too tired right now to explain since I think I'll need to make some assisting illustrations in order to explain it.
In short, each "circle" of hexagonal shapes is itself hexagonal. The number of hexagonal shapes along one edge is the same as the number of the steps from the center.
var c = document.getElementById("canvas");
var ctx = c.getContext("2d");
c.width = 500;
c.height = 500;
var hexRadius = 20;
var innerCircleRadius = hexRadius/2*Math.sqrt(3);
var TO_RADIANS = Math.PI/180;
function drawHex(x,y) {
var r = hexRadius;
ctx.beginPath();
ctx.moveTo(x,y-r);
for (var i = 0; i<=6; i++) {
ctx.lineTo(x+Math.cos((i*60-90)*TO_RADIANS)*r,y+Math.sin((i*60-90)*TO_RADIANS)*r);
}
ctx.closePath();
ctx.stroke();
}
drawHexCircle(250,250,4);
function drawHexCircle(x,y,circles) {
var rc = innerCircleRadius;
drawHex(250,250); //center
for (var i = 1; i<=circles; i++) {
for (var j = 0; j<6; j++) {
var currentX = x+Math.cos((j*60)*TO_RADIANS)*rc*2*i;
var currentY = y+Math.sin((j*60)*TO_RADIANS)*rc*2*i;
drawHex(currentX,currentY);
for (var k = 1; k<i; k++) {
var newX = currentX + Math.cos((j*60+120)*TO_RADIANS)*rc*2*k;
var newY = currentY + Math.sin((j*60+120)*TO_RADIANS)*rc*2*k;
drawHex(newX,newY);
}
}
}
}
canvas {
border: 1px solid black;
}
<canvas id="canvas"></canvas>
I think you're trying to use radial coordinates for something that isn't a circle.
As you noted correctly, the (centers of) the vertex hexagons are indeed laid out in a circle and you can use basic radial positioning to lay them out. However, the non-vertex ones are not laid out on an arc of that circle, but on a chord of it (the line connecting two vertex hexagons). So your algorithm, which tries to use a constant h (radius) value for these hexagons, will not lay them out correctly.
You can try interpolating the non-vertex hexagons from the vertex hexagons: the position of of the Kth (out of N) non-vertex hexagon H between vertex hexagons VH1 and VH2 is:
Pos(H) = Pos(VH1) + (K / (N + 1)) * (Pos(VH2)-Pos(VH1))
e.g. in a ring with 4 hexagons per edge (i.e. 2 non-vertex hexagons), look at the line of hexagons between the 3 o'clock and the 5 o'clock: the 3 o'clock is at 0% along that line, the one after that is at 1/3 of the way, the next is at 2/3 of the way, and the 5 o'clock is at 100% of the way. Alternatively you can think of each hexagon along that line as "advancing" by a predetermined vector in the direction between the two vertices until you reach the end of the line.
So basically your algorithm could go through the 6 primary vertex hexagons, each time interpolating the hexagons from the current vertex hexagon to the next. Thus you should probably have three nested loops: one for rings, one for angles on a ring (always six steps), and one for interpolating hexagons along a given angle (number of steps according to ring number).
I'm working on concept maps application, which has a set of nodes and links. I have connected the links to nodes using the center of the node as reference. Since I have nodes with different size and shapes, it is not advisable to draw arrow-head for the link by specifying height or width of the shape. My approach is to draw a link, starting from one node, pixel by pixel till the next node is reached(here the nodes are of different color from that of the background), then by accessing the pixel value, I want to be able to decide the point of intersection of link and the node, which is actually the co-ordinate for drawing the arrow-head.
It would be great, if I could get some help with this.
Sample Code:
http://jsfiddle.net/9tUQP/4/
Here the green squares are nodes and the line starting from left square and entering into the right square is the link. I want the arrow-head to be drawn at the point of intersection of link and the right square.
I've created an example that does this. I use Bresenham's Line Algorithm to walk the line of whole canvas pixels and check the alpha at each point; whenever it crosses a 'threshold' point I record that as a candidate. I then use the first and last such points to draw an arrow (with properly-rotated arrowhead).
Here's the example: http://phrogz.net/tmp/canvas_shape_edge_arrows.html
Refresh the example to see a new random test case. It 'fails' if you have another 'shape' already overlapping one of the end points. One way to solve this would be to draw your shapes first to a blank canvas and then copy the result (drawImage) to the final canvas.
For Stack Overflow posterity (in case my site is down) here's the relevant code:
<!DOCTYPE html>
<html><head>
<meta charset="utf-8">
<title>HTML5 Canvas Shape Edge Detection (for Arrow)</title>
<style type="text/css">
body { background:#eee; margin:2em 4em; text-align:center; }
canvas { background:#fff; border:1px solid #666 }
</style>
</head><body>
<canvas width="800" height="600"></canvas>
<script type="text/javascript">
var ctx = document.querySelector('canvas').getContext('2d');
for (var i=0;i<20;++i) randomCircle(ctx,'#999');
var start = randomDiamond(ctx,'#060');
var end = randomDiamond(ctx,'#600');
ctx.lineWidth = 2;
ctx.fillStyle = ctx.strokeStyle = '#099';
arrow(ctx,start,end,10);
function arrow(ctx,p1,p2,size){
ctx.save();
var points = edges(ctx,p1,p2);
if (points.length < 2) return
p1 = points[0], p2=points[points.length-1];
// Rotate the context to point along the path
var dx = p2.x-p1.x, dy=p2.y-p1.y, len=Math.sqrt(dx*dx+dy*dy);
ctx.translate(p2.x,p2.y);
ctx.rotate(Math.atan2(dy,dx));
// line
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(0,0);
ctx.lineTo(-len,0);
ctx.closePath();
ctx.stroke();
// arrowhead
ctx.beginPath();
ctx.moveTo(0,0);
ctx.lineTo(-size,-size);
ctx.lineTo(-size, size);
ctx.closePath();
ctx.fill();
ctx.restore();
}
// Find all transparent/opaque transitions between two points
// Uses http://en.wikipedia.org/wiki/Bresenham's_line_algorithm
function edges(ctx,p1,p2,cutoff){
if (!cutoff) cutoff = 220; // alpha threshold
var dx = Math.abs(p2.x - p1.x), dy = Math.abs(p2.y - p1.y),
sx = p2.x > p1.x ? 1 : -1, sy = p2.y > p1.y ? 1 : -1;
var x0 = Math.min(p1.x,p2.x), y0=Math.min(p1.y,p2.y);
var pixels = ctx.getImageData(x0,y0,dx+1,dy+1).data;
var hits=[], over=null;
for (x=p1.x,y=p1.y,e=dx-dy; x!=p2.x||y!=p2.y;){
var alpha = pixels[((y-y0)*(dx+1)+x-x0)*4 + 3];
if (over!=null && (over ? alpha<cutoff : alpha>=cutoff)){
hits.push({x:x,y:y});
}
var e2 = 2*e;
if (e2 > -dy){ e-=dy; x+=sx }
if (e2 < dx){ e+=dx; y+=sy }
over = alpha>=cutoff;
}
return hits;
}
function randomDiamond(ctx,color){
var x = Math.round(Math.random()*(ctx.canvas.width - 100) + 50),
y = Math.round(Math.random()*(ctx.canvas.height - 100) + 50);
ctx.save();
ctx.fillStyle = color;
ctx.translate(x,y);
ctx.rotate(Math.random() * Math.PI);
var scale = Math.random()*0.8 + 0.4;
ctx.scale(scale,scale);
ctx.lineWidth = 5/scale;
ctx.fillRect(-50,-50,100,100);
ctx.strokeRect(-50,-50,100,100);
ctx.restore();
return {x:x,y:y};
}
function randomCircle(ctx,color){
ctx.save();
ctx.beginPath();
ctx.arc(
Math.round(Math.random()*(ctx.canvas.width - 100) + 50),
Math.round(Math.random()*(ctx.canvas.height - 100) + 50),
Math.random()*20 + 10,
0, Math.PI * 2, false
);
ctx.fillStyle = color;
ctx.fill();
ctx.lineWidth = 2;
ctx.stroke();
ctx.restore();
}
</script>
</body></html>