Skip to content

Instantly share code, notes, and snippets.

@okikio
Last active September 25, 2022 15:42
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save okikio/bed53ed621cb7f60e9a8b1ef92897471 to your computer and use it in GitHub Desktop.
Save okikio/bed53ed621cb7f60e9a8b1ef92897471 to your computer and use it in GitHub Desktop.
custom-easing.ts - a set of easing functions that generate arrays of tweens/frames; when placed in an animation framework/library they emulate animations using said easing function. Since, the easing frames can be placed are generated on initial run and almost every conventional animation library/framework supports multiple animation frames, you…
/**
* https://github.com/gre/bezier-easing
* BezierEasing - use bezier curve for transition easing function
* by Gaëtan Renaudeau 2014 - 2015 – MIT License
*/
// These values are established by empiricism with tests (tradeoff: performance VS precision)
export const NEWTON_ITERATIONS = 4;
export const NEWTON_MIN_SLOPE = 0.001;
export const SUBDIVISION_PRECISION = 0.0000001;
export const SUBDIVISION_MAX_ITERATIONS = 10;
export const kSplineTableSize = 11;
export const kSampleStepSize = 1.0 / (kSplineTableSize - 1.0);
export const float32ArraySupported = typeof Float32Array === 'function';
export const A = (aA1: number, aA2: number) => (1.0 - 3.0 * aA2 + 3.0 * aA1);
export const B = (aA1: number, aA2: number) => (3.0 * aA2 - 6.0 * aA1);
export const C = (aA1: number) => (3.0 * aA1);
// Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2.
export const calcBezier = (aT: number, aA1: number, aA2: number) => ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT;
// Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2.
export const getSlope = (aT: number, aA1: number, aA2: number) => 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1);
export const binarySubdivide = (aX: number, aA: number, aB: number, mX1: number, mX2: number) => {
let currentX: number, currentT: number, i = 0;
do {
currentT = aA + (aB - aA) / 2.0;
currentX = calcBezier(currentT, mX1, mX2) - aX;
if (currentX > 0.0)
aB = currentT;
else aA = currentT;
} while (Math.abs(currentX) > SUBDIVISION_PRECISION && ++i < SUBDIVISION_MAX_ITERATIONS);
return currentT;
}
export const newtonRaphsonIterate = (aX: number, aGuessT: number, mX1: number, mX2: number) => {
for (var i = 0; i < NEWTON_ITERATIONS; ++i) {
let currentSlope = getSlope(aGuessT, mX1, mX2);
if (currentSlope === 0.0) return aGuessT;
let currentX = calcBezier(aGuessT, mX1, mX2) - aX;
aGuessT -= currentX / currentSlope;
}
return aGuessT;
}
export const bezier = (mX1: number, mY1: number, mX2: number, mY2: number) => {
if (!(0 <= mX1 && mX1 <= 1 && 0 <= mX2 && mX2 <= 1))
throw new Error('bezier x values must be in [0, 1] range');
if (mX1 === mY1 && mX2 === mY2) return (t: number) => t;
// Precompute samples table
var sampleValues = float32ArraySupported ? new Float32Array(kSplineTableSize) : new Array(kSplineTableSize);
for (var i = 0; i < kSplineTableSize; ++i) {
sampleValues[i] = calcBezier(i * kSampleStepSize, mX1, mX2);
}
const getTForX = (aX: number) => {
let intervalStart = 0.0;
let currentSample = 1;
let lastSample = kSplineTableSize - 1;
for (; currentSample !== lastSample && sampleValues[currentSample] <= aX; ++currentSample)
intervalStart += kSampleStepSize;
--currentSample;
// Interpolate to provide an initial guess for t
let dist = (aX - sampleValues[currentSample]) / (sampleValues[currentSample + 1] - sampleValues[currentSample]);
let guessForT = intervalStart + dist * kSampleStepSize;
let initialSlope = getSlope(guessForT, mX1, mX2);
if (initialSlope >= NEWTON_MIN_SLOPE)
return newtonRaphsonIterate(aX, guessForT, mX1, mX2);
else if (initialSlope === 0.0)
return guessForT;
else {
return binarySubdivide(aX, intervalStart, intervalStart + kSampleStepSize, mX1, mX2);
}
}
return (t: number) => {
// Because JavaScript number are imprecise, we should guarantee the extremes are right.
if (t === 0 || t === 1) return t;
return calcBezier(getTForX(t), mY1, mY2);
};
};
export default bezier;
/**
* Returns browser objects that can be used on the server
* @returns An object with the following properties: `{ document, CSS }`, this so `@okikio/animate` can be used in both the browser and the server
*/
export const getBrowserObject = () => {
return {
document: (("document" in globalThis) ? globalThis.document : {}) as Document,
CSS: (("CSS" in globalThis) ? globalThis.CSS : {}) as typeof CSS,
};
}
/**
* Returns the browser's `document` object, except if it's a server, in which case it returns an empty object
* @returns The browser document object, but allows it to be used both in the browser and the server
*/
export const getDocument = () => {
return getBrowserObject().document;
}
/**
* The browser's `CSS` object, except if it's a server, in which case it returns an empty object
* @returns The browser CSS object, except if it's a server, in which case it returns an empty object
*/
export const getCSS = () => {
return getBrowserObject().CSS;
}
/**
* Based on color-rgba https://github.com/colorjs/color-rgba
*/
import names from './colors';
export const hsl = {
name: 'hsl',
min: [0, 0, 0],
max: [360, 100, 100],
channel: ['hue', 'saturation', 'lightness'],
alias: ['HSL'],
rgb: function (hsl) {
var h = hsl[0] / 360,
s = hsl[1] / 100,
l = hsl[2] / 100,
t1, t2, t3, rgb, val;
if (s === 0) {
val = l * 255;
return [val, val, val];
}
if (l < 0.5) {
t2 = l * (1 + s);
}
else {
t2 = l + s - l * s;
}
t1 = 2 * l - t2;
rgb = [0, 0, 0];
for (var i = 0; i < 3; i++) {
t3 = h + 1 / 3 * - (i - 1);
if (t3 < 0) {
t3++;
}
else if (t3 > 1) {
t3--;
}
if (6 * t3 < 1) {
val = t1 + (t2 - t1) * 6 * t3;
}
else if (2 * t3 < 1) {
val = t2;
}
else if (3 * t3 < 2) {
val = t1 + (t2 - t1) * (2 / 3 - t3) * 6;
}
else {
val = t1;
}
rgb[i] = val * 255;
}
return rgb;
}
};
/**
* Base hues
* http://dev.w3.org/csswg/css-color/#typedef-named-hue
*/
//FIXME: use external hue detector
var baseHues = {
red: 0,
orange: 60,
yellow: 120,
green: 180,
blue: 240,
purple: 300
}
/**
* Parse color from the string passed
*
* @return {Object} A space indicator `space`, an array `values` and `alpha`
*/
export const parse = (cstr) => {
var m, parts = [], alpha = 1, space
if (typeof cstr === 'string') {
//keyword
if (names[cstr]) {
parts = names[cstr].slice()
space = 'rgb'
}
//reserved words
else if (cstr === 'transparent') {
alpha = 0
space = 'rgb'
parts = [0, 0, 0]
}
//hex
else if (/^#[A-Fa-f0-9]+$/.test(cstr)) {
var base = cstr.slice(1)
var size = base.length
var isShort = size <= 4
alpha = 1
if (isShort) {
parts = [
parseInt(base[0] + base[0], 16),
parseInt(base[1] + base[1], 16),
parseInt(base[2] + base[2], 16)
]
if (size === 4) {
alpha = parseInt(base[3] + base[3], 16) / 255
}
}
else {
parts = [
parseInt(base[0] + base[1], 16),
parseInt(base[2] + base[3], 16),
parseInt(base[4] + base[5], 16)
]
if (size === 8) {
alpha = parseInt(base[6] + base[7], 16) / 255
}
}
if (!parts[0]) parts[0] = 0
if (!parts[1]) parts[1] = 0
if (!parts[2]) parts[2] = 0
space = 'rgb'
}
//color space
else if (m = /^((?:rgb|hs[lvb]|hwb|cmyk?|xy[zy]|gray|lab|lchu?v?|[ly]uv|lms)a?)\s*\(([^\)]*)\)/.exec(cstr)) {
var name = m[1]
var isRGB = name === 'rgb'
var base: string = name.replace(/a$/, '');
space = base
var size = base === 'cmyk' ? 4 : base === 'gray' ? 1 : 3
parts = m[2].trim()
.split(/\s*[,\/]\s*|\s+/)
.map(function (x, i) {
//<percentage>
if (/%$/.test(x)) {
//alpha
if (i === size) return parseFloat(x) / 100
//rgb
if (base === 'rgb') return parseFloat(x) * 255 / 100
return parseFloat(x)
}
//hue
else if (base[i] === 'h') {
//<deg>
if (/deg$/.test(x)) {
return parseFloat(x)
}
//<base-hue>
else if (baseHues[x] !== undefined) {
return baseHues[x]
}
}
return parseFloat(x)
})
if (name === base) parts.push(1)
alpha = (isRGB) ? 1 : (parts[size] === undefined) ? 1 : parts[size]
parts = parts.slice(0, size)
}
//named channels case
else if (cstr.length > 10 && /[0-9](?:\s|\/)/.test(cstr)) {
parts = cstr.match(/([0-9]+)/g).map(function (value) {
return parseFloat(value)
})
space = cstr.match(/([a-z])/ig).join('').toLowerCase()
}
}
//numeric case
else if (!Number.isNaN(cstr)) {
space = 'rgb'
parts = [cstr >>> 16, (cstr & 0x00ff00) >>> 8, cstr & 0x0000ff]
}
//array-like
else if (Array.isArray(cstr) || cstr.length) {
parts = [cstr[0], cstr[1], cstr[2]]
space = 'rgb'
alpha = cstr.length === 4 ? cstr[3] : 1
}
//object case - detects css cases of rgb and hsl
else if (cstr instanceof Object) {
if (cstr.r != null || cstr.red != null || cstr.R != null) {
space = 'rgb'
parts = [
cstr.r || cstr.red || cstr.R || 0,
cstr.g || cstr.green || cstr.G || 0,
cstr.b || cstr.blue || cstr.B || 0
]
}
else {
space = 'hsl'
parts = [
cstr.h || cstr.hue || cstr.H || 0,
cstr.s || cstr.saturation || cstr.S || 0,
cstr.l || cstr.lightness || cstr.L || cstr.b || cstr.brightness
]
}
alpha = cstr.a || cstr.alpha || cstr.opacity || 1
if (cstr.opacity != null) alpha /= 100
}
return {
space: space,
values: parts,
alpha: alpha
}
}
export const rgba = (color) => {
// template literals
// @ts-ignore
if (Array.isArray(color) && color.raw) color = String.raw(...arguments)
var values, i, l
//attempt to parse non-array arguments
var parsed = parse(color)
if (!parsed.space) return []
values = Array(3)
values[0] = Math.min(Math.max(parsed.values[0], 0), 255)
values[1] = Math.min(Math.max(parsed.values[1], 0), 255)
values[2] = Math.min(Math.max(parsed.values[2], 0), 255)
if (parsed.space[0] === 'h') {
values = hsl.rgb(values)
}
values.push(Math.min(Math.max(parsed.alpha, 0), 1))
return values
}
export default rgba;
export default {
"aliceblue": [240, 248, 255],
"antiquewhite": [250, 235, 215],
"aqua": [0, 255, 255],
"aquamarine": [127, 255, 212],
"azure": [240, 255, 255],
"beige": [245, 245, 220],
"bisque": [255, 228, 196],
"black": [0, 0, 0],
"blanchedalmond": [255, 235, 205],
"blue": [0, 0, 255],
"blueviolet": [138, 43, 226],
"brown": [165, 42, 42],
"burlywood": [222, 184, 135],
"cadetblue": [95, 158, 160],
"chartreuse": [127, 255, 0],
"chocolate": [210, 105, 30],
"coral": [255, 127, 80],
"cornflowerblue": [100, 149, 237],
"cornsilk": [255, 248, 220],
"crimson": [220, 20, 60],
"cyan": [0, 255, 255],
"darkblue": [0, 0, 139],
"darkcyan": [0, 139, 139],
"darkgoldenrod": [184, 134, 11],
"darkgray": [169, 169, 169],
"darkgreen": [0, 100, 0],
"darkgrey": [169, 169, 169],
"darkkhaki": [189, 183, 107],
"darkmagenta": [139, 0, 139],
"darkolivegreen": [85, 107, 47],
"darkorange": [255, 140, 0],
"darkorchid": [153, 50, 204],
"darkred": [139, 0, 0],
"darksalmon": [233, 150, 122],
"darkseagreen": [143, 188, 143],
"darkslateblue": [72, 61, 139],
"darkslategray": [47, 79, 79],
"darkslategrey": [47, 79, 79],
"darkturquoise": [0, 206, 209],
"darkviolet": [148, 0, 211],
"deeppink": [255, 20, 147],
"deepskyblue": [0, 191, 255],
"dimgray": [105, 105, 105],
"dimgrey": [105, 105, 105],
"dodgerblue": [30, 144, 255],
"firebrick": [178, 34, 34],
"floralwhite": [255, 250, 240],
"forestgreen": [34, 139, 34],
"fuchsia": [255, 0, 255],
"gainsboro": [220, 220, 220],
"ghostwhite": [248, 248, 255],
"gold": [255, 215, 0],
"goldenrod": [218, 165, 32],
"gray": [128, 128, 128],
"green": [0, 128, 0],
"greenyellow": [173, 255, 47],
"grey": [128, 128, 128],
"honeydew": [240, 255, 240],
"hotpink": [255, 105, 180],
"indianred": [205, 92, 92],
"indigo": [75, 0, 130],
"ivory": [255, 255, 240],
"khaki": [240, 230, 140],
"lavender": [230, 230, 250],
"lavenderblush": [255, 240, 245],
"lawngreen": [124, 252, 0],
"lemonchiffon": [255, 250, 205],
"lightblue": [173, 216, 230],
"lightcoral": [240, 128, 128],
"lightcyan": [224, 255, 255],
"lightgoldenrodyellow": [250, 250, 210],
"lightgray": [211, 211, 211],
"lightgreen": [144, 238, 144],
"lightgrey": [211, 211, 211],
"lightpink": [255, 182, 193],
"lightsalmon": [255, 160, 122],
"lightseagreen": [32, 178, 170],
"lightskyblue": [135, 206, 250],
"lightslategray": [119, 136, 153],
"lightslategrey": [119, 136, 153],
"lightsteelblue": [176, 196, 222],
"lightyellow": [255, 255, 224],
"lime": [0, 255, 0],
"limegreen": [50, 205, 50],
"linen": [250, 240, 230],
"magenta": [255, 0, 255],
"maroon": [128, 0, 0],
"mediumaquamarine": [102, 205, 170],
"mediumblue": [0, 0, 205],
"mediumorchid": [186, 85, 211],
"mediumpurple": [147, 112, 219],
"mediumseagreen": [60, 179, 113],
"mediumslateblue": [123, 104, 238],
"mediumspringgreen": [0, 250, 154],
"mediumturquoise": [72, 209, 204],
"mediumvioletred": [199, 21, 133],
"midnightblue": [25, 25, 112],
"mintcream": [245, 255, 250],
"mistyrose": [255, 228, 225],
"moccasin": [255, 228, 181],
"navajowhite": [255, 222, 173],
"navy": [0, 0, 128],
"oldlace": [253, 245, 230],
"olive": [128, 128, 0],
"olivedrab": [107, 142, 35],
"orange": [255, 165, 0],
"orangered": [255, 69, 0],
"orchid": [218, 112, 214],
"palegoldenrod": [238, 232, 170],
"palegreen": [152, 251, 152],
"paleturquoise": [175, 238, 238],
"palevioletred": [219, 112, 147],
"papayawhip": [255, 239, 213],
"peachpuff": [255, 218, 185],
"peru": [205, 133, 63],
"pink": [255, 192, 203],
"plum": [221, 160, 221],
"powderblue": [176, 224, 230],
"purple": [128, 0, 128],
"rebeccapurple": [102, 51, 153],
"red": [255, 0, 0],
"rosybrown": [188, 143, 143],
"royalblue": [65, 105, 225],
"saddlebrown": [139, 69, 19],
"salmon": [250, 128, 114],
"sandybrown": [244, 164, 96],
"seagreen": [46, 139, 87],
"seashell": [255, 245, 238],
"sienna": [160, 82, 45],
"silver": [192, 192, 192],
"skyblue": [135, 206, 235],
"slateblue": [106, 90, 205],
"slategray": [112, 128, 144],
"slategrey": [112, 128, 144],
"snow": [255, 250, 250],
"springgreen": [0, 255, 127],
"steelblue": [70, 130, 180],
"tan": [210, 180, 140],
"teal": [0, 128, 128],
"thistle": [216, 191, 216],
"tomato": [255, 99, 71],
"turquoise": [64, 224, 208],
"violet": [238, 130, 238],
"wheat": [245, 222, 179],
"white": [255, 255, 255],
"whitesmoke": [245, 245, 245],
"yellow": [255, 255, 0],
"yellowgreen": [154, 205, 50]
};
import {
isValid,
transpose,
toStr,
convertToDash,
mapObject,
getUnit,
trim,
isEmpty
} from "./utils";
import { rgba } from "./color-rgba";
import { toRGBAArr } from "./unit-conversion";
import { getCSS } from "./browser-objects";
import bezier from "./bezier-easing";
import type { IAnimateInstanceConfig } from "./types";
export const limit = (value: number, min: number, max: number) =>
Math.min(Math.max(value, min), max);
/**
* The format to use when defining custom easing functions
*/
export type TypeEasingFunction = (
t: number,
params?: (string | number)[],
duration?: number
) => number;
/**
Easing Functions from anime.js, they are tried and true, so, its better to use them instead of other alternatives
*/
export const Quad: TypeEasingFunction = (t) => Math.pow(t, 2);
export const Cubic: TypeEasingFunction = (t) => Math.pow(t, 3);
export const Quart: TypeEasingFunction = (t) => Math.pow(t, 4);
export const Quint: TypeEasingFunction = (t) => Math.pow(t, 5);
export const Expo: TypeEasingFunction = (t) => Math.pow(t, 6);
export const Sine: TypeEasingFunction = (t) => 1 - Math.cos((t * Math.PI) / 2);
export const Circ: TypeEasingFunction = (t) => 1 - Math.sqrt(1 - t * t);
export const Back: TypeEasingFunction = (t) => t * t * (3 * t - 2);
export const Bounce: TypeEasingFunction = (t) => {
let pow2: number,
b = 4;
while (t < ((pow2 = Math.pow(2, --b)) - 1) / 11) {}
return (
1 / Math.pow(4, 3 - b) - 7.5625 * Math.pow((pow2 * 3 - 2) / 22 - t, 2)
);
};
export const Elastic: TypeEasingFunction = (t, params: number[] = []) => {
let [amplitude = 1, period = 0.5] = params;
const a = limit(amplitude, 1, 10);
const p = limit(period, 0.1, 2);
if (t === 0 || t === 1) return t;
return (
-a *
Math.pow(2, 10 * (t - 1)) *
Math.sin(
((t - 1 - (p / (Math.PI * 2)) * Math.asin(1 / a)) * (Math.PI * 2)) /
p
)
);
};
export const Spring: TypeEasingFunction = (
t: number,
params: number[] = [],
duration?: number
) => {
let [mass = 1, stiffness = 100, damping = 10, velocity = 0] = params;
mass = limit(mass, 0.1, 1000);
stiffness = limit(stiffness, 0.1, 1000);
damping = limit(damping, 0.1, 1000);
velocity = limit(velocity, 0.1, 1000);
const w0 = Math.sqrt(stiffness / mass);
const zeta = damping / (2 * Math.sqrt(stiffness * mass));
const wd = zeta < 1 ? w0 * Math.sqrt(1 - zeta * zeta) : 0;
const a = 1;
const b = zeta < 1 ? (zeta * w0 + -velocity) / wd : -velocity + w0;
let progress = duration ? (duration * t) / 1000 : t;
if (zeta < 1) {
progress =
Math.exp(-progress * zeta * w0) *
(a * Math.cos(wd * progress) + b * Math.sin(wd * progress));
} else {
progress = (a + b * progress) * Math.exp(-progress * w0);
}
if (t === 0 || t === 1) return t;
return 1 - progress;
};
/**
* Cache the durations at set easing parameters
*/
export const EasingDurationCache: Map<
string | TypeEasingFunction,
number
> = new Map();
/**
* The threshold for an infinite loop
*/
export const INTINITE_LOOP_LIMIT = 10000;
/**
* The spring easing function will only look smooth at certain durations, with certain parameters.
* This functions returns the optimal duration to create a smooth springy animation based on physics
*
* Note: it can also be used to determine the optimal duration of other types of easing function, but be careful of "in-"
* easing functions, because of the nature of the function it can sometimes create an infinite loop, I suggest only using
* `getEasingDuration` for `spring`, specifically "out-spring" and "spring"
*/
export const getEasingDuration = (
easing: string | TypeEasingFunction = "spring"
) => {
if (EasingDurationCache.has(easing)) return EasingDurationCache.get(easing);
const easingFunction =
typeof easing == "function"
? easing
: GetEasingFunction(easing as string);
const params =
typeof easing == "function" ? [] : parseEasingParameters(easing);
const frame = 1 / 6;
let elapsed = 0;
let rest = 0;
let count = 0;
while (++count < INTINITE_LOOP_LIMIT) {
elapsed += frame;
if (easingFunction(elapsed, params, null) === 1) {
rest++;
if (rest >= 16) break;
} else {
rest = 0;
}
}
const duration = elapsed * frame * 1000;
EasingDurationCache.set(easing, duration);
return duration;
};
/**
These Easing Functions are based off of the Sozi Project's easing functions
https://github.com/sozi-projects/Sozi/blob/d72e44ebd580dc7579d1e177406ad41e632f961d/src/js/player/Timing.js
*/
export const Steps: TypeEasingFunction = (t: number, params = []) => {
let [steps = 10, type] = params as [number, string];
const trunc = type == "start" ? Math.ceil : Math.floor;
return trunc(limit(t, 0, 1) * steps) / steps;
};
export const Bezier: TypeEasingFunction = (
t: number,
params: number[] = []
) => {
let [mX1, mY1, mX2, mY2] = params;
return bezier(mX1, mY1, mX2, mY2)(t);
};
/** The default `ease-in` easing function */
export const easein: TypeEasingFunction = bezier(0.42, 0.0, 1.0, 1.0);
/** Converts easing functions to their `out`counter parts */
export const EaseOut = (ease: TypeEasingFunction): TypeEasingFunction => {
return (t, params = [], duration?: number) =>
1 - ease(1 - t, params, duration);
};
/** Converts easing functions to their `in-out` counter parts */
export const EaseInOut = (ease: TypeEasingFunction): TypeEasingFunction => {
return (t, params = [], duration?: number) =>
t < 0.5
? ease(t * 2, params, duration) / 2
: 1 - ease(t * -2 + 2, params, duration) / 2;
};
/** Converts easing functions to their `out-in` counter parts */
export const EaseOutIn = (ease: TypeEasingFunction): TypeEasingFunction => {
return (t, params = [], duration?: number) => {
return t < 0.5
? (1 - ease(1 - t * 2, params, duration)) / 2
: (ease(t * 2 - 1, params, duration) + 1) / 2;
};
};
/**
* The default list of easing functions,
*
* _**Note**_: this is different from {@link EASING}
*/
export const EasingFunctions: { [key: string]: TypeEasingFunction } = {
steps: Steps,
"step-start": (t) => Steps(t, [1, "start"]),
"step-end": (t) => Steps(t, [1, "end"]),
linear: (t) => t,
"cubic-bezier": Bezier,
ease: (t) => Bezier(t, [0.25, 0.1, 0.25, 1.0]),
in: easein,
out: EaseOut(easein),
"in-out": EaseInOut(easein),
"out-in": EaseOutIn(easein),
"in-quad": Quad,
"out-quad": EaseOut(Quad),
"in-out-quad": EaseInOut(Quad),
"out-in-quad": EaseOutIn(Quad),
"in-cubic": Cubic,
"out-cubic": EaseOut(Cubic),
"in-out-cubic": EaseInOut(Cubic),
"out-in-cubic": EaseOutIn(Cubic),
"in-quart": Quart,
"out-quart": EaseOut(Quart),
"in-out-quart": EaseInOut(Quart),
"out-in-quart": EaseOutIn(Quart),
"in-quint": Quint,
"out-quint": EaseOut(Quint),
"in-out-quint": EaseInOut(Quint),
"out-in-quint": EaseOutIn(Quint),
"in-expo": Expo,
"out-expo": EaseOut(Expo),
"in-out-expo": EaseInOut(Expo),
"out-in-expo": EaseOutIn(Expo),
"in-sine": Sine,
"out-sine": EaseOut(Sine),
"in-out-sine": EaseInOut(Sine),
"out-in-sine": EaseOutIn(Sine),
"in-circ": Circ,
"out-circ": EaseOut(Circ),
"in-out-circ": EaseInOut(Circ),
"out-in-circ": EaseOutIn(Circ),
"in-back": Back,
"out-back": EaseOut(Back),
"in-out-back": EaseInOut(Back),
"out-in-back": EaseOutIn(Back),
"in-bounce": Bounce,
"out-bounce": EaseOut(Bounce),
"in-out-bounce": EaseInOut(Bounce),
"out-in-bounce": EaseOutIn(Bounce),
"in-elastic": Elastic,
"out-elastic": EaseOut(Elastic),
"in-out-elastic": EaseInOut(Elastic),
"out-in-elastic": EaseOutIn(Elastic),
spring: Spring,
"spring-in": Spring,
"spring-out": EaseOut(Spring),
"spring-in-out": EaseInOut(Spring),
"spring-out-in": EaseOutIn(Spring),
};
export let EasingFunctionKeys = Object.keys(EasingFunctions);
/**
* Allows you to register new easing functions
*/
export const registerEasingFunction = (key: string, fn: TypeEasingFunction) => {
Object.assign(EasingFunctions, {
[key]: fn,
});
EasingFunctionKeys = Object.keys(EasingFunctions);
};
/**
* Allows you to register multiple new easing functions
*/
export const registerEasingFunctions = (
...obj: Array<typeof EasingFunctions>
) => {
Object.assign(EasingFunctions, ...obj);
EasingFunctionKeys = Object.keys(EasingFunctions);
};
/**
* Convert string easing to their proper form
*/
export const ComplexEasingSyntax = (ease: string) =>
convertToDash(ease)
.replace(/^ease-/, "") // Remove the "ease-" keyword
.replace(/(\(|\s).+/, "") // Remove the function brackets and parameters
.toLowerCase()
.trim();
/** Re-maps a number from one range to another. Numbers outside the range are not clamped to 0 and 1, because out-of-range values are often intentional and useful. */
export const GetEasingFunction = (ease: string) => {
let search = ComplexEasingSyntax(toStr(ease));
if (EasingFunctionKeys.includes(search)) return EasingFunctions[search];
return null;
};
/** Convert easing parameters to Array of numbers, e.g. "spring(2, 500)" to [2, 500] */
export const parseEasingParameters = (str: string) => {
const match = /(\(|\s)([^)]+)\)?/.exec(toStr(str));
return match
? match[2].split(",").map((value) => {
let num = parseFloat(value);
return !Number.isNaN(num) ? num : value.trim();
})
: [];
};
/** map `t` from 0 to 1, to `start` to `end` */
export const scale = (t: number, start: number, end: number) =>
start + (end - start) * t;
/** Rounds numbers to a fixed decimal place */
export const toFixed = (value: number, decimal: number) =>
Math.round(value * 10 ** decimal) / 10 ** decimal;
/**
Given an Array of numbers, estimate the resulting number, at a `t` value between 0 to 1
Based on d3.interpolateBasis [https://github.com/d3/d3-interpolate#interpolateBasis],
check out the link above for more detail.
Basic interpolation works by scaling `t` from 0 - 1, to some start number and end number, in this case lets use
0 as our start number and 100 as our end number, so, basic interpolation would interpolate between 0 to 100.
If we use a `t` of 0.5, the interpolated value between 0 to 100, is 50.
{@link interpolateNumber} takes it further, by allowing you to interpolate with more than 2 values,
it allows for multiple values.
E.g. Given an Array of values [0, 100, 0], and a `t` of 0.5, the interpolated value would become 100.
*/
export const interpolateNumber = (t: number, values: number[], decimal = 3) => {
// nth index
let n = values.length - 1;
// The current index given t
let i = limit(Math.floor(t * n), 0, n - 1);
let start = values[i];
let end = values[i + 1];
let progress = (t - i / n) * n;
return toFixed(scale(progress, start, end), decimal);
};
/** If a value can be converted to a valid number, then it's most likely a number */
export const isNumberLike = (num: string | number) => {
let value = parseFloat(num as string);
return typeof value == "number" && !Number.isNaN(value);
};
/**
Given an Array of values, find a value using `t` (`t` goes from 0 to 1), by
using `t` to estimate the index of said value in the array of `values`
*/
export const interpolateUsingIndex = (
t: number,
values: (string | number)[]
) => {
// limit `t`, to a min of 0 and a max of 1
t = limit(t, 0, 1);
// nth index
let n = values.length - 1;
// The current index given t
let i = Math.round(t * n);
return values[i];
};
/**
Functions the same way {@link interpolateNumber} works.
Convert strings to numbers, and then interpolates the numbers,
at the end if there are units on the first value in the `values` array,
it will use that unit for the interpolated result.
Make sure to read {@link interpolateNumber}.
*/
export const interpolateString = (
t: number,
values: (string | number)[],
decimal = 3
) => {
let units = "";
// If the first value looks like a number with a unit
if (isNumberLike(values[0])) units = getUnit(values[0]);
return (
interpolateNumber(
t,
values.map((v) => (typeof v == "number" ? v : parseFloat(v))),
decimal
) + units
);
};
/**
Use the `color-rgba` npm package, to convert all color formats to an Array of rgba values,
e.g. `[red, green, blue, alpha]`. Then, use the {@link interpolateNumber} functions to interpolate over the array
_**Note**: the red, green, and blue colors are rounded to intergers with no decimal places,
while the alpha color gets rounded to a specific decimal place_
Make sure to read {@link interpolateNumber}.
*/
export const interpolateColor = (t: number, values: string[], decimal = 3) => {
return transpose(...values.map((v) => toRGBAArr(v))).map(
(colors: number[], i) => {
let result = interpolateNumber(t, colors);
return i < 3 ? Math.round(result) : toFixed(result, decimal);
}
);
};
/**
Converts "10px solid red #555 rgba(255, 0,5,6, 7) "
to [ '10px', 'solid', 'red', '#555', 'rgba(255, 0,5,6, 7)' ]
*/
export const ComplexStrtoArr = (str: string) => {
return (
trim(str)
.replace(/(\d|\)|\w)\s/g, (match) => match[0] + "__")
.split("__")
.map(trim)
// Remove invalid values
.filter(isValid)
);
};
/**
Interpolates all types of values including number, string, color, and complex values.
Complex values are values like "10px solid red", that border, and other CSS Properties use.
Make sure to read {@link interpolateNumber}, {@link interpolateString}, {@link interpolateColor}, and {@link interpolateUsingIndex}.
*/
export const interpolateComplex = (
t: number,
values: (string | number)[],
decimal = 3
) => {
// Interpolate numbers
let isNumber = values.every((v) => typeof v == "number");
if (isNumber) return interpolateNumber(t, values as number[], decimal);
// Interpolate colors
let isColor = values.every((v) => !isEmpty(getCSS()) ? getCSS()?.supports?.("color", toStr(v)) : isValid(rgba(v ?? null)) );
if (isColor)
return `rgba(${interpolateColor(t, values as string[], decimal)})`;
// Interpolate complex values and strings
let isString = values.some((v) => typeof v == "string");
if (isString) {
// Interpolate complex values like "10px solid red"
let isComplex = values.some((v) =>
/(\d|\)|\w)\s/.test(trim(v as string))
);
if (isComplex) {
return transpose(...values.map(ComplexStrtoArr))
.map((value) => interpolateComplex(t, value, decimal))
.join(" ");
}
// Interpolate strings with numbers, e.g. "5px"
let isLikeNumber = values.every((v) => isNumberLike(v as string));
if (isLikeNumber)
return interpolateString(t, values as (number | string)[], decimal);
// Interpolate pure strings, e.g. "inherit", "solid", etc...
else return interpolateUsingIndex(t, values as string[]);
}
};
/**
* Custom Easing has 3 properties they are `easing` (all the easings from [#easing](#easing) are supported on top of custom easing functions, like spring, bounce, etc...), `numPoints` (the size of the Array the Custom Easing function should create), and `decimal` (the number of decimal places of the values within said Array).
*
* | Properties | Default Value |
* | ----------- | ----------------------- |
* | `easing` | `spring(1, 100, 10, 0)` |
* | `numPoints` | `50` |
* | `decimal` | `3` |
*/
export type TypeCustomEasingOptions = {
/**
*
* By default, Custom Easing support easing functions, in the form,
*
* | constant | accelerate | decelerate | accelerate-decelerate | decelerate-accelerate |
* | :--------- | :----------------- | :------------- | :-------------------- | :-------------------- |
* | linear | ease-in / in | ease-out / out | ease-in-out / in-out | ease-out-in / out-in |
* | ease | in-sine | out-sine | in-out-sine | out-in-sine |
* | steps | in-quad | out-quad | in-out-quad | out-in-quad |
* | step-start | in-cubic | out-cubic | in-out-cubic | out-in-cubic |
* | step-end | in-quart | out-quart | in-out-quart | out-in-quart |
* | | in-quint | out-quint | in-out-quint | out-in-quint |
* | | in-expo | out-expo | in-out-expo | out-in-expo |
* | | in-circ | out-circ | in-out-circ | out-in-circ |
* | | in-back | out-back | in-out-back | out-in-back |
* | | in-bounce | out-bounce | in-out-bounce | out-in-bounce |
* | | in-elastic | out-elastic | in-out-elastic | out-in-elastic |
* | | spring / spring-in | spring-out | spring-in-out | spring-out-in |
*
* All **Elastic** easing's can be configured using theses parameters,
*
* `*-elastic(amplitude, period)`
*
* Each parameter comes with these defaults
*
* | Parameter | Default Value |
* | --------- | ------------- |
* | amplitude | `1` |
* | period | `0.5` |
*
* ***
*
* All **Spring** easing's can be configured using theses parameters,
*
* `spring-*(mass, stiffness, damping, velocity)`
*
* Each parameter comes with these defaults
*
* | Parameter | Default Value |
* | --------- | ------------- |
* | mass | `1` |
* | stiffness | `100` |
* | damping | `10` |
* | velocity | `0` |
*
* 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`_
*
*/
easing?: string | TypeEasingFunction;
numPoints?: number;
decimal?: number;
duration?: number;
};
/**
* returns a CustomEasingOptions object from a easing "string", or function
*/
export const CustomEasingOptions = (
options: TypeCustomEasingOptions | string | TypeEasingFunction = {}
) => {
let isEasing = typeof options == "string" || typeof options == "function";
let {
easing = "spring(1, 100, 10, 0)",
numPoints = 100,
decimal = 3,
duration,
} = (isEasing ? { easing: options } : options) as TypeCustomEasingOptions;
return { easing, numPoints, decimal, duration };
};
/**
Cache calculated tween points for commonly used easing functions
*/
export const TweenCache = new Map();
/**
* Create an Array of tween points using easing functions.
* The array is `numPoints` large, which is by default 50.
* Easing function can be custom defined, so, instead of string you can use functions,
* e.g.
* ```ts
* tweenPts({
* easing: t => t,
* numPoints: 50
* })
* ```
Based on https://github.com/w3c/csswg-drafts/issues/229#issuecomment-861415901
*/
export const EasingPts = ({
easing,
numPoints,
duration,
}: TypeCustomEasingOptions = {}): number[] => {
const pts = [];
const key = `${easing}${numPoints}`;
if (TweenCache.has(key)) return TweenCache.get(key);
const easingFunction =
typeof easing == "function"
? easing
: GetEasingFunction(easing as string);
const params =
typeof easing == "function" ? [] : parseEasingParameters(easing);
for (let i = 0; i < numPoints; i++) {
pts[i] = easingFunction(i / (numPoints - 1), params, duration);
}
TweenCache.set(key, pts);
return pts;
};
/**
* Update the duration of `spring` and `spring-in` easings ot use optimal durations
*/
const updateDuration = (optionsObj: TypeCustomEasingOptions = {}) => {
if (typeof optionsObj.easing == "string") {
let easing = ComplexEasingSyntax(optionsObj.easing as string);
if (/(spring|spring-in)$/i.test(easing)) {
optionsObj.duration = getEasingDuration(optionsObj.easing);
}
}
};
/**
* Generates an Array of values using easing functions which in turn create the effect of custom easing.
* To use this properly make sure to set the easing animation option to "linear".
* Check out a demo of CustomEasing at <https://codepen.io/okikio/pen/abJMWNy?editors=0010>
*
* Custom Easing has 3 properties they are `easing` (all the easings from {@link EasingFunctions} are supported on top of custom easing functions like spring, bounce, etc...), `numPoints` (the size of the Array the Custom Easing function should create), and `decimal` (the number of decimal places of the values within said Array).
*
* | Properties | Default Value |
* | ----------- | ----------------------- |
* | `easing` | `spring(1, 100, 10, 0)` |
* | `numPoints` | `50` |
* | `decimal` | `3` |
*
* By default, Custom Easing support easing functions, in the form,
*
* | constant | accelerate | decelerate | accelerate-decelerate | decelerate-accelerate |
* | :--------- | :----------------- | :------------- | :-------------------- | :-------------------- |
* | linear | ease-in / in | ease-out / out | ease-in-out / in-out | ease-out-in / out-in |
* | ease | in-sine | out-sine | in-out-sine | out-in-sine |
* | steps | in-quad | out-quad | in-out-quad | out-in-quad |
* | step-start | in-cubic | out-cubic | in-out-cubic | out-in-cubic |
* | step-end | in-quart | out-quart | in-out-quart | out-in-quart |
* | | in-quint | out-quint | in-out-quint | out-in-quint |
* | | in-expo | out-expo | in-out-expo | out-in-expo |
* | | in-circ | out-circ | in-out-circ | out-in-circ |
* | | in-back | out-back | in-out-back | out-in-back |
* | | in-bounce | out-bounce | in-out-bounce | out-in-bounce |
* | | in-elastic | out-elastic | in-out-elastic | out-in-elastic |
* | | spring / spring-in | spring-out | spring-in-out | spring-out-in |
*
* All **Elastic** easing's can be configured using theses parameters,
*
* `*-elastic(amplitude, period)`
*
* Each parameter comes with these defaults
*
* | Parameter | Default Value |
* | --------- | ------------- |
* | amplitude | `1` |
* | period | `0.5` |
*
* ***
*
* All **Spring** easing's can be configured using theses parameters,
*
* `spring-*(mass, stiffness, damping, velocity)`
*
* Each parameter comes with these defaults
*
* | Parameter | Default Value |
* | --------- | ------------- |
* | mass | `1` |
* | stiffness | `100` |
* | damping | `10` |
* | velocity | `0` |
*
* 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`_
*
* _**Suggestion**: if you decide to use CustomEasing on one CSS property, I suggest using CustomEasing or {@link ApplyCustomEasing} on the rest (this is no longer necessary, but for the sake of readability it's better to do)_
*
* e.g.
* ```ts
* import { animate, CustomEasing, EaseOut, Quad } from "@okikio/animate";
* animate({
* target: "div",
*
* // Notice how only, the first value in the Array uses the "px" unit
* border: CustomEasing(["1px solid red", "3 dashed green", "2 solid black"], {
* // This is a custom easing function
* easing: EaseOut(Quad)
* }),
*
* translateX: CustomEasing([0, 250], {
* easing: "linear",
*
* // You can change the size of Array for the CustomEasing function to generate
* numPoints: 200,
*
* // The number of decimal places to round, final values in the generated Array
* decimal: 5,
* }),
*
* // You can set the easing without an object
* // Also, if units are detected in the values Array,
* // the unit of the first value in the values Array are
* // applied to other values in the values Array, even if they
* // have prior units
* rotate: CustomEasing(["0turn", 1, 0, 0.5], "out"),
* "background-color": CustomEasing(["#616aff", "white"]),
* easing: "linear"
* }),
*
* // TIP... Use linear easing for the proper effect
* easing: "linear"
* })
* ```
*/
export const CustomEasing = (
values: (string | number)[],
options: TypeCustomEasingOptions | string | TypeEasingFunction = {}
): (string | number)[] => {
let optionsObj = CustomEasingOptions(options);
updateDuration(optionsObj);
return EasingPts(optionsObj).map((t) =>
interpolateComplex(t, values, optionsObj.decimal)
);
};
/**
* Returns an array containing `[easing pts, duration]`, it's meant to be a self enclosed way to create spring easing.
* Springs have an optimal duration; using `getEasingDuration()` we are able to have the determine the optimal duration for a spring with given parameters.
*
* By default it will only give the optimal duration for `spring` or `spring-in` easing, this is to avoid infinite loops caused by the `getEasingDuration()` function.
*
* Internally the `SpringEasing` uses {@link CustomEasing}, read more on it, to understand how the `SpringEasing` function works.
*
* e.g.
* ```ts
* import { animate, SpringEasing } from "@okikio/animate";
*
* // `duration` is the optimal duration for the spring with the set parameters
* let [translateX, duration] = SpringEasing([0, 250], "spring(5, 100, 10, 1)");
* // or
* // `duration` is 5000 here
* let [translateX, duration] = SpringEasing([0, 250], {
* easing: "spring(5, 100, 10, 1)",
* numPoints: 50,
* duration: 5000,
* decimal: 3
* });
*
* animate({
* target: "div",
* translateX,
* duration
* });
* ```
*/
export const SpringEasing = (
values: (string | number)[],
options: TypeCustomEasingOptions | string = {}
): [(string | number)[], number] => {
let optionsObj = CustomEasingOptions(options);
let { duration } = optionsObj;
updateDuration(optionsObj);
return [
CustomEasing(values, optionsObj),
isValid(duration) ? duration : optionsObj.duration,
];
};
/**
* Applies the same custom easings to all properties of the object and returns an object with each property having an array of custom eased values
*
* If you use the `spring` or `spring-in` easings it will also return the optimal duration as a key in the object it returns.
* If you set `duration` to a number, it will prioritize that `duration` over optimal duration given by the spring easings.
*
* Read more about {@link CustomEasing}
*
* e.g.
* ```ts
* import { animate, ApplyCustomEasing } from "@okikio/animate";
* animate({
* target: "div",
*
* ...ApplyCustomEasing({
* border: ["1px solid red", "3 dashed green", "2 solid black"],
* translateX: [0, 250],
* rotate: ["0turn", 1, 0, 0.5],
* "background-color": ["#616aff", "white"],
*
* // You don't need to enter any parameters, you can just use the default values
* easing: "spring",
*
* // You can change the size of Array for the CustomEasing function to generate
* numPoints: 200,
*
* // The number of decimal places to round, final values in the generated Array
* decimal: 5,
*
* // You can also set the duration from here.
* // When using spring animations, the duration you set here is not nesscary,
* // since by default springs will try to determine the most appropriate duration for the spring animation.
* // But the duration value here will override `spring` easings optimal duration value
* duration: 3000
* })
* })
* ```
*/
export const ApplyCustomEasing = (
options: TypeCustomEasingOptions & {
[key: string]:
| number
| string
| TypeEasingFunction
| (number | string)[];
} = {}
): IAnimateInstanceConfig & TypeCustomEasingOptions & {
[key: string]:
| number
| string
| TypeEasingFunction
| (number | string)[];
} => {
let { easing, numPoints, decimal, duration, ...rest } = options;
let optionsObj = { easing, numPoints, decimal, duration };
updateDuration(optionsObj);
let properties = mapObject(rest, (value: (string | number)[]) =>
CustomEasing(value, optionsObj)
);
let durationObj = {};
if (isValid(duration)) durationObj = { duration };
else if (isValid(optionsObj.duration))
durationObj = { duration: optionsObj.duration };
return Object.assign({}, properties, durationObj);
};
{
"compilerOptions": {
"moduleResolution": "node",
"target": "ES2021",
"module": "es2022",
"lib": [
"ES2021",
"ESNext",
"DOM"
],
"incremental": true,
"isolatedModules": true,
"esModuleInterop": true,
"outDir": "./lib",
"skipLibCheck": true
}
}
import { Manager } from "@okikio/manager";
import { getUnit, isValid, toArr, isEmpty } from "./utils";
import { getCSS, getDocument } from "./browser-objects";
import rgba from "./color-rgba";
/**
* string, number, calc(${...}) & var(--${...}) generic values for CSS Properties.
*
* The `string & {}` is a typescript hack for autocomplete,
* check out [github.com/microsoft/TypeScript/issues/29729](https://github.com/microsoft/TypeScript/issues/29729) to learn more
*/
export type TypeCommonCSSGenerics = number | (string & {}) | `calc(${string})` | `var(--${string})` | `var(--${string}, ${string | number})`;
/**
* Allow either the type alone, or an Array of that type
*/
export type OneOrMany<Type> = Type | Type[];
/**
* The CSS Generics of Property Indexed Keyframes {@link PropertyIndexedKeyframes}
*/
export type TypeCSSGenericPropertyKeyframes = OneOrMany<TypeCommonCSSGenerics>;
/**
* Returns a closure Function, which adds a unit to numbers, but simply returns strings with no changes (assuming the value has a unit if it's a string)
*
* @param unit - the default unit to give the CSS Value
* @returns
* if value already has a unit (we assume the value has a unit if it's a string), we return it;
* else return the value plus the default unit
*/
export const addCSSUnit = (unit: string = "") => {
return (value: string | number) => typeof value == "string" ? value : `${value}${unit}`;
}
/** Function doesn't add any units by default */
export const UnitLess = addCSSUnit();
/** Function adds "px" unit to numbers */
export const UnitPX = addCSSUnit("px");
/** Function adds "deg" unit to numbers */
export const UnitDEG = addCSSUnit("deg");
/**
* Returns a closure function, which adds units to numbers, strings or arrays of both
*
* @param unit - a unit function to use to add units to {@link TypeCSSGenericPropertyKeyframes TypeCSSGenericPropertyKeyframes's }
* @returns
* if input is a string split it into an array at the comma's, and add units
* else if the input is a number add the default units
* otherwise if the input is an array of both add units according to {@link addCSSUnit}
*/
export const CSSValue = (unit: typeof UnitLess) => {
return (input: TypeCSSGenericPropertyKeyframes): ReturnType<typeof UnitLess>[] => {
return isValid(input) ? toArr(input).map(val => {
if (typeof val != "number" && typeof val != "string")
return val;
// Basically if you can convert it to a number try to,
// otherwise just return whatever you can
let num = Number(val);
let value = Number.isNaN(num) ? (typeof val == "string" ? val.trim() : val) : num;
return unit(value); // Add the default units
}) : [];
};
}
/**
* Takes `TypeCSSGenericPropertyKeyframes` or an array of `TypeCSSGenericPropertyKeyframes` and adds units approriately
*
* @param arr - array of numbers, strings and/or an array of array of both e.g. ```[[25, "50px", "60%"], "25 35 60%", 50]```
* @param unit - a unit function to use to add units to {@link TypeCSSGenericPropertyKeyframes | TypeCSSGenericPropertyKeyframes's }
* @returns
* an array of an array of strings with units
* e.g.
* ```ts
* import { CSSArrValue, UnitPX } from "@okikio/animate";
*
* CSSArrValue([
* [25, "50px", "60%"],
* "25 35 60%",
* 50
* ], UnitPX)
*
* //= [
* //= [ '25px', '50px', '60%' ],
* //= [ '25px', '35px', '60%' ],
* //= [ '50px' ]
* //= ]
* ```
*/
export const CSSArrValue = (arr: TypeCSSGenericPropertyKeyframes | TypeCSSGenericPropertyKeyframes[], unit: typeof UnitLess) => {
// This is for the full varients of the transform function as well as the 3d varients
// zipping the `CSSValue` means if a user enters a string, it will treat each value (seperated by a comma) in that
// string as a seperate transition state
return toArr(arr).map(CSSValue(unit)) as TypeCSSGenericPropertyKeyframes[];
}
/** Parses CSSValues without adding any units */
export const UnitLessCSSValue = CSSValue(UnitLess);
/** Parses CSSValues and adds the "px" unit if required */
export const UnitPXCSSValue = CSSValue(UnitPX);
/** Parses CSSValues and adds the "deg" unit if required */
export const UnitDEGCSSValue = CSSValue(UnitDEG);
/**
* Cache previously converted CSS values to avoid lots of Layout, Style, and Paint computations when computing CSS values
*/
export const CSS_CACHE = new Manager();
/**
* Convert colors to an [r, g, b, a] Array
*/
export const toRGBAArr = (color = "transparent") => {
let result: number[];
color = color.trim();
if (CSS_CACHE.has(color)) return CSS_CACHE.get(color);
if (isEmpty(getCSS())) {
result = rgba(color);
}
if (!isEmpty(getDocument())) {
if (!getCSS()?.supports("background-color", color)) return color;
let el = getDocument()?.createElement("div");
el.style.backgroundColor = color;
let parent = getDocument()?.body;
parent.appendChild(el);
let { backgroundColor } = globalThis?.getComputedStyle?.(el);
el.remove();
let computedColor = /\(([^)]+)\)?/.exec(backgroundColor)?.[1].split(",");
result = (computedColor.length == 3 ? [...computedColor, "1"] : computedColor).map(v => parseFloat(v));
}
CSS_CACHE.set(color, result);
return result;
};
/**
* Convert CSS lengths to pixels
* @beta
*/
// export const toPX = (value = "0", target?: HTMLElement) => {
// if (CSS_CACHE.has(value)) return CSS_CACHE.get(value);
// if (!CSS.supports("width", value)) return toPX;
// let el = getDocument()?.createElement?.(target?.tagName ?? "div");
// el.style.width = value;
// let parent = target?.parentElement ?? getDocument()?.body;
// parent?.appendChild(el);
// let { width: result } = globalThis?.getComputedStyle?.(el);
// parent?.removeChild(el);
// CSS_CACHE.set(value, result);
// return result;
// };
// export const angleConversions = {
// 'deg': 1,
// 'rad': ( 180 / Math.PI ),
// 'grad': ( 180 / 200 ),
// 'turn': 360
// }
// export const angleUnits = Object.keys(angleConversions);
// /**
// * Convert CSS andles to degrees
// * @beta
// */
// export const toDEG = (value = "0deg") => {
// let unit = getUnit(value);
// return (parseFloat(value) * angleConversions[unit]) + "deg";
// };
/** Merges 2-dimensional Arrays into a single 1-dimensional array */
export const flatten = (arr: any[]) => [].concat(...arr);
/** Determines whether value is an pure object (not array, not function, etc...) */
export const isObject = (obj: any) =>
typeof obj == "object" && !Array.isArray(obj) && typeof obj != "function";
/** Determines if an object is empty */
export const isEmpty = (obj: any) => {
for (let _ in obj) {
return false;
}
return true;
};
/**
* Acts like array.map(...) but for functions
*/
export const mapObject = (
obj: object,
fn: (value: any, key: any, obj: any) => any
) => {
let keys = Object.keys(obj);
let key: any,
value: any,
result = {};
for (let i = 0, len = keys.length; i < len; i++) {
key = keys[i];
value = obj[key];
result[key] = fn(value, key, obj);
}
return result;
};
/** Converts values to strings */
export const toStr = (input: any) => `` + input;
/**
* Returns the unit of a string, it does this by removing the number in the string
*/
export const getUnit = (str: string | number) => {
let num = parseFloat(str as string);
return toStr(str).replace(toStr(num), "");
};
/**
Convert value to string, then trim any extra white space and line terminator characters from the string.
*/
export const trim = (str: string) => toStr(str).trim();
/**
* Convert the input to an array
* For strings if type == "split", split the string at spaces, if type == "wrap" wrap the string in an array
* For array do nothing
* For everything else wrap the input in an array
*/
export const toArr = (input: any): any[] => {
if (Array.isArray(input) || typeof input == "string") {
if (typeof input == "string") input = input.split(/\s+/);
return input;
}
return [input];
};
/**
* Checks if a value is valid/truthy; it counts empty arrays and strings as falsey,
* as well as null, undefined, and NaN, everything else is valid
*
* _**Note:** 0 counts as valid_
*
* @param value - anything
* @returns true or false
*/
export const isValid = (value: any) => {
if (Array.isArray(value) || typeof value == "string")
return Boolean(value.length);
return value != null && value != undefined && !Number.isNaN(value);
};
/** Convert a camelCase string to a dash-separated string (opposite of {@link camelCase}) */
export const convertToDash = (str: string) => {
str = str.replace(/([A-Z])/g, (letter) => `-${letter.toLowerCase()}`);
// Remove first dash
return str.charAt(0) == "-" ? str.substr(1) : str;
};
/** Convert a dash-separated string into camelCase strings (opposite of {@link convertToDash}) */
export const camelCase = (str: string) => {
if (str.includes("--")) return str;
let result = `${str}`.replace(/-([a-z])/g, (_, letter) =>
letter.toUpperCase()
);
return result;
};
/**
* Return a copy of the object without the keys specified in the keys argument
*
* @param keys - arrays of keys to remove from the object
* @param obj - the object in question
* @returns
* a copy of the object without certain keys
*/
export const omit = (keys: string[], obj: { [key: string]: any }) => {
let arr = [...keys];
let rest = { ...obj };
while (arr.length) {
let { [arr.pop()]: omitted, ...remaining } = rest;
rest = remaining;
}
return rest;
};
/**
* Return a copy of the object with only the keys specified in the keys argument
*
* @param keys - arrays of keys to keep from the object
* @param obj - the object in question
* @returns
* a copy of the object with only certain keys
*/
export const pick = (keys: string[], obj: { [key: string]: any }) => {
let arr = [...keys];
let rest = {};
for (let key of arr) {
if (isValid(obj[key])) rest[key] = obj[key];
}
return rest;
};
/**
* Flips the rows and columns of 2-dimensional arrays
*
* Read more on [underscorejs.org](https://underscorejs.org/#zip) & [lodash.com](https://lodash.com/docs/4.17.15#zip)
*
* @example
* ```ts
* transpose(
* ['moe', 'larry', 'curly'],
* [30, 40, 50],
* [true, false, false]
* );
* // [
* // ["moe", 30, true],
* // ["larry", 40, false],
* // ["curly", 50, false]
* // ]
* ```
* @param [...args] - the arrays to process as a set of arguments
* @returns
* returns the new array of grouped elements
*/
// (TypeCSSGenericPropertyKeyframes | TypeCSSGenericPropertyKeyframes[])[]
export const transpose = (...args: (any | any[])[]) => {
let largestArrLen = 0;
args = args.map((arr) => {
// Convert all values in arrays to an array
// This ensures that `arrays` is an array of arrays
let result = toArr(arr);
// Finds the largest array
let len = result.length;
if (len > largestArrLen) largestArrLen = len;
return result;
});
// Flip the rows and columns of arrays
let result = [];
let len = args.length;
for (let col = 0; col < largestArrLen; col++) {
result[col] = [];
for (let row = 0; row < len; row++) {
let val = args[row][col];
if (isValid(val)) result[col][row] = val;
}
}
return result;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment