crossani
Advanced tools
Comparing version 1.0.1 to 1.1.0
@@ -20,7 +20,10 @@ (() => { | ||
}; | ||
var generateTransition = (prevState, transition) => distinct([...Object.keys(transition.state), ...Object.keys(prevState)]).map((p) => `${p} ${transition.ms}ms ${transition.easing}`).join(","); | ||
var generateTransition = (prevState, transition) => distinct([ | ||
...Object.keys(transition.state ?? {}), | ||
...Object.keys(prevState.curr) | ||
]).map((p) => `${p} ${transition.ms ?? prevState.lastMs}ms ${transition.easing ?? prevState.lastEase}`).join(","); | ||
var distinct = (arr) => Array.from(new Set(arr)); | ||
// src/shared.ts | ||
var stateStore = /* @__PURE__ */ new WeakMap(); | ||
var stateStore = /* @__PURE__ */ new Map(); | ||
@@ -41,3 +44,5 @@ // src/util.ts | ||
queue: [], | ||
transitionPromises: [] | ||
transitionPromises: [], | ||
lastEase: EASE.ease, | ||
lastMs: 100 | ||
}; | ||
@@ -59,5 +64,4 @@ sanitiseStyleObject(newState.orig); | ||
state.transitionPromises.push([transition, promise, () => resolve()]); | ||
return state.queue.length === 1; | ||
return [state.queue.length === 1, promise]; | ||
} | ||
var getPromise = (elem, transition) => getOrInitStore(elem).transitionPromises.find((t) => t[0] === transition)?.[1] ?? Promise.reject("promise was missing from state"); | ||
function sanitiseStyleObject(obj) { | ||
@@ -71,6 +75,6 @@ delete obj.transition; | ||
function sanitiseTransitions(elem) { | ||
if (elem.transitions === void 0) | ||
if (!elem.transitions) | ||
return; | ||
for (const transition of Object.values(elem.transitions)) { | ||
if (!transition) | ||
if (!transition?.state) | ||
continue; | ||
@@ -109,3 +113,3 @@ sanitiseStyleObject(transition.state); | ||
if (transition.reset) | ||
state.curr = transition.state; | ||
state.curr = transition.state ?? {}; | ||
else | ||
@@ -127,3 +131,3 @@ Object.assign(state.curr, transition.state); | ||
return; | ||
const transitionString = generateTransition(state.curr, transition); | ||
const transitionString = generateTransition(state, transition); | ||
if (transition.reset) | ||
@@ -133,2 +137,4 @@ state.curr = { ...transition.state }; | ||
Object.assign(state.curr, transition.state); | ||
state.lastMs = transition.ms ?? state.lastMs; | ||
state.lastEase = transition.easing ?? state.lastEase; | ||
elem.style.transition = transitionString; | ||
@@ -140,32 +146,43 @@ updateStyles(elem); | ||
startAnimating(elem); | ||
}, transition.ms + 20); | ||
}, state.lastMs + 20); | ||
} | ||
// src/index.ts | ||
function load() { | ||
HTMLElement.prototype.doTransition = function(name) { | ||
if (!this.transitions) | ||
throw new Error(`${this.tagName} #${this.id} does not have transitions`); | ||
sanitiseTransitions(this); | ||
const transition = this.transitions[name]; | ||
if (!transition) | ||
throw new Error(`${this.tagName} #${this.id} has no transition "${name}"`); | ||
if (transition.cutOff) { | ||
abortAnimation(this); | ||
popAll(this); | ||
} | ||
const notAnimating = queueTransition(this, transition); | ||
if (notAnimating) { | ||
if (!transition.cutOff) | ||
startAnimating(this); | ||
else | ||
whenTransitionAborts(this, () => startAnimating(this)); | ||
} | ||
return getPromise(this, transition); | ||
}; | ||
} | ||
var unload = () => delete HTMLElement.prototype.doTransition; | ||
SVGElement.prototype.doTransition = HTMLElement.prototype.doTransition = function(transOrName) { | ||
sanitiseTransitions(this); | ||
const trans = typeof transOrName !== "object" ? this.transitions?.[transOrName] : transOrName; | ||
if (!trans) | ||
throw new Error(`${this.tagName} #${this.id} has no transition "${transOrName}"`); | ||
if (trans.cutOff) { | ||
abortAnimation(this); | ||
popAll(this); | ||
} | ||
const [notAnimating, promise] = queueTransition(this, trans); | ||
if (notAnimating) { | ||
if (!trans.cutOff) | ||
startAnimating(this); | ||
else | ||
whenTransitionAborts(this, () => startAnimating(this)); | ||
} | ||
return promise; | ||
}; | ||
SVGElement.prototype.removeCrossAni = HTMLElement.prototype.removeCrossAni = function() { | ||
const store = getOrInitStore(this); | ||
popAll(this); | ||
store.curr = {}; | ||
this.style.transition = ""; | ||
updateStyles(this); | ||
stateStore.delete(this); | ||
}; | ||
var orig = Element.prototype.remove; | ||
Element.prototype.remove = function() { | ||
stateStore.delete(this); | ||
orig.call(this); | ||
}; | ||
var unload = () => { | ||
for (const elem of stateStore.keys()) | ||
elem.removeCrossAni(); | ||
}; | ||
// src/browser.ts | ||
load(); | ||
window.EASE = EASE; | ||
@@ -172,0 +189,0 @@ window.JUMP = JUMP; |
@@ -33,3 +33,3 @@ var __defProp = Object.defineProperty; | ||
if (transition.reset) | ||
state.curr = transition.state; | ||
state.curr = transition.state ?? {}; | ||
else | ||
@@ -51,3 +51,3 @@ Object.assign(state.curr, transition.state); | ||
return; | ||
const transitionString = (0, import_generator.generateTransition)(state.curr, transition); | ||
const transitionString = (0, import_generator.generateTransition)(state, transition); | ||
if (transition.reset) | ||
@@ -57,2 +57,4 @@ state.curr = { ...transition.state }; | ||
Object.assign(state.curr, transition.state); | ||
state.lastMs = transition.ms ?? state.lastMs; | ||
state.lastEase = transition.easing ?? state.lastEase; | ||
elem.style.transition = transitionString; | ||
@@ -64,3 +66,3 @@ (0, import_util.updateStyles)(elem); | ||
startAnimating(elem); | ||
}, transition.ms + 20); | ||
}, state.lastMs + 20); | ||
} |
var import__ = require("."); | ||
var import_generator = require("./generator"); | ||
(0, import__.load)(); | ||
window.EASE = import_generator.EASE; | ||
window.JUMP = import_generator.JUMP; | ||
window.unloadCrossani = import__.unload; |
@@ -43,3 +43,6 @@ var __defProp = Object.defineProperty; | ||
}; | ||
const generateTransition = (prevState, transition) => distinct([...Object.keys(transition.state), ...Object.keys(prevState)]).map((p) => `${p} ${transition.ms}ms ${transition.easing}`).join(","); | ||
const generateTransition = (prevState, transition) => distinct([ | ||
...Object.keys(transition.state ?? {}), | ||
...Object.keys(prevState.curr) | ||
]).map((p) => `${p} ${transition.ms ?? prevState.lastMs}ms ${transition.easing ?? prevState.lastEase}`).join(","); | ||
const distinct = (arr) => Array.from(new Set(arr)); |
@@ -22,3 +22,3 @@ var __defProp = Object.defineProperty; | ||
JUMP: () => import_generator.JUMP, | ||
load: () => load, | ||
Transition: () => import_types.Transition, | ||
unload: () => unload | ||
@@ -28,26 +28,40 @@ }); | ||
var import_animator = require("./animator"); | ||
var import_shared = require("./shared"); | ||
var import_util = require("./util"); | ||
var import_generator = require("./generator"); | ||
function load() { | ||
HTMLElement.prototype.doTransition = function(name) { | ||
if (!this.transitions) | ||
throw new Error(`${this.tagName} #${this.id} does not have transitions`); | ||
(0, import_util.sanitiseTransitions)(this); | ||
const transition = this.transitions[name]; | ||
if (!transition) | ||
throw new Error(`${this.tagName} #${this.id} has no transition "${name}"`); | ||
if (transition.cutOff) { | ||
(0, import_animator.abortAnimation)(this); | ||
(0, import_animator.popAll)(this); | ||
} | ||
const notAnimating = (0, import_util.queueTransition)(this, transition); | ||
if (notAnimating) { | ||
if (!transition.cutOff) | ||
(0, import_animator.startAnimating)(this); | ||
else | ||
(0, import_util.whenTransitionAborts)(this, () => (0, import_animator.startAnimating)(this)); | ||
} | ||
return (0, import_util.getPromise)(this, transition); | ||
}; | ||
} | ||
const unload = () => delete HTMLElement.prototype.doTransition; | ||
var import_types = require("./types"); | ||
SVGElement.prototype.doTransition = HTMLElement.prototype.doTransition = function(transOrName) { | ||
(0, import_util.sanitiseTransitions)(this); | ||
const trans = typeof transOrName !== "object" ? this.transitions?.[transOrName] : transOrName; | ||
if (!trans) | ||
throw new Error(`${this.tagName} #${this.id} has no transition "${transOrName}"`); | ||
if (trans.cutOff) { | ||
(0, import_animator.abortAnimation)(this); | ||
(0, import_animator.popAll)(this); | ||
} | ||
const [notAnimating, promise] = (0, import_util.queueTransition)(this, trans); | ||
if (notAnimating) { | ||
if (!trans.cutOff) | ||
(0, import_animator.startAnimating)(this); | ||
else | ||
(0, import_util.whenTransitionAborts)(this, () => (0, import_animator.startAnimating)(this)); | ||
} | ||
return promise; | ||
}; | ||
SVGElement.prototype.removeCrossAni = HTMLElement.prototype.removeCrossAni = function() { | ||
const store = (0, import_util.getOrInitStore)(this); | ||
(0, import_animator.popAll)(this); | ||
store.curr = {}; | ||
this.style.transition = ""; | ||
(0, import_util.updateStyles)(this); | ||
import_shared.stateStore.delete(this); | ||
}; | ||
const orig = Element.prototype.remove; | ||
Element.prototype.remove = function() { | ||
import_shared.stateStore.delete(this); | ||
orig.call(this); | ||
}; | ||
const unload = () => { | ||
for (const elem of import_shared.stateStore.keys()) | ||
elem.removeCrossAni(); | ||
}; |
@@ -23,2 +23,2 @@ var __defProp = Object.defineProperty; | ||
module.exports = __toCommonJS(shared_exports); | ||
const stateStore = /* @__PURE__ */ new WeakMap(); | ||
const stateStore = /* @__PURE__ */ new Map(); |
@@ -23,3 +23,2 @@ var __defProp = Object.defineProperty; | ||
getOrInitStore: () => getOrInitStore, | ||
getPromise: () => getPromise, | ||
queueTransition: () => queueTransition, | ||
@@ -31,2 +30,3 @@ sanitiseTransitions: () => sanitiseTransitions, | ||
module.exports = __toCommonJS(util_exports); | ||
var import_generator = require("./generator"); | ||
var import_shared = require("./shared"); | ||
@@ -46,3 +46,5 @@ function cloneStyles(styles) { | ||
queue: [], | ||
transitionPromises: [] | ||
transitionPromises: [], | ||
lastEase: import_generator.EASE.ease, | ||
lastMs: 100 | ||
}; | ||
@@ -64,5 +66,4 @@ sanitiseStyleObject(newState.orig); | ||
state.transitionPromises.push([transition, promise, () => resolve()]); | ||
return state.queue.length === 1; | ||
return [state.queue.length === 1, promise]; | ||
} | ||
const getPromise = (elem, transition) => getOrInitStore(elem).transitionPromises.find((t) => t[0] === transition)?.[1] ?? Promise.reject("promise was missing from state"); | ||
function sanitiseStyleObject(obj) { | ||
@@ -76,6 +77,6 @@ delete obj.transition; | ||
function sanitiseTransitions(elem) { | ||
if (elem.transitions === void 0) | ||
if (!elem.transitions) | ||
return; | ||
for (const transition of Object.values(elem.transitions)) { | ||
if (!transition) | ||
if (!transition?.state) | ||
continue; | ||
@@ -82,0 +83,0 @@ sanitiseStyleObject(transition.state); |
@@ -11,3 +11,3 @@ import { generateTransition } from "./generator"; | ||
if (transition.reset) | ||
state.curr = transition.state; | ||
state.curr = transition.state ?? {}; | ||
else | ||
@@ -32,3 +32,3 @@ Object.assign(state.curr, transition.state); | ||
// needs to be run before updating state.curr or some values may not work correctly | ||
const transitionString = generateTransition(state.curr, transition); | ||
const transitionString = generateTransition(state, transition); | ||
// update styles | ||
@@ -39,2 +39,4 @@ if (transition.reset) | ||
Object.assign(state.curr, transition.state); | ||
state.lastMs = transition.ms ?? state.lastMs; | ||
state.lastEase = transition.easing ?? state.lastEase; | ||
// run transition | ||
@@ -50,3 +52,3 @@ elem.style.transition = transitionString; | ||
// give the transition 20ms of room to end early | ||
transition.ms + 20); | ||
state.lastMs + 20); | ||
} |
@@ -1,4 +0,3 @@ | ||
import { load, unload } from "."; | ||
import { unload } from "."; | ||
import { EASE, JUMP } from "./generator"; | ||
load(); | ||
// @ts-expect-error | ||
@@ -5,0 +4,0 @@ window.EASE = EASE; |
@@ -26,5 +26,8 @@ export const EASE = { | ||
}; | ||
export const generateTransition = (prevState, transition) => distinct([...Object.keys(transition.state), ...Object.keys(prevState)]) | ||
.map((p) => `${p} ${transition.ms}ms ${transition.easing}`) | ||
export const generateTransition = (prevState, transition) => distinct([ | ||
...Object.keys(transition.state ?? {}), | ||
...Object.keys(prevState.curr), | ||
]) | ||
.map((p) => `${p} ${transition.ms ?? prevState.lastMs}ms ${transition.easing ?? prevState.lastEase}`) | ||
.join(","); | ||
export const distinct = (arr) => Array.from(new Set(arr)); |
import { abortAnimation, popAll, startAnimating } from "./animator"; | ||
import { getPromise, queueTransition, sanitiseTransitions, whenTransitionAborts, } from "./util"; | ||
import { stateStore } from "./shared"; | ||
import { getOrInitStore, queueTransition, sanitiseTransitions, updateStyles, whenTransitionAborts, } from "./util"; | ||
export { EASE, JUMP } from "./generator"; | ||
export function load() { | ||
HTMLElement.prototype.doTransition = function (name) { | ||
if (!this.transitions) | ||
throw new Error(`${this.tagName} #${this.id} does not have transitions`); | ||
SVGElement.prototype.doTransition = HTMLElement.prototype.doTransition = | ||
function (transOrName) { | ||
// just to be sure the user isnt breaking things | ||
sanitiseTransitions(this); | ||
const transition = this.transitions[name]; | ||
if (!transition) | ||
throw new Error(`${this.tagName} #${this.id} has no transition "${name}"`); | ||
if (transition.cutOff) { | ||
const trans = typeof transOrName !== "object" | ||
? this.transitions?.[transOrName] | ||
: transOrName; | ||
if (!trans) | ||
throw new Error(`${this.tagName} #${this.id} has no transition "${transOrName}"`); | ||
if (trans.cutOff) { | ||
abortAnimation(this); | ||
popAll(this); | ||
} | ||
const notAnimating = queueTransition(this, transition); | ||
const [notAnimating, promise] = queueTransition(this, trans); | ||
if (notAnimating) { | ||
if (!transition.cutOff) | ||
if (!trans.cutOff) | ||
startAnimating(this); | ||
@@ -26,6 +27,21 @@ // wait for the transition to snap to the end | ||
} | ||
return getPromise(this, transition); | ||
return promise; | ||
}; | ||
} | ||
// @ts-expect-error, shut up TS | ||
export const unload = () => delete HTMLElement.prototype.doTransition; | ||
SVGElement.prototype.removeCrossAni = HTMLElement.prototype.removeCrossAni = | ||
function () { | ||
const store = getOrInitStore(this); | ||
popAll(this); | ||
store.curr = {}; | ||
this.style.transition = ""; | ||
updateStyles(this); | ||
stateStore.delete(this); | ||
}; | ||
const orig = Element.prototype.remove; | ||
Element.prototype.remove = function () { | ||
stateStore.delete(this); | ||
orig.call(this); | ||
}; | ||
export const unload = () => { | ||
for (const elem of stateStore.keys()) | ||
elem.removeCrossAni(); | ||
}; |
@@ -1,1 +0,1 @@ | ||
export const stateStore = new WeakMap(); | ||
export const stateStore = new Map(); |
@@ -1,1 +0,1 @@ | ||
"use strict"; | ||
export {}; |
@@ -0,1 +1,2 @@ | ||
import { EASE } from "./generator"; | ||
import { stateStore } from "./shared"; | ||
@@ -19,2 +20,4 @@ /** Converts a CSSStyleDeclaration to a Record<string, string> */ | ||
transitionPromises: [], | ||
lastEase: EASE.ease, | ||
lastMs: 100, | ||
}; | ||
@@ -39,6 +42,4 @@ sanitiseStyleObject(newState.orig); | ||
state.transitionPromises.push([transition, promise, () => resolve()]); | ||
return state.queue.length === 1; | ||
return [state.queue.length === 1, promise]; | ||
} | ||
/** Gets the promise for a given transition */ | ||
export const getPromise = (elem, transition) => getOrInitStore(elem).transitionPromises.find((t) => t[0] === transition)?.[1] ?? Promise.reject("promise was missing from state"); | ||
function sanitiseStyleObject(obj) { | ||
@@ -53,6 +54,6 @@ delete obj.transition; | ||
export function sanitiseTransitions(elem) { | ||
if (elem.transitions === undefined) | ||
if (!elem.transitions) | ||
return; | ||
for (const transition of Object.values(elem.transitions)) { | ||
if (!transition) | ||
if (!transition?.state) | ||
continue; | ||
@@ -59,0 +60,0 @@ sanitiseStyleObject(transition.state); |
/** Pops all transitions off the queue instantly and applies relevant CSS */ | ||
export declare function popAll(elem: HTMLElement): void; | ||
export declare const abortAnimation: (elem: HTMLElement) => string; | ||
export declare function startAnimating(elem: HTMLElement): void; | ||
export declare function popAll(elem: HTMLElement | SVGElement): void; | ||
export declare const abortAnimation: (elem: HTMLElement | SVGElement) => string; | ||
export declare function startAnimating(elem: HTMLElement | SVGElement): void; |
@@ -0,1 +1,2 @@ | ||
import { ElementState, Transition } from "./types"; | ||
export declare const EASE: { | ||
@@ -22,4 +23,4 @@ cubicBezier: (...points: number[]) => string; | ||
export declare const JUMP: Jumps; | ||
export declare const generateTransition: (prevState: Record<string, string>, transition: Transition) => string; | ||
export declare const generateTransition: (prevState: ElementState, transition: Transition) => string; | ||
export declare const distinct: <T>(arr: T[]) => T[]; | ||
export {}; |
export { EASE, JUMP } from "./generator"; | ||
export declare function load(): void; | ||
export declare const unload: () => boolean; | ||
export declare const unload: () => void; | ||
export { Transition } from "./types"; |
@@ -1,1 +0,2 @@ | ||
export declare const stateStore: WeakMap<HTMLElement, ElementState>; | ||
import { ElementState } from "./types"; | ||
export declare const stateStore: Map<HTMLElement | SVGElement, ElementState>; |
/** Represents a transition that may run on an element at any given time */ | ||
interface Transition { | ||
export interface Transition { | ||
/** A string map of CSS properties to their intended values */ | ||
state: Record<string, string>; | ||
state?: Record<string, string>; | ||
/** Number of milliseconds to transition for */ | ||
ms: number; | ||
ms?: number; | ||
/** An easing function to use - see {@link EASE} */ | ||
easing: string; | ||
easing?: string; | ||
/** If true, remove all crossani styles first */ | ||
@@ -15,3 +15,3 @@ reset?: boolean; | ||
/** @internal */ | ||
interface ElementState { | ||
export interface ElementState { | ||
/** Stores the styles crossani is applying */ | ||
@@ -25,8 +25,16 @@ orig: Record<string, string>; | ||
transitionPromises: [Transition, Promise<void>, () => void][]; | ||
/** Stores the previous ms value of the element */ | ||
lastMs: number; | ||
/** Stores the previous ease value of the element */ | ||
lastEase: string; | ||
} | ||
declare interface HTMLElement { | ||
/** A string map of transitions available on this element */ | ||
transitions?: Record<string, undefined | Transition>; | ||
/** Runs transitions defined in HTMLElement.transitions by name */ | ||
doTransition(name: string): void; | ||
declare global { | ||
interface Element { | ||
/** A string map of transitions available on this element */ | ||
transitions?: Record<string, undefined | Transition>; | ||
/** Runs transitions defined in Element.transitions by name */ | ||
doTransition(name: Transition | string): void; | ||
/** Removes CrossAni from this element */ | ||
removeCrossAni(): void; | ||
} | ||
} |
@@ -0,16 +1,15 @@ | ||
import { ElementState, Transition } from "./types"; | ||
/** Converts a CSSStyleDeclaration to a Record<string, string> */ | ||
export declare function cloneStyles(styles: CSSStyleDeclaration): any; | ||
/** Gets a store or inits if needed */ | ||
export declare function getOrInitStore(elem: HTMLElement): ElementState; | ||
export declare function getOrInitStore(elem: HTMLElement | SVGElement): ElementState; | ||
/** Updates the style tag according to the latest transition state etc */ | ||
export declare function updateStyles(elem: HTMLElement): void; | ||
export declare function updateStyles(elem: HTMLElement | SVGElement): void; | ||
/** Queues a transition. Returns true if the element is not currently animati */ | ||
export declare function queueTransition(elem: HTMLElement, transition: Transition): boolean; | ||
/** Gets the promise for a given transition */ | ||
export declare const getPromise: (elem: HTMLElement, transition: Transition) => Promise<void>; | ||
export declare function queueTransition(elem: HTMLElement | SVGElement, transition: Transition): [boolean, Promise<void>]; | ||
/** removes transition properties from states */ | ||
export declare function sanitiseTransitions(elem: HTMLElement): void; | ||
export declare function sanitiseTransitions(elem: HTMLElement | SVGElement): void; | ||
/** listens for transitionend, but with a timeout */ | ||
export declare const eventOrTimeout: (elem: HTMLElement, resolve: () => void, timeout: number) => undefined; | ||
export declare const eventOrTimeout: (elem: HTMLElement | SVGElement, resolve: () => void, timeout: number) => undefined; | ||
/** Waits for an element to finish transitioning before running callback, always run abortAnimation first */ | ||
export declare const whenTransitionAborts: (elem: HTMLElement, callback: () => void) => void; | ||
export declare const whenTransitionAborts: (elem: HTMLElement | SVGElement, callback: () => void) => void; |
{ | ||
"name": "crossani", | ||
"version": "1.0.1", | ||
"description": "A silky smooth declarative animation library for the web.", | ||
"main": "dist/cjs/index.js", | ||
"module": "dist/esm/index.js", | ||
"types": "dist/types/index.d.ts", | ||
"scripts": { | ||
"build": "tsc && esbuild src/*.ts --format=cjs --outdir=dist/cjs && esbuild src/browser.ts --bundle --outfile=dist/bundle.js" | ||
}, | ||
"author": "Cain Atkinson", | ||
"license": "MIT", | ||
"devDependencies": { | ||
"esbuild": "^0.14.38", | ||
"typescript": "^4.6.4" | ||
} | ||
} | ||
"name": "crossani", | ||
"version": "1.1.0", | ||
"description": "A silky smooth declarative animation library for the web.", | ||
"main": "dist/cjs/index.js", | ||
"module": "dist/esm/index.js", | ||
"types": "dist/types/index.d.ts", | ||
"author": "Cain Atkinson", | ||
"license": "MIT", | ||
"devDependencies": { | ||
"esbuild": "^0.14.38", | ||
"typescript": "^4.6.4" | ||
}, | ||
"scripts": { | ||
"build": "tsc && esbuild src/*.ts --format=cjs --outdir=dist/cjs && esbuild src/browser.ts --bundle --outfile=dist/bundle.js" | ||
} | ||
} |
205
README.md
@@ -1,1 +0,204 @@ | ||
// TODO | ||
# CrossAni | ||
A silky smooth declarative animations lib for the web. | ||
## Why CrossAni? | ||
### Browser-Native Performance | ||
A traditional animation library works by updating a property of an element repeatedly with JavaScript. | ||
This is required for things like SVG elements, and is also much much easier for more complex animations such as springs. | ||
However: this can have massive performance repurcussions. | ||
CrossAni uses CSS transitions to fix this problem, using the browser's built in animating tools. | ||
**CrossAni effectively guarantees 60fps animations every time**, or whatever refresh rate you run at. | ||
The main other way to achieve this is the Web Animations API (WAAPI). | ||
### Size Matters | ||
We live in an era of shaving milliseconds off load times, and a good chunk of this is JS bundle size. | ||
CrossAni is _TINY_. As I write this it's 2.5KB raw or 1.12KB gzipped. | ||
This is much smaller than the major animation libraries available (see comparisons further down). | ||
## How does it behave? | ||
Each element has an independent queue of pending animations. | ||
When you run an animation it will be queued to run, or if the element is stationary, will animate immediately. | ||
The queue will automatically run one-after-the-other until all animations are complete. | ||
Any animations with `cutOff: true` set will interrupt the current animation, | ||
cause all animations on the queue to finish instantly (we don't even transition them, just apply their changes!), | ||
and then run itself as usual from the final styles applied by all preceeding animations. | ||
CrossAni will only replace styles specified at each state. | ||
If you do not want a state to allow leaving residue of the previous states, set `reset: true`. | ||
All styles present on the element before the first animation ran will be respected, | ||
and will be returned to those value after a reset. | ||
Any _inline_ styles set after an animation has run on that element will readily be overwritten by CrossAni, | ||
so be aware of that. | ||
You can prevent this behaviour by calling `element.removeCrossAni()` after an animation. | ||
This will remove any residual styles from transitions, but leave your `transitions `intact. | ||
You can then modify the styles, and call `doTransition` as usual, re-initing that elem's animations | ||
## How do I use it? | ||
### (1) Assign some states to your element | ||
Any element that should be animated needs a `transitions` property with a key-value record of strings to state objects. | ||
A state object contains the following things: | ||
- `ms`: the time it should take to finish transitioning to that state | ||
- `easing`: An easing function to use, all exported on the `EASE` object. | ||
- `state`: An object containing the CSS values to set on the element this state. | ||
- `cutOff`: (OPTIONAL) see behaviour above | ||
- `reset`: (OPTIONAL) see behaviour above | ||
An example setup would be as follows: | ||
```js | ||
const box = document.getElementById("box"); | ||
box.transitions = { | ||
default: { | ||
state: {}, | ||
ms: 500, | ||
easing: EASE.inOut, | ||
reset: true, | ||
cutOff: true, | ||
}, | ||
hover: { | ||
state: { transform: "scale(1.1)" }, | ||
ms: 250, | ||
easing: EASE.inOut, | ||
}, | ||
}; | ||
``` | ||
### (2) Get animating! | ||
You may call `doTransition` on an element to cause it to animate to a named state. | ||
The first time you do this, CrossAni will take a note of inline styles, as described before. | ||
For, example, to continue with our box example: | ||
```js | ||
// ignore that this example could better be done | ||
// with a :hover selector, this is just for the demo | ||
box.onmouseover = () => box.doTransition("hover"); | ||
box.onmouseleave = () => box.doTransition("default"); | ||
``` | ||
### (3) Build up complex transitions with ease | ||
Promises make complexity into simplicity | ||
```js | ||
async function introAnimation() { | ||
box.doTransition("moveright"); | ||
box.doTransition("slowgrop"); | ||
await box.doTransition("fadetextout"); | ||
box.innerText = "the text is now different"; | ||
box.doTransition("fadetextin"); | ||
await box.doTransition("makemassive"); | ||
} | ||
``` | ||
## What's the most performant? | ||
`opacity`, `transform`, `filter`, as these are things that can be done in the compositor stage. | ||
`background-color` and `clip-path` may be accelerated in Chrome too, but ensure to test in all browsers for optimum speed. | ||
Either way, you're doing much better with CrossAni than JS animations, even if you're not getting the most blazing speed! | ||
Non `transform` _movements_ are slower as, although the browser is running the animation, | ||
repaints and often reflows are necessary, which are expensive. | ||
To answer the more important question, what's the least performant, anything that could affect layout, | ||
such as `margin`, `padding`, `width`, `height`, `font`, flex attributes, etc. | ||
## How does CrossAni stack up? | ||
Most benefits Motion One list also apply here: | ||
- super small | ||
- super smooth | ||
- not affected by JS execution freezing | ||
- high accuracy interpolation from the browser | ||
But also, SVG `d` attributes still cannot be animated with CSS transitions. | ||
### Bundle size | ||
All packages were ran through [bundlejs](https://bundlejs.com) with esm.sh as the cdn. | ||
| Pkg | Raw | Gzip | Brotli | | ||
| -------- | ------- | ------- | ------- | | ||
| crossani | 2.5kb | 1.12kb | 1.05kb | | ||
| motion | 15.07kb | 6.34kb | 6.17kb | | ||
| animejs | 17.59kb | 7.19kb | 6.99kb | | ||
| gsap | 61.97kb | 24.45kb | 23.86kb | | ||
### Features | ||
| | Feature | CrossAni | Motion | AnimeJS | Greensock | | ||
| ------------ | -----------------: | :---------: | :-------: | :----------: | :-------: | | ||
| **Values** | CSS `transform` | ✅ | ✅ | ❌ | ✅ | | ||
| | Named colours | ✅ | ✅ | ❌ | Partial | | ||
| | Colour type conv | ✅ | ✅ | Wrong interp | ✅ | | ||
| | To/from CSS vars | ✅ | ✅ | ❌ | ❌ | | ||
| | To/from CSS funcs | ✅ | ✅ | ❌ | ❌ | | ||
| | Animate CSS vars | ❌ | ✅ | ❌ | ❌ | | ||
| | Simple keyframes | ❌ Soon? | ✅ | ✅ | ❌ | | ||
| | Wildcard keyframes | N/A | ✅ | ❌ | ❌ | | ||
| | Relative values | ❌ | ✅ | ❌ | ❌ | | ||
| **Output** | Element styles | ✅ | ✅ | ✅ | ✅ | | ||
| | Element attrs | ❌ | ❌ | ✅ | ✅ | | ||
| | Custom animations | ❌ | ✅ | ✅ | ✅ | | ||
| **Options** | Duration | ✅ | ✅ | ✅ | ✅ | | ||
| | Direction | ✅ | ✅ | ✅ | ✅ | | ||
| | Repeat | ❌ | ✅ | ✅ | ✅ | | ||
| | Delay | ❌ | ✅ | ✅ | ✅ | | ||
| | End delay | ❌ | ✅ | ✅ | ✅ | | ||
| | Repeat delay | ❌ | ✅ | ❌ | ✅ | | ||
| **Stagger** | Stagger | ❌ | ✅ +.1kb | ✅ | ✅ | | ||
| **Timeline** | Timeline | ❌ | ✅ +.6kb | ✅ | ✅ | | ||
| **Controls** | Play | ✅ | ✅ | ✅ | ✅ | | ||
| | Pause | ❌ | ✅ | ✅ | ✅ | | ||
| | Finish | ❌ | ✅ | ✅ | ✅ | | ||
| | Reverse | ❌ | ✅ | ✅ | ✅ | | ||
| | Stop | ❌ | ✅ | ✅ | ✅ | | ||
| | Playback rate | ❌ | ✅ | ✅ | ✅ | | ||
| **Easing** | Linear | ✅ | ✅ | ✅ | ✅ | | ||
| | Cubic bezier | ✅ | ✅ | ✅ | ✅ | | ||
| | Steps | ✅ | ✅ | ✅ | ✅ | | ||
| | Spring simulation | ❌ | ✅ +1kb | ❌ | ❌ | | ||
| | Glide | ❌ | ✅ +1.3kb | ❌ | ✅ $99/yr | | ||
| | Spring easing | ❌ | ❌ | ✅ | ❌ | | ||
| | Custom easings | ❌ | ❌ | ✅ | ✅ | | ||
| **Events** | Complete | ✅ | ✅ | ✅ | ✅ | | ||
| | Cancel | Fires above | ✅ | ✅ | ✅ | | ||
| | Start | ❌ | ❌ | ✅ | ✅ | | ||
| | Update | ❌ | ❌ | ✅ | ✅ | | ||
| | Repeat | N/A | ❌ | ✅ | ✅ | | ||
| **Path** | Motion path | ❌ | ✅ | ✅ | ✅ +9.5kb | | ||
| | Path morphing | ❌ | ✅ lib | ✅ = #points | ✅ $99/yr | | ||
| | Path drawing | ✅ | ✅ | ✅ | ✅ $99/yr | | ||
| **Other** | license | MIT | MIT | MIT | Custom | | ||
| | GPU acceleration | ✅ | ✅ | ❌ | ❌ | | ||
| | IE11 (ew) | ❌ | ❌ | ✅ | ✅ | | ||
| | Frameworks | ✅ | ✅ | ❌ | ❌ | |
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
42795
28
854
205