Disable dragging nodes outside of area in cytoscape.js - javascript

I am using cytoscape.js.
I have allowed the user to drag the nodes, but right now it is also possible to drag the node outside of the 'canvas'.
I have looked at the options at http://js.cytoscape.org/#core/initialisation but I don't know how to make sure the nodes cannot leave the visible area <div id="cy"></div>.

Try the automove extension: https://github.com/cytoscape/cytoscape.js-automove
It lets you set rules for node position, like constraining within the current viewport or a specified region.

I know this is long time ago, but today I have the same problem and this is my solution, just move the node back in.
cy.on('mouseup', function (e) {
let tg = e.target;
if (tg.group != undefined && tg.group() == 'nodes') {
let w = cy.width();
let h = cy.height();
if (tg.position().x > w) tg.position().x = w;
if (tg.position().x < 0) tg.position().x = 0;
if (tg.position().y > h) tg.position().y = h;
if (tg.position().y < 0) tg.position().y = 0;
}
})

Related

Generate random divs without overlapping

I am trying to create a simple project, using TypeScript and React, to be able to generate a new <div> that has random width and height (min/max 50/300px), and is randomly placed inside a wrapper (which for this example is 1920x1080).
Goal is to check if newly generated div rectangle is overlapping or not existing divs.
If not, create the element, else generate new position and size and check again, goal being to fill up the wrapper as much as possible since min/max size of div is set and have no overlapping ones.
So far I managed to write code for generating random positions and sizes, but I am stuck at checking collisions and sending message when empty space isn't left.
const [count,setCount]=useState(0)
const wrapper = document.getElementById('wrapper');
var posX:number,posY:number,divSizeH:number,divSizeW:number;
var willOverlap:boolean=false;
function createRandomRectangle(){
divSizeW = Math.round(((Math.random()*250) + 50));
divSizeH = Math.round(((Math.random()*250) + 50));
if (wrapper!=null) {
const width = wrapper.offsetWidth , height = wrapper.offsetHeight;
posX = Math.round( (Math.random() * ( width - divSizeW )) );
posY = Math.round( (Math.random() * ( height - divSizeH )) );
//checking collision
document.querySelectorAll('.Rectangle').forEach(element=>{
var r2 = element.getBoundingClientRect();
//my attempt //if(((posX>=r2.x&&posX<=r2.right)&&(posY>=r2.top&&posY<=r2.bottom))||((posX+divSizeW>=r2.x&&posX+divSizeW<=r2.right)&&(posY>=r2.top&&posY<=r2.bottom))||((posX>=r2.x&&posX<=r2.right)&&(posY+divSizeH>=r2.top//&&posY+divSizeH<=r2.bottom))||((posX+divSizeW>=r2.x&&posX+divSizeW<=r2.right)&&(posY+divSizeH>=r2.top&&posY+divSizeH<=r2.bottom))){
//copied from someone elses code for checking collisions
if((posX <= r2.x && r2.x <= posX+divSizeW) && (posY <= r2.y && r2.y <= posY+divSizeH) ||
(posX <= r2.x && r2.x <= posX+divSizeW) && (posY <= r2.bottom && r2.bottom <= posY+divSizeH) ||
(posX <= r2.x+r2.height && r2.x+r2.height <= posX+divSizeW) && (posY <= r2.y+r2.width && r2.y+r2.width <= posY+divSizeW) ||
(posX <= r2.x+r2.height && r2.x+r2.height <= posX+divSizeW) && (posY <= r2.y && r2.y <= posY+divSizeW)){
willOverlap=true;
while(willOverlap){
posX = Math.round((Math.random() * ( width- divSizeW)));
posY = Math.round((Math.random() * ( height- divSizeH)));
divSizeW = Math.round(((Math.random()*250) + 50));
divSizeH = Math.round(((Math.random()*250) + 50));
if(!(((posX>=r2.x&&posX<=r2.right)&&(posY>=r2.top&&posY<=r2.bottom))||((posX+divSizeW>=r2.x&&posX+divSizeW<=r2.right)&&(posY>=r2.top&&posY<=r2.bottom))||((posX>=r2.x&&posX<=r2.right)&&(posY+divSizeH>=r2.top&&posY+divSizeH<=r2.bottom))||((posX+divSizeW>=r2.x&&posX+divSizeW<=r2.right)&&(posY+divSizeH>=r2.top&&posY+divSizeH<=r2.bottom)))){
willOverlap=false;
}
}
}
})
}
}
//if there is no more place send message and dont create....
const newDiv = document.createElement('div');
newDiv.classList.add('Rectangle');
newDiv.style.width=divSizeW+"px";
newDiv.style.height=divSizeH+"px";
newDiv.style.left=posX+"px";
newDiv.style.top=posY+"px";
boxxy?.appendChild(newDiv);
setCount(count+1);
}
As you started to guess, you need to repeat your code (here new position & size + check collision) in case you find a collision.
But since you implement the repetition within the forEach loop of your list of already existing Rectangles, the newly generated position & size is not re-checked against the previous Rectangles (beginning of the list). Hence you may end up with still colliding elements.
Another issue, much more difficult, is to detect when your wrapper is "full", or rather when the empty space can no longer take any new of your Rectangles, given their minimum size. Because this step is so much more complex, let's leave it aside for now, and use a much more simplistic approach, by using a maximum number of repetitions.
With this simplification, your algorithm could look like:
const existingRectangles = document.querySelectorAll('.Rectangle');
let repCount = 0; // Number of repetitions
do {
var overlapping = false;
var newPositionAndSize = generateRandomPositionAndSize();
// Check for collision with any existing Rectangle.
// Use a classic for loop to be able to break it
// on the first collision (no need to check further)
for (let i = 0: i < existingRectangles.length; i += 1) {
if (checkCollision(existingRectangles[i], newPositionAndSize)) {
overlapping = true;
repCount += 1;
break; // No need to check the rest of the list
}
}
} while (overlapping && repCount < 1000);
if (overlapping) {
// If still overlapping, it means we could not find
// a new empty spot (here because of max repetitions reached)
showMessageWrapperIsFull();
} else {
// If we checked through the entire list without raising
// the overlapping flag, it means we have found
// a suitable empty spot
createNewRectangleAndInsertIt(newPositionAndSize);
}

Editing terrain generated randomly in Javascript with cursor

I'm making an University project and at the moment I have this randomly generated terrain. It simply gets a random value with some limits to it, puts it into an array and then i use lineTo(i,array[i]) to show me the world.
It works as intended, but now I'd like to be able to edit this "map" with my cursor if I enter let's call it "Edit Mode". At the moment I have a button that makes me enter this mode and then I check this:
function mouseMoveHandler(e) {
var relativeX = e.clientX - canvas.offsetLeft;
var relativeY = e.clientY - canvas.offsetTop;
if(relativeX < canvas.width && dragging && editing && relativeY < world[relativeX] + 20 && (relativeY > world[relativeX])) {
removeWorldPart(relativeX);
}
function removeWorldPart(coordinate){
for(var i = 0; i< 5; i++) {
if(coordinate > world[coordinate] + 5 || coordinate > world[coordinate] - 5) {
world[coordinate+i] += 6-i;
world[coordinate-i] += 6-i;
}
}
So what I'm trying to achieve (numbers can be set easily) is that like a little circle from the array that stores all those heights of my random world gets modified a bit.
It does change the heights in "world[]" but not really as a circle and probably I'm missing something else.
Any suggestions?

KineticJS - Swapping shape positions upon contact/mousemove trigger

I'm using KineticJS and trying to get something done that seems simple enough: trying to get a shape to change position with the shape that's currently being dragged.
The idea is that:
• You pick up a shape on the canvas. This triggers a mousedown event listener, which saves the current position of the shape you've picked up.
• While holding the shape, if you trigger the mouseover on another shape, that shape's event gets triggered and swaps its position, based on the current shape's saved position.
Here is the relevant code I've written to try to get that working:
Board setup:
Simply just setting up the board and calling the needed functions here. I'm not doing anything with the stage, gameBoard, or ctx yet (part of it was for when I tried to get drawImage to work on multiple canvases, but have abandoned that for now).
class BoardView {
constructor(stage, gameBoard, ctx) {
this.stage = stage;
this.gameBoard = gameBoard;
this.ctx = ctx;
this.orbs = [[], [], [], [], []];
this.setupBoard();
}
Board setup functions:
This is where I go about setting up the board and giving each Kinetic Circle the attributes it needs to render on the Layer. Maybe I'm missing something obvious here?
setupBoard () {
for (let colIdx = 0; colIdx < 5; colIdx++) {
this.addRow(colIdx);
}
this.renderBoard();
}
addRow (colIdx) {
for (let rowIdx = 0; rowIdx < 6; rowIdx++) {
let orbType = Math.round(Math.random() * 5);
let orbColor;
if (orbType === 0) {
orbColor = "#990000";
} else if (orbType === 1) {
orbColor = "#112288";
} else if (orbType === 2) {
orbColor = "#005544";
} else if (orbType === 3) {
orbColor = "#776611";
} else if (orbType === 4) {
orbColor = "#772299";
} else {
orbColor = "#dd2277";
}
let orbject = new Kinetic.Circle({
x: (rowIdx + 0.5) * 100,
y: (colIdx + 0.5) * 100,
width: 100,
height: 100,
fill: orbColor,
draggable: true
});
this.orbs[colIdx].push(orbject);
}
}
Board render function:
This is where I add all the Kinetic Circle objects into new layers, and give those layers all its own attributes to work with when I call the event handlers. I also set up the event handlers here after adding the layers to the stage. Am I perhaps messing this up by adding too many layers?
renderBoard () {
for (let row = 0; row < this.orbs.length; row++) {
for (let orb = 0; orb < this.orbs[row].length; orb++) {
let layer = new Kinetic.Layer();
layer.add(this.orbs[row][orb]);
layer.moving = false;
layer.orbId = `orb${row}${orb}`;
layer.pos = [this.orbs[row][orb].attrs.x, this.orbs[row][orb].attrs.y];
this.stage.add(layer);
layer.on("mousedown", this.handleMouseDown);
layer.on("mouseup", this.handleMouseUp);
layer.on("mouseout", this.handleMouseOut);
layer.on("mousemove", this.handleMouseMove);
}
}
}
Mouse event handlers:
This is where I think I'm having my main problem. How I handle the event of moving the mouse to change orbs, perhaps I'm doing something terribly wrong?
handleMouseDown (e) {
window.currentOrb = this;
console.log(window.currentOrb.orbId);
this.moving = true;
}
//handleMouseUp (e) {
// window.currentOrb = undefined;
// this.moving = false;
//}
//handleMouseOut (e) {
//}
handleMouseMove (e) {
if (window.currentOrb !== undefined && window.currentOrb.orbId != this.orbId) {
this.children[0].attrs.x = window.currentOrb.pos[0];
this.children[0].attrs.y = window.currentOrb.pos[1];
this.children[0].draw();
} else {
}
}
}
module.exports = BoardView;
I've tried looking at the KineticJS docs and many StackOverflow answers as I could in hopes of finding a solution that would work for me, but nothing I've seen and tried so far (including the suggestions that came up as I wrote this question) seemed to be of help, and I'm aware the way I've gone about this so far is probably far from the best way to accomplish what I want, so I'm open to any suggestions, pointers, answered questions, or whatever can point me in the right direction to what I'm missing in order to get this to work.
In case this is helpful, here is also a visualization of how things look when the board is rendered.
The circles are all Kinetic Circles (orbs for the purpose of what I'm going for), and clicking and dragging one to another, the one that isn't being dragged but hovered over should move to the original position of the dragged circles.
Thanks!
EDIT:
I made a few changes to the code since then. First off, I changed adding many layers to the stage, to just one:
renderBoard () {
let layer = new Kinetic.Layer();
for (let row = 0; row < this.orbs.length; row++) {
for (let orb = 0; orb < this.orbs[row].length; orb++) {
layer.add(this.orbs[row][orb]);
this.orbCanvases.push(orbCanvas.id);
}
}
this.stage.add(layer);
}
I instead added listeners to the orb objects instead:
addRow (colIdx) {
for (let rowIdx = 0; rowIdx < 6; rowIdx++) {
//same code as before
let orbject = new Kinetic.Circle({
x: (rowIdx + 0.5) * 100, y: (colIdx + 0.5) * 100,
width: 100, height: 100,
fill: orbColor, draggable: true, pos: [rowIdx, colIdx]
});
orbject.on("mousedown", this.handleMouseDown);
orbject.on("mouseup", this.handleMouseUp);
orbject.on("mouseout", this.handleMouseOut);
orbject.on("mousemove", this.handleMouseMove);
this.orbs[colIdx].push(orbject);
}
}
This has had the benefit of making drag and drop much much faster where before, it was going very slow, but I still can't get my objects to swap position.
To be clear, my main issue is knowing which x, y values I should be changing. At the moment in handleMouseMove, I've been trying to change:
e.target.attrs.x = newX;
e.target.attrs.y = newY;
// newX and newY can be any number
However, no matter what I change it to, it has no effect. So it would help me to know if I'm changing the wrong thing/place, for example, maybe I'm supposed to be changing the Kinetic Circle from the array I've stored? Thanks again.
EDIT 2:
I think I got it! However, I had to take this.orbs and set it in the window at window.orbs, and to test it I did:
window.orbs[0][0].x(450);
window.orbs[0][0].draw();
And this caused the x position to change. But putting it in a window does not seem like good practice?
EDIT 3:
I got the orbs to now swap continuously, except for when it's the same orb being swapped again while mouseover is continuing to fire. At mouseup however, it can be swapped again. I also had to set up multiple layers again to get the mouseover events to work while holding another orb, but the performance seems to have improved a little.
I'm going to try and figure out how to get them to be able to swap continuously on the same mouse hold, but in the meantime, here is the code I wrote to achieve this:
addRow (colIdx) {
for (let rowIdx = 0; rowIdx < 6; rowIdx++) {
// same code as before, changed attr given to Kinetic.Circle
let orbject = new Kinetic.Circle({
x: (rowIdx + 0.5) * 100, y: (colIdx + 0.5) * 100,
width: 100, height: 100,
fill: orbColor, draggable: true, orbId: `orb${colIdx}${rowIdx}`
});
}
}
handleMouseDown (e) {
window.currentOrb = window.orbs[e.target.attrs.orbId];
window.newX = e.target.attrs.x;
window.newY = e.target.attrs.y;
}
Mouse down saving currentOrb by ID and its X and Y
handleMouseUp (e) {
window.currentOrb.x(window.newX);
window.currentOrb.y(window.newY);
window.currentOrb.parent.clear();
window.currentOrb.parent.draw();
window.currentOrb.draw();
window.currentOrb = undefined;
for (let i = 0; i < 5; i++) {
for (let j = 0; j < 6; j++) {
window.orbs[`orb${i}${j}`].draw();
}
}
}
When mouse is released, currently, all orbs are redrawn so they can all be used. I plan to refactor this so only orbs hovered over have this change.
handleMouseMove (e) {
if (window.currentOrb !== undefined && (window.currentOrb.attrs.orbId !== e.target.attrs.orbId)) {
window.orbMove.pause();
window.currentTime = 0;
window.orbMove.play();
let targOrbX = e.target.attrs.x;
let targOrbY = e.target.attrs.y;
// This is the target orb that's being changed's value
// We're storing this in targOrb
e.target.x(window.newX);
e.target.y(window.newY);
e.target.parent.clear();
e.target.parent.draw();
e.target.draw();
// Here we have the previously set current orb's position becoming
// the target orb's position
window.newX = targOrbX;
window.newY = targOrbY;
// Now that the move is made, we can set the newX and Y to be the
// target orb's position once mouseup
}
}
Orb swapping logic which works for passing over orbs once, but not if they are passed over again in the same turn.
When does the "hover" officially take place?
When the mouse event's position enters the second orb? If yes, hit test the mouse vs every non-dragging orb:
// pseudo-code -- make this test for every non-dragging orb
var dx=mouseX-orb[n].x;
var dy=mouseY-orb[n].y;
if(dx*dx+dy*dy<orb[n].radius){
// change orb[n]'s x,y to the dragging orb's x,y (and optionally re-render)
}
When the dragging orb intersects the second orb? If yes, collision test the dragging orb vs every non-dragging orb:
// pseudo-code -- make this test for every non-dragging orb
var dx=orb[draggingIndex].x-orb[n].x;
var dy=orb[draggingIndex].y-orb[n].y;
var rSum=orb[draggingIndex].radius+orb[n].radius;
if(dx*dx+dy*dy<=rSum*rSum){
// change orb[n]'s x,y to the dragging orb's x,y (and optionally re-render)
}
BTW, if you drag an orb over all other orbs, the other orbs will all stack on the dragging orbs original position -- is that what you want?

How to get visible height for overflow element in jQuery

I have an element who's overflow is hidden until a function is called. I am trying to determine if the mouse is hovering over this overflow material after I set overflow: visible;, but instead, it tells me that my mouse is hovering on the overflow content even when it's still invisible.
Is there a way to check visible height in jQuery? Here is what I was trying:
off = $(curSub).offset();
xSubStart = parseInt(off.left);
ySubStart = parseInt(off.top);
xSubEnd = xSubStart + parseInt($(curSub).width());
ySubEnd = ySubStart + parseInt($(curSub).height());
if ( (x >= xStart && x <= xEnd && y >= yStart && y <= yEnd) ||
(x >= xSubStart && x <= xSubEnd && y >= ySubStart && y <= ySubEnd) ) {
// display menu
$(cur).css('overflow', 'visible');
match = true;
}
The xStart, xEnd, yStart, and yEnd variables are defined above that code and work just fine. I believe the problem is that the jQuery function width(), height(), outerWidth(), and outerHeight() don't test to see if the element is visible.
Is there anyway to achieve this? I thought about just moving it from hidden to visible physically with top and left specifications, but I think this way would be cleaner if it's possible.
Hope someone knows the answer.
The problem is that JavaScript did not complete the action before executing the next line of your code.
You could use a callback function:
var setHoverAfterOverFlowVisible = function(cur, callback) {
$(cur).css('overflow', 'visible');
match = true;
//other stuff
callback();
}
setHoverAfterOverFlowVisible(cur, /*hoverFunction*/);
More info about callbacks: http://www.impressivewebs.com/callback-functions-javascript/

How to track divs scrolling over point on page with JavaScript/jQuery

I'm looking for a very fast solution to a div scrolling problem.
I have a set of divs, like forum posts, that are laid out one on top of the other. As the page scrolls down or up, I'd like to know when one of those divs hit's an arbitrary point on the page.
One way I tried was adding an onScroll event to each item, but as the number of items grow the page really starts to lag.
Anyone know a more efficient way to do this? Thanks /w
Well, I'm new to all this, so may be someone should correct me :)
I propose to
cache posts position
caсhe current
use binary search
Demo: http://jsfiddle.net/zYe8M/
<div class="post"></div>
<div class="post"></div>
<div class="post"></div>
...
var posts = $(".post"), // our elements
postsPos = [], // caсhe for positions
postsCur = -1, // cache for current
targetOffset = 50; // position from top of window where you want to make post current
// filling postsPos with positions
posts.each(function(){
postsPos.push($(this).offset().top);
});
// on window scroll
$(window).bind("scroll", function(){
// get target post number
var targ = postsPos.binarySearch($(window).scrollTop() + targetOffset);
// only if we scrolled to another post
if (targ != postsCur) {
// set new cur
postsCur = targ;
// moving cur class
posts.removeClass("cur").eq(targ).addClass("cur");
}
});
// binary search with little tuning on return to get nearest from bottom
Array.prototype.binarySearch = function(find) {
var low = 0, high = this.length - 1,
i, comparison;
while (low <= high) {
i = Math.floor((low + high) / 2);
if (this[i] < find) { low = i + 1; continue; };
if (this[i] > find) { high = i - 1; continue; };
return i;
}
return this[i] > find ? i-1 : i;
};
You shouldn't bind scroll event to all the divs but only to window instead. Then, you should check whether one of the divs overlap with the target point by making a simple calculation of the element offset values.
$(window).scroll(function(event)
{
var isCaptured = capture();
console.log(isCaptured);
});
function capture()
{
var c = $('.box'); //this is the divs
var t = $('#target'); //this is the target element
var cPos = c.offset(); var tPos = t.offset();
var overlapY = (cPos.top <= tPos.top + t.height() && cPos.top + c.height() >= tPos.top);
var overlapX = (cPos.left <= tPos.left + t.width() && cPos.left + c.width() >= tPos.left);
return overlapY && overlapX;
}
Instead of the $('#target') element, you can pass top and left (X, Y) offset values directly to the function.
Well, here is a dirty demonstration.

Categories

Resources