Responsive Heatmaps - javascript

The Problem
I've encountered a slight problem where I've written some code where I've automated a script to track the users interactions with a UI, including mousemove & click events. If I didn't have to worry about making it responsive, then I could probably call it a day & ship the work that I've done already, however my brain is seeking some wizardry knowledge, I'm struggling to make it super responsive. Here's just a quick example of the kinda thing that I'm working on, it's nothing genius, if anything I it's mostly heatmap.js that's doing the heavy lifting. Currently I'm just seeing if I can do this as a proof of concept more than anything else...
The Code
So currently, I'm tracking the event.pageX & event.pageY values to store exactly where an event took place, I'm also storing the window.innerWidth & window.innerHeight values to try & work out some function that would allow me to offset the positions based on the size(s) of other devices.
E.g. If you look at the sample image above, that's perfect for a static page, but if I were to say make the page a little more narrow, you can see here that it's doesn't line up with the image above:
Anyway, without blabbering on too much, here's some sample code:
// A lot of other code...
var setupHeatMaps = function (pages, heatMaps) {
pages.forEach(function (page) {
page.addEventListener("click", function (event) {
heatMaps.push({ x: event.pageX, y: event.pageY, value: 10000 });
onStateChange();
});
// Don't collect ALL mouse movements, that'd be crazy, so collect
// every 1/10 mouse movements.
var counter = 0;
page.addEventListener("mousemove", function (event) {
if (counter === 10) {
heatMaps.push({ x: event.pageX, y: event.pageY, value: 20 });
onStateChange();
counter = 0;
} else {
counter ++;
}
});
});
};
// A lot of other code...
// Curried function so that it can be passed around without exposing the state...
var renderHeatMaps = function (heatMaps) {
return function () {
var max = heatMaps.length;
var points = heatMaps;
var parent = getParentElement();
var styleObj = window.getComputedStyle(parent);
var div = document.createElement("div");
var body = document.querySelector("body");
var background = document.createElement("div");
// This element needs to sit in front of the
// background element, hence the higher z-index value.
div.style.position = "absolute";
div.style.zIndex = 9;
div.style.left = "0px";
div.style.top = "-80px";
div.style.width = "100vw";
// Even though this element will sit behind the element
// that's created above, we will still want this element to
// sit in front of 99% of the content that's on the page.
background.style.position = "fixed";
background.style.top = "0px";
background.style.left = "0px";
background.style.height = "100vh";
background.style.width = "100vw";
background.style.zIndex = 5;
background.style.backgroundColor = "rgba(255, 255, 255, 0.35)";
background.setAttribute("id", "quote-customer-heat-map-background");
var heightInPx = styleObj.getPropertyValue("height");
var rawHeight = parseInt(heightInPx.replace("px", ""));
var newHeight = parseInt((rawHeight + 80));
div.style.height = newHeight + "px";
div.setAttribute("id", "quote-customer-heat-map-foreground");
body.style.paddingBottom = "0px";
body.appendChild(background);
body.appendChild(div);
var heatMap = h337.create({
container: div,
radius: 45
});
heatMap.setData({ max: max, data: points });
};
};
// A lot of other code...
As the pages elements that you can see being used in setupHeatMaps changes in width, viewing this data gets offset quite badly. I'll be honest, I've spent a lot of time yesterday thinking about this issue & I've still not thought of anything that seems reasonable.
Alternatively
I have wondered if I should somehow just store the page as an image, with the heatmap overplayed, that way I wouldn't really have to worry about the heatmap being responsive. But then I need to figure out some other things... E.g. Versioning this data, so in the event that a user views a page on their phone, it'll stored that data separately to data that was collected from a previous session where they were on a laptop or on a desktop device.
Conclusion
I'll be honest, I'm not entirely sure what the best course of action is, have any of you guys encountered anything like this before? Have you guys thought of something genius that solves an issue like this?
P.S. I would share a lot more code, however there's an immense amount of code that goes into this overall solution, so I kinda can't share all of it, I'm pretty sure the guys at Stackoverflow would hate me for that! 😅 - You can also tell that I'm doing this as a POC because normally I'd just offload the rendering of the background element & the div element to the underlying framework rather than do it programatically like this, keep that in mind, this is just a POC.

Perhaps you could change the setup and record what element an event is on and save the location data relative to that element, not the whole page.
You record all events on the page still but you save the data relative to the elements instead of the whole page, which prevents you from mashing the data together in your heatmap afterwards when you view
it over your website at various widths.
Say you you have a setup like this in pseudocode:
#1000px width
[html width 1000px, height 500px
[div width 800px, height 400px centered
[button1 width 100px, height 30px centered]
]
]
#500px width
[html width 500px, height 1000px
[div width 400px, height 800px centered
[button1 width 80px, height 30px centered]
]
]
Your users move over some element at all times. Just capture that data like this, example data from 2 users at the two different screen sizes moving the cursor towards the button in the center:
user interaction{
over element: html {dimensions: width 1000px, height 500px}
user position: {x 900, y 20}
}
user interaction{
over element: html {dimensions: width 1000px, height 500px}
user position: {x 850, y 60}
}
user interaction{
over element: div {width 800px, height 400px}
user position: {x 700, y 60}
}
user interaction{
over element: button1 {width 100px, height 30px}
user position: {x 90, y 10}
}
user interaction{
over element: html {dimensions: width 500, height 1000px}
user position: {x 450, y 100}
}
user interaction{
over element: div {width 400px, height 800px}
user position: {x 380, y 40}
}
user interaction{
over element: button1 {width 80px, height 30px}
user position: {x 60, y 10}
}
Then when you view your website draw the heat over all elements and calculate the relative position of the captured data.
So when you would view your site at #500px width the heat over your button1 would be as following:
[button 1 width 80px, height 30px
heat 1, x 72px y 10
heat 2, x 60px y 10
]
And you do the same for all other elements. I don't know how useful the data is like this, but it was for the sake of weird wizardry right?
Personally I'd just exclude data based on the screen width your viewing your heatmap at. That way you can use this setup and get useful heatmap data. So you'd exclude data based on if it was captured at a specific responsive width. So you could at the very least mash all user data together at high widths cause you'd know your interesting web elements would probably be centered still and the same size.
Its common to cut up your responsive design in 2 or 3 sizes, monitor, tablet and phone. You'd be surprised how similar you can keep your design layout across those 3. The more similar the more useful it will be to mix the data from different width that will fall into the specific media query range.
As long as you use the same technique for getting the width and height for saving your data as painting it later it will be fine. Even if you'd ignore margins and borders for instance your event would still capture on the element when its in the margin and that way you could get data like this: user position: {x -10, y -1} and still use that to paint your heat just fine on the element.
You could also give the option to mix and filter the user data across different size to the user, just call it experimental data mixing or something. You could potentially still get some very useful visual information on the most clicked elements for instance if you mix all user data regardless of screen size.
Another thing you could do is soft-mix the results. If your user data differs too much in width and height from the current element's dimensions, say more then 30%, you could exclude it. You could make this dynamic by letting the user set the sensitivity (feathering) of that percentage. That way you can still view all user heat on all smaller elements while it ignores the not very relevant user events on your larger more changeable elements.

My understanding of the issue is that the project works well, in terms of correctly performing the events, storing the information and displaying it, yet, there is a UX problem, because the system is offsetting (in time, I assume). I would use an idea which resembles very much compression. Imagine your monitor as a grid of pixels. It's perfectly clear that there are some precision problems, because even if we use the maximum precision our computer and its components allows us to use, if we take two adjacent pixels and want to click in between the two, we quickly realize that the precision our system offers is quite limited, which is axiomatically true for digital devices, which are unable to respect the Hausdorf separation.
Now that we have explored the limitations with our little thought experiment, we can acknowledge that such limitations exist. Hence, in principle, lowering the number of pixels, but not the number of events would not introduce more problems, it would decrease spatial precision though for a certain amount (the amount that you deem to be still acceptable, off course). So, if you have x * y resolution applied for the map, then you can think about your resolution as "every 5 pixels in with and height counts". That would make a mapping between actual pixels and imaginary pixels that would form the basis of our compression, decreasing the clickable places 25x.
This would allow you to think about the monitor as a set of regions, the number of regions being significantly smaller than the actual number of pixels (25x in our example). That would allow you to avoid storing each coordination/click separately and instead, you could always store the (x, y, n), that is, the center of the region where the event occurred and the number that event was repeated.
This way if the user clicks on a pixel (x', y'), which is located in a region whose center is (x, y), then you would only have new data if a click did not occur in that region yet. Otherwise, if a click occurred there (not necessarily at the exact same pixel, but in the region), then you would just increase n. As a result, instead of a very large set of raw pixel-event data you would have a much smaller set of pixel-event-number data.
The drawback of this approach, off course is that it somewhat reduces geometrical precision, but it should contribute to the optimization of data processing.
I confess that I'm not very experienced with Heatmap.js, so I'm unsure whether it has API support for what I am suggesting. If it has, then trying to use it would make sense. If it is not optimized properly, or does not support such a feature, then I would implement the heatmap feature using a canvas or an svg, depending on the actual needs.

While this solution may not be the most elegant solution ever, it does at the very least work, if anyone has any ideas or anything that's just plain ol' better, by all means chip in!
My Solution Explained
So I was thinking, I could render this via an iframe, sure it's not the most beautiful solution ever, but at least that means that there's no complicated positioning logic involved, it's relatively lightweight in terms of computational complexity compared to some other solutions I've looked at & that it works for all sorts of screen sizes, it essentially removes the need for this to be responsive... Kinda...
Sure that means that you'll have scroll bars left, right & centre, but it's simple, straight to the point & it means that I can produce an MVP in very little time, and realistically, I'd expect your average junior developer may have an easier time understanding what's going on? What do you guys think? 🙂
I'm trying to think of it from a user perspective too, realistically in this kinda application, I would much prefer true, raw accuracy over something looking so nice it makes me want to become a designer. I've even done some homework into this subject, as I've essentially been trying to build some session-replay software, it has been a bloody interesting project/feature I must say! 😀
With some styling, I've been able to accomplish something like this... Obviously this isn't a direct snippet of the website/application, but this is some marketing material that my boss has created, but the content within the 'laptop', that is an actual screenshot/snip of the web application! 😅 - So all in all, I think it looks okay? - You can see where the overflow happens & the right hand side of the screen is a bit cut off, but because everything else within the UI sorta behaves like they have a fixed position there, I personally think it seems to work pretty darn well.
Edit
I'd just like to thank everyone that spent their time, whether that was adding comments or providing answers, fantastic to see the dev community be so helpful! Thank you all guys! 😀
Blog Post
So I've written a little more on this subject matter here:
LinkedIn Post
Blog Post

Related

Make a multi-part "fluid body" act as one single body

I am using Phaser.js and its p2 physics in order to simulate what "fluid bodies". You can see in this example that a kind of fluid body is created (credits to John Watson). The only possible interaction is with mouse movement.
I have noticed some interesting properties that could probably help me getting what I pretend which are restitution, gravity and damping. All of these are in included in the shown example.
// Add a force that slows down the droplet over time
droplet.body.damping = 0.3;
// Add bounciness and gravity
this.game.physics.p2.restitution = 0.8;
this.game.physics.p2.gravity.y = 250;
After some research and reflexion I concluded that the body I seek must be more united than the example body which means that, within certain limits, a force with the direction of the center of the body (center +- top of body) should be applied to all the "small bodies" that make the body. I suppose that after achieving that even if I move the entire body with cursors it will move all together. The overall efect I want is a pile-type body form (less mass on upper part and more mass on the bottom part):
The only way for the body to lose mass should be an external force applied to the body that surpasses the resistance force that keeps the body united.
Even after researcing for quite a while I seem very lost in the matter...
Should I literally opt for p2 forces ( I believe that exists ) ?
Should I use springs to connect all the small bodies? (springs)
How would I always get the "center of the body"?
Thanks.
Disclaimer: I have not used Phaser.js, so I can't help you with the framework. I will however try to share some of my ideas on the problem, and hopefully it helps you.
I recently wrote this answer regarding plasticity (which could be of interest to you), but what you want is slightly different.
The simulation
First, lets talk some about the simulation you showed. You write "fluid bodies", but from what I see in the code example is nothing fluid -- it's a pure particle simulation with "cheated" physics features that comes from (1) the automatic damping of the particles and (2) the restitution which in normal speak means how elastic a collision with the object is (e.g. the 0.8 value in this case means that 20% of the kinetic energy is lost with every collision). So basically, we have a particle simulation with a lot of damping.
That this yields something that looks 'fluid like' is really pretty cool, but my guess is that this is also due to the rendering (e.g. show the particles as small disks instead of the blurred thing and it will look a lot more like what you'd expect).
Your questions
Should I literally opt for p2 forces ( I believe that exists ) ?
I am actually not sure what this means, but I'm guessing it's Phaser related.
Should I use springs to connect all the small bodies? (springs)
No. If you use a elastic potential to calculate the force you will get an elastic body as differing from plasticity which is what you're looking for. What other function you use will entirely determine the behaviour of your simulation, so going with this idea there will be a lot of experimentation.
If x_cm is the position (vector) to the center of mass, and x[i] is the position of particle i, then one example could be:
F(i) = F_constant*(x_cm - x[i])
A purely linear function. F_constant is some (constant) coefficient. Particles far away will receive a greater force than particles nearby. You would then calculate this force for all particles, and apply it accordingly.
How would I always get the "center of the body"?
The center of mass of the body is straight forward to calculate. In pseudo code it looks like this:
var x_cm
var total_mass = 0
for each particle p:
total_mass += p.mass()
x_cm += p.mass()*p.position()
x_cm /= total_mass
Formulas are hard to display nicely here, but it's just the same as described on wikipedia.
Another possibility
This answer is already long, but just a finishing thought. From my perspective it sounds like you want something similar to sand (i.e. it is a particle simulation similar to the one you showed, but it piles as well). A common way to simulate sand is by the exact simulation as you have above, but with friction added to the particles. If this is possible to do with Phaser I don't know, but I would expect it being easy to do.
EDIT: Had a typo in the last sentence. Simulating sand with Phaser, by adding friction to the example simulation, should be easy.

The d3 force layout central node positioning

I have use the d3.js to visualize my data. And the result like this.
PIC_1
PIC_2
My question is how can I make the data present like PIC_1,the center point local in the fixed position,and the other points (children points) around the center point like a circle.
Now,when I refresh the page the data will reload in the brower and all the points' position will randomly change as well.
So which d3's api or tricks can be used to this purpose. :)
Take a look at this example:
link to jsfiddle
If you exampne the code, and play with layout, you'll see that the root node has special treatment that makes it always remain in the center of the graph (unless you drag it, but even that you can prevent if you want).
On initialization, its property fixed is set to true, so that d3 force layout simulation doesn't move it. Also, it is placed in the center of rectangle containing layout:
root.fixed = true;
root.x = width / 2;
root.y = height / 2;
You ofcourse need to change or add some code in order to integrate this feature to your example, but the core idea is really clear from the example I linked.
Hope this helps. Let me know if you have a question, need a clarification, etc.

KineticJS - Moving 4000 tiles with Stage.draggable

(Sorry if this is a duplicate, but I don't think it is)
Just so you know, I'm using Google Chrome 29.0.1547.66 m.
I have a KineticJS Project going on at the moment which builds a tiled "staggered" map 40 x 100 tiles. The map takes about 500ms to render which is fine. All 4000 tiles have been put onto one layer.
Once that has completed, I try to drag the stage but I am getting very poor performance as it tries to move all 4000 tiles at once. We are talking about 160ms of CPU time.
I have tried breaking each row up into its own separate layer as others suggested, but that made it even worse (620ms CPU time) as it had to move each layer separately.
I wouldn't say I'm very good at JavaScript but I can't see a way to improve the performance of the drag and I have done some descent research.
Maybe caching the entire layer or something could work?
The project so far has quite a lot of code, so I'm just going to show snippets:
//Stage Object
window.stage = new Kinetic.Stage({
container: element,
width: 800,
height: 600,
draggable: true
});
//Map Loop for create and position tiles
var tempLayer = map.addLayer(); //makes a new layer and adds it to stage etc.
for (var y = 0; y < height; y++) { //100 tiles high
for (var x = width-1; x > -1; x--) { //40 tiles wide
var tileImg = map._tiles[mapArray[x][y]-1]; //map._tiles is just an array of Kinetic.Image objects
if (typeof(tileImg) != "undefined"){
var tileClone = tileImg.clone();
map.place(x, y, 0, tileClone, tempLayer); //places tile in world scope positions
}
}
}
tempLayer.draw();
If I can't work out how to improve the performance, there is no way anyone will be able to run this and the project is going to have to be binned, so all thoughts are appreciated! Thanks!
Have a look at this SO Question: How to cache a whole layer right before dragstart and revert it back on dragend?
The question and my answer describes a similar issue and I think the solution I came up with may help.
Basically what I was suggesting (although I haven't tried it completely so I don't know if it will work well):
Cache the layer using toImage
Drag the image on a different layer while hiding the original layer.
Calculate dx and dy (the distance that you moved)
Update the original layer with dx and dy
Hide image layer, show shapes layer.
I managed to create a quick example JSFIDDLE to work with so check it out. One thing I noticed is that the stage size really affected the performance of the drag, even if it was just 1 rectangle instead of 4000. So, if the stage is really big, even with this image caching thing it didn't really help. But when I decrease the stage size it seems to help
UPDATE:
I found the reason for that "stretched/scaled" image when dragging. It's because I had the image size set statically like so:
var image = new Kinetic.Image({
image: img,
x: 0,
y: 0,
width: 2000,
height: 5000
});
This caused the image to stretch since the image was larger than the stage. If we remove the width and height properties like so:
var image = new Kinetic.Image({
image: img,
x: 0,
y: 0
});
You'll see that the image doesn't stretch anymore.
The other good news is that I reduced the stage dimensions by half (although the number of rectangles, area taken by rectangles and size of image remains the same) and the performance has improved greatly. Hopefully your stage dimension isn't as large (2000x5000) as I had it before right? Check the JSFIDDLE now and see it in action!

How can I use scrollbars to pan around a virtual canvas?

I want to create a jQuery plugin that implements a virtual HTML5 Canvas, i.e. a canvas that is physically no larger (or not much larger) that its appearance on the page. But the contents of what is intended to be shown on the canvas may be many times larger that the canvas and will be dynamically redrawn on depending on scrollbars.
You would think that this is very common functionality, but so far i have not been able to find examples either with jQuery plugins or otherwise. This is very similar to what e.g. SlickGrid does for a Div, except this is with a Canvas. I can think of two solutions:
Use a jQuery UI Slider to implement a scrollbar as a completely separate element and use its event to control the Canvas redrawing.
Do whatever it is SlickGrid does for the Div. It appears to make a Div that is slightly larger than what is being displayed and the hook up to scroll events to dynamically add/remove element to/from the Div. But I can't see how it modifies the scrollbar to make it appear as if there is much more in the Div that what is currently being displayed.
What would you recommend? Sample code would be greatly appreciated.
I dug into the SlickGrid code and used method 2) (sort of) - it was something like this I had in mind:
/*
jensk#migselv.com
Simple virtual CANVAS controlled by a native scrollbar made with two DIVs
Uses jCanvas by Caleb Evans (http://calebevans.me/projects/jcanvas/index.php)
Thanks to Michael Leibman of SlickGrid fame for ideas.
Still need to clean it up (get rid of hardcoded values) and make it a nice, configurable
jQuery component.
Currently also redraws the entire canvas on each scroll event. Could be optimized to
do real browser scrolling and only redrawing the needed parts.
Another gotcha is that since it is the zero width DIVs that causes the scroll events,
mouse wheel, trackpad, touchscreen etc. scrolling over the Canvas will not work - only
the scrollbar is active. To solve this, one could make the Canvas larger inside a
smaller DIV too, catch scroll events from it and perform redrawing and setting the DIV's
scrollTop accordingly.
*/
var h = 10000; // virtual canvas height
var vp = 400; // viewport height
var viewport, fakescrolldiv, canvas;
function onScroll() {
var scrollTop = viewport.scrollTop();
console.log("onScroll scrollTop=" + scrollTop);
$("canvas").clearCanvas();
// Red box top
$("canvas").drawRect({
fillStyle: "#F00",
x: 150,
y: 20 - scrollTop,
width: 100,
height: 100,
fromCenter: false
});
// Green box middle
$("canvas").drawRect({
fillStyle: "#0F0",
x: 150,
y: 140 - scrollTop,
width: 100,
height: 100,
fromCenter: false
});
// Blue box bottom
$("canvas").drawRect({
fillStyle: "#00F",
x: 150,
y: 260 - scrollTop,
width: 100,
height: 100,
fromCenter: false
});
var i = 0;
for (i = 0; i <= 396; i++) {
$("canvas").drawLine({
strokeStyle: "#000",
strokeWidth: 1,
x1: 0,
y1: i,
x2: (scrollTop + i) % 50,
y2: i
});
if ((scrollTop + i) % 50 === 0) {
$("canvas").drawText({
fillStyle: "#729fcf",
text: (scrollTop + i).toString(),
align: "left",
baseline: "top",
font: "normal 12pt Verdana",
x: 60,
y: i
});
}
}
}
$(function() {
viewport = $("#viewport");
fakescrolldiv = $("#fakescrolldiv");
canvas = $("#gfx");
viewport.css("height", vp);
fakescrolldiv.css("height", h);
viewport.scroll(onScroll);
viewport.trigger("scroll");
});
Live demo
Any suggestions for improvements or simplifications are greatly appreciated.
From my personal experience your option (1) is an attractive choice, but there might be some interesting points in making it into a useable jQuery plugin.
I've been working on a financial data visualisation system that explicitly uses HTML5 Canvas for graph and chart drawing. We have different virtual 'scenes' or 'slides' in the canvas which 'slide in' and 'slide out' in the canvas, much like the same way you'd navigate in a big virtual canvas. All the event handling buttons are exclusively drawn on the canvas which dictate which screen we'd be showing, but we have a one/two normal HTML forms that take user inputs and brings those 'slides'. We use jQuery to handle events from these text boxes, but the jQuery codes are deeply nested inside the other Canvas drawing codes, (unlike an externalised call, which would be an idea candidate for making a plugin).
Sliding or updating the canvas is another thing. This is because not only it depends on the jQuery event that triggers the update but it also depends on the Canvas Framework (plain code or KineticJS, EaselJS, jCotton etc) that is responsible for the update. If you use a framework, you'll need to interface with the framework as well.
For simplicity let's assume that there is a callback function that you can call for that Canvas framework with parameters like movement offset (x, y), and the framework will add/remove this offset to the x and y positions of all the objects drawn in the canvas, most Canvas drawing frameworks also have a render() function that it calls periodically so that next time it draws the scene the results will automatically show (in your case, scrolling through the virtual canvas).
So it basically comes down to not only writing it as a jQuery plugin but also a binding it to a particular Canvas Framework e.g. KineticJS or others.
If you use basic Canvas functions instead of using any of those Frameworks, then it's another story, you can write your own render and update functions for the canvas, but in that case it'll be restricting the potential user to adhere the limitations of your drawing functions, unless you expose an API to extend them; but then again, that means you are writing your own Canvas framework :)
I'm not sure if I understood your problem correctly, in that case you can safely ignore my advice :), but If I'm right, my opinion would be: making a plugin like this would require also binding to a Canvas framework to make it really useable.
Hope it helps.

Detect mouseover of certain points within an HTML canvas?

I've built an analytical data visualization engine for Canvas and have been requested to add tooltip-like hover over data elements to display detailed metrics for the data point under the cursor.
For simple bar & Gaant charts, tree graphs and node maps with simple square areas or specific points of interest, I was able to implement this by overlaying absolutely-positioned DIVs with :hover attributes, but there are some more complicated visualizations such as pie charts and a traffic flow rendering which has hundreds of separate areas defined by bezeir curves.
Is is possible to somehow attach an overlay, or trigger an event when the user mouses over a specific closed path?
Each area for which hover needs to be specified is defined as follows:
context.beginPath();
context.moveTo(segmentRight, prevTop);
context.bezierCurveTo(segmentRight, prevTop, segmentLeft, thisTop, segmentLeft, thisTop);
context.lineTo(segmentLeft, thisBottom);
context.bezierCurveTo(segmentLeft, thisBottom, segmentRight, prevBottom, segmentRight, prevBottom);
/*
* ...define additional segments...
*/
// <dream> Ideally I would like to attach to events on each path:
context.setMouseover(function(){/*Show hover content*/});
// </dream>
context.closePath();
Binding to an object like this is almost trivial to implement in Flash or Silverlight, since but the current Canvas implementation has the advantage of directly using our existing Javascript API and integrating with other Ajax elements, we are hoping to avoid putting Flash into the mix.
Any ideas?
You could handle the mousemove event and get the x,y coordinates from the event. Then you'll probably have to iterate over all your paths to test if the point is over the path. I had a similar problem that might have some code you could use.
Looping over things in this way can be slow, especially on IE. One way you could potentially speed it up - and this is a hack, but it would be quite effective - would be to change the color that each path is drawn with so that it is not noticeable by humans but so that each path is drawn in a different color. Have a table to look up colors to paths and just look up the color of the pixel under the mouse.
Shadow Canvas
The best method I have seen elsewhere for mouseover detection is to repeat the part of your drawing that you want to detect onto a hidden, cleared canvas. Then store the ImageData object. You can then check the ImageData array for the pixel of interest and return true if the alpha value is greater than 0.
// slow part
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.fillRect(100,100,canvas.width-100,canvas.height-100);
var pixels = ctx.getImageData(0,0,canvas.width,canvas.height).data;
// fast part
var idx = 4 * (mouse_x + mouse_y * canvas.width) + 3;
if (pixels[idx]) { // alpha > 0
...
}
Advantages
You can detect anything you want since you're just repeating the context methods. This works with PNG alpha, crazy compound shapes, text, etc.
If your image is fairly static, then you only need to do this one time per area of interest.
The "mask" is slow, but looking up the pixel is dirt cheap. So the "fast part" is great for mouseover detection.
Disadvantages
This is a memory hog. Each mask is W*H*4 values. If you have a small canvas area or few areas to mask, it's not that bad. Use chrome's task manager to monitor memory usage.
There is currently a known issue with getImageData in Chrome and Firefox. The results are not garbage collected right away if you nullify the variable, so if you do this too frequently, you will see memory rise rapidly. It does eventually get garbage collected and it shouldn't crash the browser, but it can be taxing on machines with small amounts of RAM.
A Hack to Save Memory
Rather than storing the whole ImageData array, we can just remember which pixels have alpha values. It saves a great deal of memory, but adds a loop to the mask process.
var mask = {};
var len = pixels.length;
for (var i=3;i<len;i+=4) if ( pixels[i] ) mask[i] = 1;
// this works the same way as the other method
var idx = 4 * (mouse_x + mouse_y * canvas.width) + 3;
if (mask[idx]) {
...
}
This could be done using the method ctx.isPointInPath, but it is not implemented in ExCanvas for IE.
But another solution would be to use HTML maps, like I did for this little library : http://phenxdesign.net/projects/phenx-web/graphics/example.htm you can get inspiration from it, but it is still a little buggy.
I needed to do detect mouse clicks for a grid of squares (like cells of an excel spreadsheet). To speed it up, I divided the grid into regions recursively halving until a small number of cells remained, for example for a 100x100 grid, the first 4 regions could be the 50x50 grids comprising the four quadrants.
Then these could be divided into another 4 each (hence giving 16 regions of 25x25 each).
This requires a small number of comparisons and finally the 25x25 grid could be tested for each cell (625 comparisons in this example).
There is a book by Eric Rowell named "HTML5 CANVAS COOKBOOK". In that book there is a chapter named "Interacting with the Canvas: Attaching Event Listeners to Shapes and Regions". mousedown, mouseup, mouseover, mouseout, mousemove, touchstart, touchend and touchmove events can be implemented. I highly suggest you read that.
This can't be done (well, at least not that easily), because objects you draw on the canvas (paths) are not represented as the same objects in the canvas. What I mean is that it is just a simple 2D context and once you drawn something on it, it completely forgets how it was drawn. It is just a set of pixels for it.
In order to watch mouseover and the likes for it, you need some kind of vector graphics canvas, that is SVG or implement your own on top of existing (which is what Sam Hasler suggested)
I would suggest overlaying an image map with proper coordinates set on the areas to match your canvas-drawn items. This way, you get tooltips AND a whole lot of other DOM/Browser functionality for free.

Categories

Resources