Shaker Maker: Part II

Animated Playback

Shaw
Hard Mode
5 min readJan 21, 2018

--

Welcome to Part II of the Shaker Maker series. Shaker Maker is a web app that I built that allows users to create choreography, play music, sync their choreography to the exact tempo of any song and playback their choreography with it. In Part I, I showed you how I built a posable figure so that users can click-and-drag the figure into poses and create a choreography. In Part II, I want to show you how I implemented an animated playback feature so that users can watch the choreography that they created.

You can see a full demo video of Shaker Maker here, and if you’re a Tinashe fan there is an extra choreography demo for you here.

The Concept

Users create choreography by dragging the joints of a posable figure into a series of poses. The posable figure is shown below and the button to add a pose is located on the speaker to the left of the figure.

posable figure, Add Pose button shown in green

When a user clicks the Add Pose button, the figure’s current pose is added to the choreography as a new frame. The choreography is represented as a list of poses that are displayed below the posable figure.

a choreography is represented as a list of poses

The playback feature is displayed as a TV screen with a play/pause button, a stop button, and a slider to adjust playback speed. When a user clicks the play button, the playback feature cycles through each pose in the current choreography. A user can adjust the slider bar to change the playback speed.

Conceptually, that is how the playback feature works. Technically, I implemented animated playback in JavaScript using React, Redux, and Konva.

How It Works

First, a user creates a choreography. All of the poses that make up a choreography are stored in state using Redux. Each pose is represented as an array of lines where each line is both a Konva object and React component. Once a user has created a choreography, they can use the playback feature to watch their choreography. The playback feature lives within a React component called PlaybackContainer . This component is connected to the Redux store, but it also has a local state with the following shape.

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

frames points to the array of poses that the animated playback feature will cycle through, frameCounter points to the current frame being displayed, playing points to a boolean representing whether the choreography is currently playing or not, and playbackSpeed points to the current value of the slider bar controlling how fast the choreography plays.

Frames

The poses stored in Redux need to be resized before they can be played back. In the componentWillReceiveProps() lifecycle method, these poses are mapped to a new array of resized poses and are stored in local state as frames .

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

The resizePose() function is responsible for resizing each line of a pose, representing the figure’s body parts, as well as an ellipse that represents the figure’s head. For my purposes, the length of each line is doubled and the thickness is increased. For the head, the x and y positions of the center of the ellipse are doubled and the line thickness is increased. The function returns a resized pose which is an array of lines that are simultaneously Konva objects and React components as I am using the react-konva library.

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}
/>
)
}
})
}

Once the poses are resized, they are stored in local state as frames .

Animating

Animation for Shaker Maker basically consists of: a canvas to display the animation, buttons to control starting and stoping playback, a function to control cycling through the poses, and a slider bar to control the playback speed.

There are a number of event handlers controlling animated playback: handlePlay() , handlePause() , handleStop() , andhandlePlaybackSpeed() . The play, pause, and stop handlers are attached to the buttons that correspond with their name. The playback speed handler is attached to the slider and updates the playbackSpeed in local state whenever the slider bar is adjusted.

The most important element of the animated playback feature is the playbackTimer() . This function is responsible for the actual animation by cycling through each of the poses in a choreography. The playbackTimer() is a recursive function that calculates the required timeInterval between frames based on the selected playbackSpeed , updates the frameCounter , and calls itself using setTimeout() with the delay equal to the calculated 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)
}

The timeInterval is calculated by subtracting 100 from the current playbackSpeed (the value represented by the slider) and multiplying the result by 10. The choice of 10 as a multiplier was somewhat arbitrary, it just controls the range of possible playback speeds. The time interval is in milliseconds and ranges from one frame per 1000 milliseconds (one per second) up to one frame per 10 milliseconds*. This corresponds to a tempo range of 60 BPM (beats per minute) to a wildly impractical 6000 BPM !

* The actual max playback speed is as fast as setTimeout can run with a delay of 0, but the maximum calculable playback speed is when the slider is at 99 yielding one frame per 10 milliseconds.

The playbackTimer() is initially called by handlePlay() . When a user clicks the play button, handlePlay() is invoked setting playing to true and then calling the playbackTimer().

handlePlay = () => {
if (this.state.frames.length === 0 || this.state.playing)
return false

this.setState({
playing: true
}, this.playbackTimer)
}

The Result

Stick figures WEEERKING it!

Thanks for reading Part II ! If you enjoyed it, check out Part III (coming soon) on how I made an in app music player and a beat matcher to sync choreography to a user’s music.

--

--

Shaw
Hard Mode

programming sorcery and black magic bit witchery