KineticJS: Draw Arrow between two shapes - javascript

So, I want to create a finite state machine-visualizer/editor with the help of kineticjs and i'm stumbling with the following scenario:
I have two "nodes", let's say circle-objects (grouped with a label) which are draggable on my stage. Now I wan't to click on one circle, hold the mouse and move it and add a connection (an arrow, for simplicities sake) between the two shapes.
So it would be great to have any hints on how to accomplish this for I haven't found a solution yet.
To specify it: The nodes themselves should stay draggable. My thought was: Add a black circle and a white circle with a slightly smaller radius, group them. then on dragstart white circle -> drag node, on dragstart black circle -> draw arrow.
The Problem is how to draw an arrow starting from one shape and following the mouse to it's target (which can be another nodegroup => connection to this group or a blank point of the stage => an overlay opens which lets the user choose another node to draw or cancel the drawing).
I hope this is somewhat clear to understand. For more information please feel free to ask me.
Best regards,
Dominik
p.s.: The behaviour seems to be exactly like the behaviour lucidchart (dot com) uses when creating diagrams, so maybe you understand what I want to achieve better looking at their demo here: https://www.lucidchart.com/demo .

First off, for simplicity's sake here is a fiddle on how to draw a basic Line with your mouse and KineticJS: http://jsfiddle.net/projeqht/fF3hh/
Let's say you already have two circles on the stage, and you need to draw a line to connect them.
We can use e.targetNode to select the nodes on each event (mousedown, mouseup), for example:
layer.on("mousedown", function (e) {
var nodeDown = e.targetNode;
}
layer.on("mouseup", function (e) {
var nodeUp = e.targetNode;
}
We need to check if the parent of nodeDown is a Kinetic.Group or something else.
If the target node nodeDown has a Kinetic.Group for a parent, we can use this Group to store the new line, and the 2nd target node nodeUp.
If the target node nodeUp does not have a Kinetic.Group for a parent, we need to see if nodeUp has a Group for a parent. If nodeUp has a Kinetic.Group for a parent, then we can use that Group to store the new line, and the first target node nodeDown.
If neither nodeDown or nodeUp have a group for a parent, then we will need to create a new group for them and add all 3 shapes (2 circles and a line) to that new group.
Use this tutorial to learn how to move shapes from 1 group to another: http://www.html5canvastutorials.com/kineticjs/html5-canvas-move-shape-to-another-container-with-kineticjs/
Also, if you move a shape from one group to another, you may want to remove() or destroy() the extra group if it is no longer needed.
While drawing a Line, you will have to disable dragging the shapes, so that you can drag and draw with a mouse. You can do that by doing something similar to this:
function stopDrag() {
for (var i=0; i<layer.children.length; i++) {
layer.children[i].setDraggable(false);
}
}
function startDrag() {
for (var i=0; i<layer.children.length; i++) {
layer.children[i].setDraggable(true);
}
}
This will make all the children of layer draggable and undraggable, but you might want to limit that by being more specific than select layer.children. A nice trick I liked to use here was to name all groups that were draggable as "draggable_shapes" and then use var draggableArray = stage.get('.draggable_shapes') to select all the groups that are allowed to be dragged, then you could loop through that array and setDraggable().
Another point to note is that the X and Y coordinates of the Line will be a bit tricky to calculate, depending on if it has a Group as a parent or a Layer. If the Line is grouped, line's coordinates will be relative to the Group position, or else the Line's coordinates will be relative to the Stage (top left corner).
This will get you started on connecting a line with two different circles. It's up to you to do the coordinate logic if you want the lines to only connect on the outer rim of the circles.
Maybe you might want to add a transparent rectangle (attribute opacity: 0) behind each circle, so that on mousedown with the rectangle, you will call drawLine() to start drawing a line. Or else if the user clicks the circle, it will drag the group. At least that has similar functionality to the lucid charts application.
Custom Hit Function (http://www.html5canvastutorials.com/kineticjs/html5-canvas-kineticjs-custom-hit-function-tutorial/) would probably be a cleaner way to do this but I'm not 100% on using Custom Hit Functions, someone else might know better.
Let me know if you need further help. Good luck!

Related

2D overlapping rectangles occlusion

I'm looking for an algorithm that can find intersecting rectangles that overlap each other.
The catch is that my data structure is similar to a quadtree with bounding boxes instead of points.
I'm doing a basic rectangle intersection check but the problem is as I zoom into the tree the child nodes get detected and the parents, I would like to exclude the parent if its fully occluded by a child for the given camera rectangle.
zoom animation
As you can see from the image above as the camera rectangle (black box) sits inside the green node the purple node is still highlighted (filled), consequently as I zoom more and more the parents are always highlighted, even though the camera rectangle can be fully filled with child nodes only.
This makes sense since the camera rectangle is still inside the parent but I have searched and thought about the problem for a while and can't seem to figure out an elegant solution. There seems to be several ways of doing this for 3D spaces but I can't find anything simple for 2D AABB rectangles.
A few solutions that I thought of:
Subtract the child nodes from the parents resulting in concave polygons and then perform polygon intersection.
Use the fill color to check which rectangles are visible, therefore occluding the ones behind.
Perform raycasting or sub-division and check which is the smallest node for a given section.
Is there a better way to do this?
Thank you
Update 1
I have solved the problem by subdividing the camera into smaller sections and for each section the smallest intersecting node is found. This works but there must be a more efficient and cleaner way of doing this.
Update 2
Thank you Trentium for your answer. I can clearly see that an algorithm like that would be a lot more performant than what I'm currently doing.
Eventually I will implement it as splitting a rectangle into smaller rectangles and not polygons sounds like a fun challenge.
Also, I did some very non scientific benchmarks on the current approach and it takes 0.5ms-1ms to both filter and draw everything, so for now performance is still not a concern.
Suggest considering a variation of your first solution proposed "Subtract the child nodes from the parents resulting in concave polygons and then perform polygon intersection."
Specifically, if the immediate children are wholly contained within the parent, then suggest that for each rectangle, that an associated array of visible residual rectangles also be maintained. And then use this array of residual rectangles as the means of determining if the camera / viewport rectangle includes the parent or not.
For example, let's say the parent rectangle is (0,0) - (100,100) and there is an initial child rectangle added at (75,75)-(100,100). The data structure will appear as...
parent.rectangle = {0,0,100,100}
parent.visible = [ {0,0,100,75}, {0,75,75,100} ]
child1.rectangle = {75,75,100,100}
child1.visible = [ {75,75,100,100} ]
...and then if a second child comes along, say at {50,50,75,90}, then this new child is checked against each residual rectangle in the parent.visible array, subdividing the parent visible rectangles further as necessary...
parent.rectangle = {0,0,100,100}
parent.visible = [ {0,0,100,50}, {0,50,50,75}, {75,50,100,75}, {0,75,50,100}, {50,90,75,100} ]
child1.rectangle = {75,75,100,100}
child1.visible = [ {75,75,100,100} ]
child2.rectangle = {50,50,75,90}
child2.visible = [ {50,50,75,90} ]
This method will add a bit of work up front adjusting the immediate parent's visible rectangles as children are added, but should greatly reduce the amount of rectangle intersection tests relative to the current algorithm that involves subdivision of the camera / viewport. Plus this proposed algorithm only makes use of rectangle-to-rectangle intersection tests (both when adding a child to a parent, and when testing the camera / viewport intersections!) rather than the suggested rectangle-to-polygon tests...

Moving multiple objects simultaneously in createJS/easelJS

I've been using easelJS within the createJS framework for a project and have enjoyed it a lot until recently hitting a roadblock. I have multiple objects that I'd like to move simultaneously when one of the group is dragged. Here is my current situation:
What I'd like to do is when the red circle is moved, the red crosshairs would also move so that they appear to be "locked" to the circle. The same with the green circle.
I have been able to accomplish something very close to this by adding the circles and crosshairs to a container, as mentioned in the answers to this question:
Easeljs Scrollable Container
But the issue I encounter is that the container is actually a rectangle, such that I can click anywhere between the circle and crosshairs to move the various objects contained within the container. Instead I would like for the objects to be moved only when I click on a circle.
Does anyone have any idea how to accomplish this? Am I correct in thinking this can be accomplished somehow with easelJS containers?
Containers should be fine. You can turn off mouseEnabled on the cross-hair in order to make it ignore the mouse.
You could also just store the offset for each cross-hair/circle, and just set the cross-hair position when the circle moves.
Here is a quick demo:
http://jsfiddle.net/lannymcnie/kah9of6e/
// Set the offset when the circle is pressed
circle.on("mousedown", function(e) {
circle.offset = new createjs.Point(crosshair.x-circle.x, crosshair.y-circle.y);
});
// Add drag and drop to each shape
circle.on("pressmove", handleDrag);
crosshair.on("pressmove", handleDrag);
function handleDrag(e) {
// Move the target to the mouse
e.target.x = e.stageX; e.target.y = e.stageY;
// If the target is the circle, also move the cross-hair
if (e.target == circle) {
// Move the cross-hair
crosshair.x = circle.x + circle.offset.x;
x.y = circle.y + circle.offset.y;
}
}

javascript picture.js - Moving grouped items around the canvas

I basically have a rectangle with a PointText item overlayed on top of it. I currently have a working sample where I can move items around on the canvas by using sample code from the paperjs-v0.9.23\examples\Paperjs.org\HitTesting.html which has been really useful. However, I want to treat the rectangle and the text as one logical grouping to be moved around.
Please see the link below to see what I mean:
http://jsfiddle.net/svt9wa9f/6/
In the HitTesting.html example it has this within the onMouseDown event: `
if (hitResult)
{
path = hitResult.item;
if (hitResult.type == 'segment')
{
segment = hitResult.segment;
} else if (hitResult.type == 'stroke')
{
var location = hitResult.location;
segment = path.insert(location.index + 1, event.point);
path.smooth();
}
}
`
I was hoping I could modify this to work with the groups, but it seems this type of hitesting doesn't work. Everytime I click within any object I just get the 'fill' type. So I was then thinking I would have to do a linear search through the array of groups performing a hittest just to see which item is within the group. Or a hashmap keyd on items, and the value as the group. But there must be an easier way?
Within the fiddle example, the group code is commented out because. When you first execute it you can't see the items, you have to hover the mouse pointer of the canvas to get them to appear. Any help on this would be appreciated.
Ideally, I just need to be able to move these groups around through drag events, and extend the code to be able to determine whilst dragging if I am over another grouping. But that is further down the line.
Thank you for your time.

How do I make specific elements appear on top of others in D3.js?

I have a plot in D3 where I draw some circles and then some ellipses afterwards to show the median. I have a 1.5 second delay on my median, to try and draw it after the circles have appeared, but I still run into problems.
Here is a screenshot of an example: http://puu.sh/8csEK.png
The circle to the far right are behind it's median, the rest of the circles are all in front. When areas are crowded you cannot see the median anymore.
I have even tried using the following on transitions of my circles, but it's no use:
.each("end", <call function to draw ellipses>)
So my question is, how do i make sure that my ellipses are drawn on top of my circles?
I have a function that draws my ellipses and a function that draws my circles right now.
I'm assuming that you're using SVG to render your elements. In SVG, the display order is the order of drawing/appending to the DOM. That is, the element you append first is drawn at the back, the element you append last at the front. Child elements (e.g. something underneath a g) are drawn when their parent elements are drawn.
To make sure that groups of elements have the right order, it's usually easiest to add them to different SVG groups that are drawn in the right order. In code, this looks something like this.
var circles = svg.append("g");
var ellipses = svg.append("g");
// ...
ellipses.append(...); // this element appears in the front although it is drawn
// earlier because it is appended to the group appended last
circles.append(...); // this element appears behind the one appended to ellipses

Raphael JS move/rotate group of drawings as one?

I have a little tool that draws up a grid of circles(representing holes) that allows the user to add text and lines to these circles. Right now I have it set up so if the user clicks on any of the holes then wherever the hole is moved so is every other element on the Paper object. What I am trying to implement next is the ability to rotate everything as one object. I realize that for this to work that I need to know the central point of all the objects, which I can easily get.
What I want to know is should I draw everything on another object. This object will act as another Paper object of sorts, but will only serve for movement and rotation. Any click events on the holes drawn on the object will be passed on to the parent (i.e. the pseudo-paper object everything is drawn on). Is this possible? If so how would I draw everything onto say, a rectangle? And if not what would be the best way to go implementing it?
What you need is a Set. You create it, push objects to it, and then treat it as an entire group, in your case by applying transformations.
Example:
var elements = paper.set();
if (!view.text) {
view.text = App.R.text(0, 0, this.value);
view.text.attr({
'font-size': font_size,
});
elements.push(view.text);
}
elements.transform('something');
Note that you can also bind events to this entire set.

Categories