d3: zoom on double y scale broken - reactjs

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).

Related

React - Map single field from array to array issue

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.

Use React Spring With Orbit Controls in React Three Fiber

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>
)
}

Wav sound is not playing in react

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.

How to get VictoryAxis tickValues evenly distributed on the axis in victory-native?

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

ReactJS: AnimationValue.interpolate for sin and cos

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?

Resources