2👍
-
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 endlessly
3 – a rough formula for min height would be:
const height = Math.max(
(3 * radius) * (circles.length / (containerWidth / (3.5 * radius))),
radius * 3.5
)
which could be written as:
const height = Math.max(
10.5 * circles.length * radius ** 2 / containerWidth,
3.5 * radius
)
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:
const {
createApp,
reactive,
toRefs,
onMounted,
onBeforeUnmount,
watch,
nextTick,
computed
} = Vue
const circles = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').map((id) => ({ id }))
const useBindExternal = (target, ...args) => {
onMounted(() => target.addEventListener(...args))
onBeforeUnmount(() => target.removeEventListener(...args))
}
createApp({
setup() {
const { hypot, floor, random, max } = Math
const randomBetween = (m, M) => floor(random() * (M - m + 1)) + m
const getDistance = (a, b) => hypot(b.x - a.x, b.y - a.y)
const state = reactive({
box: null,
height: 0,
width: 0,
radius: 25,
circles: [],
getStyle: (c) => ({
transform: `translate(${c.x - state.radius}px, ${
c.y - state.radius
}px)`,
width: `${state.radius * 2}px`,
height: `${state.radius * 2}px`
})
})
const addPosition = (c) => ({
...c,
x: randomBetween(state.radius + 1, state.width - state.radius - 2),
y: randomBetween(state.radius + 1, state.height - state.radius - 2)
})
const positionCircles = (arr = circles) => {
const out = [...state.circles]
arr.forEach((circle) => {
let item = addPosition(circle)
let intersects = true
while (intersects) {
if (
!out.length ||
!out.some((c) => getDistance(c, item) <= state.radius * 2)
) {
intersects = false
out.push(item)
} else {
item = addPosition(circle)
}
}
})
state.circles = out
}
const invalidateCircles = () => {
state.circles = state.circles.filter(
(c) =>
c.x < state.width - state.radius - 2 &&
c.y < state.height - state.radius - 2
)
positionCircles(
circles.filter((c) => !state.circles.map(({ id }) => id).includes(c.id))
)
}
const updateWidth = () => {
state.width = state.box?.offsetWidth || 0
}
useBindExternal(window, 'resize', updateWidth)
onMounted(updateWidth)
watch(
() => state.width,
(val) =>
val &&
((state.height = max(
(circles.length * state.radius ** 2 * 10.5) / val,
3.5 * state.radius
)),
(state.circles.length ? invalidateCircles : positionCircles)(),
nextTick(updateWidth))
)
return toRefs(state)
}
}).mount('#app')
.box {
position: relative;
border: 1px solid #f00;
}
.box > * {
border-radius: 50%;
background-color: #000;
position: absolute;
color: white;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
<script src="https://unpkg.com/vue@3.2.47/dist/vue.global.prod.js"></script>
<div id="app">
<div ref="box" class="box" :style="{ height: height + 'px'}">
<div v-for="circle in circles" :key="circle.id" :style="getStyle(circle)">
{{ circle.id }}
</div>
</div>
</div>
Or, if you prefer Options API:
const circles = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').map((id) => ({ id }))
const { hypot, floor, random, max } = Math
const randomBetween = (m, M) => floor(random() * (M - m + 1)) + m
const getDistance = (a, b) => hypot(b.x - a.x, b.y - a.y)
Vue.createApp({
data: () => ({
height: 0,
width: 0,
radius: 25,
circles: []
}),
methods: {
getStyle(c) {
return {
transform: `translate(${c.x - this.radius}px, ${c.y - this.radius}px)`,
width: `${this.radius * 2}px`,
height: `${this.radius * 2}px`
}
},
addPosition(c) {
return {
...c,
x: randomBetween(this.radius + 1, this.width - this.radius - 2),
y: randomBetween(this.radius + 1, this.height - this.radius - 2)
}
},
positionCircles(arr = circles) {
const out = [...this.circles]
arr.forEach((circle) => {
let item = this.addPosition(circle)
let intersects = true
while (intersects) {
if (
!out.length ||
!out.some((c) => getDistance(c, item) <= this.radius * 2)
) {
intersects = false
out.push(item)
} else {
item = this.addPosition(circle)
}
}
})
this.circles = out
},
invalidateCircles() {
this.circles = this.circles.filter(
(c) =>
c.x < this.width - this.radius - 2 &&
c.y < this.height - this.radius - 2
)
this.positionCircles(
circles.filter((c) => !this.circles.map(({ id }) => id).includes(c.id))
)
},
updateWidth() {
this.width = this.$refs.box?.offsetWidth
}
},
mounted() {
window.addEventListener('resize', this.updateWidth)
this.updateWidth()
},
beforeUnmount() {
window.removeEventListener('resize', this.updateWidth)
},
watch: {
width(val) {
val &&
((this.height = max(
(circles.length * this.radius ** 2 * 10.5) / val,
3.5 * this.radius
)),
this[`${this.circles.length ? 'invalidate' : 'position'}Circles`](),
this.$nextTick(this.updateWidth))
}
}
}).mount('#app')
.box {
position: relative;
border: 1px solid #f00;
}
.box > * {
border-radius: 50%;
background-color: #000;
position: absolute;
color: white;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
<script src="https://unpkg.com/vue@3.2.47/dist/vue.global.prod.js"></script>
<div id="app">
<div ref="box" class="box" :style="{ height: height + 'px'}">
<div v-for="circle in circles" :key="circle.id" :style="getStyle(circle)">
{{ circle.id }}
</div>
</div>
</div>
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).
- [Vuejs]-Why can I not push data to new object after filtering it with forEach?
- [Vuejs]-What is the best practice to pass null to a required props in component in vuejs?
1👍
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:
getCoordinates(numberOfCircles, circleSize, areaWidth, areaHeight)
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
and areaHeight
(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 your getCoordinates
method again and Vue will take care of the rest.
- [Vuejs]-Nuxt 3 (Vue.js 3) component transitions not working
- [Vuejs]-Vue 3 class attribute appears to ignore ref