How do you animate menu with framer motion on-click? - javascript

(Using React obviously + Gatsby)
I have a hamburger button that gonna open a nav-menu in my website.
I wanted to know how to make the menu open with an animation using Framer Motion.

you could use this method that is given in the examples section of the framer motion documentation.
Framer Motion API Documentation
import { motion } from "framer-motion"
const variants = {
open: { opacity: 1, x: 0 },
closed: { opacity: 0, x: "-100%" },
}
export const MyComponent = () => {
const [isOpen, setIsOpen] = useState(false)
return (
<motion.nav
animate={isOpen ? "open" : "closed"}
variants={variants}
>
'Menu Content'
</motion.nav>
)
}

<MonkeyPic rotate={showFistBump} />
// ...
// Then switch animation variant depending on rotate prop
const variants = {
rotate: { rotate: [0, -30, 0], transition: { duration: 0.5 } },
// You can do whatever you want here, if you just want it to stop completely use `rotate: 0`
stop: { y: [0, -10, 0], transition: { repeat: Infinity, repeatDelay: 3 } }
};
const MonkeyPic = ({ rotate }) => {
return (
<div>
<motion.img
variants={variants}
animate={rotate ? 'rotate' : 'stop'}
id="monkeyFace"
src="/images/Monkey.png"
/>
</div>
);
};

Related

Assign variable to Animated.timing toValue

I want to rotate a picture using Animated.timing but assigning a variable containing a number doesnt animate anything. Putting in int like 50, works.
toValue: degree does nothing, toValue:50 does what I need. I print out degree inside Text block like it should.
I put animated.timing inside render() so that I can access degree const directly, I tried putting it in componentDidMount() but that just generated errors.
This code prints out x y z and calculated degree in xxx.xx format, I set up xyz in state and later use it to pass it to compassHeading function to calculate degree:
render() {
const movementX = this.state.x;
const movementY = this.state.y;
const movementZ = this.state.z;
const degree= compassHeading(movementX, movementY, movementZ).toFixed(2);
Animated.timing(
this.state.spinValue,
{
toValue: degree,
duration: 3000,
easing: Easing.linear,
useNativeDriver: true
}
)
.start();
return (
<View >
<Animated.Image
style={{
width: 350, height: 350,
transform: [{
rotate: this.state.spinValue.interpolate({
inputRange: [0, 360],
outputRange: ['0deg', '360deg']
}) }] }}
source={require('./compass.png')}
/>
<Text> {movementX}</Text>
<Text> {movementY}</Text>
<Text> {movementZ}</Text>
<Text> {degree}</Text>
</View>
);
}
}
You start a new animation in every render and each animation will renders loads of time to vary this.state.spinValue for your animation and each render you start the animation again ...
You need to start the Animated.timing outside of the render function and depends on how often the movementXYZ change.(let say they change every 100ms) you may need to wait the animation finish, before you start another one. While it is animating, if any new movement coming in, store the latest value for the next animation. (I dont' suggest interrupting the on going animation too frequent, particularly using nativeDriver....so I let it finish one first.)
It look something like:
onMovementChange(){
const degree= compassHeading(movementX, movementY, movementZ).toFixed(2);
if(!this.state.isAnimating){
this.setState({isAnimating:true,nextDegree:degree});
Animated.timing(
this.state.spinValue,
{
toValue: degree,
duration: 3000,
easing: Easing.linear,
useNativeDriver: true
}
).start(()=>{
if(this.state.nextDegree!=degree){
onMovementChange()
}
this.setState({isAnimating:false})
});
}else{
if(this.state.nextDegree!=degree){
this.setState({nextDegree:degree});
}
}
}
render() {
const movementX = this.state.x;
const movementY = this.state.y;
const movementZ = this.state.z;
return (
<View >
<Animated.Image
style={{
width: 350, height: 350,
transform: [{
rotate: this.state.spinValue.interpolate({
inputRange: [0, 360],
outputRange: ['0deg', '360deg']
}) }] }}
source={require('./compass.png')}
/>
<Text> {movementX}</Text>
<Text> {movementY}</Text>
<Text> {movementZ}</Text>
<Text> {this.state.nextDegree}</Text>
</View>
);
}

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!

Scale stage and drag element relative positions not working

Im having small issue with calculating realtime position while scaled stage in KonvaJS React. In my example I have two Rect's small (red) one position is relative to Big Rect (yellow), While dragging big rectangle small one moves relative to big one but when I scale UP or DOWN small one jumps some pixels in x, y positions.
Working example
import React from "react";
import ReactDOM from "react-dom";
import { Rect, Stage, Layer } from "react-konva";
import "./styles.css";
function Box({ attrs, draggable, updateAttr }) {
const onDragStart = e => {
const element = e.target;
if (typeof updateAttr === "function") {
updateAttr(element._lastPos);
}
};
const onDragMove = e => {
const element = e.target;
if (typeof updateAttr === "function") {
updateAttr(element._lastPos);
}
};
const onDragEnd = e => {
const element = e.target;
if (typeof updateAttr === "function") {
updateAttr(element._lastPos);
}
};
return (
<Rect
draggable={draggable}
onDragEnd={onDragEnd}
onDragMove={onDragMove}
onDragStart={onDragStart}
fill={attrs.fill}
x={attrs.x}
y={attrs.y}
width={attrs.width}
height={attrs.height}
/>
);
}
const calculatePos = ({ r1, r2 }) => ({
...r2,
x: parseInt(r1.width + r1.x + 10, 10),
y: parseInt(r1.y + r1.height / 2 - r2.height / 2, 10)
});
const boxAttr = {
x: 50,
y: 50,
width: 200,
height: 150,
fill: "yellow"
};
const secondAttr = calculatePos({
r2: {
fill: "red",
width: 20,
height: 20
},
r1: boxAttr
});
class App extends React.Component {
state = {
scale: 1,
boxAttr,
secondAttr
};
updateScale = scale => {
this.setState({ scale });
};
updateAttr = pos => {
const { secondAttr, boxAttr } = this.state;
this.setState({
secondAttr: calculatePos({
r2: secondAttr,
r1: { ...boxAttr, ...pos }
})
});
};
render() {
const { scale, boxAttr, secondAttr } = this.state;
return (
<div className="App">
<button
onClick={() => {
this.updateScale((parseFloat(scale) + 0.1).toFixed(1));
}}
>
+ Scale ({scale})
</button>
<button
onClick={() => {
this.updateScale((parseFloat(scale) - 0.1).toFixed(1));
}}
>
- Scale ({scale})
</button>
<Stage
scaleX={scale}
scaleY={scale}
width={window.innerWidth}
height={window.innerHeight}
>
<Layer>
<Box
updateAttr={this.updateAttr}
draggable={true}
attrs={boxAttr}
/>
<Box draggable={false} attrs={secondAttr} />
</Layer>
</Stage>
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Please help me to update correct x,y for red rectangle.
I know Grouped Draggable solution will fix this issue, but I cannot
implement in my real application due to complex relations that cannot
be group for draggable reason
When the first box is dragged, you have to translate the mouse coordinates on the canvas to the coordinate system inside the scaled canvas to move the second box.
I modified the updateAttr method to do so.
There is a catch however, when the updateAttr is called by konva itself (after the dragEnd event) it does so with translated coordinates. That is why I added a second argument to the updateAttr called doScale. When calling it yourself, set it true and it will translate the coordinates, if konva calls it, there is no second argument and it evaluates to false and does no translation.
The code is available at: https://codesandbox.io/s/6126wz0kkk

React Native Animated PanResponder reverse on second input

I got a component using the PanResponder in combination with Animated from React Native API's. The code of my component:
import React, { Component } from 'react';
import { Animated, PanResponder } from 'react-native';
import { SVG } from '../';
import { Icon, LockContainer, StatusCircle } from './styled';
class VehicleLock extends Component {
state = {
pan: new Animated.ValueXY({ x: 9, y: 16 }),
};
componentWillMount() {
this.animatedValueY = 0;
this.minYValue = 16;
this.maxYValue = 175;
this.state.pan.y.addListener((value) => {
this.animatedValueY = value.value;
});
this.panResponder = PanResponder.create({
onStartShouldSetPanResponderCapture: () => true,
onMoveShouldSetPanResponderCapture: () => true,
onPanResponderGrant: (evt, gestureState) => {
this.state.pan.setOffset({ x: 0, y: 0 });
// this.state.pan.setOffset(this.state.pan.__getValue());
this.state.pan.setValue({ x: 9, y: this.minYValue });
},
onPanResponderMove: (evt, gestureState) => {
// deltaY: amount of pixels moved vertically since the beginning of the gesture
let newY = gestureState.dy;
if (newY < this.minYValue) {
newY = this.minYValue;
} else if (newY > this.maxYValue) {
newY = this.maxYValue;
}
Animated.event([null, {
dy: this.state.pan.y,
}])(evt, {
dy: newY,
});
},
onPanResponderRelease: (evt, gestureState) => {
let newY = this.minYValue;
const releaseY = gestureState.dy;
if (releaseY > 83) {
newY = this.maxYValue;
}
Animated.spring(this.state.pan, {
toValue: {
x: 9,
y: newY,
},
}).start();
},
});
}
componentWillUnmount() {
this.state.pan.x.removeAllListeners();
this.state.pan.y.removeAllListeners();
}
render() {
const customStyles = {
...this.state.pan.getLayout(),
position: 'absolute',
zIndex: 10,
transform: [
{
rotate: this.state.pan.y.interpolate({
inputRange: [this.minYValue, this.maxYValue],
outputRange: ['0deg', '180deg'],
}),
},
],
};
return (
<LockContainer>
<SVG icon="lock_open" width={16} height={21} />
<Animated.View
{...this.panResponder.panHandlers}
style={customStyles}
>
<StatusCircle>
<Icon>
<SVG icon="arrow_down" width={23} height={23} />
</Icon>
</StatusCircle>
</Animated.View>
<SVG icon="lock_closed" width={16} height={21} />
</LockContainer>
);
}
}
export default VehicleLock;
As you can see in my code I animate the Y value with a boundary. It has to stay in a box between certain values. As soon users release the drag and it's over half of the max Y value it's animated to the max value.
This works without any problems, but on the second interaction I would like to reverse the action. So instead of going down, it has to go up. Unfortunately on release the Y value resets.
As you can see in the comment in my code I know the movement is based on the delta, so the moved Y value since the interaction started. This is explained in this great comment PanResponder snaps Animated.View back to original position on second drag.
Although I don't know how to fix it. Here you can see my current behaviour:
On the second input the element snap back to the top. Which is expected behavior. As #jevakallio states in his comment, you can reset the values in the offset in onPanResponderGrant. As I do that (commented out), the element resets the values, but on second interaction it will animate outside the container. So in that case 0 is the maxYValue and it animates 175 outside the container to the bottom.
How can I make a reversed animated outside back to the top? I don't seem to get this. Thanks in advance!
According to Panresponder docs the onPanResponderGrant should indicate the start of a gesture. So if you set the this.state.pan.setOffset and the this.state.pan.setValue at this point, it will reset with the start of every gesture. Try a console.log in the onPanResponderGrant and see what happens.
Also, a parent or wrapping component implementing the PanResponder could be interfering with your PanResponder. This could be another good starting point to troubleshoot.
From the PanResponder docs:
onPanResponderGrant: (evt, gestureState) => {
// The gesture has started. Show visual feedback so the user knows
// what is happening!
// gestureState.d{x,y} will be set to zero now
},

React Native PanGesture set pan position from tap

I'm currently working on an HSV-Colorpicker component for a react-native project I have. I have everything setup so that I have a draggable object on the HSV-canvas and a slider next to it to handle the hue change.
However, I now want to be able to set the position of the draggable object by clicking on the hsv-canvas. I got it working so that the draggable element moves to the right position, however, when I then try to drag the element with a pan-gesture again, the offset for the pan-gesture starts from the wrong position.
Here is the corresponding parts of code:
setPanFromTouch(event) {
const x = event.nativeEvent.locationX;
const y = event.nativeEvent.locationY;
const gesture = {
moveX: x,
moveY: y
};
this.state.pan.setOffset({ x: x, y: y });
this.state.pan.setValue({ x: 0, y: 0 });
this.updateColorFromGesture((gesture as any));
}
//inside the render method those are the 2 components that respond to
//touches and pans
//Parent container for the HSV-hue and the draggable element
<TouchableWithoutFeedback
style={{
width: this.props.colorBoxWidth,
height: this.props.height
}}
onPress={this.setPanFromTouch}
>
<Animated.View
{...this.panResponder.panHandlers}
style={[this.state.pan.getLayout(), {
height: this.draggableSize,
width: this.draggableSize,
borderRadius: this.draggableSize / 2,
backgroundColor: 'transparent',
borderColor: '#fff',
borderWidth: 1
}]}>
</Animated.View>
//pan-gesture setup inside the constructor method
(this as any).panResponder = PanResponder.create({
onMoveShouldSetPanResponder: () => true,
onMoveShouldSetPanResponderCapture: () => true,
onStartShouldSetPanResponder: () => true,
onPanResponderGrant: () => {
this.state.pan.setOffset({ x: this.state.panValues.x, y: this.state.panValues.y });
this.state.pan.setValue({ x: 0, y: 0 });
},
onPanResponderMove: Animated.event([null, {
dx: this.state.pan.x,
dy: this.state.pan.y
}]),
onPanResponderRelease: (e: GestureResponderEvent, gesture: PanResponderGestureState) => {
this.state.pan.flattenOffset();
this.updateColorFromGesture(gesture);
}
});
//component will mount code
componentWillMount() {
this.state.pan.addListener((c) => this.setState({
...this.state,
panValues: c
}));
}
I know that inside the onPanResponderGrant I am resetting the offset again, however this is necessary for the dragging to work correctly. I also tried to set the new values for this.state.panValues.x and this.state.panValues.y after updating the pan position from the touch event but setState does not change the behavior at all.
This is what the colorpicker looks like:
I would be really grateful if someone has an idea what I could do in this situation. Let me know if you need more code or anything else. Thanks in advance :)
I solved the issue by using a normal instance variable instead of the state for detecting if a pan position was set by a touch. Here is what I did:
//At the top of my react component
private setFromTouch = false;
//in the onPanResponderGrant method of the PanResponder.create inside
//the constructor
...
onPanResponderGrant: () => {
if (!this.setFromTouch) {
this.state.pan.setOffset({ x: this.state.panValues.x, y:
this.state.panValues.y });
} else {
this.setFromTouch = false;
}
this.state.pan.setValue({ x: 0, y: 0 });
}
...
//New setFromTouch() method
setPanFromTouch(event) {
const x = event.nativeEvent.locationX;
const y = event.nativeEvent.locationY;
const gesture = {
moveX: x,
moveY: y
};
this.state.pan.setOffset({ x: x, y: y });
this.state.pan.setValue({ x: 0, y: 0 });
this.setFromTouch = true;
this.updateColorFromGesture((gesture as any));
}

Categories