I'm trying to make a circle object move to a ball position but whenever it is near to the ball position, it slows down no matter where it starts to move. I can't seem to make it move at a constant speed without slowing it down.
I'm using lerp for linear interpolation and using it directly in my move function.
function lerp(v0, v1, t) {
return v0 * (1 - t) + v1 * t;
};
FPS = 60;
function Objects(/*parameters*/){
this.pos = new Vector2D(x, y);
this.move = function(x, y){
this.pos.x = lerp(this.pos.x, x, FPS/1000);
this.pos.y = lerp(this.pos.y, y, FPS/1000);
};
};
function update(){
//Move object to ball position
SuperObject.move(ball.pos.x, ball.pos.y);
drawObjects();
setTimeout(update, 1000/FPS);
};
DEMO link: http://codepen.io/knoxgon/pen/EWVyzv
This is the expected behavior. As you set the position by linearly interpolating from the current position to the target, it defines a convergent series.
Lets see a simpler example: Say you have only one dimension, and the circle is originally at x(0)=10 and the target is at tx=0. You define every step by x(n+1) = lerp(x(n), tx, 0.1) = 0.9 * x(n) + 0.1 * tx = 0.9 * x(n) (0.9 for simplicity). So the series becomes x(0) = 10, x(1) = 9, x(2) = 8.1, x(n) = 10 * pow(0.9, n), which is a convergent geometric progression, and will never describe a motion at constant speed.
You have to change your equation:
move(x, y) {
let deltax = x - this.pos.x;
let deltay = y - this.pos.y;
const deltaLength = Math.sqrt(deltax * deltax + deltay * deltay);
const speed = 10;
if (deltaLength > speed) {
deltax = speed * deltax / deltaLength;
deltay = speed * deltay / deltaLength;
}
this.pos.x += deltax;
this.pos.y += deltay;
}
http://codepen.io/anon/pen/LWpRWJ
Related
I have a simple transform class to apply translations, scales and rotations on a div in any arbitrary order:
class TransformDiv{
constructor(div)
{
this.div = div;
this.translateX = 0;
this.translateY = 0;
this.scaleX = 1;
this.scaleY = 1;
this.shearX = 0;
this.shearY = 0;
}
translate(x, y)
{
this.translateX += x;
this.translateY += y;
this.setTransform();
}
scale(x, y, anchorX = 0, anchorY = 0)
{
this.scaleX *= x;
this.shearX *= x;
this.scaleY *= y;
this.shearY *= y;
this.translateX -= (this.translateX - anchorX) * (1 - x);
this.translateY -= (this.translateY - anchorY) * (1 - y);
this.setTransform();
}
rotate(rad, anchorX = 0, anchorY = 0)
{
let cos = Math.cos(rad);
let sin = Math.sin(rad);
// the composition of two successive rotations are additive
let newScaleX = this.scaleX * cos + this.shearX * sin;
let newShearX = this.scaleX * (-sin) + this.shearX * cos;
let newShearY = this.shearY * cos + this.scaleY * sin;
let newScaleY = this.shearY * (-sin) + this.scaleY * cos;
this.scaleX = newScaleX;
this.shearX = newShearX;
this.shearY = newShearY;
this.scaleY = newScaleY;
//rotation about an arbitrary point
let originX = (this.translateX - anchorX);
let originY = (this.translateY - anchorY);
this.translateX -= (originY * sin - originX * (cos - 1));
this.translateY -= (-originY * (cos - 1) - originX * sin);
this.setTransform();
}
setTransform()
{
this.div.style.transform = `matrix(${this.scaleX}, ${this.shearY}, ${this.shearX}, ${this.scaleY}, ${this.translateX}, ${this.translateY})`;
}
}
A problem arises when I wish to rotate after a non-uniform scale has been made.
Edit - Newer interactive example: https://codepen.io/manstie/pen/RwGGOmB
Here is the example I made:
https://jsfiddle.net/ft61q230/1/
In the example here:
div2.translate(100, 100);
div2.scale(2, 1, 100, 100);
div2.rotate(Math.PI / 2, 100, 100);
The expected result is for Test 1 Text and Test 2 Text to be the same length, as if you were rotating from the top left of the div clockwise 90 degrees; but as you can see the result is such that the rotation logic I am performing retains the scale on the world-space axis, so now Test 2 Text is twice as tall rather than twice as long.
Current outcome:
Desired outcome:
The current rotation logic is based on multiplying the existing transformation matrix that makes up rotation by another transformation matrix containing an angle to rotate by, but I realize it is not as simple as that and I am missing something to retain local-axial scale.
Thank you for your assistance.
Edit:
Was recommended DOMMatrix which does all this math for me, but it has the same problem, although there is some skew which I don't think is accurate:
https://jsfiddle.net/heqo7vrt/1/
The skew is caused by the scale function scaling it's local X axis while it is rotated, and then rotating after not keeping that local X axis scaling. Also, DOMMatrix translate function has the translations apply on its local axis which is not desired in my situation but if its rotate function worked as expected I would be able to use it.
I managed to fix it here:
Regular: https://jsfiddle.net/sbca61k5/
let newScaleX = cos * this.scaleX + sin * this.shearY;
let newShearX = cos * this.shearX + sin * this.scaleY;
let newShearY = -sin * this.scaleX + cos * this.shearY;
let newScaleY = -sin * this.shearX + cos * this.scaleY;
DOMMatrix version: https://jsfiddle.net/b36kqrsg/
this.matrix = new DOMMatrix([cos, sin, -sin, cos, 0, 0]).multiply(this.matrix);
// or
this.matrix = new DOMMatrix().rotate(deg).multiply(this.matrix);
The difference is to have the rotation matrix multiplied by the rest of the matrix to "add" it on, not the other way round:
[a c e] [cos -sin 0] [scx shy tx]
[b d f] = [sin cos 0] . [shx scy ty]
[0 0 1] [0 0 1] [0 0 1 ]
I'm unsure about the details of the anchor mathematics but the DOMMatrix version's anchor is relative to its own top left whereas the other is relative to the top left of the document.
From my interactive example the anchor maths does not work as after a multitude of rotations the objects get further away from the anchor origin.
https://codepen.io/manstie/pen/PoGXMed
I'm working on an HTML Canvas demo to learn more about circle to circle collision detection and response. I believe that the detection code is correct but the response math is not quite there.
The demo has been implemented using TypeScript, which is a typed superset of JavaScript that is transpiled to plain JavaScript.
I believe that the problem exists within the checkCollision method of the Circle class, specifically the math for calculating the new velocity.
The blue circle position is controlled by the mouse (using an event listener). If the red circle collides from the right side of the blue circle, the collision response seems to work correctly, but if it approaches from the left it does not respond correctly.
I am looking for some guidance on how I can revise the checkCollision math to correctly handle the collision from any angle.
Here is a CodePen for a live demo and dev environment:
CodePen
class DemoCanvas {
canvasWidth: number = 500;
canvasHeight: number = 500;
canvas: HTMLCanvasElement = document.createElement('canvas');
constructor() {
this.canvas.width = this.canvasWidth;
this.canvas.height = this.canvasHeight;
this.canvas.style.border = '1px solid black';
this.canvas.style.position = 'absolute';
this.canvas.style.left = '50%';
this.canvas.style.top = '50%';
this.canvas.style.transform = 'translate(-50%, -50%)';
document.body.appendChild(this.canvas);
}
clear() {
this.canvas.getContext('2d').clearRect(0, 0, this.canvas.width, this.canvas.height);
}
getContext(): CanvasRenderingContext2D {
return this.canvas.getContext('2d');
}
getWidth(): number {
return this.canvasWidth;
}
getHeight(): number {
return this.canvasHeight;
}
getTop(): number {
return this.canvas.getBoundingClientRect().top;
}
getRight(): number {
return this.canvas.getBoundingClientRect().right;
}
getBottom(): number {
return this.canvas.getBoundingClientRect().bottom;
}
getLeft(): number {
return this.canvas.getBoundingClientRect().left;
}
}
class Circle {
x: number;
y: number;
xVelocity: number;
yVelocity: number;
radius: number;
color: string;
canvas: DemoCanvas;
context: CanvasRenderingContext2D;
constructor(x: number, y: number, xVelocity: number, yVelocity: number, color: string, gameCanvas: DemoCanvas) {
this.radius = 20;
this.x = x;
this.y = y;
this.xVelocity = xVelocity;
this.yVelocity = yVelocity;
this.color = color;
this.canvas = gameCanvas;
this.context = this.canvas.getContext();
}
public draw(): void {
this.context.fillStyle = this.color;
this.context.beginPath();
this.context.arc(this.x, this.y, this.radius, 0, 2 * Math.PI);
this.context.fill();
}
public move(): void {
this.x += this.xVelocity;
this.y += this.yVelocity;
}
checkWallCollision(gameCanvas: DemoCanvas): void {
let top = 0;
let right = 500;
let bottom = 500;
let left = 0;
if(this.y < top + this.radius) {
this.y = top + this.radius;
this.yVelocity *= -1;
}
if(this.x > right - this.radius) {
this.x = right - this.radius;
this.xVelocity *= -1;
}
if(this.y > bottom - this.radius) {
this.y = bottom - this.radius;
this.yVelocity *= -1;
}
if(this.x < left + this.radius) {
this.x = left + this.radius;
this.xVelocity *= -1;
}
}
checkCollision(x1: number, y1: number, r1: number, x2: number, y2: number, r2: number) {
let distance: number = Math.abs((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
// Detect collision
if(distance < (r1 + r2) * (r1 + r2)) {
// Respond to collision
let newVelocityX1 = (circle1.xVelocity + circle2.xVelocity) / 2;
let newVelocityY1 = (circle1.yVelocity + circle1.yVelocity) / 2;
circle1.x = circle1.x + newVelocityX1;
circle1.y = circle1.y + newVelocityY1;
circle1.xVelocity = newVelocityX1;
circle1.yVelocity = newVelocityY1;
}
}
}
let demoCanvas = new DemoCanvas();
let circle1: Circle = new Circle(250, 250, 5, 5, "#F77", demoCanvas);
let circle2: Circle = new Circle(250, 540, 5, 5, "#7FF", demoCanvas);
addEventListener('mousemove', function(e) {
let mouseX = e.clientX - demoCanvas.getLeft();
let mouseY = e.clientY - demoCanvas.getTop();
circle2.x = mouseX;
circle2.y = mouseY;
});
function loop() {
demoCanvas.clear();
circle1.draw();
circle2.draw();
circle1.move();
circle1.checkWallCollision(demoCanvas);
circle2.checkWallCollision(demoCanvas);
circle1.checkCollision(circle1.x, circle1.y, circle1.radius, circle2.x, circle2.y, circle2.radius);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
Elasic 2D collision
The problem is likely because the balls do not move away from each other and then in the next frame they are still overlapping and it gets worse. My guess from just looking at the code.
A simple solution.
Before you can have the two balls change direction you must ensure that they are positioned correctly. They must be just touching, (no overlay) or they can get caught up in each other.
Detect collision, and fix position.
// note I am using javascript.
// b1,b2 are the two balls or circles
// b1.dx,b1.dy are velocity (deltas) to save space same for b2
// get dist between them
// first vect from one to the next
const dx = b2.x - b1.x;
const dy = b2.y - b1.y;
// then distance
const dist = Math.sqrt(dx*dx + dy*dy);
// then check overlap
if(b1.radius + b2.radius >= dist){ // the balls overlap
// normalise the vector between them
const nx = dx / dist;
const ny = dy / dist;
// now move each ball away from each other
// along the same line as the line between them
// Use the ratio of the radius to work out where they touch
const touchDistFromB1 = (dist * (b1.radius / (b1.radius + b2.radius)))
const contactX = b1.x + nx * touchDistFromB1;
const contactY = b1.y + ny * touchDistFromB1;
// now move each ball so that they just touch
// move b1 back
b1.x = contactX - nx * b1.radius;
b1.y = contactY - ny * b1.radius;
// and b2 in the other direction
b2.x = contactX + nx * b2.radius;
b2.y = contactY + ny * b2.radius;
If one is static
If one of the balls is static then you can keep its position and move the other ball.
// from contact test for b1 is immovable
if(b1.radius + b2.radius >= dist){ // the balls overlap
// normalise the vector between them
const nx = dx / dist;
const ny = dy / dist;
// move b2 away from b1 along the contact line the distance of the radius summed
b2.x = b1.x + nx * (b1.radius + b2.radius);
b2.y = b1.y + ny * (b1.radius + b2.radius);
Now you have the balls correctly separated a you can calculate the new trajectories
Changing the trajectories.
There are a wide variety of ways to do this, but the one I like best is the elastic collision. I created a function from the Elastic collision in Two dimensional space wiki source and have been using it in games for some time.
The function and information is in the snippet at the bottom.
Next I will show how to call the function continuing on from the code above
// get the direction and velocity of each ball
const v1 = Math.sqrt(b1.dx * b1.dx + b1.dy * b1.dy);
const v2 = Math.sqrt(b2.dx * b2.dx + b2.dy * b2.dy);
// get the direction of travel of each ball
const dir1 = Math.atan2(b1.dy, b1.dx);
const dir2 = Math.atan2(b2.dy, b2.dx);
// get the direction from ball1 center to ball2 cenet
const directOfContact = Math.atan2(ny, nx);
// You will also need a mass. You could use the area of a circle, or the
// volume of a sphere to get the mass of each ball with its radius
// this will make them react more realistically
// An approximation is good as it is the ratio not the mass that is important
// Thus ball are spheres. Volume is the cubed radius
const mass1 = Math.pow(b1.radius,3);
const mass1 = Math.pow(b2.radius,3);
And finally you can call the function
ellastic2DCollistionD(b1, b2, v1, v2, d1, d2, directOfContact, mass1, mass2);
And it will correctly set the deltas of both balls.
Moving the ball position along their deltas is done after the collision function
b1.x += b1.dx;
b1.y += b1.dy;
b2.x += b1.dx;
b2.y += b1.dy;
If one of the balls is static you just ignore the deltas.
Elasic 2D collision function
Derived from information at Elastic collision in Two dimensional space wiki
// obj1, obj2 are the object that will have their deltas change
// velocity1, velocity2 is the velocity of each
// dir1, dir2 is the direction of travel
// contactDir is the direction from the center of the first object to the center of the second.
// mass1, mass2 is the mass of the first and second objects.
//
// function ellastic2DCollistionD(obj1, obj2, velocity1, velocity2, dir1, dir2, contactDir, mass1, mass2){
// The function applies the formula below twice, once fro each object, allowing for a little optimisation.
// The formula of each object's new velocity is
//
// For 2D moving objects
// v1,v2 is velocity
// m1, m2 is the mass
// d1 , d2 us the direction of moment
// p is the angle of contact;
//
// v1* cos(d1-p) * (m1 - m2) + 2 * m2 * v2 * cos(d2- p)
// vx = ----------------------------------------------------- * cos(p) + v1 * sin(d1-p) * cos(p + PI/2)
// m1 + m2
// v1* cos(d1-p) * (m1 - m2) + 2 * m2 * v2 * cos(d2- p)
// vy = ----------------------------------------------------- * sin(p) + v1 * sin(d1-p) * sin(p + PI/2)
// m1 + m2
// More info can be found at https://en.wikipedia.org/wiki/Elastic_collision#Two-dimensional
// to keep the code readable I use abbreviated names
function ellastic2DCollistionD(obj1, obj2, v1, v2, d1, d2, cDir, m1, m2){
const mm = m1 - m2;
const mmt = m1 + m2;
const v1s = v1 * Math.sin(d1 - cDir);
const cp = Math.cos(cDir);
const sp = Math.sin(cDir);
var cdp1 = v1 * Math.cos(d1 - cDir);
var cdp2 = v2 * Math.cos(d2 - cDir);
const cpp = Math.cos(cDir + Math.PI / 2)
const spp = Math.sin(cDir + Math.PI / 2)
var t = (cdp1 * mm + 2 * m2 * cdp2) / mmt;
obj1.dx = t * cp + v1s * cpp;
obj1.dy = t * sp + v1s * spp;
cDir += Math.PI;
const v2s = v2 * Math.sin(d2 - cDir);
cdp1 = v1 * Math.cos(d1 - cDir);
cdp2 = v2 * Math.cos(d2 - cDir);
t = (cdp2 * -mm + 2 * m1 * cdp1) / mmt;
obj2.dx = t * -cp + v2s * -cpp;
obj2.dy = t * -sp + v2s * -spp;
}
Note just realized that you are using a typeScript and the function above is specifically type agnostic. Does not care about obj1, obj2 type, and will add the deltas to any object that you pass.
You will have to change the function for typeScript.
The velocity vector should change by a multiple of the normal vector at the collision point, which is also the normalized vector between the circle mid points.
There are several posts here and elsewhere on elastic circle collisions and the computation of the impulse exchange (for instance Collision of circular objects, with jsfiddle for planet billiard https://stackoverflow.com/a/23671054/3088138).
If circle2 is bound to the mouse, then the event listener should also update the velocity using the difference to the previous point and the difference of time stamps, or better some kind of moving average thereof. The mass of this circle in the collision formulas is to be considered infinite.
As you are using requestAnimationFrame, the spacing of the times it is called is to be considered random. It would be better to use actual time stamps and some effort at implementing the Euler method (or whatever the resulting order 1 integration method amounts to) using the actual time increments. The collision procedure should not contain a position update, as that is the domain of the integration step, which in turn makes it necessary to add a test that the disks are actually moving together.
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>
So I'm creating a brick breaker game, and I need some help finding an angle.
Pretty much the game consists of blocks that, when hit, will cause you to lose 1 health. The point of the game is to hit the blocks with the balls to break them before they reach the bottom. If the ball hits a wall or a block, its trajectory is reversed.
I want the user to be able to click someone within the html canvas. Then the balls, which start in the center of the screen at the bottom of the canvas, will follow that angle. In other words, the user will click and the balls will move to that spot and then continue until it hits something.
I have some code here, But it probably won't help on how to achieve the angle thing.
function animate(callback) {
window.requestAnimationFrame(function() {
window.setTimeout(callback, 1000/60);
});
}
// canvas
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
// variables
var ballList = [];
var maxBalls = 1;
var checkAmount = 0;
var interval;
// onload/refresh/update/render
window.onload = function() {
refresh();
}
function refresh() {
update();
render();
animate(refresh);
}
function update() {
document.addEventListener("click", spawn);
for(var i = 0; i < ballList.length; i++) {
ballList[i].move();
}
}
function render() {
context.fillStyle = '#000';
context.fillRect(0, 0, canvas.width, canvas.height);
for(var i = 0; i < ballList.length; i++) {
ballList[i].show();
}
}
// ball
function Ball() {
this.x = canvas.width / 2;
this.y = canvas.height - 50;
this.width = 10;
this.height = 10;
this.xVel = 5;
this.yVel = -10;
this.show = function() {
context.fillStyle = '#fff';
context.fillRect(this.x, this.y, this.width, this.height);
}
this.move = function() {
this.x += this.xVel;
this.y += this.yVel;
if(this.x >= canvas.width || this.x <= 0) {
this.xVel *= -1;
}
if(this.y >= canvas.height || this.y <= 0) {
this.yVel *= -1;
}
}
}
function spawn(event) {
var xVel = (event.clientX - canvas.width / 2) / 90;
if(ballList.length < maxBalls) {
if(checkAmount < maxBalls) {
interval = setInterval(function() {
ballList.push(new Ball((event.clientX)));
checkAmount++;
if(checkAmount > maxBalls) {
clearInterval(interval);
checkAmount = 0;
}
}, 10);
}
}
}
Thanks in advance.
Unit Vectors
To move an object from one point towards another you use a vector. A vector is just two numbers that represent a direction and a speed. It can be polar in that one number is an angle and the other is a distance, or cartesian that represent the vector as the amount of change in x and y.
Cartesian unit vector
For this you can use either but I prefer the cartesian vector and a particular type called a unit vector. The unit vector is 1 unit long. In computer graphics the unit is normally the pixel.
So we have a point to start at
var startX = ?
var startY = ?
And a point the we want to head towards
var targetX = ?
var targetY = ?
We want the unit vector from start to target,
var vectorX = targetX - startX;
var vectorY = targetY - startY;
The vector's length is the distance between the two points. This is not so handy so we will turn it into a unit vector by dividing both the x and y by the length
var length = Math.sqrt(vectorX * vectorX + vectorY * vectorY);
var unitVectX = vectorX / length;
var unitVectY = vectorY / length;
Now we have a one pixel long unit vector.
The Ball will start at start
var ballX = startX
var ballY = startY
And will move at a speed of 200 pixels per second (assuming 60fps)
var ballSpeed = 200 / 60;
Now to move the ball just add the unit vector times the speed and you are done. Well till the next frame that is.
ballX += unitVectX * ballSpeed;
ballY += unitVectY * ballSpeed;
Using the cartesian makes it very easy to bounce off of walls that are aligned to the x or y axis.
if(ballX + ballRadius > canvas.width){
ballX = canvas.width - ballRadius;
unitVectX = - unitVectX;
}
Polar vector
You can also use polar coordinates. As we use a unit vector the polar unit vector just needs the direction. You use the trig function atan2
// get the direction in radians
var polarDirection = Math.atan2(targetY - startY, targetX - startX);
The direction is in radians, many poeple don't like radians and convert to degrees, but there is no need to know which way it is going just as long as it goes in the correct direction. To remember radians is easy. 360 degrees is 2 radian 180 is 1 randian 90 is 0.5. The actual units used are PI (not many people know many of the digits of pi but you don't need to). So 270 degree is 1.5 radians or as a number 1.5 * Math.PI.
The angles start at the 3 o'clock point (pointing to the right of screen) as 0 radians or 0 deg then clockwise 90deg is at 6 o'clock 0.5 radian, and 180deg 1 radian at 6 o'clock and so on.
To move the ball with the polarDirection you need to use some more trig.
// do this once a frame
ballX += Math.cos(polarDirection) * ballSpeed;
ballY += Math.sin(polarDirection) * ballSpeed;
// note that the cos and sin actually generate the cartesian unit vector
/**
* #param {number} x1 - x coordinate of the first point
* #param {number} y1 - y coordinate of the first point
* #param {number} x2 - x coordinate of the second point
* #param {number} y2 - y coordinate of the second point
* #return {number} - the angle (between 0 and 360)
*/
function getDirection(x1, y1, x2, y2) {
// might be negative:
var angle = Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI;
// correct, positive angle:
return (angle + 360) % 360;
}
I wrote this function for a similar purpose. Don't forget that you might have to negate x.
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;