I'm performing drawing operations on canvas. The best way to calculate cursor position relative to canvase top left corner is, in my opinion, usage of .getBoundingClientRect():
HTMLCanvasElement.prototype.relativeCoords = function(event) {
var x,y;
//This is the current screen rectangle of canvas
var rect = this.getBoundingClientRect();
//Recalculate mouse offsets to relative offsets
x = event.clientX - rect.x;
y = event.clientY - rect.y;
//Debug
console.log("x(",x,") = event.clientX(",event.clientX,") - rect.x(",rect.x,")");
//Return as array
return [x,y];
}
I see nothing wrong with this code and it works in firefox. Test it.
In google chrome however, my debug line prints this:
x(NaN) = event.clientX(166) - rect.x(undefined)
What am I doing wrong? Is this not according to the specifications?
Edit: seems my code follows W3C:
From the specs:
getBoundingClientRect()
The getBoundingClientRect() method, when invoked, must return the
result of the following algorithm:
Let list be the result of invoking getClientRects() on the same element this method was invoked on.
If the list is empty return a DOMRect object whose x, y, width and height members are zero.
Otherwise, return a DOMRect object describing the smallest rectangle that includes the first rectangle in list and all of the
remaining rectangles of which the height or width is not zero.
DOMRect
interface DOMRect : DOMRectReadOnly {
inherit attribute unrestricted double x;
inherit attribute unrestricted double y;
inherit attribute unrestricted double width;
inherit attribute unrestricted double height;
};
The object returned by getBoundingClientRect() may have x and y properties in some browsers, but not all. It always has left, top, right, and bottom properties.
I recommend using the MDN docs instead of any W3C specs when you want to know what browsers actually implement. See the MDN docs for getBoundingClientRect() for more accurate information on this function.
So all you need to do is change your rect.x and rect.y to rect.left and rect.top.
Related
I have simply canvas code which draw rect on the canvas
var x=document.getElementById("canvas");
var ctx=x.getContext("2d");
ctx.rect(20,20,150,100);
ctx.stroke();
is it possible to add eventListener on said rect? For example, if I click on rect, it will turn red.
Regions
Depending on how well you want to support various and older browsers, there is addHitRegion() that you can use by enabling it through flags in Firefox and Chrome (at the moment of this being written):
Firefox: about:config -> search "hitregions" and set to true
Chrome: chrome://flags/ -> Enable experimental canvas features
This is the only technique that integrates directly with the event system. I would not recommend it for production quite yet though, and AFAIK there is not a polyfill for it either - but to show how easy it is to use:
var x=document.getElementById("canvas");
var ctx=x.getContext("2d");
ctx.rect(20,20,150,100);
ctx.addHitRegion({id: "demo"}); // enable in flags in Chrome/Firefox
ctx.stroke();
x.addEventListener("click", function(e) {
if (e.region && e.region === "demo") alert("Hit!");
})
<canvas id="canvas"></canvas>
Path: isPointInPath
The other techniques require one to manually implement a mechanism for hit-detection. One is by using isPointInPath(). You simply rebuild the paths you want to test, one by one, then run your (adjusted) x/y mouse coordinate against it:
var x=document.getElementById("canvas");
var ctx=x.getContext("2d");
generatePath();
ctx.stroke();
x.addEventListener("click", function(e) {
var r = this.getBoundingClientRect(),
x = e.clientX - r.left,
y = e.clientY - r.top;
// normally you would loop through your paths:
generatePath();
if (ctx.isPointInPath(x, y)) alert("Hit!");
})
function generatePath() {
ctx.beginPath(); // reset path
ctx.rect(20,20,150,100); // add region to draw/test
}
<canvas id="canvas"></canvas>
Path: Path2D objects
For the latter example there is also the new Path2D objects which can hold a path on their own - the advantage here is that you don't need to rebuild the paths, just pass in the path object with x/y to the isPointInPath() method.
The problem is that Path2D is not supported in all browsers yet, but there is this polyfill that will fix that for you,
var x=document.getElementById("canvas");
var ctx=x.getContext("2d");
var path1 = new Path2D();
path1.rect(20,20,150,100); // add rect to path object
ctx.stroke(path1);
x.addEventListener("click", function(e) {
var r = this.getBoundingClientRect(),
x = e.clientX - r.left,
y = e.clientY - r.top;
// normally you would loop through your paths objects:
if (ctx.isPointInPath(path1, x, y)) alert("Hit!");
})
<canvas id="canvas"></canvas>
Manually check boundary
And of course, there is the old technique of using manual boundary checks. This will work in all browsers. Here the advisable thing to do is to create objects that holds the bounds and can also be used to render it. This typically limits you to rectangular areas - more complex shapes will require more complex algorithms (such as the isPointInPath() embeds).
var x=document.getElementById("canvas");
var ctx=x.getContext("2d");
ctx.rect(20,20,150,100);
ctx.stroke();
x.addEventListener("click", function(e) {
var r = this.getBoundingClientRect(),
x = e.clientX - r.left,
y = e.clientY - r.top;
// normally you would loop through your region objects:
if (x >= 20 && x < 20+150 && y >= 20 && y < 20+100) alert("Hit!");
})
<canvas id="canvas"></canvas>
Shapes and paths are drawn to the canvas as side-effects, so there is no element to add an event listener to; you could, however, add an event listener to the entire canvas or to an element that shares a location with the canvas, and when it is clicked then redraw the canvas with the rectangle, but red (or anything else changed). (make sure to clear the canvas before redrawing it with the .clearRect() method).
If you draw something to a canvas, the shape that is drawn is not a javascript object, but rather changes the particular state that the canvas is in. Therefore, you cannot attach an event listener to it, and should instead attach the event to the canvas itself.
Your javascript could then check the co-ordinates of the click, and find whether or not it is inside the rectangle. Bear in mind that if you draw something on top of the rectangle or shape, the code will have to be adjusted to check the new area formed. You might also find it difficult to check the area if it is not a rectangle, but it will still be possible.
If you want to redraw the rectangle as red, you should repaint the canvas, changing the colour of the new rectangle that you redraw (the rectangle is not an object, so you cannot change the colour directly). This would also involve repainting all the other shapes on the canvas.
I'm trying to get an image to flow horizontally in a sinusoidal fashion, and repeat seamlessly when it gets to the end of its own width in relation to its canvas size.
So far, I've got the image repeating and waving, but there is a significant jump when the x axis needs to be reset.
I think the problem is here:
if (x > canvasX) {
console.log('reset!!');
x = canvasX-imgW;
y = sineY(x);
}
//draw aditional image
if (x > (canvasX-imgW)) {
var ax = x-imgW+dx;
y = sineY(ax);
ctx.drawImage(img,ax,y,imgW,imgH);
}
Ultimately, what happens is that the sineY of the reset x value is about 19 degrees off of what it should be at the end of its regular period where the x value is highest. However, I can't really figure out how to adjust the bounds to make the movement seamless through the multiple periods.
Here's the fiddle: http://jsfiddle.net/3L7Dp/
The period variable needs to be normalized based on the total distance x will move.
In this case x will go image.width so period must be:
var period = x / imgW; //period must be a value in the range [0.0, 1.0]
This should give an usable value for cycling the image.
Modified fiddle
Hope this helps!
One way is to declare an offset by which x will be adjusted, such as var xOffset = 0. When calculating the sine, use x + xOffset. Every time you do x -= imgW, update the offset based on the current offset and the image width, so that the sin at the new position will equal the sin at the current position.
Doing this will allow you to have any period, even one unrelated to the width of your image.
I made my own version of your page with many simplifications, you can see it in this JsFiddle. The sine wave is seamless. My implementation also supports images much narrower than the canvas--they will be repeated all the way across, always filling the canvas (try img.width = 100 in my JsFiddle to see what I mean). In my function, since I based the period on a certain number of x-pixels, my xOffset recalculation is simplified and I can simply use modulus to calculate the new offset after subtracting from x.
Some style considerations I would like to suggest are:
Use more consistent variable names (such as context vs. ctx--if both are truly needed, give them prefixes such as baseContext, canvasContext so that context is consistent throughout the code).
Name variables closer to what they represent (for example, canvasX is not a good variable name for canvas.Width.
Don't be afraid of slightly longer variable names. imgW is less clear than imageWidth. W doesn't always mean width.
Put spaces after commas and the word function, and around operators.
Using parameter x in your sineY function is confusing as x is already declared outside.
Parameterizing your animation function is fine, but just as good is to wrap the entire script in a SEAF (self-executing anonymous function), as that properly gives all the variables a scope (keeping them out of global scope), thus simplifying your code by not having to pass around the variables.
I am having some trouble right now with the event.pageX and event.pageY functions. I am trying to use them to grab the location of the mouse on a click event relative to my canvas element. I get the position relative to my canvas with these lines of code:
var mx = e.pageX - this.xPos;
var my = e.pageY - this.yPos;
this.xPos and this.yPos are obtained by using jQuery's offset function:
this.xPos = $("#timelines").offset().left;
this.yPos = $("#timelines").offset().top;
When I log either of these separately (e.pageX/Y or this.x/yPos), they give me the correct numbers. However, when I log the result of mx and my after subtracting, it says that it is not a number (NaN). I have looked around online but haven't found anything that could explain why this is happening. I have tried explicitly casting them using Number(event.pageX/Y) and/or Number(this.x/yPos) but that didn't work. Does anyone have any suggestions?
Try parseFloat() around whatever base vars are non-numeric. That turns '1' (a float) into 1 (numeric)
Not familiar with jQuery, but ensure there is no 'px' at the end of the offset function?
Finally try and trigger one alert (or console.log if you must) with ALL variables that are giving you trouble to find and isolate the problem.
Nvm, i figured out this issue. the value of this was changing in the event listener and not accessing the correct xPos/yPos
Fiddle!!
I have set this fiddle up to show what parameters are going into the setViewBox() function. The thing is, the way it is working makes no sense to me.
Why does setViewBox(0, 0, 625, 625) result in a larger box / further zoomed in than setViewBox(0, 0, 1250, 1250)?
Also, why does setViewBox(-100, 0, 625, 625); adjust the image to the right (I would expect it to go left because of the negative x value)?
This is what the docs say about setViewBox(x, y, w, h):
parameters:
x - new x position, default is 0
y - new y position, default is 0
w - new width of the canvas
h - new height of the canvas
Also, I am trying to figure out the relationship between the first(x) / third(w) and the second(y) / fourth(h) parameters such that the box zooms in and out from the middle, instead of expanding from the top left corner, if anyone has any suggestions.
The answer is that the parameters passed to setViewBox are basically telling the size of the window you're looking through at the object. If the window is smaller, you see less of the object, or the object zoomed in. If you move the window left, you'll see the object move right, relative to the window. The key is to think of it as a window you're looking through, not the size of the object itself.
this is an example showing how to calculate the setViewBox values, you have to include jquery (to get SVG cocntainer X and Y ) :
var original_width = 777;
var original_height = 667;
var zoom_width = map_width*100/original_width/100;
var zoom_height = map_height*100/original_height/100;
if(zoom_width<=zoom_height)
zoom = zoom_width;
else
zoom = zoom_height;
rsr.setViewBox($("#"+map_name).offset().left, $("#"+map_name).offset().top, (map_width/zoom), (map_height/zoom));
I know that javascript doesn't have pointers in terms of a variable referring to a place in memory but what I have is a number of variables which are subject to change and dependent on each other.
For example:
Center (x,y) = (offsetLeft + width/scale , offsetTop + height/scale)
As of now I have rewritten the equation in terms of each individual variable and after any changes I call the appropriate update function.
For example:
If scale changes, then then the center, height, and width stay the same. So I call
updateoffset() {
offsetLeft = centerx - width/scale;
offsetTop = centery - height/scale;
}
Is this the easiest way to update each of these variables when any of them changes?
I read your question in two different ways, so two answers:
Updating calculated values when other values change
The two usual ways are: 1. To require that the values only be changed via "setter" functions, and then you use that as an opportunity to recalcuate the things that changed, or 2. To require that you use "getter" functions to get the calculated value, which you calculate on the fly (or if that's expensive, you retrieve from a cached calculation).
Returning multiple values from a function
If you're looking for a way of returning multiple values from a single function, you can do that easily by returning an object. Example:
// Definition:
function center(offsetLeft, offsetTop, width, height, scale) {
return {
x: offsetLeft + width/scale,
y: offsetTop + height/scale
};
}
// Use:
var pos = center(100, 120, 10, 20, 2);
// pos.x is now 105
// pos.y is now 130