@proto-cool/cascade
Version:
A lightweight TypeScript library for easily defining smooth, performant animations when elements enter the viewport.
339 lines (324 loc) • 13.2 kB
JavaScript
;
var motion = require('motion');
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
function __rest(s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
var animConfigs = {
fadeIn: {
initial: { opacity: "0" },
keyframes: { opacity: [0, 1] },
settings: { duration: 1, ease: "easeOut" },
},
fadeInZoom: {
initial: { opacity: "0", transform: "scale(0.8)" },
keyframes: { opacity: [0, 1], transform: ["scale(0.7)", "scale(1)"] },
settings: { duration: 1, ease: "easeOut" },
},
fadeInUp: {
initial: { opacity: "0", transform: "translateY(1rem)" },
keyframes: {
opacity: [0, 1],
transform: ["translateY(1rem)", "translateY(0)"],
},
settings: { duration: 1, ease: "easeOut", offset: "1rem" },
},
fadeInDown: {
initial: { opacity: "0", transform: "translateY(-1rem)" },
keyframes: {
opacity: [0, 1],
transform: ["translateY(-1rem)", "translateY(0)"],
},
settings: { duration: 1, ease: "easeOut" },
},
fadeInLeft: {
initial: { opacity: "0", transform: "translateX(1rem)" },
keyframes: {
opacity: [0, 1],
transform: ["translateX(1rem)", "translateX(0)"],
},
settings: { duration: 1, ease: "easeOut" },
},
fadeInRight: {
initial: { opacity: "0", transform: "translateX(-1rem)" },
keyframes: {
opacity: [0, 1],
transform: ["translateX(-1rem)", "translateX(0)"],
},
settings: { duration: 1, ease: "easeOut" },
},
fadeOut: {
initial: { opacity: "1" },
keyframes: { opacity: [1, 0] },
settings: { duration: 1, ease: "easeOut" },
},
fadeOutZoom: {
initial: { opacity: "1", transform: "scale(1)" },
keyframes: { opacity: [1, 0], transform: ["scale(1)", "scale(0.7)"] },
settings: { duration: 1, ease: "easeOut" },
},
fadeOutUp: {
initial: { opacity: "1", transform: "translateY(0)" },
keyframes: {
opacity: [1, 0],
transform: ["translateY(0)", "translateY(1rem)"],
},
settings: { duration: 1, ease: "easeOut" },
},
fadeOutDown: {
initial: { opacity: "1", transform: "translateY(0)" },
keyframes: {
opacity: [1, 0],
transform: ["translateY(0)", "translateY(-1rem)"],
},
settings: { duration: 1, ease: "easeOut" },
},
fadeOutLeft: {
initial: { opacity: "1", transform: "translateX(0)" },
keyframes: {
opacity: [1, 0],
transform: ["translateX(0)", "translateX(1rem)"],
},
settings: { duration: 1, ease: "easeOut" },
},
fadeOutRight: {
initial: { opacity: "1", transform: "translateX(0)" },
keyframes: {
opacity: [1, 0],
transform: ["translateX(0)", "translateX(-1rem)"],
},
settings: { duration: 1, ease: "easeOut" },
},
bounceIn: {
initial: { opacity: "0", transform: "scale(0.7)" },
keyframes: {
opacity: [0, 1],
transform: ["scale(0.7)", "scale(1.05)", "scale(0.95)", "scale(1)"],
},
settings: { duration: 0.8, ease: "easeOut" },
},
bounceOut: {
initial: { opacity: "1", transform: "scale(1)" },
keyframes: {
opacity: [1, 0],
transform: ["scale(1)", "scale(1.05)", "scale(0.7)"],
},
settings: { duration: 0.8, ease: "easeIn" },
},
flipIn: {
initial: { opacity: "0", transform: "rotateY(90deg)" },
keyframes: {
opacity: [0, 1],
transform: ["rotateY(90deg)", "rotateY(0deg)"],
},
settings: { duration: 1, ease: "easeOut" },
},
flipOut: {
initial: { opacity: "1", transform: "rotateY(0deg)" },
keyframes: {
opacity: [1, 0],
transform: ["rotateY(0deg)", "rotateY(90deg)"],
},
settings: { duration: 1, ease: "easeIn" },
},
rotateIn: {
initial: { opacity: "0", transform: "rotate(-180deg)" },
keyframes: {
opacity: [0, 1],
transform: ["rotate(-180deg)", "rotate(0deg)"],
},
settings: { duration: 1, ease: "easeOut" },
},
rotateOut: {
initial: { opacity: "1", transform: "rotate(0deg)" },
keyframes: {
opacity: [1, 0],
transform: ["rotate(0deg)", "rotate(180deg)"],
},
settings: { duration: 1, ease: "easeIn" },
},
};
function createAnimations(config) {
return function (el, opts) {
if (opts === void 0) { opts = {}; }
// 1) Merge the per-animation defaults with any overrides
var merged = __assign(__assign({}, config.settings), opts);
// 2) Pull out inView-specific props (and drop "offset" if present)
var root = merged.root, margin = merged.margin, amount = merged.amount; merged.offset; var motionOpts = __rest(merged, ["root", "margin", "amount", "offset"]);
// 3) Apply your initial CSS styles
Object.assign(el.style, config.initial);
// 4) Observe and, on enter, fire motion.animate() with the rest of the options
motion.inView(el, function () {
motion.animate(el, config.keyframes, motionOpts);
}, { root: root, margin: margin, amount: amount });
};
}
var animations = Object.fromEntries(Object.entries(animConfigs).map(function (_a) {
var name = _a[0], cfg = _a[1];
return [
name,
createAnimations(cfg),
];
}));
var defaultConfig = {
attributePrefix: "cascade",
defaults: {
// Basic transition settings
duration: 0.7,
delay: 0,
type: "tween",
repeat: 0,
repeatType: "loop",
repeatDelay: 0,
// Tween transition type
ease: "easeInOut",
// Spring-specific settings
bounce: 0.25,
damping: 10,
mass: 1,
stiffness: 1,
velocity: 0,
restSpeed: 0.1,
restDelta: 0.01,
// Inertia specific settings
power: 0.8,
timeConstant: 700,
min: undefined,
max: undefined,
bounceStiffness: 500,
bounceDamping: 10,
},
};
function generateNoscript() {
var prefix = defaultConfig.attributePrefix; // e.g. "cascade"
var sel = "data-".concat(prefix); // e.g. "data-cascade"
var noscript = "\n<noscript>\n <style>\n /* show all [".concat(sel, "] if javascript is disabled */\n [").concat(sel, "] {\n opacity: 1 !important;\n }\n `\n </style>\n</noscript>");
return noscript.trim();
}
/**
* index.ts
*
* This module exports a single function, `enable`, which scans the document
* for any elements using our custom data attribute (e.g. `data-cascade`) and
* applies the corresponding animation to each one.
* It merges three levels of configuration, in order of increasing priority:
* 1. Built-in defaults
* 2. Global overrides passed to `enable()`
* 3. Per-element overrides via `data-<prefix>-<option>` attributes
*/
/**
* Scans the document for elements marked with `data-<prefix>` and runs the
* chosen animation on each one, using defaults + globalOpts + elementAttrs.
*
* @param globalOpts
* Optional configuration that applies to *every* element. These values
* override the built-in defaults but are themselves overridden by any
* per-element `data-<prefix>-<option>` attributes.
*/
function enable(globalOpts) {
// Extract our configured attribute prefix (typically "cascade") and the
// bundle of default animation settings (duration, ease, spring params, etc.).
var _a = defaultConfig, attributePrefix = _a.attributePrefix, defaults = _a.defaults;
// Build a CSS selector for elements that have the bare `data-<prefix>` key.
// e.g. if prefix = "cascade", selector = "[data-cascade]"
var baseSelector = "[data-".concat(attributePrefix, "]");
// Find and loop through every such element in the DOM.
document.querySelectorAll(baseSelector).forEach(function (el) {
// 1) Read the animation name (e.g. "fade", "slide") from data-<prefix>
var animationName = el.getAttribute("data-".concat(attributePrefix));
var animFn = animations[animationName];
// If we don’t recognize that animation name, warn and skip this element.
if (!animFn) {
console.warn("Unknown animation: \"".concat(animationName, "\". ") +
"Valid names are: ".concat(Object.keys(animations).join(", "), "."));
return;
}
// 2) Build per-element overrides by scanning for any attributes of the
// form `data-<prefix>-<optionName>`. For instance:
// - data-cascade-delay="0.3"
// - data-cascade-ease="easeOut"
// - data-cascade-bounce="0.5"
//
// We extract <optionName>, convert it to camelCase if needed,
// parse it into the right type (number, boolean, or string),
// and stash it in elementOverrides.
var elementOverrides = {};
var attrStart = "data-".concat(attributePrefix, "-");
for (var _i = 0, _a = el.getAttributeNames(); _i < _a.length; _i++) {
var fullAttrName = _a[_i];
if (!fullAttrName.startsWith(attrStart)) {
continue; // skip irrelevant attributes
}
// Extract the option key from the attribute name
// e.g. "data-cascade-delay" → optionKeyRaw = "delay"
var optionKeyRaw = fullAttrName.slice(attrStart.length);
// Convert hyphenated option keys to camelCase property names
// e.g. "repeat-delay" → "repeatDelay"
var propKey = optionKeyRaw.replace(/-([a-z])/g, function (_, char) {
return char.toUpperCase();
});
// Only proceed if this prop actually exists in our default settings
if (!(propKey in defaults)) {
continue;
}
// Read the raw string value from the attribute
var rawValue = el.getAttribute(fullAttrName);
var defaultValue = defaults[propKey];
// Parse the string into the correct type
var parsedValue = rawValue;
if (typeof defaultValue === "number") {
parsedValue = Number(rawValue);
}
else if (typeof defaultValue === "boolean") {
parsedValue = rawValue === "true";
}
// Store it as a per-element override
elementOverrides[propKey] = parsedValue;
}
// 3) Merge everything: built-in defaults ← globalOpts ← per-element overrides
// Later spreads overwrite earlier keys, so HTML attributes win over
// JS global overrides, which in turn win over the defaults.
var finalOpts = __assign(__assign(__assign({}, defaults), globalOpts), elementOverrides);
// 4) Finally, call the animation function with its element + resolved options.
animFn(el, finalOpts);
});
}
var cascade = {
enable: enable,
noscript: generateNoscript(),
};
module.exports = cascade;