Reducing the number of times a helper function is run - javascript

I am trying to create a word cloud. In order to render text to the screen I am generating a random position for each word. This works perfectly, however there are a lot of overlapping words. In order to solve this I am storing the position and size of the elements in an array and then I created a helper function that checks for collisions, generates a new position for the element if it finds one, and then calls it's self again to check again from the start of the array. When I run my code the first 2-3 words render just fine but then I get an error saying Maximum call stack size exceeded. I saw there was already a post on this same issue on stack overflow.
I saw that the other person was using a forEach function and so was I so I converted it into a for loop like the answer suggested but it did not do anything. I think the issue boils down to the fact that there are so many collisions but I am not sure how to best approach the issue. Is there another way that I can generate unique positions for elements while still avoiding collisions?
Code:
function calculatePosition(parent, child) {
return Math.random() * parent - (child / 2)
}
// needed for rendering position of span elements
var ranges = []
var totalWidthOfWords = 0
var totalHeightOfWords = 0
// reposition element if there is a collision
function checkForCollisions(element, height, width, wordCloud, injectedSpan) {
for(var i = 0; i < ranges.length; i++) {
let current = ranges[i]
if(element.left >= current.width[0] && element.left <= current.width[1]) {
injectedSpan.style.left = calculatePosition(wordCloud.clientWidth, width) + "px";
checkForCollisions(element, height, width, wordCloud, injectedSpan)
}
if(element.top >= current.height[0] && element.top <= current.height[1]) {
injectedSpan.style.top = calculatePosition(wordCloud.clientHeight, height) + "px";
checkForCollisions(element, height, width, wordCloud, injectedSpan)
}
}
}
// Create content in DOM
const injectedContent = data.map(line => {
const injectedSpan = document.createElement("span")
const injectedWord = document.createElement("p")
const wordCloud = document.querySelector(".word-cloud")
// mod weight value to get more managable inputs
let weightValue = (line.weight * 100).toFixed(2)
// sets values of words and renders them to the screen
injectedWord.innerText = line.word
injectedSpan.appendChild(injectedWord)
wordCloud.appendChild(injectedSpan)
// sets style attribute based on weight value
injectedWord.setAttribute("style", `--i: ${weightValue}`)
// flips words
if(Math.random() > 0.75) {
injectedWord.style.writingMode = "vertical-rl";
}
// Entrance animation
let left = innerWidth * Math.random()
let top = innerHeight * Math.random()
if(Math.random() < 0.5) {
injectedWord.style.left = "-" + left + "px";
injectedSpan.style.left = calculatePosition(wordCloud.clientWidth, injectedSpan.clientWidth) + "px";
} else {
injectedWord.style.left = left + "px";
injectedSpan.style.left = calculatePosition(wordCloud.clientWidth, injectedSpan.clientWidth) + "px";
}
if(Math.random() < 0.5) {
injectedWord.style.top = "-" + top + "px";
injectedSpan.style.top = calculatePosition(wordCloud.clientHeight, injectedSpan.clientHeight) + "px";
} else {
injectedWord.style.top = top + "px";
injectedSpan.style.top = calculatePosition(wordCloud.clientWidth, injectedSpan.clientWidth) + "px";
}
// Get position of span and change coordinites if there is a collision
let spanPosition = injectedSpan.getBoundingClientRect()
console.log(spanPosition)
if(spanPosition) {
checkForCollisions(spanPosition, spanPosition.height, spanPosition.width, wordCloud, injectedSpan)
}
totalWidthOfWords += spanPosition.width
totalHeightOfWords += spanPosition.height
ranges.push({width: [spanPosition.left, spanPosition.right], height: [spanPosition.top, spanPosition.bottom]})
})
Link: https://jsfiddle.net/amotor/mdg7rzL1/4/

There is still a lot of work to do to make sure that it works properly, especially to make sure that the code does not produce any errors!
The general idea would be to follow IllsuiveBrian's comment to make sure, that checkForCollision only does the work of checking if there is a collision and that another function takes care of recalculating the position if necessary and then reevaluating a potential collision.
function checkForCollisions(element, wordCloud, injectedSpan) {
for(var i = 0; i < ranges.length; i++) {
let current = ranges[i];
// return true if there is a collision (you probably have to update the code you are using here to truly avoid collisions!)
if (collision) { return true; }
}
return false; // return false otherwise
}
Finally this part would take care of recalculating position and and rechecking for collision:
ranges.forEach(function(injectedSpan) {
// Get position of span and change coordinites if there is a collision
let spanPosition = injectedSpan.getBoundingClientRect();
if (spanPosition) {
while (checkForCollisions(spanPosition, wordCloud, injectedSpan)) {
injectedSpan.style.left = calculatePosition(wordCloud.clientWidth, element.width) + "px";
injectedSpan.style.top = calculatePosition(wordCloud.clientHeight, element.height) + "px";
}
}
});
Here is a quick idea on how to go into this direction: https://jsfiddle.net/euvbax1r/4/

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);
}

Javascript : place a number into an array, splitted into segments

I have a custom HTML slider made with the Angular CDK. It works with a drag and drop. It is vertical, the original position is at the bottom. This slider is divided into 10 segments, each of equal size.
I am able to get the slider handle position, and the slider total height.
The slider will be used to determine a color and is made of 10 colors. It should be the full height of the page (which means I can't use ticks like any normal slider would do).
Let's assume the total height is 600px, and the handle position is randomly generated.
I would like to be able to know in which segment the handle is dropped (given by the handle position).
I have used the method in the following snippet, but I don't find it very efficient.
Would someone be able to tell me if there is a better way ? Or even an array operator that would exist for that ?
const interval = 600; // Slider total size
const segmentsNb = 10; // Total number of segments
const segmentSize = interval / segmentsNb; // Size of a single segment
function doTheThing() {
const nb = (Math.random() * 600); // random number generation to simulate slider handle pos
let pos = -1;
let index = 0;
do {
if (nb > segmentSize * (index + 1)) {
index++;
continue;
}
pos = index;
} while(pos === -1 && index <= segmentsNb);
console.log('number is', Math.round(nb), 'at position', pos);
}
<button onclick="doTheThing()">Do something</button>
You could take
Math.floor(nb / segmentSize)
as you see. Maybe you need to adjust the
const interval = 600; // Slider total size
const segmentsNb = 10; // Total number of segments
const segmentSize = interval / segmentsNb; // Size of a single segment
function doTheThing(nb) {
//const nb = (Math.random() * 600); // random number generation to simulate slider handle pos
let pos = -1;
let index = 0;
do {
if (nb > segmentSize * (index + 1)) {
index++;
continue;
}
pos = index;
} while(pos === -1 && index <= segmentsNb);
return [pos, Math.floor(nb / segmentSize)].join(' ');
}
for (var i = 0; i <= 600; i++) document.getElementById('out').innerHTML += [i, doTheThing(i)].join(' ') + '\n';
<pre id="out"></pre>
You can do the same process for getting the position:
Math.ceil((segmentsNb / interval) * nb))
This should return the correct position.

Detecting if two divs are too close or collide/overlap

I'm trying to detect if two given div's are too close or collide/overlap .
I have the below codepen which tries to generate 20 random div's and only append them to body if their position isn't too close to other existing div.
That's the idea but it doesn't work as expected where i get div's that get through with close/overlapping positions to existing divs. (run it multiple times if first time is perfect and you should come across it).
http://codepen.io/anon/pen/fHLzj
Can anyone see the mistake and way to make it work?
This is somewhat hard to explain and get..but here goes:
check every div against every div by running for loop.
x,y,h,w
x is top-left corner's distance from left.
y is top-left corner's distance from top.
h is div's height.
w is div's width.
Point to consider... you don't really need to check every div..consider this
there are 10 divs...
First you will check 1st against 9.
Second one against 8.
.............
Eight one against 2.
Ninth one against 1.
And don't the last one.
Also it's a good idea to assign values and check for collisions in data, before assigning them to dom. Dom should be just for rendering final result.
I'll assume you want to keep none of the two colliding divs.
Preview
http://jsfiddle.net/techsin/m4fSf/6/
as expected code is huge
var
div={},
number=10,
size=20,
m = ele('main');
mw= parseFloat(getComputedStyle(m).getPropertyValue("width"))-size,
mh= parseFloat(getComputedStyle(m).getPropertyValue("height"))-size,
f=true,
nn;
var i
for (i = 0; i < number; i++) {
div[i] = {};
var t = true, newX, newY, nn;
if (i!=0){
while (t) {
newX = rand(mw);
newY = rand(mh);
for (nn = 0; nn < i; nn++) {
if (!(((newX > div[nn].x + size+5) || (newY > div[nn].y + size+5)) ||
((newX + size+5 < div[nn].x) || (newY + size+5 < div[nn].y)))) {
break;
}
if (nn == i-1) t = false;
}}} else {
newX = rand(mw);
newY = rand(mh);
}
console.log(newX);
div[i].x = newX;
div[i].y = newY;
}
for (i = 0; i < number; i++) {
render(div[i]);
}
console.log(div);
function render(x){
var d=document.createElement('div');
d.style.position='absolute';
d.style.left=(x.x+'px');
d.style.top=(x.y+'px');
m.appendChild(d);
}
function rand(x) { return Math.random()*x;}
function ele(x){return document.getElementById(x);}
this code is from my collision site...ill try and put it in the code above, but this what's needed to avoid collisions and close gaps.
if (xpost+30>xx.left && xx.left>xpost && xx.top+30>ypost && xx.top<ypost+30) { xspeed = -speed; }
if (xpost<xx.left+30 && xx.left<xpost && xx.top+30>ypost && xx.top<ypost+30) { xspeed = speed; }
if (ypost+30>xx.top && xx.top>ypost && xx.left+30>xpost && xx.left<xpost+30) { yspeed = -speed; }
if (ypost<xx.top+30 && xx.top<ypost && xx.left+30>xpost && xx.left<xpost+30) { yspeed = speed; }
How about using one of these libraries to detect the collisions for you?
http://sourceforge.net/projects/jquerycollision/
http://gamequeryjs.com/
I changed the collision logic. It detects if an object is close to another object by comparing the distance between the objects. I wrapped the logic in a do-while loop as well, so that it will keep attempting to find a position to place the square and you'll have exactly 20 squares.
This works:
var positions = []; //stroe positions of appended divs
var divsize = 20;
var topGap = 40; // gap from top
var leftGap = 80; //gap from left
function generateRandomPositionedDiv(){
for(var c = 0; c < 20; c++){
var color = '#'+ Math.round(0xffffff * Math.random()).toString(16);
$newdiv = $('<div/>').css({
'width':divsize+'px',
'height':divsize+'px',
'background-color': color
});
var posLeft;
var posTop;
var checkObj;
var collide = false;
posLeft = Math.floor((Math.random() * ($(document).width() - divsize)));//.toFixed();
posTop = Math.floor((Math.random() * ($(document).height() - divsize)));//.toFixed();
checkObj = {x: posLeft, y: posTop};
collide = checkForCollisions(checkObj);
if(!collide) {
positions.push({x: posLeft, y: posTop});
$newdiv.css({
'position':'absolute',
'left':posLeft+'px',
'top':posTop+'px'
});
$('body').append($newdiv);
}
}
}
/*function getPositions(box) {
var $box = $(box);
var pos = $box.position();
var width = $box.width();
var height = $box.height();
return [ [ pos.left, pos.left + width + leftGap ], [ pos.top, pos.top + height + topGap ] ];
}*/
function comparePositions(obj1, obj2) {
if(Math.abs(obj1.x - obj2.x) <= (divsize + leftGap) && Math.abs(obj1.y - obj2.y) <= (divsize + topGap)) {
return true;
} else {
return false;
}
}
function checkForCollisions(posObj){
for(var i = 0; i < positions.length; i++){
var match = comparePositions(positions[i], posObj);
if (match) {
//return true if two positions are close or overlapping
return match;
}
}
}
generateRandomPositionedDiv();

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.

Javascript mouseover/mouseout animated button

I have a button and onmouseover I want it to move right 100ish pixels at 10 pixels a move then stop. The moving isn't a problem its the stopping. I can do this no problem with jquery but I need to learn how to do it from scratch in javascript. this is my script to move right so far.
function rollRight() {
imgThumb.style.left = parseInt (imgThumb.style.left) + 10 + 'px';
animate = setTimeout(rollRight,20);
}
That moves it right just fine so to stop it i tried taking the amount of loops 5x10=50px and wrote it again as
function rollRight() {
var i=0;
while (i < 5) {
imgThumb.style.left = parseInt (imgThumb.style.left) + 10 + 'px';
animate = setTimeout(rollRight,20);
i++;
};
}
Now, I think I'm missing a piece to make it return the [] values for the while function, but I'm not sure how to make it work. Once I have the move right I can apply the same principle to move it back onmouseout.
If anyone can help me fix this that would be great. If you have a better script to do the animation that is just javascript, no libraries, that would be great too.
EDIT: Because leaving it as a comment didn't work well this is my current code
function rollRight() {
var left = parseInt (imgThumb.style.left);
if(left < 50) { // or any other value
imgThumb.style.left = left + 10 + 'px';
animate = setTimeout(rollRight,20);
}
}
function revert() {
var left = parseInt (imgThumb.style.left);
if(left < 50) { // or any other value
imgThumb.style.left = left + -10 + 'px';
animate = setTimeout(rollRight,20);
}
}
In the revert I'm having a problem getting it to move back. It's probably in the if(left<50) part.
var animate = null;
function rollRight() {
if(animate) clearTimeout(animate);
var left = parseInt (imgThumb.style.left);
if(left < 50) { // or any other value
imgThumb.style.left = left + 10 + 'px';
animate = setTimeout(rollRight,20);
}
}
function revert() {
if(animate) clearTimeout(animate);
var left = parseInt (imgThumb.style.left);
if(left > 0) {
imgThumb.style.left = (left - 10) + 'px';
animate = setTimeout(revert,20);
}
}
If you want to stick with setTimeout, you were close on your first one. I moved some hard coded numbers into variables:
var totalPixels = 100;
var increment = 10;
var frameTime = 20;
var numFrames = totalPixels / increment;
var curFrame = 0;
function rollRight() {
imgThumb.style.left = parseInt (imgThumb.style.left) + increment + 'px';
if(curFrame++ < numFrames) {
animate = setTimeout(rollRight,frameTime);
}
}
You could also switch to use setInterval and then every time the interval fires, decide if you should stop the interval based on some incrementing value.
Try this
var i = 0;
var moveInterval = setInterval(function(){
if(i>=5) clearInterval(moveInterval);
rollRight();
i++;
},20);
function rollRight() {
imgThumb.style.left = parseInt (imgThumb.style.left) + 10 + 'px';
}
Once it gets to 5, it will clear the interval out, and be done.

Categories

Resources