acdlite/recompose

View on GitHub
types/flow-example/src/ItemsAnimator.js

Summary

Maintainability
A
1 hr
Test Coverage
/* @flow */

import React from 'react'
import { css } from 'glamor'
import { compose, defaultProps, withHandlers, withProps } from 'recompose'
import Item from './Item'
import { TransitionMotion, spring } from 'react-motion'
// types
import type { HOC } from 'recompose'
import type { MousePosition } from './MouseDetector'

type ItemT = {
  id: number,
  title: string,
  color?: string,
}

// Props type of enhanced component
// It's the only props type we need to declare using supporting recompose enhancers
type ItemsAnimatorProps = {
  items: Array<ItemT>,
  config?: {
    size: number,
    hoverSize: number,
    hoverRotation: number,
    spacing: number,
  },
  mousePos: MousePosition,
}

// set existential * type for base component,
// flow is smart enough to infer base component and enhancers props types
const itemsAnimator = ({ styles, animStyles }) =>
  <TransitionMotion styles={animStyles}>
    {(
      interpolated: Array<{
        key: string,
        style: { x: number, y: number, size: number, borderK: number },
        data: { ...$Exact<ItemT>, hovered: boolean },
      }>
    ) =>
      <div {...styles.component}>
        {interpolated.map(({ key, data, style }) =>
          <Item
            key={key}
            color={data.color}
            title={data.title}
            size={style.size}
            hovered={data.hovered}
            x={style.x}
            y={style.y}
            borderK={style.borderK}
          />
        )}
      </div>}
  </TransitionMotion>

const enhanceItemsAnimator: HOC<*, ItemsAnimatorProps> = compose(
  /**
   * Defaults
   */
  defaultProps({
    styles: {
      component: css({
        position: 'absolute',
      }),
    },
    config: {
      size: 110,
      hoverSize: 150,
      hoverRotation: 2 * Math.PI,
      spacing: -10,
    },
    springConfig: {
      stiffness: 170,
      damping: 4,
      precision: 0.001,
    },
  }),
  /**
   * Function to calculate items positions size and hover
   * based on mouse position and previously hovered item
   */
  withHandlers(() => {
    let hoveredItemId_ = -1

    return {
      getItemsViewProps: ({ mousePos, items, config }) => () => {
        if (items.length === 0) return []

        const itemMaxWidth = config.size * Math.sqrt(2)
        const cIdx = (items.length - 1) / 2
        const itemsD = items
          .map((item, index) => ({
            ...item,
            size: hoveredItemId_ === item.id ? config.hoverSize : config.size,
            x: (index - cIdx) * (itemMaxWidth + config.spacing),
            y: 0,
          }))
          .map(item => ({
            ...item,
            x:
              hoveredItemId_ === item.id
                ? item.x + (mousePos.x - item.x) / 3
                : item.x,
            y:
              hoveredItemId_ === item.id
                ? item.y + (mousePos.y - item.y) / 3
                : item.y,
          }))
          .map(item => ({
            ...item,
            distance:
              Math.sqrt(
                Math.pow(mousePos.x - item.x, 2) + Math.pow(mousePos.y, 2)
              ) /
              (item.size * Math.sqrt(2) / 2),
          }))
        const nearestItem = [...itemsD].sort(
          (a, b) => a.distance - b.distance
        )[0]

        if (nearestItem && nearestItem.distance < 1) {
          hoveredItemId_ = nearestItem.id
          // console.log('nearestItem', nearestItem);
        } else {
          hoveredItemId_ = -1
        }

        return itemsD.map(item => ({
          ...item,
          hovered: item.id === hoveredItemId_,
        }))
      },
    }
  }),
  /**
   * Recalculate items positions, size, hover
   */
  withProps(({ getItemsViewProps }) => ({
    items: getItemsViewProps(),
  })),
  /**
   * Prepare data for react-motion
   */
  withProps(({ items, springConfig }) => ({
    animStyles: items.map(item => ({
      key: `${item.id}`,
      data: item,
      style: {
        x: spring(item.x, springConfig),
        y: spring(item.y, springConfig),
        size: spring(item.size, springConfig),
        borderK: spring(item.hovered ? 3 : 2, springConfig),
      },
    })),
  }))
)

export default enhanceItemsAnimator(itemsAnimator)