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?
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 an Animated.View in React Native which I am animating the width of.
const pollRef = useRef(new Animated.Value(0)).current;
const resultWidth = ((votes/totalVotes)*100).toFixed(0) // GET A PERCENTAGE VALUE
const AnimateResult = () => {
Animated.timing(pollRef, {
toValue: resultWidth,
duration: 3000,
useNativeDriver: false
}).start();
};
useEffect(() => {
AnimateResult()
},[])
<Animated.View
style={{
position: 'absolute',
top: 0,
left: 0,
height: '100%',
width: pollRef,
backgroundColor: '#155A8B',
}}
/>
The value of resultWidth (and therefore pollRef) is 100. The width of the element is animating fine, apart from it's correctly treating the 100 as an absolute value and animating the width to 100 pixels, whereas I would like to express it as a percentage. Whatever syntax I use in the style object I can't seem to do it.
I have tried things like:
style={{
...
width: "'"+pollRef+"%'"
...
}}
and
style={{
...
width: `${pollRef}%`,
...
}}
But neither worked. I have also noticed that there seems to be whitespace around the variable - if I console log:
console.log('pollRef:',pollRef,'X')
the output is:
pollRef: 0 X
which may make adding a percentage sign to the end a little more difficult.
Any help very much appreciated.
I think https://stackoverflow.com/a/48741068/12637199 answers your question.
You need to use interpolate to transform numbers to strings.
You can just have:
const animation = new Animated.Value(0);
const inputRange = [0, 100];
const outputRange = ["0%", "100%"]
const animatedWidth = animation.interpolate({inputRange, outputRange});
And then use it like this:
width: animatedWidth,
const handlePress=()=>{
Animated.parallel([
Animated.timing(current, {
toValue:1,
duration:400,
useNativeDriver:false,
}),
Animated.timing(progress, {
toValue:index+1,
duration:400,
useNativeDriver:false
})
]).start(()=>{
setIndex(index+1);
current.setValue(0);
if(index+1>=5){
setTimeout(()=>{
setRunning(0)
}, 500)
}
})
}
const {width} = Dimensions.get("window");
const currentAnim=current.interpolate({
inputRange:[0, 1],
outputRange:[0, -width]
});
const nextAnim=current.interpolate({
inputRange:[0, 1],
outputRange:[width, 0]
});
const progressAnim=progress.interpolate({
inputRange:[0, questions.length],
outputRange:["20%", "100%"],
});
const barWidth={
width:progressAnim
}
const translate={
transform:[
{translateX:currentAnim}
]
}
const translateNext={
transform:[
{translateX:nextAnim}
]
}
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>
)
}
We have an app with many timeseries charts (custom made with d3), where each chart displays values from different hardware from a tool. Each chart can be displayed in either linear, logarithmic or normalized scale (domain = [0, 1]). We were recently asked to provide a new scale option, we called it config scale, where the user would provide a range (min/max in a configuration file) and all charts would then have the same y axis range.
I thought that since we needed to transform our data twice, I would create 2 d3 scales, like the following:
yScale => from original data domain => to configured range
configScale => from configured range => to pixels
I tried to create a repro in https://jsfiddle.net/XeniaSiskaki/skf42jot/159/ (only the new config scale is displayed, with random data points). The big problem I have (assuming that my solution to the original request was correct) is that zooming on y axis is somewhat broken. When either dragging the body of the chart or using the mouse wheel to zoom in/out, zooming is very fast and it appears that the data and the y axis aren't updated properly. Reseting the y viewport also doesn't work properly, don't know if it's related to the zoom issue
const zoom = ['x', 'y'];
const range = [-10, 10];
const sampleDate = new Date('2020-01-01T09:00:00.000Z').getTime();
const innerWidth = 390;
const innerHeight = 278;
const chartData = {
a: [
{
v: 0,
t: sampleDate - 10000
},
{
v: -1,
t: sampleDate - 9000
},
{
v: 0.6,
t: sampleDate - 8000
},
{
v: 1,
t: sampleDate - 7000
},
{
v: -2,
t: sampleDate - 6000
},
{
v: 10,
t: sampleDate - 5000
},
{
v: 0.8,
t: sampleDate - 4000
},
{
v: 5.3,
t: sampleDate - 3000
},
{
v: 0,
t: sampleDate - 2000
},
{
v: 1.2,
t: sampleDate - 1000
},
{
v: 3,
t: sampleDate
},
{
v: 4.5,
t: sampleDate + 1000
},
{
v: 10,
t: sampleDate + 2000
}
],
b: [
{
v: 0.8,
t: sampleDate - 10000
},
{
v: 11,
t: sampleDate - 9000
},
{
v: -7.9,
t: sampleDate - 8000
},
{
v: 22.1,
t: sampleDate - 7000
},
{
v: -0.3,
t: sampleDate - 6000
},
{
v: 10,
t: sampleDate - 5000
},
{
v: 1.3,
t: sampleDate - 4000
},
{
v: 4.3,
t: sampleDate - 3000
},
{
v: 0,
t: sampleDate - 2000
},
{
v: -14.4,
t: sampleDate - 1000
},
{
v: 3,
t: sampleDate
},
{
v: -0.1,
t: sampleDate + 1000
},
{
v: 10,
t: sampleDate + 2000
}
]
};
const allPoints = chartData.a.concat(chartData.b);
class Chart extends React.Component {
constructor(props) {
super(props);
const [x0, x1] = d3.extent(allPoints.map(({ t }) => t));
this.state = {
zoom: 1,
viewport: {
x0,
x1
}
};
this.zoomPane = React.createRef();
this.svg = React.createRef();
this.line = d3.svg
.line()
.interpolate('linear')
.x(({ t }) => this.xScale(t))
.y(({ v }) => this.configScale(this.yScale(v)));
}
render() {
return (
<div>
<div>
<button onClick={this.handleZoomChange.bind(this)}>
Set zoom to {zoom[this.getNextZoomIndex()]}
</button>
<button onClick={this.resetViewport.bind(this)}> Reset viewport </button>
</div>
<div
style={{
height: 300
}}>
<svg width={450} height={300} ref={this.svg}>
<rect
ref={this.zoomPane}
style={{
cursor: 'move'
}}
width={innerWidth}
height={innerHeight}
fill='transparent'
transform='translate(55,0)'>
</rect>
<defs>
<clipPath id={'clipId'}>
<rect x={5} y={5} width={innerWidth - 5} height={innerHeight - 5} />
</clipPath>
</defs>
<g className='x axis' transform={'translate(55,281)'} />
<g className='y axis' transform={'translate(55,0)'} />
<g
clipPath={'url(#clipId)'}
width={innerWidth}
height={innerHeight}
transform={'translate(55,0)'}>
{this.renderLines()}
</g>
</svg>
</div>
</div>
);
}
UNSAFE_componentWillMount() {
this.setupScales();
this.updateScaleDomains();
}
componentDidMount() {
this.xAxis = d3.svg
.axis()
.orient('bottom')
.ticks(450 / 150)
.tickFormat(date => new Date(date).toISOString());
this.yAxis = d3.svg.axis().orient('left');
this.zoom = d3.behavior.zoom().on('zoom', this.handleZoom.bind(this));
this.updateAxes();
this.updateZoom();
d3.select(this.zoomPane.current).call(this.zoom);
}
UNSAFE_componentWillUpdate(nextProps, nextState) {
const { viewport, zoom } = this.state;
if (!_.isEqual(nextState.viewport, viewport)) {
this.updateScaleDomains(nextState);
}
}
componentDidUpdate(prevProps, prevState) {
if (!_.isEqual(prevState.viewport, this.state.viewport)) {
this.updateAxes();
if (prevState.viewport.y0 !== this.state.viewport.y0) {
this.updateZoom();
}
} else if (prevState.zoom !== this.state.zoom) {
this.updateZoom();
}
}
setupScales() {
this.xScale = d3.time.scale().range([10, 380]);
this.yScale = d3.scale.linear().range(range);
this.configScale = d3.scale.linear().domain(range).range([263, 15]);
}
updateScaleDomains(state = this.state) {
const { x0, x1, y0, y1 } = state.viewport;
if (x0 && y0) {
this.xScale.domain([x0, x1]);
this.xScale.range([10, 380]);
this.yScale.domain([y0, y1]);
this.yScale.range(range);
return;
}
if (x0) {
this.xScale.domain([x0, x1]);
} else {
this.xScale.domain(d3.extent(allPoints.map(({ t }) => new Date(t))));
this.xScale.range([10, 380]);
}
if (y0) {
this.yScale.domain([y0, y1]);
} else {
this.yScale.domain(d3.extent(allPoints.map(({ v }) => v)));
this.yScale.range(range);
}
}
updateZoom() {
const z = zoom[this.state.zoom];
if (z === 'x') {
this.zoom.x(this.xScale);
const x0 = this.xScale.invert(10);
const x1 = this.xScale.invert(innerWidth - 10);
const maxScale = (x1.getTime() - x0.getTime()) / 3600;
this.zoom.scaleExtent([0, maxScale]);
} else {
this.zoom.x(d3.scale.identity());
this.zoom.scaleExtent([0, Infinity]);
}
if (z === 'y') {
this.zoom.y(this.configScale);
} else {
this.zoom.y(d3.scale.identity());
}
}
resetViewport() {
this.setState(({ viewport }) => ({
viewport: _.omit(viewport, 'y0', 'y1')
}));
}
updateAxes() {
const svg = d3.select(this.svg.current);
this.xAxis.scale(this.xScale).tickSize(-innerHeight);
this.yAxis.scale(this.configScale).tickSize(-innerWidth);
svg.select('.x.axis').call(this.xAxis);
svg.select('.y.axis').call(this.yAxis);
}
getViewport() {
return {
x0: this.xScale.invert(10),
x1: this.xScale.invert(380),
y0: this.yScale.invert(this.configScale.invert(263)),
y1: this.yScale.invert(this.configScale.invert(15))
};
}
handleZoom() {
d3.select(this.zoomPane.current).call(this.zoom);
this.setState({
viewport: this.getViewport()
});
}
handleZoomChange() {
this.setState({
zoom: this.getNextZoomIndex()
});
this.updateZoom();
}
getNextZoomIndex() {
return this.state.zoom + 1 >= zoom.length ? 0 : this.state.zoom + 1;
}
renderLines() {
return Object.keys(chartData).map(key => {
const d = this.line(chartData[key]);
if (!d) {
return null;
}
return (
<path
d={d}
strokeLinecap='round'
strokeLinejoin='bevel'
fill='none'
strokeWidth={1}
stroke='#000000'
key={key} />
);
});
}
}
ReactDOM.render(<Chart />, document.querySelector('#app'));
svg {
font: 10px sans-serif;
}
button {
width: 120px;
margin: 0 15px;
}
.axis path,
.axis line {
fill: none;
stroke: #000000;
shape-rendering: crispEdges;
}
.axis path {
display: none;
}
.tick line {
stroke: rgba(220, 220, 220, 0.6);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<div id="app"></div>
PS: Using d3 v3
I think there's a few problems here.
First, handleZoom repeatedly registers this.zoom on every zoom event. This is not necessary; the caller only needs to be registered once. This might be part of the reason that after zooming several times, the behaviour gets more extreme (as each zoom event is effectively being performed many times).
Second, updateZoom uses configScale but not yScale. The call to this.zoom.y binds d3's zoom to automatically update the domain of whatever is provided; we don't want to change the domain of the configScale but we do want to change the yScale domain.
Finally, it might be simpler to remove the double scaling and instead do an equivalent calculation on the data before giving it to the graph. This would simplify all your logic quite significantly.
Additionally, I found the approach of storing the zoom as the viewport bounds to be a little strange; consider storing the d3 zoom transform instead (and applying the transform when drawing the lines and axes).
I'm trying to make a temperature gauge that looks like a mercury thermometer. To do this I'm using Formidable Lab's victory native library.
Where I'm running into trouble is in getting the VictoryAxis component to spread its VictoryLabel components evenly along the y-axis.
My loop in componentDidMount is generating an array of numbers, in increments of 0.2, between the range of this.state.cLower and this.state.cUpper that are supposed to be set as the tick labels on the y-axis.
export default class Thermometer extends React.Component {
constructor (props) {
super(props)
this.state = {
temp: [0],
cUpper: 29,
cLower: 24,
tickValues: []
}
DB.ref('tempLog').limitToLast(1).on('value', snap => {
this.setState({ temp: [{ x: 0, y: [snap.val()[Object.keys(snap.val())[0]].temp] }], y0: this.state.cLower })
})
}
componentDidMount () {
let temps = new Set()
for (let i = parseFloat(this.state.cLower); i <= parseFloat(this.state.cUpper); i += 0.2) {
temps.add(Math.round(i * 10) / 10)
}
this.setState({ tickValues: Array.from(temps) })
}
render () {
return (
<VictoryChart
height={300}
width={65}>
<VictoryAxis
style={{ ticks: { size: 12 } }}
orientation={'left'}
tickValues={this.state.tickValues}
dependentAxis />
<VictoryBar
style={{ data: { fill: 'rgb(255, 0, 0)' } }}
data={this.state.temp} />
</VictoryChart>
)
}
}
I've verified that the values rendered are correct but as you can see here all the labels get rendered nearly on top of each other.
Because this.state.cLower and this.state.cUpper set the bounds for the lower and upper data, respectively, that is rendered I had to set VictoryBar's y0 prop to the value of this.state.cLower
<VictoryBar
y0={() => this.state.cLower}
style={{ data: { fill: 'rgb(255, 0, 0)' } }}
data={this.state.temp} />
VictoryAxis then picks up on this, presumably through VictoryChart, and the VictoryAxis' VictoryLabels get distributed, and the rendered VictoryBar data aligned as expected