I have this code (in the fiddle the circles overlap, but it works alright on my PC – most likely due to pixel widths being off in the fiddle?): https://codesandbox.io/s/objective-hamilton-zspre8?file=/src/App.vue
TestCircle.vue:
<template>
<div class="circle" :style="{left: this.left,
top: this.top}">
{{value}}
</div>
</template>
<script>
export default {
name: "TestCircle",
props: {
value: String,
left: Number,
top: Number
}
}
</script>
<style scoped>
.circle {
background-color: red;
min-height: 49px;
height: 49px;
min-width: 49px;
width: 49px;
border-radius: 20em;
display: inline;
padding: 1em;
position: absolute;
border: 1px solid black;
}
</style>
TestComponent.vue:
<template>
<div id="wrapper" ref="wrapper">
<TestCircle v-for="(circle, i) in this.circles.length"
:value = this.circles[i] :key="i"
:left="this.coordinates[i].left"
:top="this.coordinates[i].top"
/>
</div>
</template>
<script>
import TestCircle from "@/app/test/TestCircle";
export default {
name: "TestComponent",
components: {TestCircle},
data() {
return {
circles: ['A', 'B', 'C', 'D', 'E', 'F'],
coordinates: []
}
},
created() {
console.log("circles: " + this.circles);
let wrapperWidth = 500;
let wrapperHeight = 100;
let circleWidth = 50;
let circleHeight = 50;
for (let i = 0; i < this.circles.length; i++) {
let circleCoordinates = {};
let isValid = false;
do {
isValid = false;
circleCoordinates.left = (this.randomInRange(0, wrapperWidth - circleWidth)).toString() + 'px';
circleCoordinates.top = (this.randomInRange(0, wrapperHeight - circleHeight)).toString() + 'px';
circleCoordinates.width = 50;
circleCoordinates.height = 50;
isValid = !this.overlaps(this.coordinates, i, circleCoordinates);
} while (!isValid);
this.coordinates.push(circleCoordinates);
}
},
methods: {
randomInRange: function(min, max) {
return Math.random() * (max - min + 1) + min;
},
overlaps(coordinates, size, rectangle) {
// Loop through all the rectangles in the coordinates array
for (let i = 0; i < size; i++) {
// Create a rectangle object for the other rectangle
// Check whether the two rectangles overlap using the rectanglesOverlap function
if (this.rectanglesOverlap(rectangle, coordinates[i])) {
return true;
}
}
// None of the rectangles overlap
return false;
},
rectanglesOverlap(first, second) {
console.log("rectanglesOverlap: " + JSON.stringify(first)
+ " and "+ JSON.stringify(second));
// Determine the coordinates of the edges of both rectangles
const firstLeft = parseFloat(first.left);
const firstRight = firstLeft + first.width;
const firstTop = parseFloat(first.top);
const firstBottom = firstTop + first.height;
const secondLeft = parseFloat(second.left);
const secondRight = secondLeft + second.width;
const secondTop = parseFloat(second.top);
const secondBottom = secondTop + second.height;
// Check if any of the edges of the two rectangles overlap
if (firstLeft < secondRight &&
firstRight > secondLeft &&
firstTop < secondBottom &&
firstBottom > secondTop) {
// The rectangles overlap
console.log("rectanglesOverlap: true");
return true;
}
// The rectangles don't overlap
console.log("rectanglesOverlap: false");
return false;
}
},
mounted() {
}
}
</script>
<style scoped>
#wrapper {
margin: 2em;
height: 100px;
min-height: 100px;
width: 500px;
min-width: 500px;
background-color: darkblue;
position: relative;
}
</style>
TestPage.vue:
<template>
<div id="test-page">
<TestComponent/>
</div>
</template>
<script>
import TestComponent from "@/app/test/TestComponent";
export default {
name: "TestPage",
components: {TestComponent },
async beforeCreate() {
},
methods: {
},
}
</script>
<style scoped>
#test-page {
background-color: yellow;
width: 50%;
display: flex;
justify-content: center;
}
</style>
When I go to TestPage
, I see this:
if I renew the page:
As can be seen, the circles are randomly placed inside the blue div
, in a way that the circles don’t overlap.
The width of 500px
and height of 100px
of the blue div and the height&width of each circle (50px
) are hardcoded.
It works fine (at least on my PC), however it doesn’t work with resizing the browser window, or switching to mobile:
I need it to look OK on mobile/smaller browser windows/scales.
However I don’t understand how to approach it. I have an idea that I would need to replace wrapperWidth
, wrapperHeight
, circleWidth
, circleHeight
in the created
with appropriate values. However in the created()
there is no access to the wrapper div, because it doesn’t exist yet. How should I approach it?
It should look something like this on mobile:
So far I’ve been using media queries to make my website responsive:
@media screen and (max-width: 500px) {
.wrapper {
max-width: 90% !important;
}
.btn {
font-size: 0.8em;
}
}
So assuming the width of the blue wrapper div is controlled by the media queries, same with the width of the circles, how should I approach placing them responsively on the blue div?
Any help is appreciated!
EDIT:
A clear, specific question: How to make it work on mobile-sized screens?
2
Answers
First of all, when you want to interact with DOM, you shouldn’t use
created
hook, butmounted
hook. The difference is that inmounted
the component has been mounted (a.k.a. "added to DOM"). That’s because you need the currently available width in the next step.Secondly, before you can translate anything into code, you have to define it logically, down to the last single step. Your current code indicates you haven’t done that yet.
In my estimation, to achieve the desired functionality, you need to define a function which takes in the available width and the number of circles, returning the minimal height of a box with that width which could fit all the circles, regardless of each circle’s position. You’ll need a bit of trigonometry to solve this.
If you have trouble coming up with the formula, consider asking it on mathematics 1.
Once you have the minimal height formula, what you have should work, provided you fix the
overlaps
function. The correct way to check if two circles intersect is to compare the distance between their centres with the sum of their radiuses.If you want to skip asking on maths website and would settle for a rough estimation, here’s a pattern in which the circles are most spread out without actually allowing any extra circles in between 2:
The distance between centres is roughly
3.5 × r
and each subsequent row has a height of approx.3 × r
. If they’re more distanced apart, the gaps become big enough to accommodate extra circles.If I had to solve this problem, I would estimate the minimum height of the box based on this pattern 3, rather than calculate the exact trigonometrical formula. I’d give the first row a height of
3 × r
(although it’s actually shorter) to simplify the formula and to make sure the circles would fit even in the unlikely event they position themselves "randomly" in the exact pattern shown above.1 – you’ll need to ask a non-code related question there. Don’t ask them about Vue or mobile devices, they’ll send you back here, without a minimal height formula, which is what you actually need.
2 – if the first 12 circles are positioned as in the image, the 13th circle would cause the
while
in your code to loop endlessly3 – a rough formula for min height would be:
which could be written as:
To make sure it will never loop endlessly, a counter could be set on the
while
. If it exceeds a certain value (e.g: 100 × for the same circle), you could simply recalculate all circles, but the probability that it would ever freeze is negligible, if at all. Note this guardrail has not been included in the example below.Here’s a demo using this formula:
Or, if you prefer Options API:
Note on
resize
event I’m only recalculating positions of the circles previously placed outside of the current container (I’m keeping the circles which don’t need to move).Given that you would like to randomly place the circles inside a predefined area, none of the css responsive layout mechanisms would be able to help you. You just have to manually design the mathematical algorithm to calculate the coordinates.
That being said, your problem can be defined as calculating the coordinates of a number of circles within the given bounds so that they do not overlap:
And assuming you do have the algorithm (looks like you do, although very brute force), the only problem left is to know the actual
areaWidth
andareaHeight
(instead of hardcoding them), and to know when they change (so that you can re-run your algorithm).You can use Vue’s template ref to gain access to your container div (Note template refs are not going to be populated until at least the
mounted
hook), and then you can use ResizeObserver to observe the size change of that div. Every time the size changes, you update the coordinates by running yourgetCoordinates
method again and Vue will take care of the rest.