I'm working on a flow-network visualization with Javascript.
Vertices are represented as circles and edges are represented as arrows.
Here is my Edge class:
function Edge(u, v) {
this.u = u; // start vertex
this.v = v; // end vertex
this.draw = function() {
var x1 = u.x;
var y1 = u.y;
var x2 = v.x;
var y2 = v.y;
context.beginPath();
context.moveTo(x1, y1);
context.lineTo(x2, y2);
context.stroke();
var dx = x1 - x2;
var dy = y1 - y2;
var length = Math.sqrt(dx * dx + dy * dy);
x1 = x1 - Math.round(dx / ((length / (radius))));
y1 = y1 - Math.round(dy / ((length / (radius))));
x2 = x2 + Math.round(dx / ((length / (radius))));
y2 = y2 + Math.round(dy / ((length / (radius))));
// calculate the angle of the edge
var deg = (Math.atan(dy / dx)) * 180.0 / Math.PI;
if (dx < 0) {
deg += 180.0;
}
if (deg < 0) {
deg += 360.0;
}
// calculate the angle for the two triangle points
var deg1 = ((deg + 25 + 90) % 360) * Math.PI * 2 / 360.0;
var deg2 = ((deg + 335 + 90) % 360) * Math.PI * 2 / 360.0;
// calculate the triangle points
var arrowx = [];
var arrowy = [];
arrowx[0] = x2;
arrowy[0] = y2;
arrowx[1] = Math.round(x2 + 12 * Math.sin(deg1));
arrowy[1] = Math.round(y2 - 12 * Math.cos(deg1));
arrowx[2] = Math.round(x2 + 12 * Math.sin(deg2));
arrowy[2] = Math.round(y2 - 12 * Math.cos(deg2));
context.beginPath();
context.moveTo(arrowx[0], arrowy[0]);
context.lineTo(arrowx[1], arrowy[1]);
context.lineTo(arrowx[2], arrowy[2]);
context.closePath();
context.stroke();
context.fillStyle = "black";
context.fill();
};
}
Given the code
var canvas = document.getElementById('canvas'); // canvas element
var context = canvas.getContext("2d");
context.lineWidth = 1;
context.strokeStyle = "black";
var radius = 20; // vertex radius
var u = {
x: 50,
y: 80
};
var v = {
x: 150,
y: 200
};
var e = new Edge(u, v);
e.draw();
The draw() function will draw an edge between two vertices like this:
If we add the code
var k = new Edge(v, u);
k.draw();
We will get:
but I want to draw edges both directions as following:
(sorry for my bad paint skills)
Of course the vertices and the edge directions are not fixed.
A working example (with drawing vertex fucntion) on JSFiddle:
https://jsfiddle.net/Romansko/0fu01oec/18/
Aligning axis to a line.
It can make everything a little easier if you rotate the rendering to align with the line. Once you do that it is then easy to draw above or below the line as that is just in the y direction and along the line is the x direction.
Thus if you have a line
const line = {
p1 : { x : ? , y : ? },
p2 : { x : ? , y : ? },
};
Convert it to a vector and normalise that vector
// as vector from p1 to p2
var nx = line.p2.x - line.p1.x;
var ny = line.p2.y - line.p1.y;
// then get length
const len = Math.sqrt(nx * nx + ny * ny);
// use the length to normalise the vector
nx /= len;
ny /= len;
The normalised vector represents the new x axis we want to render along, and the y axis is at 90 deg to that. We can use setTransform to set both axis and the origin (0,0) point at the start of the line.
ctx.setTransform(
nx, ny, // the x axis
-ny, nx, // the y axis at 90 deg to the x axis
line.p1.x, line.p1.y // the origin (0,0)
)
Now rendering the line and arrow heads is easy as they are axis aligned
ctx.beginPath();
ctx.lineTo(0,0); // start of line
ctx.lineTo(len,0); // end of line
ctx.stroke();
// add the arrow head
ctx.beginPath();
ctx.lineTo(len,0); // tip of arrow
ctx.lineTo(len - 10, 10);
ctx.lineTo(len - 10, -10);
ctx.fill();
To render two lines offset from the center
var offset = 10;
ctx.beginPath();
ctx.lineTo(0,offset); // start of line
ctx.lineTo(len,offset); // end of line
ctx.moveTo(0,-offset); // start of second line
ctx.lineTo(len,-offset); // end of second line
ctx.stroke();
// add the arrow head
ctx.beginPath();
ctx.lineTo(len,offset); // tip of arrow
ctx.lineTo(len - 10, offset+10);
ctx.lineTo(len - 10, offset-10);
ctx.fill();
offset = -10;
// add second arrow head
ctx.beginPath();
ctx.lineTo(0,offset); // tip of arrow
ctx.lineTo(10, offset+10);
ctx.lineTo(10, offset-10);
ctx.fill();
And you can reset the transform with
ctx.setTransform(1,0,0,1,0,0); // restore default transform
I've got this game in this plunker.
When the swords are not rotating, it all works fine (you can check by uncommenting lines 221 and commenting out 222-223). When they are rotating like in the plunker above, the collision doesn't work well.
I guess that's because the "getImageData" remembers the old images, but I gather it's an expensive thing to recalculate over and over again.
Is there a better way to rotate my images and make this work? Or do I have to recalculate their pixel map?
Code of the culprit:
for (var i = 0; i < monsters.length; i++) {
var monster = monsters[i];
if (monster.ready) {
if (imageCompletelyOutsideCanvas(monster, monster.monsterImage)) {
monster.remove = true;
}
//else {
//ctx.drawImage(monster.monsterImage, monster.x, monster.y);
drawRotatedImage(monster.monsterImage, monster.x, monster.y, monster);
monster.rotateCounter += 0.05;
//}
}
}
Geometric solution
To do this via a quicker geometry solution.
The simplest solution is a line segment with circle intersection algorithm.
Line segment.
A line has a start and end described in a variety of ways. In this case we will use the start and end coordinates.
var line = {
x1 : ?,
y1 : ?,
x2 : ?,
y2 : ?,
}
Circle
The circle is described by its location and radius
var circle = {
x : ?,
y : ?,
r : ?,
}
Circle line segment Intersect
The following describes how I test for the circle line segment collision. I don't know if there is a better way (most likely there is) but this has served me well and is reliable with the caveat that line segments must have length and circles must have area. If you can not guarantee this then you must add checks in the code to ensure you don't get divide by zeros.
Thus to test if a line intercepts the circle we first find out how far the closest point on the line (Note a line is infinite in size while a line segment has a length, start and end)
// a quick convertion of vars to make it easier to read.
var x1 = line.x1;
var y1 = line.y1;
var x2 = line.x2;
var y2 = line.y2;
var cx = circle.x;
var cy = circle.y;
var r = circle.r;
The result of the test, will be true if there is a collision.
var result; // the result of the test
Convert the line to a vector.
var vx = x2 - x1; // convert line to vector
var vy = y2 - y1;
var d2 = (vx * vx + vy * vy); // get the length squared
Get the unit distance from the circle of the near point on the line. The unit distance is a number from 0, to 1 (inclusive) and represents the distance along the vector of a point. if the value is less than 0 then the point is before the vector, if greater then 1 the point is past the end.
I know this by memory and forget the concept. Its the dot product of the line vector and the vector from the start of the line segment to the circle center divided by the line vectors length squared.
// dot product of two vectors is v1.x * v2.x + v1.y * v2.y over v1 length squared
u = ((cx - x1) * vx + (cy - y1) * vy) / d2;
Now use the unit position to get the actual coordinate of the point on the line closest to the circle by adding to the line segment start position the line vector times the unit distance.
// get the closest point
var xx = x1 + vx * u;
var yy = y1 + vy * u;
Now we have a point on the line, we calculate the distance from the circle using pythagoras square root of the sum of the two sides squared.
// get the distance from the circle center
var d = Math.hypot(xx - cx, yy - cy);
Now if the line (not line segment) intersects the circle the distance will be equal or less than the circle radius. Otherwise the is no intercept.
if(d > r){ //is the distance greater than the radius
result = false; // no intercept
} else { // else we need some more calculations
To determine if the line segment has intercepted the circle we need to find the two points on the circle's circumference that the line has crossed. We have the radius and the distance the circle is from the line. As the distance from the line is always at right angles we have a right triangle with the hypot being the radius and one side being the distance found.
Work out the missing length of the triangle. UPDATE see improved version of the code from here at bottom of answer under "update" it uses unit lengths rather than normalise the line vector.
// ld for line distance is the square root of the hyp subtract side squared
var ld = Math.sqrt(r * r - d * d);
Now add that distance to the point we found on the line xx, yy to do that normalise the line vector (makes the line vector one unit long) by dividing the line vector by its length, and then to multiply it by the distance found above
var len = Math.sqrt(d2); // get the line vector length
var nx = (vx / len) * ld;
var ny = (vy / len) * ld;
Some people may see that I could have used the Unit length and skipped a few calculations. Yes but I can be bothered rewriting the demo so will leave it as is
Now to get the to intercept points by adding and subtracting the new vector to the point on the line that is closest to the circle
ix1 = xx + nx; // the point furthest alone the line
iy1 = xx + ny;
ix2 = xx - nx; // the point in the other direction
iy2 = xx - ny;
Now that we have these two points we can work out if they are in the line segment but calculating the unit distance they are on the original line vector, using the dot product divide the squared distance.
var u1 = ((ix1 - x1) * vx + (iy1 - y1) * vy) / d2;
var u2 = ((ix2 - x1) * vx + (iy1 - y1) * vy) / d2;
Now some simple tests to see if the unit postion of these points are on the line segment
if(u1 < 0){ // is the forward intercept befor the line segment start
result = false; // no intercept
}else
if(u2 > 1){ // is the rear intercept after the line end
result = false; // no intercept
} else {
// though the line segment may not have intercepted the circle
// circumference if we have got to here it must meet the conditions
// of touching some part of the circle.
result = true;
}
}
Demo
As always here is a demo showing the logic in action. The circle is centered on the mouse. There are a few test lines that will go red if the circle touches them. It will also show the point where the circle circumference does cross the line. The point will be red if in the line segment or green if outside. These points can be use to add effects or what not
I am lazy today so this is straight from my library. Note I will post the improved math when I get a chance.
Update
I have improved the algorithm by using unit length to calculate the circle circumference intersects, eliminating a lot of code. I have added it to the demo as well.
From the Point where the distance from the line is less than the circle radius
// get the unit distance to the intercepts
var ld = Math.sqrt(r * r - d * d) / Math.sqrt(d2);
// get that points unit distance along the line
var u1 = u + ld;
var u2 = u - ld;
if(u1 < 0){ // is the forward intercept befor the line
result = false; // no intercept
}else
if(u2 > 1){ // is the backward intercept past the end of the line
result = false; // no intercept
}else{
result = true;
}
}
var demo = function(){
// the function described in the answer with extra stuff for the demo
// at the bottom you will find the function being used to test circle intercepts.
/** GeomDependancies.js begin **/
// for speeding up calculations.
// usage may vary from descriptions. See function for any special usage notes
var data = {
x:0, // coordinate
y:0,
x1:0, // 2nd coordinate if needed
y1:0,
u:0, // unit length
i:0, // index
d:0, // distance
d2:0, // distance squared
l:0, // length
nx:0, // normal vector
ny:0,
result:false, // boolean result
}
// make sure hypot is suported
if(typeof Math.hypot !== "function"){
Math.hypot = function(x, y){ return Math.sqrt(x * x + y * y);};
}
/** GeomDependancies.js end **/
/** LineSegCircleIntercept.js begin **/
// use data properties
// result // intercept bool for intercept
// x, y // forward intercept point on line **
// x1, y1 // backward intercept point on line
// u // unit distance of intercept mid point
// d2 // line seg length squared
// d // distance of closest point on line from circle
// i // bit 0 on for forward intercept on segment
// // bit 1 on for backward intercept
// ** x = null id intercept points dont exist
var lineSegCircleIntercept = function(ret, x1, y1, x2, y2, cx, cy, r){
var vx, vy, u, u1, u2, d, ld, len, xx, yy;
vx = x2 - x1; // convert line to vector
vy = y2 - y1;
ret.d2 = (vx * vx + vy * vy);
// get the unit distance of the near point on the line
ret.u = u = ((cx - x1) * vx + (cy - y1) * vy) / ret.d2;
xx = x1 + vx * u; // get the closest point
yy = y1 + vy * u;
// get the distance from the circle center
ret.d = d = Math.hypot(xx - cx, yy - cy);
if(d <= r){ // line is inside circle
// get the distance to the two intercept points
ld = Math.sqrt(r * r - d * d) / Math.sqrt(ret.d2);
// get that points unit distance along the line
u1 = u + ld;
if(u1 < 0){ // is the forward intercept befor the line
ret.result = false; // no intercept
return ret;
}
u2 = u - ld;
if(u2 > 1){ // is the backward intercept past the end of the line
ret.result = false; // no intercept
return ret;
}
ret.i = 0;
if(u1 <= 1){
ret.i += 1;
// get the forward point line intercepts the circle
ret.x = x1 + vx * u1;
ret.y = y1 + vy * u1;
}else{
ret.x = x2;
ret.y = y2;
}
if(u2 >= 0){
ret.x1 = x1 + vx * u2;
ret.y1 = y1 + vy * u2;
ret.i += 2;
}else{
ret.x1 = x1;
ret.y1 = y1;
}
// tough the points of intercept may not be on the line seg
// the closest point to the must be on the line segment
ret.result = true;
return ret;
}
ret.x = null; // flag that no intercept found at all;
ret.result = false; // no intercept
return ret;
}
/** LineSegCircleIntercept.js end **/
// mouse and canvas functions for this demo.
/** fullScreenCanvas.js begin **/
var canvas = (function(){
var canvas = document.getElementById("canv");
if(canvas !== null){
document.body.removeChild(canvas);
}
// creates a blank image with 2d context
canvas = document.createElement("canvas");
canvas.id = "canv";
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style.position = "absolute";
canvas.style.top = "0px";
canvas.style.left = "0px";
canvas.style.zIndex = 1000;
canvas.ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
return canvas;
})();
var ctx = canvas.ctx;
/** fullScreenCanvas.js end **/
/** MouseFull.js begin **/
var canvasMouseCallBack = undefined; // if needed
var mouse = (function(){
var mouse = {
x : 0, y : 0, w : 0, alt : false, shift : false, ctrl : false,
interfaceId : 0, buttonLastRaw : 0, buttonRaw : 0,
over : false, // mouse is over the element
bm : [1, 2, 4, 6, 5, 3], // masks for setting and clearing button raw bits;
getInterfaceId : function () { return this.interfaceId++; }, // For UI functions
startMouse:undefined,
};
function mouseMove(e) {
var t = e.type, m = mouse;
m.x = e.offsetX; m.y = e.offsetY;
if (m.x === undefined) { m.x = e.clientX; m.y = e.clientY; }
m.alt = e.altKey;m.shift = e.shiftKey;m.ctrl = e.ctrlKey;
if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1];
} else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2];
} else if (t === "mouseout") { m.buttonRaw = 0; m.over = false;
} else if (t === "mouseover") { m.over = true;
} else if (t === "mousewheel") { m.w = e.wheelDelta;
} else if (t === "DOMMouseScroll") { m.w = -e.detail;}
if (canvasMouseCallBack) { canvasMouseCallBack(m.x, m.y); }
e.preventDefault();
}
function startMouse(element){
if(element === undefined){
element = document;
}
"mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",").forEach(
function(n){element.addEventListener(n, mouseMove);});
element.addEventListener("contextmenu", function (e) {e.preventDefault();}, false);
}
mouse.mouseStart = startMouse;
return mouse;
})();
if(typeof canvas === "undefined"){
mouse.mouseStart(canvas);
}else{
mouse.mouseStart();
}
/** MouseFull.js end **/
// helper function
function drawCircle(ctx,x,y,r,col,col1,lWidth){
if(col1){
ctx.lineWidth = lWidth;
ctx.strokeStyle = col1;
}
if(col){
ctx.fillStyle = col;
}
ctx.beginPath();
ctx.arc( x, y, r, 0, Math.PI*2);
if(col){
ctx.fill();
}
if(col1){
ctx.stroke();
}
}
// helper function
function drawLine(ctx,x1,y1,x2,y2,col,lWidth){
ctx.lineWidth = lWidth;
ctx.strokeStyle = col;
ctx.beginPath();
ctx.moveTo(x1,y1);
ctx.lineTo(x2,y2);
ctx.stroke();
}
var h = canvas.height;
var w = canvas.width;
var unit = Math.ceil(Math.sqrt(Math.hypot(w, h)) / 32);
const U80 = unit * 80;
const U60 = unit * 60;
const U40 = unit * 40;
const U10 = unit * 10;
var lines = [
{x1 : U80, y1 : U80, x2 : w /2, y2 : h - U80},
{x1 : w - U80, y1 : U80, x2 : w /2, y2 : h - U80},
{x1 : w / 2 - U10, y1 : h / 2 - U40, x2 : w /2, y2 : h/2 + U10 * 2},
{x1 : w / 2 + U10, y1 : h / 2 - U40, x2 : w /2, y2 : h/2 + U10 * 2},
];
function update(){
var i, l;
ctx.clearRect(0, 0, w, h);
drawCircle(ctx, mouse.x, mouse.y, U60, undefined, "black", unit * 3);
drawCircle(ctx, mouse.x, mouse.y, U60, undefined, "yellow", unit * 2);
for(i = 0; i < lines.length; i ++){
l = lines[i]
drawLine(ctx, l.x1, l.y1, l.x2, l.y2, "black" , unit * 3)
drawLine(ctx, l.x1, l.y1, l.x2, l.y2, "yellow" , unit * 2)
// test the lineSegment circle
data = lineSegCircleIntercept(data, l.x1, l.y1, l.x2, l.y2, mouse.x, mouse.y, U60);
// if there is a result display the result
if(data.result){
drawLine(ctx, l.x1, l.y1, l.x2, l.y2, "red" , unit * 2)
if((data.i & 1) === 1){
drawCircle(ctx, data.x, data.y, unit * 4, "white", "red", unit );
}else{
drawCircle(ctx, data.x, data.y, unit * 2, "white", "green", unit );
}
if((data.i & 2) === 2){
drawCircle(ctx, data.x1, data.y1, unit * 4, "white", "red", unit );
}else{
drawCircle(ctx, data.x1, data.y1, unit * 2, "white", "green", unit );
}
}
}
requestAnimationFrame(update);
}
update();
}
// resize if needed by just starting again
window.addEventListener("resize",demo);
// start the demo
demo();
... and here's how to find the sword blade lines when the sword is moved & rotated
Start by finding the vertices of the original sword blade and saving them in an array.
var pts=[{x:28,y:42},{x:69,y:3},{x:83,y:1},{x:83,y:19},{x:42,y:57}];
When the sword rotates, each blade vertex point will rotate around the rotation point. In your case the rotation point is the center of the image.
Gray rect is the rectangular border of the image
Blue dot is one sword vertex (at the tip of the blade)
Green dot is at the center of the image (== the rotation point)
Green line is the distance from center-image to vertex
Blue circle is the path the blade tip will follow as it rotates 360 degrees
The green line will change its angle depending on the image's rotation.
You can calculate the position of the blade tip at any angle of rotation like this:
// [cx,cy] = the image centerpoint (== the rotation point)
// [vx,vy] = the coordinate position of the blade tip
// Calculate the distance and the angle between the 2 points
var dx=vx-cx;
var dy=vy-cy;
var distance=Math.sqrt(dx*dx+dy*dy);
var originalAngle=Math.atan2(dy,dx);
// rotationAngle = the angle the image has been rotated expressed in radians
var rotatedX = cx + distance * Math.cos(originalAngle + rotationAngle);
var rotatedY = cy + distance * Math.sin(originalAngle + rotationAngle);
Here's example code and a Demo that tracks blade vertices while being moved and rotated:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
function reOffset(){
var BB=canvas.getBoundingClientRect();
offsetX=BB.left;
offsetY=BB.top;
}
var offsetX,offsetY;
reOffset();
window.onscroll=function(e){ reOffset(); }
window.onresize=function(e){ reOffset(); }
var isDown=false;
var startX,startY;
var sword={
img:null,
rx:0,
ry:0,
angle:0,
pts:[{x:28,y:42},{x:69,y:3},{x:83,y:1},{x:83,y:19},{x:42,y:57}],
// precalculated properties -- for efficiency
radii:[],
angles:[],
halfWidth:0,
halfHeight:0,
//
initImg:function(img){
var PI2=Math.PI*2;
this.img=img;
this.halfWidth=img.width/2;
this.halfHeight=img.height/2;
for(var i=0;i<this.pts.length;i++){
var dx=this.halfWidth-this.pts[i].x;
var dy=this.halfHeight-this.pts[i].y;
this.radii[i]=Math.sqrt(dx*dx+dy*dy);
this.angles[i]=((Math.atan2(dy,dx)+PI2)%PI2)-Math.PI;
}
},
// draw sword with translation & rotation
draw:function(){
var img=this.img;
var rx=this.rx;
var ry=this.ry;
var angle=this.angle;
ctx.translate(rx,ry);
ctx.rotate(angle);
ctx.drawImage(img,-this.halfWidth,-this.halfHeight);
ctx.rotate(-angle);
ctx.translate(-rx,-ry);
},
// recalc this.pts after translation & rotation
calcTrxPts:function(){
var trxPts=[];
for(var i=0;i<this.pts.length;i++){
var r=this.radii[i];
var ptangle=this.angles[i]+this.angle;
trxPts[i]={
x:this.rx+r*Math.cos(ptangle),
y:this.ry+r*Math.sin(ptangle)
};
}
return(trxPts);
},
}
// load image & initialize sword object & draw scene
var img=new Image();
img.onload=function(){
// set initial sword properties
sword.initImg(img);
sword.rx=150;
sword.ry=75;
sword.angle=0; //(Math.PI/8);
// draw scene
drawAll();
// listen for mouse events
$("#canvas").mousedown(function(e){handleMouseDown(e);});
$("#canvas").mousemove(function(e){handleMouseMove(e);});
$("#canvas").mouseup(function(e){handleMouseUpOut(e);});
$("#canvas").mouseout(function(e){handleMouseUpOut(e);});
// listen for mousewheel events
$("#canvas").on('DOMMouseScroll mousewheel',function(e){
e.preventDefault();
e.stopPropagation();
var e=e || window.event; // old IE support
sign=((e.originalEvent.wheelDelta||e.originalEvent.detail*-1)>0)?1:-1;
sword.angle+=Math.PI/45*sign;
drawAll();
});
}
img.src = "data:image/png;base64,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";
/////////////////////
// helper functions
/////////////////////
function drawAll(){
ctx.clearRect(0,0,cw,ch);
sword.draw();
drawHitArea();
}
function drawHitArea(){
// lines
var trxPts=sword.calcTrxPts();
ctx.beginPath();
ctx.moveTo(trxPts[0].x,trxPts[0].y);
for(var i=1;i<trxPts.length;i++){
ctx.lineTo(trxPts[i].x,trxPts[i].y);
}
ctx.closePath();
ctx.strokeStyle='red';
ctx.stroke();
// dots
for(var i=0;i<trxPts.length;i++){
ctx.beginPath();
ctx.arc(trxPts[i].x,trxPts[i].y,3,0,Math.PI*2);
ctx.closePath();
ctx.fillStyle='blue';
ctx.fill();
}
}
function getClosestPointOnLineSegment(line,x,y) {
//
lerp=function(a,b,x){ return(a+x*(b-a)); };
var dx=line.x1-line.x0;
var dy=line.y1-line.y0;
var t=((x-line.x0)*dx+(y-line.y0)*dy)/(dx*dx+dy*dy);
var lineX=lerp(line.x0, line.x1, t);
var lineY=lerp(line.y0, line.y1, t);
return({x:lineX,y:lineY,isOnSegment:(t>=0 && t<=1)});
};
function handleMouseDown(e){
// tell the browser we're handling this event
e.preventDefault();
e.stopPropagation();
startX=parseInt(e.clientX-offsetX);
startY=parseInt(e.clientY-offsetY);
// Put your mousedown stuff here
isDown=true;
}
function handleMouseUpOut(e){
// tell the browser we're handling this event
e.preventDefault();
e.stopPropagation();
// clear the isDragging flag
isDown=false;
}
function handleMouseMove(e){
if(!isDown){return;}
// tell the browser we're handling this event
e.preventDefault();
e.stopPropagation();
// calc distance moved since last drag
mouseX=parseInt(e.clientX-offsetX);
mouseY=parseInt(e.clientY-offsetY);
var dx=mouseX-startX;
var dy=mouseY-startY;
startX=mouseX;
startY=mouseY;
// drag the sword to new position
sword.rx+=dx;
sword.ry+=dy;
drawAll();
}
body{ background-color: ivory; }
#canvas{border:1px solid red; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<h6>Drag sword and<br>Rotate sword using mousewheel inside canvas<br>Red "collision" lines follow swords translation & rotation.</h6>
<h5></h5>
<canvas id="canvas" width=300 height=300></canvas>
So I am a bit confused on how I can make a shape animate to the center of a canvas. I can get the center value:
width = canvas.width = window.innerWidth,
height = canvas.height = window.innerHeight,
centerX = width / 2,
centerY = height / 2;
and a simple decrement or increment depending on whether the initial position is positive or negative can be done as well:
var x = 100;
var y = 100;
function fn (){
ctx.beginPath();
ctx.arc(x, y, 50, 0, 2 * Math.PI, false);
ctx.fillStyle = '#444';
ctx.fill();
ctx.closePath();
x -= 1;
y -= 1;
}
The animation would be done using:
requestAnimationFrame(fn)
Problem with all this is. I need to manually adjust the x and y everytime. How can I better simply make the x and y values random for the shape and make it animate to the center, no matter from what direction and if the initial position is negative or positive. I was thinking of atang2 but honestly im not entirely sure.
You're basically on the right track. Use Math.sqrt for the distance and Math.atan2 to find the direction. Then its just the matter of how fast (velocity) you want the object to move to the target (centre of the canvas).
var tx = centerX - x,
tx = centerY - y,
distance = Math.sqrt(tx * tx + ty * ty),
radius = Math.atan2(ty, tx),
angle = (radius / Math.PI) * 180;
// Ensure we don't divide by zero if distance is 0
if (distance !== 0)
{
velX = (tx / distance) * velocity;
velY = (ty / distance) * velocity;
x += velX;
y += velY;
}
The answer given is flawed as there is no check for divide by zero. This error can easily be overlooked and then crop up in production code making it very hard to find out what has gone wrong.
Should be
var tx = centre.x - x;
var ty = centre.y - y;
var dist = Math.sqrt(tx * tx + ty * ty);
// or
var dist = Math.sqrt(Math.pow(tx, 2) + Math.pow(ty, 2));
if(dist !== 0){ // must have this test or when the coords get to the centre
// you will get a divide by zero
tx /= dist; // Normalise direction vector
ty /= dist;
}
tx *= speed; // set the magnitude to required speed;
ty *= speed; // Note that if at the centre this will be zero
x += tx;
y += ty;
I'm trying to draw a smooth curved arc between two points in canvas, I have set up the points as sutch note these are dynamic and can change.
var p1 = {
x=100, y=100
}
var p2 = {
x=255, y=255
}
The curve would look something like this
Here my started code, I can't get my head around the math/logic of this function:
function curveA2B(a,b){
var mindpoint = {
x: (a.x+b.x)/2,
y: (a.y+b.y)/2,
d: Math.sqrt(Math.pow(b.x-a.x,2) + Math.pow(b.y-a.y,2))
};
context.beginPath();
context.arc(
a.x,
a.y,
mindpoint.d/2,
1.5*Math.PI,
0,
false
);
context.arc(
b.x,
b.y,
mindpoint.d/2,
1*Math.PI,
0.5*Math.PI,
true
);
context.context.stroke();
}
The dynamic examples is here: http://jsfiddle.net/CezarisLT/JDdjp/6/
I created a function that would be easily modifiable to many needs called plot_curve that gives you an idea of the breakdown of your problem.
A quick DEMO: http://jsfiddle.net/LVFat/
function plot_curve(x,y,xx,yy, target,color)
{
var startX=x;
var startY=y;
var endX=xx;
var endY=yy;
var diff_x = xx - x;
var diff_y = yy - y;
var bezierX=x; // x1
var bezierY=yy; // y2
console.log("bx:"+bezierX);
console.log("by:"+bezierY);
var cx,cy, t;
for(t=0.0; t<=1; t+=0.01)
{
cx = Math.round( (1-t)*(1-t)*startX + 2*(1-t) * t * bezierX + t*t*endX);
cy = Math.round( (1-t)*(1-t)*startY + 2*(1-t) * t * bezierY + t*t*endY);
// change this part to whatever you are trying to manipulate to the curve
plot_pixel( Math.round(cx), Math.round(cy), target, color);
}
}
example... (works with a divCanvas function I made.. check out jsfiddle link...)
plot_curve(25,25,5,5, ".divCanvas","blue");
if you just want the coords for the curve between the two points, try this:
function plot_curve(x,y,xx,yy)
{
// returns an array of x,y coordinates to graph a perfect curve between 2 points.
var startX=x;
var startY=y;
var endX=xx;
var endY=yy;
var diff_x = xx - x;
var diff_y = yy - y;
var xy = [];
var xy_count = -1;
var bezierX=x; // x1
var bezierY=yy; // y2
var t;
for(t=0.0; t<=1; t+=0.01)
{
xy_count++;
xy[xy_count] = {};
xy[xy_count].x = Math.round( (1-t)*(1-t)*startX + 2*(1-t) * t * bezierX + t*t*endX);
xy[xy_count].y = Math.round( (1-t)*(1-t)*startY + 2*(1-t) * t * bezierY + t*t*endY);
}
return xy; // returns array of coordinates
}
You can use the mid of the two points as two radius settings for the x and y axis.
The following example is simplified but it shows one approach to create smooth curves inside the boxes as in your example.
The boxes will always scale so that the curves goes through the mid point between the two points (alter the end point for example).
DEMO
/// set up some values
var ctx = demo.getContext('2d'),
p1 = {x:100, y:100}, /// point 1
p2 = {x:355, y:255}, /// point 2
mx = (p2.x - p1.x) * 0.5, /// mid-point between point 1 and 2
my = (p2.y - p1.y) * 0.5,
c1 = {x: p1.x, y: p1.y + my}, /// create center point objects
c2 = {x: p2.x, y: p2.y - my},
steps = 0.05; /// curve resolution
/// mark the points and the boxes which represent the center of those
ctx.fillStyle = '#ff6e6e';
ctx.fillRect(p1.x, p1.y, mx, my);
ctx.fillStyle = '#6e93ff';
ctx.fillRect(p1.x + mx, p1.y + my, mx, my);
Then we render the quarter ellipse for each "box":
/// render the smooth curves using 1/4 ellipses
ctx.beginPath();
for(var isFirst = true, /// first point is moveTo, rest lineTo
angle = 1.5 * Math.PI, /// start angle in radians
goal = 2 * Math.PI, /// goal angle
x, y; angle < goal; angle += steps) {
/// calculate x and y using cos/sin
x = c1.x + mx * Math.cos(angle);
y = c1.y + my * Math.sin(angle);
/// move or draw line
(isFirst) ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
isFirst = false;
}
/// second box
for(var isFirst = true,
angle = Math.PI,
goal = 0.5 * Math.PI,
x, y;angle > goal; angle -= steps) {
x = c2.x + mx * Math.cos(angle);
y = c2.y + my * Math.sin(angle);
(isFirst) ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
isFirst = false;
}
ctx.stroke();
I'll leave it to you to put this into re-usable functions. Hope this helps!
If this doesn't cut it I would recommend you to take a look at my cardinal spline implementation.
Is there a way in javascript to plot x,y coordinates so they fall into a circle rather than a square?
For example if I have the following code:
circleRadius = 100;
context.drawImage(img_elem, dx, dy, dw, dh);
I need to figure out a combination of x,y values that would fall inside a 100 pixel circle.
Thanks!
choose an x at random between -100 and 100
a circle is defined by x^2 + y^2 = r^2, which in your case equals 100^2 = 10000
From this equation you can get that y^2 = 10000 - x^2 , therefore the points with a chosen x and y = +/-sqrt(10000 - x^2) will lye on the circle.
choose an y at random between the two coordinates found at point 3
You're set!
EDIT:
In JS:
var radius = 100;
x = Math.random() * 2 * radius - radius;
ylim = Math.sqrt(radius * radius - x * x);
y = Math.random() * 2 * ylim - ylim;
Another edit: a jsFiddle Example
If you want equidistributed coordinates you better go for
var radius = 100
var center_x = 0
var center_y = 0
// ensure that p(r) ~ r instead of p(r) ~ constant
var r = radius*Math.sqrt(Math.random(1))
var angle = Math.sqrt(2*Math.PI)
// compute desired coordinates
var x = center_x + r*Math.cos(angle);
var y = center_y + r*Math.sin(angle);
If you want more points close to the middle then use
var r = radius*Math.random(1)
instead.
not sure what you mean for javascript but
x = R*cos(theta) and y = R*sin(theta) are the Cartesian points for a circle. R is the radius of course and theta is the angle which goes from 0 to 2*Pi.
I'm posting this as a solution because this question was the only relevant result in google.
My question/problem was how to add cartesian coordinates inside a circle where x and y would not exceed r.
Examples:
plot: (45,75) inside a circle with a radius of 100 (this would normally fall inside the circle, but not the correct position)
plot: (100,100) inside a circle with a radius of 100 (this would normally fall outside the circle
Solution
// The scale of the graph to determine position of plot
// I.E. If the graph visually uses 300px but the values only goto 100
var scale = 100;
// The actual px radius of the circle / width of the graph
var radiusGraph = 300;
// Plot the values on a cartesian plane / graph image
var xCart = xVal * radiusGraph;
var yCart = yVal * radiusGraph;
// Get the absolute values for comparison
var xCartAbs = Math.abs( xCart );
var yCartAbs = Math.abs( yCart );
// Get the radius of the cartesian plot
var radiusCart = Math.sqrt( xCart * xCart + yCart * yCart );
// Compare to decide which value is closer to the limit
// Once we know, calculate the largest possible radius with the graphs limit.
// r^2 = x^2 + y^2
if ( xCartAbs > yCartAbs ) { // Less than 45°
diff = scale / xCartAbs;
radiusMaximum = Math.sqrt( radiusGraph * radiusGraph + Math.pow( yCartAbs * diff, 2) );
} else if ( yCartAbs > xCartAbs ) { // Greater than 45°
diff = scale / yCartAbs;
radiusMaximum = Math.sqrt( radiusGraph * radiusGraph + Math.pow( xCartAbs * diff, 2) );
} else { // 45°
radiusMaximum = Math.sqrt( 2 * ( radiusGraph * radiusGraph ) );
}
// Get the percent of the maximum radius that the cartesian plot is at
var radiusDiff = radiusCart / radiusMaximum;
var radiusAdjusted = radiusGraph * radiusDiff;
// Calculate the angle of the cartesian plot
var theta = Math.atan2( yCart, xCart );
// Get the new x,y plot inside the circle using the adjust radius from above
var xCoord = radiusAdjusted * Math.cos( theta );
var yCoord = radiusAdjusted * Math.sin( theta );
Not sure if this is correct JavaScript code, but something like this:
for (x = -r; x < r; x++) {
for (y = -r; x < r; y++) {
if ((x * x + y * y) < (r * r)) {
// This x/y coordinate is inside the circle.
// Use <= if you want to count points _on_ the circle, too.
}
}
}