2D overlapping rectangles occlusion - javascript

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...

Related

D3-Force-Quicker stabilization of nodes & transition of elements in projectiles

I have implemented the following version of the force diagram to show inter-cluster movement of nodes.
https://jsfiddle.net/Edwig_Noronha/67ey5rz0/
The nodes are grouped into four clusters. After the first initialization of the force diagram ends I call a function to transition the nodes from source to destination clusters.
function moveNodes() {
Object.keys(inputdata).forEach(function(key, index) {
svg.selectAll("circle.viewernodes" + index)
.each(function(d) {
d.type = d.destination;
});
});
viewersTransitioned = true;
force.start();
}
However, The stabilization of the first initialization of the force diagram takes about 35 seconds. Hence the transition happens after that much time.
Q1) is it possible to achieve a quicker stabilization of the force diagram with collision detection?
The transition of the nodes from source to destination clusters happens along a linear path.
Q2) Is it possible to make the nodes move along projectile paths?
To achieve quicker stabilization you can do one of two things in my experience.
Initialize the nodes X and Y values to be near to their end/goal state
Such as nodes[i].x = 500 etc, then calling the simulation start.
This would somewhat defeat the purpose of what you're trying to show in your example, unless you don't want the nodes to be shown moving to the groups and just be in them to begin with...
Stronger force
Have the force moving/pulling the nodes be stronger. This would require an essentially fundamental change to your approach to this example. Instead of just transitioning their positions, create custom forces within your force-layout that affect the appropriate nodes only based on their attributes. Place these forces in the center of your 'sorting circles' and they would attract the nodes appropriately.
See here for what something like this would look like: https://bl.ocks.org/mbostock/1021841

How to calculate the index of the tile underneath the mouse in an isometric world taking into account tile elevation

I have a tile-based isometric world and I can calculate which tile is underneath specific (mouse) coordinates by using the following calculations:
function isoTo2D(pt:Point):Point{
var tempPt:Point = new Point(0, 0);
tempPt.x = (2 * pt.y + pt.x) / 2;
tempPt.y = (2 * pt.y - pt.x) / 2;
return(tempPt);
}
function getTileCoordinates(pt:Point, tileHeight:Number):Point{
var tempPt:Point = new Point(0, 0);
tempPt.x = Math.floor(pt.x / tileHeight);
tempPt.y = Math.floor(pt.y / tileHeight);
return(tempPt);
}
(Taken from http://gamedevelopment.tutsplus.com/tutorials/creating-isometric-worlds-a-primer-for-game-developers--gamedev-6511, this is a flash implementation but the maths is the same)
However, my problem comes in when I have tiles that have different elevation levels:
In these scenarios, due to the height of some tiles which have a higher elevation, the tiles (or portions of tiles) behind are covered up and shouldn't be able to be selected by the mouse, instead selecting the tile which is in front of it.
How can I calculate the tile by mouse coordinates taking into account the tiles' elevation?
I'm using a javascript and canvas implementation.
There is a technique of capturing object under the mouse on a canvas without needing to recalculate mouse coordinates into your "world" coordinates. This is not perfect, has some drawbacks and restrictions, yet it does it's job in some simple cases.
1) Position another canvas atop of your main canvas and set it's opacity to 0. Make sure your second canvas has the same size and overlaps your main one.
2) Whenever you draw your interactive objects to the main canvas, draw and fill the same objects on the second canvas, but using one unique color per object (from #000000 to #ffffff)
3) Set mouse event handling to the second canvas.
4) Use getPixel on the second canvas at mouse position to get the "id" of the object clicked/hovered over.
Main advantage is WYSIWYG principle, so (if everything is done properly) you can be sure, that objects on the main canvas are in the same place as on the second canvas, so you don't need to worry about canvas resizing or object depth (like in your case) calculations to get the right object.
Main drawback is need to "double-render" the whole scene, yet it can be optimized by not drawing on the second canvas when it's not necessary, like:
in "idling" scene state, when interactive objects are staying on their places and wait for user action.
in "locked" scene state, when some stuff is animated or smth. and user is not allowed to interact with objects.
Main restriction is a maximum number of interactive objects on the scene (up to #ffffff or 16777215 objects).
So... Not reccomended for:
Games with big amount of interactive objects on a scene. (bad performance)
Fast-paced games, where interactive objects are constantly moved/created/destroyed.(bad performance, issues with re-using id's)
Good for:
GUI's handling
Turn-based games / slow-paced puzzle games.
Your hit test function will need to have access to all your tiles in order to determine which one is hit. It will then perform test hits starting with the tallest elevation.
Assuming that you only have discreet (integer) tile heights, the general algorithm would be like this (pseudo code, assuming that tiles is a two-dimensional array of object with an elevation property):
function getTile(mousePt, tiles) {
var maxElevation = getMaxElevation(tiles);
var minElevation = getMinElevation(tiles);
var elevation;
for (elevation = maxElevation; elevation >= minElevation; elevation--) {
var pt = getTileCoordinates(mousePt, elevation);
if (tiles[pt.x][pt.y].elevation === elevation) {
return pt;
}
}
return null; // not tile hit
}
This code would need to be adjusted for arbitrary elevations and could be optimized to skip elevation that don't contain any tiles.
Note that my pseudocode ignores vertical sides of a tile and clicks on them will select the (lower elevation) tile obscured by the vertical side. If vertical tiles need to be accounted for, then a more generic surface hit detection approach will be needed. You could visit every tile (from closest to farthest away) and test whether the mouse coordinates are in the "roof" or in one of the viewer facing "wall" polygons.
If map is not rotatable and exatly same as picture you posted here,
When you are drawing polygons, save each tile's polygon(s) in a polygon array. Then sort the array only once using distance of them(their tile) to you(closest first, farthest last) while keeping them grouped by tile index.
When click event happens, get x,y coordinates of mouse, and do point in polygon test starting from first element array until last element. When hit, stop at that element.
No matter how high a tile is, will not hide any tile that is closer to you(or even same distance to you).
Point in polygon test is already solved:
Point in Polygon Algorithm
How can I determine whether a 2D Point is within a Polygon?
Point in polygon
You can even check every pixel of canvas once with this function and save results into an 2d array of points, vect2[x][y] which gives i,j indexes of tiles from x,y coordinates of mouse, then use this as a very fast index finder.
Pros:
fast and parallelizable using webworkers(if there are millions of tiles)
scalable to multiple isometric maps using arrays of arrays of polygons sorted by distance to you.
Elevation doesnt decrease performance because of only 3 per tile maximum.
Doesn't need any conversion to isometric to 2d. Just the coordinates of corners of polygons on canvas and coordinates of mouse on the same canvas.
Cons:
You need coordinates of each corner if you haven't already.
Clicking a corner will pick closest tile to you while it is on four tiles at the same time.
The answer, oddly, is written up in the Wikipedia page, in the section titled "Mapping Screen to World Coordinates". Rather than try to describe the graphics, just read the section three times.
You will need to determine exactly which isomorphic projection you are using, often by measuring the tile size on the screen with a ruler.

Rotate a Two.js object in its position

I have a large circle with smaller ones inside made using two.js.
My problem is that these two do not rotate in their own place but in the top left axis.
I want the group of circles (circlesGroup) rotate only inside the large one in a static position. The circlesGroup and the large circle are grouped together as rotatoGroup.
two.bind('update', function(frameCount, timeDelta) {
circlesGroup.rotation = frameCount / 120;
});
two.bind('update', function(frameCount, timeDelta) {
rotatoGroup.rotation = frameCount / 60;
});
The whole code is in CodePen.
All visible shapes when invoked with two.make... ( circles, rectangles, polygons, and lines ) are oriented in the center like this Adobe Illustrator example:
When this shape's translation, rotation, or scale change those changes will be reflected as transformations about the center of the shape.
Two.Groups however do not behave this way. Think of them as display-less rectangles. They're origin, i.e group.translation vector, always begins at (0, 0). In your case you can deal with this by normalizing the translation your defining on all your circles.
Example 1: Predefined in normalized space
In this codepen example we're defining the position of all the circles around -100, 100, effectively half the radius in both positive-and-negative x-and-y directions. Once we've defined the circles within these constraints we can move the whole group with group.translation.set to place it in the center of the screen. Now when the circles rotate they are perceived as rotating around themselves.
Example 2: Normalizing after the fact
In this codepen example we're working with what we already have. A Two.Group that contains all of our shapes ( the bigger circle as well as the array of the smaller circles ). By using the method group.center(); ( line 31 ) we can normalize the children of the group to be around (0, 0). We can then change the translation of the group in order to be in the desired position.
N.B: This example is a bit complicated because it invokes underscore's defer method which forces the centering of the group after all the changes have been registered. I'm in the process of fixing this.

D3 force layout: How to maintain a given minimum distance between nodes?

I am using force layout. New d3 nodes are created by clicking inside a div element. The node is created at the point of click. The nodes are rectangles of size 50 pixels x 50 pixels. Immediately after creating a node, I set its fixed property to true so that it does not move on its own. I am not calling force.drag. The nodes can be moved by holding down ctrl key and dragging the node. An edge can be created by dragging mouse (without holding ctrl key) from one node to the other.
Now, I want to add the following feature.
The closest distance between any two nodes should be more than a certain minimum. You can assume any positive value for the minimum distance. Let us assume 100 pixels. When any new node is created too close to an existing node, then the nodes should move so that the distance between any two nodes becomes more than 100 pixels. Similarly, when one node is moved and brought too close to another, then also the nodes should move to maintain minimum 100 pixels distance.
There is no condition on which nodes to move and in which direction. One way is to check coordinates and distances and then calculate which nodes to move, how much, in what direction and execute code accordingly. But, is there a simpler way in d3?
Consider just using force.linkDistance() and force.linkStrength() to achieve this. linkDistance represents your minimum distance constraint, and linkStrength (in the range of [0, 1]) determines how 'rigid' the link distance is, or how much linkDistance can be overridden by the simulation.
force.linkDistance
force.linkStrength

KineticJS: Draw Arrow between two shapes

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!

Categories