@okikio/animate
Version:
An animation library for the modern web which utilizes the Web Animation API (WAAPI) to create butter smooth animation.
1,069 lines (833 loc) • 76.6 kB
Markdown
# @okikio/animate
[](https://www.npmjs.com/package/@okikio/animate) [](https://bundlephobia.com/package/@okikio/animate)  
An animation library for the modern web. Inspired by animate plus, and animejs, [@okikio/animate](https://www.skypack.dev/view/@okikio/animate) is a Javascript animation library focused on performance and ease of use. It utilizes the Web Animation API to deliver butter smooth animations at a small size, it weighs **~11.6 KB** (minified and gzipped), since, `@okiko/animate` is treeshakeable the minimum usable file size you can reach is **~5.6 KB** (minified and gzipped).
I suggest reading the in depth article I made on CSS-Tricks about `@okikio/animate`, <https://css-tricks.com/how-i-used-the-waapi-to-build-an-animation-library/>, it will help you determine if `@okikio/animate` is right for your project.
_**Note**: [Custom Easing](#custom-easing), as well as a proper [Timeline](#timeline-class) are now supported._
*Before even getting started, you will most likely need the Web Animation API, Promise, WeakMap, Set, and Map polyfills. If you install [@okikio/animate](https://www.skypack.dev/view/@okikio/animate) via [npm](https://www.npmjs.com/package/@okikio/animate) you are most likely going to need [rollup](https://rollupjs.org/) or [esbuild](https://esbuild.github.io/). You can use [web-animations-js](https://github.com/web-animations/web-animations-js), or [polyfill.io](https://polyfill.io/) to create a polyfill. The minimum feature requirement for a polyfill are Maps, Set, WeakMap, Promise, and a WebAnimation polyfill (that supports KeyframeEffect), e.g. [https://cdn.polyfill.io/v3/polyfill.min.js?features=default,es2015,es2018,Array.prototype.includes,Map,WeakMap,Set,Promise,WebAnimations](https://cdn.polyfill.io/v3/polyfill.min.js?features=default,es2015,es2018,Array.prototype.includes,Map,WeakMap,Set,Promise), and [https://cdn.jsdelivr.net/npm/web-animations-js/web-animations-next.min.js](https://cdn.jsdelivr.net/npm/web-animations-js/web-animations-next.min.js). I suggest checking out the [demo](../../build/ts/webanimation-polyfill.ts) to see how I setup the Web Animation Polyfill*
***Warning**: polyfilling may not fix animation format bugs, e.g. [composite animations](https://developer.mozilla.org/en-US/docs/Web/API/KeyframeEffect/composite) don't work on older browsers, so, if you use `polyfill.io` and set it to check if the browser supports the feature before applying the polyfill, your project might encounter errors, as the browser may only have partial support of the Web Animation API.*
***Also Note**: to properly understand `@okikio/animate`, please read up on the [Web Animation API](https://developer.mozilla.org/en-US/docs/Web/API/Element/animate) on MDN.*
You can try out `@okikio/animate` using Gitpod:
[](https://gitpod.io/#https://github.com/okikio/native/blob/beta/packages/animate/README.md)
By default Gitpod will start the dev script for you, but if you need to restart the dev script you can do so by typing into the terminal.
```bash
pnpm demo
```
Once Gitpod has booted up, click on the `@okikio/animate (no-pjax)` button in the preview, then go to [../../build/pug/animate.pug](../../build/pug/animate.pug) and [../../build/ts/animate.ts](../../build/ts/animate.ts) and start tweaking and testing to your hearts content.
You can run `@okikio/animate` locally by first installing some packages via these commands into your terminal,
```bash
npm install -g pnpm && pnpm install -g gulp ultra-runner commitizen && pnpm install && pnpm build
```
and then you can test/demo it using this command,
```bash
pnpm demo
```
You can build your changes/contributions using,
```bash
pnpm build
```
## Table of Contents
- [@okikio/animate](#okikioanimate)
- [Table of Contents](#table-of-contents)
- [Installation](#installation)
- [Demo](#demo)
- [Getting started](#getting-started)
- [API Documentation](#api-documentation)
- [Options](#options)
- [target(s)](#targets)
- [easing](#easing)
- [duration](#duration)
- [delay](#delay)
- [timelineOffset](#timelineoffset)
- [endDelay](#enddelay)
- [padEndDelay](#padenddelay)
- [loop](#loop)
- [onfinish](#onfinish)
- [oncancel](#oncancel)
- [autoplay](#autoplay)
- [direction](#direction)
- [speed](#speed)
- [fillMode](#fillmode)
- [options](#options-1)
- [offset](#offset)
- [timeline](#timeline)
- [keyframes](#keyframes)
- [composite](#composite)
- [extend](#extend)
- [Animations](#animations)
- [Animation Options & CSS Properties as Methods](#animation-options--css-properties-as-methods)
- [Transformable CSS Properties](#transformable-css-properties)
- [Promises and Promise-Like](#promises-and-promise-like)
- [Events](#events)
- [Custom Easing](#custom-easing)
- [SpringEasing](#springeasing)
- [ApplyCustomEasing](#applycustomeasing)
- [DestroyableAnimate](#destroyableanimate)
- [Tweens](#tweens)
- [tween](#tween)
- [tweenAttr](#tweenattr)
- [Effects](#effects)
- [Additional methods & properties](#additional-methods--properties)
- [mainAnimation: Animation](#mainanimation-animation)
- [targetIndexes: WeakMap<Node, number>](#targetindexes-weakmapnode-number)
- [keyframeEffects: WeakMap<HTMLElement, KeyframeEffect>](#keyframeeffects-weakmaphtmlelement-keyframeeffect)
- [computedOptions: WeakMap<HTMLElement, TypeComputedOptions>](#computedoptions-weakmaphtmlelement-typecomputedoptions)
- [animations: WeakMap<KeyframeEffect, Animation>](#animations-weakmapkeyframeeffect-animation)
- [computedKeyframes: WeakMap<HTMLElement, TypeKeyFrameOptionsType> = new WeakMap()](#computedkeyframes-weakmaphtmlelement-typekeyframeoptionstype--new-weakmap)
- [minDelay: number](#mindelay-number)
- [maxSpeed: number](#maxspeed-number)
- [on(...), off(...), and emit(...)](#on-off-and-emit)
- [is(playstate: TypePlayStates)](#isplaystate-typeplaystates)
- [play(), pause(), reverse(), and reset()](#play-pause-reverse-and-reset)
- [The long list of Get Methods](#the-long-list-of-get-methods)
- [`getAnimation(target: HTMLElement): Animation`](#getanimationtarget-htmlelement-animation)
- [`getTiming(value: HTMLElement | Animation): EffectTiming | AnimationOptions`](#gettimingvalue-htmlelement--animation-effecttiming--animationoptions)
- [`getCurrentTime(): number`](#getcurrenttime-number)
- [`getProgress(): number`](#getprogress-number)
- [`getPlayState(): string`](#getplaystate-string)
- [`getSpeed(): number`](#getspeed-number)
- [The almost as long list of Set Methods; these methods are chainable](#the-almost-as-long-list-of-set-methods-these-methods-are-chainable)
- [`setCurrentTime(time: number)`](#setcurrenttimetime-number)
- [`setProgress(percent: number)`](#setprogresspercent-number)
- [`setSpeed(speed: number = 1)`](#setspeedspeed-number--1)
- [then(...), catch(...), and finally(...)](#then-catch-and-finally)
- [toJSON()](#tojson)
- [all(method: (animation: Animation, target?: HTMLElement) => void)](#allmethod-animation-animation-target-htmlelement--void)
- [finish()](#finish)
- [cancel()](#cancel)
- [stop()](#stop)
- [Pause Animation when Page is out of Focus](#pause-animation-when-page-is-out-of-focus)
- [Examples](#examples)
- [Browser support](#browser-support)
- [CSS & SVG Animations Support](#css--svg-animations-support)
- [Content delivery networks](#content-delivery-networks)
- [Memory Management](#memory-management)
- [Best practices (these are from Animate Plus, but they are true for all Animation libraries)](#best-practices-these-are-from-animate-plus-but-they-are-true-for-all-animation-libraries)
- [Contributing](#contributing)
- [Licence](#licence)
## Installation
You can install [@okikio/animate](https://www.skypack.dev/view/@okikio/animate) from [npm](https://www.npmjs.com/package/@okikio/animate) via `npm i @okikio/animate`, `pnpm i @okikio/animate` or `yarn add @okikio/animate`.
You can use `@okikio/animate` on the web via
- [https://unpkg.com/@okikio/animate/lib/api.es.js](https://unpkg.com/@okikio/animate/lib/api.es.js),
- [https://cdn.skypack.dev/@okikio/animate](https://cdn.skypack.dev/@okikio/animate) or
- [https://cdn.jsdelivr.net/npm/@okikio/animate/lib/api.es.js](https://cdn.jsdelivr.net/npm/@okikio/animate/lib/api.es.js).
Once installed it can be used like this:
```ts
import { animate } from "@okikio/animate";
import { animate } from "https://unpkg.com/@okikio/animate/lib/api.es.js";
import { animate } from "https://cdn.jsdelivr.net/npm/@okikio/animate/lib/api.es.js";
// Or
import { animate } from "https://cdn.skypack.dev/@okikio/animate";
// Via script tag
<script src="https://unpkg.com/@okikio/animate/lib/api.js"></script>
// Do note, on the web you need to do this, if you installed it via the script tag:
const animate = window.animate.default;
// or
const { default: animate } = window.animate;
// or
const { default: anime } = window.animate; // LOL
```
## Demo
I built a small demo showing off the abilities of the `@okikio/animate` library. You can find the files for the demo in [./build](https://github.com/okikio/native/tree/beta/build) folder. For more info on how to use the demo go to [okikio/native#usage](https://github.com/okikio/native/blob/beta/README.md#usage) on Github.
I recommend using the Gitpod link at the top of the page to get started with development, as it removes the need for setup.
> [Click to view demo →](https://okikio.github.io/native/demo/animate)
## Getting started
`@okikio/animate` create animations by creating instances of `Animate`, a class that acts as a wrapper around the Web Animation API. To create new instances of the `Animate` class, you can either import the `Animate` class and do this, `new Animate({ ... })` or import the `animate` (lowercase) method and do this, `animate({ ... })`. The `animate` method creates new instances of the `Animate` class and passes the options it recieves as arguments to the `Animate` class.
The `Animate` class recieves a set of targets to animate, it then creates a list of Web Animation API `Animation` instances, along side a main animation, which is small `Animation` instance that is set to animate the opacity of a non visible element, the `Animate` class then plays each `Animation` instances keyframes including the main animation.
The main animation is there to ensure accuracy in different browser vendor implementation of the Web Animation API. The main animation is stored in `Animate.prototype.mainAnimation: Animation`, the other `Animation` instances are stored in a `WeakMap` `Animate.prototype.animations: WeakMap<KeyframeEffect, Animation>`.
```ts
import animate from "@okikio/animate";
// Do note, on the web you need to do this, if you installed it via the script tag:
// const { animate } = window.animate;
(async () => {
let [options] = await animate({
target: ".div",
/* NOTE: If you turn this on you have to comment out the transform property. The keyframes property is a different format for animation you cannot you both styles of formatting in the same animation */
// keyframes: [
// { transform: "translateX(0px)" },
// { transform: "translateX(300px)" }
// ],
transform: ["translateX(0px)", "translateX(300px)"],
easing: "out",
duration(i) {
return (i + 1) * 500;
},
loop: 1,
speed: 2,
fillMode: "both",
direction: "normal",
autoplay: true,
delay(i) {
return (i + 1) * 100;
},
endDelay(i) {
return (i + 1) * 100;
},
});
animate({
options,
transform: ["translateX(300px)", "translateX(0px)"],
});
})();
// or you can use the .then() method
animate({
target: ".div",
// NOTE: If you turn this on you have to comment out the transform property. The keyframes property is a different format for animation you cannot you both styles of formatting in the same animation
// keyframes: [
// { transform: "translateX(0px)" },
// { transform: "translateX(300px)" }
// ],
transform: ["translateX(0px)", "translateX(300px)"],
easing: "out",
duration(i) {
return (i + 1) * 500;
},
loop: 1,
speed: 2,
fillMode: "both",
direction: "normal",
delay(i) {
return (i + 1) * 100;
},
autoplay: true,
endDelay(i) {
return (i + 1) * 100;
}
}).then((options) => {
animate({
options,
transform: ["translateX(300px)", "translateX(0px)"]
});
});
```
[Preview this example →](https://codepen.io/okikio/pen/mdPwNbJ?editors=0010)
## API Documentation
Not all available methods and properties are listed here (otherwise this README would be too long), so go through the [API documentation](https://okikio.github.io/native/docs/modules/_okikio_animate.html) for the full documented API.
## Options
Animation options control how an animation is produced, it shouldn't be too different for those who have used `animejs`, or `jquery`'s animate method.
An animation option is an object with keys and values that are computed and passed to the `Animate` class to create animations that match the specified options.
The default options are:
```ts
export const DefaultAnimationOptions: AnimationOptions = {
keyframes: [],
loop: 1, // iterations: number,
delay: 0,
speed: 1,
endDelay: 0,
easing: "ease",
autoplay: true,
duration: 1000,
fillMode: "auto",
direction: "normal",
padEndDelay: true,
extend: {}
};
```
### target(s)
| Default | Type |
| :---------- | :--------------------------------------------------------------------------------------------------- |
| `undefined` | AnimationTarget = string \| Node \| NodeList \| HTMLCollection \| HTMLElement[] \| AnimationTarget[] |
Determines the DOM elements to animate. You can pass it a CSS selector, DOM elements, or an Array of DOM Elements and/or CSS Selectors.
```ts
animate({
target: document.body.children, // You can use either `target` or `targets` for your animations
// or
// target: ".div",
// target: document.querySelectorAll(".el"),
// target: [document.querySelectorAll(".el"), ".div"]",
// targets: [document.querySelectorAll(".el"), ".div"]",
transform: ["rotate(0turn)", "rotate(1turn)"],
});
```
### easing
| Default | Type |
| :------ | :------------------------------------------------------------------------------------------------------- |
| `ease` | String \| [TypeCallback](https://okikio.github.io/native/docs/modules/_okikio_animate.html#typecallback) |
Determines the acceleration curve of your animation.
Based on the easings of [easings.net](https://easings.net)
| constant | accelerate | decelerate | accelerate-decelerate |
| :--------- | :----------- | :------------- | :-------------------- |
| linear | ease-in / in | ease-out / out | ease-in-out / in-out |
| ease | in-sine | out-sine | in-out-sine |
| steps | in-quad | out-quad | in-out-quad |
| step-start | in-cubic | out-cubic | in-out-cubic |
| step-end | in-quart | out-quart | in-out-quart |
| | in-quint | out-quint | in-out-quint |
| | in-expo | out-expo | in-out-expo |
| | in-circ | out-circ | in-out-circ |
| | in-back | out-back | in-out-back |
You can create your own custom cubic-bezier easing curves. Similar to css you type `cubic-bezier(...)` with 4 numbers representing the shape of the bezier curve, for example, `cubic-bezier(0.47, 0, 0.745, 0.715)` this is the bezier curve for `in-sine`.
_**Note**: the `easing` property supports the original values and functions for easing as well, for example, `steps(1)`, and etc... are supported._
_**Note**: you can also use camelCase when defining easing functions, e.g. `inOutCubic` to represent `in-out-cubic`_
Yay, [Custom Easing](#custom-easing) are now supported, they have limitations, but those shouldn't affect too much.
```ts
// cubic-bezier easing
animate({
target: ".div",
easing: "cubic-bezier(0.47, 0, 0.745, 0.715)",
/* or */
// easing: "in-sine",
/* or */
// easing: "inSine",
transform: ["translate(0px)", "translate(500px)"],
});
```
As of right now these are the limits of easing, but a couple standards are in discussions right now, so cross your fingers and hope they are standardized soon.
Here are some standards in discussion:
- [Easing Worklet](https://github.com/jakearchibald/easing-worklet)
- [Animation Worklet](https://drafts.css-houdini.org/css-animation-worklet-1/#example-1)
- [CSS Defined Easing](https://github.com/w3c/csswg-drafts/issues/229)
### duration
| Default | Type |
| :------ | :----------------------------------------------------------------------------------------------------------------- |
| `1000` | Number \| String \| [TypeCallback](https://okikio.github.io/native/docs/modules/_okikio_animate.html#typecallback) |
Determines the duration of your animation in milliseconds. By passing it a callback, you can define a different duration for each element. The callback takes the index of each element, the target dom element, and the total number of target elements as its argument and returns a number.
```ts
// First element fades out in 1s, second element in 2s, etc.
animate({
target: ".div",
easing: "linear",
duration: 1000,
// or
duration: (index) => (index + 1) * 1000,
opacity: [1, 0],
});
```
### delay
| Default | Type |
| :------ | :------------------------------------------------------------------------------------------------------- |
| `0` | Number \| [TypeCallback](https://okikio.github.io/native/docs/modules/_okikio_animate.html#typecallback) |
Determines the delay of your animation in milliseconds. By passing it a callback, you can define a different delay for each element. The callback takes the index of each element, the target dom element, and the total number of target elements as its argument and returns a number.
```ts
// First element starts fading out after 1s, second element after 2s, etc.
animate({
target: ".div",
easing: "linear",
delay: 5,
// or
delay: (index) => (index + 1) * 1000,
opacity: [1, 0],
});
```
### timelineOffset
| Default | Type |
| :------ | :------------------------------------------------------------------------------------------------------- |
| `0` | Number \| [TypeCallback](https://okikio.github.io/native/docs/modules/_okikio_animate.html#typecallback) |
Adds an offset amount to the `delay` option, for creating a timeline similar to `animejs`.
I don't intend to create a `timeline` function for this library but if you wish to please try your hands at creating one, if it's small, light-weight, and there is a need I might incorperate it.
### endDelay
| Default | Type |
| :------ | :------------------------------------------------------------------------------------------------------- |
| `0` | Number \| [TypeCallback](https://okikio.github.io/native/docs/modules/_okikio_animate.html#typecallback) |
Similar to delay but it indicates the number of milliseconds to delay **after** the full animation has played **not before**.
_**Note**: `endDelay` will delay the `onfinish` method and event, but will not reserve the finished state of the CSS animation, if you need to use `endDelay` you may need to use the `fillMode` property to reserve the changes to the animation target._
```ts
// First element fades out but then after 1s finishes, the second element after 2s, etc.
animate({
target: ".div",
easing: "linear",
endDelay: 1000,
// or
endDelay: (index) => (index + 1) * 1000,
opacity: [1, 0],
});
```
### padEndDelay
| Default | Type |
| :------ | :------ |
| `false` | Boolean |
This ensures all `animations` match up to the total duration, and don't finish too early, if animations finish too early when the `.play()` method is called specific animations that are finished will restart while the rest of the animations will continue playing.
_**Note**: you cannot use the `padEndDelay` option and set a value for `endDelay`, the `endDelay` value will replace the padded endDelay, `padEndDelay` is also ignored if `loop` is `true` or is set to `infinity`._
When creating progress/seek bars this needs to be enabled for the animation to function properly.
### loop
| Default | Type |
| :------ | :------------------------------------------------------------------------------------------------------------------ |
| `1` | Boolean \| Number \| [TypeCallback](https://okikio.github.io/native/docs/modules/_okikio_animate.html#typecallback) |
Determines if the animation should repeat, and how many times it should repeat.
```ts
// Loop forever
animate({
target: ".div",
easing: "linear",
loop: true, // Using `loop: Infinity,` would also have worked
// or
loop: 5, // If you want the animation to loop 5 times
opacity: [1, 0],
});
```
### onfinish
| Default | Type |
| :--------------------------------------------------------------------------------- | :------- |
| `(element: HTMLElement, index: number, total: number, animation: Animation) => {}` | Function |
Occurs when the animation for one of the targets completes, meaning when animating many targets that finish at different times this will run multiple times. The arguments it takes is slightly different from the rest of the animation options.
The animation argument represents the animation for the current target.
**Warning**: the order of the callback's arguments are in a different order, with the target element first, and the index second.
```ts
// Avoid using fillMode, use this instead to commit style changes
// Do note endDelay delays the onfinish method
animate({
target: ".div",
opacity: [0, 1],
/**
* @param {HTMLElement} element - the current target element
* @param {number} index - the index of the current target element in `Animate.prototype.targets`
* @param {number} total - the total number of target elements
* @param {Animation} animation - the animation of the current target element
*/
// Note the order of the arguments -- it's different from other properties
onfinish(element, index, total, animation) {
element.style.opacity = 0;
console.log(
`${
index + 1
} out of ${total}, elements have finished their animations. Animation playback speed is ${animation.playbackRate}`
);
},
});
```
### oncancel
| Default | Type |
| :--------------------------------------------------------------------------------- | :------- |
| `(element: HTMLElement, index: number, total: number, animation: Animation) => {}` | Function |
Occurs when the animation for one of the targets is cancelled, meaning when animating many elements that are cancelled at different times this will run multiple times. The arguments it takes is slightly different from the rest of the animation options.
The animation argument represents the animation for the current element.
**Warning**: the order of the callback's arguments are in a different order, with the target element first, and the index second.
```ts
// Avoid using fillMode, use this instead to commit style changes
// Do note endDelay delays the onfinish method
animate({
target: ".div",
opacity: [0, 1],
/**
* @param element - the current target element
* @param index - the index of the current target element in `Animate.prototype.targets`
* @param total - the total number of target elements
* @param animation - the animation of the current target element
*/
// Note the order of the arguments -- it's different from other properties
oncacel(element, index, total, animation) {
console.log(
`${
index + 1
} out of ${total}, elements have been cancelled. Animation playback speed is ${animation.playbackRate}, the target elements id attribute is ${element.id}`
);
},
});
```
### autoplay
| Default | Type |
| :------ | :------ |
| `true` | Boolean |
Determines if the animation should automatically play, immediately after being instantiated.
### direction
| Default | Type |
| :------- | :------------------------------------------------------------------------------------------------------- |
| `normal` | String \| [TypeCallback](https://okikio.github.io/native/docs/modules/_okikio_animate.html#typecallback) |
Determines the direction of the animation, the directions available are:
- `reverse` runs the animation backwards,
- `alternate` switches direction after each iteration if the animation loops.
- `alternate-reverse` starts the animation at what would be the end of the animation if the direction were
- `normal` but then when the animation reaches the beginning of the animation it alternates going back to the position it started at.
### speed
| Default | Type |
| :------ | :------------------------------------------------------------------------------------------------------- |
| `1` | Number \| [TypeCallback](https://okikio.github.io/native/docs/modules/_okikio_animate.html#typecallback) |
Determines the animation playback rate. Useful in the authoring process to speed up some parts of a
long sequence (value above 1) or slow down a specific animation to observe it (value between 0 to 1),
_**Note**: negative numbers reverse the animation._
### fillMode
| Default | Type |
| :------ | :------------------------------------------------------------------------------------------------------- |
| `auto` | String \| [TypeCallback](https://okikio.github.io/native/docs/modules/_okikio_animate.html#typecallback) |
_Be careful when using fillMode, it has some problems when it comes to concurrency of animations read more on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/EffectTiming/fill), if browser support were better I would remove fillMode and use Animation.commitStyles, I'll have to change the way `fillMode` functions later. Use the onfinish method to commit styles [onfinish](#onfinish)._
Defines how an element should look after the animation. The fillModes availble are:
- `none` means the animation's effects are only visible while the animation is playing.
- `forwards` the affected element will continue to be rendered in the state of the final animation frame.
- `backwards` the animation's effects should be reflected by the element(s) state prior to playing.
- `both` combining the effects of both forwards and backwards; The animation's effects should be reflected by the element(s) state prior to playing and retained after the animation has completed playing.
- `auto` if the animation effect fill mode is being applied to is a keyframe effect. "auto" is equivalent to "none". Otherwise, the result is "both".
You can learn more here on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/EffectTiming/fill).
### options
| Default | Type |
| :------ | :------------------------------------------------------------------------------------------------------------------------------------------- |
| `{}` | [IAnimationOptions](https://okikio.github.io/native/docs/interfaces/_okikio_animate.ianimationoptions.html) = Object \| Animate \| Animate[] |
Another way to input options for an animation, it's also used to chain animations.
The `options` animation option is another way to declare options, it can take an instance of `Animate`, a single `Animate` instance in an Array, e.g. `[Animate]` or an object containing animation options.
`options` extends the animation properties of an animation, but more importance is given to the actual animation options object, so, the properties from `options` will be ignored if there is already an animation option with the same name declared.
_**Note**: you can't use this property as a method._
```ts
(async () => {
// animate is Promise-like, as in it has a then() method like a Promise but it isn't a Promise.
// animate resolves to an Array that contains the Animate instance, e.g. [Animate]
let [options] = await animate({
target: ".div",
opacity: [0, 1],
});
animate({
options,
// opacity overrides the opacity property from `options`
opacity: [1, 0],
});
console.log(options); //= Animate
})();
// or
(async () => {
let options = await animate({
target: ".div",
opacity: [0, 1],
duration: 2000,
});
// Remeber, the `options` animation option can handle Arrays with an Animate instance, e.g. [Animate]
// Also, remeber that Animate resolves to an Arrays with an Animate instance, e.g. [Animate]
// Note: the `options` animation option can only handle one Animate instance in an Array and that is alway the first element in the Array
animate({
options,
opacity: [1, 0],
});
console.log(options); //= [Animate]
})();
// or
(async () => {
let options = animate({
target: ".div",
opacity: [0, 1],
});
await options;
animate({
options,
// opacity overrides the opacity property from `options`
opacity: [1, 0],
});
console.log(options); //= Animate
})();
// or
(async () => {
let options = {
target: ".div",
opacity: [0, 1],
};
await animate(options);
animate({
options,
opacity: [1, 0],
});
console.log(options); //= { ... }
})();
```
### offset
| Default | Type |
| :---------- | :--------------------------------------------------------------------------------------------------------------------- |
| `undefined` | (Number \| String)[] \| [TypeCallback](https://okikio.github.io/native/docs/modules/_okikio_animate.html#typecallback) |
Controls the starting point of certain parts of an animation.
The offset of the keyframe specified as a number between `0.0` and `1.0` inclusive or null. This is equivalent to specifying start and end states in percentages in CSS stylesheets using @keyframes. If this value is null or missing, the keyframe will be evenly spaced between adjacent keyframes.
Read more on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API/Keyframe_Formats)
```ts
animate({
duration: 2000,
opacity: [ 0, 0.9, 1 ],
easing: [ 'ease-in', 'ease-out' ],
offset: [ "from", 0.8 ], // Shorthand for [ 0, 0.8, 1 ]
// or
offset: [ 0, "80%", "to" ], // Shorthand for [ 0, 0.8, 1 ]
// or
offset: [ "0", "0.8", "to" ], // Shorthand for [ 0, 0.8, 1 ]
});
```
### timeline
| Default | Type |
| :------------------------------------------------------------------------------------ | :---------------- |
| [DocumentTimeline](https://developer.mozilla.org/en-US/docs/Web/API/DocumentTimeline) | AnimationTimeline |
Represents the timeline of animation. It exists to pass timeline features to Animations.
As of right now it doesn't contain any features but in the future when other timelines like the [ScrollTimeline](https://drafts.csswg.org/scroll-animations-1/#scrolltimeline), read the Google Developer article for [examples and demos of ScrollTimeLine](https://developers.google.com/web/updates/2018/10/animation-worklet#hooking_into_the_space-time_continuum_scrolltimeline)
_**Note**: timeline cannot be a callback/function_
### keyframes
| Default | Type |
| :------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `[]` | TypeCSSLikeKeyframe \| ICSSComputedTransformableProperties[] & Keyframe[] \| object[] \| [TypeCallback](https://okikio.github.io/native/docs/modules/_okikio_animate.html#typecallback) |
I highly suggest going through the API documentation to better understand [keyframes](https://okikio.github.io/native/docs/interfaces/_okikio_animate.ianimationoptions.html#keyframes).
Allows you to manually set keyframes using a `keyframe` array
Read more on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/KeyframeEffect/setKeyframes).
An `array` of objects (keyframes) consisting of properties and values to iterate over. This is the canonical format returned by the [getKeyframes()](https://developer.mozilla.org/en-US/docs/Web/API/KeyframeEffect/getKeyframes) method.
`@okikio/animate` also offers another format called `CSSLikeKeyframe`, read more about [KeyframeParse](https://okikio.github.io/native/docs/modules/_okikio_native.html#keyframeparse)
It basically functions the same way CSS's `@keyframe` works.
_**Note**: the order of `transform` functions in CSS Property form...matter, meanwhile in keyframes the transform order doesn't, keep this in mind when you are try to create complex rotation based animations or other complex animations in general._
```ts
animate({
keyframes: {
"from, 50%, to": {
opacity: 1
},
"25%, 0.7": {
opacity: 0
}
}
})
// Results in a keyframe array like this
//= [
//= { opacity: 1, offset: 0 },
//= { opacity: 0, offset: 0.25 },
//= { opacity: 1, offset: 0.5 },
//= { opacity: 0, offset: 0.7 },
//= { opacity: 1, offset: 1 }
//= ]
```
### composite
The `composite` property of a `KeyframeEffect` resolves how an element's animation impacts its underlying property values.
To understand these values, take the example of a `keyframeEffect` value of `blur(2)` working on an underlying property value of `blur(3)`.
- `replace` - The keyframeEffect overrides the underlying value it is combined with: `blur(2)` replaces `blur(3)`.
- `add` - The keyframeEffect is added to the underlying value with which it is combined (aka additive): `blur(2) blur(3)`.
- `accumulate` - The keyframeEffect is accumulated on to the underlying value: `blur(5)`.
Read more on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/KeyframeEffect/composite)
I recommend reading [web.dev](https://web.dev)'s article on [web-animations](https://web.dev/web-animations/#smoother-animations-with-composite-modes).
### extend
| Default | Type |
| :------ | :---------------------------------------------------------------------------------------------- |
| `{}` | [KeyframeEffectOptions](https://developer.mozilla.org/en-US/docs/Web/API/KeyframeEffectOptions) |
The properties of the `extend` animation option are computed and can use [TypeCallback](https://okikio.github.io/native/docs/modules/_okikio_animate.html#typecallback), they are a way to access features that haven't been implemented in `@okikio/animate`, for example, `iterationStart`.
`extend` is supposed to future proof the library if new features are added to the Web Animation API that you want to use, but that has not been implemented yet.
_**Note**: it doesn't allow for declaring actual animation keyframes; it's just for animation timing options, and it overrides all other animation timing options that accomplish the same goal, e.g. `loop` & `iterations`, if `iterations` is a property of `extend` then `iterations` will override `loop`._
_**Warning**: `extend` itself **cannont** be computed, so, it doesn't support TypeCallback._
```ts
animate({
target: ".div",
opacity: [0, 1],
loop: 5,
extend: {
iterationStart: 0.5,
// etc...
fill: "both", // This overrides fillMode
iteration: 2, // This overrides loop
composite: "add"
}
});
```
## Animations
`@okikio/animate` lets you animate HTML and SVG elements with any property that takes numeric values, including hexadecimal colors.
The `@okikio/animate` module contains a class that controls animatations called `Animate`. To create new instances of the `Animate` class I created the `animate()` function.
```ts
// Animate the fill color of an SVG circle
animate({
target: "circle",
fill: ["#80f", "#fc0"],
});
```
Each property you animate needs an array defining the start and end values, or an Array of keyframes.
```ts
animate({
target: ".div",
transform: ["translate(0px)", "translate(1000px)"],
});
// Or
// Same as ["translate(0px)", "translate(100px)"]
animate({
target: ".div",
keyframes: [
{ transform: "translate(0px)" },
{ transform: "translate(100px)" },
],
});
```
*Note: you can only use one of these formats for an animation, and if `Animate` sees the `keyframes` property, it ignores all other css properties, in situations where `Animate` sees the keyframes property it will still accept animation properties like `easing`, `duration`, etc...*
These arrays can optionally be returned by a callback that takes the index of each element, the total number of elements, and each specific element, just like with other properties.
```ts
// First element translates by 100px, second element by 200px, etc.
animate({
target: ".div",
transform(index) {
return ["translate(0px)", `translate(${(index + 1) * 100}px)`];
},
});
// Or
// Same as above
animate({
target: ".div",
keyframes(index) {
return [
{ transform: "translate(0px)" },
{ transform: `translate(${(index + 1) * 100}px)` },
];
},
});
```
## Animation Options & CSS Properties as Methods
All options & properties except `target`, `targets`, `autoplay`, `extend`, `onfinish`, and `options` can be represented by a method with the arguments `(index: number, total: number, element: HTMLElement)`.
*Note: the `keyframes` option **can** be a method*.
```ts
/**
* @param {number} [index] - index of each element
* @param {number} [total] - total number of elements
* @param {HTMLElement} [element] - the target element
* @returns any
*/
// For example
animate({
target: ".div",
opacity(index, total, element) {
console.log(element);
return [0, (index + 1) / total];
},
duration(index, total) {
return 200 + (500 * (index + 1) / total);
}
});
```
## Transformable CSS Properties
I added the ability to use single value unitless numbers, strings, and arrays, as well as added the transform functions as CSS properties.
Single value means,
```ts
animate({
opacity: 0.5, // This will turn into `["5"]`, notice no units, this could also be a string
translateX: 250, // This will turn into `["250px"]`, notice how it adds units, this could also be a string
rotate: 360, // This will turn into `["360deg"]`, notice how it adds units, this could also be a string
skew: "1.25turn", // This will turn into `["1.25turn"]`, notice how it doesn't add "deg" as the units
left: 50, // This is actually an error, this will turn into `["50"]`, notice no units, this could also be a string. Only transform properties support automatic units
})
```
This is in preperation for [Implicit to/from keyframes](https://developer.mozilla.org/en-US/docs/Web/API/Element/animate#implicit_tofrom_keyframes)
Removes the need for the full transform statement in order to use translate, rotate, scale, skew, or perspective including their X, Y, Z, and 3d varients. Plus adds automatic units to the transform CSS properties.
Also, adds the ability to use single string or number values for transform functions.
_**Note**: the `transform` animation option will override all transform CSS properties_
_**Note**: dash & camel case are supported as CSS property names, this also includes transforms, so, you can use `translate-x` or `translateX`, when setting a CSS property_
_**Note**: all other features will work with Transformable CSS Properties, this includes the `keyframes` animation options and `animation options` as callbacks_
_**Note**: the order of `transform` functions in CSS Property form...matter, meanwhile in keyframes the transform order doesn't, keep this in mind when you are try to create complex rotation based animations or other complex animations in general._
_**Warning**: only the transform function properties and CSS properties with the keys ["margin", "padding", "size", "width", "height", "left", "right", "top", "bottom", "radius", ,"gap", "basis", "inset", "outline-offset", "perspective", "thickness", "position", "distance", "spacing", "rotate"] will get automatic units. It will also work with multiple unit CSS properties like "margin", "padding", and "inset", etc..., however, no automatic units will be applied to any CSS properties that can accept color, this is to avoid unforseen bugs_
Read more about the [ParseTransformableCSSProperties](https://okikio.github.io/native/docs/modules/_okikio_animate.html#parsetransformablecssproperties) method.
Also, read about the [ParseTransformableCSSKeyframes](https://okikio.github.io/native/docs/modules/_okikio_animate.html#parsetransformablecsskeyframes) method.
Check out an example on [Codepen](https://codepen.io/okikio/pen/qBrNXoY?editors=0110)
```ts
animate({
// ...
/*
keyframes(index) {
return [
{ translateX: 0 },
{ translateX: (index + 1) * 250 }
];
},
// or
translateX(index) {
return `0, ${(index + 1) * 250}`;
},
*/
// It will automatically add the "px" units for you, or you can write a string with the units you want
translate3d: [
"25, 35, 60%",
[50, "60px", 70],
["70", 50]
],
translate: "25, 35, 60%",
translateX: [50, "60px", "70"],
translateY: ["50, 60", "60"], // Note: this will actually result in an error, make sure to pay attention to where you are putting strings and commas
translateZ: 0,
perspective: 0,
opacity: "0, 5",
scale: [
[1, "2"],
["2", 1]
],
rotate3d: [
[1, 2, 5, "3deg"], // The last value in the array must be a string with units for rotate3d
[2, "4", 6, "45turn"],
["2", "4", "6", "-1rad"]
],
opacity: [0, 1],
"border-left": 5
})
//= {
//= transform: [
//= // `translateY(50, 60)` will actually result in an error
//= 'translate(25px) translate3d(25px, 35px, 60%) translateX(50px) translateY(50, 60) translateZ(0px) rotate3d(1, 2, 5, 3deg) scale(1, 2) perspective(0px)',
//= 'translate(35px) translate3d(50px, 60px, 70px) ranslateX(60px) translateY(60px) rotate3d(2, 4, 6, 45turn) scale(2, 1)',
//= 'translate(60%) translate3d(70px, 50px) translateX(70px) rotate3d(2, 4, 6, -1rad)'
//= ],
//= opacity: [ '0', '5' ],
//= borderLeft: ["5px"]
//= }
```
## Promises and Promise-Like
`animate()` is promise-like meaning it has `then`, `catch`, and `finally` methods, but `Animate` itself isn't a Promise (this is important to keep in mind when dealing with async/await asynchronous animations). `Animate`'s `then` resolves once all animations are complete. The promise resolves to an Array with the `Animate` instance being the only element, but the `options` animation option can use the options of another `Animate` instance allowing animation chaining to be straightforward and convenient. The [Getting started](#getting-started) section gives a basic example.
Since `Animate` relies on native promises, you can benefit from all the usual features promises
provide, such as `Promise.all`, `Promise.race`, and especially `async/await` which makes animation timelines easy to set up.
> *An interesting quirk of Promises is that even though `Animate` is not a Promise, async/await still work with it because it has a `then`, and `catch`.*
*Warning: `then`, `catch`, and `finally` are not resetable, however, the `Animate.prototype.on("finish", ...)` event is, meaning if you reset an animation while then using `then`, `catch`, and `finally`, they will **not** fire again after the reset.*
For example,
```ts
animate({
target: ".div",
duration: 3000,
transform: ["translateY(-100vh)", "translateY(0vh)"],
});
// This will only run once
/*
Note that the AnimateInstance is in an Array when it is resolved,
this is due to Promises not wanting to resolve references,
so, you can't resolve the `this` keyword directly.
I chose to resolve `this` in an Array as it seemed like the best alternative
*/
.then(( [AnimateInstance] ) => {
console.log(`${getProgress()}`%);
AnimateInstance.reset();
});
// or
(async () => {
const [AnimateInstance] = await animate({
target: ".div",
duration: 3000,
transform: ["translateY(-100vh)", "translateY(0vh)"],
});
await animate({
options: AnimateInstance,
transform: ["rotate(0turn)", "rotate(1turn)"],
});
await animate({
options: AnimateInstance,
duration: 800,
easing: "in-quint",
transform: ["scale(1)", "scale(0)"],
});
})();
```
## Events
There are `8` events in total, they are:
- "update"
- "play"
- "pause"
- "begin"
- "finish"
- "cancel"
- "error"
- "stop"
- "playstate-change"
```ts
/*
The update event is triggered continously while an animation is playing, it does this by calling requestAnimationFrame.
By default, whenever an animation is played, the current Animate instance is added to `Animate.RUNNING` WeakSet and
a requests for an animation frame is called, where it checks if there are any listeners for the "update" event.
If there are none, the Animate instance is removed from the `Animate.RUNNING` WeakSet.
If there are listeners the Animate instances loop method is called, and the "update" event is emitted every frame until,
the Animation is done, where it stops
If there are no Animate instances in the `Animate.RUNNING` WeakSet, the requestAnimationFrame loop, is stopped,
until there are other Animate instances that are played
*/
....on("update", (progress, instance) => {
/**
* @param {number} progress - it is the animation progress from 0 to 100
* @param {Animate} instance - it is the instance of Animate the update event is triggered from
*/
});
// The play & pause events are triggered when the Animate.prototype.play() or .pause() methods are called.
// The "playstate-change" event occurs when the playstate changes, so, when the animation is played, paused, cancelled, or finished
....on("play" | "pause" | "playstate-change", (playstate, instance) => {
/**
* @param {"idle" | "running" | "paused" | "finished"} playstate - it is the animations play state
* @param {Animate} instance - it is the instance of Animate the event is triggered from
*/
});
// The begin, finish, and cancel events are triggered when all animations in an instance of Animate begin, finish or are cancelled.
// The begin event occurs at the begining of all animations and its when the the Animate instance has started, while the finish event is when all animations (taking into account the endDelay and loops as well) have ended.
// Note: By the time events are registered the animation would have started and there wouldn't have be a `begin` event listener to actually emit, so,
// the `begin` event emitter is wrapped in a setTimeout of 0ms so that the event can be defered; by the end of the timeout the rest of the js to run,
// the `begin` event to be registered thus the `begin` event can be emitter
// The cancel event occurs when the mainElement animation is cancelled or the `.cancel()` method is called
....on("begin" | "finish" | "cancel", (instance) => {
/**
* @param {Animate}