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.
const animations = {
'animatedType': {
default: // ...
},
};
new BoxArtMutation({animations})
.observe(document.body);
<BoxArt animations={animations}>
<App />
</BoxArt>
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 animation
s 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 id
s 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.
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.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.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;
}()),
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;
}()),
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 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;
}