BoxArt

API Documentation

Usage

BoxArt is a state-ful animate-on-class-change library. It starts an animation when an element it is watching changes its class list. BoxArt currently animates html elements by listening to mutation observer events, React's virtual dom, or Preact's virtual dom.

DOM Usage:

const animations = {
  'animatedType': {
    default: // ...
  },
};

new BoxArtMutation({animations})
.observe(document.body);

JS Bin on jsbin.com

React/Preact usage:

<BoxArt animations={animations}>
  <App />
</BoxArt>

JS Bin on jsbin.com

Classful Animations

BoxArt animations respond to changes in an animated element's location in the DOM, not spatial position, or its class list. Animating elements are identified by up to 3 names in their class list. The three names are the animation type, the element's id, and the current animation. The type determines what animations are available to a given element. At minimum an element can animate with only the type in its class list. The id defaults to the type. Other ids are the type with any suffix. animation defaults to default.

An animation is run with three state objects, a beginning state, an end state, and a modifiable state. An animation works by updating these states from the DOM or javascript values, animating the modifiable state between the begin and end states, and presenting the modifiable state on the element and its children. The animating element's id indicates what set of states with that element. This lets elements move in the DOM's hierarchy or for a new element to reuse an old element's state.

Animation Functions

BoxArt provides a family of functions as shorthand for creating its animation functions. They fall into three families: update, animate, and present.

As mentioned update functions update the state objects BoxArt uses. animate functions animate a third state object between the first two. present functions present the third state on the animated element.

update

update: update.rect().asElement(update.object({
  left: update.property('left'),
  top: update.property('top'),
})),

BoxArt's function shorthand allows for some chaining but this can also be written:

update: update.asElement(update.rect(), update.object({
  left: update.property('left'),
  top: update.property('top'),
})),

Lets break down what this function is doing.

The returned function takes three arguments state, element, and data and returns its final state.

Written out the above might look like:

update: (function() {
  return function(state, element, data) {
    element = element.getBoundingClientRect();
    state = state || {};
    state.left = element.left;
    state.top = element.top;
    return state;
  };
}()),

This function is using the element's client rectangle to build a left and top state. In addition to the main function, update has a few helper functions copy, merge and should. copy and merge are used to update the animation states without querying the element when it isn't necessary or desired. should if given shortcuts animations if it returns false.

Our update written with its helpers would be:

update: (function() {
  const func = function(state, element, data) {
    element = element.getBoundingClientRect();
    state = state || {};
    state.left = element.left;
    state.top = element.top;
    return state;
  };
  func.copy = function(dest, src) {
    dest = dest || {};
    dest.left = src.left;
    dest.top = src.top;
    return dest;
  };
  // This example creates a merge that is the same as copy.
  func.merge = function(dest, src) {
    dest = dest || {};
    dest.left = src.left;
    dest.top = src.top;
    return dest;
  };
  // This example normally does not have a should but here is a barebones
  // example.
  func.should = function(stateA, stateB) {
    return true;
  };
  return func;
}()),

animate

animate: animate.object({
  left: animate.begin().to(animate.end()),
  top: animate.top().to(animate.end()),
}),

animate functions like update's may chain. This function expands to:

animate: animate.object({
  left: animate.to(animate.begin(), animate.end()),
  top: animate.to(animate.begin(), animate.end()),
}),

animate functions slightly break a rule in the order of arguments to BoxArt animation functions. The first argument in most BoxArt functions is the destination for values. Animation functions instead pass a time value t. t starts as the number of seconds since the start of the animation but normally is transformed by easing functions into a value between 0 and 1. The other arguments are state, begin, end and data.

With its done helper function that by its namesake determines when the animation is done, the example animate function handwritten may look like:

animate: (function() {
  const func = function(t, state, begin, end, data) {
    state.left = (end.left - begin.left) * t + begin.left;
    state.top = (end.top - begin.top) * t + begin.top;
  };
  func.done = function(t) {
    return t >= 1;
  };
  return func;
}()),

present

update and animate functions share their shape around the defined data structure for an animation. present will use that data structure to change how the animated element is presented.

present: present.style({
  transform: present.translate([
    present.key('left').to(present.end).px(),
    present.key('top').to(present.end).px(),
  ]),
}),

present functions commonly expand more than update and animate.

present: present.style({
  transform: present.translate([
    present.px(present.sub(present.key('left'), present.end(present.key('left')))),
    present.px(present.sub(present.key('top'), present.end(present.key('top')))),
  ]),
}),

This present function is creating a difference on the left and top keys so that the transform is relative to where the element should current render.

Since present functions may overwrite the current presentation of an element, the helpers create some functions used to store and restore the original values that may be overwritten. Together these functions follow update's argument layout where the value being modified is the first.

present takes 3 arguments: element, state, and data. Our example might look like:

present: (function() {
  return function(element, state, data) {
    const end = data.end;
    element.style.transform =
      `translate(${state.left - end.left}px, ${state.top - end.top}px)`;
  };
}()),

store like update initializes and stores its target data with arguments store, element, and data. restore swaps the first two arguments and takes element, store, and data.

present: (function() {
  const func = function(element, state, data) {
    const end = data.end;
    element.style.transform =
      `translate(${state.left - end.left}px, ${state.top - end.top}px)`;
  };
  func.store = function(store, element, data) {
    store = store || {};
    const style = store.style = store.style || {};
    style.transform = element.style.transform;
    return store;
  };
  func.restore = function(element, store, data) {
    element.style.transform = store.style.transform;
  };
  return func;
}()),

Animated State Machine

The animation functions are used by an animated state machine. The object tracks the current animation class name of an animated element. When the element is updated to the current animation class name, it updates end, stores the presentation and starts animating and presenting the updated modifiable state. The first animation also copies the end state into the begin and state state. These values, the element being animated and the time t make up the data argument stored by the animated state machine and passed to most of the animation functions.

this.data = {
  animated: {
    root: {
      element: // HTMLElement
    },
  },
  t: // Time in seconds
  state: // The modifiable state
  begin: // The beginning state
  end: // The target end state
  store: // The stored possibly overwritten presentation
};

With a reference to an AnimatedManager the state machine for an element by its type and id.

React/Preact enter and leave Animations

React and Preact integrations support animated elements having a enter and leave animation used when the element enters and leaves the virtual dom.

These animations normally need animate functions with constant values to help and leave animations need a little CSS to help.

We can create an example that fades in and out.

enter: {
  update: update.rect().asElement(update.object({
    opacity: animate.constant(0),
  })),
  animate: animate.object({
    opacity: animate.constant(0).to(animate.end()),
  }),
  present: present.style({
    opacity: present.key('opacity'),
  }),
},
leave: {
  update: update.rect().asElement(update.object({
    opacity: animate.constant(0),
  })),
  animate: animate.object({
    opacity: animate.begin().to(animate.constant(0)),
  }),
  present: present.style({
    opacity: present.key('opacity'),
  }),
},
.animatedType .leave {
  opacity: 0;
}