ExamplesAnimation
import bezier from 'bezier-easing'
import {useMemo} from 'react'
import {
  useObservable,
  useObservableEvent,
} from 'react-rx'
import {Subject, timer} from 'rxjs'
import {
  map,
  startWith,
  switchMap,
  tap,
} from 'rxjs/operators'
import {styled} from 'styled-components'

const BALL_SIZE = 30
const BOX_SIZE = 300
const MAX_TOP = BOX_SIZE - BALL_SIZE

// Create subject for easing changes
const easing$ = new Subject<EasingName>()

function easeCustom(n: number) {
  return n
}

type EasingName = keyof typeof EASINGS

function AnimationExample() {
  // Handle easing changes
  const handleEasingChange = useObservableEvent<
    EasingName,
    any
  >((change$) =>
    change$.pipe(
      tap((easing) => easing$.next(easing)),
    ),
  )

  // Create animation stream
  const animation$ = useMemo(
    () =>
      easing$.pipe(
        startWith('easeCustom' as EasingName),
        switchMap((easing: EasingName) =>
          timer(0, 16).pipe(
            map((n) => (n % MAX_TOP) * 2),
            map((n) =>
              n > MAX_TOP ? MAX_TOP * 2 - n : n,
            ),
            map(
              (linearTop) =>
                [
                  EASINGS[easing](
                    linearTop / MAX_TOP,
                  ) * MAX_TOP,
                  easing,
                ] as [number, EasingName],
            ),
          ),
        ),
      ),
    [],
  )

  const [top, currentEasing] = useObservable(
    animation$,
    [0, 'easeCustom' as EasingName],
  )

  return (
    <>
      <SelectWrapperLabel>
        Easing function:
      </SelectWrapperLabel>
      <SelectWrapper>
        {Object.keys(EASINGS).map(
          (easingName) => (
            <label
              key={easingName}
              className={
                easingName === currentEasing
                  ? 'selected'
                  : ''
              }
            >
              <input
                tabIndex={0}
                type="checkbox"
                checked={
                  easingName === currentEasing
                }
                key={easingName}
                onChange={() =>
                  handleEasingChange(
                    easingName as EasingName,
                  )
                }
              />
              {easingName.substring(4)}
            </label>
          ),
        )}
      </SelectWrapper>
      <BoxWrapper>
        <Box>
          <Ball style={{top}} />
        </Box>
      </BoxWrapper>
    </>
  )
}

// --- easing definitions and stylings
const EASINGS = {
  easeCustom: easeCustom,
  easeInSine: bezier(0.47, 0, 0.745, 0.715),
  easeOutSine: bezier(0.39, 0.575, 0.565, 1),
  easeInOutSine: bezier(0.445, 0.05, 0.55, 0.95),
  easeInQuad: bezier(0.55, 0.085, 0.68, 0.53),
  easeOutQuad: bezier(0.25, 0.46, 0.45, 0.94),
  easeInOutQuad: bezier(
    0.455,
    0.03,
    0.515,
    0.955,
  ),
  easeInCubic: bezier(0.55, 0.055, 0.675, 0.19),
  easeOutCubic: bezier(0.215, 0.61, 0.355, 1),
  easeInOutCubic: bezier(0.645, 0.045, 0.355, 1),
  easeInQuart: bezier(0.895, 0.03, 0.685, 0.22),
  easeOutQuart: bezier(0.165, 0.84, 0.44, 1),
  easeInOutQuart: bezier(0.77, 0, 0.175, 1),
  easeInQuint: bezier(0.755, 0.05, 0.855, 0.06),
  easeOutQuint: bezier(0.23, 1, 0.32, 1),
  easeInOutQuint: bezier(0.86, 0, 0.07, 1),
  easeInExpo: bezier(0.95, 0.05, 0.795, 0.035),
  easeOutExpo: bezier(0.19, 1, 0.22, 1),
  easeInOutExpo: bezier(1, 0, 0, 1),
  easeInCirc: bezier(0.6, 0.04, 0.98, 0.335),
  easeOutCirc: bezier(0.075, 0.82, 0.165, 1),
  easeInOutCirc: bezier(0.785, 0.135, 0.15, 0.86),
  easeInBack: bezier(0.6, -0.28, 0.735, 0.045),
  easeOutBack: bezier(0.175, 0.885, 0.32, 1.275),
  easeInOutBack: bezier(0.68, -0.55, 0.265, 1.55),
  easeLinear: (n: number) => n,
}

const BoxWrapper = styled.div`
  display: flex;
  flex-direction: column;
  align-items: center;
`
const Box = styled.div`
  position: relative;
  border: 1px solid #555;
  height: ${BOX_SIZE}px;
  width: ${BOX_SIZE}px;
`

const Ball = styled.div`
  position: absolute;
  border-radius: 100%;
  background-color: #901a3b;
  width: ${BALL_SIZE}px;
  height: ${BALL_SIZE}px;
  left: ${BOX_SIZE / 2 - BALL_SIZE / 2}px;
`

const SelectWrapperLabel = styled.h2`
  margin-top: 2em;
  font-size: 0.8em;
`

const SelectWrapper = styled.div`
  display: flex;
  flex-wrap: wrap;
  flex-direction: row;
  input[type='checkbox'] {
    opacity: 0;
    position: absolute;
    height: 1px;
    width: 1px;
  }
  font-size: 90%;
  label {
    border-radius: 3px;
    padding: 0 2px;
    margin: 0 2px;
  }
  label:focus-within {
    color: #333;
    background-color: #ccc;
  }
  label.selected {
    background-color: #5588ee;
    color: #333;
  }
`

export default function App() {
  return <AnimationExample />
}

Open on CodeSandboxOpen Sandbox