React-three-fiber with react-spring animations - reactjs

sorry if this seems like an obvious answer, but I am trying to create an animation similar to this. I am trying to create one similar with react-three-fiber but I can't use the ones shown in the docs since they manipulate the style transform property which isn't possible with three.js. I have tried to manipulate it through the position attribute and props but it comes with an error as shown here and I don't really know where to start to fix this. Here is my code:
const props = useSpring({
to: async next => {
await next({ position: [100, 100, 100] });
await next({ position: [50, 50, 50] });
},
from: { position: [100, 100, 100] },
config: { duration: 3500 },
reset: true
});
return (
<Canvas>
<a.group {...props}>
<Box position={[-1.2, 0, 0]} />
<Box position={[1.2, 0, 0]} />
</a.group>
</Canvas>
)
I have used react-spring successfully with hovering and scale but it just doesn't work when using position.

i think something like that is best done with trigonometry
const ref = useRef()
useFrame(() => {
ref.current.position.y = Math.sin(state.clock.getElapsedTime()) * 10
})
return <group ref={ref}> ...
Math.sin will yield a value between -1 to 1 and alternates smoothly between the two. multiply your factor (the distance) and you have it. here's an example that dollies the camera like that: https://codesandbox.io/s/r3f-lod-e9vpx
otherwise it's react-spring/three, not react-spring: https://codesandbox.io/s/clever-bartik-tkql8

Related

Simple testing example of canvas element using Jest

I am trying to create a jest test for the example found in this sandbox:
https://codesandbox.io/s/r3f-basic-demo-forked-297k3?file=/src/App.js
Basically I would like to test the onClick functionality on each box. I have already come across with the fact that jest tests don't run in a real browser. Jest usesĀ jsdomĀ for mocking the necessary parts of the DOM. Consequently I may need canvas support for jsdom, yet I am not quite sure exactly what to do.
App.js
import React, { useRef, useState } from 'react'
import { Canvas, useFrame } from 'react-three-fiber'
function Box(props) {
// This reference will give us direct access to the mesh
const mesh = useRef()
// Set up state for the hovered and active state
const [hovered, setHover] = useState(false)
const [active, setActive] = useState(false)
// Rotate mesh every frame, this is outside of React without overhead
useFrame(() => {
mesh.current.rotation.x = mesh.current.rotation.y += 0.01
})
return (
<mesh
{...props}
ref={mesh}
scale={active ? [1.5, 1.5, 1.5] : [1, 1, 1]}
onClick={(e) => setActive(!active)}
onPointerOver={(e) => setHover(true)}
onPointerOut={(e) => setHover(false)}>
<boxBufferGeometry args={[1, 1, 1]} />
<meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
</mesh>
)
}
export const App = () => {
return (
<>
<Canvas>
<ambientLight intensity={0.5} />
<spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} />
<pointLight position={[-10, -10, -10]} />
<Box position={[-1.2, 0, 0]} />
<Box position={[1.2, 0, 0]} />
</Canvas>
</>
)
}
It's not much of an answer but I found this article useful: https://www.valentinog.com/blog/canvas/.
The suggestion is for proper testing of a canvas element one should not be using Jest (or any unit test) but instead, use some sort of e2e testing so that it can actually render the canvas.
I was able to get some smaller tests working in jest with the following:
Firstly render your page normally using jest. You can either give your canvas a test-id or (as in my case) put a div around your canvas and give the div a test-id.
I ended up with the following:
let canvasContainer = getByTestId("canvas-container");
let canvas = canvasContainer.getElementsByTagName("canvas")[0];
let canvasContext = canvas.getContext("2d");
In either case, your aim is to get the canvas' elements context.
From there you can inspect it with canvasContext.__getDrawCalls()
This will give you some information on all of the "drawCalls" that have been done so far (such as drawing lines or rectangles or what have you).
It takes a little of a bit of inspection into the __getDrawCalls but you should find that every call so far is included.
I wrote some funcitons to help me get the exact details I wanted:
const getLineInfo = (lineInfo) => {
if (lineInfo.type !== "stroke") {
return "not a line";
}
let lineInfoDetail = lineInfo.props.path[3];
return {
lineStart: {
x: lineInfoDetail.transform[4],
y: lineInfoDetail.transform[5],
},
lineEnd: {
x: lineInfoDetail.transform[4] + lineInfoDetail.props.x,
y: lineInfoDetail.transform[5] + lineInfoDetail.props.y,
},
};
};
const getRectInfo = (rectInfo) => {
if (rectInfo.type !== "fill") {
return "not a rect";
}
let rectInfoDetail = rectInfo.props.path[1];
return {
rectStart: {
x: rectInfoDetail.transform[4],
y: rectInfoDetail.transform[5],
},
rectEnd: {
x: rectInfoDetail.transform[4] + rectInfoDetail.props.width,
y: rectInfoDetail.transform[5] + rectInfoDetail.props.height,
},
};
};
So for example if I know that the second draw is a line I can do the following:
expect(getLineInfo(canvasContext.__getDrawCalls()[1])).toStrictEqual({
lineStart: { x: 150, y: 250 },
lineEnd: { x: 150, y: 350 },
});
This is testing that that element is a line with a start point of { x: 150, y: 250 } and an endpoint of { x: 150, y: 350 }
The issue with this though is that you can't trigger click and drag events of the canvas elements within jest and therefore using jest to test canvas seems to be very limited.
Edit:
It looks like https://yonatankra.com/how-to-test-html5-canvas-with-jest/ gives another explanation.

How to apply animation in react-native?

I want to add animation to this search bar.
when the user touches the search bar it size decreases and again increases and gets to its default size(like animation in popups)
This is my code
<View style={{flexDirection:'row',alignSelf:'center'}}>
<TextInput
onChangeText={(text) => setSearch(text)}
onFocus={()=>{
setSize('92%');
setInterval(()=>{setSize('95%')},1000)
}}
placeholder="Search"
style={{...styles.searchbox,width:size}}
></TextInput>
</View>
I am currently trying to change width..
Firstly, I suggest you to take a look at RN animated documentation, maybe it will help you to understand better how the animations work.
Also, it depends on what you're having there: a class component or a function component.
If you're using a function component, you could do it like this:
Creating a custom hook, called, let's say useAnimation(), which would look something like this
export const useAnimation = ({ doAnimation, duration, initialValue, finalValue }) => {
const [animation, setAnimation] = useState(new Animated.Value(initialValue))
useEffect(() => {
Animated.spring(animation, {
toValue: doAnimation ? initialValue : finalValue,
duration,
bounciness: 8,
useNativeDriver: false
}).start();
}, [doAnimation]);
return animation
}
As it is said in the documentation, you could animate only Animated components, and for example if you want to have an animated View, the tag will be <Animated.View> {...} </Animated.View, but for the <TextInput> we have to create the animated component:
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput)
and combining the first 2 steps
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput)
const [collapsed, setCollapsed] = useState(true)
const animation = useAnimation({ doAnimation: collapsed, duration: 300, initialValue: 20, finalValue: 200 });
const onFocusText = () => setWidth(false)
return (
<AnimatedTextInput
onFocus={onFocusText}
placeholder={"Search something"}
style={{width: animation, height: 50, borderColor: 'gray', borderWidth: 1, borderRadius: 4, padding: 10}}
/>
)
Also, if you're having a class component, you could have a method which will start the animation (but don't forget about the step 2 which is essential)
private size: Animated.Value = new Animated.Value(COLLAPSED_VALUE)
get resizeInputWidth(): Animated.CompositeAnimation {
return Animated.timing(this.size, {
toValue: EXPANDED_VALUE,
duration: 500,
})
}
startAnimation = () => this.resizeInputWidth.start()
<AnimatedTextInput
onFocus={this.startAnimation}
style={{ width: this.size }}
/>

How to make a Header that Animates from Transparent to Opaque Color on Scrolling down in React-Native React Navigation 5?

I'm trying to make header that will animate from transparent to solid opaque color upon scrolling down using in React-Native React Navigation 5.
Starts to transition to opaque when scrolling halfway
Becomes fully opaque when reach the maximum offset
You can do this by setting the header style opacity to an animated value.
First define your animated value, we'll interpolate the yOffset to get the opacity desired.
const yOffset = useRef(new Animated.Value(0)).current;
const headerOpacity = yOffset.interpolate({
inputRange: [0, 200],
outputRange: [0, 1],
extrapolate: "clamp",
});
then you want to attach an animated.event listener to an animated scroll view
<Animated.ScrollView
onScroll={Animated.event(
[
{
nativeEvent: {
contentOffset: {
y: yOffset,
},
},
},
],
{ useNativeDriver: true }
)}
scrollEventThrottle={16}
>
Your content should be inside the scroll view
In your screen add a on mount or use effect where you set the animatedValue as the header opacity
useEffect(() => {
navigation.setOptions({
headerStyle: {
opacity: headerOpacity,
},
headerBackground: () => (
<Animated.View
style={{
backgroundColor: "white",
...StyleSheet.absoluteFillObject,
opacity: headerOpacity,
}}
/>
),
headerTransparent: true,
});
}, [headerOpacity, navigation]);
I've used header transparent and header background so that the background component changes also.
Here is an example:
https://snack.expo.io/#dannyhw/react-navigation-animated-header
const handleScroll = () => {
YourElementRef.current.style.backgroundColor = `rgba(245, 245, 245, ${window.scrollY > 300 ? 1 : '0.' + (window.scrollY * 3)})`;
}
window.addEventListener('scroll', handleScroll, true);
You need to add scroll listener and call function that animates it.
The scroll element is represented by a ref stuff. e.g.
const YourElementRef = React.useRef(null);
<SomeElement ref={YourElementRef}...

How to center two markers with react native map view

I got two markers at two different coordinates.
How do people normally center two markers so that you can see two marker at the same time on map, regardless how far away they are.
<MapView
ref={ (map)=> this.map = map}
initialRegion={this.state.init}
style={css.map}>
{ this.state.marker1 !==0 && <MapView.Marker
coordinate={{
'latitude':this.state.marker1.lat,
'longitude':this.state.marker1.lng}}
/>}
{ this.state.marker2 !==0 && <MapView.Marker
coordinate={{
'latitude':this.state.marker2.lat,
'longitude':this.state.marker2.lng}}
/>}
</MapView>
You can use the fitToSuppliedMarkers or fitToCoordinates method of the MapView.
https://github.com/react-native-community/react-native-maps/blob/master/docs/mapview.md
For anyone like me who comes across this, what you need is fitToSuppliedMarkers from MapView and identifiers from Marker. Here is an example: https://github.com/react-native-maps/react-native-maps/blob/master/example/src/examples/FitToSuppliedMarkers.tsx
You need to use fitToSuppliedMarkers or fitToCoordinates method of the MapView in ComponentDidMount or useEffect and make sure you put it in a setTimeout or it will cause performance problems.
useEffect(() => {
if (!destination) return;
setTimeout(() => {
mapRef?.current?.fitToSuppliedMarkers(["destination", "origin"], {
edgePadding: { top: 70, right: 70, bottom: 70, left: 70 },
});
}, 500);
// console.log("2", destination);
}, [origin, destination]);
Note edgePadding is Google Maps only
https://github.com/react-native-maps/react-native-maps/blob/master/docs/mapview.md

How to scale the size of a button when the user holds down in Animated React / React Native?

I'm trying to create some code that increases the size of a button when the user holds it down then reverts back to the initial size when released.
I'm relatively new to React/RN and have searched tons of websites to find the result, but can't seem to find anything.
I can't tell whether I should be using PanResponder here or not. I also tried using Animated.timing, but the timing is hard-coded & not bound to the length of time that the user holds down the button. I tried Animated.spring, but again that's not bound to length of time that the user holds the button down.
I'll post a quick gif that replicates what I'm trying to go for.
https://imgur.com/a56pSQl
Here's what I have so far:
this.scaleAnimation = new Animated.value(3)
handlePress = () => {
Animated.spring(this.scaleAnimation, {
toValue: 4,
friction: 2,
tension: 160
}).start()
}
render() {
const pauseStyle = {
transform: [
{ scale: this.scaleAnimation }
]
}
return (
<TouchableWithoutFeedback onPress={this.handlePress}>
<Animated.View style={[ pauseStyle ]}>
<Ionicons name="md-pause" />
</Animated.View>
</TouchableWithoutFeedback>
)
}
Any takes are greatly appreciated :D
Please find the detailed answer with code snippets.
Add this to constructor of component
this.handlePressIn = this.handlePressIn.bind(this);
this.handlePressOut = this.handlePressOut.bind(this);
this.animatedValue = new Animated.Value(1);
This method is for scaling button with animation when button gets pressed
handlePressIn() {
Animated.spring(this.animatedValue, {
toValue: 3,
friction: 3,
tension: 40
}).start();
}
This method is for resetting button to its initial scale with animation when touch gets released
handlePressOut() {
Animated.spring(this.animatedValue, {
toValue: 1,
friction: 3,
tension: 40
}).start();
}
Render method
render() {
const animatedStyle = {
transform: [{ scale: this.animatedValue }]
}
return (
<TouchableWithoutFeedback
onPressIn={this.handlePressIn}
onPressOut={this.handlePressOut}
>
<Animated.View style={animatedStyle}>
<Text style={styles.text}>Press Me</Text>
</Animated.View>
</TouchableWithoutFeedback>
);
}
You can use different types of animations. Please find the link for reference:
https://facebook.github.io/react-native/docs/animated#configuring-animations

Resources