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,iVBORw0KGgoAAAANSUhEUgAAAFUAAABVCAYAAAA49ahaAAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAVlpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KTMInWQAAKuVJREFUeAHtfAdgVeX99nPOXcnN3nsnQAiEhEDYEFDAASpIRBwUxY3r70JrbUOr1jqqomLdo64ShgMDgkCYIQkBEpKQANmb7NzMO875nvdCLPrZFhzU0RdOzrlnvOM5v/e336PB/8rXEEgDZKRAs6QSyARUnGFJuy3V2UXp0B5tMFmkM3zm13CbwEJsyumDTdu+XZv24Q59YEOpcZJDgzv6On2s/X0B67P6Q9FTFwpYgmMADz4UpPHRrj/abH34f6ASwbQ0yNzsYC6alTTs0Oa8WBMwJAkY6gz42wgYfwdYAUfepBvgFpcE2XcoMGzETAR7RkKRvHDjg49/frQdc7Snv5Vf4/HpgF4+OWzZh5vz/vjc8oWe06eNh4ObBxxdXaF3NkJVrbDauqDYeqAqNrg4a+Fo9FEMjkGK3Ku1ddc3GKICDNLR9gH8qkE9HdBFiXja1Nd37yMLSKIxEbb42TeRu5ImUcvZLIjYzM2dx6RbexGTXJJVtUdGRxdsXV1w8IhyBIp/vaAOAqqqqvTAvIg3q3dWLvn9mhfg52KxZX1yjaa+egECw/2gKF0Er8fObFX7XzuYkFQLz8uAhptjv9RbkQtLZ7MBcdDzzK+vDAIqRv74DaNebThWueT+9BfVsdPHKyFxPpqwcMJoKuDVGm6dkGULaVLiXgtZskK2dkHq7YVkExRMCJ08YFJ06CxsNnhHXWL41YGalpb2lVB68raxz3VUFdxw/8tPKEkzJlF/6pQlR1cEjbkLmoH9BOwYaZPTXtXzWGhXfdy1cesFrBqoZsESeF6VYFMdBB1rRshdgnZ/PeUkoGmCvJB2bfTTVXtz71q4/AE1fsoswSslKIKHmuATlQBrzwBsZokUSpkPTnVVQEVQBX8loOjv5safAkqbDQ687BxO3bayQ9Dur6OcDuifb0x4Ti9p7r3j5WeQdD4lE3okVWkngCZK+QE4OLugs1eH5qoGXlN5ToBJwBVSrIWynb9VYqtQ0lOXAnpV6GGEp5+bFYcOWX8VoKampmoIqp1C/7g45A2l+9BdC+/5PYaOH8O52yupFDqCZwLtAmBuMrx9PVByoIzHrpAkXlM77UQKmwMpVQtJy3M9fVCa+SJaemG1DqDXJok2lF88qEIopaen24SU//21ka8H+IZdf81DG5XIUaN4ikKclGkHza4qCWBJiaTOoOhI5GTmo7akhL9JmTZBlZzyFs55iw6qmPo2DQWXDMlihartZmUd5AvR2l80qKdL+d9eGfiaq7Pj0gtvflwNHTmWYHaT/kihAkfBJwmuHVAKHSqd0ElaqFYVlQcpsHqo8AsZNTAA1UJ20CcTXwMUErW1rAeWpmZo9SbIDjqncHQaxWv5RZbTAb19XuxzYcGWpXNuelkhBRLQMhKnkNykKZXKvABWbAI5ib/NKrqPN8KTNmpvsw5oliHryE815BYWvoA+3txLYIX8clah9XaTbDV6aC1G777IZp9fJKinA3rr7LDHgtyP3HXhdZsRNGQMAa0mcQqJzvlL09OOppA6QsJzmks0QXuqq3GspBx7D9ahttMZDk5u8KG56uFlhIu/B3RmPTQmDad9P/SRAZDc3SXtwRrILZ3aJtmo/cWBejqgj1wT/FSAr+6+i6/fgtC4aeShHaRQAZ6Y7kLvFMekRPJTFQYS6wD66ktxOOcwPl6bh+SYKPh4G5Gz7Qv4+HnB1ehCKaSD2ayBs8ERNosMY10EHNz1anlmkbSvltW49yq/KFC/Bui1Q1Z4u3vfd+nNf0HgkAmKCjPlu7DbhTJPUMV0F4Cq5J0EWZK6YW2uw/5t+fj4rY9xyXW/wZQpk4XaChuplzoXrAMKTF0dONFQjerSOtTV2NCeU4DSrQXSbtbW6KxBtCrb9QhR+8++nA7on2+dcL+Xa/OTF9/wBAKjxyuq2sWBEkCJkkXt4F5MdzH9BYU6ErB29DeUYs/mPGxLX4/UO29BwnQ6/mz0TnVTOxBKEsGlqAf0TkCLCbaqRnR2uaK3px9dXZ3K+t1b5T9u29Of6O02+RdBqacD+vSySb9X+tpWzL76CgKqkEKzCYWBQIrpTo2HIBJJFoUcQOinZvTWFeGLDzajrKwdNz3+MMJGxfE6pbyZ9r2LTPllhuThRBYsw1bTyWf43BBfmA8NoL+3Xy2uq8FHOXsQ4uy8yS9Ed1S08rMuQrFftapYkB5W/Cbp8WDv/kcuWDwD4YlTCKibLKmkLEGZKocqgKVDxA4YJb9E/mhtr0HBjhwc3L0NC++5FaHxCQRb8AYq+HSggDqoRG1ApcmqdhBksg95iB/kIF/0Vbdh55YvbS/sSNfktUif6Ts9rj5Q09z9s6ZUAahQ7AWgTyyb/Ddfh46bZy2eD79h44iiKwHltLVTqLhDWEpCKAlwBTgyBlobkb1xF0qz1nDK34uQEdEEVNwnWAKr7aHjZID6aDdfRrsJkp4vJDoUkrMb1OZ+FBSW2F75cqPW5OSZn+ThcH1eX714WPOzBXUQ0FfSkoxNTYZXjFLLNdMWzVP9hglvE4WFUOYlmpRCFxWFyjw1d17j1OXU722qwf6NGaivKMUldz8Gv5hhvC5MrFOep17i00ueSyq1u/i8+EK8qD7RL4D2PuSs3WVd/qeV2l4Pp8LpMR4LVn1R1pJC8s486W4RLf68SmoqSKHFtgevCvUor9GtMZqr5i245UIlauxU6qEazl3qm5Jw1wnuJtQmUp1w3xFYSaJgqSrGzvXrMDDQgxlXpcI7Mp7X6coToNLshJnPDJCXdgntgI/pSbGe7gTUC+gyo2rnIeszrz2uzThmq5iTEDL3tW3lx1PZWIa9oZOG7s8K0ZOAwvanOyZGtTT3f+hvqB0776Y5tqjkqURDT0DJKyUKJrvOJAx0onKKDdjMreiqKURRVg7llBnTFl0PZ99galh08YknhJvPQju/h8/0cVPIRbQ0Td3cCKgvKdeqZq/OtL30wQfakhpzWeqEyHlv7yw/lpICbXqm3aKwY8mO/HzKySkP25P3p/jX15nWGnrrx15+82xrwozpJEk3UqmegAq+SVC/spJ4KPEyDfWW4jzsWb8RZflbMWHeQgIaSa2KOqgwBgSq/Zzu7Zz2HTRX+8lDdXSgeNBL5epPlapPzd+Urb70/BPayqrq45Njoi9Nzyo/LF5y5mmACjRFVT+LMkihNy1K8jY1ta2NDHCbes2didZhyePILAM4dSlcxNS1F1KZAMpOPCf3to4aHNi4Ee3kh0lzr4RXSJDd8yTJQnDxuZ42SncCStte7SPf1ZDKfQNpgoZDae1Qdn+4WfrLsy9Lej+PDRMiQ25d/n5B7WCfTrb5z78/C0E12Pn7bxvrX7K/aV1MmO+EpQ+eZ4sYEcL+e3wDUKEykVY45ckhCXQ/LO21KNuXC7NVh/GXXwpXvzASMoETmgH5qgiHoJvafQ+r0/EpId+chE0fjIGGE8q219+TX3r3M5zQeb05wT35tuXvbxwY7NM/ofzn0U+eUgc7v+TK8PDeEw6rx0QpYxfeOdUaOmKUVlVdORJ/giMEkihCuxIuPAOBFoCaYW6rQMmuLPR1tyJhztUwuPlRie8g3ASfMXxQBgnPvWrlAf2kZKhQHZwhBw6FtaHZtvnVdzV/fWsjbAFBj2Tm1j3KuzHYJ3H8bWWwN9927b9+brDzdy5JiGmtw/qEyIHRVy9LsoXGjySgFB7wJnBCKIkiVCHyQJ4Vk1+iLqr2NKIidx9MrbUYddECxuUDCZ6gUFKkhZTZT+cywyZqB21/Rz4vWIHOCbJXEPoqamxbXn5XszJ9S58uMOyWL7Nrnme1SONdq4rtvEX8/Nbyk6XUQUDvuyE5Imd/9YbzYluH33D/Imtg4kQCSlMTLgRHUKoogm8S0EGrSaFt3nkCdcVH0FJbj9hZl8HRI4Y8lOESuuylAapMvaRoOpvtj9KvpPAZa98A9KGR6K5rtGWsel7z9q7S9iO28Ksqiyo3sQHBsMX7Etu/LYOc/d/edK4vDgKadse44O3rDqybkhQz/DfLb7UGJowliQkXnZGAup3WLbtbnuRppabUjJ7yY2g9UkztqA9DzptLQCnIrK1U7AmoSsrsMUNt7Ye1zYwBUz8GKPUZyofe0Qnmmmbb7g/TNRvySnvCo2OvFYCK/rCxMwJUdOonR6lfAXpvkvemT1o2jE/Qj3vwz3OtftGRpFAdO0zqlLxPdV10XwDKqU77XqhG7aV5qM4/hIBho+A9ZCxDHA6kUGFF8X2IOBOpUW2ng4SqlJWbhRQr29wYDdWhtuqobfuWDZpdxSW2yk7vq77Mr1udSqU+/SQ9/0cKFYCK8pOi1LQ0EaRjXoKaqln9WvWbwwIbxj3w2PkENFCrUO+U4H4aoOy9sNOFYCG2wsHcW1+B2sJ8uAaGwpugyg6OBJTSXKatLtx4PTRT21vIfkmpjIZqnA0wuLvD4O2NjvYmW/o/XtVkHMofgMFviQDU3p+zBFSA+q8oVZw/abVRiLJye+FeMK8fq9jbFJUnR4esGh9rvXX5Xy+zBUZHaRTyPFmmRQOxDRYSjqBQkeck9cF8ogbVBw+g16xD3PkXQOPoc9JSEgpBb//JbBKT8AfwN4UYXx21LnoJ/COg6vS2T1e+oVn37ppup6j4q19eV/BpGgmO2xlPedb6VflXoH51wzcORMoMfgRwvwJ0dGRo2qig5j/87oVrETkqUVWVHoZA6MSQItiVUxPLHvmk5KYKJcIjtq5q1OTm8I3LCB53HlMfBaB02Qnz1EYe2kqGycw8yZmsQ0dPf3enXcGX6XC2ejrbNm/aoXnj0RdtgTERi178vCL9FAsSBHTGU/50nL4Jqug1KzMGLBrX+46bBk6Ve3FU8TCsvuyBy/fd9tAHItsAaRzdH9ggH/5OjYo6vlGEICBNeS5NTWh7/fE370R04hB67C10MPOS5MfLPtzEOIVzpJkNk79KBMVUj5KdW+DAXLuIySmk0BACSoFkI6+lso8Bqljko+jmlHOgtST+MaVHpnuvrarJumvXRu2aj7Z1tzn6LM0obLZPeRLNd6JQds5evgbqYGrMXBc89ujq+3/rFxKIY8eOIG9vAT5+at/hTOCTMcB7jISXiqfTTk4RMdLvU+yAusJz9vzZDh///i/zHCJGxRI5q4YeJwJnZN1CMAl9lAo7I6AqQyHCGjK3NeLwtkzqoe0Yt2AhHL38CahIbmA2iZjeJlbTJVJ5nMlXhcefuqiZFEuTqb7shG3jpnWajOxDzR5+8YvfyCjYlPY9pjw791URAxoscmZmpjJx1kRfW23NyriJI9xHzxhnDh3qL42Kj5Iuvna030VTJk3VehqudO2o8Pzd80/tu2v9FnMaO5LJ7g5Wcjb7FHp3Kithm39h7Hjd8Zo1f3x2huvwqUmEg1q4SlcbhNokXHesXkRBv+KhdGmWl2Hfxi1or2/E2Dk0PYOiiDelujA9qSIJ1x1M5J0MJ0t6BvBZq2LqZs0OqD1aZ01f+5p2f1VDmaINn//3L4t3iilPpf57Uejg2E8HVVQo1ZTV9PjHhHQ+9cb2uReND9cFRYRD5+COtuYO1eAerowaNc45OCp2cuYbj0z0dMKWN+lNS/sOwIpBZGQQ0Clho1vrutb/ZrGPf8plCTaDG5mO6kdA6RmSCIY92UHLY0py4WCmkOmsbcD6l9agv7MJU69YQNUp3k6EEvVSu2Tv4nTvIk8l/5T0NOSpOqk2mfm5NnQ0tlnXZHymzTl6PN8/eMi8Vz8rLhhU4wZB+b579vafJY1d56bGDXHev68IA22NJUbop6K5xSwtv2mVFKGtx/jUa5WZC+9QkqdOS3nnmXtf21JaMIfPKILquH2NFRQV2WUt4uKg/iHtnzxY3EvVyZo6PTSptKrnkysnuAfNnR1vc3HXMQdEmJ/kn/Z8HE5X7kUImSjx0EQd9DjWvrMT/l6OuPTGG2H0DeOUJ5sYoEXVJyiUwIuYknA6g8PjKdViYaKuRTW1dto2btmi3Vt0MDc2JDn1d+/trRrsCxv4wcr/B6qo+Xh153zujE7OPqReJ8ZiijFrZBiupLm3dssqua11ihQWP0WZOPPSi245XLDsb/vxovApcvvWQgCRxitpaZAF0ALQKyeEJR6o7F5/YYxj0KJJUbYAbz8NWskzhS3vrOfdQqknUGJPsSICbsf3F2HVw68gln255Ma7CShjSkwSk+hYpguKFignHiPQYPzdzoLpfFZ7GaizadSW+kY1c/ce7e7Cw9vDApMW/e6DvU2nKJSN/rDla6D+s2p7Ggec3Dj9OJyGxk648HiADN434gL4+jpRwLWp/qHOUIyeT0wa5e65J7987WXj0WOxQe5kZuFugufl7GUcOdJFHRPtOxAXNbzhurS36aQElswePiWn9MTqSQEG//+bPcoW5uOu6W80w8Hgy1neyDuEQOJ/oTo5iCnsjqP7j+PZe9/D5GmjMf/Wq+HoG86EMUp0cSNj76pJSHWqWI6U+LLw3dEo6O8loFq1trxK3bTjS7mkqXlNxPDEJfc/vbnnh57yYlyD5ZugCr6KHsVQKfY6F2eCJ6nO/R1467kt8HrMF9HBQagqqoC+c0Dem1Osfrazzen664asGDPM/beq4tChUSw6iou+pJFa2dHQ72mzKLYTLV0De9pzMu67Nv4e1hpxtLztowhHxf/ui2KtYcGBWovQKU3sSqtMENlkTxMVdoLjJqwhD1QdPoJXHv0HpqcE4tLr58HgQ0DNTHIQWXi9vF8o9cwekRxI4RpSLMPKTIOG3N+vVlfUSNtz9klFtY1vTpSTbrri6XTbjwmowO2boIpzcHYzHOaut7n+BPWZNnX69EBJ89wsHDpWi6P5R3DswAkcaQCi5k+XXnr1ZtVobVQ17sMNoXEpfq5uXvT4KEyNqUR+zlYU5xfD1c1mdHN2uLq6pmtKZ1eXm6W7383NQba19kraXibOks+Q4l3sMglNND1F+reFr0Z2RnFuER5/bA3Om6bFnOuvgcGL7rvWE0xycLZnP6q1baRMHWR3KvWcJqpwtngy/76hXS09UoKPt+1Bc6ftmefWlt/3HMr/oy9UjP/7Fr7mrxXx206t3L9wz0KP2x955gnVPciP56gADujRmb2XXrM2FBzJhqPncIyYcB4Tii04vC9bzcvOg0v4eOYuaNR/ZOyW1PoCDBvpC4vGS7WaOqS28gbmNvgy99OquOn0snWgFRMT4rB07kS4entwmRLfoQiGCv3SSY/i4lo8/eynSBot4boHLocxhEKpg2ET8lER41O6yGt7SZ2Owlvfzo4TWLMnZCoQFdl7bZ9mbNdU1Hf/7flPSm8Vo0wjT+f2NWEqzv/Qhb37/4od6NTUCZkbVh/z1ksDYwKZf2np7rVpdLIsG53g6B+E6NEEj3H0/P3FaOuwYfLMGdKwkbHSyytWSM//I1e60LNJeubd5dI1110mzYj3kc6fOoL8OFztbKxSvXzd5Umxgbh81hjsL29AUVUrYvwp8c0ymTIxctSgoKAaz6fvxtBAK65fegFcaIgovZTiXFqjdNP0FMD3Ex8rBZtQZfV0kvRyOQ7106qCQ5Yvtm/SNprkTWNjY3+Tnllsn/KrVv34gAo0vw1UcV4mlVg4ETc0ZpW3b/voyynZezY5WLvblaxdWdKu7QeYWhiNmLHJiEoaxey3OmSs2Q4LFewtWRVIGRGC555YxNyDMJTVW9BSUgY/V29p9KSRUkCwl1RXkkWz0oDoMWMQPWIoCvZlo5Vo7so9iP2HjnGpooJV63bCkx6o25fOgWdEEIEk3+3h+x7QwlJJJZ5ee5FlorT2UK+ndPR1g2yyoqmywrZ1/15tXml7boBBc8VNK7d3/dg8VAB2evlXoAoWQKkB9QSQPSNlyNb6dt20SIce73GRIRY/gyRlZ7wqNTQaEB4VjqhRo8gdOvF/8x9HbpMJf37seoQPjcPrT3+IObe8ij259ehq66aQc8Gwqcm0z71R8uUGuHh6o6aiGa4OGhyhUzkxji+ow4SVGw9hTKgH7kydDD+uaxIZyxKz78DpDirxYoWIpKOkZ9BOCvGAHOcPiU6T2sJjto937tLkH2sqCfD0SH3o3QM1J8PaxSTrc1f+FaiiBwJYwQq0B8taa4Id5VY3X4/LRwyN0YT6+UouulDl0PatalNlrxQaHggPJ0cUHtyDgzV9eOLxm1CeX4vrH3wbz10/E5OHBeOet7dh/JhQxE2cgZCoKHjwmc7q41BOmODk6IuUcdPg5eePrTuyGJdvwqi4AEwdNQIGshvVRFOTrEGhcq+Q5ch6gsqUHAR7QRoRAbm5lS/pS9t7W3ZrCqtPfDEywDv1gTezK09S6LkFVAD370AV10WxC66nZg4cWbu9pfLj7QX6NkVy6bcqLsOjh0nHjxcr5QfqJU965bdn7qNfcwA33nsF6o/X4uDHu3Hh/OnM6fLBcE8jiminT5k2DnpXL/hFBCDMxxPhbmGIiohGWXMLXnh5JS5adjnuuGMasjKzUN5gQ2xIMAxMzFX6SakifGzgtLfQlRcaAjk+mhTapuZ99Ln13cy92sZO26Ghvl6X3fdWDhV7kbx27gEVgJ0JqOI+KZ0RxKpeHKrrwfs7Cxo+O1pV1WAwGiNJtZ75h/erTs5u0qjZk9HcUY5J8SPgrtczTpRLnbcVw4ZEU5YYUHV8E8ZPnwknrxCYaVCgpgd6hoO35xzEaw+9jgfeWYbEcYnYsqUUWXlH4OGoRTyVfCcNhZGGXRWRUAudJOGRkBOG0XpqVfd/uF5998sd2j7JuXBclNuVd7+SXZ2WkqJdlZFxTqe8AGmwnCmo4n7BCoRKItEcbWnpwe5dxQ2rXQ1K1BA/79iO3l4ldtwIKb+0AUGyHgnnjcWURXMRaQxC54AZOYcKEBrkgFFTZ0FPHmljendXSx82Z+VyscJx3PX6nXChSvXCo+8h+5nNaKofoLbgh+SoMBi1NEUFlTI2rw6PgiZxJCwn6pSNr/0d7362V7Y4eH16aaL3/EVP7K5OE32srPyvASqAOhtQxf0qAVXThBBLgaaykvle1R17Q3xdFuicVPeK0kqlrfmEVFBQRrWLXiadDsWHqrBlZybayvIw/aI5iIkcCsvRenR3dGLLnjx66cpw7fL/g2d4OD7/aCNKN27FA0/dgmkLZ+DzrHyIZbRDfDy42oa8dEIc5BGx6K48qmT87U151bocyebg/eoHN4xYPHRZht30/E8xeTGIH7tov0sDaZT1aSkAAcbi2bHW6s5Oc3WlBb6uHbj4kjH2JTJrnnwMpbTLfNnAyDnAtOuvRPywBPoWuynNrdh6IAtaqxUL7rsTeh/eRa+SkWEQht0xNHk45JAIJO84hHe/2IhkHycMWXABL0Si8cB+Zd3Lr8vp+9rg5Rv07NptJfdIO46eE0uJQzmj8p1AFTWTDdjLtqL6a1wcNVG+tODLu7Xy3qyjWHbPIryY8TJqyiuhOPjAiQsL/akaaXsc0NnYhe37s2kZNeDS+Quh93CjRKdbmi6+5JRobE+XcaSgGOaiOmTmliAxJBJuzLdXmJtPKrauX/Wydm2ZET4uxuUE9EnRCfZFsKX/6pQ/icbJv2c7/QefFc8pEYGGuYpiWZni76Yf6u2C9n6r9MbWMibLtmF4Ao2DCRPgHRQDFyrlaoMZ9Q0d+GTrdhiY1njp0sVwcKE3SWZyhAtNVNTCoJNReLgee3blouJoIY42WHBpfBiCXQ3q8bw820frdmg/rTEg1E2+/fPsqudEZ9IED808N5aSaO9MylmDmsZBZBLQ0VGIjjO6r18cGuTppZdsWldXOcTNBeGBzlwtVwNrzQGMou6pZa5n14FjKK1vxWsffgR/FwVXLb8JVn93bPlsL/oZX/IJoh6qbQfNYHRysUIGzdNpF8Wjt88KV61W1dEl8FFRiya72VocoB1Y+kl27XunBmfvy5kM9Fzec9agplALyCQHbNDhrmdmJF2cGD3aMvL+hdqg2DB055egSWackqvkbryWgbguCe3VjdhTfBSvvL4SE5MTcfXDt8Lm7ogD6V+gq7AK+fkFaK0sREi4P3Sc5r6+XGG3pwo+/q7w93VRiyp7pOMmq1TR1rtZ29iauqHwxH4CRLPKXuw69Knjn8xusHNn3KE0Amq/2dFB215YDb8LRsmBidFoJUAHmppR0dqKK8eNhJfGEzVlTVizeQ8+/vxdLLvxZly3Yhn66HSu+CIXkQHhGJoYj5nTufChV4MDO3Oohypw8wvBtcumcynNEXh5BKp1bT0oKj/xmt6r5KKNx9vsibZsX3iavgugzLYWkfUft5w1qKmDVOLoWr0utxqd0gBzGhRouhVs3VWN5KThiBs2BE3kn18cLMbBnM9x59K7MOW6i1FnMuGN5SsRmDAEnb4u+P09j9L3asGIYUORv/cA2kvonGZWdCx9CT6+WlXknXp5OaC5vG0vQzC2lJOxre8jkFjnV2uAfjRkzxZUieEm+6CWuGqnrmG39v1jswSakAlXTcdf5o3HwX152JB9DFsO5yPri09xx9IbkTgzmT5V4IMnX4BZhPao1Lf39yNxQixCffxQ29CPkv1VMPczE4VBP51DKDzpbpQYsBsT4w+T1TpaIEAV7vsAKqo4J+WsQKWDwn7/A3OGJHf19i0QjmJTdonctbMQOi7ZnrJgOp66dDJy9u/C2oyN+MPz/KRGygRs27ENR/bkMqLgS//ALuRuzEJsGD1LMdF4+50tWPv+x4iOHQ1jJ0NYJ2rhYPSA0T0Izswf9TcG4Fh7X4tAI43W3DlBhY18HzahPYtOiiionVI2Hz76iK/qoJ/h52jb7wDN5M8PIIQLEGRHd4QPn4D7+J2m3771Dpoovcs35+Kj919HZNwYuLkyds9Y/p/v+wN11MuQFOIN/fTRmOR6IYJr2+HI+8Wi2tqaVi6+3YrkG+7Fjt3ZGB6JmuLys+jpD3Or4L8Mi589uzhTUAWFiE0Ih/smRw6Z01VfoY4LjpazyktwuKMR/tlH4TA9AUpsKKIZK7p31uVYedvj6PJizMslBNnbj0MT6IPY4aEwMdy66p0NmDdtLOZPTkBETDgzn49AE6RDfUct/vrER7gwJVGNiPGTWncw/6kKgtliMI9AHP/YhWB+57DLGalUFE6a4pMS97K7ZkW9eu+NdzCq4qK69LTJM0eORNOJSui9/BDgzpwnLuqytfUhwNUFoeGhDBHrkDolHuNiQlB5OAfFdTZEhLlRmPmhrOYY1nFt/bHCJtSYWnGkrQ133vQo/FjPI0//lizFHZ+vyZQ2lza8SRAr4ooZkfhuUv/Hfgdfq/8/UaqUQkApnKwIdxy3yNr36gMP3a0JHDfC5upj05QdsCFUcUNoaAD2Hc2He0MownX0xBsc0MfFXcOCaA35hTMqqnIVrMwAnxHbD+/DtqImJE0Ixaxpk9DFbzu99eJW5JAuotm1ux+aw4iAG4N+TfA1MoWirhRDI3w8SxkhiCOg7MtPvnwbpcpCIBWTJFikSkr7yxOCY45UdH8a1m4LvODG2Tb3yECNrr4SqtHMxXGOCEqaAFffAGzK3sq1CU7wcvWAkzP9S9Z+Rla1al+/KmnpEHN39YEDP9rmEeyk7sqrgaOTs3TJ7Cm4hkvJZw53hY+xFfMuno74hHg88sAyydpsYyqBk9TY3lJS29yf6XOyX99FP7UP5lz9+RqoaWk0QWlHE9DBjqs+E6KiPbra3nt0RlDcocI2q0ewUTuSoRCVkU0XXwt6bFUoPtLGRQj0MHm5Yc/+TGTX0LUn69U+piorPZ0wSmbFzvC5nKa106Tk1xyVmXkqlZQeVytOmKXJicOQNHYEqdoLmzduhiPzThOGx+Pp9emKQesqu7saXOePDnxrVXqz9VQfB/t3rnA6q3ZOB1U4n9UlMyNjhvpr7/F1xjJHVbrKWNnyoOsJ8/Bb0xYrU+9coMl48nmM9XOFk78TJCrwnkP86bLrQe62vSgqq0dIgB8G+trVDXkHpdv/9pnk6iNJXu5aWWfTySWN9dLO0hy59ESr4qCTzWNjo7T7vtyL9K0Hcd7CCxAxaji8qU69vy6dKtdQJAZFSFsYFQgP9QmwGQzWvQUNO0Qf06gjZJIVnNVIz+HNg6DaJfvtF4YvzM2q+Effid6LzKoUGxLmNuyJhy9x89AV2fzHnKeJp7XUnp8FZ09f+PhwehuYyWRwhlNkKIYNT4ZS14udWZ8oNVYPuc1k4RJc65rsmtpPjpsG6k3WXoejbdVSRUdPrtFJf52XrH+prLpp/LK5M/09+izKupomKYaLa2MCgjGSaYJvb9gGL71R6uy3IK+8Xaouqxnn32HV/2Zx8uFH89nQScvuJwmsJu3UW797duTFx8sq1l0+K8l5bnyQtb2hRrn7qgnKxMULEKJ31FjKj8Gb4WE/fh+sqaEEtu4++kf10Hcz3dHI2FFwIIKHpCgWk01evekzhk/6ny/rNC1t77RtLa1sXbO1oGqNYlHW+h7UP/d2eeXxrPK2xsKGHvWK8fFzFqQkYveH70hrMrYwtqeHpdsGR70Of9+6Hif6DVJdfr1y25Lz9fFTh6Rs25jpXN6BDKFDrmDixjkkwDNuSpPJafTs3Ze5F+/O+mD2oit8b3ribmtE3BCt+cgheUhEjOzh7S1pTHSUFGbBKLvA28Wd2fY2FBSXYEPmMX5ihFaPp0xNqk/5YvsR+e11O9B1omHlsU7L3aIXwl6vqIC6YgVMNW29jYfQYb0pCbq8BihTozVR/V3dqRemjFZiYiPk1blHkL7+MMImRMA1PBiHStqxeV8FXn1hqbT4/iXKmJRJamxyQnL3kc01qTeuOLh6tT1i+pOjVq0YePbhw4t7qjBialKsoje6agcMJgyNTcY+eqECAn2ZhqNF1PDxKOIapZigEPgHBGCSuycCwuqwZvuX+KIwSHF3V+Wt6yqwpRNPsMqHRL0sQvBZRf5umpiu/JOWBjUw76RlxlTNvC5rX3O3WfZp61etu0rM2k//cAnm/m4Jyqta8Mj9Lwo1CpMYm+po75E/fT/TNjp5KM6ffulfKrI/2XfFFenFrO+c5EexG2dc7La8k6My1YV6O5fFKOLDrPogL6bjjIbOasL+kirmKbnBLywIUdHRKD1+HHuYSqk4OpP/eWD62AhlT2Gl/OBbLbBGhD3LWuyApgkQTxoM3NlBVQiAsFKEoBF7aWNeJz8/qL392TUb2l/6dJ82kddGTKDvROOEpiN0bLdboPUFDn65D9oarl0x6TR3pdxlDbX6e1+1ZBLTMu0vyV6XOP6pFI26PUX7yifNdzV0mYJpdqpOsoGfEDZAwy/W+pFCi8qPMEuP32Rhgr+rXoMQPwM/2NqGF9N3oLi2STnY2i+39Oms7preB7KOtaedGpiceRqg/2awzBs1Fe0saUw/3GPVJmEg+YJ54xTPoTHQ9bRJDp9tRi3XlRblZKOzRSsWPePiq6dJY2dNlioOVoS3HixZU801FYJahVbwb9o5p5c0za3SfFdrzz1jQvWoarRIflYHydjYyyQHGe6kRIWJte9t/BS+PqFwdzZDQ8+UysD9/sZW5f28fvloeX+9UaO5Kruy791TPf8ahf6n0YjUHOrFbeo7f/9ixUfpF0SEOAQHuTsrvl7+ql+AG5w7dknDY+Jhaj+sBFqMynmpVyuucRFQ66zGpk2bt9C1fczHJ5XGSvFPB9T2+o4Vty2aOTxl3Gglu2iT7BkUgOEB3tAwX1SimejaY0JrtwXL/7ITHVobjp2w4ZOCctXDzYuJykDZUdPtxa0DwrU6qJ6d1eCEoZFEwXXzynTr8LjQvZkZh5Iby3cGWyw+sqqTJb3qqAwbNtEWHZ2sgVaRK8pqZQ9NlOTp5A6TY1Hhx4cadxddcYW0IjPzrNr9Ty/7+1yXj3ejzsw8qPBxI9SESfHYkZNDsmmCtv04rLVV0HHKp06Kxd/TJsLaWI30VbkI6HNT9L2OqGs2tV+U4r9TdCCN/JC77zSwvDwwZwXyrqLq4tnXTDqvqlNz+4evv7j/7bf+1lbTocpdNoN2R1G+aU1B6YaH/rRy9Uern+2sbT0E2TvgJ+m0Fuvpki6cFLLr2eUXO2ptTerLb6+XXD0TcPX0GPgYuDiB+UuK2QEGrqXv7Wzg91sGlJIeR/n3GwspsDruJhrPExChL34nQPnc6eUr1lH/WZpx9SdrovcfLExmXNCJ82D37l7kiZvTrsVFRleMjo2f+9e5N38mDIEfqv3T+/Kdj0VnRPn4lZvHXnrjvNGWJlOn7r1Ps9BYVYVr5k9FgJseTlwTr6OyreNULKrtlB/c1oCM3Boh5YX6JMoPOShJOHQGHeInqz/596Q3Xnwb5Wsv8Ids+/TmvvOxHdQ5ib4T2nvaMx9ePEs/PTbCovcyaA/VVWAr7fK+5k4lNsgDkRFhane3QfunD3Zha7ntYbb4+KlWf5RBkeylKwhuXDrUolT7S8Mg0MIhPzjibwA8ePq/uheds4NyeaLHLa1m23PnJ8QY5k8fAdlVT2smB298mI/AQA+MiHBDYVWLdWNe98PUMp881esfBdD/KiI/dOOz4yGilm9ya70mGeoF8fpeH732US8H3RKee5rbFG6D5StqGTzxv/1JBE4H5iuqGxroPdQ8YE52knprClusmd8Aa/CZH0IwfaPqX+BPEYv6lmEJEMX5wf233PK/U6cj8P8AeSmglmH0LMsAAAAASUVORK5CYII=";
/////////////////////
// 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>
Related
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 have searched around but I can't find anything like what I'm trying to do that doesn't use Three.js in some way (I can't use Three.js because my computer is too old to support Webgl). Here's what I've got so far:
HTML:
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="terrain.js"></script>
<title>Terrain</title>
</head>
<body>
<canvas id="canvas" height="400" width="400"></canvas>
</body>
</html>
Javascript:
var canvas, ctx, row1 = [], row2 = [], intensity = 15, width = 20, height = 20, centery = 200, centerx = 200, minus, delta = 1.6, nu = .02;
window.onload = function() {
canvas = document.getElementById('canvas'), ctx = canvas.getContext('2d');
ctx.lineStyle = '#000'
for (var i = 0; i < height; i++) {
row2 = [];
minus = 200
for (var j = 0; j < width; j++) {
row2[j] = {
x: centerx - (minus * (delta * (nu * i))),
y: Math.floor(Math.random() * intensity) + (height * i)
}
minus -= height;
}
ctx.beginPath();
ctx.moveTo(row2[0].x,row2[0].y)
for (var k = 1; k < row2.length; k++) {
ctx.lineTo(row2[k].x,row2[k].y)
if (k == row2.length) {ctx.clostPath()}
}
ctx.stroke();
if (row1[0] && row2[0]) {
for (var l = 0; l < row2.length; l++) {
ctx.beginPath();
ctx.moveTo(row2[l].x,row2[l].y)
ctx.lineTo(row1[l].x,row1[l].y)
ctx.closePath();
ctx.stroke();
}
}
row1 = row2;
}
}
Currently, the result looks like a Christmas tree but I want it to look more like actual 3d wireframe terrain.
3D wire frame basics
3D can be done on any systems that can move pixels. Thought not by dedicated hardware Javascript can do alright if you are after simple 3d.
This answers shows how to create a mesh, rotate and move it, create a camera and move it, and project the whole lot onto the 2D canvas using simple moveTo, and lineTo calls.
This answer is a real rush job so apologies for the typos (if any) and messy code. Will clean it up in the come few days (if time permits). Any questions please do ask in the comments.
Update
I have not done any basic 3D for some time so having a little fun I have added to the answer with more comments in the code and added some extra functionality.
vec3 now has normalise, dot, cross functions.
mat now has lookat function and is ready for much more if needed.
mesh now maintains its own world matrix
Added box, and line that create box and line meshs
Created a second vector type vec3S (S for simple) that is just coordinates no functionality
Demo now shows how to add more objects, position them in the scene, use a lookat transform
Details about the code.
The code below is the basics of 3D. It has a mesh object to create objects out of 3D points (vertices) connected via lines.
Simple transformation for rotating, moving and scaling a model so it can be placed in the scene.
A very very basic camera that can only look forward, move up,down, left,right, in and out. And the focal length can be changed.
Only for lines as there is no depth sorting.
The demo does not clip to the camera front plane, but rather just ignores lines that have any part behind the camera;
You will have to work out the rest from the comments, 3D is a big subject and any one of the features is worth a question / answer all its own.
Oh and coordinates in 3D are origin in center of canvas. Y positive down, x positive right, and z positive into the screen. projection is basic so when you have perspective set to 400 than a object at 400 units out from camera will have a one to one match with pixel size.
var ctx = canvas.getContext("2d");
// some usage of vecs does not need the added functionality
// and will use the basic version
const vec3Basic = { x : 0, y : 0, z: 0};
const vec3Def = {
// Sets the vector scalars
// Has two signatures
// setVal(x,y,z) sets vector to {x,y,z}
// setVal(vec) set this vector to vec
setVal(x,y = x.y,z = x.z + (x = x.x) * 0){
this.x = x;
this.y = y;
this.z = z;
},
// subtract v from this vector
// Has two signatures
// setVal(v) subtract v from this returning a new vec3
// setVal(v,vec) subtract v from this returning result in retVec
sub(v,retVec = vec3()){
retVec.x = this.x - v.x;
retVec.y = this.y - v.y;
retVec.z = this.z - v.z;
return retVec;
},
// Cross product of two vectors this and v.
// Cross product can be thought of as get the vector
// that is perpendicular to the plane described by the two vector we are crossing
// Has two signatures
// cross(vec); // returns a new vec3 as the cross product of this and vec
// cross(vec, retVec); // set retVec as the cross product
cross (v, retVec = vec3()){
retVec.x = this.y * v.z - this.z * v.y;
retVec.y = this.z * v.x - this.x * v.z;
retVec.z = this.x * v.y - this.y * v.x;
return retVec;
},
// Dot product
// Dot product of two vectors if both normalized can be thought of as finding the cos of the angle
// between two vectors. If not normalised the dot product will give you < 0 if v points away from
// the plane that this vector is perpendicular to, if > 0 the v points in the same direction as the
// plane perpendicular to this vector. if 0 then v is at 90 degs to the plane this is perpendicular to
// Using vector dot on its self is the same as getting the length squared
// dot(vec3); // returns a number as a float
dot (v){ return this.x * v.x + this.y * v.y + this.z * this.z },
// normalize normalizes a vector. A normalized vector has length equale to 1 unit
// Has two signitures
// normalise(); normalises this vector returning this
// normalize(retVec); normalises this vector but puts the normalised vector in retVec returning
// returning retVec. Thiis is unchanged.
normalize(retVec = this){
// could have used len = this.dot(this) but for speed all functions will do calcs internaly
const len = Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
// it is assumed that all vector are valid (have length) so no test is made to avoid
// the divide by zero that will happen for invalid vectors.
retVec.x = this.x / len;
retVec.y = this.y / len;
retVec.z = this.z / len;
}
}
// Created as a singleton to close over working constants
const matDef = (()=>{
// to seed up vector math the following closed over vectors are used
// rather than create and dispose of vectors for every operation needing them
// Currently not used
const V1 = vec3();
return {
// The matrix is just 3 pointers one for each axis
// They represent the direction and scale in 3D of each axis
// when you transform a point x,y,z you move x along the x axis,
// then y along y and z along the z axis
xAxis : null,
yAxis : null,
zAxis : null,
// this is a position x,y,z and represents where in 3D space an objects
// center coordinate (0,0,0) will be. It is simply added to a point
// after it has been moved along the 3 axis.
pos : null,
// This function does most of the 3D work in most 3D environments.
// It rotates, scales, translates, and a whole lot more.
// It is a cut down of the full 4 by 4 3D matrix you will find in
// Libraries like three.js
transformVec3(vec,retVec = {}){
retVec.x = vec.x * this.xAxis.x + vec.y * this.yAxis.x + vec.z * this.zAxis.x + this.pos.x;
retVec.y = vec.x * this.xAxis.y + vec.y * this.yAxis.y + vec.z * this.zAxis.y + this.pos.y;
retVec.z = vec.x * this.xAxis.z + vec.y * this.yAxis.z + vec.z * this.zAxis.z + this.pos.z;
return retVec;
},
// resets the matrix
identity(){ // default matrix
this.xAxis.setVal(1,0,0); // x 1 unit long in the x direction
this.yAxis.setVal(0,1,0); // y 1 unit long in the y direction
this.zAxis.setVal(0,0,1); // z 1 unit long in the z direction
this.pos.setVal(0,0,0); // and position at the origin.
},
init(){ // need to call this before using due to the way I create these
// objects.
this.xAxis = vec3(1,0,0);
this.yAxis = vec3(0,1,0);
this.zAxis = vec3(0,0,1);
this.pos = vec3(0,0,0);
return this; // must have this line for the constructor function to return
},
setRotateY(amount){
var x = Math.cos(amount);
var y = Math.sin(amount);
this.xAxis.x = x;
this.xAxis.y = 0;
this.xAxis.z = y;
this.zAxis.x = -y;
this.zAxis.y = 0;
this.zAxis.z = x;
},
// creates a look at transform from the current position
// point is a vec3.
// No check is made to see if look at is at pos which will invalidate this matrix
// Note scale is lost in this operation.
lookAt(point){
// zAxis along vector from pos to point
this.pos.sub(point,this.zAxis).normalize();
// use y as vertical reference
this.yAxis.x = 0;
this.yAxis.y = 1;
this.yAxis.z = 0;
// get x axis perpendicular to the plane described by z and y axis
// need to normalise as z and y axis may not be at 90 deg
this.yAxis.cross(this.zAxis,this.xAxis).normalize();
// Get the y axis that is perpendicular to z and x axis
// Normalise is not really needed but rounding errors can be problematic
// so the normalise just fixes some of the rounding errors.
this.zAxis.cross(this.xAxis,this.yAxis).normalize();
},
}
})();
// Mesh object has buffers for the
// model as verts
// transformed mesh as tVerts
// projected 2D verts as dVerts (d for display)
// An a array of lines. Each line has two indexes that point to the
// vert that define their ends.
// Buffers are all preallocated to stop GC slowing everything down.
const meshDef = {
addVert(vec){
this.verts.push(vec);
// vec3(vec) in next line makes a copy of the vec. This is important
// as using the same vert in the two buffers will result in strange happenings.
this.tVerts.push(vec3S(vec)); // transformed verts pre allocated so GC does not bite
this.dVerts.push({x:0,y:0}); // preallocated memory for displaying 2d projection
// when x and y are zero this means that it is not visible
return this.verts.length - 1;
},
addLine(index1,index2){
this.lines.push(index1,index2);
},
transform(matrix = this.matrix){
for(var i = 0; i < this.verts.length; i++){
matrix.transformVec3(this.verts[i],this.tVerts[i]);
}
},
eachVert(callback){
for(var i = 0; i < this.verts.length; i++){
callback(this.tVerts[i],i);
}
},
eachLine(callback){
for(var i = 0; i < this.lines.length; i+= 2){
var ind1 = this.lines[i];
var v1 = this.dVerts[ind1]; // get the start
if(v1.x !== 0 && v1.y !== 0){ // is valid
var ind2 = this.lines[i+ 1]; // get end of line
var v2 = this.dVerts[ind2];
if(v2.x !== 0 && v2.y !== 0){ // is valid
callback(v1,v2);
}
}
}
},
init(){ // need to call this befor using
this.verts = [];
this.lines = [];
this.dVerts = [];
this.tVerts = [];
this.matrix = mat();
return this; // must have this line for the construtor function to return
}
}
const cameraDef = {
projectMesh(mesh){ // create a 2D mesh
mesh.eachVert((vert,i)=>{
var z = (vert.z + this.position.z);
if(z < 0){ // is behind the camera then ignor it
mesh.dVerts[i].x = mesh.dVerts[i].y = 0;
}else{
var s = this.perspective / z;
mesh.dVerts[i].x = (vert.x + this.position.x) * s;
mesh.dVerts[i].y = (vert.y + this.position.y) * s;
}
})
},
drawMesh(mesh){ // renders the 2D mesh
ctx.beginPath();
mesh.eachLine((v1,v2)=>{
ctx.moveTo(v1.x,v1.y);
ctx.lineTo(v2.x,v2.y);
})
ctx.stroke();
}
}
// vec3S creates a basic (simple) vector
// 3 signatures
//vec3S(); // return vec 1,0,0
//vec3S(vec); // returns copy of vec
//vec3S(x,y,z); // returns {x,y,z}
function vec3S(x = {x:1,y:0,z:0},y = x.y ,z = x.z + (x = x.x) * 0){ // a 3d point
return Object.assign({},vec3Basic,{x, y, z});
}
// vec3S creates a basic (simple) vector
// 3 signatures
//vec3S(); // return vec 1,0,0
//vec3S(vec); // returns copy of vec
//vec3S(x,y,z); // returns {x,y,z}
function vec3(x = {x:1,y:0,z:0},y = x.y ,z = x.z + (x = x.x) * 0){ // a 3d point
return Object.assign({},vec3Def,{x,y,z});
}
function mat(){ // matrix used to rotate scale and move a 3d point
return Object.assign({},matDef).init();
}
function mesh(){ // this is for storing objects as points in 3d and lines conecting points
return Object.assign({},meshDef).init();
}
function camera(perspective,position){ // this is for displaying 3D
return Object.assign({},cameraDef,{perspective,position});
}
// grid is the number of grids x,z and size is the overal size for x
function createLandMesh(gridx,gridz,size,maxHeight){
var m = mesh(); // create a mesh
var hs = size/2 ;
var step = size / gridx;
for(var z = 0; z < gridz; z ++){
for(var x = 0; x < gridx; x ++){
// create a vertex. Y is random
m.addVert(vec3S(x * step - hs, (Math.random() * maxHeight), z * step-hs)); // create a vert
}
}
for(var z = 0; z < gridz-1; z ++){
for(var x = 0; x < gridx-1; x ++){
if(x < gridx -1){ // dont go past end
m.addLine(x + z * gridx,x + 1 + z * gridx); // add line across
}
if(z < gridz - 1){ // dont go past end
m.addLine(x + z * (gridx-1),x + 1 + (z + 1) * (gridx-1));
}
}
}
return m;
}
function createBoxMesh(size){
var s = size / 2;
var m = mesh(); // create a mesh
// add bottom
m.addVert(vec3S(-s,-s,-s));
m.addVert(vec3S( s,-s,-s));
m.addVert(vec3S( s, s,-s));
m.addVert(vec3S(-s, s,-s));
// add top verts
m.addVert(vec3S(-s,-s, s));
m.addVert(vec3S( s,-s, s));
m.addVert(vec3S( s, s, s));
m.addVert(vec3S(-s, s, s));
// add lines
/// bottom lines
m.addLine(0,1);
m.addLine(1,2);
m.addLine(2,3);
m.addLine(3,0);
/// top lines
m.addLine(4,5);
m.addLine(5,6);
m.addLine(6,7);
m.addLine(7,4);
// side lines
m.addLine(0,4);
m.addLine(1,5);
m.addLine(2,6);
m.addLine(3,7);
return m;
}
function createLineMesh(v1 = vec3S(),v2 = vec3S()){
const m = mesh();
m.addVert(v1);
m.addVert(v2);
m.addLine(0,1);
return m;
}
//Create a land mesh grid 20 by 20 and 400 units by 400 units in size
var land = createLandMesh(20,20,400,20); // create a land mesh
var box = createBoxMesh(50);
var box1 = createBoxMesh(25);
var line = createLineMesh(); // line conecting boxes
line.tVerts[0] = box.matrix.pos; // set the line transformed tVect[0] to box matrix.pos
line.tVerts[1] = box1.matrix.pos; // set the line transformed tVect[0] to box1 matrix.pos
var cam = camera(200,vec3(0,0,0)); // create a projection with focal len 200 and at 0,0,0
box.matrix.pos.setVal(0,-100,400);
box1.matrix.pos.setVal(0,-100,400);
land.matrix.pos.setVal(0,100,300); // move down 100, move away 300
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center of canvas
var ch = h / 2;
function update(timer){
// next section just maintains canvas size and resets state and clears display
if (canvas.width !== innerWidth || canvas.height !== innerHeight) {
cw = (w = canvas.width = innerWidth) /2;
ch = (h = canvas.height = innerHeight) /2;
}
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.fillStyle = "black";
ctx.fillRect(0,0,canvas.width,canvas.height);
// end of standard canvas maintenance
// render from center of canvas by setting canvas origin to center
ctx.setTransform(1,0,0,1,canvas.width / 2,canvas.height / 2)
land.matrix.setRotateY(timer/1000); // set matrix to rotation position
land.transform();
// move the blue box
var t = timer/1000;
box1.matrix.pos.setVal(Math.sin(t / 2.1) * 100,Math.sin( t / 3.2) * 100, Math.sin(t /5.3) * 90+300);
// Make the cyan box look at the blue box
box.matrix.lookAt(box1.matrix.pos);
// Transform boxes from local to world space
box1.transform();
box.transform();
// set camera x,y pos to mouse pos;
cam.position.x = mouse.x - cw;
cam.position.y = mouse.y - ch;
// move in and out
if (mouse.buttonRaw === 1) { cam.position.z -= 1 }
if (mouse.buttonRaw === 4) {cam.position.z += 1 }
// Converts mesh transformed verts to 2D screen coordinates
cam.projectMesh(land);
cam.projectMesh(box);
cam.projectMesh(box1);
cam.projectMesh(line);
// Draw each mesh in turn
ctx.strokeStyle = "#0F0";
cam.drawMesh(land);
ctx.strokeStyle = "#0FF";
cam.drawMesh(box);
ctx.strokeStyle = "#00F";
cam.drawMesh(box1);
ctx.strokeStyle = "#F00";
cam.drawMesh(line);
ctx.setTransform(1,0,0,1,cw,ch / 4);
ctx.font = "20px arial";
ctx.textAlign = "center";
ctx.fillStyle = "yellow";
ctx.fillText("Move mouse to move camera. Left right mouse move in out",0,0)
requestAnimationFrame(update);
}
requestAnimationFrame(update);
// A mouse handler from old lib of mine just to give some interaction
// not needed for the 3d
var mouse = (function () {
var m; // alias for mouse
var mouse = {
x : 0, y : 0, // mouse position
buttonRaw : 0,
buttonOnMasks : [0b1, 0b10, 0b100], // mouse button on masks
buttonOffMasks : [0b110, 0b101, 0b011], // mouse button off masks
bounds : null,
event(e) {
m.bounds = m.element.getBoundingClientRect();
m.x = e.pageX - m.bounds.left - scrollX;
m.y = e.pageY - m.bounds.top - scrollY;
if (e.type === "mousedown") { m.buttonRaw |= m.buttonOnMasks[e.which - 1] }
else if (e.type === "mouseup") { m.buttonRaw &= m.buttonOffMasks[e.which - 1] }
e.preventDefault();
},
start(element) {
m.element = element === undefined ? document : element;
"mousemove,mousedown,mouseup".split(",").forEach(name => document.addEventListener(name, mouse.event) );
document.addEventListener("contextmenu", (e) => { e.preventDefault() }, false);
return mouse;
},
}
m = mouse;
return mouse;
})().start(canvas);
canvas { position:absolute; top : 0px; left : 0px;}
<canvas id="canvas"></canvas>
I'm trying to rotate an image inside a canvas.
Here's my Fiddle: https://jsfiddle.net/kevinludwig11/s6rgpjm9/
I try it with save and restore, but the path is also rotating.
The falcon should fly with his face towards and change the angle in the corners.
Can anybody help me?
Edit: One solution i've found: save the image 360 times with every rotation and load every image in the right position. But i think thats not the smartest solution.
Canvas 2D image lookat transform.
No need to create 360 images to rotate a single image. Also you had a few incorrect ways of doing things.
Code problems
Only load the image once. You were loading it each time it was rendered.
Use requestAnimationFrame on its own. Putting it inside a timer makes its use completely redundant.
If you find yourself typing in long lists of numbers, and especially if you repeat these numbers in other sections of code you should use a single store to hold everything. Eg your paths were all hand coded. Move them into an array then iterate the array for the various things you need to do with the paths. One of the top ten programing rules. "Don't repeat/duplicate anything."
The lookat transform
To do the bird you will need to get the direction it is heading towards so I added a second point on the curves that is ahead of the bird. With these two points (birds pos and lookat pos) I then create a transformation using the lookat direction as the xAxis of the transformation. See function drawImageLookat(image,pos,lookat) I found that the image is not along the X axis so I rotate the bird 90deg after finding the lookat transformation.
Lookat function
// function assumes front (forward) of image is along the x axis to the right
function drawImageLookat(image, point, lookat ) {
var xAx,xAy; // vector for x Axis of image
var x,y;
x = lookat.x - point.x;
y = lookat.y - point.y;
var dist = Math.max(0.01,Math.sqrt(x * x + y * y)); // Math.max to avoid zero which will create NaN
xAx = x / dist; // get x component of x Axis
xAy = y / dist; // get y component of x Axis
// y axis is at 90 deg so dont need y axis vector
ctx.setTransform( // position the image using transform
xAx, xAy, // set direction of x Axis
-xAy, xAx, // set direction oy y axis
point.x, point.y
);
ctx.drawImage(image, -image.width / 2, -image.height / 2);
}
Demo from fiddle.
Your code that I took from the fiddle https://jsfiddle.net/kevinludwig11/s6rgpjm9/ and modified to run as your question implies.
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
// only load image once
var birdImage = new Image();
birdImage.src = 'http://www.happy-innovation.de/images/Falke_Flug.png';
birdImage.onload = function(){animate()}; // start animation when image has loaded
// set starting values
var speed = 0.25
var percent = speed;
var direction = speed;
var length = 300;
function animate() {
ctx.setTransform(1,0,0,1,0,0); // restore default transform incase its not
ctx.clearRect(0, 0, canvas.width, canvas.height);
percent += direction;
// need to keep the position away from the ends as there is no lookat beyond the path.
if(percent >= length - speed){
percent = length- speed;
direction = -speed;
}else if(percent <= speed){
percent = speed;
direction = speed;
}
draw(percent,direction);
requestAnimationFrame(animate);
}
function P(x,y){return {x,y}}; // quick way to create a point
var paths = [
{col : 'red', points : [P(100, 200), P(600, 350), P( 700, 400)]},
{col : "green", points : [P(700, 400), P( 900, 500), P( 200, 600), P( 950, 900)]},
{col : "blue", points : [P(950, 900), P(1200, 950), P( 300, 200), P( 150, 1200)]},
{col : "brown", points : [P(150, 1200),P( 120, 1700),P( 1000, 700),P(850, 1500)]},
{col : "Purple",points : [P(850, 1500),P(800, 1900), P( 200, 900), P( 250, 1800)]},
{col : "yellow", points : [P(250, 1800),P(250, 1950), P( 600, 1500),P(950, 1800)]},
]
// draw the current frame based on sliderValue
function draw(sliderValue,direction) {
var getPos = false; // true if need pos on current curve
var getForwardPos = false; // true if need pos on current curve
var percent,percent1; // get the percentage on curves
var birdPos; // get bird pos
var birdLookAtPos; // get bird look at pos
ctx.lineWidth = 5;
for(var i = 0; i < paths.length; i ++){
var path = paths[i]; // get a path from array
var p = path.points;
ctx.strokeStyle = path.col;
ctx.beginPath();
ctx.moveTo(p[0].x,p[0].y);
if(sliderValue >= i * 50 && sliderValue < (i+1) * 50){
getPos = true;
percent = (sliderValue % 50) / 50;
}
if(sliderValue + direction >= i * 50 && sliderValue + direction < (i+1) * 50){
getForwardPos = true;
percent1 = ((sliderValue + direction) % 50) / 50;
}
if(p.length > 3){
ctx.bezierCurveTo(p[1].x,p[1].y,p[2].x,p[2].y,p[3].x,p[3].y);
if(getPos){
birdPos = getCubicBezierXYatPercent(p[0],p[1],p[2],p[3],percent);
getPos = false;
}
if(getForwardPos){
birdLookAtPos = getCubicBezierXYatPercent(p[0],p[1],p[2],p[3],percent1);
getForwardPos = false;
}
}else{
ctx.quadraticCurveTo(p[1].x,p[1].y,p[2].x,p[2].y);
if(getPos){
birdPos = getQuadraticBezierXYatPercent(p[0],p[1],p[2],percent);
getPos = false;
}
if(getForwardPos){
birdLookAtPos = getQuadraticBezierXYatPercent(p[0],p[1],p[2],percent1);
getForwardPos = false;
}
}
ctx.stroke();
}
drawImageLookingAt(birdImage,birdPos,birdLookAtPos);
}
function drawImageLookingAt(image, point, lookat ) {
if(lookat === undefined){ // if no lookat then exit or it will crash.
return;
}
var xAx,xAy; // vector for x Axis of image
var x,y;
x = lookat.x - point.x;
y = lookat.y - point.y;
var dist = Math.max(0.01,Math.sqrt(x * x + y * y)); // Math.max to avoid zero which will create NaN
xAx = x / dist; // get x component of x Axis
xAy = y / dist; // get y component of x Axis
// y axis is at 90 deg so dont need y axis vector
ctx.setTransform( // position the image using transform
xAx, xAy, // set direction of x Axis
-xAy, xAx, // set direction oy y axis
point.x, point.y
);
// bird is pointing in the wrong direction. Not along x axis
// so rotate the image 90 deg clockwise
ctx.rotate(Math.PI / 2);
ctx.drawImage(image, -image.width / 2, -image.height / 2);
ctx.setTransform(1,0,0,1,0,0); // Restore default Not really needed if you only use setTransform to do transforms
// but in case you use transform, rotate, translate or scale you need to reset the
// transform.
}
// line: percent is 0-1
function getLineXYatPercent(startPt, endPt, percent) {
var dx = endPt.x - startPt.x;
var dy = endPt.y - startPt.y;
var X = startPt.x + dx * percent;
var Y = startPt.y + dy * percent;
return ({
x: X,
y: Y
});
}
// quadratic bezier: percent is 0-1
function getQuadraticBezierXYatPercent(startPt, controlPt, endPt, percent) {
var x = Math.pow(1 - percent, 2) * startPt.x + 2 * (1 - percent) * percent * controlPt.x + Math.pow(percent, 2) * endPt.x;
var y = Math.pow(1 - percent, 2) * startPt.y + 2 * (1 - percent) * percent * controlPt.y + Math.pow(percent, 2) * endPt.y;
return ({
x: x,
y: y
});
}
// cubic bezier percent is 0-1
function getCubicBezierXYatPercent(startPt, controlPt1, controlPt2, endPt, percent) {
var x = CubicN(percent, startPt.x, controlPt1.x, controlPt2.x, endPt.x);
var y = CubicN(percent, startPt.y, controlPt1.y, controlPt2.y, endPt.y);
return ({
x: x,
y: y
});
}
// cubic helper formula at percent distance
function CubicN(pct, a, b, c, d) {
var t2 = pct * pct;
var t3 = t2 * pct;
return a + (-a * 3 + pct * (3 * a - a * pct)) * pct + (3 * b + pct * (-6 * b + b * 3 * pct)) * pct + (c * 3 - c * 3 * pct) * t2 + d * t3;
}
<canvas height="1961" width="1000" id="canvas"></canvas>
I've got the linear component of collision resolution down relatively well, but I can't quite figure out how to do the same for the angular one. From what I've read, it's something like... torque = point of collision x linear velocity. (cross product) I tried to incorporate an example I found into my code but I actually don't see any rotation at all when objects collide. The other fiddle works perfectly with a rudimentary implementation of the seperating axis theorem and the angular velocity calculations. Here's what I've come up with...
Property definitions (orientation, angular velocity, and angular acceleration):
rotation: 0,
angularVelocity: 0,
angularAcceleration: 0
Calculating the angular velocity in the collision response:
var pivotA = this.vector(bodyA.x, bodyA.y);
bodyA.angularVelocity = 1 * 0.2 * (bodyA.angularVelocity / Math.abs(bodyA.angularVelocity)) * pivotA.subtract(isCircle ? pivotA.add(bodyA.radius) : {
x: pivotA.x + boundsA.width,
y: pivotA.y + boundsA.height
}).vCross(bodyA.velocity);
var pivotB = this.vector(bodyB.x, bodyB.y);
bodyB.angularVelocity = 1 * 0.2 * (bodyB.angularVelocity / Math.abs(bodyB.angularVelocity)) * pivotB.subtract(isCircle ? pivotB.add(bodyB.radius) : {
x: pivotB.x + boundsB.width,
y: pivotB.y + boundsB.height
}).vCross(bodyB.velocity);
Updating the orientation in the update loop:
var torque = 0;
torque += core.objects[o].angularVelocity * -1;
core.objects[o].angularAcceleration = torque / core.objects[o].momentOfInertia();
core.objects[o].angularVelocity += core.objects[o].angularAcceleration;
core.objects[o].rotation += core.objects[o].angularVelocity;
I would post the code that I have for calculating the moments of inertia but there's a seperate one for every object so that would be a bit... lengthy. Nonetheless, here's the one for a circle as an example:
return this.mass * this.radius * this.radius / 2;
Just to show the result, here's my fiddle. As shown, objects do not rotate on collision. (not exactly visible with the circles, but it should work for the zero and seven)
What am I doing wrong?
EDIT: Reason they weren't rotating at all was because of an error with groups in the response function -- it rotates now, just not correctly. However, I've commented that out for now as it messes things up.
Also, I've tried another method for rotation. Here's the code in the response:
_bodyA.angularVelocity = direction.vCross(_bodyA.velocity) / (isCircle ? _bodyA.radius : boundsA.width);
_bodyB.angularVelocity = direction.vCross(_bodyB.velocity) / (isCircle ? _bodyB.radius : boundsB.width);
Note that direction refers to the "collision normal".
Angular and linear acceleration due to force vector
Angular and directional accelerations due to an applied force are two components of the same thing and can not be separated. To get one you need to solve for both.
Define the calculations
From simple physics and standing on shoulders we know the following.
F is force (equivalent to inertia)
Fv is linear force
Fa is angular force
a is acceleration could be linear or rotational depending on where it is used
v is velocity. For angular situations it is the tangential component only
m is mass
r is radius
For linear forces
F = m * v
From which we derive
m = F / v
v = F / m
For rotational force (v is tangential velocity)
F = r * r * m * (v / r) and simplify F = r * m * v
From which we derive
m = F / ( r * v )
v = F / ( r * m )
r = F / ( v * m )
Because the forces we apply are instantaneous we can interchange a acceleration and v velocity to give all the following formulas
Linear
F = m * a
m = F / a
a = F / m
Rotational
F = r * m * a
m = F / ( r * a )
a = F / ( r * m )
r = F / ( a * m )
As we are only interested in the change in velocity for both linear and rotation solutions
a1 = F / m
a2 = F / ( r * m )
Where a1 is acceleration in pixels per frame2 and a2 is acceleration in radians per frame2 ( the frame squared just denotes it is acceleration)
From 1D to 2D
Because this is a 2D solution and all above are 1D we need to use vectors. I for this problem use two forms of the 2D vector. Polar that has a magnitude (length, distance, the like...) and direction. Cartesian which has x and y. What a vector represents depends on how it is used.
The following functions are used as helpers in the solution. They are written in ES6 so for non compliant browsers you will have to adapt them, though I would not ever suggest you use these as they are written for convenience, they are very inefficient and do a lot of redundant calculations.
Converts a vector from polar to cartesian returning a new one
function polarToCart(pVec, retV = {x : 0, y : 0}) {
retV.x = Math.cos(pVec.dir) * pVec.mag;
retV.y = Math.sin(pVec.dir) * pVec.mag;
return retV;
}
Converts a vector from cartesian to polar returning a new one
function cartToPolar(vec, retV = {dir : 0, mag : 0}) {
retV.dir = Math.atan2(vec.y, vec.x);
retV.mag = Math.hypot(vec.x, vec.y);
return retV;
}
Creates a polar vector
function polar(mag = 1, dir = 0) {
return validatePolar({dir : dir,mag : mag});
}
Create a vector as a cartesian
function vector(x = 1, y = 0) {
return {x : x, y : y};
}
True is the arg vec is a vector in polar form
function isPolar(vec) {
if (vec.mag !== undefined && vec.dir !== undefined) {return true;}
return false;
}
Returns true if arg vec is a vector in cartesian form
function isCart(vec) {
if (vec.x !== undefined && vec.y !== undefined) {return true;}
return false;
}
Returns a new vector in polar form also ensures that vec.mag is positive
function asPolar(vec){
if(isCart(vec)){ return cartToPolar(vec); }
if(vec.mag < 0){
vec.mag = - vec.mag;
vec.dir += PI;
}
return { dir : vec.dir, mag : vec.mag };
}
Copy and converts an unknown vec to cart if not already
function asCart(vec){
if(isPolar(vec)){ return polarToCart(vec); }
return { x : vec.x, y : vec.y};
}
Calculations can result in a negative magnitude though this is valid for some calculations this results in the incorrect vector (reversed) this simply validates that the polar vector has a positive magnitude it does not change the vector just the sign and direction
function validatePolar(vec) {
if (isPolar(vec)) {
if (vec.mag < 0) {
vec.mag = - vec.mag;
vec.dir += PI;
}
}
return vec;
}
The Box
Now we can define an object that we can use to play with. A simple box that has position, size, mass, orientation, velocity and rotation
function createBox(x,y,w,h){
var box = {
x : x, // pos
y : y,
r : 0.1, // its rotation AKA orientation or direction in radians
h : h, // its height
w : w, // its width
dx : 0, // delta x in pixels per frame 1/60th second
dy : 0, // delta y
dr : 0.0, // deltat rotation in radians per frame 1/60th second
mass : w * h, // mass in things
update :function(){
this.x += this.dx;
this.y += this.dy;
this.r += this.dr;
},
}
return box;
}
Applying a force to an object
So now we can redefine some terms
F (force) is a vector force the magnitude is the force and it has a direction
var force = polar(100,0); // create a force 100 units to the right (0 radians)
The force is meaningless without a position where it is applied.
Position is a vector that just holds and x and y location
var location = vector(canvas.width/2, canvas.height/2); // defines a point in the middle of the canvas
Directional vector holds the direction and distance between to positional vectors
var l1 = vector(canvas.width/2, canvas.height/2); // defines a point in the middle of the canvas
var l2 = vector(100,100);
var direction = asPolar(vector(l2.x - l1.x, l2.y - l1.y)); // get the direction as polar vector
direction now has the direction from canvas center to point (100,100) and the distance.
The last thing we need to do is extract the components from a force vector along a directional vector. When you apply a force to an object the force is split into two, one is the force along the line to the object center and adds to the object acceleration, the other force is at 90deg to the line to the object center (the tangent) and that is the force that changes rotation.
To get the two components you get the difference in direction between the force vector and the directional vector from where the force is applied to the object center.
var force = polar(100,0); // the force
var forceLoc = vector(50,50); // the location the force is applied
var direction2Center = asPolar(vector(box.x - forceLoc.x, box.y - forceLoc.y)); // get the direction as polar vector
var pheta = direction2Center - force.dir; // get the angle between the force and object center
Now that you have that angle pheta the force can be split into its rotational and linear components with trig.
var F = force.mag; // get the force magnitude
var Fv = Math.cos(pheta) * F; // get the linear force
var Fa = Math.sin(pheta) * F; // get the angular force
Now the forces can be converted back to accelerations for linear a = F/m and angular a = F/(m*r)
accelV = Fv / box.mass; // linear acceleration in pixels
accelA = Fa / (box.mass * direction2Center.mag); // angular acceleration in radians
You then convert the linear force back to a vector that has a direction to the center of the object
var forceV = polar(Fv, direction2Center);
Convert is back to the cartesian so we can add it to the object deltaX and deltaY
forceV = asCart(forceV);
And add the acceleration to the box
box.dx += forceV.x;
box.dy += forceV.y;
Rotational acceleration is just one dimensional so just add it to the delta rotation of the box
box.dr += accelA;
And that is it.
Function to apply force to Box
The function if attached to the box will apply a force vector at a location to the box.
Attach to the box like so
box.applyForce = applyForce; // bind function to the box;
You can then call the function via the box
box.applyForce(force, locationOfForce);
function applyForce(force, loc){ // force is a vector, loc is a coordinate
var toCenter = asPolar(vector(this.x - loc.x, this.y - loc.y)); // get the vector to the center
var pheta = toCenter.dir - force.dir; // get the angle between the force and the line to center
var Fv = Math.cos(pheta) * force.mag; // Split the force into the velocity force along the line to the center
var Fa = Math.sin(pheta) * force.mag; // and the angular force at the tangent to the line to the center
var accel = asPolar(toCenter); // copy the direction to center
accel.mag = Fv / this.mass; // now use F = m * a in the form a = F/m to get acceleration
var deltaV = asCart(accel); // convert acceleration to cartesian
this.dx += deltaV.x // update the box delta V
this.dy += deltaV.y //
var accelA = Fa / (toCenter.mag * this.mass); // for the angular component get the rotation
// acceleration from F=m*a*r in the
// form a = F/(m*r)
this.dr += accelA;// now add that to the box delta r
}
The Demo
The demo is only about the function applyForce the stuff to do with gravity and bouncing are only very bad approximations and should not be used for any physic type of stuff as they do not conserve energy.
Click and drag to apply a force to the object in the direction that the mouse is moved.
const PI90 = Math.PI / 2;
const PI = Math.PI;
const PI2 = Math.PI * 2;
const INSET = 10; // playfeild inset
const ARROW_SIZE = 6
const SCALE_VEC = 10;
const SCALE_FORCE = 0.15;
const LINE_W = 2;
const LIFE = 12;
const FONT_SIZE = 20;
const FONT = "Arial Black";
const WALL_NORMS = [PI90,PI,-PI90,0]; // dirction of the wall normals
var box = createBox(200, 200, 50, 100);
box.applyForce = applyForce; // Add this function to the box
// render / update function
var mouse = (function(){
function preventDefault(e) { e.preventDefault(); }
var i;
var mouse = {
x : 0, y : 0,buttonRaw : 0,
bm : [1, 2, 4, 6, 5, 3], // masks for setting and clearing button raw bits;
mouseEvents : "mousemove,mousedown,mouseup".split(",")
};
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; }
if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1];
} else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2];}
e.preventDefault();
}
mouse.start = function(element = document){
if(mouse.element !== undefined){ mouse.removeMouse();}
mouse.element = element;
mouse.mouseEvents.forEach(n => { element.addEventListener(n, mouseMove); } );
}
mouse.remove = function(){
if(mouse.element !== undefined){
mouse.mouseEvents.forEach(n => { mouse.element.removeEventListener(n, mouseMove); } );
mouse.element = undefined;
}
}
return mouse;
})();
var canvas,ctx;
function createCanvas(){
canvas = document.createElement("canvas");
canvas.style.position = "absolute";
canvas.style.left = "0px";
canvas.style.top = "0px";
canvas.style.zIndex = 1000;
document.body.appendChild(canvas);
}
function resizeCanvas(){
if(canvas === undefined){
createCanvas();
}
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx = canvas.getContext("2d");
if(box){
box.w = canvas.width * 0.10;
box.h = box.w * 2;
box.mass = box.w * box.h;
}
}
window.addEventListener("resize",resizeCanvas);
resizeCanvas();
mouse.start(canvas)
var tempVecs = [];
function addTempVec(v,vec,col,life = LIFE,scale = SCALE_VEC){tempVecs.push({v:v,vec:vec,col:col,scale:scale,life:life,sLife:life});}
function drawTempVecs(){
for(var i = 0; i < tempVecs.length; i ++ ){
var t = tempVecs[i]; t.life -= 1;
if(t.life <= 0){tempVecs.splice(i, 1); i--; continue}
ctx.globalAlpha = (t.life / t.sLife)*0.25;
drawVec(t.v, t.vec ,t.col, t.scale)
}
}
function drawVec(v,vec,col,scale = SCALE_VEC){
vec = asPolar(vec)
ctx.setTransform(1,0,0,1,v.x,v.y);
var d = vec.dir;
var m = vec.mag;
ctx.rotate(d);
ctx.beginPath();
ctx.lineWidth = LINE_W;
ctx.strokeStyle = col;
ctx.moveTo(0,0);
ctx.lineTo(m * scale,0);
ctx.moveTo(m * scale-ARROW_SIZE,-ARROW_SIZE);
ctx.lineTo(m * scale,0);
ctx.lineTo(m * scale-ARROW_SIZE,ARROW_SIZE);
ctx.stroke();
}
function drawText(text,x,y,font,size,col){
ctx.font = size + "px "+font;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.setTransform(1,0,0,1,x,y);
ctx.globalAlpha = 1;
ctx.fillStyle = col;
ctx.fillText(text,0,0);
}
function createBox(x,y,w,h){
var box = {
x : x, // pos
y : y,
r : 0.1, // its rotation AKA orientation or direction in radians
h : h, // its height, and I will assume that its depth is always equal to its height
w : w, // its width
dx : 0, // delta x in pixels per frame 1/60th second
dy : 0, // delta y
dr : 0.0, // deltat rotation in radians per frame 1/60th second
getDesc : function(){
var vel = Math.hypot(this.dx ,this.dy);
var radius = Math.hypot(this.w,this.h)/2
var rVel = Math.abs(this.dr * radius);
var str = "V " + (vel*60).toFixed(0) + "pps ";
str += Math.abs(this.dr * 60 * 60).toFixed(0) + "rpm ";
str += "Va " + (rVel*60).toFixed(0) + "pps ";
return str;
},
mass : function(){ return (this.w * this.h * this.h)/1000; }, // mass in K things
draw : function(){
ctx.globalAlpha = 1;
ctx.setTransform(1,0,0,1,this.x,this.y);
ctx.rotate(this.r);
ctx.fillStyle = "#444";
ctx.fillRect(-this.w/2, -this.h/2, this.w, this.h)
ctx.strokeRect(-this.w/2, -this.h/2, this.w, this.h)
},
update :function(){
this.x += this.dx;
this.y += this.dy;
this.dy += 0.061; // alittle gravity
this.r += this.dr;
},
getPoint : function(which){
var dx,dy,x,y,xx,yy,velocityA,velocityT,velocity;
dx = Math.cos(this.r);
dy = Math.sin(this.r);
switch(which){
case 0:
x = -this.w /2;
y = -this.h /2;
break;
case 1:
x = this.w /2;
y = -this.h /2;
break;
case 2:
x = this.w /2;
y = this.h /2;
break;
case 3:
x = -this.w /2;
y = this.h /2;
break;
case 4:
x = this.x;
y = this.y;
}
var xx,yy;
xx = x * dx + y * -dy;
yy = x * dy + y * dx;
var details = asPolar(vector(xx, yy))
xx += this.x;
yy += this.y;
velocityA = polar(details.mag * this.dr, details.dir + PI90);
velocityT = vectorAdd(velocity = vector(this.dx, this.dy), velocityA);
return {
velocity : velocity, // only directional
velocityT : velocityT, // total
velocityA : velocityA, // angular only
pos : vector(xx, yy),
radius : details.mag,
}
},
}
box.mass = box.mass(); // Mass remains the same so just set it with its function
return box;
}
// calculations can result in a negative magnitude though this is valide for some
// calculations this results in the incorrect vector (reversed)
// this simply validates that the polat vector has a positive magnitude
// it does not change the vector just the sign and direction
function validatePolar(vec){
if(isPolar(vec)){
if(vec.mag < 0){
vec.mag = - vec.mag;
vec.dir += PI;
}
}
return vec;
}
// converts a vector from polar to cartesian returning a new one
function polarToCart(pVec, retV = {x : 0, y : 0}){
retV.x = Math.cos(pVec.dir) * pVec.mag;
retV.y = Math.sin(pVec.dir) * pVec.mag;
return retV;
}
// converts a vector from cartesian to polar returning a new one
function cartToPolar(vec, retV = {dir : 0, mag : 0}){
retV.dir = Math.atan2(vec.y,vec.x);
retV.mag = Math.hypot(vec.x,vec.y);
return retV;
}
function polar (mag = 1, dir = 0) { return validatePolar({dir : dir, mag : mag}); } // create a polar vector
function vector (x= 1, y= 0) { return {x: x, y: y}; } // create a cartesian vector
function isPolar (vec) { if(vec.mag !== undefined && vec.dir !== undefined) { return true; } return false; }// returns true if polar
function isCart (vec) { if(vec.x !== undefined && vec.y !== undefined) { return true; } return false; }// returns true if cartesian
// copy and converts an unknown vec to polar if not already
function asPolar(vec){
if(isCart(vec)){ return cartToPolar(vec); }
if(vec.mag < 0){
vec.mag = - vec.mag;
vec.dir += PI;
}
return { dir : vec.dir, mag : vec.mag };
}
// copy and converts an unknown vec to cart if not already
function asCart(vec){
if(isPolar(vec)){ return polarToCart(vec); }
return { x : vec.x, y : vec.y};
}
// normalise makes a vector a unit length and returns it as a cartesian
function normalise(vec){
var vp = asPolar(vec);
vap.mag = 1;
return asCart(vp);
}
function vectorAdd(vec1, vec2){
var v1 = asCart(vec1);
var v2 = asCart(vec2);
return vector(v1.x + v2.x, v1.y + v2.y);
}
// This splits the vector (polar or cartesian) into the components along dir and the tangent to that dir
function vectorComponentsForDir(vec,dir){
var v = asPolar(vec); // as polar
var pheta = v.dir - dir;
var Fv = Math.cos(pheta) * v.mag;
var Fa = Math.sin(pheta) * v.mag;
var d1 = dir;
var d2 = dir + PI90;
if(Fv < 0){
d1 += PI;
Fv = -Fv;
}
if(Fa < 0){
d2 += PI;
Fa = -Fa;
}
return {
along : polar(Fv,d1),
tangent : polar(Fa,d2)
};
}
function doCollision(pointDetails, wallIndex){
var vv = asPolar(pointDetails.velocity); // Cartesian V make sure the velocity is in cartesian form
var va = asPolar(pointDetails.velocityA); // Angular V make sure the velocity is in cartesian form
var vvc = vectorComponentsForDir(vv, WALL_NORMS[wallIndex])
var vac = vectorComponentsForDir(va, WALL_NORMS[wallIndex])
vvc.along.mag *= 1.18; // Elastic collision requiers that the two equal forces from the wall
vac.along.mag *= 1.18; // against the box and the box against the wall be summed.
// As the wall can not move the result is that the force is twice
// the force the box applies to the wall (Yes and currently force is in
// velocity form untill the next line)
vvc.along.mag *= box.mass; // convert to force
//vac.along.mag/= pointDetails.radius
vac.along.mag *= box.mass
vvc.along.dir += PI; // force is in the oppisite direction so turn it 180
vac.along.dir += PI; // force is in the oppisite direction so turn it 180
// split the force into components based on the wall normal. One along the norm the
// other along the wall
vvc.tangent.mag *= 0.18; // add friction along the wall
vac.tangent.mag *= 0.18;
vvc.tangent.mag *= box.mass //
vac.tangent.mag *= box.mass
vvc.tangent.dir += PI; // force is in the oppisite direction so turn it 180
vac.tangent.dir += PI; // force is in the oppisite direction so turn it 180
// apply the force out from the wall
box.applyForce(vvc.along, pointDetails.pos)
// apply the force along the wall
box.applyForce(vvc.tangent, pointDetails.pos)
// apply the force out from the wall
box.applyForce(vac.along, pointDetails.pos)
// apply the force along the wall
box.applyForce(vac.tangent, pointDetails.pos)
//addTempVec(pointDetails.pos, vvc.tangent, "red", LIFE, 10)
//addTempVec(pointDetails.pos, vac.tangent, "red", LIFE, 10)
}
function applyForce(force, loc){ // force is a vector, loc is a coordinate
validatePolar(force); // make sure the force is a valid polar
// addTempVec(loc, force,"White", LIFE, SCALE_FORCE) // show the force
var l = asCart(loc); // make sure the location is in cartesian form
var toCenter = asPolar(vector(this.x - l.x, this.y - l.y));
var pheta = toCenter.dir - force.dir;
var Fv = Math.cos(pheta) * force.mag;
var Fa = Math.sin(pheta) * force.mag;
var accel = asPolar(toCenter); // copy the direction to center
accel.mag = Fv / this.mass; // now use F = m * a in the form a = F/m
var deltaV = asCart(accel); // convert it to cartesian
this.dx += deltaV.x // update the box delta V
this.dy += deltaV.y
var accelA = Fa / (toCenter.mag * this.mass); // for the angular component get the rotation
// acceleration
this.dr += accelA;// now add that to the box delta r
}
// make a box
ctx.globalAlpha = 1;
var lx,ly;
function update(){
// clearLog();
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.lineWidth = 1;
ctx.strokeStyle = "black";
ctx.fillStyle = "#888";
ctx.fillRect(INSET, INSET, canvas.width - INSET * 2, canvas.height - INSET * 2);
ctx.strokeRect(INSET, INSET, canvas.width - INSET * 2, canvas.height - INSET * 2);
ctx.lineWidth = 2;
ctx.strokeStyle = "black";
box.update();
box.draw();
if(mouse.buttonRaw & 1){
var force = asPolar(vector(mouse.x - lx, mouse.y - ly));
force.mag *= box.mass * 0.1;
box.applyForce(force,vector(mouse.x, mouse.y))
addTempVec(vector(mouse.x, mouse.y), asPolar(vector(mouse.x - lx, mouse.y - ly)), "Cyan", LIFE, 5);
}
lx = mouse.x;
ly = mouse.y;
for(i = 0; i < 4; i++){
var p = box.getPoint(i);
// only do one collision per frame or we will end up adding energy
if(p.pos.x < INSET){
box.x += (INSET) - p.pos.x;
doCollision(p,3)
}else
if( p.pos.x > canvas.width-INSET){
box.x += (canvas.width - INSET) - p.pos.x;
doCollision(p,1)
}else
if(p.pos.y < INSET){
box.y += (INSET) -p.pos.y;
doCollision(p,0)
}else
if( p.pos.y > canvas.height-INSET){
box.y += (canvas.height - INSET) -p.pos.y;
doCollision(p,2)
}
drawVec(p.pos,p.velocity,"blue")
}
drawTempVecs();
ctx.globalAlpha = 1;
drawText(box.getDesc(),canvas.width/2,FONT_SIZE,FONT,FONT_SIZE,"black");
drawText("Click drag to apply force to box",canvas.width/2,FONT_SIZE +17,FONT,14,"black");
requestAnimationFrame(update)
}
update();
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.