Framer Motion (React): How to order initial, exit and layout animations? - javascript

I am using framer-motion to animate a change in grid columns.
Here is what I want to do:
I've got nine buttons in a grid (the #db-wrapper is the grid container).
The user switches to the "maximized" view (props.minimize changes) A fourth column gets added to the grid, the buttons which are already there move to their new positions, leaving empty cells for the remaining buttons. Then, new buttons slide in from the bottom, filling the empty cells. The buttons in the grid are now positioned in consecutive cells.
Now the user switches back to the minimized view. First, all the buttons that will get deleted should slide out to the bottom.
Then, the remaining buttons should move to their new positions in the grid (their old positions, like at the beginning), so that they fill consecutive cells.
(Basically, I want to do step 1 and 2 backwards after the user has switched back)
I've already achieved step 1 and 2. Here is the functional component:
const DrumButtons = (props) => {
const drumButtonsVariants = {
hidden: {
y: "140vh",
},
visible: {
y: 0,
transition: {
type: "tween",
duration: 1,
delay: 0.1,
},
},
exit: {
y: "140vh",
transition: {
type: "tween",
duration: 1,
delay: 0.1,
},
},
};
let dbWrapperStyle = {};
if (!props.minimized) {
dbWrapperStyle = {
gridTemplateColumns: "1fr 1fr 1fr 1fr",
};
}
let singleWrapperStyle = {
width: "100%",
height: "100%",
};
let buttonTransition = { duration: 0.5, delay: 0.1 };
return (
<div id="db-wrapper" style={dbWrapperStyle}>
<AnimatePresence>
{props.buttonsarr.map((elem, i) => {
return (
<motion.div
variants={drumButtonsVariants}
initial="hidden"
animate="visible"
exit="exit"
key={elem.press}
style={singleWrapperStyle}
>
<motion.button
layout
transition={buttonTransition}
key={elem.press}
className="drum-pad"
onClick={dbHandleClickWrapper(
props.changetext,
props.buttonsarr
)}
id={elem.name}
>
<span className="front">{elem.press.toUpperCase()}</span>
<audio
key={elem.press.toUpperCase()}
id={elem.press.toUpperCase()}
src={props.buttonsarr[i].source}
preload="auto"
className="clip"
></audio>
</motion.button>
</motion.div>
);
})}
</AnimatePresence>
</div>
);
};
What is the problem, exactly?
So here's what currently happens when switching back from "maximized" to "minimized":
The buttons that are no longer needed slide out to the bottom. At the same time, the 4 grid columns get reduced to 3 columns. So the remaining buttons slide to their position in the 3-column-wide grid, leaving empty cells for the buttons that are currently sliding out, but still present in the DOM.
After the other buttons have been removed, the remaining buttons move again to fill consecutive cells of the grid.
Here is what I've tried:
I've tried adding a when: "beforeChildren" to the transitions of the drumButtonsVariants. Nothing changed.
I've played around with the delays, but I've never achieved the desired outcome. If I delay the layout animation, the removal of the buttons from the DOM will get delayed too, resulting in the same unwanted outcome.
You can view my full code here:
(main branch) https://github.com/Julian-Sz/FCC-Drum-Machine/tree/main
(version with the problem) https://github.com/Julian-Sz/FCC-Drum-Machine/tree/284606cac7cbc4bc6e13bf432c563eab4814d370
Feel free to copy and fork this repository!

The trick is to use State for the dbWrapperStyle.
Then, AnimatePresence with onExitComplete can be used.
This happens when the user switches to minimized again:
The removed buttons slide out (but they are still in the DOM - occupying grid cells)
Now they get removed from the DOM, and onExitComplete fires: The grid columns change to 3 columns and the component gets rerendered. The removal from the DOM and the grid change are happening after each other (without any delay) - resulting in a smooth animation!
Here is the new component:
const DrumButtons = (props) => {
const drumButtonsVariants = {
visible: {
y: 0,
transition: {
type: "tween",
duration: 0.8,
delay: 0.1,
},
},
exit: {
y: "140vh",
transition: {
type: "tween",
duration: 0.8,
delay: 0.1,
},
},
};
// BEFORE------------
// let dbWrapperStyle = {};
// if (!props.minimized) {
// dbWrapperStyle = {
// gridTemplateColumns: "1fr 1fr 1fr 1fr",
// };
// }
// AFTER-------------
const [dbWrapperStyle, setWrapperStyle] = useState({});
useEffect(() => {
if (!props.minimized) {
setWrapperStyle({ gridTemplateColumns: "1fr 1fr 1fr 1fr" });
}
}, [props.minimized]);
let singleWrapperStyle = {
width: "100%",
height: "100%",
};
let buttonTransition = {
type: "spring",
duration: 0.9,
delay: 0.1,
bounce: 0.5,
};
return (
<div id="db-wrapper" style={dbWrapperStyle}>
<AnimatePresence
onExitComplete={() => {
setWrapperStyle({ gridTemplateColumns: "1fr 1fr 1fr" });
}}
>
{props.buttonsarr.map((elem, i) => {
return (
<motion.div
variants={drumButtonsVariants}
initial="exit"
animate="visible"
exit="exit"
key={elem.press}
style={singleWrapperStyle}
>
<motion.button
layout
transition={buttonTransition}
key={elem.press}
className="drum-pad"
onClick={dbHandleClickWrapper(
props.changetext,
props.buttonsarr
)}
id={elem.name}
>
<span className="front">{elem.press.toUpperCase()}</span>
<audio
key={elem.press.toUpperCase()}
id={elem.press.toUpperCase()}
src={props.buttonsarr[i].source}
preload="auto"
className="clip"
></audio>
</motion.button>
</motion.div>
);
})}
</AnimatePresence>
</div>
);
};

Related

How to rotate item text in a menu list so that overflow can be seen

Say i have a TextMenuItem, which uses MenuItem from mui, that is an item in this chain DropDownSearch > SimpleListMenu > FixedSizeList > TextMenuItem, which TLDR is a searchable dropdown component with text items.
When the text is too long for the FixedSizeList (a container) we hide the overflow, great... but we can't see the text when the text is long.
So what can i do? The PM's proposed solution was to pop-out the item text from the already popped out menu, but that does not seem right.
On mouse over is there a simple way to read? rotate? spin? the text so that the menu item text can be read while not popping from a pop-out menu?
export type TextMenuItemType = FunctionComponent<ITextMenuItemProps>;
const TextMenuItem: TextMenuItemType = (props) => {
const { text, selected, onSelect } = getTextMenuItemProps(props);
const styles = useTextMenuItemStyles();
return (
<MenuItem sx={styles.container} selected={selected} onClick={onSelect}>
<Typography sx={styles.label}>{text}</Typography>
</MenuItem>
);
};
const useTextMenuItemStyles = () => {
return css({
container: {
minHeight: 'auto',
},
label: {
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
});
};
This is the css that was used to get the text rotating on hover, its not ideal as it rotates on all size inputs. Ideally would like to only rotate where context overflows.
Not ideal, looking for better solutions.
container: {
minHeight: 'auto',
overflow: 'hidden',
},
label: {
whiteSpace: 'nowrap',
overflow: undefined,
overflowWrap: 'break-word',
'#keyframes scrollText': {
'0%': { transform: 'translate(0%, 0)' },
'100%': { transform: 'translate(-100%, 0)' },
},
'&:hover': {
animation: 'scrollText 4s linear infinite',
},
},

Images not loading fully until hovered over

On my React website, I have a vertical list of screenshots of my past projects, when the page first loads the images load, but are only a sliver until the user hovers over them.
Once hovered over the images expand to their full size and then work properly. I am looking to prevent the user from hovering over them in order for it to load properly.
The list is mapped from an array
items: [
{
image: "./images/spokanepowerstroke.jpg",
title: "Spokane Power Stroke",
link: "/powerstroke",
},
...
]
<Col md="auto">
{this.state.items.map(({ title, image, link }) => (
<motion.div className="thumbnail" variants={thumbnailVariants}>
{" "}
<motion.div
key={title}
className="frame"
whileHover="hover"
variants={frameVariants}
transition={transition}
>
<p className="projectstitle">{title}</p>
<Link to={link}>
<motion.img
src={image}
alt={image}
variants={imageVariants}
transition={transition}
/>
</Link>
</motion.div>
</motion.div>
))}
</Col>
And the hover and transition effects are controlled with framer motion.
const transition = { duration: 0.5, ease: [0.43, 0.13, 0.23, 0.96] };
const thumbnailVariants = {
initial: { scale: 0.9, opacity: 0 },
enter: { scale: 1, opacity: 1, transition },
exit: {
scale: 0.5,
opacity: 0,
transition: { duration: 1.5, ...transition },
},
};
const frameVariants = {
hover: { scale: 0.95 },
};
const imageVariants = {
hover: { scale: 1.1 },
};
const changepage = {
in: {
opacity: 1,
},
out: {
opacity: 0,
},
};
const pagetransition = {
duration: 1.5,
};
I've looked over my code and have found no reason why the images are only loading partially.
The website is viewable here Website
And the Github repo with all the code is here Github
(the projects page)
Thank you in advance for your expertise.
In your App.css code, change this:
.thumbnail img { width: 46vw; height: 100%; }
to height:auto;
% heights will stretch/distort an image usually, if you want to keep aspect ratio use auto
also maybe setting width & height on the img element in your code might help prevent sizing issues, because right now the browser doesn't know how big the image actually is, and since it's loaded in via React JS not in the HTML first served it probably can't calculate it until the hover animation forces a repaint.

React - Carousel with transitions between slides, react-spring

I've currently made a carousel component, I'm passing my data to it but I would like to add a transition between switching the data.
I've heard a lot of people recommending react-spring so I thought I'd give it a try. I'm currently trying to implement the useTransition hook, but it's not working as desired.
I simply want to fade in/out only the data when the user presses the "left" / "right" buttons. Currently it starts to fade my content, but then creates a clone of my carousel, and doesn't fade... I'm not sure what I'm doing wrong or if this is the correct hook I should be using?
Ultimately I would prefer a stagger animation when the content fades
in with some delay between each element. For example: the title, then price, then
desciption, etc, would fade in but with a 300ms delay between each
element.
Here's what I currently have:
const mod = (n: any, m: any) => ((n % m) + m) % m;
const CarouselContainer: React.FC = () => {
const [index, setIndex] = useState(0);
const handlePrev = useCallback(() => {
setIndex(state => mod(state - 1, data.length));
}, [setIndex]);
const handleNext = useCallback(() => {
setIndex(state => mod(state + 1, data.length));
}, [setIndex]);
const transitions = useTransition([index], (item: number) => item, {
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 }
});
return (
<>
{transitions.map(({ item, props, key }) => (
<Carousel
prevClick={handlePrev}
icon={data[item].icon}
title={data[item].title}
price={data[item].price}
description={data[item].description}
href={data[item].href}
nextClick={handleNext}
style={props}
key={key}
/>
))}
</>
);
};
There's quite a lot of code so I'm providing a CodeSandBox, forks are appretiated! :)
When the mount and unmount happens during the transition, there is a moment when the new and the old element is in the dom simultaneously. Most of the time I use absolute positioning with transition for this reason. This way the change blend in well.
For this you have to change to position: absolute here:
const StyledWrapper = styled.div`
position: absolute;
display: grid;
grid-template-columns: auto auto 20% 20% 1fr auto auto;
grid-template-rows: 3em 1fr auto 1fr 3em;
grid-gap: 0;
`;
I would try to add some movement to it:
const transitions = useTransition([index], (item: number) => item, {
from: { opacity: 0, transform: 'translateX(-420px)' },
enter: { opacity: 1, transform: 'translateX(0px)' },
leave: { opacity: 0, transform: 'translateX(420px)' }
});
And here is your modified code:
https://codesandbox.io/s/empty-rgb-xwqh1
Next you will want to handle the direction of the movement, according to the button pressed. Here is an example for that: https://codesandbox.io/s/image-carousel-with-react-spring-usetransition-q1ndd

Framer motion animate when element is in-view (When you scroll to element)

Is there any built-in way to make the animation start when the element is in-view (for example, when we scroll to the element)?
Framer Motion has mount animations section which says:
When a component mounts, it'll automatically animate to the values in animate if they're different from those defined in style or initial
So I couldn't really find a straight forward way to make the animation start when it comes into view.
However, I reached the only option I see for now is using Animation Controls which means I'll have to implement a listener on the scroll manually and trigger the control.start(), any easier ways are much appreciated.
framer-motion has built-in support for this use case since version 5.3.
Here's a CodeSandbox demonstrating the pattern: https://codesandbox.io/s/framer-motion-animate-in-view-5-3-94j13
Relevant code:
function FadeInWhenVisible({ children }) {
return (
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ duration: 0.3 }}
variants={{
visible: { opacity: 1, scale: 1 },
hidden: { opacity: 0, scale: 0 }
}}
>
{children}
</motion.div>
);
}
Usage:
<FadeInWhenVisible>
<Box />
</FadeInWhenVisible>
Previous versions:
You can currently use the imperative animation controls to achieve this effect. Intersection observers are useful to detect if an element is currently visible.
Here's a CodeSandbox demonstrating the pattern: https://codesandbox.io/s/framer-motion-animate-in-view-gqcc8.
Relevant code:
function FadeInWhenVisible({ children }) {
const controls = useAnimation();
const [ref, inView] = useInView();
useEffect(() => {
if (inView) {
controls.start("visible");
}
}, [controls, inView]);
return (
<motion.div
ref={ref}
animate={controls}
initial="hidden"
transition={{ duration: 0.3 }}
variants={{
visible: { opacity: 1, scale: 1 },
hidden: { opacity: 0, scale: 0 }
}}
>
{children}
</motion.div>
);
}

What is the correct way to set the new coordinates of a view after translation animation in react-native?

I am trying to run a few simple animations using react-native-animatable library. (But I believe the question should be generic to any react animations so adding other tags as well.)
The problem is, in the first time, the image animates just as expected. But when aimed to start second animation animation with the gesture, the image translation starts from its original coordinates.
A search yielt, in Android development (which is obviously not my case) there seems a method, setFillAfter which sets the coordinate after the animation.
My question is, how to set the location (left / top values for example) to the final translated point so that consecutive animation starts from the point the previous translation left.
The expo snack for below code block is here.
import * as React from 'react';
import { Image, StyleSheet, ImageBackground } from 'react-native';
import * as Animatable from 'react-native-animatable';
import { PanGestureHandler, State } from 'react-native-gesture-handler';
import testImg from './test.png';
import backImg from './back.png';
export default class App extends React.Component {
onTestMove(event) {
this.testAnimRef.transitionTo({
translateX: event.nativeEvent.translationX,
translateY: event.nativeEvent.translationY,
}, 0);
}
render() {
return (
<ImageBackground source={backImg} style={{ flex: 1 }} >
<PanGestureHandler
key={`test`}
onGestureEvent={(e) => { this.onTestMove(e) }}
onHandlerStateChange={e => { }}
>
<Animatable.View style={styles._animatable_view}
ref={((ref) => { this.testAnimRef = ref }).bind(this)}
useNativeDriver={true}
>
<Image source={testImg} style={styles._image} />
</Animatable.View>
</PanGestureHandler>
</ImageBackground>
);
}
}
const styles = StyleSheet.create({
_image: {
width: 50,
height: 25,
resizeMode: 'contain',
backgroundColor: 'black',
borderColor: 'gainsboro',
borderWidth: 2,
},
_animatable_view: {
position: "absolute",
top: 200,
left: 100,
},
});
I had the same problem trying to move around some cards in a view, and upon further dragging, they would reset to their origin.
My theory is/was that while the translated view would have its x / y coordinates translated, this would not apply to the parent of that view, and so the animated event passed from that component would initially have the original coordinates (nuke me if I'm wrong here)
So my solution was to keep an initial offset value in state, and maintain this every time the user releases the dragged motion
_onHandleGesture: any
constructor(props: OwnProps) {
super(props)
this.state = {
animationValue: new Animated.ValueXY({ x: 0, y: 0 }),
initialOffset: { x: 0, y: 0 },
}
this._onHandleGesture = (e: PanGestureHandlerGestureEvent) => {
this.state.animationValue.setValue({
x: e.nativeEvent.translationX + this.state.initialOffset.x, <- add initial offset to coordinates passed
y: e.nativeEvent.translationY + this.state.initialOffset.y,
})
}
}
_acceptCard = (cardValue: number) => {
const { targetLocation, onAccept } = this.props
const { x, y } = targetLocation
onAccept(cardValue)
Animated.spring(this.state.animationValue, {
// Some animation here
}).start(() => {
this.setState({ initialOffset: targetLocation }) // <- callback to set state value for next animation start
})
}
and the render method
<PanGestureHandler
onHandlerStateChange={this.onPanHandlerStateChange}
onGestureEvent={this._onHandleGesture}
failOffsetX={[-xThreshold, xThreshold]}
>
<Animated.View
style={{
position: "absolute",
left: 0,
top: 0,
transform: [{ translateX: this.state.animationValue.x }, { translateY: this.state.animationValue.y }],
}}
>
<CardTile size={size} content={content} layout={layout} backgroundImage={backgroundImage} shadow={shadow} />
</Animated.View>
</PanGestureHandler>
This example is based on the react-native-gesture-handler library, but the concept should apply to other solutions.
Dont know if this way is advisable, though it is functional.
Hope this helps!

Categories