I am writing an application that draws rectangles on a HTML canvas using the fillRect function. I currently track the movement of the mouse and detect when the mouse pointer hovers over a rectangle to highlight it:
This is how I am currently detecting collision which works great.
//boxes2 is my array of rectangles
var l = boxes2.length;
for (var i = l - 1; i >= 0; i--) {
if (mouseX >= boxes2[i].x && mouseX <= (boxes2[i].x + boxes2[i].w ) &&
mouseY >= boxes2[i].y && mouseY <= (boxes2[i].y + boxes2[i].h )) {
selectedBoxNum = i;
}
}
My problem is that this hover detection no longer works well after zooming in/out as the actual bounds of the rectangles desync from their values in my rectangle array.
var currentZoomValue = 1;
function myOnMouseWheel(event) {
event.preventDefault();
// Normalize wheel to +1 or -1.
var wheel = event.wheelDelta / 120;
if (wheel == 1) {
zoom = 1.1;
}
else {
zoom = .9;
}
currentZoomValue = currentZoomValue * zoom
canvas.style.transform = "scale(" + currentZoomValue + ")";
}
What I have tried:
Scaling the values in the array as I zoom in/out so that the rectangle bounds will stay in sync
This will not work for me because the scale function is stretching my canvas to make the rectangles look bigger. If I also actually make them bigger, they will be doubly enlarged and outpace the zoom of my canvas background.
Compensating my hover detection based upon my current zoom level
I have tried something like:
if (mouseX >= boxes2[i].x && mouseX <= (boxes2[i].x + (boxes2[i].w * currentZoomValue) ) &&
mouseY >= boxes2[i].y && mouseY <= (boxes2[i].y + (boxes2[i].h * currentZoomValue) )) {
selectedBoxNum = i;
}
My attempts at this do not work because while the rectangle height and width do scale in an easily predictable way, the x,y coordinates do not. When zooming in, the rectangles will radiate out from the center so some rectangles will gain x value and other lose based upon their position. I also considered maintaining a second rectangle array that I could use just for hover detection but decided against it for this reason.
A good solution would be to actually scale the rectangle's sizes to give the illusion of zooming, but the rectangles positions on the background image is important, and this technique will not affect the background.
Since there is no standard way of knowing page zoom level, I would suggest catching the click event with an absolutely-positioned div.
You can get the offset of your canvas element with the getBoundingClientRect() method.
Then, the code would look something like this:
boxes2.forEach(function(box, i) {
cx.fillRect(box.x, box.y, box.w, box.h);
/* We create an empty div */
var div = document.createElement("div");
/* We get the position of the canvas */
var rect = canvas.getBoundingClientRect();
div.style.position = "absolute";
div.style.left = (rect.left + box.x) + "px"; //Don't forget the pixels!!!
div.style.top = (rect.top + box.y) + "px";
div.style.width = box.w + "px";
div.style.height = box.h + "px";
/* For demonstration purposes we display a border */
div.style.border = "1px dashed black"
div.onclick = function() {/* Your event handler */}
document.body.appendChild(div);
});
Here's a live demonstration. At least in my browser, regions stay consistent even if I zoom in and out the page.
I'm currently working on this script for "tooltips" on a website. I'm finding that the code I currently have will get the image height for my first tooltip image on the page ('pop1') but it ignores the rest (they come out as null).
What's the most effective way to get all the tooltip image heights, and use them every time the user scrolls over the tooltip image?
Another issue, if anyone is able to figure this one out - is that on my FULL webpage (many more divs, rows, columns, etc.) the script begins to break because clientX and clientY are being affected by the various divs and page elements.
I'd like to be able to set clientX and clientY to the exact (x, y) coordinates that the user's mouse is at, relative to the entire webpage, not relative to the page's child elements.
Thanks
Here's my JSFiddle: http://jsfiddle.net/tgs7px4f/18/
JS Code:
$('a.popper').hover(function (e) {
var target = '#' + ($(this).attr('data-popbox'));
$(target).show();
}, function () {
var target = '#' + ($(this).attr('data-popbox'));
if (!($("a.popper").hasClass("show"))) {
$(target).hide();
}
});
$('a.popper').mousemove(function (e) {
var target = '#' + ($(this).attr('data-popbox'));
// images vary in height!
// images are all 366px wide.
var imageWidth = 366;
var imageHeight = $(".popimg").height();
//alert('Image Height: ' + imageHeight);
//Offset tooltip:
//10px to the right of cursor
var imageX = e.clientX + 20;
//imageHeight up from cursor
var imageY = e.clientY - imageHeight - 20;
// Find bounds of current window, and if...
// Tooltip goes off right side:
if ((imageX + imageWidth) > $(window).width()) {
//Move tooltip left so it meets edge:
imageX = $(window).width() - imageWidth;
}
// Tooltip goes off top
if (imageY < 0) {
//Move tooltip down so it meets top:
imageY = 0;
}
$(target).css('top', imageY).css('left', imageX);
});
What's the most effective way to get all the tooltip image heights, and use them every time the user scrolls over the tooltip image?
First of all, I suppose you mean whenever a user does a mouseover on one of the elements? However, this seems to work and it cashes the height of the image directly on the element and uses it the next time a mouseover occurs:
$('a.popper').mousemove(function (e) {
var target = '#' + ($(this).attr('data-popbox'));
// images vary in height!
// images are all 366px wide.
var imageWidth = 366;
$target = $(target);
if (!$target.attr("height")) {
var img = $target.closest(".popbox").children("img");
var imageHeight = img.height();
$target.attr("height", imageHeight);
console.log("height attribute set");
} else {
var imageHeight = +($target.attr("height")) + 0;
console.log("cached height used");
}
console.log('Image Height: ', imageHeight);
//Offset tooltip:
//10px to the right of cursor
var imageX = e.clientX + 20;
//imageHeight up from cursor
var imageY = e.clientY - imageHeight - 20;
// Find bounds of current window, and if...
// Tooltip goes off right side:
if ((imageX + imageWidth) > $(window).width()) {
//Move tooltip left so it meets edge:
imageX = $(window).width() - imageWidth;
}
// Tooltip goes off top
if (imageY < 0) {
//Move tooltip down so it meets top:
imageY = 0;
}
$(target).css('top', imageY).css('left', imageX);
});
Obviously, you should remove all the console.log statements which are for testing purposes only.
jsFiddle
Regarding your second question, it's hard to say anything concrete without another jsFiddle or additional code.
In a UIWebView I need to access properties of the DOM element (from a SVG graph) when I longTap on it. To do so, I've added a UILongPressGestureRecognizer as following :
UILongPressGestureRecognizer* longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action: #selector(longPress:)];
[self.webView addGestureRecognizer: longPress];
When I longPress on the view, the handler get called from which I call a JS function :
- (void) longPress: (UIGestureRecognizer *) gesture {
CGPoint curCoords = [gesture locationInView:self.webView];
if (!CGPointEqualToPoint(curCoords, self.lastLongPress)) {
self.lastLongPress = curCoords;
[self.webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:#"longPress(%f, %f)", curCoords.x, curCoords.y]];
}
}
This is my JS handler :
function longPress(x, y) {
x = x + window.pageXOffset;
y = y + window.pageYOffset;
var element = svgDocument.elementFromPoint(x, y);
alert(element.localName + ' ' + x + ' ' + y + ' ' + window.innerWidth + ' ' + window.innerHeight);
}
However it appears that UIWebView coordinates are != from the DOM coordinates (where I click does not correspond to the localName shown in the alert). I've managed to figure out that there is +/- a 1.4 factor between UIWebView coordinates & JS ones (by clicking in the lower-right hand side of the screen and comparing these values with window.innder{Width,Height}.
My guess was that UIWebView might apply a default Zoom ratio initially, but I can't find what this values corresponds to.
Moreover, I'll also need a way to make this work when the user actually zooms/moves the page.
Does anybody knows what I'm doing wrong ?
Thanks,
Okay, I finally found what's the problem.
It was coming from the zoom ratio, and here is how I managed to fix it :
- (void) longPress: (UIGestureRecognizer *) gesture {
int displayWidth = [[self.webView stringByEvaluatingJavaScriptFromString:#"window.innerWidth"] intValue];
CGFloat scale = self.webView.frame.size.width / displayWidth;
CGPoint curCoords = [gesture locationInView:self.webView];
curCoords.x /= scale;
curCoords.y /= scale;
if (!CGPointEqualToPoint(curCoords, self.lastLongPress)) {
self.lastLongPress = curCoords;
[self.webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:#"longPress(%f, %f)", curCoords.x, curCoords.y]];
}
}
And the JS Handler :
function longPress(x, y) {
var e = svgDocument.elementFromPoint(x, y);
alert('Youhouu ' + e.localName);
}
It appears that it is not needed to add the pageOffset as now UIWebView automatically adds it (from iOS 5 I believe).
Cheers,
The extension I'm talking about is the Raphael-zpd: http://pohjoisespoo.net84.net/src/raphael-zpd.js
/* EDIT The script is added to a Raphael document with this command var zpd = new RaphaelZPD(paper, { zoom: true, pan: true, drag: false}); where paper is your canvas */
The script was originally released at the authors github http://www.github.com/somnidea which no longer exists.
What I wanted to do was run the mousewheel zoom out to the threshold as soon as the raphael is loaded. The zoomthreshold is set at the beginning of the script zoomThreshold: [-37, 20]. In the mousewheel scroll function it is compared to zoomCurrent which is by default 0 me.zoomCurrent = 0;
This is the whole mousewheel event part
me.handleMouseWheel = function(evt) {
if (!me.opts.zoom) return;
if (evt.preventDefault)
evt.preventDefault();
evt.returnValue = false;
var svgDoc = evt.target.ownerDocument;
var delta;
if (evt.wheelDelta)
delta = evt.wheelDelta / 3600; // Chrome/Safari
else
delta = evt.detail / -90; // Mozilla
if (delta > 0) {
if (me.opts.zoomThreshold)
if (me.opts.zoomThreshold[1] <= me.zoomCurrent) return;
me.zoomCurrent++;
} else {
if (me.opts.zoomThreshold)
if (me.opts.zoomThreshold[0] >= me.zoomCurrent) return;
me.zoomCurrent--;
}
var z = 1 + delta; // Zoom factor: 0.9/1.1
var g = svgDoc.getElementById("viewport"+me.id);
var p = me.getEventPoint(evt);
p = p.matrixTransform(g.getCTM().inverse());
// Compute new scale matrix in current mouse position
var k = me.root.createSVGMatrix().translate(p.x, p.y).scale(z).translate(-p.x, -p.y);
me.setCTM(g, g.getCTM().multiply(k));
if (!me.stateTf)
me.stateTf = g.getCTM().inverse();
me.stateTf = me.stateTf.multiply(k.inverse());
}
The reason I can't just draw a smaller SVG to begin with is that I'm using raster images as the background and need them to be higher resolution. I would still like to start at the furthest point I've set at the threshold. Is it possible for me to somehow use this script to do this? I'm naturally using it otherwise to handle mouse zoom/pan.
//EDIT
There is also this function at the end of the script, but so far I've been unable to work it.
Raphael.fn.ZPDPanTo = function(x, y) {
var me = this;
if (me.gelem.getCTM() == null) {
alert('failed');
return null;
}
var stateTf = me.gelem.getCTM().inverse();
var svg = document.getElementsByTagName("svg")[0];
if (!svg.createSVGPoint) alert("no svg");
var p = svg.createSVGPoint();
p.x = x;
p.y = y;
p = p.matrixTransform(stateTf);
var element = me.gelem;
var matrix = stateTf.inverse().translate(p.x, p.y);
var s = "matrix(" + matrix.a + "," + matrix.b + "," + matrix.c + "," + matrix.d + "," + matrix.e + "," + matrix.f + ")";
element.setAttribute("transform", s);
return me;
}
Seems like it's used for panning through the document through say click events so that a click would execute the function with the given coordinates. However, as said I've been unable to work it. I don't know how it's supposed to function. I tried paper.ZPDPanTo(100, 100); as well as just ZPDPanTo(100,100) but nothing happens.
You may also want to check out the working branch for Raphaël 2.0, which supposedly adds support for viewBox and transforms, see https://github.com/DmitryBaranovskiy/raphael/tree/2.0.
This doesn't answer your question fully, but it seems quite possible that Raphaël 2.0 will address your use-case.
If you're using pure svg then you can manipulate the zoom&pan positions via the SVG DOM properties currentTranslate and currentScale, see this example.
An example using RAPHAEL ZPD:
var paper = Raphael("container",800,760);
window.paper = paper;
zpd = new RaphaelZPD(paper, { zoom: true, pan: true, drag: false });
paper.circle(100,100, 50).attr({fill:randomRGB(),opacity:0.95});
paper.rect(100,100, 250, 300).attr({fill:randomRGB(),opacity:0.65});
paper.circle(200,100, 50).attr({fill:randomRGB(),opacity:0.95});
paper.circle(100,200, 50).attr({fill:randomRGB(),opacity:0.95});
http://jsfiddle.net/4PkRm/1/
You can easily see the problem on the first page here: http://m.vancouverislandlife.com/
Scroll down (slide up) and allow the content to leave the page, and it doesn't bounce back and is lost forever. However, on pages whose content does overflow the page and is therefore supposed to be scrollable, the scrolling works correctly (see Accomodations > b&b's and scroll down for an example of this).
I noticed that on my computer, the scrolling on the first page is always stuck at -899px. I can't find anybody else who's experienced this problem and no matter what I try, I just can't fix it! Help!
(It's not exactly urgent, however, as the target audience of iPhones and iPod Touches aren't affected by this since they have so little screen room.)
Okay, new problem. To solve the iScroll issue, I just created a custom script. However, it's not working correctly on the actual device. On desktop browsers, it works just fine. On mobile, it occasionally jumps back to the top and won't recognize some touches. This is probably because of the way I cancelled the default event and had to resort to a bit of a hack. How can I fix this? (Yup - simple problem for a +500 bounty. Not bad, huh?)
Here's the script, and the website is at the usual place:
function Scroller(content) {
function range(variable, min, max) {
if(variable < min) return min > max ? max : min;
if(variable > max) return max;
return variable;
}
function getFirstElementChild(element) {
element = element.firstChild;
while(element && element.nodeType !== 1) {
element = element.nextSibling;
}
return element;
}
var isScrolling = false;
var mouseY = 0;
var cScroll = 0;
var momentum = 0;
if("createTouch" in document) {
content.addEventListener('touchstart', function(evt) {
isScrolling = true;
mouseY = evt.pageY;
evt.preventDefault();
}, false);
content.addEventListener('touchmove', function(evt) {
if(isScrolling) {
evt = evt.touches[0];
var dY = evt.pageY - mouseY;
mouseY = evt.pageY;
cScroll += dY;
momentum = range(momentum + dY * Scroller.ACCELERATION, -Scroller.MAX_MOMENTUM, Scroller.MAX_MOMENTUM);
var firstElementChild = getFirstElementChild(content);
content.style.WebkitTransform = 'translateY(' + range(cScroll, -(firstElementChild.scrollHeight - content.offsetHeight), 0).toString() + 'px)';
}
}, false);
window.addEventListener('touchend', function(evt) {
isScrolling = false;
}, false);
} else {
content.addEventListener('mousedown', function(evt) {
isScrolling = true;
mouseY = evt.pageY;
}, false);
content.addEventListener('mousemove', function(evt) {
if(isScrolling) {
var dY = evt.pageY - mouseY;
mouseY = evt.pageY;
cScroll += dY;
momentum = range(momentum + dY * Scroller.ACCELERATION, -Scroller.MAX_MOMENTUM, Scroller.MAX_MOMENTUM);
var firstElementChild = getFirstElementChild(content);
content.style.WebkitTransform = 'translateY(' + range(cScroll, -(firstElementChild.scrollHeight - content.offsetHeight), 0).toString() + 'px)';
}
}, false);
window.addEventListener('mouseup', function(evt) {
isScrolling = false;
}, false);
}
function scrollToTop() {
cScroll = 0;
content.style.WebkitTransform = '';
}
function performAnimations() {
if(!isScrolling) {
var firstElementChild = getFirstElementChild(content);
cScroll = range(cScroll + momentum, -(firstElementChild.scrollHeight - content.offsetHeight), 0);
content.style.WebkitTransform = 'translateY(' + range(cScroll, -(firstElementChild.scrollHeight - content.offsetHeight), 0).toString() + 'px)';
momentum *= Scroller.FRICTION;
}
}
return {
scrollToTop: scrollToTop,
animationId: setInterval(performAnimations, 33)
}
}
Scroller.MAX_MOMENTUM = 100;
Scroller.ACCELERATION = 1;
Scroller.FRICTION = 0.8;
I think Andrew was on the right track with regards to setting the height of the #wrapper div. As he pointed out that,
that.maxScrollY = that.wrapperH - that.scrollerH;
Normally, this would work. But now that you've changed your #content to position: fixed, the wrapper element is no longer "wrapping" your content, thus that.wrapperH has a value of 0, things break.
Disclaimer: I did not go through the entire script so I may be wrong here
When manually setting a height to #wrapper, say 500px, it becomes,
that.maxScrollY = 500 - that.scrollerH;
The folly here is that when there's a lot of content and the window is small, that.scrollerH is relatively close in value to 500, say 700px. The difference of the two would be 200px, so you can only scroll 200 pixels, thus giving the appearance that it is frozen. This boils down to how you set that maxScrollY value.
Solution (for Chrome browser at least):
Since #wrapper effectively contains no content, we cannot use it in the calculations. Now we are left with the only thing that we can reliably get these dimensions from, #content. In this particular case, it appears that using the content element's scrollHeight yield what we want. This is most likely the one that has the expected behavior,
that.maxScrollY = that.scrollerH - that.scroller.scrollHeight;
scrollerH is the offsetHeight, which is roughly the height of what you see in the window. scroller.scrollHeight is the height that's considered scrollable. When the content does not exceed the length of the page, they are roughly equivalent to one another. That means no scroll. When there are a lot of content, the difference of these two values is the amount of scroll you need.
There is still a minor bug, and this looks like it's already there. When you have a lot of content, the last few elements are covered up by the bar when scrolled to the bottom. To fix this, you can set an offset such as,
that.maxScrollY = that.scrollerH - that.scroller.scrollHeight - 75;
The number 75 arbitrary. It's probably best if it's the height of the bar itself with 2 or 3 pixels for a bit of padding. Good luck!
Edit:
I forgot to mention last night, but here are the two sample pages that I used in trying to debug this problem.
Long page
Short page
This may be a CSS issue. In your stylesheet (mobile.css line 22), try removing position:fixed from #content.
That should allow the document to scroll normally (vertical scrollbar on a computer, "slideable" on a mobile browser).
Elements with position:fixed exit the normal flow of the document, their positioning is relative to the browser window. This is probably why you're having issues with scrolling. Fixed positioning is generally for elements which should always remain in the same place, even when the page is scrolled (ie. a notification bar "pinned" at the top of a page).
No definite solution, but more a direction I'd go for:
#wrapper and #content's overflow:hidden paired #content's postion:fixed and seem to be the cause of the issue.
If position: fixed is removed from #content, scrolling is possible but the "blank" divs are wrongly layered (tested in Firefox 5).
Your wrapper div seems to have a height of 0. So all the calculations are negative, setting it's height to the window height will correct the scroll issue. When I manually set the wrappers height via firebug and chromes debug bar the scroll functions as it should.
You #content div seems to have its size change on resize, probably a better idea to have the #wrapper div have its size change and then have #content inherit the size.
[Edit]
You don't believe me so codez, From iscroll-lite.js
refresh: function () {
var that = this,
offset;
that.wrapperW = that.wrapper.clientWidth;
that.wrapperH = that.wrapper.clientHeight;
that.scrollerW = that.scroller.offsetWidth;
that.scrollerH = that.scroller.offsetHeight;
that.maxScrollX = that.wrapperW - that.scrollerW;
that.maxScrollY = that.wrapperH - that.scrollerH;
In your page that translates to,
that.wrapperH = 0;
that.maxScrollY = -that.scrollerH
When a scroll finishes, this code gets called.
var that = this,
resetX = that.x >= 0 ? 0 : that.x < that.maxScrollX ? that.maxScrollX : that.x,
resetY = that.y >= 0 || that.maxScrollY > 0 ? 0 : that.y < that.maxScrollY ? that.maxScrollY : that.y;
...
that.scrollTo(resetX, resetY, time || 0);
See that that.maxScrollY > 0 ? ? If maxScrollY is negative then scrolling up will never bounce back.
I ended up just making my own, small script to handle the scrolling:
// A custom scroller
function range(variable, min, max) {
if(variable < min) return min > max ? max : min;
if(variable > max) return max;
return variable;
}
var isScrolling = false;
var mouseY = 0;
var cScroll = 0;
if("createTouch" in document) {
// TODO: Add for mobile browsers
} else {
content.addEventListener('mousedown', function(evt) {
isScrolling = true;
mouseY = evt.pageY;
}, false);
content.addEventListener('mousemove', function(evt) {
if(isScrolling) {
var dY = evt.pageY - mouseY;
mouseY = evt.pageY;
cScroll += dY;
var firstElementChild = content.getElementsByTagName("*")[0];
content.style.WebkitTransform = 'translateY(' + range(cScroll, -(firstElementChild.scrollHeight - content.offsetHeight), 0).toString() + 'px)';
}
}, false);
window.addEventListener('mouseup', function(evt) {
isScrolling = false;
}, false);
}
and modifying a few other parts. It does save a lot of download time, I suppose, also.
I'm still going to accept answers and award the bounty in 5 days, though.
Changed question warrants a new answer. I took a look at the code and I saw that you calculated the momentum on each step of the "move" function. This does not make sense because the momentum is used after the move has ended. What this meant was to capture the mouse position at the beginning, and then calculate the difference at the end. So I added two new variables,
var startTime;
var startY;
Inside the start event (mousedown/touchstart), I added,
startY = evt.pageY;
startTime = evt.timeStamp || Date.now();
Then I have the following for my end handler,
var duration = (evt.timeStamp || Date.now()) - startTime;
if (duration < 300) {
var dY = evt.pageY - startY;
momentum = range(momentum + dY * Scroller.ACCELERATION, -Scroller.MAX_MOMENTUM, Scroller.MAX_MOMENTUM);
} else {
momentum = 0;
}
I also removed the momentum calculation from inside of mousemove/touchmove. Doing this removed the jumping around behavior that I was seeing on my iPhone. I am seeing other unwanted behaviors as well (the whole window "scrolls"), but I'm guessing that you've been working to get rid of those so I didn't attempt.
Good luck. Here's a coded up page that I duplicated for my testing. I also took the liberty to refactor the code for this section to remove some duplicated code. It's under mobile3.js if you want to look at it.