Shaker Maker: Part I

Building a posable figure in JavaScript

Shaw
Hard Mode
7 min readDec 29, 2017

--

I love to dance. When I’m dancing I have this feeling of joy and freedom that flows through my body. What’s more, when you are dancing with other people you feel connected and share that feeling of joy and freedom. This feeling inspired me to build a tool that allows people to create, explore, and experience dance.

I built a web application, Shaker Maker, that allows users to create choreography, sync their choreography to any music that they choose, listen to their music, and watch their choreography in action. You can see a full demo video of Shaker Maker here. I built Shaker Maker with a Ruby on Rails backend and a JavaScript frontend utilizing React and Redux. I’d like to take you back through the application’s development and describe my thought process, how I approached problems, challenges that I encountered, and my solutions to challenges. Hopefully you learn something along the way. This will be part of a series of posts about the making of Shaker Maker. Part I will focus on building the posable figure.

The Idea

In its infancy, the idea for Shaker Maker was: a posable figure, a button to save an image of the figure’s current pose, and a list of all the images saved. This series of images would effectively make up a choreography. These three features were my minimum viable product, but I had goals for quite a few other features as well: a playback feature that would allow a user to watch their choreography, an in-app music player, and a way to sync the playback of the choreography to the music so that they perfectly match. However, the first step was to build a posable figure so that users could start to sketch out a choreography.

Posable Figure

The first feature that I needed to build was the posable figure. I knew that I wanted to use canvas for the figure, and I had used a library called Konva in a previous project (a game called Code Caverns), so I wanted to use this library again. Konva provides a layer of abstraction on top of canvas and it has a nice React wrapper, react-konva, which allows you to make React components out of Konva objects (the documentation for Konva can be found here).

The essential idea for the posable figure is a stick figure with draggable joints (elbows, shoulders, hips, knees, etc.). A user is able to pose the figure into a dance move by clicking and dragging its joints. The joints are connected by lines which form all of the body parts except for the head which is an ellipse.

The figure is made up of two container components: a JointLayer and a LineLayer. The JointLayer component renders all of the joints as circles on the canvas. The position of each joint is stored in the Redux store and is updated every time a joint is moved. Then there is a LineLayer that renders all of the body parts connecting the joints. This layer is re-rendered every time a joint is moved so that the body parts move with the joints.

Perhaps the hardest part about coding the posable figure was getting the joints to move together. For example, if a user drags the knee joint, the foot needs to move with it, otherwise the leg would deform. Similarly, when a shoulder is dragged, both the elbow and hand need to move with the shoulder to maintain the proportions of the figure. More generally, if a user drags a joint, all of the children of that joint need to move with it. Most of the relationships between parent and child joints are intuitive, the only two that might need defining are the pelvis and neck joints. The pelvis is essentially the root node — every other joint is a child of the pelvis — so moving the pelvis moves the entire figure. The neck is the parent node of the entire upper body and pivots about the pelvis. Moving the neck joint will rotate the upper body about the pelvis.

JointLayer

Drag movement bubbling down to children and all other aspects of joint movement are controlled by callback functions which are methods of the JointLayer component. These callbacks are passed down to each joint as props. The four main callback functions that control joint movement are: onDragStart(), onDragMove(), onDragEnd(), and dragBound().

onDragStart()

This function is called once every time a user initializes a click and drag. It calculates the initial distances between the joint that the user clicked on and all of its child joints. These distances are stored in the local state of the JointLayer component. They are used to maintain the distance between each joint and its children as the figure is being posed.

onDragStart = (e) => {
const { name, x, y } = e.target.attrs
const newState = {}
jointChildren[name].forEach (child => {
const distX = this.props.joints[child].x - x
const distY = this.props.joints[child].y - y
newState[child] = {x: distX, y: distY}
})
this.setState({
dragStartDist: newState
})
}

jointChildren is an object that maps each parent joint to an array of its child joints.

onDragMove()

This function is called every time the position of a joint changes during a click and drag. It iterates through the children of the joint that is being dragged and updates their positions such that the distance between each child and the joint being dragged is the same as the distance right before the joint started being dragged (i.e. the distance that was stored in state by the onDragStart function). The position of each joint is updated by dispatching an action to the Redux store by calling moveJoint().

onDragMove = (e) => {
const {name, x, y} = e.target.attrs
const dist = this.state.dragStartDist
jointChildren[name].forEach(child => {
this.props.moveJoint(
child,
dist[child].x + this.props.joints[name].x,
dist[child].y + this.props.joints[name].y
)
})
this.props.moveJoint(name, x, y)
}

onDragEnd()

onDragEnd() simply clears out the distances that were stored in local state by onDragStart().

onDragEnd = () => {
this.setState({
dragStartDist: {}
})
}

dragBound()

The dragBound function defines the conditions for which a joint is allowed to be dragged. The main condition is that the distance between a joint and its pivot point must remain constant. For example, the pivot point of the elbow is the shoulder, so when dragging the elbow the distance between the elbow and the shoulder must not change. This effectively means that a joint can only move in a circle around its pivot point. The dragBound function calls findPivot() in order to get the pivot joint of the joint being dragged. Then, it calculates the current scale of the body part — the limb that connects the joint being dragged and its pivot joint — which represents how much it would be stretched or squashed due to the joint movement. Finally, it corrects for the stretch or squash such that the scale is always equal to 1 and the length of the body part remains constant.

dragBound = function(pos) {
const pivot = this.attrs.findPivot(this.attrs.name)
const stretch = Math.sqrt(Math.pow(pos.x - pivot.x, 2) +
Math.pow(pos.y - pivot.y, 2))
const scale = pivot.radius / stretch
if(scale !== 1 && this.attrs.name !== 'pelvis') {
return {
y: Math.round((pos.y - pivot.y) * scale + pivot.y),
x: Math.round((pos.x - pivot.x) * scale + pivot.x)
}
} else {
return {x: pos.x, y: pos.y}
}
}

LineLayer

All of the figure’s body parts are rendered in the LineLayer. Basically, a line is constructed that connects each joint to its parent and an ellipse is drawn for the head.There is one method of the LineLayer component that handles rendering, drawLines() .

drawLines()

This function is called every time the LineLayer renders and a re-render is triggered every time a joint is moved. The function iterates through each body part and constructs a Konva line connecting each joint to its parent. It constructs an ellipse for the head with its center at the midpoint between the joint at the top of the head and the joint at the bottom of the head.

const drawLines = () => {
const joints = props.joints
const lines = []
let i = 0
for(let part in bodyMap) {
const {start, stop} = bodyMap[part]
if (part === 'head') {
const centerX = (joints[stop].x + joints[start].x) / 2
const centerY = (joints[stop].y + joints[start].y) / 2

lines.push(
<Ellipse
key={++i}
x={centerX}
y={centerY}
radius={{x: 10, y: 13}}
stroke='#000'
strokeWidth={4}
/>
)
} else {
const points = [
joints[start].x,
joints[start].y,
joints[stop].x,
joints[stop].y
]
lines.push(
<Line
key={++i}
points={points}
stroke='#000'
strokeWidth={4}
/>
)
}
}
props.setCurrentPose(lines)
return lines
}

The Result

Behold an interactive posable figure!

To recap: the posable figure essentially consists of two container components, a JointLayer and a LineLayer. The JointLayer has methods that control the movement of the joints that it passes down as props to each joint component. The LineLayer has a single method that renders each line component and an ellipse component for the head every time a joint is moved.

That’s it for Part I. I hope you find it interesting and/or useful. Check out Part II to see how I created the animated playback feature and Part III (coming soon) to see how I implemented an in-app music player. Thanks for reading!

--

--

Shaw
Hard Mode

programming sorcery and black magic bit witchery