How to stop at the nearest row using react-spring + decay? - javascript

I implement calendar which can be veristically dragged by a mouse. I would like to have some inertia on mouse release. I was able to achieve this functionality by using use-gesture + react-spring libs.
This is how it looks
The problem I would like to solve is how to force the inertia to stop at the nearest row? Currently it can be stopped at the middle of the row.
Here is my code. I removed some parts for simplicity
[...]
const [animatedOffsetY, animatedOffsetYProps] = useSpring(() => ({
offsetY: 0,
config: {
decay: true
frequency: 5.5,
}
}))
[...]
const bind = useDrag((state) => {
const [, currentY] = state.movement
const [, lastOffsetY] = state.lastOffset
const offsetY = lastOffsetY + currentY
if (state.down) {
setOffsetY(offsetY)
animatedOffsetYProps.set({ offsetY: offsetY })
} else {
animatedOffsetYProps.start({
offsetY: offsetY,
config: {
velocity: state.direction[1] * state.velocity[1],
}
})
}
}, {
from: [0, offsetY],
bounds: { bottom: 0, top: (totalHeight - containerHeight) * -1 },
rubberband: true
})
[...]
Update
I investigated, popmotion and framer-motion (which use popmotion under the hood) and they have modifyTarget prop which should solve my problem. I would prefer to stay with react-spring or custom solution.

For some reason using decay in the spring config is giving me lots of trouble. I've got it working just fine with a standard friction/tension based spring.
You want to animate to a position that is at the exact top of the nearest row when ending a drag. While dragging, ie. if (state.down), you want to animate to the exact coordinates. When the mouse is released, ie. else, you want to compute the desired resting position and animate to that.
I'm calling this value snapOffsetY, and you can compute it by rounding how many boxes you've scrolled:
const snapIndex = Math.round(offsetY / boxHeight);
const snapOffsetY = snapIndex * boxHeight;
Instead of calling setOffsetY on every movement, I'm using a component state to store the last offset which was snapped to. So I'm only setting it on drag release.
We can then use this lastOffsetY as part of the bounds calculation. Initially, bottom is 0 meaning that we can't drag above the top. But you need to be able to scroll back up once you've scrolled down the list. So the bounds need to change based on the last stopping position.
bounds: {
bottom: 0 - lastOffsetY,
top: -1 * (totalHeight - containerHeight) - lastOffsetY
},
My code looks like this:
import React, { useState } from "react";
import { animated, config, useSpring } from "#react-spring/web";
import { useDrag } from "react-use-gesture";
import { range } from "lodash";
import "./styles.css";
export default function SnapGrid({
boxHeight = 75,
rows = 10,
containerHeight = 300
}) {
const totalHeight = rows * boxHeight;
const [lastOffsetY, setLastOffsetY] = useState(0);
const [animation, springApi] = useSpring(
() => ({
offsetY: 0,
config: config.default
}),
[]
);
const bind = useDrag(
(state) => {
const [, currentY] = state.movement;
const offsetY = lastOffsetY + currentY;
// while animating
if (state.down) {
springApi.set({ offsetY: offsetY });
}
// when stopping
else {
const snapIndex = Math.round(offsetY / boxHeight);
const snapOffsetY = snapIndex * boxHeight;
setLastOffsetY(snapOffsetY);
springApi.start({
to: {
offsetY: snapOffsetY
},
config: {
velocity: state.velocity
}
});
}
},
{
axis: "y",
bounds: {
bottom: 0 - lastOffsetY,
top: -1 * (totalHeight - containerHeight) - lastOffsetY
},
rubberband: true
}
);
return (
<div className="container">
<animated.ul
{...bind()}
className="grid"
style={{
y: animation.offsetY,
touchAction: "none",
height: containerHeight
}}
>
{range(0, 5 * rows).map((n) => (
<li key={n} className="box" style={{ height: boxHeight }}>
{n}
</li>
))}
</animated.ul>
</div>
);
}
Code Sandbox Demo

Are you sure you need to put this logic into JS? CSS might be all you need here:
.scroller {
scroll-snap-type: y mandatory;
max-height: 16em;
overflow: scroll;
letter-spacing: 2em;
}
.row {
scroll-snap-align: start;
box-sizing: border;
padding: 1em;
}
.row:nth-child(2n) {
background: #bbb;
}
<div class = "scroller">
<div class = "row">AAAA</div>
<div class = "row">BBBB</div>
<div class = "row">CCCC</div>
<div class = "row">AAAA</div>
<div class = "row">BBBB</div>
<div class = "row">CCCC</div>
<div class = "row">AAAA</div>
<div class = "row">BBBB</div>
<div class = "row">CCCC</div>
<div class = "row">AAAA</div>
<div class = "row">BBBB</div>
<div class = "row">CCCC</div>
<div class = "row">AAAA</div>
<div class = "row">BBBB</div>
<div class = "row">CCCC</div>
<div class = "row">AAAA</div>
<div class = "row">BBBB</div>
<div class = "row">CCCC</div>
</div>

Related

Calculate transform y-scale property as the function of elapsed time

I have a container that is expanded and collapsed on click of chevron icon. The code to collapse/expand the container is in the function transformAnimation. The code of transformAnimation is similar to the code on MDN web docs for requestAnimationFrame. The code to animate (scale) the container has been developed on the guidelines of this article on Building performant expand & collapse animations on Chrome Developers website.
I am not able to figure out how to calculate yScale value (which is nothing but y scale values for collapse/expand animation) as a function of the time elapsed since the start of the animation.
To elaborate what I mean, let's assume that the container is in expanded state. In this state the scaleY value of the container is 6. Now when user clicks on the toggle button, in the transformAnimation function for each animation frame, i.e, execution of the requestAnimationFrame callback step function, the value of scaleY should decrease from 6 (the expanded state) to 1 (the collapsed state) in the exact duration that I want the animation to run for.
In the present state, the code to calculate yScale is not working as expected.
const dragExpandableContainer = document.querySelector('.drag-expandable-container');
const dragExpandableContents = document.querySelector('.drag-expandable__contents');
const resizeableControlEl = document.querySelector('.drag-expandable__resize-control');
const content = document.querySelector(`.content`);
const toggleEl = document.querySelector(`.toggle`);
const collapsedHeight = calculateCollapsedHeight();
/* This height is used as the basis for calculating all the scales for the component.
* It acts as proxy for collapsed state.
*/
dragExpandableContainer.style.height = `${collapsedHeight}px`;
// Apply iniial transform to expand
dragExpandableContainer.style.transformOrigin = 'bottom left';
dragExpandableContainer.style.transform = `scale(1, 10)`;
// Apply iniial reverse transform on the contents
dragExpandableContents.style.transformOrigin = 'bottom left';
dragExpandableContents.style.transform = `scale(1, calc(1/10))`;
let isOpen = true;
const togglePopup = () => {
if (isOpen) {
collapsedAnimation();
toggleEl.classList.remove('toggle-open');
isOpen = false;
} else {
expandAnimation();
toggleEl.classList.add('toggle-open');
isOpen = true
};
};
function calculateCollapsedHeight() {
const collapsedHeight = content.offsetHeight + resizeableControlEl.offsetHeight;
return collapsedHeight;
}
const calculateCollapsedScale = function() {
const collapsedHeight = calculateCollapsedHeight();
const expandedHeight = dragExpandableContainer.getBoundingClientRect().height;
return {
/* Since we are not dealing with scaling on X axis, we keep it 1.
* It can be inverse to if required */
x: 1,
y: expandedHeight / collapsedHeight,
};
};
const calculateExpandScale = function() {
const collapsedHeight = calculateCollapsedHeight();
const expandedHeight = 100;
return {
x: 1,
y: expandedHeight / collapsedHeight,
};
};
function expandAnimation() {
const {
x,
y
} = calculateExpandScale();
transformAnimation('expand', {
x,
y
});
}
function collapsedAnimation() {
const {
x,
y
} = calculateCollapsedScale();
transformAnimation('collapse', {
x,
y
});
}
function transformAnimation(animationType, scale) {
let start, previousTimeStamp;
let done = false;
function step(timestamp) {
if (start === undefined) {
start = timestamp;
}
const elapsed = timestamp - start;
if (previousTimeStamp !== timestamp) {
const count = Math.min(0.1 * elapsed, 200);
//console.log('count', count);
let yScale;
if (animationType === 'expand') {
yScale = (scale.y / 100) * count;
} else yScale = scale.y - (scale.y / 100) * count;
//console.log('yScale', yScale);
if (yScale < 1) yScale = 1;
dragExpandableContainer.style.transformOrigin = 'bottom left';
dragExpandableContainer.style.transform = `scale(${scale.x}, ${yScale})`;
const inverseXScale = 1;
const inverseYScale = 1 / yScale;
dragExpandableContents.style.transformOrigin = 'bottom left';
dragExpandableContents.style.transform = `scale(${inverseXScale}, ${inverseYScale})`;
if (count === 200) done = true;
//console.log('elapsed', elapsed);
if (elapsed < 1000) {
// Stop the animation after 2 seconds
previousTimeStamp = timestamp;
if (!done) requestAnimationFrame(step);
}
}
}
requestAnimationFrame(step);
}
.drag-expandable-container {
position: absolute;
bottom: 0px;
display: block;
overflow: hidden;
width: 100%;
background-color: #f3f7f7;
}
.drag-expandable__contents {
height: 0;
}
.toggle {
position: absolute;
top: 2px;
right: 15px;
height: 10px;
width: 10px;
transition: transform 0.2s linear;
}
.toggle-open {
transform: rotate(180deg);
}
.drag-expandable__resize-control {
background-color: #e7eeef;
}
.burger-icon {
width: 12px;
margin: 0 auto;
padding: 2px 0;
}
.burger-icon__line {
height: 1px;
background-color: #738F93;
margin: 2px 0;
}
.drag-expandable__resize-control:hover {
border-top: 1px solid #4caf50;
cursor: ns-resize;
}
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="css.css">
</head>
<body>
<div class="drag-expandable-container">
<div class="drag-expandable__contents">
<div class="drag-expandable__resize-control">
<div class="burger-icon">
<div class="burger-icon__line"></div>
<div class="burger-icon__line"></div>
<div class="burger-icon__line"></div>
</div>
</div>
<div class="content" />
<div>
<div class="toggle toggle-open" onclick="togglePopup()">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.1.1 by #fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M416 352c-8.188 0-16.38-3.125-22.62-9.375L224 173.3l-169.4 169.4c-12.5 12.5-32.75 12.5-45.25 0s-12.5-32.75 0-45.25l192-192c12.5-12.5 32.75-12.5 45.25 0l192 192c12.5 12.5 12.5 32.75 0 45.25C432.4 348.9 424.2 352 416 352z"/></svg>
</div>
</div>
</div>
</div>
</body>
<script type="text/javascript" src="js.js"></script>
</html>

How to make items draggable and clickable?

I'm new to Matter JS, so please bear with me. I have the following code I put together from demos and other sources to suit my needs:
function biscuits(width, height, items, gutter) {
const {
Engine,
Render,
Runner,
Composites,
MouseConstraint,
Mouse,
World,
Bodies,
} = Matter
const engine = Engine.create()
const world = engine.world
const render = Render.create({
element: document.getElementById('canvas'),
engine,
options: {
width,
height,
showAngleIndicator: true,
},
})
Render.run(render)
const runner = Runner.create()
Runner.run(runner, engine)
const columns = media({ bp: 'xs' }) ? 3 : 1
const stack = Composites.stack(
getRandom(gutter, gutter * 2),
gutter,
columns,
items.length,
0,
0,
(x, y, a, b, c, i) => {
const item = items[i]
if (!item) {
return null
}
const {
width: itemWidth,
height: itemHeight,
} = item.getBoundingClientRect()
const radiusAmount = media({ bp: 'sm' }) ? 100 : 70
const radius = item.classList.contains('is-biscuit-4')
? radiusAmount
: 0
const shape = item.classList.contains('is-biscuit-2')
? Bodies.circle(x, y, itemWidth / 2)
: Bodies.rectangle(x, y, itemWidth, itemHeight, {
chamfer: { radius },
})
return shape
}
)
World.add(world, stack)
function positionDomElements() {
Engine.update(engine, 20)
stack.bodies.forEach((block, index) => {
const item = items[index]
const xTrans = block.position.x - item.offsetWidth / 2 - gutter / 2
const yTrans = block.position.y - item.offsetHeight / 2 - gutter / 2
item.style.transform = `translate3d(${xTrans}px, ${yTrans}px, 0) rotate(${block.angle}rad)`
})
window.requestAnimationFrame(positionDomElements)
}
positionDomElements()
World.add(world, [
Bodies.rectangle(width / 2, 0, width, gutter, { isStatic: true }),
Bodies.rectangle(width / 2, height, width, gutter, { isStatic: true }),
Bodies.rectangle(width, height / 2, gutter, height, { isStatic: true }),
Bodies.rectangle(0, height / 2, gutter, height, { isStatic: true }),
])
const mouse = Mouse.create(render.canvas)
const mouseConstraint = MouseConstraint.create(engine, {
mouse,
constraint: {
stiffness: 0.2,
render: {
visible: false,
},
},
})
World.add(world, mouseConstraint)
render.mouse = mouse
Render.lookAt(render, {
min: { x: 0, y: 0 },
max: { x: width, y: height },
})
}
I have a HTML list of links that mimics the movements of the items in Matter JS (the positionDomElements function). I'm doing this for SEO purposes and also to make the navigation accessible and clickable.
However, because my canvas sits on top of my HTML (with opacity zero) I need to be able to make the items clickable as well as draggable, so that I can perform some other actions, like navigating to the links (and other events).
I'm not sure how to do this. I've searched around but I'm not having any luck.
Is it possible to have each item draggable (as it already is) AND perform a click event of some kind?
Any help or steer in the right direction would be greatly appreciated.
It seems like your task here is to add physics to a set of DOM navigation list nodes. You may be under the impression that matter.js needs to be provided a canvas to function and that hiding the canvas or setting its opacity to 0 is necessary if you want to ignore it.
Actually, you can just run MJS headlessly using your own update loop without injecting an element into the engine. Effectively, anything related to Matter.Render or Matter.Runner will not be needed and you can use a call to Matter.Engine.update(engine); to step the engine forward one tick in the requestAnimationFrame loop. You can then position the DOM elements using values pulled from the MJS bodies. You're already doing both of these things, so it's mostly a matter of cutting out the canvas and rendering calls.
Here's a runnable example that you can reference and adapt to your use case.
Positioning is the hard part; it takes some fussing to ensure the MJS coordinates match your mouse and element coordinates. MJS treats x/y coordinates as center of the body, so I used body.vertices[0] for the top-left corner which matches the DOM better. I imagine a lot of these rendering decisions are applicaton-specific, so consider this a proof-of-concept.
const listEls = document.querySelectorAll("#mjs-wrapper li");
const engine = Matter.Engine.create();
const stack = Matter.Composites.stack(
// xx, yy, columns, rows, columnGap, rowGap, cb
0, 0, listEls.length, 1, 0, 0,
(xx, yy, i) => {
const {x, y, width, height} = listEls[i].getBoundingClientRect();
return Matter.Bodies.rectangle(x, y, width, height, {
isStatic: i === 0 || i + 1 === listEls.length
});
}
);
Matter.Composites.chain(stack, 0.5, 0, -0.5, 0, {
stiffness: 0.5,
length: 20
});
const mouseConstraint = Matter.MouseConstraint.create(
engine, {element: document.querySelector("#mjs-wrapper")}
);
Matter.Composite.add(engine.world, [stack, mouseConstraint]);
listEls.forEach(e => {
e.style.position = "absolute";
e.addEventListener("click", e =>
console.log(e.target.textContent)
);
});
(function update() {
requestAnimationFrame(update);
stack.bodies.forEach((block, i) => {
const li = listEls[i];
const {x, y} = block.vertices[0];
li.style.top = `${y}px`;
li.style.left = `${x}px`;
li.style.transform = `translate(-50%, -50%)
rotate(${block.angle}rad)
translate(50%, 50%)`;
});
Matter.Engine.update(engine);
})();
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body {
height: 100%;
}
body {
min-width: 600px;
}
#mjs-wrapper {
/* position this element */
margin: 1em;
height: 100%;
}
#mjs-wrapper ul {
font-size: 14pt;
list-style: none;
user-select: none;
position: relative;
}
#mjs-wrapper li {
background: #fff;
border: 1px solid #555;
display: inline-block;
padding: 1em;
cursor: move;
}
#mjs-wrapper li:hover {
background: #f2f2f2;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.18.0/matter.min.js"></script>
<div id="mjs-wrapper">
<ul>
<li>Foo</li>
<li>Bar</li>
<li>Baz</li>
<li>Quux</li>
<li>Garply</li>
<li>Corge</li>
</ul>
</div>

image coords changing on browser width change

Hi I am clicking on particular point on image and displaying a icon on selected area. When i change resolution that point is moving to other part of image. below is my code.How to fix the coords on resolution or browser width change. Added eventlistner and need to know on window resize any calculation have to be done?
My component is:
import React, { Component } from "react";
import { withRouter } from "react-router-dom";
import { connect } from "react-redux";
import { showCardDetails } from "../../store/Action";
let temp = [];
class cardDetails extends Component {
constructor(props) {
super(props);
}
state = {
left: "",
right: "",
coords: []
};
componentDidMount() {
const { dispatch } = this.props;
console.log(this.props.backOffice + "props");
console.log(this.props.match.params.userId);
let coords = JSON.parse(localStorage.getItem("coords"));
this.setState({ coords: coords });
window.addEventListener("resize", this.handlePress);
}
handlePress = () => {
const { coords } = this.state;
console.log(this.state);
//anycalculation here?
};
handleclick = e => {
var canvas = document.getElementById("imgCoord");
var w = document.documentElement.clientWidth;
var h = document.documentElement.clientHeight;
console.log(e.offsetLeft);
var x =
e.clientX +
document.body.scrollLeft +
document.documentElement.scrollLeft;
var y =
e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
let left = x + "px";
let top = y + "px";
let obj = {
left: left,
top: top,
width: w,
height: h
};
temp.push(obj);
this.setState({ left: left, top: top, coords: temp });
localStorage.setItem("coords", JSON.stringify(temp));
};
render() {
const { backOffice, match } = this.props;
const { left, top, coords } = this.state;
console.log(coords);
return (
<React.Fragment>
<div>
<img
id="imgCoord"
src={require("../../assets/images/imageTagging.PNG")}
onClick={$event => this.handleclick($event)}
/>
{coords &&
coords.map(item => (
<img
style={{
width: "20px",
position: "absolute",
left: item.left,
top: item.top,
backgroundColor: "white"
}}
src={require("../../assets/images/tag.png")}
id="marker"
/>
))}
</div>
</React.Fragment>
);
}
}
/**
* Redux map state
#param {} state
#param {} ownParams
*/
function mapStateToProps(state, ownParams) {
console.log(state);
return {
backOffice: state.selectedCardData
};
}
export default withRouter(connect(mapStateToProps)(cardDetails));
[EDITED]
you need to add a event listener (here is docs https://www.w3schools.com/jsref/event_onresize.asp ) in componentDidMount method, and remove this listener in componentWillUnmount method.
inside listener put a function to set new coordinates.
and also need to calculate a percentage of icon's coordinates relative to width and height of the window
handleclick = e => {
...your previous code
const yPercent = h / top;
const xPercent = w / left;
this.setState({ yPercent, xPercent });
};
handleResize = () => {
var w = document.documentElement.clientWidth;
var h = document.documentElement.clientHeight;
const newTop = (this.state.yPercent * h) + "px";
const newLeft = (this.state.xPercent * w) + "px";
this.setState({ left: newLeft; top: newTop });
}

how to animate a div within a boundry

To preface, this is my first time using JQuery so i dont really know the syntax and i'm a beginning programmer in javascript and I'm new to stackoverflow too. So apologies in advance.
Also apologies in advance if this has been asked before. This should be simple, yet somehow i can't figure it out and it's a little hard to search for.
Problem: I need my animation to be moving within a div boundry that it's in.
i tried changing the window in var h and var w to the id of my container, it doesn't work:
var h = $(#ghosts).height() - 75;
var w = $(#ghosts).height() - 75;
// html
<div id="playBoxProperties">
<div id="playBox"> // play area
<div id="ghosts"> // container for all 8 ghosts
<div id='g1' class="boo"> // one of the moving ghosts
</div>
</div>
</div>
</div>
// to randomize the movement
function randomPos() {
var h = $(window).height() - 75; // i need to change the window, to something else.
var w = $(window).width() - 75; // But i dont know what.
var newH = Math.floor(Math.random() * h);
var newW = Math.floor(Math.random() * w);
return [newH, newW];
}
// animation to move around
function animateDiv(divID) {
var newPos = randomPos();
$(divID).animate({ top: newPos[0], left: newPos[1] }, 4000, function () {
animateDiv(divID);
});
I expect it to be inside the black box
Subtract the currently iterating ghost element size from the random coordinate relative to the parent wrapper:
pass the parent and the animating child like randomPos($chi, $par)
Use Strings as your selectors. $(#ghosts); should be $('#ghosts');
Create a small jQuery plugin if you want: $.fn.animateGhosts. Use it like $ghosts.animateGhosts();
const rand = (min, max) => Math.random() * (max - min) + min;
const $ghostsWrapper = $('#ghosts');
const randomPos = ($chi, $par) => ({ // Randomize position
x: ~~(Math.random() * ($par.width() - $chi.width())),
y: ~~(Math.random() * ($par.height() - $chi.height()))
});
$.fn.animateGhosts = function() {
function anim() {
const pos = randomPos($(this), $ghostsWrapper);
$(this).stop().delay(rand(100, 500)).animate({
left: pos.x,
top: pos.y,
}, rand(1000, 4000), anim.bind(this));
}
return this.each(anim);
};
$('.boo').animateGhosts();
#ghosts {
position: relative;
height: 180px;
outline: 2px solid #000;
}
.boo {
position: absolute;
background: fuchsia;
width: 50px;
height: 50px;
}
<div id="ghosts">
<div class="boo">1</div>
<div class="boo">2</div>
<div class="boo">3</div>
<div class="boo">4</div>
<div class="boo">5</div>
<div class="boo">6</div>
</div>
<script src="//code.jquery.com/jquery-3.4.1.js"></script>
Better performance using CSS transition and translate
Here's an example that will use the power of CSS3 to move our elements in a hardware (GPU) accelerated fashion. Notice how, when slowing down, the elements are not zigzagging to the round pixel value (since jQuery animates top and left).
Instead we'll use transition for the CSS3 animation timing and translate(x, y) for the positions:
const rand = (min, max) => Math.random() * (max - min) + min;
const $ghostsWrapper = $('#ghosts');
const randomPos = ($chi, $par) => ({ // Randomize position
x: ~~(Math.random() * ($par.width() - $chi.width())),
y: ~~(Math.random() * ($par.height() - $chi.height()))
});
$.fn.animateGhosts = function() {
function anim() {
const pos = randomPos($(this), $ghostsWrapper);
$(this).css({
transition: `${rand(1, 4)}s ${rand(0.1, 0.4)}s ease`, // Speed(s) Pause(s)
transform: `translate(${pos.x}px, ${pos.y}px)`
}).one('transitionend', anim.bind(this));
}
return this.each(anim);
};
$('.boo').animateGhosts();
#ghosts {
position: relative;
height: 180px;
outline: 2px solid #000;
}
.boo {
position: absolute;
background: fuchsia;
width: 50px;
height: 50px;
}
<div id="ghosts">
<div class="boo">1</div>
<div class="boo">2</div>
<div class="boo">3</div>
<div class="boo">4</div>
<div class="boo">5</div>
<div class="boo">6</div>
</div>
<script src="//code.jquery.com/jquery-3.4.1.js"></script>

Calculating height and width of Vue slots

I am having trouble calculating the height and width of slots. I am trying to render images in my Perimeter component. These images have a size 105x160. However, when I console.log the clientWidth and clientHeight, I get 0x24.
I believe my problem is related to this: In vue.js 2, measure the height of a component once slots are rendered but I still can't figure it out. I've tried using $nextTick on both the Perimeter component and the individual slot components.
In my Perimeter component, I have:
<template>
<div class="d-flex">
<slot></slot>
<div class="align-self-center">
<slot name="center-piece"></slot>
</div>
</div>
</template>
<script>
export default {
name: 'Perimeter',
mounted() {
this.distributeSlots();
},
updated() {
this.distributeSlots();
},
computed: {
centerRadius() {
return this.$slots['center-piece'][0].elm.clientWidth / 2;
},
},
methods: {
distributeSlots() {
let angle = 0;
const {
clientHeight: componentHeight,
clientWidth: componentWidth,
offsetTop: componentOffsetTop,
offsetLeft: componentOffsetLeft,
} = this.$el;
const componentXCenter = componentWidth / 2;
const componentYCenter = componentHeight / 2;
const slots = this.$slots.default.filter(slot => slot.tag) || [];
const step = (2 * Math.PI) / slots.length;
slots.forEach((slot) => {
slot.context.$nextTick(() => {
const { height, width } = slot.elm.getBoundingClientRect();
console.log(`height ${height}, width ${width}`);
const distanceFromCenterX = (this.centerRadius + componentXCenter) * Math.cos(angle);
const distanceFromCenterY = (this.centerRadius + componentYCenter) * Math.sin(angle);
const x = Math.round((componentXCenter + distanceFromCenterX + componentOffsetLeft) - (width / 2));
const y = Math.round((componentYCenter + distanceFromCenterY + componentOffsetTop) - (height / 2));
slot.elm.style.left = `${x}px`;
slot.elm.style.top = `${y}px`;
angle += step;
});
});
},
},
};
</script>
I also had my distributeSlots() method written without $nextTick:
distributeSlots() {
let angle = 0;
const {
clientHeight: componentHeight,
clientWidth: componentWidth,
offsetTop: componentOffsetTop,
offsetLeft: componentOffsetLeft,
} = this.$el;
const componentXCenter = componentWidth / 2;
const componentYCenter = componentHeight / 2;
const slots = this.$slots.default.filter(slot => slot.tag) || [];
const step = (2 * Math.PI) / slots.length;
slots.forEach((slot) => {
const { height, width } = slot.elm.getBoundingClientRect();
const distanceFromCenterX = (this.centerRadius + componentXCenter) * Math.cos(angle);
const distanceFromCenterY = (this.centerRadius + componentYCenter) * Math.sin(angle);
const x = Math.round((componentXCenter + distanceFromCenterX + componentOffsetLeft) - (width / 2));
const y = Math.round((componentYCenter + distanceFromCenterY + componentOffsetTop) - (height / 2));
slot.elm.style.left = `${x}px`;
slot.elm.style.top = `${y}px`;
angle += step;
});
},
I am passing to the Perimeter component as follows:
<template>
<perimeter>
<div v-for="(book, index) in books.slice(0, 6)" v-if="book.image" :key="book.asin" style="position: absolute">
<router-link :to="{ name: 'books', params: { isbn: book.isbn }}">
<img :src="book.image" />
</router-link>
</div>
<perimeter>
</template>
Even worse, when I console.log(slot.elm) in the forEach function and open up the array in the browser console, I see the correct clientHeight + clientWidth:
Usually in such cases it's a logical mistake, rather than an issue with the framework. So I would go with simplifying your code to the bare minimum that demonstrates your issue.
Assuming you get clientWidth and clientHeight on mounted() or afterwards, as demonstarted below, it should just work.
Avoid any timer hacks, they are the culprit for bugs that are extremely hard to debug.
<template>
<div style="min-height: 100px; min-width: 100px;">
<slot />
</div>
</template>
<script>
export default {
name: 'MyContainer',
data (){
return {
width: 0,
height: 0,
}
},
mounted (){
this.width = this.$slots["default"][0].elm.clientWidth
this.height = this.$slots["default"][0].elm.clientHeight
console.log(this.width, this.height) // => 100 100 (or more)
},
}
</script>
<style scoped lang="scss">
</style>
You can use a trick and put the code in a setTimeout method to execute it in another thread with a tiny delay:
setTimeout(() => {
// Put your code here
...
}, 80)

Categories