Анимированное воспроизведение

Добро пожаловать во вторую часть серии Shaker Maker. Shaker Maker — это созданное мной веб-приложение, которое позволяет пользователям создавать хореографию, воспроизводить музыку, синхронизировать свою хореографию с точным темпом любой песни и воспроизводить свою хореографию вместе с ней. В Части I я показал вам, как я создал подвижную фигуру, чтобы пользователи могли щелкнуть и перетащить фигуру в позы и создать хореографию. Во второй части я хочу показать вам, как я реализовал функцию анимированного воспроизведения, чтобы пользователи могли наблюдать за созданной ими хореографией.

Вы можете посмотреть полное демо-видео Shaker Maker здесь, а если вы поклонник Тинаше, для вас есть дополнительное демо хореографии здесь.

Концепт

Пользователи создают хореографию, перетаскивая суставы подвижной фигуры в серию поз. Фигура в позе показана ниже, а кнопка для добавления позы находится на динамике слева от фигуры.

Когда пользователь нажимает кнопку Добавить позу, текущая поза фигуры добавляется в хореографию как новый кадр. Хореография представлена ​​в виде списка поз, которые отображаются под подвижной фигурой.

Функция воспроизведения отображается в виде экрана телевизора с кнопкой воспроизведения/паузы, кнопкой остановки и ползунком для регулировки скорости воспроизведения. Когда пользователь нажимает кнопку воспроизведения, функция воспроизведения циклически повторяет каждую позу в текущей хореографии. Пользователь может настроить ползунок, чтобы изменить скорость воспроизведения.

Концептуально именно так работает функция воспроизведения. Технически я реализовал анимированное воспроизведение на JavaScript с помощью React, Redux и Konva.

Как это работает

Сначала пользователь создает хореографию. Все позы, составляющие хореографию, сохраняются в состоянии с помощью Redux. Каждая поза представлена ​​в виде массива строк, где каждая строка является как объектом Konva, так и компонентом React. После того, как пользователь создал хореографию, он может использовать функцию воспроизведения, чтобы посмотреть свою хореографию. Функция воспроизведения находится внутри компонента React под названием PlaybackContainer. Этот компонент подключен к хранилищу Redux, но также имеет локальное состояние следующего вида.

state = {
  frames: [],
  frameCounter: 0,
  playing: false,
  playbackSpeed: 75
}

frames указывает на массив поз, через которые будет циклически перемещаться функция анимированного воспроизведения, frameCounter указывает на текущий отображаемый кадр, playing указывает на логическое значение, представляющее, воспроизводится хореография в данный момент или нет, и playbackSpeed указывает на текущее значение ползунка. бар, контролирующий скорость воспроизведения хореографии.

Рамки

Позы, хранящиеся в Redux, должны быть изменены, прежде чем их можно будет воспроизвести. В методе жизненного цикла componentWillReceiveProps() эти позы сопоставляются с новым массивом поз с измененным размером и сохраняются в локальном состоянии как frames .

componentWillReceiveProps(nextProps) {
  const frames = nextProps.poses.list.map(pose => {
    return this.resizePose(pose.lines) 
  })
  this.setState({
    frames
  })
}

Функция resizePose() отвечает за изменение размера каждой строки позы, представляющей части тела фигуры, а также эллипс, представляющий голову фигуры. Для моих целей длина каждой линии удваивается, а толщина увеличивается. Для головы координаты x и y центра эллипса удваиваются, а толщина линии увеличивается. Функция возвращает позу с измененным размером, которая представляет собой массив линий, которые одновременно являются объектами Konva и компонентами React, поскольку я использую библиотеку react-konva.

resizePose = (lines) => {
  let i = 0
  return lines.map(line => {
    if (line.type === 'Ellipse') {
      const newCenterX = line.props.x * 2
      const newCenterY = line.props.y * 2 
      return (
        <Ellipse 
          key={++i} 
          x={newCenterX} 
          y={newCenterY} 
          radius={{x: 10, y: 14}} 
          stroke='#000' 
          strokeWidth={4}
        />
      )
    } else {
      const newPoints = line.props.points.map(point => point*2)
      return (
        <Line 
          key={++i} 
          points={newPoints} 
          stroke='#000' 
          strokeWidth={4} 
        />
      )
    }
  })
}

После изменения размера позы они сохраняются в локальном состоянии как frames .

Анимация

Анимация для Shaker Maker в основном состоит из: холста для отображения анимации, кнопок для управления запуском и остановкой воспроизведения, функции для управления циклическим переключением поз и ползунка для управления скоростью воспроизведения.

Существует несколько обработчиков событий, управляющих воспроизведением анимации: handlePlay(), handlePause(), handleStop() и handlePlaybackSpeed(). Обработчики воспроизведения, паузы и остановки прикреплены к кнопкам, которые соответствуют их имени. Обработчик скорости воспроизведения прикреплен к ползунку и обновляет playbackSpeed в локальном состоянии всякий раз, когда ползунок настраивается.

Наиболее важным элементом функции анимированного воспроизведения является playbackTimer() . Эта функция отвечает за фактическую анимацию, циклически повторяя каждую из поз в хореографии. playbackTimer() — это рекурсивная функция, которая вычисляет необходимое timeInterval между кадрами на основе выбранного playbackSpeed, обновляет frameCounter и вызывает себя, используя setTimeout() с задержкой, равной рассчитанному timeInterval.

playbackTimer = () => {
  if (!this.state.playing) return false
  const timeInterval = (100 - this.state.playbackSpeed) * 10
    
  let newFrameCount
  if (counter >= this.state.frames.length - 1) {
    newFrameCount = 0
  } else
    newFrameCount = this.state.frameCounter + 1
  }
  this.setState({frameCounter: newFrameCount})
  this.timeout = setTimeout(this.playbackTimer, timeInterval)
}

timeInterval вычисляется путем вычитания 100 из текущего playbackSpeed (значение, представленное ползунком) и умножения результата на 10. Выбор 10 в качестве множителя был несколько произвольным, он просто контролирует диапазон возможных скоростей воспроизведения. Временной интервал указывается в миллисекундах и варьируется от одного кадра в 1000 миллисекунд (один кадр в секунду) до одного кадра в 10 миллисекунд*. Это соответствует диапазону темпа от 60 BPM (ударов в минуту) до дико непрактичных 6000 BPM!

* Фактическая максимальная скорость воспроизведения равна setTimeout, которая может работать с задержкой 0, но максимальная вычисляемая скорость воспроизведения достигается, когда ползунок находится в положении 99, что дает один кадр за 10 миллисекунд.

playbackTimer() изначально вызывается handlePlay() . Когда пользователь нажимает кнопку воспроизведения, вызывается handlePlay(), устанавливая playing в значение true, а затем вызывая метод playbackTimer().

handlePlay = () => {
  if (this.state.frames.length === 0 || this.state.playing) 
    return false 
    
  this.setState({
    playing: true
  }, this.playbackTimer)
}

Результат

Придерживайтесь цифры WEEERKING это!

Спасибо, что прочитали Часть II! Если вам понравилось, ознакомьтесь с Частью III (скоро) о том, как я создал музыкальный проигрыватель в приложении и битматчер для синхронизации хореографии с музыкой пользователя.