Songwriting assistant prototype

Aug 22, 2016

When writing music, I sometimes struggle with coming up with good chord progressions that lay the foundation for the song. It turns out that popular music follows predictable patterns which can be calculated and therefore potentially automated.

After I recently experimented with swipe gestures on the web, I was wondering if there would be a perceivable performance difference when you implement something similar with React Native.

My idea with this app is that the user can swipe through different moods to get matching chord progressions.

The Animated API works really well for this use case: You can create a single animated value that subscribes to the horizontal scroll position.

const scrollValue = new Animated.Value(initialIndex);

In the render function, this can be interpolated into corresponding values for every single panel.

Children.map(children, (child, i) => {
  // Don't render invisible panels
  const isVisible = Math.abs(index - i) <= 1;
  if (!isVisible) {
    return <View style={styles.panelPlaceholder} />;
  }

  const transitionValue = scrollValue.interpolate({
    inputRange: [i - 1, i, i + 1],
    outputRange: [-1, 0, 1]
  });

  return React.cloneElement(child, {transitionValue, isVisible});
});

A panel can take this value and interpolate it further to animate the opacity and position of the background, creating the fading parallax effect.

const {mood, imageParallaxFactor, transitionValue} = props;

const imageOffset = viewWidth * imageParallaxFactor;

const imageOpacity = transitionValue.interpolate({
  inputRange: [-1, 0, 1],
  outputRange: [0, 1, 0]
});

const imageTranslateX = transitionValue.interpolate({
  inputRange: [-1, 0, 1],
  outputRange: [-imageOffset, 0, imageOffset]
});

const style = [
  styles.backgroundImage,
  {
    left: -imageOffset,
    width: viewWidth + imageOffset * 2,
    opacity: imageOpacity,
    transform: [{translateX: imageTranslateX}]
  }
];

return <Animated.Image style={style} source={mood.image} />;

This ensures that all animatable elements always operate in sync.

When the user releases the touch, scrollValue can be set to an integer, resulting in a single panel to be visible. For a natural feel, the current velocity of the panel needs to be incorporated via spring dynamics.

// Decide based on the current drag position and
// velocity which panel should be visible.
const pageNumber = ...;

Animated.spring(scrollValue, {
  toValue: pageNumber,
  velocity: -gestureState.vx,
  friction: springFriction,
  tension: springTension
}).start();

Read next

Animating with React Hooks and RxJS

Are these technologies are a good fit for gesture-based animations?

Oct 29, 2018