Related
I have a bit of a complicated question (I did a lot of research but wasn't able to find what I'm looking for), basically I'm building a labeling tool where i get a set of images and i want to be able to click the corners of objects and create a point where the user clicks.
Few things to note (I've already done these)
Images can be any orientation and i need to rotate them (rotate from an orientation)
The image should start out scaled to fit the canvas (setting a scale to "zoom out" from image and canvas sizes)
Users can "pan" around (translation based on arrow keys)
Users can zoom in and out on the image (scale with shift + arrow up/down)
Users can reset an image back to center (spacebar centers, shift + spacebar resets zoom initial and re-centers)
The issue I have now is I'm building the click portion (where I draw a point at the cursor location). I have tried multiple things to put the mouse coordinates at the correct location (accounting for scale, translation and rotation) and I'm having a hard time wrapping my head around it. Would love some help or pointers as to how to basically inverse the rotation, scale and translation I've applied to get the point in the correct place.
To give some real context around this I made a Codepen to show whats happening.
Codepen to see it live with the arrow keys / clicks on the canvas
const red = '#ff0000';
class App extends React.Component<{}, {}> {
private canvas: HTMLCanvasElement
private image = new Image
private ctx: CanvasRenderingContext2D | null
private data: any
private orientation: number = 270
private moveKeys: {[key: number]: number} = {}
private cw: number
private ch: number
private scaleFactor: number = 1.00
private startX: number
private startY: number
private panX: number
private panY: number
private isShiftPressed: boolean
private defaultScaleFactor: number = 1.00
private imagePoints: number[][] = []
loadImage = (url: string) => {
this.image.onload = () => {
const iw = this.orientation === 0 || this.orientation === 180 ? this.image.width : this.image.height
const ih = this.orientation === 0 || this.orientation === 180 ? this.image.height : this.image.width
const smaller = Math.min(this.canvas.width / iw, this.canvas.height / ih)
this.defaultScaleFactor = smaller
this.scaleFactor = smaller
}
this.image.src = 'https://i.stack.imgur.com/EYvnM.jpg'
}
componentWillUnmount() {
document.removeEventListener('keyup', this.handleKeyUp)
document.removeEventListener('keydown', this.handleKeyDown)
// window.removeEventListener('resize', this.resizeCanvas)
this.canvas.removeEventListener('click', this.handleCanvasClick)
}
componentDidMount() {
this.isShiftPressed = false
document.addEventListener('keyup', this.handleKeyUp)
document.addEventListener('keydown', this.handleKeyDown)
// window.addEventListener('resize', this.resizeCanvas) // dont need for this example
requestAnimationFrame(this.animate)
const elem = document.getElementById('canvasContainer')
if (!elem) return
const rect = elem.getBoundingClientRect()
this.canvas.addEventListener('click', this.handleCanvasClick)
this.canvas.width = rect.width
this.canvas.height = rect.height
this.ctx = this.canvas.getContext('2d')
this.cw = this.canvas.width
this.ch = this.canvas.height
this.startX = -(this.cw / 2)
this.startY = -(this.ch / 2)
this.panX = this.startX
this.panY = this.startY
this.loadImage()
}
handleCanvasClick = (e) => {
let rect = this.canvas.getBoundingClientRect()
let x = e.clientX - rect.left
let y = e.clientY - rect.top
this.imagePoints.push([x, y])
}
animate = () => {
Object.keys(this.moveKeys).forEach( key => {
this.handleMovement(key, this.moveKeys[key])
})
this.drawTranslated()
requestAnimationFrame(this.animate)
}
handleMovement = (key, quantity) => {
const moveUnit = 20
switch (parseInt(key)) {
case 32: // spacebar
this.panX = this.startX
this.panY = this.startY
if (this.isShiftPressed) {
this.scaleFactor = this.defaultScaleFactor
}
break
case 37: // left
if (this.orientation === 90 || this.orientation === 270) {
this.panY -= moveUnit
} else {
this.panX -= moveUnit
}
break
case 38: // up
if (this.isShiftPressed) {
this.scaleFactor *= 1.1
} else {
if (this.orientation === 90 || this.orientation === 270) {
this.panX += moveUnit
} else {
this.panY += moveUnit
}
}
break
case 39: // right
if (this.orientation === 90 || this.orientation === 270) {
this.panY += moveUnit
} else {
this.panX += moveUnit
}
break
case 40: // down
if (this.isShiftPressed) {
this.scaleFactor /= 1.1
} else {
if (this.orientation === 90 || this.orientation === 270) {
this.panX -= moveUnit
} else {
this.panY -= moveUnit
}
}
break
default:
break
}
}
handleKeyUp = (e) => {
if (e.shiftKey || e.keyCode === 16) {
this.isShiftPressed = false
}
delete this.moveKeys[e.keyCode]
}
handleKeyDown = (e) => {
e.preventDefault()
if (e.shiftKey || e.keyCode === 16) {
this.isShiftPressed = true
}
e.keyCode in this.moveKeys ? this.moveKeys[e.keyCode] += 1 : this.moveKeys[e.keyCode] = 1
}
drawTranslated = () => {
if (!this.ctx) return
const ctx = this.ctx
ctx.clearRect(0, 0, this.cw, this.ch)
ctx.save()
ctx.translate(this.cw / 2, this.ch / 2)
ctx.rotate(this.orientation * Math.PI / 180)
ctx.scale(this.scaleFactor, this.scaleFactor)
ctx.translate(this.panX, this.panY)
const transformedWidth = this.canvas.width / 2 - this.image.width / 2
const transformedHeight = this.canvas.height / 2 - this.image.height / 2
ctx.drawImage(
this.image,
transformedWidth,
transformedHeight
)
const pointSize = 10
if (this.imagePoints.length > 0) {
this.imagePoints.forEach( ([x, y]) => {
ctx.fillStyle = red
ctx.beginPath()
// Obviously the x and y here need to be transformed to work with the current scale, rotation and translation. But I'm stuck here!
ctx.arc(x, y, pointSize, 0, Math.PI * 2, true)
ctx.closePath()
ctx.fill()
})
}
ctx.restore()
}
handleResetUserClicks = () => {
this.imagePoints = []
}
render() {
return (
<div id="container">
<div>Use arrow keys to pan the canvas, shift + up / down to zoom, spacebar to reset</div>
<div id="canvasContainer">
<canvas ref={this.assignCameraRef} id="canvasElement" style={{ position: 'absolute' }} ref={this.assignCameraRef} />
</div>
<div>
<button onClick={this.handleResetUserClicks}>Reset Clicks</button>
</div>
</div>
)
}
assignCameraRef = (canvas: HTMLCanvasElement) => this.canvas = canvas
}
Please ignore the lack of defined props and the few hardcoded values (like orientation). I removed a bit of code and abstracted this to be more generic and part of that meant hardcoding the image url to a dummy one I found online and setting some of the parameters for that image as well.
Inverting the transform
Inverse transform to find world coordinates.
World coordinates are in this case the image pixel coords, and the function toWorld will convert from canvas coords to world coords.
However you translate to cx,cy, rotate, scale and then translate by pan. You will need to multiply the pan coords by the matrix of the above 3 transforms and add that to the last two values of the matrix before you calculate the inverse transform.
Note you have panned twice once for this.panX, this.panY and then you pan by transformedWidth and transformedHeight the function below needs the complete pan this.panX + transformedWidth and this.panY + transformedHeight as the last two arguments.
The modified function from the linked answer is
// NOTE rotate is in radians
// with panX, and panY added
var matrix = [1,0,0,1,0,0];
var invMatrix = [1,0,0,1];
function createMatrix(x, y, scale, rotate, panX, panY){
var m = matrix; // just to make it easier to type and read
var im = invMatrix; // just to make it easier to type and read
// create the rotation and scale parts of the matrix
m[3] = m[0] = Math.cos(rotate) * scale;
m[2] = -(m[1] = Math.sin(rotate) * scale);
// add the translation
m[4] = x;
m[5] = y;
// transform pan and add to the position part of the matrix
m[4] += panX * m[0] + panY * m[2];
m[5] += panX * m[1] + panY * m[3];
//=====================================
// calculate the inverse transformation
// first get the cross product of x axis and y axis
cross = m[0] * m[3] - m[1] * m[2];
// now get the inverted axis
im[0] = m[3] / cross;
im[1] = -m[1] / cross;
im[2] = -m[2] / cross;
im[3] = m[0] / cross;
}
You can then use the toWorld function from the linked answer to get the world coordinates (coordinates in image space)
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'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've got this game in this plunker.
When the swords are not rotating, it all works fine (you can check by uncommenting lines 221 and commenting out 222-223). When they are rotating like in the plunker above, the collision doesn't work well.
I guess that's because the "getImageData" remembers the old images, but I gather it's an expensive thing to recalculate over and over again.
Is there a better way to rotate my images and make this work? Or do I have to recalculate their pixel map?
Code of the culprit:
for (var i = 0; i < monsters.length; i++) {
var monster = monsters[i];
if (monster.ready) {
if (imageCompletelyOutsideCanvas(monster, monster.monsterImage)) {
monster.remove = true;
}
//else {
//ctx.drawImage(monster.monsterImage, monster.x, monster.y);
drawRotatedImage(monster.monsterImage, monster.x, monster.y, monster);
monster.rotateCounter += 0.05;
//}
}
}
Geometric solution
To do this via a quicker geometry solution.
The simplest solution is a line segment with circle intersection algorithm.
Line segment.
A line has a start and end described in a variety of ways. In this case we will use the start and end coordinates.
var line = {
x1 : ?,
y1 : ?,
x2 : ?,
y2 : ?,
}
Circle
The circle is described by its location and radius
var circle = {
x : ?,
y : ?,
r : ?,
}
Circle line segment Intersect
The following describes how I test for the circle line segment collision. I don't know if there is a better way (most likely there is) but this has served me well and is reliable with the caveat that line segments must have length and circles must have area. If you can not guarantee this then you must add checks in the code to ensure you don't get divide by zeros.
Thus to test if a line intercepts the circle we first find out how far the closest point on the line (Note a line is infinite in size while a line segment has a length, start and end)
// a quick convertion of vars to make it easier to read.
var x1 = line.x1;
var y1 = line.y1;
var x2 = line.x2;
var y2 = line.y2;
var cx = circle.x;
var cy = circle.y;
var r = circle.r;
The result of the test, will be true if there is a collision.
var result; // the result of the test
Convert the line to a vector.
var vx = x2 - x1; // convert line to vector
var vy = y2 - y1;
var d2 = (vx * vx + vy * vy); // get the length squared
Get the unit distance from the circle of the near point on the line. The unit distance is a number from 0, to 1 (inclusive) and represents the distance along the vector of a point. if the value is less than 0 then the point is before the vector, if greater then 1 the point is past the end.
I know this by memory and forget the concept. Its the dot product of the line vector and the vector from the start of the line segment to the circle center divided by the line vectors length squared.
// dot product of two vectors is v1.x * v2.x + v1.y * v2.y over v1 length squared
u = ((cx - x1) * vx + (cy - y1) * vy) / d2;
Now use the unit position to get the actual coordinate of the point on the line closest to the circle by adding to the line segment start position the line vector times the unit distance.
// get the closest point
var xx = x1 + vx * u;
var yy = y1 + vy * u;
Now we have a point on the line, we calculate the distance from the circle using pythagoras square root of the sum of the two sides squared.
// get the distance from the circle center
var d = Math.hypot(xx - cx, yy - cy);
Now if the line (not line segment) intersects the circle the distance will be equal or less than the circle radius. Otherwise the is no intercept.
if(d > r){ //is the distance greater than the radius
result = false; // no intercept
} else { // else we need some more calculations
To determine if the line segment has intercepted the circle we need to find the two points on the circle's circumference that the line has crossed. We have the radius and the distance the circle is from the line. As the distance from the line is always at right angles we have a right triangle with the hypot being the radius and one side being the distance found.
Work out the missing length of the triangle. UPDATE see improved version of the code from here at bottom of answer under "update" it uses unit lengths rather than normalise the line vector.
// ld for line distance is the square root of the hyp subtract side squared
var ld = Math.sqrt(r * r - d * d);
Now add that distance to the point we found on the line xx, yy to do that normalise the line vector (makes the line vector one unit long) by dividing the line vector by its length, and then to multiply it by the distance found above
var len = Math.sqrt(d2); // get the line vector length
var nx = (vx / len) * ld;
var ny = (vy / len) * ld;
Some people may see that I could have used the Unit length and skipped a few calculations. Yes but I can be bothered rewriting the demo so will leave it as is
Now to get the to intercept points by adding and subtracting the new vector to the point on the line that is closest to the circle
ix1 = xx + nx; // the point furthest alone the line
iy1 = xx + ny;
ix2 = xx - nx; // the point in the other direction
iy2 = xx - ny;
Now that we have these two points we can work out if they are in the line segment but calculating the unit distance they are on the original line vector, using the dot product divide the squared distance.
var u1 = ((ix1 - x1) * vx + (iy1 - y1) * vy) / d2;
var u2 = ((ix2 - x1) * vx + (iy1 - y1) * vy) / d2;
Now some simple tests to see if the unit postion of these points are on the line segment
if(u1 < 0){ // is the forward intercept befor the line segment start
result = false; // no intercept
}else
if(u2 > 1){ // is the rear intercept after the line end
result = false; // no intercept
} else {
// though the line segment may not have intercepted the circle
// circumference if we have got to here it must meet the conditions
// of touching some part of the circle.
result = true;
}
}
Demo
As always here is a demo showing the logic in action. The circle is centered on the mouse. There are a few test lines that will go red if the circle touches them. It will also show the point where the circle circumference does cross the line. The point will be red if in the line segment or green if outside. These points can be use to add effects or what not
I am lazy today so this is straight from my library. Note I will post the improved math when I get a chance.
Update
I have improved the algorithm by using unit length to calculate the circle circumference intersects, eliminating a lot of code. I have added it to the demo as well.
From the Point where the distance from the line is less than the circle radius
// get the unit distance to the intercepts
var ld = Math.sqrt(r * r - d * d) / Math.sqrt(d2);
// get that points unit distance along the line
var u1 = u + ld;
var u2 = u - ld;
if(u1 < 0){ // is the forward intercept befor the line
result = false; // no intercept
}else
if(u2 > 1){ // is the backward intercept past the end of the line
result = false; // no intercept
}else{
result = true;
}
}
var demo = function(){
// the function described in the answer with extra stuff for the demo
// at the bottom you will find the function being used to test circle intercepts.
/** GeomDependancies.js begin **/
// for speeding up calculations.
// usage may vary from descriptions. See function for any special usage notes
var data = {
x:0, // coordinate
y:0,
x1:0, // 2nd coordinate if needed
y1:0,
u:0, // unit length
i:0, // index
d:0, // distance
d2:0, // distance squared
l:0, // length
nx:0, // normal vector
ny:0,
result:false, // boolean result
}
// make sure hypot is suported
if(typeof Math.hypot !== "function"){
Math.hypot = function(x, y){ return Math.sqrt(x * x + y * y);};
}
/** GeomDependancies.js end **/
/** LineSegCircleIntercept.js begin **/
// use data properties
// result // intercept bool for intercept
// x, y // forward intercept point on line **
// x1, y1 // backward intercept point on line
// u // unit distance of intercept mid point
// d2 // line seg length squared
// d // distance of closest point on line from circle
// i // bit 0 on for forward intercept on segment
// // bit 1 on for backward intercept
// ** x = null id intercept points dont exist
var lineSegCircleIntercept = function(ret, x1, y1, x2, y2, cx, cy, r){
var vx, vy, u, u1, u2, d, ld, len, xx, yy;
vx = x2 - x1; // convert line to vector
vy = y2 - y1;
ret.d2 = (vx * vx + vy * vy);
// get the unit distance of the near point on the line
ret.u = u = ((cx - x1) * vx + (cy - y1) * vy) / ret.d2;
xx = x1 + vx * u; // get the closest point
yy = y1 + vy * u;
// get the distance from the circle center
ret.d = d = Math.hypot(xx - cx, yy - cy);
if(d <= r){ // line is inside circle
// get the distance to the two intercept points
ld = Math.sqrt(r * r - d * d) / Math.sqrt(ret.d2);
// get that points unit distance along the line
u1 = u + ld;
if(u1 < 0){ // is the forward intercept befor the line
ret.result = false; // no intercept
return ret;
}
u2 = u - ld;
if(u2 > 1){ // is the backward intercept past the end of the line
ret.result = false; // no intercept
return ret;
}
ret.i = 0;
if(u1 <= 1){
ret.i += 1;
// get the forward point line intercepts the circle
ret.x = x1 + vx * u1;
ret.y = y1 + vy * u1;
}else{
ret.x = x2;
ret.y = y2;
}
if(u2 >= 0){
ret.x1 = x1 + vx * u2;
ret.y1 = y1 + vy * u2;
ret.i += 2;
}else{
ret.x1 = x1;
ret.y1 = y1;
}
// tough the points of intercept may not be on the line seg
// the closest point to the must be on the line segment
ret.result = true;
return ret;
}
ret.x = null; // flag that no intercept found at all;
ret.result = false; // no intercept
return ret;
}
/** LineSegCircleIntercept.js end **/
// mouse and canvas functions for this demo.
/** fullScreenCanvas.js begin **/
var canvas = (function(){
var canvas = document.getElementById("canv");
if(canvas !== null){
document.body.removeChild(canvas);
}
// creates a blank image with 2d context
canvas = document.createElement("canvas");
canvas.id = "canv";
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style.position = "absolute";
canvas.style.top = "0px";
canvas.style.left = "0px";
canvas.style.zIndex = 1000;
canvas.ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
return canvas;
})();
var ctx = canvas.ctx;
/** fullScreenCanvas.js end **/
/** MouseFull.js begin **/
var canvasMouseCallBack = undefined; // if needed
var mouse = (function(){
var mouse = {
x : 0, y : 0, w : 0, alt : false, shift : false, ctrl : false,
interfaceId : 0, buttonLastRaw : 0, buttonRaw : 0,
over : false, // mouse is over the element
bm : [1, 2, 4, 6, 5, 3], // masks for setting and clearing button raw bits;
getInterfaceId : function () { return this.interfaceId++; }, // For UI functions
startMouse:undefined,
};
function mouseMove(e) {
var t = e.type, m = mouse;
m.x = e.offsetX; m.y = e.offsetY;
if (m.x === undefined) { m.x = e.clientX; m.y = e.clientY; }
m.alt = e.altKey;m.shift = e.shiftKey;m.ctrl = e.ctrlKey;
if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1];
} else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2];
} else if (t === "mouseout") { m.buttonRaw = 0; m.over = false;
} else if (t === "mouseover") { m.over = true;
} else if (t === "mousewheel") { m.w = e.wheelDelta;
} else if (t === "DOMMouseScroll") { m.w = -e.detail;}
if (canvasMouseCallBack) { canvasMouseCallBack(m.x, m.y); }
e.preventDefault();
}
function startMouse(element){
if(element === undefined){
element = document;
}
"mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",").forEach(
function(n){element.addEventListener(n, mouseMove);});
element.addEventListener("contextmenu", function (e) {e.preventDefault();}, false);
}
mouse.mouseStart = startMouse;
return mouse;
})();
if(typeof canvas === "undefined"){
mouse.mouseStart(canvas);
}else{
mouse.mouseStart();
}
/** MouseFull.js end **/
// helper function
function drawCircle(ctx,x,y,r,col,col1,lWidth){
if(col1){
ctx.lineWidth = lWidth;
ctx.strokeStyle = col1;
}
if(col){
ctx.fillStyle = col;
}
ctx.beginPath();
ctx.arc( x, y, r, 0, Math.PI*2);
if(col){
ctx.fill();
}
if(col1){
ctx.stroke();
}
}
// helper function
function drawLine(ctx,x1,y1,x2,y2,col,lWidth){
ctx.lineWidth = lWidth;
ctx.strokeStyle = col;
ctx.beginPath();
ctx.moveTo(x1,y1);
ctx.lineTo(x2,y2);
ctx.stroke();
}
var h = canvas.height;
var w = canvas.width;
var unit = Math.ceil(Math.sqrt(Math.hypot(w, h)) / 32);
const U80 = unit * 80;
const U60 = unit * 60;
const U40 = unit * 40;
const U10 = unit * 10;
var lines = [
{x1 : U80, y1 : U80, x2 : w /2, y2 : h - U80},
{x1 : w - U80, y1 : U80, x2 : w /2, y2 : h - U80},
{x1 : w / 2 - U10, y1 : h / 2 - U40, x2 : w /2, y2 : h/2 + U10 * 2},
{x1 : w / 2 + U10, y1 : h / 2 - U40, x2 : w /2, y2 : h/2 + U10 * 2},
];
function update(){
var i, l;
ctx.clearRect(0, 0, w, h);
drawCircle(ctx, mouse.x, mouse.y, U60, undefined, "black", unit * 3);
drawCircle(ctx, mouse.x, mouse.y, U60, undefined, "yellow", unit * 2);
for(i = 0; i < lines.length; i ++){
l = lines[i]
drawLine(ctx, l.x1, l.y1, l.x2, l.y2, "black" , unit * 3)
drawLine(ctx, l.x1, l.y1, l.x2, l.y2, "yellow" , unit * 2)
// test the lineSegment circle
data = lineSegCircleIntercept(data, l.x1, l.y1, l.x2, l.y2, mouse.x, mouse.y, U60);
// if there is a result display the result
if(data.result){
drawLine(ctx, l.x1, l.y1, l.x2, l.y2, "red" , unit * 2)
if((data.i & 1) === 1){
drawCircle(ctx, data.x, data.y, unit * 4, "white", "red", unit );
}else{
drawCircle(ctx, data.x, data.y, unit * 2, "white", "green", unit );
}
if((data.i & 2) === 2){
drawCircle(ctx, data.x1, data.y1, unit * 4, "white", "red", unit );
}else{
drawCircle(ctx, data.x1, data.y1, unit * 2, "white", "green", unit );
}
}
}
requestAnimationFrame(update);
}
update();
}
// resize if needed by just starting again
window.addEventListener("resize",demo);
// start the demo
demo();
... and here's how to find the sword blade lines when the sword is moved & rotated
Start by finding the vertices of the original sword blade and saving them in an array.
var pts=[{x:28,y:42},{x:69,y:3},{x:83,y:1},{x:83,y:19},{x:42,y:57}];
When the sword rotates, each blade vertex point will rotate around the rotation point. In your case the rotation point is the center of the image.
Gray rect is the rectangular border of the image
Blue dot is one sword vertex (at the tip of the blade)
Green dot is at the center of the image (== the rotation point)
Green line is the distance from center-image to vertex
Blue circle is the path the blade tip will follow as it rotates 360 degrees
The green line will change its angle depending on the image's rotation.
You can calculate the position of the blade tip at any angle of rotation like this:
// [cx,cy] = the image centerpoint (== the rotation point)
// [vx,vy] = the coordinate position of the blade tip
// Calculate the distance and the angle between the 2 points
var dx=vx-cx;
var dy=vy-cy;
var distance=Math.sqrt(dx*dx+dy*dy);
var originalAngle=Math.atan2(dy,dx);
// rotationAngle = the angle the image has been rotated expressed in radians
var rotatedX = cx + distance * Math.cos(originalAngle + rotationAngle);
var rotatedY = cy + distance * Math.sin(originalAngle + rotationAngle);
Here's example code and a Demo that tracks blade vertices while being moved and rotated:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
function reOffset(){
var BB=canvas.getBoundingClientRect();
offsetX=BB.left;
offsetY=BB.top;
}
var offsetX,offsetY;
reOffset();
window.onscroll=function(e){ reOffset(); }
window.onresize=function(e){ reOffset(); }
var isDown=false;
var startX,startY;
var sword={
img:null,
rx:0,
ry:0,
angle:0,
pts:[{x:28,y:42},{x:69,y:3},{x:83,y:1},{x:83,y:19},{x:42,y:57}],
// precalculated properties -- for efficiency
radii:[],
angles:[],
halfWidth:0,
halfHeight:0,
//
initImg:function(img){
var PI2=Math.PI*2;
this.img=img;
this.halfWidth=img.width/2;
this.halfHeight=img.height/2;
for(var i=0;i<this.pts.length;i++){
var dx=this.halfWidth-this.pts[i].x;
var dy=this.halfHeight-this.pts[i].y;
this.radii[i]=Math.sqrt(dx*dx+dy*dy);
this.angles[i]=((Math.atan2(dy,dx)+PI2)%PI2)-Math.PI;
}
},
// draw sword with translation & rotation
draw:function(){
var img=this.img;
var rx=this.rx;
var ry=this.ry;
var angle=this.angle;
ctx.translate(rx,ry);
ctx.rotate(angle);
ctx.drawImage(img,-this.halfWidth,-this.halfHeight);
ctx.rotate(-angle);
ctx.translate(-rx,-ry);
},
// recalc this.pts after translation & rotation
calcTrxPts:function(){
var trxPts=[];
for(var i=0;i<this.pts.length;i++){
var r=this.radii[i];
var ptangle=this.angles[i]+this.angle;
trxPts[i]={
x:this.rx+r*Math.cos(ptangle),
y:this.ry+r*Math.sin(ptangle)
};
}
return(trxPts);
},
}
// load image & initialize sword object & draw scene
var img=new Image();
img.onload=function(){
// set initial sword properties
sword.initImg(img);
sword.rx=150;
sword.ry=75;
sword.angle=0; //(Math.PI/8);
// draw scene
drawAll();
// listen for mouse events
$("#canvas").mousedown(function(e){handleMouseDown(e);});
$("#canvas").mousemove(function(e){handleMouseMove(e);});
$("#canvas").mouseup(function(e){handleMouseUpOut(e);});
$("#canvas").mouseout(function(e){handleMouseUpOut(e);});
// listen for mousewheel events
$("#canvas").on('DOMMouseScroll mousewheel',function(e){
e.preventDefault();
e.stopPropagation();
var e=e || window.event; // old IE support
sign=((e.originalEvent.wheelDelta||e.originalEvent.detail*-1)>0)?1:-1;
sword.angle+=Math.PI/45*sign;
drawAll();
});
}
img.src = "data:image/png;base64,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";
/////////////////////
// helper functions
/////////////////////
function drawAll(){
ctx.clearRect(0,0,cw,ch);
sword.draw();
drawHitArea();
}
function drawHitArea(){
// lines
var trxPts=sword.calcTrxPts();
ctx.beginPath();
ctx.moveTo(trxPts[0].x,trxPts[0].y);
for(var i=1;i<trxPts.length;i++){
ctx.lineTo(trxPts[i].x,trxPts[i].y);
}
ctx.closePath();
ctx.strokeStyle='red';
ctx.stroke();
// dots
for(var i=0;i<trxPts.length;i++){
ctx.beginPath();
ctx.arc(trxPts[i].x,trxPts[i].y,3,0,Math.PI*2);
ctx.closePath();
ctx.fillStyle='blue';
ctx.fill();
}
}
function getClosestPointOnLineSegment(line,x,y) {
//
lerp=function(a,b,x){ return(a+x*(b-a)); };
var dx=line.x1-line.x0;
var dy=line.y1-line.y0;
var t=((x-line.x0)*dx+(y-line.y0)*dy)/(dx*dx+dy*dy);
var lineX=lerp(line.x0, line.x1, t);
var lineY=lerp(line.y0, line.y1, t);
return({x:lineX,y:lineY,isOnSegment:(t>=0 && t<=1)});
};
function handleMouseDown(e){
// tell the browser we're handling this event
e.preventDefault();
e.stopPropagation();
startX=parseInt(e.clientX-offsetX);
startY=parseInt(e.clientY-offsetY);
// Put your mousedown stuff here
isDown=true;
}
function handleMouseUpOut(e){
// tell the browser we're handling this event
e.preventDefault();
e.stopPropagation();
// clear the isDragging flag
isDown=false;
}
function handleMouseMove(e){
if(!isDown){return;}
// tell the browser we're handling this event
e.preventDefault();
e.stopPropagation();
// calc distance moved since last drag
mouseX=parseInt(e.clientX-offsetX);
mouseY=parseInt(e.clientY-offsetY);
var dx=mouseX-startX;
var dy=mouseY-startY;
startX=mouseX;
startY=mouseY;
// drag the sword to new position
sword.rx+=dx;
sword.ry+=dy;
drawAll();
}
body{ background-color: ivory; }
#canvas{border:1px solid red; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<h6>Drag sword and<br>Rotate sword using mousewheel inside canvas<br>Red "collision" lines follow swords translation & rotation.</h6>
<h5></h5>
<canvas id="canvas" width=300 height=300></canvas>
I’m looking for a way to create a wave in a shape designed in canvas. After much research I found something that is pretty close to what I want:
var c = document.getElementById('c'),
ctx = c.getContext('2d'),
cw = c.width = window.innerWidth,
ch = c.height = window.innerHeight,
points = [],
tick = 0,
opt = {
count: 5,
range: {
x: 20,
y: 80
},
duration: {
min: 20,
max: 40
},
thickness: 10,
strokeColor: '#444',
level: .35,
curved: true
},
rand = function(min, max) {
return Math.floor((Math.random() * (max - min + 1)) + min);
},
ease = function(t, b, c, d) {
if ((t /= d / 2) < 1) return c / 2 * t * t + b;
return -c / 2 * ((--t) * (t - 2) - 1) + b;
};
ctx.lineJoin = 'round';
ctx.lineWidth = opt.thickness;
ctx.strokeStyle = opt.strokeColor;
var Point = function(config) {
this.anchorX = config.x;
this.anchorY = config.y;
this.x = config.x;
this.y = config.y;
this.setTarget();
};
Point.prototype.setTarget = function() {
this.initialX = this.x;
this.initialY = this.y;
this.targetX = this.anchorX + rand(0, opt.range.x * 2) - opt.range.x;
this.targetY = this.anchorY + rand(0, opt.range.y * 2) - opt.range.y;
this.tick = 0;
this.duration = rand(opt.duration.min, opt.duration.max);
}
Point.prototype.update = function() {
var dx = this.targetX - this.x;
var dy = this.targetY - this.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (Math.abs(dist) <= 0) {
this.setTarget();
} else {
var t = this.tick;
var b = this.initialY;
var c = this.targetY - this.initialY;
var d = this.duration;
this.y = ease(t, b, c, d);
b = this.initialX;
c = this.targetX - this.initialX;
d = this.duration;
this.x = ease(t, b, c, d);
this.tick++;
}
};
Point.prototype.render = function() {
ctx.beginPath();
ctx.arc(this.x, this.y, 3, 0, Math.PI * 2, false);
ctx.fillStyle = '#000';
ctx.fill();
};
var updatePoints = function() {
var i = points.length;
while (i--) {
points[i].update();
}
};
var renderPoints = function() {
var i = points.length;
while (i--) {
points[i].render();
}
};
var renderShape = function() {
ctx.beginPath();
var pointCount = points.length;
ctx.moveTo(points[0].x, points[0].y);
var i;
for (i = 0; i < pointCount - 1; i++) {
var c = (points[i].x + points[i + 1].x) / 2;
var d = (points[i].y + points[i + 1].y) / 2;
ctx.quadraticCurveTo(points[i].x, points[i].y, c, d);
}
ctx.lineTo(-opt.range.x - opt.thickness, ch + opt.thickness);
ctx.lineTo(cw + opt.range.x + opt.thickness, ch + opt.thickness);
ctx.closePath();
ctx.fillStyle = 'hsl(' + (tick / 2) + ', 80%, 60%)';
ctx.fill();
ctx.stroke();
};
var clear = function() {
ctx.clearRect(0, 0, cw, ch);
};
var loop = function() {
window.requestAnimFrame(loop, c);
tick++;
clear();
updatePoints();
renderShape();
//renderPoints();
};
var i = opt.count + 2;
var spacing = (cw + (opt.range.x * 2)) / (opt.count - 1);
while (i--) {
points.push(new Point({
x: (spacing * (i - 1)) - opt.range.x,
y: ch - (ch * opt.level)
}));
}
window.requestAnimFrame = function() {
return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function(a) {
window.setTimeout(a, 1E3 / 60)
}
}();
loop();
canvas {
display: block;
}
<canvas id="c"></canvas>
http://codepen.io/jackrugile/pen/BvLHg
The problem is that the movement of the wave appears a bit unreal. I'd like to keep this notion of random motion and not have a shape that repeats itself by moving from left to right but it will be great if I found a way to create a ‘realistic’ water movement (good fluid dynamics, collisions of this wave with the edges of its container (custom shape)).
I think I'm asking a lot but ... A small line of research could help :)
Interference
You can make a more realistic wave using interference.
Have one big wave (swell) running slowly with a big motion
Have another one or two smaller sine waves running (oscillators)
All with random amplitudes
Mix the waves horizontally using average and calculate the various points
Draw the result using a cardinal spline (or if the resolution is high you can just draw simple lines between the points instead).
Use various parameters so you can adjust it live to find a good combination.
You can also add oscillators to represent the z axis to make it more realistic in case you want to layer the waves to make a pseudo-3D wave.
Example
I cannot give you wave collision, fluid dynamics - that would be a bit too broad for SO but I can give you a fluid-ish wave example (as you have the point of each segment you can use that for collision detection).
And example would be to create an oscillator object which you could set the various settings on such as amplitude, rotation speed (phase) etc.
Then have a mixer function which mixes the result of these oscillators that you use.
Live demo here (full-screen version here)
The oscillator object in this demo look like this:
function osc() {
/// various settings
this.variation = 0.4; /// how much variation between random and max
this.max = 100; /// max amplitude (radius)
this.speed = 0.02; /// rotation speed (for radians)
var me = this, /// keep reference to 'this' (getMax)
a = 0, /// current angle
max = getMax(); /// create a temp. current max
/// this will be called by mixer
this.getAmp = function() {
a += this.speed; /// add to rotation angle
if (a >= 2.0) { /// at break, reset (see note)
a = 0;
max = getMax();
}
/// calculate y position
return max * Math.sin(a * Math.PI) + this.horizon;
}
function getMax() {
return Math.random() * me.max * me.variation +
me.max * (1 - me.variation);
}
return this;
}
This do all the setup and calculations for us and all we need to do is to call the getAmp() to get a new value for each frame.
Instead of doing it manually we can use a "mixer". This mixer allows us to add as many oscillators we want to the mix:
function mixer() {
var d = arguments.length, /// number of arguments
i = d, /// initialize counter
sum = 0; /// sum of y-points
if (d < 1) return horizon; /// if none, return
while(i--) sum += arguments[i].getAmp(); /// call getAmp and sum
return sum / d + horizon; /// get average and add horizon
}
Putting this in a loop with a point recorder which shifts the point in one direction will create a fluid looking wave.
The demo above uses three oscillators. (A tip in that regard is to keep the rotation speed lower than the big swell or else you will get small bumps on it.)
NOTE: The way I create a new random max is not the best way as I use a break point (but simple for demo purpose). You can instead replace this with something better.
Since you are searching for a realistic effect, best idea might be to simulate the water. It is not that hard, in fact : water can be nicely enough approximated by a network of springs linked together.
Result is quite good, you can find it here :
http://jsfiddle.net/gamealchemist/Z7fs5/
So i assumed it was 2D effect and built an array holding, for each point of a water surface, its acceleration, speed, and position. I store them in a single array, at 3*i + 0, 3*i + 1, and 3*i+2.
Then on each update, i simply apply newton's laws with elasticity, and with some friction to get the movement to stop.
I influence each point so it goes to its stable state + get influenced by its right and left neighboor.
(If you want smoother animation, use also i-2 and i+2 points, and lower kFactor.)
var left = 0, y = -1;
var right = water[2];
for (pt = 0 ; pt < pointCount; pt++, i += 3) {
y = right;
right = (pt < pointCount - 1) ? water[i + 5] : 0;
if (right === undefined) alert('nooo');
// acceleration
water[i] = (-0.3 * y + (left - y) + (right - y)) * kFactor - water[i + 1] * friction;
// speed
water[i + 1] += water[i] * dt;
// height
water[i + 2] += water[i + 1] * dt;
left = y;
}
The draw is very simple : just iterate though the points and draw. But it's hard to get a smooth effect while drawing, since it's hard to have bezier and quadraticCurve to have their derivates match. I suggested a few drawing methods, you can switch if you want.
Then i added rain, so that the water can move in a random way. Here it's just very simple trajectory, then i compute if there's collision with the water, and, if so, i add some velocity and shift the point.
I'd like to create a ‘realistic’ water movement with good fluid dynamics, collisions of this wave with the edges of a custom
container..
Oh boy.. That is quite a mouthful.
You should probably ask your Question here: gamedev.stackexchange
Anyways, have you tried to program any sort of wave yet, or are you just looking for WaveCreator.js ?
Go and Google some Non-language-specific Guides on how to create 2D water.
If you are a beginner, then start with something simple to get the idea behind things.
How about creating a bunch of Boxes for "minecraft-style" water ?
Here every "line" of water could be represented as a position in an Array. Then loop through it and set the "height" of the water based on the previous Array Index.
(You could smooth the water out by either making the blocks very thin (More work for your program!) or by smoothing out the edges and giving them an angle based on the other Squares.
I think this could be a neat solution. Anyhow. Hope that gave you some ideas.