Анимированное воспроизведение
Добро пожаловать во вторую часть серии 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 (скоро) о том, как я создал музыкальный проигрыватель в приложении и битматчер для синхронизации хореографии с музыкой пользователя.