mo.js is a JavaScript library devoted to motion for the web. It offers a declarative syntax for motion and the creation of elements for animation. Even though mo.js is still in beta, there are already a host of amazing features to play with. Its author, Oleg Solomka (otherwise known as LegoMushroom), creates incredibly impressive demos and tutorials for the library’s offerings that you should check out, but in this article we’ll run through a really quick overview of features and tutorials to get you started.
mo.js basically offers two ways to make something that moves. You can do what other libraries do and reach inside the DOM or SVG DOM and move it, or you can create a special mo.js object, which has some unique offerings. There are fundamental things available to both ways of working, such as custom path easing and timelines. The path easing and timelines also have pretty impressive working tools to make them easier to adjust while you’re working.
Depending on what you’re animating, the shapes and other objects that mo.js allows you to make might simplify your workflow. mo.js offers a declarative syntax that makes it very easy to create something on the fly.
Figure 13-1 and its corresponding example are basic demos of the syntax.
The code for this example is as follows:
var shape = new mojs.Shape({ shape: 'circle', // shape "circle" is default radius: 50, fill: '#A8CABA', // default is pink stroke: '#5D4157', strokeWidth: 3, isShowStart: true, // show before any animation starts });
Here are some of the base things you need to know:
circle
, rect
, cross
, equal
, zigzag
, and polygon
. (Default is circle
).fill
, a stroke
, and a strokeWidth
. (Default is fill
with no stroke
or strokeWidth
. equal
and cross
don’t have space to fill, so they will not appear unless a stroke
is specified.)radiusX
or radiusY
. (Default is 50
.)isShowStart
. This is a Boolean—true
or false
. true
allows you to see it even if you’re not going to animate the shape. (Default is false
.)polygon
, zigzag.
, and equal
allow you to pick a number of points so that you can create different types of shapes. (Default is 3
.)top
, left
, etc.Figure 13-2 and its corresponding example demonstrate some of the shapes you can create.
The code that creates the first shape (top left) is as follows:
const zigzag = new mojs.Shape({ shape: 'zigzag', points: 7, radius: 25, radiusY: 50, top: pos.row1, left: pos.col1, fill: 'none', stroke: color1, isShowStart: true, });
You might notice if you look into the DOM that these are SVG shapes placed inside of a div for positioning. You can also pass a parent, like parent: '#id-to-be-placed-under'
, if you’d like to put the shape somewhere within the DOM. You can pass any DOM node as a parent, so parent: someEl
would work as well. At some point, you’ll also be able to choose between using a div or SVG, which will be awesome, because it makes it much easier to create a scaling animation for mobile if you can place it with an SVG viewBox
.
You can create custom shapes to animate as well, and add them in as the shape object:
// custom shape class OneNote extends mojs.CustomShape { getShape () { return '<path d="M18.709 ... "/>'; } } mojs.addShape( 'oneNote', OneNote ); const note1 = new mojs.ShapeSwirl({ shape: 'oneNote', ... });
To create an animation with a shape in mo.js, you pass in an object, with the key and value expressing what you’d like to tween from and to: { fromvalue : tovalue }
. Also worth mentioning is that in ES5 environments, this means you can’t easily use a value stored in a variable as your “from” value. This would look like options = {scale: {} }; options.scale[from]=to;
. In ES6, it’s easier because you can use variables for key values when you declare object literals.
We can use transform properties like scale
, angle
(known in CSS as rotate
), and opacity
, and we can interpolate colors as well, as shown here with fill
and in Figure 13-3:
scale: { 0 : 1.5 }, angle: { 0 : 180 }, fill: { '#721e5f' : '#a5efce' },
We can also specify a few other parameters:
duration
delay
repeat
speed
—1
is the default speed, so 0.5
would be half speed and 1.5
would be 1.5× fasterisYoyo
—whether or not it tweens back and fortheasing
—written as an object, like ease.in
, ease.out
, or ease.inout
backwardEasing
—when using isYoyo: true
, if you want the way that it eases back (on the backswing of the yoyo) to be different from the way that it eases forward, you would specify that with this method (defaults to easing
if not specified)isSoftHide
—whether it hides the shape with transforms rather than display
(Boolean, defaults to true
)In other JavaScript libraries, we’d either write a helper function to use random values, or use a bit of code that allows us to pick the ceiling and floor values to choose between. (See the note in the opening section of Chapter 11 for more details on Math.random()
.)
In mo.js, we can abstract this away very nicely. We can pass in random values by writing the string property : 'rand(min, max)'
; for instance, angle: 'rand(0, 360)'
.
If we’d like to chain two animations on a Shape
, we can call .then()
on the initial tween, like so:
const polygon = new mojs.Shape({ shape: 'polygon', points: 5, stroke: '#A8CABA', scale: { 0 : 1.5 }, angle: { 0 : 180 }, fill: { '#721e5f' : '#a5efce' }, radius: 25, duration: 1200, easing: 'sin.out' }).then ({ stroke: '#000', angle: [-360], scale: 0, easing: 'sin.in' });
Features like ShapeSwirl
and Burst
are interesting parts of mo.js; they’re pretty beautiful out of the box. A ShapeSwirl
is similar to a regular Shape
object, but the movement is pretty much how it sounds—the shape swirls around. You have a few parameters to work with for a swirl, and they are all based on the sine that the swirl works with:
swirlSize
—the amount it swirls horizontally (the deviation or amplitude of the sine)swirlFrequency
—the frequency of the sinepathScale
—the scale (length) of the sinedegreeShift
—the angle of the sine, used if you’d like to make it move toward a different direction on a 360 degree plane (especially useful for using ShapeSwirl
with a Burst
)direction
—the direction of the sine, either -1
or 1
(good for setting something in the other direction if you want it to look a little random)isSwirl
—whether the shape should follow a sinusoidal path (Boolean—true
or false
)These can be a little confusing to read and grok, so I’ve made a demo so you can play with the values to understand them a little better. Figure 13-4 is a screenshot of the demo.
Also, you can set up a few base configurations—like a custom shape, or an object, with some configuration—to reuse for different ShapeSwirl
s (or any other shape) with an ES6 spread operator like this (Figure 13-5). This is really nice if you have a few similar objects:
const note_opts_two = { shape: 'twoNote', scale: { 5 : 20 }, y: { 20: -10 }, duration: 3000, easing: 'sin.out' }; const note1 = new mojs.ShapeSwirl({ ...note_opts_two, fill: { 'cyan' : color2 }, swirlSize: 15, swirlFrequency: 20 }).then({ opacity: 0, duration: 200, easing: 'sin.in' });
A Burst
is also really quite lovely out of the box. If you use the default configuration, you would say this:
const burst = new mojs.Burst().play();
To configure a Burst
, you have a few options, as shown in Figure 13-6:
count
—the number of children in the Burst
(default is 5
)degree
—the number of degrees around the center that the children come fromradius
—the radius that the children spread out to (radiusX
and radiusY
apply here as well)isSoftHide
—whether it hides the children with transforms rather than display
(Boolean, defaults to true
); this applies to all shapes, but I bring it up again because it’s particularly useful for a Burst
with several childrenAll of the same rules for Shape
also apply to Burst
, and we can apply them to the nodes themselves as a separate object using children
, like this:
const burst = new mojs.Burst({ radius: { 0: 100 }, count: 12, children: { shape: 'polygon', ... } });
In a Timeline
, you can either .add
a bunch of objects or tweens that you have previously declared as a variable, or you can .append
them, and have them fall in order:
const timeline = new mojs.Timeline({ .add( tween ) .append( tween ) });
.add
: allows you to add any objects or shapes to the timeline. They’ll all fire at once, but you can still use delays or staggering to adjust their timing. .append
: adds objects, but staggers them in the order they are added.
There are a few things you can do in a Timeline
that are worth noting. You can add repeat
, delay
, and speed
, just like to the objects themselves, as object parameters, like this:
new mojs.Timeline({ repeat: 3, isYoyo: true });
You can also nest a Timeline
inside another Timeline
; you can even nest them infinitely:
const subTimeline = new mojs.Timeline(); const master = new mojs.Timeline() .add( subTimeline );
With all of these constructors, we haven’t really spoken too much about tweening (animating) what already exists. All of the same parameters for shape motion (duration
, repeat
, easing
, etc.) are also available for Tween
s. To use a Tween
, we update styles or attributes (whatever we’re trying to change) along a path. Here’s a simple example:
var thingtoselect = document.querySelector('#thingtoselect'); new mojs.Tween({ duration: 2000, onUpdate: function (progress) { square.style.transform = 'translateY(' + 200*progress + 'px)'; } }).play();
I used this kind of tween to create the effect on the far left side of the example in Figure 13-7, of the little zigzags drawing themselves on repeatedly using the SVG line drawing trick with stroke-dashoffset
. I’m also using the path easing available in mo.js, which is discussed in the next section. I made the water in the tanks appear to move by updating SVG path attributes as well.
Here’s the code for the laser beam:
new mojs.Tween({ repeat: 999, duration: 2000, isYoyo: true, isShowEnd: false, onUpdate: function (progress) { var laser1EProgress = laser1E(progress); for (var i = 0; i < allSideL.length; i++) { allSideL[i].style.strokeDashoffset = 20*laser1EProgress + '%'; allSideL[i].style.opacity = Math.abs(0.8*laser1EProgress); } } }).play();
Tweens have rich callbacks available that take into account fine-tuning that can sometimes make all the difference. Some examples of this are onStart
versus onRepeatStart
, onComplete
versus onRepeatComplete
, and onPlaybackStart
versus onPlaybackPause
. A full list is available in the docs.
A very cool feature of mo.js is that aside from the other built-in easing values, you can also pass in an SVG path as an easing value. I use this feature in several demos in this chapter, but to be honest, I could never do path easing justice like the gorgeous tutorial LegoMushroom has prepared. I’ll simply explain the base premises to get you started and show you how it works, but I highly recommend going through his post.
Before we go through all of path easing, it’s important to establish that if you’d like to work with something out of the box, the base functions will get you very far. The syntax for built-in easing is written like so:
easing: 'cubic.in'
Mastering easing can be the key ingredient to bringing your animations to life, so being able to fine-tune your motion with custom paths is helpful. If you’re comfortable animating in CSS, you might like the mo.js bezier
easing, which accepts the same curve values (without some of the same restrictions), as CSS’s cubic-bezier
. Here’s an example of this kind of easing:
easing: 'bezier( 0.910, 0.000, 0.110, 1.005 )'
If you’d like more refined control than what bezier
easing allows, path easing is really nice. You pass in an SVG path, and your shape is updated to work with it. Let’s look back again at the example I pulled out earlier from the raygun. I used path easing to interpolate the values as it updated:
const laser1E = mojs.easing.path('M0,400S58,111.1,80.5,175.1s43,286.4, 63,110.4,46.3-214.8,70.8-71.8S264.5,369,285,225.5s16.6-209.7, 35.1-118.2S349.5,258.5,357,210,400,0,400,0'); new mojs.Tween({ repeat: 999, duration: 2000, isYoyo: true, isShowEnd: false, onUpdate: function (progress) { var laser1EProgress = laser1E(progress); for (var i = 0; i < allSideL.length; i++) { allSideL[i].style.strokeDashoffset = 20*laser1EProgress + '%'; allSideL[i].style.opacity = Math.abs(0.8*laser1EProgress); } } }).play();
To really get a sense of how a path ease can affect the movement and behavior of an animation, check out the CodePen demo in the next section. The curve editor tool that mo.js offers will help you to visualize and immediately get a sense of how an ease can refine what you create.
One of the most impressive things about mo.js is the tooling. To whet your palate, there is a demonstration on Vimeo; see Figure 13-8.
I made a quick pen to showcase both the player tool and the curve editor so that you can play around with them (Figure 13-9). Feel free to either fork it or just adjust it live in CodePen; it’s fun to try out. The curve tool is on the left side and the timeline is tucked at the bottom with a little arrow.
Here’s the coolest part: LegoMushroom isn’t done. He’s working on new tooling for the timeline now. Check out the awesome design for this tool on GitHub. If you’re interested in contributing to open source, here’s an opportunity to dig in and help make a really useful tool—click on the “help wanted” label on the right!
The other finished tools are available in their own repos:
There is also a Slack channel you can join if you’re interested in contributing or learning.
If you’re the kind of person who likes playing with things more than reading, all of the CodePen demos from this chapter are available in a collection.
There are, of course, things in the library that I didn’t cover in this chapter. I went over some of the most useful features of mo.js in my opinion, but there are more things to discover. Check out the docs for more information.
3.19.75.133