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

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.

Related

Responsive Heatmaps

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

how to make rectangle scale vertically animation in Raphael JS

The Raphael Js website is down so I can't find any documentation or anything on how to do this. I want to create a rectangle with initial vertical size of 0 and make it animate so that it gets vertically larger and larger when I click another object. Thanks!
so i've got a rectangle
var water = paper.rect( 0, 300, 600, 0).attr({fill:"blue"});
how do I make it animate?
In particular, check out the documentation for
Element.attribute(),
Element.animate(),
Element.click(), and
Raphael.animation().
The following snippet demonstrates a simple animation like the one you are looking for. Click the red square to make the water "fill". It may not work in some browsers (e.g. Chrome) possibly because the snippet is trying to access an external third-party library, i.e. Raphael.js. So, to run it, either go to this SO question/answer page on Firefox or copy the code and run it on your own computer.
Note that, to make the water "fill" in the up direction, you can't just increase the height of the water rectangle...that would make the rectangle grow downwards. You also have to simultaneously raise the top of the rectangle by the identical amount. Thus you have to animate height and y simultaneously, as shown in the code.
UPDATE: provided an up-to-date link for the Raphael minified library
var paper = Raphael(0, 0, 500, 120);
var button = paper.rect( 10, 10, 40, 20).attr({fill:"red"});
var water = paper.rect( 10, 100, 400, 0).attr({fill:"blue"});
var anim = Raphael.animation({
height: 60,
y: (100 - 60)
}, 2000);
button.click(function() {water.animate(anim);});
<script src="https://raw.githubusercontent.com/DmitryBaranovskiy/raphael/master/raphael.min.js"></script>
To get the animation to start, click on the red rectangle.

Draggable element make it snap to the vertices around it with a high snapTolerance

I just wanted to make a grid with a dragged element and make it snap to the vertices around the element I'm dragging. Happens that if the snapTolerance is too high (grid size i.e: 20px) but the asset is not divisible by 20 in size... it won't snap to the next vertex but it will make a 20px jump to the next block. I don't know if I'm being clear but here's a jsfiddle that might help you understand this better.
In this example, I would like the right side to touch the next vertex before the next-block-jump. Is that possible with jQuery UI right now?
I can decrease the snapTolerance but the snappy effect won't be that smooth since the dragged elements may vary in sizes.
I would like to make it stuck on the vertices at all times but have the edges of the dragged element to snap on every vertex around.
$(".draggable-block").draggable({
snapTolerance: 20,
snap: '.guide-line'
});
Try using this option instead of snap:
$(".draggable-block").draggable({
//snapTolerance: 20,
//snap: '.guide-line',
//snapMode: "inner",
grid: [ 5, 5 ]
});
http://api.jqueryui.com/draggable/#option-grid
It looks like the JQuery UI draggable only snaps with its top left corner and always aligns with the lines at that spot.
You could try to do something on the drag event and see if there is anything helpful on the ui parameter:
http://api.jqueryui.com/draggable/#event-drag
Are you required to use a 20px grid? If not, you could define your grid lines based off the width or height of the draggable.
var grid = $('<div>', {'id': 'grid'});
var gridWidth = $('.draggable-block').width()/5;
var guideLinePos = gridWidth;
var guideLabel = "";
while(guideLinePos <= 1000) {
if(((guideLinePos - gridWidth) % gridWidth) == 0) {
guideLabel = "<div class='guide-line vertical-line'></div>";
$(guideLabel).css( "left", guideLinePos+"px" ).appendTo(grid);
}
guideLinePos = (guideLinePos + gridWidth);
}
Unfortunatly the JS fiddle either does not illustrate the issue : or the problem is the object cant be divided evenly by the grid (IE: a 60px*60px block does not fit well in a 40*40 grid)
I would suggest you take a look at the Gsap Draggable Library for tackling this issue. It does quite a good job of tackling the issue.

What's the best way to create a user turnable wheel in HTML5/JavaScript?

I need to create a wheel which will be dragged around 360 degrees by the user (in order to have a specific segment of the wheel placed at the top).
I've found examples of a roulette wheel using HTML5 canvas which just spin an image of a wheel for a random amount of time. However, when it comes to allowing a user to drag a wheel as they wish, I'm stumped.
I'd also rather make the wheel from HTML/CSS rather than use an image, because it needs to be dynamically updated.
mmm this looks like a math problem...
so define a center point and then use trigonometry to define the angle of the line (from the center point to the mouse) from the horizon
http://en.wikipedia.org/wiki/Trigonometry
now draw in a canvas on the center point inside the cnavas the weel, get the angle and redraw the weel as needed
The Sencha drawing framework allows you to just about do anything that you might have seen done in flash - but without flash. It works in just about all browsers without plugins. It uses SVG or VML depending on the browser. Have a look at the draggable tiger example here
Edit:
Documentation for the drawing framework which is part of the larger ExtJS framework is found at: http://docs.sencha.com/ext-js/4-0/#!/api/Ext.draw.Component
This is the code I knocked up for a draggable circle/wheel. Didn't put any spinning action in as I think you've got that worked out.
<script src="ext-all-debug.js" type="text/javascript"></script>
<script type="text/javascript">
Ext.onReady( function() {
//create the drawing surface
var drawComponent = Ext.create('Ext.draw.Component', {
width: 400,
height: 400,
draggable: {
constrain: true,
constrainTo: Ext.getBody()
},
autoShow:true,
floating: true,
renderTo: Ext.getBody(),
items:[{
//add the circle sprite to the
//drawing surface
type: 'circle',
fill: '#79BB3F',
"stroke-width": "1",
stroke: "#000",
radius: 100,
x: 100,
y: 100
}]
});
});
</script>
You will want to use probably Canvas to draw: http://diveintohtml5.info/canvas.html
For dragging the wheel you will need to handle mouse down, mouse move and mouse up events.

Three mouse detection techniques for HTML5 canvas, none adequate

I've built a canvas library for managing scenes of shapes for some work projects. Each shape is an object with a drawing method associated with it. During a refresh of the canvas, each shape on the stack is drawn. A shape may have typical mouse events bound which are all wrapped around the canvas' own DOM mouse events.
I found some techniques in the wild for detecting mouseover on individual shapes, each of which works but with some pretty serious caveats.
A cleared ghost canvas is used to draw an individual shape by itself. I then store a copy of the ghost canvas with getImageData(). As you can imagine, this takes up a LOT of memory when there are many points with mouse events bound (100 clickable shapes on a 960x800 canvas is ~300MB in memory).
To sidestep the memory issue, I began looping over the pixel data and storing only addresses to pixels with non-zero alpha. This worked well for reducing memory, but dramatically increased the CPU load. I only iterate on every 4th index (RGBA), and any pixel address with a non-zero alpha is stored as a hash key for fast lookups during mouse moves. It still overloads mobile browsers and Firefox on Linux for 10+ seconds.
I read about a technique where all shapes would be drawn to one ghost canvas using color to differentiate which shape owned each pixel. I was really happy with this idea, because it should theoretically be able to differentiatate between millions of shapes.
Unfortunately, this is broken by anti-aliasing, which cannot be disabled on most canvas implementations. Each fuzzy edge creates dozens of colors which might be safely ignored except that /they can blend/ with overlapping shape edges. The last thing I want to happen when someone crosses the mouse over a shape boundary is to fire semi-random mouseover events for unrelated shapes associated with colors that have emerged from the blending due to AA.
I know that this not a new problem for video game developers and there must be fast algorithms for this kind of thing. If anyone is aware of an algorithm that can resolve (realistically) hundreds of shapes without occupying the CPU for more than a few seconds or blowing up RAM consumption dramatically, I would be very grateful.
There are two other Stack Overflow topics on mouseover detection, both of which discuss this topic, but they go no further than the 3 methods I describe.
Detect mouseover of certain points within an HTML canvas? and
mouseover circle HTML5 canvas.
EDIT: 2011/10/21
I tested another method which is more dynamic and doesn't require storing anything, but it's crippled by a performance problem in Firefox. The method is basically to loop over the shapes and: 1) clear 1x1 pixel under mouse, 2) draw shape, 3) get 1x1 pixel under mouse. Surprisingly this works very well in Chrome and IE, but miserably under Firefox.
Apparently Chrome and IE are able to optimize if you only want a small pixel area, but Firefox doesn't appear to be optimizing at all based on the desired pixel area. Maybe internally it gets the entire canvas, then returns your pixel area.
Code and raw output here: http://pastebin.com/aW3xr2eB.
If I understand the question correctly, you want to detect when the mouse enters/leaves a shape on the canvas, correct?
If so, then you can use simple geometric calculations, which are MUCH simpler and faster than looping over pixel data. Your rendering algorithm already has a list of all visible shapes, so you know the position, dimension and type of each shape.
Assuming you have some kind of list of shapes, similar to what #Benjammmin' is describing, you can loop over the visible shapes and do point-inside-polygon checks:
// Track which shape is currently under the mouse cursor, and raise
// mouse enter/leave events
function trackHoverShape(mousePos) {
var shape;
for (var i = 0, len = visibleShapes.length; i < len; i++) {
shape = visibleShapes[i];
switch (shape.type ) {
case 'arc':
if (pointInCircle(mousePos, shape) &&
_currentHoverShape !== shape) {
raiseEvent(_currentHoverShape, 'mouseleave');
_currentHoverShape = shape;
raiseEvent(_currentHoverShape, 'mouseenter');
return;
}
break;
case 'rect':
if (pointInRect(mousePos, shape) &&
_currentHoverShape !== shape) {
raiseEvent(_currentHoverShape, 'mouseleave');
_currentHoverShape = shape;
raiseEvent(_currentHoverShape, 'mouseenter');
}
break;
}
}
}
function raiseEvent(shape, eventName) {
var handler = shape.events[eventName];
if (handler)
handler();
}
// Check if the distance between the point and the shape's
// center is greater than the circle's radius. (Pythagorean theroem)
function pointInCircle(point, shape) {
var distX = Math.abs(point.x - shape.center.x),
distY = Math.abs(point.y - shape.center.y),
dist = Math.sqrt(distX * distX + distY * distY);
return dist < shape.radius;
}
So, just call the trackHoverShape inside your canvas mousemove event and it will keep track of the shape currently under the mouse.
I hope this helps.
From comment:
Personally I would just switch to using SVG. It's more what it was
made for. However it may be worth looking at EaselJS
source. There's a method Stage.getObjectUnderPoint(), and their demo's
of this seem to work perfectly fine.
I ended up looking at the source, and the library utilises your first approach - separate hidden canvas for each object.
One idea that came to mind was attempting to create some kind of a content-aware algorithm to detect anti-aliased pixels and with what shapes they belong. I quickly dismissed this idea.
I do have one more theory, however. There doesn't seem to be a way around using ghost canvases, but maybe there is a way to generate them only when they're needed.
Please note the following idea is all theoretical and untested. It is possible I may have overlooked something that would mean this method would not work.
Along with drawing an object, store the method in which you drew that object. Then, using the method of drawing an object you can calculate a rough bounding box for that object. When clicking on the canvas, run a loop through all the objects you have on the canvas and extract ones which bounding boxes intercept with the point. For each of these extracted objects, draw them separately onto a ghost canvas using the method reference for that object. Determine if the mouse is positioned over a non-white pixel, clear the canvas, and repeat.
As an example, consider I have drawn two objects. I will store the methods for drawing the rectangle and circle in a readable manner.
circ = ['beginPath', ['arc', 75, 75, 10], 'closePath', 'fill']
rect = ['beginPath', ['rect', 150, 5, 30, 40], 'closePath', 'fill']
(You may want to minify the data saved, or use another syntax, such as the SVG syntax)
As I am drawing these circles for the first time, I will also keep note of the dimensional values and use them to determine a bounding box (Note: you will need to compensate for stroke widths).
circ = {left: 65, top: 65, right: 85, bottom: 85}
rect = {left: 150, top: 5, right: 180, bottom: 45}
A click event has occurred on the canvas. The mouse point is {x: 70, y: 80}
Looping through the two objects, we find that the mouse coordinates fall within the circle bounds. So we mark the circle object as a possible candidate for collision.
Analysing the circles drawing method, we can recreate it on a ghost canvas and then test if the mouse coordinates fall on a non-white pixel.
After determining if it does or does not, we can clear the ghost canvas to prepare for any more objects to be drawn on it.
As you can see this removes the need to store 960 x 800 x 100 pixels and only 960 x 800 x2 at most.
This idea would best be implemented as some kind of API for automatically handling the data storage (such as the method of drawing, dimensions...).

Categories

Resources