I'm working on an application that uses Raphael to draw primitive shapes (rectangles, ellipses, triangles etc) and lines but allows the user to move/resize these objects as well. One of the main requirements is that the face of shapes can have formatted text. The actual text is a subset of Markdown (simple things like bolding, italics, lists) and is rendered as HTML.
FWIW - I'm using Backbone.js views to modularize the shape logic.
Approach 1
My initial thought was to use a combination of foreignObject for SVG and direct HTML with VML for IE. However, IE9 doesn't support foreignObject, and therefore this approach had to be abandoned.
Approach 2
With the beside the canvas object, add divs that contain the actual HTML. Then, position them over the actual shape with a transparent background. I've created a shape view that has references to both the actual Raphael shape and the "overlay" div. There are a couple of problems with this approach:
Using overlay that aren't children of the SVG/VML container feels wrong. Does having this overlay element cause other issues with rendering down the road?
Events that are normally trapped by Raphael (drag, click etc) need to be forwarded from the overlay to the Raphael object. For browsers that support pointer-events, this is easily done:
div.shape-text-overlay {
position: absolute;
background: none;
pointer-events: none;
}
However, other browsers (like IE8 and below) need forwarding of the events:
var forwardedEvents = 'mousemove mousedown mouseup click dblclick mouseover mouseout';
this.$elText.on(forwardedEvents, function(e) {
var original = e.originalEvent;
var event;
if (document.createEvent) {
event = document.createEvent('HTMLEvents');
event.initEvent(e.type, true, true);
}
else {
event = document.createEventObject();
event.eventType = e.type;
}
// FYI - This is the most simplistic approach to event forwarding.
// My real implementation is much larger and uses MouseEvents and UIEvents separately.
event.eventName = e.type;
_.extend(event, original);
if (document.createEvent) {
that.el.node.dispatchEvent(event);
}
else {
that.el.node.fireEvent('on' + event.eventType, event);
}
});
Overlapping shapes cause the text to be overlapped because the text/shapes are on different layers. Although overlapping shapes won't be common, this looks bad:
This approach is what I'm currently using but it just feels like a huge hack.
Approach 3
Almost like Approach 1, but this would involve writing text nodes/VML nodes directly. The biggest problem with this is the amount of manual conversion necessary. Breaking outside of Raphael's API seems like it could cause stability issues with the other Raphael objects.
Question
Has anyone else had to do something similar (rendering HTML inside of SVG/VML)? If so, how did you solve this problem? Were events taken into account?
I built this project (no longer live) using Raphael. What I did is actually abandoned the idea of using HTML inside of SVG because it was just too messy. Instead, I absolutely positioned an HTML layer on top of the SVG layer and moved them around together. When I wanted the HTML to show, I merely faded it in and faded the corresponding SVG object out. If timed and lined up correctly, it's not really noticeable to the untrained eye.
While this may not necessarily be what you're looking for, maybe it will get your mind thinking of new ways to approach your problem! Feel free to look at the JS on that page, as it is unminified ;)
PS, the project is a lead-gathering application. If you just want to see how it works, select "Friend" in the first dropdown and you don't have to provide any info.
Unless another answer can be provided and trump my solution, I have continued with the extra div layer. Forwarding events was the most difficult part (if anyone requires the forwarding events code, I can post it here). Again, the largest downside to this solution is that overlapping shapes will cause their text to overlap above the actual drawn shape.
Related
I've gotten myself into a big and weird position.
I use VivaGraph JS for drawing Conceptual Graphs in the browser. The specific implementation I am using, relies on SVG, and thus my main graph DOM element is an SVG.
During the creation of Edges between nodes, I wrote a small piece of code using Paper.JS which uses canvas from HTML5. In fact, I hacked into the source code provided by vektor.js and simply changed it to listen to CTRL+MouseDown events.
Those two elements, the svg graph and canvas, overlap and have exactly the same dimensions. The graph has nodes and edges manipulated, which listen to mouse and keyboard events, and sadly so does my canvas.
In fact, the reason for using the canvas, was that I wanted to draw a line (a vector or edge or arc) during the mouse-movement, to show to the user what the edge being created would be, before I actually created that edge in the graph.
I could not do this using SVG (Yes, I know, it should be doable) and Paper.js made it extremely easy for me.
Sadly, depending of the order those DOM elements are displayed, either the canvas captures the events, leaving the graph useless, or the graph captures all events, leaving the canvas useless.
Is there some way to add transparency to both DOM elements?
The event listener for the graph, is built into VivaGraphJS, and the event listener for the Paper Vertex, is built into Paper.JS
Ideally, I would like to have the graph on-top, capture the events, and then propagate them back to the canvas, so that the arrows are drawn. I have the feeling that this should be doable, either via pure JavaScript, or by using jQuery.
So far, the events captured in the graph like this:
var graphics = Viva.Graph.View.svgGraphics();
/// some other stuff
graphics.node( function( node )
{
var ui = Viva.Graph.svg('g' ).attr('width', nodeSize )
.attr('height', nodeSize )
// Position the node, add its text, etc, etc...
$( ui ).mousedown( function( event )
{
event.preventDefault();
if ( event.ctrlKey )
{
if ( ctrl_mouse )
{
createEdge( ctrl_mouse, node.id );
ctrl_mouse = null;
// Remove the temporary arrow from canvas - the graph now has a permanent edge
}
else if ( !ctrl_mouse )
{
ctrl_mouse = node.id;
// Start drawing a temporary arrow on canvas
}
}
All this takes place in one file graph.js
In another file edge.js, I setup the event listeners and the way the vector is drawn. I've added it into jsfiddle but sadly it won't run there (I am guessing the keyboard events may not be propagated properly?).
The problem is that Paper.Js has its own event listeners:
function onMouseDown( event )
function onKeyUp( event )
function onMouseMove(event)
Obviously those events have their equivalent in pure JavaScript and jQuery, but the ones I capture in VivaGraphJS or jQuery cannot be propagated to PaperJS, since they are different objects.
So, can I somehow (preferably by using pure JavaScript, but jQuery will also work) emulate or send those events to Paper.JS?
Since nobody answered, and I am guessing this is due to the very specific nature of my question, I stumbled upon the correct answer on another post here in stack overflow. Weirdly enough it was not accepted as the correct answer.
The poster suggested the following:
quickDelegate = function(event, target) {
var eventCopy = document.createEvent("MouseEvents");
eventCopy.initMouseEvent(event.type, event.bubbles, event.cancelable, event.view, event.detail,
event.pageX || event.layerX, event.pageY || event.layerY, event.clientX, event.clientY, event.ctrlKey, event.altKey,
event.shiftKey, event.metaKey, event.button, event.relatedTarget);
target.dispatchEvent(eventCopy);
// ... and in webkit I could just dispath the same event without copying it. eh.
};
This worked for me, and it was the only thing that worked. I tried other libraries I found on github which supposedly forward events, but they didn't work.
This is not a: "Do all the work for me!" kind of question. I just wanna know which approach you think would be suitable for this challenge.
I have this map:
As you can see by the blue marker, I've roughly drawned some selections/areas of the map. Theese areas I want to serve as links.
But I don't quite know how to grasp this challenge, since all of the areas have quite odd shapes.
I have looked at cords, but it seems like a huge job with all of the twists and turns that I would need to do.
I would be awesome if I could just slice up the areas in Photoshop and save each of them as .png and just tell my page to ignore the transparent area! But that's just wishfull thinking I suppose.
I hope that one of you have a suggestion that I've overlooked.
Give a try to these -
http://polymaps.org/
http://www.amcharts.com/javascript-maps/
Raphael JS
You can try making an SVG version of your map and then implement it's clickiness with one of these libraries depending on which one you choose.
Here's one tutorial to do this with Raphael JS - http://parall.ax/blog/view/2985/tutorial-creating-an-interactive-svg-map
Make an image for each clickeable zone, like this:
Register to the click event of the img element from the page, this way:
var getAreaFromXY = function(x,y) {
// for each section colored map
// get pixel color on x,y (see http://stackoverflow.com/questions/8751020/how-to-get-a-pixels-x-y-coordinate-color-from-an-image)
// if the color is red, that is the zone
};
$(".post-text img").click(function(e) {
var area = getAreaFromXY(e.offsetX, e.offsetY);
});
The default stroke for creating SVG elements with raphaeljs seems to be black 1px. I can manually turn it off every time I create an element, but I rather set it as a default attribute "stroke: none" to the entire paper. Is it possible?
I encountered this problem a little while ago; I believe this is a library default. Whilst you could change this in your library source, doing so would make updating library versions difficult, so you might be better off disabling it in the code that calls Raphael.
If you're worried about this being verbose, you could use a delegate function that hides the 'defaulting hacks' as you build similar shapes.
It depends if this is just for defaults, or trying to cut down repeated code etc. You could create your own shape with defaults you want as a possible alternative is what I was thinking...
var paper = Raphael('mydiv',400,400);
Raphael.fn.myBlueCircle = function (x,y,r) {
this.circle(x,y,r).attr({fill: "#00f", stroke: "none"});
};
paper.myBlueCircle(100,100,100);
paper.myBlueCircle(150,200,100);
jsfiddle
You may also want to do the same for sets if those are to be used with the new elements reference
How do I bind a mouseover or any event for that matter to a drawn object on the canvas? For instance, I tried this:
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
ctx.beginPath();
//STEP ONE
var stepOneRec = ctx.rect(20, 60, 266, 50);
ctx.stroke();
stepOneRec.addEventListener("mouseover", function() { alert('it works!'); });
On one site I looked at it showed a method using Kinetic.js. If that's the only way, I'll use it, I just assume that there's a way to bind events to drawn elements without extra plug-ins. Sorry Canvas noob. I made a fiddle with my code here: http://jsfiddle.net/jyBSZ/2/
(I started this as a posted comment, then realized it's an actual answer.)
Unfortunately, in javascript on it's own, you can't. There are no canvas objects, just the canvas as a whole, and whatever you drew on to its context. Plugins like kinetic can make objects for you, but the whole point of canvas is that the browser can think of it as a single static image.
If you want to, you can bind mousemove events to the canvas, track its position and the position where you drew stuff, and imply on your own that it's over "that object" (effectively what the plugins do), but it's all mousemove events on a single canvas rather than mouseover events on components of it. (You could even make your event binding simulate a mouseover event for the "objects", but underneath, it's still based on checking movement and checking your own object setup.)
The objects drawn within a canvas element are not HTML elements, just pixels, and therefore will not throw DOM events the way that HTML elements would.
You would need to track the locations of your objects yourself and handle the canvas' onmousemove event in order to determine when the mouse is over one of your drawn objects.
you can use jCanvas, take a look here
i made a jsfiddle example for your problem.
just modify next callbacks for desired result
function mouseOut(layer){
$("#mouse-over-text").html('none options selected');
}
function mouseIn(layer){
$("#mouse-over-text").html(getTextForId(layer.name));
}
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.