I have a simple sword image (drawn pointing up) that is in an 'open world' game setting, so the player can move around the world to all coordinates. I would like the sword to point at the mouse.
My issue (I think) is that the world coordinates and webpage coordinates do not match up. For example, the player could be at location (6000, 6000), while the mouse coordinates would only be between 0 and 1600 (or whatever the width of the screen is). On top of that, the webpage won't always exist in the same place.
Thoughts so far:
So I need to calculate the position of the mouse relative to the canvas, or vice versa.
Formula I was using. This worked in an older project of mine for XNA:
var xdir = mouse.x - swordcenter.x;
var ydir = mouse.y - swordcenter.y;
var theta = (Math.atan2( ydir, xdir ) - Math.PI/2.0) * (180.0/Math.PI);
I feel like the solution should be simpler than what I'm thinking, but no formula has been working so far. Any ideas?
So the question I suppose is why isn't this working? And my best guess is because of the difference in coordinates. I can't figure out how to factor it in.
Edit: I have figured out how to factor it in. With the following code, the mouse position is put in to game coordinates. So if the mouse hovers over the player, then the mouse pos and player pos are equal. However, the image still spins rapidly with the mouse movement.
document.addEventListener('mousemove', function(e){
mouse.x = e.clientX || e.pageX;
mouse.y = e.clientY || e.pageY;
var view = document.getElementById('viewport');
var rect = view.getBoundingClientRect();
mouse.x -= rect.left;
mouse.y -= rect.top;
var camX = clamp(x - canvas.width/2, -1000, 1000 - canvas.width);
var camY = clamp(y - canvas.height/2, -1000, 1000 - canvas.height);
mouse.x += camX;
mouse.y += camY;
}, false);
Edit 2: Here is how I get the angle:
var getAngle = function() {
var xdir = mouse.x - x;//where x and y are the sword center
var ydir = mouse.y - y;
var theta = Math.atan2( ydir, xdir ) * (180.0/Math.PI);
return theta;
}
Edit 3: Here is how I draw the image:
var draw = function(ctx) {
ctx.fillRect(x-15, y-15, 30, 30);//background player rect
ctx.save();
ctx.translate(x, y);//x and y is the center of the sword/player
ctx.rotate(getAngle());
//this correctly draws the sword on top of the player rect, except for rotation
ctx.drawImage(stanceTexture, -stanceTexture.width/2, -stanceTexture.height/2);
ctx.restore();
};
After all the additional math done to compute the correct "in-game" mouse location, it turns out that my canvas context wanted radians as well. Much confuse. Every other post I found regarding this turns the value back into degrees (WRONG) so I hope this helps someone else out. So here is the complete answer,
document.addEventListener('mousemove', function(e){
mouse.x = e.clientX || e.pageX;
mouse.y = e.clientY || e.pageY;
var view = document.getElementById('viewport');
var rect = view.getBoundingClientRect();
mouse.x -= rect.left;
mouse.y -= rect.top;
var camX = clamp(x - canvas.width/2, world.minX, world.maxX - canvas.width);
var camY = clamp(y - canvas.height/2, world.minY, world.maxX - canvas.height);
mouse.x += camX;
mouse.y += camY;
}, false);
var getAngle = function() {
var xdir = mouse.x - x;//where x and y are the sword center
var ydir = mouse.y - y;
//Note: I only subtract Math.PI/2 to flip the image 180 degrees.
// The value to subtract will depend on the original angle of your image
var theta = Math.atan2( ydir, xdir ) - Math.PI/2.0;
return theta;
}
var draw = function(ctx) {
ctx.fillRect(x-15, y-15, 30, 30);//background player rect
ctx.save();
ctx.translate(x, y);//x and y is the center of the sword/player
ctx.rotate(getAngle());
//this correctly draws the sword on top of the player rect, except for rotation
ctx.drawImage(stanceTexture, -stanceTexture.width/2, -stanceTexture.height/2);
ctx.restore();
};
You don't need trigonometry for that.
Basic vector calculus is enough:
const sword_length = 10;
var sword_x_start = 0;
var sword_y_start = 0;
var mouse_x = ...; // current mouse position
var mouse_y = ...;
var dx = (mouse_x - sword_x_start);
var dy = (mouse_y - sword_y_start);
// sword to mouse distance
var length = Math.sqrt( dx*dx + dy*dy );
// unit vector, see: http://en.wikipedia.org/wiki/Unit_vector
// in sword to mouse direction:
var unit_v_x = dx / length;
var unit_v_y = dy / length;
// and now coordinates of the sword pointing to mouse:
var sword_x_end = sword_x_start + unit_v_x * sword_length ;
var sword_y_end = sword_y_start + unit_v_y * sword_length ;
Related
Hello my dear fellows,
I've been trying to recreate the effect: image scales up as the mouse get closer to the center of the image found on https://www.davidwilliambaum.com/
I have been very unsuccessfull so far, as I am not sure how to approach the problem.
I started a codepen with some ideas : https://codepen.io/dindon-studio/pen/RwLwRKM
As you can see I first get the center coordinate of the image, and then i try some dirty formula to scales it up with the mouse distance.
But it is very buggy and not convincing at all.
Does anyone got a better approach?
Deep thanks for you help!
var mX, mY, distance, element
element = $('.project')
function calculateDistance(elem, mouseX, mouseY) {
return Math.floor(Math.sqrt(Math.pow(mouseX - (elem.offset().left+(elem.width()/2)), 2) + Math.pow(mouseY - (elem.offset().top+(elem.height()/2)), 2))); }
$(document).mousemove(function(e) {
mX = e.pageX;
mY = e.pageY;
distance = calculateDistance(element, mX, mY);
if (distance< 500 && distance >50){
var scaling = 1 + (1/distance) *100
gsap.to(".project", {duration: 0.01, scale: scaling,ease: "power2.in",});
}
});
I build off from your codepen and made some adjustments: https://codepen.io/Mookiie/pen/qBPBmNe
The higher the scalingFactor the closer the mouse needs to be for a size change.
function calculateCenter(image) {
var rect1 = image.getBoundingClientRect();
var x = rect1.left + rect1.width * 0.5;
var y = rect1.top + rect1.height * 0.5;
return { x: x, y: y }
}
function getDistance(x1, y1, x2, y2){
let y = x2 - x1;
let x = y2 - y1;
return Math.sqrt(x * x + y * y);
}
function distanceFromCenter(image, mouseX, mouseY) {
var imageCenter = calculateCenter(image);
return getDistance(imageCenter.x, imageCenter.y, mouseX, mouseY)
}
function adjustImage(image, mX, mY) {
var distance = distanceFromCenter(image, mX, mY);
const baseScale = 1
const maxScaling = 1.5;
const scalingFactor = 1;
const adjustedScaling = maxScaling - ((distance / 1000) * scalingFactor)
const scaling = adjustedScaling >= baseScale ? adjustedScaling : baseScale
gsap.to(image, {duration: 0.01, scale: scaling, ease: "power2.in",});
}
$(document).mousemove(function(e) {
const mX = e.pageX;
const mY = e.pageY;
const images = $("img")
images.each(function() {
adjustImage(this, mX, mY)
})
});
I'm currently working on a googly eye that follows your mouse movements. I've been able to center the googly and listen for mouse movements, but I'm having trouble finding the center of the page after I've resized it.
var DrawEye = function(eyecontainer, pupil, eyeposx, eyeposy){
// Initialise core variables
var r = $(pupil).width()/2;
var center = {
x: $(eyecontainer).width()/2 - r,
y: $(eyecontainer).height()/2 - r
};
var distanceThreshold = $(eyecontainer).width()/2.2 - r;
var mouseX = 0, mouseY = 0;
// Listen for mouse movement
$(window).mousemove(function(e){
var d = {
x: e.pageX - r - eyeposx - center.x,
y: e.pageY - r - eyeposy - center.y
};
var distance = Math.sqrt(d.x*d.x + d.y*d.y);
if (distance < distanceThreshold) {
mouseX = e.pageX - eyeposx - r;
mouseY = e.pageY - eyeposy - r;
} else {
mouseX = d.x / distance * distanceThreshold + center.x;
mouseY = d.y / distance * distanceThreshold + center.y;
}
});
// Update pupil location
var pupil = $(pupil);
var xp = 0, yp = 0;
var loop = setInterval(function(){
// change 1 to alter damping/momentum - higher is slower
xp += (mouseX - xp) / 5;
yp += (mouseY - yp) / 5;
pupil.css({left:xp, top:yp});
}, 1);
};
var pariseye1 = new DrawEye("#eyeleft", "#pupilleft", 650, 300);
I'm trying to get it to follow the mouse no matter how big or small the window size is, I'm just having trouble figuring that out.
As of right now, if you resize the page the googly eye still follows the mouse, but it becomes slight ajar and doesn't quite follow the mouse exactly. It seems like where it's actually tracking the mouse stays the same.
I'm fairly new to javascript, so if anyone could help that would be great!
Thanks, James
So I built some time ago a little Breakout clone, and I wanted to upgrade it a little bit, mostly for the collisions. When I first made it I had a basic "collision" detection between my ball and my brick, which in fact considered the ball as another rectangle. But this created an issue with the edge collisions, so I thought I would change it. The thing is, I found some answers to my problem:
for example this image
and the last comment of this thread : circle/rect collision reaction but i could not find how to compute the final velocity vector.
So far I have :
- Found the closest point on the rectangle,
- created the normal and tangent vectors,
And now what I need is to somehow "divide the velocity vector into a normal component and a tangent component; negate the normal component and add the normal and tangent components to get the new Velocity vector" I'm sorry if this seems terribly easy but I could not get my mind around that ...
code :
function collision(rect, circle){
var NearestX = Max(rect.x, Min(circle.pos.x, rect.x + rect.w));
var NearestY = Max(rect.y, Min(circle.pos.y, rect.y + rect.w));
var dist = createVector(circle.pos.x - NearestX, circle.pos.y - NearestY);
var dnormal = createVector(- dist.y, dist.x);
//change current circle vel according to the collision response
}
Thanks !
EDIT: Also found this but I didn't know if it is applicable at all points of the rectangle or only the corners.
Best explained with a couple of diagrams:
Have angle of incidence = angle of reflection. Call this value θ.
Have θ = normal angle - incoming angle.
atan2 is the function for computing the angle of a vector from the positive x-axis.
Then the code below immediately follows:
function collision(rect, circle){
var NearestX = Max(rect.x, Min(circle.pos.x, rect.x + rect.w));
var NearestY = Max(rect.y, Min(circle.pos.y, rect.y + rect.h));
var dist = createVector(circle.pos.x - NearestX, circle.pos.y - NearestY);
var dnormal = createVector(- dist.y, dist.x);
var normal_angle = atan2(dnormal.y, dnormal.x);
var incoming_angle = atan2(circle.vel.y, circle.vel.x);
var theta = normal_angle - incoming_angle;
circle.vel = circle.vel.rotate(2*theta);
}
Another way of doing it is to get the velocity along the tangent and then subtracting twice this value from the circle velocity.
Then the code becomes
function collision(rect, circle){
var NearestX = Max(rect.x, Min(circle.pos.x, rect.x + rect.w));
var NearestY = Max(rect.y, Min(circle.pos.y, rect.y + rect.h));
var dist = createVector(circle.pos.x - NearestX, circle.pos.y - NearestY);
var tangent_vel = dist.normalize().dot(circle.vel);
circle.vel = circle.vel.sub(tangent_vel.mult(2));
}
Both of the code snippets above do basically the same thing in about the same time (probably). Just pick whichever one you best understand.
Also, as #arbuthnott pointed out, there's a copy-paste error in that NearestY should use rect.h instead of rect.w.
Edit: I forgot the positional resolution. This is the process of moving two physics objects apart so that they're no longer intersecting. In this case, since the block is static, we only need to move the ball.
function collision(rect, circle){
var NearestX = Max(rect.x, Min(circle.pos.x, rect.x + rect.w));
var NearestY = Max(rect.y, Min(circle.pos.y, rect.y + rect.h));
var dist = createVector(circle.pos.x - NearestX, circle.pos.y - NearestY);
if (circle.vel.dot(dist) < 0) { //if circle is moving toward the rect
//update circle.vel using one of the above methods
}
var penetrationDepth = circle.r - dist.mag();
var penetrationVector = dist.normalise().mult(penetrationDepth);
circle.pos = circle.pos.sub(penetrationVector);
}
Bat and Ball collision
The best way to handle ball and rectangle collision is to exploit the symmetry of the system.
Ball as a point.
First the ball, it has a radius r that defines all the points r distance from the center. But we can turn the ball into a point and add to the rectangle the radius. The ball is now just a single point moving over time, which is a line.
The rectangle has grown on all sides by radius. The diagram shows how this works.
The green rectangle is the original rectangle. The balls A,B are not touching the rectangle, while the balls C,D are touching. The balls A,D represent a special case, but is easy to solve as you will see.
All motion as a line.
So now we have a larger rectangle and a ball as a point moving over time (a line), but the rectangle is also moving, which means over time the edges will sweep out areas which is too complicated for my brain, so once again we can use symmetry, this time in relative movement.
From the bat's point of view it is stationary while the ball is moving, and from the ball, it is still while the bat is moving. They both see each other move in the opposite directions.
As the ball is now a point, making changes to its movement will only change the line it travels along. So we can now fix the bat in space and subtract its movement from the ball. And as the bat is now fixed we can move its center point to the origin, (0,0) and move the ball in the opposite direction.
At this point we make an important assumption. The ball and bat are always in a state that they are not touching, when we move the ball and/or bat then they may touch. If they do make contact we calculate a new trajectory so that they are not touching.
Two possible collisions
There are now two possible collision cases, one where the ball hits the side of the bat, and one where the ball hits the corner of the bat.
The next images show the bat at the origin and the ball relative to the bat in both motion and position. It is travelling along the red line from A to B then bounces off to C
Ball hits edge
Ball hits corner
As there is symmetry here as well which side or corner is hit does not make any difference. In fact we can mirror the whole problem depending on which size the ball is from the center of the bat. So if the ball is left of the bat then mirror its position and motion in the x direction, and the same for the y direction (you must keep track of this mirror via a semaphore so you can reverse it once the solution is found).
Code
The example does what is described above in the function doBatBall(bat, ball) The ball has some gravity and will bounce off of the sides of the canvas. The bat is moved via the mouse. The bats movement will be transferred to the ball, but the bat will not feel any force from the ball.
const ctx = canvas.getContext("2d");
const mouse = {x : 0, y : 0, button : false}
function mouseEvents(e){
mouse.x = e.pageX;
mouse.y = e.pageY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down","up","move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents));
// short cut vars
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
const gravity = 1;
// constants and helpers
const PI2 = Math.PI * 2;
const setStyle = (ctx,style) => { Object.keys(style).forEach(key=> ctx[key] = style[key] ) };
// the ball
const ball = {
r : 50,
x : 50,
y : 50,
dx : 0.2,
dy : 0.2,
maxSpeed : 8,
style : {
lineWidth : 12,
strokeStyle : "green",
},
draw(ctx){
setStyle(ctx,this.style);
ctx.beginPath();
ctx.arc(this.x,this.y,this.r-this.style.lineWidth * 0.45,0,PI2);
ctx.stroke();
},
update(){
this.dy += gravity;
var speed = Math.sqrt(this.dx * this.dx + this.dy * this.dy);
var x = this.x + this.dx;
var y = this.y + this.dy;
if(y > canvas.height - this.r){
y = (canvas.height - this.r) - (y - (canvas.height - this.r));
this.dy = -this.dy;
}
if(y < this.r){
y = this.r - (y - this.r);
this.dy = -this.dy;
}
if(x > canvas.width - this.r){
x = (canvas.width - this.r) - (x - (canvas.width - this.r));
this.dx = -this.dx;
}
if(x < this.r){
x = this.r - (x - this.r);
this.dx = -this.dx;
}
this.x = x;
this.y = y;
if(speed > this.maxSpeed){ // if over speed then slow the ball down gradualy
var reduceSpeed = this.maxSpeed + (speed-this.maxSpeed) * 0.9; // reduce speed if over max speed
this.dx = (this.dx / speed) * reduceSpeed;
this.dy = (this.dy / speed) * reduceSpeed;
}
}
}
const ballShadow = { // this is used to do calcs that may be dumped
r : 50,
x : 50,
y : 50,
dx : 0.2,
dy : 0.2,
}
// Creates the bat
const bat = {
x : 100,
y : 250,
dx : 0,
dy : 0,
width : 140,
height : 10,
style : {
lineWidth : 2,
strokeStyle : "black",
},
draw(ctx){
setStyle(ctx,this.style);
ctx.strokeRect(this.x - this.width / 2,this.y - this.height / 2, this.width, this.height);
},
update(){
this.dx = mouse.x - this.x;
this.dy = mouse.y - this.y;
var x = this.x + this.dx;
var y = this.y + this.dy;
x < this.width / 2 && (x = this.width / 2);
y < this.height / 2 && (y = this.height / 2);
x > canvas.width - this.width / 2 && (x = canvas.width - this.width / 2);
y > canvas.height - this.height / 2 && (y = canvas.height - this.height / 2);
this.dx = x - this.x;
this.dy = y - this.y;
this.x = x;
this.y = y;
}
}
//=============================================================================
// THE FUNCTION THAT DOES THE BALL BAT sim.
// the ball and bat are at new position
function doBatBall(bat,ball){
var mirrorX = 1;
var mirrorY = 1;
const s = ballShadow; // alias
s.x = ball.x;
s.y = ball.y;
s.dx = ball.dx;
s.dy = ball.dy;
s.x -= s.dx;
s.y -= s.dy;
// get the bat half width height
const batW2 = bat.width / 2;
const batH2 = bat.height / 2;
// and bat size plus radius of ball
var batH = batH2 + ball.r;
var batW = batW2 + ball.r;
// set ball position relative to bats last pos
s.x -= bat.x;
s.y -= bat.y;
// set ball delta relative to bat
s.dx -= bat.dx;
s.dy -= bat.dy;
// mirror x and or y if needed
if(s.x < 0){
mirrorX = -1;
s.x = -s.x;
s.dx = -s.dx;
}
if(s.y < 0){
mirrorY = -1;
s.y = -s.y;
s.dy = -s.dy;
}
// bat now only has a bottom, right sides and bottom right corner
var distY = (batH - s.y); // distance from bottom
var distX = (batW - s.x); // distance from right
if(s.dx > 0 && s.dy > 0){ return }// ball moving away so no hit
var ballSpeed = Math.sqrt(s.dx * s.dx + s.dy * s.dy); // get ball speed relative to bat
// get x location of intercept for bottom of bat
var bottomX = s.x +(s.dx / s.dy) * distY;
// get y location of intercept for right of bat
var rightY = s.y +(s.dy / s.dx) * distX;
// get distance to bottom and right intercepts
var distB = Math.hypot(bottomX - s.x, batH - s.y);
var distR = Math.hypot(batW - s.x, rightY - s.y);
var hit = false;
if(s.dy < 0 && bottomX <= batW2 && distB <= ballSpeed && distB < distR){ // if hit is on bottom and bottom hit is closest
hit = true;
s.y = batH - s.dy * ((ballSpeed - distB) / ballSpeed);
s.dy = -s.dy;
}
if(! hit && s.dx < 0 && rightY <= batH2 && distR <= ballSpeed && distR <= distB){ // if hit is on right and right hit is closest
hit = true;
s.x = batW - s.dx * ((ballSpeed - distR) / ballSpeed);;
s.dx = -s.dx;
}
if(!hit){ // if no hit may have intercepted the corner.
// find the distance that the corner is from the line segment from the balls pos to the next pos
const u = ((batW2 - s.x) * s.dx + (batH2 - s.y) * s.dy)/(ballSpeed * ballSpeed);
// get the closest point on the line to the corner
var cpx = s.x + s.dx * u;
var cpy = s.y + s.dy * u;
// get ball radius squared
const radSqr = ball.r * ball.r;
// get the distance of that point from the corner squared
const dist = (cpx - batW2) * (cpx - batW2) + (cpy - batH2) * (cpy - batH2);
// is that distance greater than ball radius
if(dist > radSqr){ return } // no hit
// solves the triangle from center to closest point on balls trajectory
var d = Math.sqrt(radSqr - dist) / ballSpeed;
// intercept point is closest to line start
cpx -= s.dx * d;
cpy -= s.dy * d;
// get the distance from the ball current pos to the intercept point
d = Math.hypot(cpx - s.x,cpy - s.y);
// is the distance greater than the ball speed then its a miss
if(d > ballSpeed){ return } // no hit return
s.x = cpx; // position of contact
s.y = cpy;
// find the normalised tangent at intercept point
const ty = (cpx - batW2) / ball.r;
const tx = -(cpy - batH2) / ball.r;
// calculate the reflection vector
const bsx = s.dx / ballSpeed; // normalise ball speed
const bsy = s.dy / ballSpeed;
const dot = (bsx * tx + bsy * ty) * 2;
// get the distance the ball travels past the intercept
d = ballSpeed - d;
// the reflected vector is the balls new delta (this delta is normalised)
s.dx = (tx * dot - bsx);
s.dy = (ty * dot - bsy);
// move the ball the remaining distance away from corner
s.x += s.dx * d;
s.y += s.dy * d;
// set the ball delta to the balls speed
s.dx *= ballSpeed;
s.dy *= ballSpeed;
hit = true;
}
// if the ball hit the bat restore absolute position
if(hit){
// reverse mirror
s.x *= mirrorX;
s.dx *= mirrorX;
s.y *= mirrorY;
s.dy *= mirrorY;
// remove bat relative position
s.x += bat.x;
s.y += bat.y;
// remove bat relative delta
s.dx += bat.dx;
s.dy += bat.dy;
// set the balls new position and delta
ball.x = s.x;
ball.y = s.y;
ball.dx = s.dx;
ball.dy = s.dy;
}
}
// main update function
function update(timer){
if(w !== innerWidth || h !== innerHeight){
cw = (w = canvas.width = innerWidth) / 2;
ch = (h = canvas.height = innerHeight) / 2;
}
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0,0,w,h);
// move bat and ball
bat.update();
ball.update();
// check for bal bat contact and change ball position and trajectory if needed
doBatBall(bat,ball);
// draw ball and bat
bat.draw(ctx);
ball.draw(ctx);
requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas { position : absolute; top : 0px; left : 0px; }
body {font-family : arial; }
Use the mouse to move the bat and hit the ball.
<canvas id="canvas"></canvas>
Flaws with this method.
It is possible to trap the ball with the bat such that there is no valid solution, such as pressing the ball down onto the bottom of the screen. At some point the balls diameter is greater than the space between the wall and the bat. When this happens the solution will fail and the ball will pass through the bat.
In the demo there is every effort made to not loss energy, but over time floating point errors will accumulate, this can lead to a loss of energy if the sim is run without some input.
As the bat has infinite momentum it is easy to transfer a lot of energy to the ball, to prevent the ball accumulating to much momentum I have added a max speed to the ball. if the ball moves quicker than the max speed it is gradually slowed down until at or under the max speed.
On occasion if you move the bat away from the ball at the same speed, the extra acceleration due to gravity can result in the ball not being pushed away from the bat correctly.
Correction of an idea shared above, with adjusting velocity after collision using tangental velocity.
bounciness - constant defined to represent lost force after collision
nv = vector # normalized vector from center of cricle to collision point (normal)
pv = [-vector[1], vector[0]] # normalized vector perpendicular to nv (tangental)
n = dot_product(nv, circle.vel) # normal vector length
t = dot_product(pv, circle.vel) # tangental_vector length
new_v = sum_vectors(multiply_vector(t*bounciness, pv), multiply_vector(-n*self.bounciness, nv)) # new velocity vector
circle.velocity = new_v
i've a canvas dom element inside a div #content with transform rotateX(23deg) and #view with perspective 990px
<div id="view">
<div id="content">
<canvas></canvas>
</div>
</div>
if i draw a point (300,300) inside canvas, the projected coordinates are different (350, 250).
The real problem is when an object drawn in a canvas is interactive (click o drag and drop), the hit area is translated.
Which equation i've to use? Some kind of matrix?
Thanks for your support.
This is something I am dealing with now. Lets start out with something simple. Let's say your canvas is right up against the top left corner. If you click the mouse and make an arc on that spot it will be good.
canvasDOMObject.onmouseclick = (e) => {
const x = e.clientX;
const y = e.clientY;
}
If your canvas origin is not at client origin you would need to do something like this:
const rect = canvasDOMObject.getBoundingRect();
const x = e.clientX - rect.x;
const y = e.clientY - rect.y;
If you apply some pan, adding pan, when drawing stuff you need to un-pan it, pre-subtract the pan, when capturing the mouse point:
const panX = 30;
const panY = 40;
const rect = canvasDOMObject.getBoundingRect();
const x = e.clientX - rect.x - panX;
const y = e.clientY - rect.y - panY;
...
ctx.save();
ctx.translate(panX, panY);
ctx.beginPath();
ctx.strokeArc(x, y);
ctx.restore();
If you apply, for instance, a scale when you draw it, you would need to un-scale it when capturing the mouse point:
const panX = 30;
const panY = 40;
const scale = 1.5;
const rect = canvasDOMObject.getBoundingRect();
const x = (e.clientX - rect.x - panX) / scale;
const y = (e.clientY - rect.y - panY) / scale;
...
ctx.save();
ctx.translate(panX, panY);
ctx.scale(scale);
ctx.beginPath();
ctx.strokeArc(x, y);
ctx.restore();
The rotation I have not figured out yet but I'm getting there.
Alternative solution.
One way to solve the problem is to trace the ray from the mouse into the page and finding the point on the canvas where that ray intercepts.
You will need to transform the x and y axis of the canvas to match its transform. You will also have to project the ray from the desired point to the perspective point. (defined by x,y,z where z is perspective CSS value)
Note: I could not find much info about CSS perspective math and how it is implemented so it is just guess work from me.
There is a lot of math involved and i had to build a quick 3dpoint object to manage it all. I will warn you that it is not well designed (I dont have the time to inline it where needed) and will incur a heavy GC toll. You should rewrite the ray intercept and remove all the point clone calls and reuse points rather than create new ones each time you need them.
There are a few short cuts. The ray / face intercept assumes that the 3 points defining the face are the actual x and y axis but it does not check that this is so. If you have the wrong axis you will not get the correct pixel coordinate. Also the returned coordinate is relative to the point face.p1 (0,0) and is in the range 0-1 where 0 <= x <= 1 and 0 <= y <= 1 are points on the canvas.
Make sure the canvas resolution matches the display size. If not you will need to scale the axis and the results to fit.
DEMO
The demo project a set of points creating a cross through the center of the canvas. You will notice the radius of the projected circle will change depending on distance from the camera.
Note code is in ES6 and requires Babel to run on legacy browsers.
var divCont = document.createElement("div");
var canvas = document.createElement("canvas");
canvas.width = 400;
canvas.height = 400;
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
var ctx = canvas.getContext("2d");
// perspectiveOrigin
var px = cw; // canvas center
var py = 50; //
// perspective
var pd = 700;
var mat;
divCont.style.perspectiveOrigin = px + "px "+py+"px";
divCont.style.perspective = pd + "px";
divCont.style.transformStyle = "preserve-3d";
divCont.style.margin = "10px";
divCont.style.border = "1px black solid";
divCont.style.width = (canvas.width+8) + "px";
divCont.style.height = (canvas.height+8) + "px";
divCont.appendChild(canvas);
document.body.appendChild(divCont);
function getMatrix(){ // get canvas matrix
if(mat === undefined){
mat = new DOMMatrix().setMatrixValue(canvas.style.transform);
}else{
mat.setMatrixValue(canvas.style.transform);
}
}
function getPoint(x,y){ // get point on canvas
var ww = canvas.width;
var hh = canvas.height;
var face = createFace(
createPoint(mat.transformPoint(new DOMPoint(-ww / 2, -hh / 2))),
createPoint(mat.transformPoint(new DOMPoint(ww / 2, -hh / 2))),
createPoint(mat.transformPoint(new DOMPoint(-ww / 2, hh / 2)))
);
var ray = createRay(
createPoint(x - ww / 2, y - hh / 2, 0),
createPoint(px - ww / 2, py - hh / 2, pd)
);
return intersectCoord3DRayFace(ray, face);
}
// draw point projected onto the canvas
function drawPoint(x,y){
var p = getPoint(x,y);
if(p !== undefined){
p.x *= canvas.width;
p.y *= canvas.height;
ctx.beginPath();
ctx.arc(p.x,p.y,8,0,Math.PI * 2);
ctx.fill();
}
}
// main update function
function update(timer){
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.fillStyle = "green";
ctx.fillRect(0,0,w,h);
ctx.lineWidth = 10;
ctx.strokeRect(0,0,w,h);
canvas.style.transform = "rotateX("+timer/100+"deg)" + " rotateY("+timer/50+"deg)";
getMatrix();
ctx.fillStyle = "gold";
drawPoint(cw,ch);
for(var i = -200; i <= 200; i += 40){
drawPoint(cw + i,ch);
drawPoint(cw ,ch + i);
}
requestAnimationFrame(update);
}
requestAnimationFrame(update);
// Math functions to find x,y pos on plain.
// Warning this code is not built for SPEED and will incure a lot of GC hits
const small = 1e-6;
var pointFunctions = {
add(p){
this.x += p.x;
this.y += p.y;
this.z += p.z;
return this;
},
sub(p){
this.x -= p.x;
this.y -= p.y;
this.z -= p.z;
return this;
},
mul(mag){
this.x *= mag;
this.y *= mag;
this.z *= mag;
return this;
},
mag(){ // get length
return Math.hypot(this.x,this.y,this.z);
},
cross(p){
var p1 = this.clone();
p1.x = this.y * p.z - this.z * p.y;
p1.y = this.z * p.x - this.x * p.z;
p1.z = this.x * p.y - this.y * p.x;
return p1;
},
dot(p){
return this.x * p.x + this.y * p.y + this.z * p.z;
},
isZero(){
return Math.abs(this.x) < small && Math.abs(this.y) < small && Math.abs(this.z) < small;
},
clone(){
return Object.assign({
x : this.x,
y : this.y,
z : this.z,
},pointFunctions);
}
}
function createPoint(x,y,z){
if(y === undefined){ // quick add overloaded for DOMPoint
y = x.y;
z = x.z;
x = x.x;
}
return Object.assign({
x, y, z,
}, pointFunctions);
}
function createRay(p1, p2){
return { p1, p2 };
}
function createFace(p1, p2, p3){
return { p1,p2, p3 };
}
// Returns the x,y coord of ray intercepting face
// ray is defined by two 3D points and is infinite in length
// face is 3 points on the intereceptin plane
// For correct intercept point face p1-p2 should be at 90deg to p1-p3 (x, and y Axis)
// returns unit coordinates x,y on the face with the origin at face.p1
// If there is no solution then returns undefined
function intersectCoord3DRayFace(ray, face ){
var u = face.p2.clone().sub(face.p1);
var v = face.p3.clone().sub(face.p1);
var n = u.cross(v);
if(n.isZero()){
return; // return undefined
}
var vr = ray.p2.clone().sub(ray.p1);
var b = n.dot(vr);
if (Math.abs(b) < small) { // ray is parallel face
return; // no intercept return undefined
}
var w = ray.p1.clone().sub(face.p1);
var a = -n.dot(w);
var uDist = a / b;
var intercept = ray.p1.clone().add(vr.mul(uDist)); // intersect point
var uu = u.dot(u);
var uv = u.dot(v);
var vv = v.dot(v);
var dot = uv * uv - uu * vv;
w = intercept.clone().sub(face.p1);
var wu = w.dot(u);
var wv = w.dot(v);
var x = (uv * wv - vv * wu) / dot;
var y = (uv * wu - uu * wv) / dot;
return {x,y};
}
Currently im trying to make a canvas zoom and also draw again the whole canvas
var painter = document.getElementById('painter');
var cp = painter.getContext('2d');
var scale = 1;
var originx = 0;
var savedData = [];
var originy = 0;
var zoom = 0;
$('#painter').on('mousewheel', function(event) {
var imgData = cp.getImageData(0, 0, painter.width, painter.height);
savedData.push(imgData);
var mousex = event.clientX - painter.offsetLeft;
var mousey = event.clientY - painter.offsetTop;
var wheel = event.deltaY;//n or -n
var zoom = 1 + wheel/2;
cp.translate(
originx,
originy
);
cp.scale(zoom,zoom);
cp.translate(
-( mousex / scale + originx - mousex / ( scale * zoom ) ),
-( mousey / scale + originy - mousey / ( scale * zoom ) )
);
if (savedData.length > 0) {
var imgData = savedData.pop();
cp.putImageData(imgData, 0, 0);
}
originx = ( mousex / scale + originx - mousex / ( scale * zoom ) );
originy = ( mousey / scale + originy - mousey / ( scale * zoom ) );
scale *= zoom;
});
Thing is it does not work. Canvas is never updated... the variables seems to be fine and since I need to draw again the whole canvas Im saving all the data into an array
getImageData & context.putImageData are very expensive operations.
Instead, you could store your original content in a second in-memory-only canvas created with
var secondCanvas = document.createElement('canvas')
var secondContext = secondCanvas.getContext('2d');
secondCanvas.width=mainCanvas.width;
secondCanvas.height=mainCanvas.height;
secondContext.drawImage(mainCanvas,0,0);
Then use that second canvas as an image source for your scaled drawing on the main canvas:
mainContext.drawImage(secondCanvas,x,y);
Once you have your content cached to a second canvas, you can use this Stackoverflow answer to zoom with the mousewheel:
Zoom and pan in animated HTML5 canvas