Framer Motion opacity keyframe only animates on initial render - javascript

I'm using Framer Motion
The box should go from opacity: 0 to 1 and increase in height on every state toggle. However, the height is different based on the condition.
I don't have a clue why "height" is being animated every time - correctly, but "opacity" is only animated in the first render.
I tried [0, 1, 0] & [1, 0, 1] too, but none of them has the desired effect.
I even tried [0, 1, 0.5] to test better, but it stays at 0.5.
Code
https://codesandbox.io/s/angry-raman-bc9sf?file=/src/App.js
import { motion } from "framer-motion";
import { useState } from "react";
export default function App({ href = "#", label, icon, notifCount }) {
const [conditionIsMet, setConditionIsMet] = useState(false);
const notifVariants = {
conditionA: {
opacity: [0, 1],
height: ["40px", "90px"]
},
conditionB: {
opacity: [0, 1],
height: ["40px", "200px"]
}
};
return (
<>
{`Condition is ${conditionIsMet ? "A" : "B"}`}
<button onClick={() => setConditionIsMet(!conditionIsMet)} />
<motion.span
animate={conditionIsMet ? "conditionA" : "conditionB"}
variants={notifVariants}
/>
</>
);
}

I think it's a bug but as a temporary fix you can use opacity: [0, 1.01] for conditionB.
please open an issue for this bug.

Related

Passing div style as a function argument Javascript/ React

I'm building an application using React, framer-motion and react-intersection-observer.
Inside animation.js I have a function which is imported inside App.js and used as a component in App.js.
I want to apply an aspect ratio to some divs using style as a parameter, but it doesn't work.
<FadeAnimation name={'project-image--image'} style={'--ratio':16/9} />
Failed to set an indexed property on 'CSSStyleDeclaration': Indexed property setter is not supported.
I have other divs with this property and they are displayed correctly
<div className='project-image--image' style={{'--ratio':1/2}}/>
Animation.js
export const container = {
hidden: { opacity: 0, y: 5 },
visible: { opacity: 1, y: 0 }
}
function FadeAnimation({ children, name, delay, duration, style }) {
const controls = useAnimation();
const [ref, inView] = useInView();
useEffect(() => {
if (inView) {
controls.start("visible");
}
}, [controls, inView]);
return (
<motion.div className={`${name}`} style={{`${style}`}}
ref={ref}
animate={controls}
initial="hidden"
transition={{ duration: duration, delay: delay }}
variants={{
visible: { opacity: 1, y: 0 },
hidden: { opacity: 0, y: 5 }
}}
>
{children}
</motion.div>
);
}
Have you tried:
<FadeAnimation name={'project-image--image'} style={{'--ratio':16/9}} />
(Adding another curly brace)
And then, in the FadeAnimationComponent using it as
<motion.div className={`${name}`} style={style} {/*...*/}/>

How to turn on and off the scroll trigger in Material UI useScrollTrigger module?

I'm trying to create a survey page using Material UI in React. I'd like my survey to turn on when the user scrolls over a question and turns off when the user scrolls out of a question. A behavior similar to this page.
https://www.surveymonkey.com/r/Typical-Customer-Demographics-Template
After doing some research, I decided to use useScrollTrigger module in MUI. I was able to turn on the questions when I scroll down to them, but still can't figure out a way to turn them back off when I scroll down, out of them.
This is how I created my scrollToColor function:
import { useScrollTrigger } from "#material-ui/core";
export default function ScrollToColor(props) {
const {
threshold,
threshold2,
textColorBefore,
textColorAfter,
fadeIn,
fadeOut,
children,
...other
} = {
threshold: 50,
threshold2: 100,
textColorBefore: "gray",
textColorAfter: "black",
fadeIn: "0.1s ease-in",
fadeOut: "0.1s ease-out",
...props,
};
const trigger = useScrollTrigger({
disableHysteresis: true,
threshold: threshold,
target: props.window ? window() : undefined,
});
const trigger2 = useScrollTrigger({
disableHysteresis: true,
threshold: threshold2,
target: props.window ? window() : undefined,
});
return React.cloneElement(children, {
style: {
color: trigger ? textColorAfter : textColorBefore,
transition: trigger ? fadeIn : fadeOut,
color: trigger2 ? textColorBefore : textColorAfter,
transition: trigger2 ? fadeOut : fadeIn,
},
...other,
});
}
I created 2 triggers so when it scrolls to the question, the text color turns black and when scroll out, it turns back to gray. This is a sample code on how I added this to the parent component.
import {
AppBar,
Toolbar,
Typography,
ThemeProvider,
CssBaseline,
createMuiTheme
} from "#material-ui/core";
import ScrollToColor from "./ScrollToColor";
const NavbarscrollToColor = props => {
const theme = createMuiTheme();
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<ScrollToColor>
<AppBar position="static">
<Toolbar>
<Typography variant="h5" noWrap>
{props.title}
</Typography>
</Toolbar>
</AppBar>
</ScrollToColor>
</ThemeProvider>
);
};
export default NavbarscrollToColor;
But this unfortunately doesn't work. Any tips on how to make this work?
Also, is there any easier and cleaner way in React I could have archived this other than using the MUI useScrollTrigger module?
Thanks!
After spending more time on this issue, here is the solution that I came up with that works:
import { useScrollTrigger } from "#material-ui/core";
export default function ScrollToColor(props) {
const {
threshold,
threshold2,
textColorBefore,
textColorAfter,
fadeIn,
fadeOut,
children,
...other
} = {
threshold: 0,
threshold2: 100,
textColorBefore: "gray",
textColorAfter: "black",
fadeIn: "0.3s ease-in",
fadeOut: "0.3s ease-out",
...props,
};
const trigger = useScrollTrigger({
disableHysteresis: true,
threshold: threshold,
target: props.window ? window() : undefined,
});
const trigger2 = useScrollTrigger({
disableHysteresis: true,
threshold: threshold2,
target: props.window ? window() : undefined,
});
return React.cloneElement(children, {
style: {
color: trigger && !trigger2 ? textColorAfter : textColorBefore,
transition: trigger && !trigger2 ? fadeIn : fadeOut,
},
...other,
});
}

How to move plane icon with framer motion?

I have a react paper plane icon that I would like to make a cool effect by making it travel around the HTML document reaching the final position which is the menu button at the top.
This is how my react component called NavPlane looks like:
import React, { Component, useEffect } from "react";
import { useCycle } from "framer-motion";
import { FaPaperPlane } from "react-icons/fa";
import { motion } from "framer-motion";
const PlaneVariants = {
animationOne: {
x: 370,
y: 250,
transition: { ease: [0.17, 0.67, 0.83, 0.67] },
},
animationTwo: {
x: 140,
y: 150,
transition: { duration: 1.0 },
},
animationThree: {
x: 170,
y: 200,
transition: { duration: 1.0 },
},
};
export default function NavPlane() {
const [animation, cycleAnimation] = useCycle(
"animationOne",
"animationTwo",
"animationThree"
);
useEffect(() => {
setTimeout(() => {
cycleAnimation();
}, 1000);
}, []);
return (
<motion.div
className="text-2xl text-gray-600"
variants={PlaneVariants}
animate={animation}
>
<FaPaperPlane />
</motion.div>
);
}
cycleAnimation() was supposed to cycle through 3 animations but only cycling through the first two. The 3rd is only applied when making some change to the code and updating it.
The final goal is to have a movement that goes from right corner of the page to middle, does a knot movement and then travels to the top of the page.
Any ideas how to make it cycle through as many animation variants as I want?
cycleAnimation only advances one step. Initially it will start "animationOne". Once you call cycleAnimation() after 1 second, "animationTwo" will start. You need another timer if you want to play "animationThree"
useEffect(() => {
setTimeout(cycleAnimation, 1000); // start "animationTwo" after 1 second
setTimeout(cycleAnimation, 2000); // start "animationThree" after 2 seconds
}, []);

Listen to PanResponder to change state values

I'm pretty new to React Native and I'm facing a problem:
I understood the transformations that you can do with a PanResponder and then using the interpolate function.
Example:
let cardOpacity = {opacity: this.state.pan.x.interpolate({inputRange: [-150, 0, 150], outputRange: [0.9, 0, 0.9]})};
The problem I'm facing is a bit more tricky because I want to extract values from this interpolation and not any style:
I want that code to work basically:
_renderApplyOrDislikeHint() {
let cardOpacity = {opacity: this.state.pan.x.interpolate({inputRange: [-150, 0, 150], outputRange: [0.9, 0, 0.9]})};
let value = {opacity: this.state.pan.x.interpolate({inputRange: [-150, 0, 150], outputRange: [1, 0, 1]})};
this.state.pan.x.addListener(({value}) => {
this.x = value._value
console.log('Value changed', this.x);
});
let dimensionStyle = {
backgroundColor: 'white',
width: this.props.cardWidth,
height: this.props.cardHeight,
position: 'absolute'
};
return (
<Animated.View style={[dimensionStyle, cardOpacity]}>
{this.renderHint()}
</Animated.View>
)
}
renderHint(){
console.log('X in render hint', this.x)
return this.x > 0 ? <Text>Yes</Text> : <Text>Nope</Text>
}
The problem is that animated value work great for styles but they do not force the re-rendering of the renderHint() function.
And it is a very bad idea to change state during the render process.
How can I achieve that?
Soooo, I found a solution
Basically in React, the views are listening to the state of the component: so to "re-render" we need to change the state.
Therefore I added a simple change in the pan responder:
onPanResponderGrant: (e, gestureState) => {
this.state.pan.setOffset({x: this.state.pan.x._value, y: this.state.pan.y._value});
this.state.pan.setValue({x: 0, y: 0});
this.state.pan.x.addListener(({value}) => {
if (value < 0 && this.state.right){
this.setState({right: false})
}
else if (value > 0 && !this.state.right){
this.setState({right: true})
}
});
}
And then just listen to the property:
renderHint(){
return this.state.right > 0 ? <Text>Right</Text> : <Text>Left</Text>
}

How to use React TransitionMotion willEnter()

Using React Motion's TransitionMotion, I want to animate 1 or more boxes in and out. When a box enters the view, it's width and height should go from 0 pixels to 200 pixels and it's opacity should go from 0 to 1. When the box leaves the view, the reverse should happen (width/height = 0, opacity = 0)
I have tried to solve this problem here http://codepen.io/danijel/pen/RaboxO but my code is unable to transition the box in correctly. The box's style jumps immediately to a width/height of 200 pixels instead of transitioning in.
What is wrong with the code?
let Motion = ReactMotion.Motion
let TransitionMotion = ReactMotion.TransitionMotion
let spring = ReactMotion.spring
let presets = ReactMotion.presets
const Demo = React.createClass({
getInitialState() {
return {
items: []
}
},
componentDidMount() {
let ctr = 0
setInterval(() => {
ctr++
console.log(ctr)
if (ctr % 2 == 0) {
this.setState({
items: [{key: 'b', width: 200, height: 200, opacity: 1}], // fade box in
});
} else {
this.setState({
items: [], // fade box out
});
}
}, 1000)
},
willLeave() {
// triggered when c's gone. Keeping c until its width/height reach 0.
return {width: spring(0), height: spring(0), opacity: spring(0)};
},
willEnter() {
return {width: 0, height: 0, opacity: 1};
},
render() {
return (
<TransitionMotion
willEnter={this.willEnter}
willLeave={this.willLeave}
defaultStyles={this.state.items.map(item => ({
key: item.key,
style: {
width: 0,
height: 0,
opacity: 0
},
}))}
styles={this.state.items.map(item => ({
key: item.key,
style: {
width: item.width,
height: item.height,
opacity: item.opacity
},
}))}
>
{interpolatedStyles =>
<div>
{interpolatedStyles.map(config => {
return <div key={config.key} style={{...config.style, backgroundColor: 'yellow'}}>
<div className="label">{config.style.width}</div>
</div>
})}
</div>
}
</TransitionMotion>
);
},
});
ReactDOM.render(<Demo />, document.getElementById('app'));
As per the documentation of styles under the TransitionMotion section (and I don't claim to have understood all of it entirely :)):
styles: ... an array of TransitionStyle ...
The key thing to note here is that there are 2 types of style objects that this library deals with (or at least this TransitionMotion part of it) and it calls them TransitionStyle and TransitionPlainStyle.
The previous values passed into styles attribute were of TransitionPlainStyle. Changing them to TransitionStyle magically starts animating the Enter sequence.
You can read more about 2 different types mentioned above over here.
styles={this.state.items.map(item => ({
key: item.key,
style: {
width: spring(item.width),
height: spring(item.height),
opacity: spring(item.opacity)
}
}))}
Forked codepen demo.
Again, I do not fully understand the inner workings of it just yet. I just know that your styles had to be changed in the above way to make it work.
I will be happy if someone can educate me on this as well.
Hope this helps.

Categories

Resources