JS randomly distribute child elements in their parent without overlap - javascript

I am trying to make something where a bunch of circles (divs with border-radius) can be dynamically generated and laid out in their container without overlapping.
Here is my progress so far - https://jsbin.com/domogivuse/2/edit?html,css,js,output
var sizes = [200, 120, 500, 80, 145];
var max = sizes.reduce(function(a, b) {
return Math.max(a, b);
});
var min = sizes.reduce(function(a, b) {
return Math.min(a, b);
});
var percentages = sizes.map(function(x) {
return ((x - min) * 100) / (max - min);
});
percentages.sort(function(a, b) {
return b-a;
})
var container = document.getElementById('container');
var width = container.clientWidth;
var height = container.clientHeight;
var area = width * height;
var maxCircleArea = (area / sizes.length);
var pi = Math.PI;
var maxRadius = Math.sqrt(maxCircleArea / pi);
var minRadius = maxRadius * 0.50;
var range = maxRadius - minRadius;
var radii = percentages.map(function(x) {
return ((x / 100) * range) + minRadius;
});
function getRandomArbitrary(min, max) {
return Math.random() * (max - min) + min;
}
var coords = [];
radii.forEach(function(e, i) {
var circle = document.createElement('div');
var randomTop = getRandomArbitrary(0, height);
var randomLeft = getRandomArbitrary(0, width);
var top = randomTop + (e * 2) < height ?
randomTop :
randomTop - (e * 2) >= 0 ?
randomTop - (e * 2) :
randomTop - e;
var left = randomLeft + (e * 2) < width ?
randomLeft :
randomLeft - (e * 2) >= 0 ?
randomLeft - (e * 2) :
randomLeft - e;
var x = left + e;
var y = top + e;
coords.push({x: x, y: y, radius: e});
circle.className = 'bubble';
circle.style.width = e * 2 + 'px';
circle.style.height = e * 2 + 'px';
circle.style.top = top + 'px';
circle.style.left = left + 'px';
circle.innerText = i
container.appendChild(circle);
});
I have got them being added to the parent container but as you can see they overlap and I don't really know how to solve this. I tried implementing a formula like (x1 - x2)^2 + (y1 - y2)^2 < (radius1 + radius2)^2 but I have no idea about this.
Any help appreciated.

What you're trying to do is called "Packing" and is actually a pretty hard problem. There are a couple potential approaches you can take here.
First, you can randomly distribute them (like you are currently doing), but including a "retry" test, in which if a circle overlaps another, you try a new location. Since it's possible to end up in an impossible situation, you would also want a retry limit at which point it gives up, goes back to the beginning, and tries randomly placing them again. This method is relatively easy, but has the down-side that you can't pack them very densely, because the chances of overlap become very very high. If maybe 1/3 of the total area is covered by circle, this could work.
Second, you can adjust the position of previously placed circles as you add more. This is more equivalent to how this would be accomplished physically -- as you add more you start having to shove the nearby ones out of the way in order to fit the new one. This will require not just finding the things that your current circle hits, but also the ones that would be hit if that one was to move. I would suggest something akin to a "springy" algorithm, where you randomly place all the circles (without thinking about if they fit), and then have a loop where you calculate overlap, and then exert a force on each circle based on that overlap (They push each other apart). This will push the circles away from each other until they stop overlapping. It will also support one circle pushing a second one into a third, and so on. This will be more complex to write, but will support much more dense configurations (since they can end up touching in the end). You still probably need a "this is impossible" check though, to keep it from getting stuck and looping forever.

Related

How can I simplify my game enemy spawning algorithm? (code snippet included)

I have a hero character in the middle of the screen and I want to spawn zombies all around him in random positions but some distance away from him. heroDistance defines this distance.
It does not matter if they are pushed outside the boundaries of the screen when they are spawned, they all come towards him. It would not matter if this did not happen, but it just seemed easier.
At the moment the random location of the zombie is created for the x axis with random(screenWidth) and y axis random(screenHeight), and those values are fed into the spawnLocation function that depending on where they are in relation to the hero are either increased or decreased to move they away.
My code seems far too verbose, even though I have worked really hard on it. Am I missing some obvious technique to make it simpler?
const state = {
options: {
numberOfZombies: 10,
},
characters: {
hero: {
xPosition: 150,
yPosition: 150,
},
},
};
const screenWidth = 400;
const screenHeight = 400;
const random = range => Math.floor(Math.random() * range);
function createZombies(state) {
const heroDistance = 10;
const spawnLocation = (zomPos, heroPos, axisLength) => {
return zomPos > heroPos
? zomPos + axisLength / heroDistance
: zomPos - axisLength / heroDistance;
};
for (let index = 0; index < state.options.numberOfZombies; index += 1) {
console.log({
xPosition: spawnLocation(
random(screenWidth),
state.characters.hero.xPosition,
screenWidth,
),
yPosition: spawnLocation(
random(screenHeight),
state.characters.hero.yPosition,
screenHeight,
),
});
}
}
createZombies(state);
Generate a random angle and radius, and then transform these values into Cartesian coordinates.
let theta = Math.random() * (2 * Math.PI)
let r = Math.random() * variationInR + minimumR
let zombieX = Math.cos(theta) * r + heroX
let zombieY = Math.sin(theta) * r + heroY
If you want these to be integers, then make them so. This generates zombies uniformly radially from the hero at least minimumR units away (Pythagorean distance). If you want to maintain the Manhattan distance behavior, then generate your dX and dY and add them to the hero's position.

svg.js animated rotation of elements gives unexpected results (visible "jiggling")

I am using svg.js to create an animation of a bicyle rider. Semi-complete version here: https://pedalfuriously.neocities.org/. I'm running in to a bit of a problem with moving and rotating svg elements during animation created with requestAnimationFrame (rather than the svg.js built in animation).
If you take a look at the link, and use the cadence slider to make the rider pedal very fast, and then flip the slider quickly all the way back to zero, you can see that his lower leg "jiggles" in a disconnected way. What's really doing my head in is that the postion of the legs are determined in each frame based on an absolute relation to the rotation of the cranks (rather than taking some delta time value to determine movement over that frame).
I think I've been able to confirm what aspect of my code is causing the problem. Here is a minimal example that doesn't exhibit the exact behaviour, but I think illustrates the kind of thing I think is responsible:
var draw = SVG("drawing").viewbox(0, 0, 400, 400)
var origin = {
x: 70,
y: 70
}
var length = 60
var blueLine = draw.group()
blueLine.line(0, 0, 0 + length, 0).move(origin.x, origin.y)
.stroke({
color: "#00f",
width: 4
})
blueLine.angle = 0
var greenLine = draw.group()
greenLine.line(0, 0, 0 + length, 0).move(origin.x, origin.y)
.stroke({
color: "#0f0",
width: 4
})
greenLine.angle = 0
var previous = 0
var dt = 0
var step = function(timestamp) {
dt = timestamp - previous
previous = timestamp
blueLine.angle += 0.18 * dt
blueLine.rotate(blueLine.angle, origin.x, origin.y)
var endX = Math.cos(toRad(blueLine.angle)) * length
var endY = Math.sin(toRad(blueLine.angle)) * length
// Comment out this line, and rotation works fine
greenLine.move(endX, endY)
greenLine.angle = blueLine.angle - 10
// Comment out this line, and movement works fine
greenLine.rotate(greenLine.angle, origin.x, origin.y)
// But they don't work together. If I both move and rotate
// the green line, it goes in this crazy huge arc, rather
// than rotating neatly around the end of the blue line
// as expected.
window.requestAnimationFrame(step)
}
window.requestAnimationFrame(step)
function toRad(deg) {
return deg * (Math.PI / 180)
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/svg.js/2.6.4/svg.js"></script>
<div id="drawing"></div>
Something else I noticed with my actual code is that if I move the position of the legs, it changes the severity of the problem, or even stops it altogether. If the hips are positioned all the way near the front of the bicycle, the problem is not nearly as bad. Also, if I disable rotation on the lower legs, there is no jiggling. In some positions, the lower leg will just rotate out of the screen instantly on load, even before any motion has been started.
I'm hoping for some guidance on wether I'm misunderstanding the way manipulating elements works, either in svg.js in particular, or SVG in general.
Thank you kind vector graphics experts!
Here is the actual code for the legs. The step() function would probably be the most relevant. Not sure if it will be helpful:
Rider.Leg = function(foot, front, xOffset, yOffset) {
var upper = front ? SVGE.upperLeg : SVGE.upperLegBack
var lower = front ? SVGE.lowerLeg : SVGE.lowerLegBack
this.foot = foot
this.draw = foot.draw
this.geo = {
upper: {
x: this.foot.pedal.gear.x + 150,
y: this.foot.pedal.gear.y - 750,
length: 396
},
lower: {
length: 390
}
}
this.upper = this.draw.group().svg(upper).move(this.geo.upper.x, this.geo.upper.y)
.transform({ scale: 0.95, cx: 0, cy: 0 })
this.lower = this.draw.group().svg(lower).move(this.geo.upper.x, this.geo.upper.y)
}
// Step function does not take in a time argument. Positioning of legs is based only on
// the absolute position of other elements, none of which jiggle.
Rider.Leg.prototype.step = function () {
var angle = this.pedalAngle() - Math.PI
var ha = this.scaleneAngle(this.geo.lower.length, this.geo.upper.length, this.pedalDistance())
var ka = this.scaleneAngle(this.pedalDistance(), this.geo.lower.length, this.geo.upper.length)
var x = this.geo.upper.length * Math.cos(ha + angle)
var y = this.geo.upper.length * Math.sin(ha + angle)
this.upper.rotate(Drive.toDeg(angle + ha), 0, 0)
this.lower.move(this.geo.upper.x + x, + this.geo.upper.y + y)
this.lower.rotate(Drive.toDeg(angle + ha + ka - Math.PI), 0, 0)
}
// Gets the distance between the hip joint and the pedal
Rider.Leg.prototype.pedalDistance = function () {
var pos = this.foot.getPos()
var xDist = this.geo.upper.x - pos.x
var yDist = this.geo.upper.y - pos.y
return Math.hypot(xDist, yDist)
}
// Gets the angle between the hip joint and the pedal
Rider.Leg.prototype.pedalAngle = function () {
var pos = this.foot.getPos()
var xDist = this.geo.upper.x - pos.x
var yDist = this.geo.upper.y - pos.y
return Math.atan2(yDist, xDist)
}
Rider.Leg.prototype.scaleneAngle = function (a, b, c) {
return Math.acos(((b * b) + (c * c) - (a * a)) / (2 * b * c))
}
When you call move() on a group it is internally represented as a translation. svg.js figures out crazy ways to translate the object to the new place without changing any other transformations. That often does not work out. Especially not, when you rotate.
Thats why you should avoid these absolute transformations and go with relative ones. Just call untransform before every move and go from zero. Then you can do:
greenLine.transform({x:endX, y:endY, relative: true})
To move the line by a certain amount. That should work way better.

Find the Points of Intersection of a Circle with a Line in Javascript

I'm trying to animate a given element to go around a pre-defined radius and I'm having trouble getting the position of the element at a Y point given.
I'm trying to find each point with the circle equation, but I can only get one point out of the two possible ones.
In Javascript, I use Math.sqrt( Math.pow(radius, 2) - Math.pow(y, 2) , 2) to get the point. assuming the center of the of the circle is 0,0.
but then I need to translate it to pixels on the screen since there are no negative pixels in positions on the browser.
All the sizing is relative to the window. so the radius, for example, is 80% of the height of the window in my tests.
Also, I'm trying to calculate what the distance of the element between each frame should be for the duration, but I'm not using it yet because I try to fix the issue above first.
This is what I have(a cleaned up version):
let height = window.innerHeight * 0.8,
radius = height / 2,
circumferance = (radius * 2) * Math.PI,
container = document.getElementById('container'),
rotating = document.querySelector('.rotating'),
centerX = radius - (rotating.offsetWidth / 2),
centerY = radius - (rotating.offsetHeight / 2),
duration = 10,
stepDistance = circumferance / 16;
// Setting the dimensions of the container element.
container.style.height = height + 'px';
container.style.width = height + 'px';
// return positive X of any given Y.
function getXOffset(y) {
return Math.sqrt( Math.pow(radius, 2) - Math.pow(y, 2) , 2);
}
// Setting the position of the rotating element to the start.
rotating.style.top = 0 + 'px';
rotating.style.left = centerX + 'px';
setInterval(() => {
let top = parseInt(rotating.style.top),
y = radius - top;
rotating.style.top = (top + 1) + 'px';
rotating.style.left = (centerX + getXOffset(y)) + 'px';
}, 16);
Here is a fiddle with a bit more code for trying to get the right amount of distance between points for a smoother animation(currently needs fixing, but it doesn't bother me yet.)
https://jsfiddle.net/shock/1qcfvr4y/
Last note: I know that there might be other ways to do this with CSS, but I chose to use javascript for learning purposes.
Math.sqrt would only return the positive root. You'll have to account for the negative value based on the application. In this case, you need the positive x value during the 1st half of the cycle and negative during the 2nd half.
To do that, you should implement a method to track the progress and reverse the sign accordingly.
Here is a sample. I modified upon yours.
edit:
Instead of Math.sqrt( Math.pow(radius, 2) - Math.pow(y, 2) , 2) You can use the full formula to get x if you do not want to assume origin as center, which in this case is Math.sqrt( Math.pow(radius, 2) - Math.pow((actualY - centerY), 2) , 2)
explanation:
The original equation (x-a)² + (y'-b)² = r²
becomes x = √(r² - (y'-b)²) + a
Assuming .rotating box have 0 width and height.
The variable equivalents in your code are centerX = a, centerY = b.
By assuming origin as center you're basically doing a pre-calculation so that your y value becomes the equivalent of (y'-b). Hence x = √(r² - y²) + a is valid.
At initial state top = 0
i.e (y'-b) => height - centerY.
In your code y = radius => height/2.
Now (height - centerY) being equal to (height/2) is a side effect of your circle being bound by a square container whose height determines the y value.
In other words, when you use origin as center, you are taking the center offsets outside of circle equation and handling it separately. You could do the same thing by using the whole formula, that is, x = √(r² - (y'-b)²) + a

Translating a function that styles a div based on mouse movement from jQuery to Angular makes coordinates spazz out

Here is a JSFiddle of the function I built some time ago in JQuery before I learned AngularJS for the project I intended to use it in.
var numberOfPosts = 1; // To calculate the absolute/starting position of each post
var post = $('#post'); // Need to track multiple posts, ideally by array of getElementsByClass
var postOffset = post.offset(); // Relative to the document
var postPosition = post.position(); // Relative to the parent
var radiansBetweenPosts = (90 / numberOfPosts) * Math.PI / 180;
$('#wrapper').mousemove(function(event) {
// Mouse horizontal percentage position inside the wrapper (double to make full circle)
mouseX = (event.pageX - postOffset.left) / post.parent().width() * 2;
x = (Math.cos(Math.PI * mouseX + radiansBetweenPosts)) * 50 + 50; // Multiply by % size of a quadrant,
y = (Math.sin(Math.PI * mouseX + radiansBetweenPosts)) * 50 + 50; // add a % offset to the centre of the circle
post.css({
'left': x + '%',
'top': y + '%'
});
// Mouse horizontal % coordinates from the centre of the circle
$('p').html(Math.round(mouseX * 100));
});
And here is a Plunker of the same idea I translated to AngularJS, which is how it currently behaves on my project.
angular.module('mouseMovement', [])
.controller('MouseMovementController', ['$scope', '$element', function MouseMovementController($scope, $element) {
$scope.msg = "Mouse X position inside the div"
numberOfPosts = 1
radiansBetweenPosts = (90 / numberOfPosts) * Math.PI / 180
$scope.mousePosition = function(event) {
postOffsetLeft = event.target.querySelector('.postDiv').offsetLeft
frameWidth = event.target.offsetWidth
mouseXpercent = (event.pageX - postOffsetLeft) / frameWidth * 2
x = (Math.cos(Math.PI * mouseXpercent + radiansBetweenPosts)) * 50 + 50
y = (Math.sin(Math.PI * mouseXpercent + radiansBetweenPosts)) * 50 + 50
$scope.position = {
left: x + '%',
top: y + '%'
}
$scope.mouseX = Math.round(mouseXpercent * 100)
$scope.postX = Math.round(x)
$scope.postY = Math.round(y)
}
}])
It appears to me that when the mouse is moved across the div, the coordinates jump between a single digit and a two or three digit number very quickly, which you can observe if you move the mouse for a bit and check the numbers a few times. That I believe is what causes the position to spazz out like that.
Oddly, that only happens when the $scope.position variable is there, so if you comment that bit out, both the Post X and Post Y numbers will steadily change as they should when you move your mouse across the div.
What am I missing here? It seems like the coordinate calculation is suddenly wrong when the styles are applied, but that can't be true. To make it more weird, at some sedctions of the div the numbers are steadily and correctly changing, for example verticall under this bolded word on the Plunker "Mouse X position inside the div"
If it's something in the way AngularJS works internally, what solutions are there?
In addition to that, I'll need to somehow keep tracking the mouse movement across the gray div even if the mouse appears on top of the
Your math is off. Try this. My math isn't exact but it's closer to what you're looking for
angular.module('mouseMovement', [])
.controller('MouseMovementController', ['$scope', '$element', function MouseMovementController($scope, $element) {
$scope.msg = "Mouse X position inside the div"
numberOfPosts = 1
radiansBetweenPosts = (2 / numberOfPosts) * Math.PI
$scope.mousePosition = function(event) {
postOffsetLeft = event.target.querySelector('.postDiv').offsetLeft
frameWidth = event.target.offsetWidth
mouseXpercent = (event.pageX) / frameWidth
x = Math.PI * (Math.cos(mouseXpercent * radiansBetweenPosts)) * 10 + 40
y = Math.PI * (Math.sin(mouseXpercent * radiansBetweenPosts)) * 10 + 20
$scope.position = {
left: x + '%',
top: y + '%'
}
$scope.mouseX = Math.round(mouseXpercent * 100)
$scope.postX = Math.round(x)
$scope.postY = Math.round(y)
}
}])

JavaScript canvas, scale between two points on chart/graph?

So I've built a small graph application with JavaScript to help me practice using the canvas. I've spent the last 10 hours trying to scale between two points on the X-Axis and can't for the life of me figure it out. I've learned that to scale you need to translate > scale > translate. This works fine when I scale to the far left/right using the following type code.
let x = 0;
let y = this.getCanvasHeight() / 2;
this.getCanvasContext().clearRect(0, 0, this.getCanvas().width, this.getCanvas().height);
this.setCanvas();
ctx.translate(x, y);
ctx.scale(scale, 1);
ctx.translate(-x, -y);
this.resetCanvasLines();
this.renderGraph(this.state.points, scale);
This piece of code simply allows me to zoom into the far left of the graph. So now I'm trying to pick two points on this graph and zoom in on top of them, so that they fit evenly on the screen. The Y-Axis will always be the same.
My thinking was to get the midpoint between the two points and zoom in on that location, which I feel should work but I just can't get it working. My graph width is 3010px and split into 5 segments of 602px. I want to zoom let's say from x1 = 602 and x2 = 1806, which has the midpoint of 1204. Is there a technique to properly calculating the scale amount?
rangeFinder(from, to) {
let points = this.state.points;
if (points.length === 0) {
return;
}
let ctx = this.getCanvasContext();
let canvasWidth = this.getCanvasWidth();
let canvasHeight = this.getCanvasHeight() / 2;
let seconds = this.state.seconds;
let second = canvasWidth / seconds;
let scale = 1;
// My graph starts from zero, goes up to 5 and the values are to represent seconds.
// This gets the pixel value for the fromX value.
let fromX = from * second;
to = isNaN(to) ? 5 : to;
// Get the pixel value for the to location.
let toX = parseInt(to) * second;
let y = canvasHeight / 2;
// get the midpoint between the two points.
let midpoint = fromX + ((toX - fromX) / 2);
// This is where I really go wrong. I'm trying to calculate the scale amount
let zoom = canvasWidth - (toX - fromX);
let zoomPixel = (zoom / 10) / 1000;
let scaleAmount = scale + ((zoom / (canvasWidth / 100)) / 100) + zoomPixel;
ctx.clearRect(0, 0, this.getCanvas().width, this.getCanvas().height);
this.setCanvas();
// translate and scale.
ctx.translate(midpoint, y);
ctx.scale(scaleAmount, 1);
ctx.translate(-midpoint, -y);
this.resetCanvasLines();
this.renderGraph(points);
}
Any help would be great, thanks.
Scale = 5/3 = total width / part width.
After scale, x = 602 should have moved to 602 * 5/3 ~ 1000. Translate the new image by -1000. There is no need to find mid-point.

Categories