I'm trying to use Orbitcontrols in combination with react spring to animate my camera in React Three Fiber. This is my approach so far:
function Controls({ cameraData, duration }) {
const [orbit, setOrbit] = useState(true);
const [target, setTarget] = useState(cameraData.lookAt);
const { gl, camera } = useThree();
const springProps = useSpring({
config: { duration: duration ? duration : 1000, easing: easings.easeCubic },
from: {
x: camera.position.x - 0.1,
y: camera.position.y - 0.1,
z: camera.position.z - 0.1,
lookAtX: camera.lookAt.x - 0.1,
lookAtY: camera.lookAt.y - 0.1,
lookAtZ: camera.lookAt.z - 0.1,
},
to: {
x: cameraData.position[0],
y: cameraData.position[1],
z: cameraData.position[2],
lookAtX: cameraData.lookAt[0],
lookAtY: cameraData.lookAt[1],
lookAtZ: cameraData.lookAt[2],
},
onStart: (ya) => {
setOrbit(false);
},
onRest: (ya) => {
setOrbit(true);
setTarget(cameraData.lookAt)
},
});
useFrame((state, delta) => {
if (!orbit) {
camera.position.x = springProps.x.animation.values[0]._value;
camera.position.y = springProps.y.animation.values[0]._value;
camera.position.z = springProps.z.animation.values[0]._value;
camera.lookAt(
springProps.lookAtX.animation.values[0]._value,
springProps.lookAtY.animation.values[0]._value,
springProps.lookAtZ.animation.values[0]._value
);
}
});
return (
<OrbitControls
enabled={orbit}
target={target}
args={[camera, gl.domElement]}
/>
);
}
I disable OrbitControls when my Spring starts. Everything works.
But: When using OrbitControl my camera position changes. After that, when I start my Spring Animation the 'from' values are not updated.
For example I tween from x: 100 to x: 500. Then Rotate my Camera via OrbitControls to x: 700. When I start my next Spring Animation it animates starting from x: 500 instead of x: 700.
How can I update my from values.
Thanks in regard
It seems there are several conversations around not animating the camera such as the one found at https://github.com/pmndrs/react-three-fiber/discussions/505#discussioncomment-3120683 I post there an approach I did using someone else idea to animate the composition and make the illusion of a camera animation, that mindset shift can be the key for this use case.
Basically you have a wrapper that moves the entire composition to the focus of the screen to pretend the camera has moved.
The main part is the component below:
const AnimatedGroup = ({ children }: { children: React.ReactNode }) => {
const [focusPoint, setFocusPoint] = useState({
from: [0, -3, -100],
to: [0, 0, 0],
})
const { position } = useSpring({
position: focusPoint.to,
from: { position: focusPoint.from },
})
const newPosition = position as unknown as Vector3
const handleOnClick = (e: ThreeEvent<MouseEvent>) => {
const objectPosition = e.object.position.toArray()
const newFocusPoint = {
from: [focusPoint.to[0], focusPoint.to[1], focusPoint.to[2]],
to: [objectPosition[0] * -1, objectPosition[1], objectPosition[2] * -1],
}
if (!e.object.userData.pitstopVariant) return
setFocusPoint(newFocusPoint)
}
return (
<animated.group position={newPosition} onClick={handleOnClick}>
{children}
</animated.group>
)
}
Related
I found this nice implementation for a basic card game
It is based on this (however slightly different):
https://react-spring.io/hooks/use-springs#usesprings < doc
https://codesandbox.io/s/github/pmndrs/react-spring/tree/master/demo/src/sandboxes/cards-stack < doc example code
So here's the issue:
It has two arrays
cards which stores the index of cards (hardcoded)
cardData which stores the contents of cards (hardcoded)
What I'm trying to do is bind array 1 dynamically based on cardData.id
This does semi-works, it compiles and you can swipe the cards. However when all cards have cleared the board it wont reset as it would with the card-coded cards.
import React, { useState, useEffect } from "react";
import { useSprings } from "react-spring";
import { useGesture } from "react-with-gesture";
import Card from "./Card";
const cardData = [
{
id: 1,
question: "Islamic finance doesnt have different requirements",
pic:"https://images.unsplash.com/photo-1519817650390-64a93db51149?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=627&q=80",
text:
"Check out week 5 documents",
correct: false,
value: 15
},
{
id: 2,
question: "CEO requirements",
pic:
"https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fdoerlife.com%2Fwp-content%2Fuploads%2F2020%2F03%2FSP.png&f=1&nofb=1",
text: "They must decide high-level policy and strategy",
correct: true,
value: 6
},
{
id: 3,
question: "The sky is green",
text: "Make sure to look outside!",
correct: false,
value: 2
}
,
{
id: 4,
question: "This signifies British currency",
pic: "https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fwww.kindpng.com%2Fpicc%2Fm%2F112-1123896_black-pound-sterling-symbol-clipart-pound-sign-hd.png&f=1&nofb=1",
text:
"Maybe check in your wallet",
correct: true,
value: 17
}
];
const cards = [1, 2, 3, 4];
const to = i => ({
x: 0,
y: i * -12,
scale: 1,
rot: -10 + Math.random() * 20,
delay: i * 150
});
const from = i => ({ rot: 0, scale: 1.5, y: -1000 });
const trans = (r, s) =>
`perspective(1500px) rotateX(15deg) rotateY(${r /
10}deg) rotateZ(${r}deg) scale(${s})`;
function Swipe() {
console.log(cardData)
// const [cards, setCards] = useState([]);
// useEffect( () => {
// cardData.map(({id}) => {
// setCards(prev => [...prev, id]) // this doesnt reset cards after sorting all
// })
// },[])
const [gone] = useState(() => new Set());
const [props, set] = useSprings(cards.length, i => ({
...to(i),
from: from(i)
}));
const bind = useGesture(
(
{
args: [index],
down,
delta: [xDelta],
distance,
direction: [xDir],
velocity
}) => {
const trigger = velocity > 0.2;
const dir = xDir < 0 ? -1 : 1;
if (!down && trigger) gone.add(index);
set(i => {
if (index !== i) return;
const isGone = gone.has(index);
if (isGone){
console.log("index",i)
console.log("isgone",isGone) // grab by is gone
console.log("cards",cards)
// set()
}
const x = isGone ? (200 + window.innerWidth) * dir : down ? xDelta : 0;
const rot = xDelta / 100 + (isGone ? dir * 10 * velocity : 0);
const scale = down ? 1.1 : 1;
return {
x,
rot,
scale,
delay: undefined,
config: { friction: 50, tension: down ? 800 : isGone ? 200 : 500 }
};
});
if (!down && gone.size === cards.length)
setTimeout(() => gone.clear() || set(i => to(i)), 600);
// insert game end here
}
);
return (
<div className="swipe">
{props.map(({ x, y, rot, scale }, i) => (
<Card i={i} x={x} y={y} rot={rot} scale={scale} trans={trans} cardData={cardData} bind={bind} key={i}/>
))};
</div>);
}
export default Swipe;
Here is a live example to make it more legible: https://codesandbox.io/s/wandering-lake-iwzns5?file=/src/App.js
Any suggestions are welcome!
Turns out the impl I found which was a little different from the docs, for some reason had two separate arrays when it was unneeded. Changing instances ofcards into cardData seems to work fine.
I have a simple animation that works everywhere except on Samsung Galasy S8+ phones.
I tried looking everywhere and couldn't find how to solve this.
const { ref, inView } = useInView({ threshold: 0.2 });
const animation = useAnimation();
useEffect(() => {
if (inView) {
animation.start({
x: 0,
transition: {
type: "spring",
duration: 1,
bounce: 0.3,
},
});
}
if (!inView) {
animation.start({
x: -300,
});
}
}, [inView]);
I'm lost since it works everywhere else
I'm getting the error "Uncaught (in promise) DOMException: The media resource indicated by the src attribute or assigned media provider object was not suitable.", while trying to play a wav sound file using react-sound and "Uncaught (in promise) DOMException: The buffer passed to decodeAudioData contains an unknown content type." in a three.js scene using a positionalAudio. It works when I use mp3 files. I already tried to fix it by importing the wav file through file-loader and a webpack config. On the dev and build server everything works also using wav files but after deplyoing to a cloudflare server I'm getting this error when I try to use wav.
Now my second guess is, that I somehow have to set to code up using a promise but I am struggling as how to do it.
Code for react-sound
import Sound from 'react-sound'
import wavURL from '../../../public/sounds/ambientStereoSound.wav'
export default function AmbientSound(props) {
let state = ''
if(props.state){
state = Sound.status.PLAYING
}else{
state = Sound.status.PAUSED
}
return (
<Sound
url={wavURL}
autoLoad={true}
playStatus={state}
loop={true}
volume={100}
/>
)
}
Code for three.js (react-three-fiber)
import * as THREE from 'three'
import React, { Suspense, useMemo, useEffect, useState } from 'react'
import { useThree, useLoader } from '#react-three/fiber'
import { PositionalAudioHelper } from 'three/examples/jsm/helpers/PositionalAudioHelper.js'
import { useControls } from 'leva'
function PositionalAudio({ refs, url, volume, rolloffFactor, coneOuterGain, state }) {
const sound = refs
const { camera } = useThree()
const [listener] = useState(() => new THREE.AudioListener())
const buffer = useLoader(THREE.AudioLoader, url)
useEffect(() => {
sound.current.setBuffer(buffer)
sound.current.setRefDistance(1)
sound.current.setRolloffFactor(1)
sound.current.setDirectionalCone(180, 260, 0)
sound.current.setLoop(true)
sound.current.setVolume(volume)
sound.current.play()
const helper = new PositionalAudioHelper(sound.current)
sound.current.add(helper)
camera.add(listener)
return () => camera.remove(listener)
}, [])
useEffect(() => {
sound.current.setDirectionalCone(180, 260, coneOuterGain)
sound.current.setRolloffFactor(rolloffFactor)
sound.current.setVolume(volume)
}, [rolloffFactor, volume, coneOuterGain])
if(state) {
useEffect(() => {
sound.current.play()
}, [state])
}else{
useEffect(() => {
sound.current.pause()
}, [state])
}
return <positionalAudio ref={sound} args={[listener]} />
}
type SoundObject = {
id: number
x: number
y: number
z: number
position: number[]
filePath: string
name: string
rotation: number
}
type SoundObjectProps = {
soundObjects: SoundObject[]
}
export default function SoundObject(props: SoundObjectProps) {
const audioRefs = useMemo(
() =>
Array(props.soundObjects.length)
.fill(0)
.map(() => React.createRef()),
[]
)
const PositionalSoundObject = props.soundObjects.map((soundObject, index) => {
const name = soundObject.name
const { Rotation, Volume, Rolloff, X, Y, Z, ConeOuterGain } = useControls( name, {
Rotation: {
value: soundObject.rotation,
min: 0,
max: Math.PI * 2,
step: Math.PI * 0.25
},
Volume: {
value: 1,
min: 0,
max: 1,
step: 0.05
},
Rolloff: {
value: 1,
min: 0,
max: 1,
step: 0.05
},
ConeOuterGain: {
value: 0,
min: 0,
max: 1
},
X: {
value: soundObject.x,
},
Y: {
value: soundObject.y,
},
Z: {
value: soundObject.z,
}
})
return (
<mesh position={[X, Y, Z]} rotation={[0, Rotation, 0]}>
<sphereGeometry args={[0.1, 8, 8]} />
<meshStandardMaterial color='hotpink' wireframe />
<PositionalAudio
refs={audioRefs[index]}
volume={Volume}
rolloffFactor={Rolloff}
url={soundObject.filePath}
key={soundObject.id}
coneOuterGain={ConeOuterGain}
state={props.state}
/>
</mesh>
)
})
return <Suspense fallback={null}>{PositionalSoundObject}</Suspense>
}
Not exactly sure why, but hosting the wav files on the server and requesting it through https fixed the problem. My guess is that next.js (which runs node.js) can't handle loading wav files from the public folder out of the box and that it is not a problem related to react.
I'm running into a problem where I'd like the user to be able to slide their finger down the y-axis and the screen will go down with it, but unfortunately right now if the user slides across the x-axis (from left to right) the screen will interpolate that down the y-axis.
Here's a gif of the problem:
https://imgur.com/FKG5Cax
export const ExchangeNavigator = createStackNavigator(
{
addExchange: { screen: AddExchangeScreen },
addCoinbase: { screen: AddCoinbaseScreen },
addBinance: { screen: AddBinanceScreen },
},
{
navigationOptions: {
header: null,
gesturesEnabled: true,
},
transitionConfig: TransitionConfiguration,
},
)
export default ExchangeNavigator
export const SlideToTopTransition = (index, position) => {
const inputRange = [index - 1, index, index + 0.99, index + 1]
const translateY = position.interpolate({
inputRange,
outputRange: [DEVICE_RESOLUTION.height, 0, 0, 0],
})
return { transform: [{ translateY }] }
}
export const TransitionConfiguration = () => {
return {
transitionSpec: {
useNativeDriver: true
},
// Define scene interpolation, eq. custom transition
screenInterpolator: sceneProps => {
const { position, scene } = sceneProps
const { index, route } = scene
const params = route.params || {}
const transition = params.transition || 'default'
return {
slideToTop: SlideToTopTransition(index, position),
slideToRight: SlideToRightTransition(index, position),
slideToLeft: SlideToLeftTransition(index, position),
modalTransition: ModalTransition(index, position),
fadeTransition: FadeTransition(index, position),
default: SlideToTopTransition(index, position),
}[transition]
},
}
}
export default TransitionConfiguration
What's the best way to change the user gesture from left to right -> top to bottom & interpolate on the y-axis accordingly?
Thank you for your time!!
I am trying to animate a component in ReactJS native so that it orbits continuously around a point in a circular fashion. Basically, I need a timer that continuously increases that I can use to set the X, Y coordinates as something like {x: r * Math.sin(timer), y: r * Math.cos(timer)}
I found this guide which has some really helpful information about interpolation but I am still not having any success:
http://browniefed.com/react-native-animation-book/INTERPOLATION.html
How can I update the position of my component like this?
Bonus question: How can I make the animation continuous?
Since react-native interpolation doesn't support function callback, I made a helper function:
function withFunction(callback) {
let inputRange = [], outputRange = [], steps = 50;
/// input range 0-1
for (let i=0; i<=steps; ++i) {
let key = i/steps;
inputRange.push(key);
outputRange.push(callback(key));
}
return { inputRange, outputRange };
}
Apply animation to translateX and translateY:
transform: [
{
translateX:
this.rotate.interpolate(
withFunction( (value) => Math.cos(Math.PI*value*2) * radius )
)
},
{
translateY:
this.rotate.interpolate(
withFunction( (value) => Math.sin(Math.PI*value*2) * radius )
)
},
]
Visual result:
And to make animation continuous, I did something like this:
rotate = new Animated.Value(0);
componentDidMount() {
setInterval( () => {
let step = 0.05;
let final = this.rotate._value+step;
if (final >= 1) final = 0;
this.rotate.setValue(final);
}, 50);
}
I ended up figuring out a way to do this, although it is not ideal and I'd appreciate any other input. Here is my code:
class MyComponent extends Component {
constructor(props) {
super(props);
this.state = {
rotation: new Animated.Value(0),
pos: new Animated.ValueXY()
}
}
componentDidMount() {
this.animate(10000);
}
animate(speed) {
Animated.parallel([
Animated.timing(this.state.rotation, {
toValue: 360,
duration: speed,
easing: Easing.linear
}),
Animated.timing(this.state.pos, {
toValue: {x: 360, y: 360},
duration: speed,
easing: Easing.linear,
extrapolate: 'clamp'
})
]).start(() => {
// Cycle the animation
this.state.rotation.setValue(0);
this.state.pos.setValue({x: 0, y: 0});
this.animate(speed);
});
}
render() {
let range = new Array(360);
for (let i = 0; i < 360; i++) {
range[i] = i;
}
var radius = 51,
offset = 40;
return (
<View style={this.props.style}>
<Animated.View style={{position: 'absolute', transform: [
// Map the inputs [0..360) to x, y values
{translateX: this.state.pos.x.interpolate({
inputRange: range,
outputRange: range.map(i => offset + radius * Math.cos(i * Math.PI / 180))
})},
{translateY: this.state.pos.y.interpolate({
inputRange: range,
outputRange: range.map(i => offset - radius * Math.sin(i * Math.PI / 180))
})},
{rotate: this.state.rotation.interpolate({
inputRange: [0, 360],
outputRange: ['360deg', '0deg']
})}
]}}>
<Image source={require('image.png')}/>
</Animated.View>
</View>
)
}
}
Basically, I use the interpolate function to map the the Animated.Value pos to the result of my function for all 360 degrees. I think this could be improved by having a sort of map function instead of an interpolate function. Does anyone know if there is one?