@dvcol/svelte-utils
Version:
Svelte library for common utility functions and constants
243 lines (242 loc) • 10.3 kB
JavaScript
import { clamp } from '@dvcol/common-utils/common/math';
import { flip } from 'svelte/animate';
import { cubicOut } from 'svelte/easing';
import { scale, slide } from 'svelte/transition';
export const emptyTransition = () => () => ({});
export const emptyAnimation = () => ({});
/**
* Parses a CSS property from a string to a number.
* @param node - The element to parse the CSS property from.
* @param css - The CSS property to parse.
*/
export const parseCSSString = (node, css) => {
if (!node)
return 0;
let value = getComputedStyle(node)[css];
if (typeof value === 'number')
return value;
if (typeof value !== 'string')
return 0;
if (!value)
return 0;
value = parseFloat(value);
if (Number.isNaN(value))
return 0;
return value;
};
const evaluateFn = (value, node) => {
if (typeof value === 'function')
return value(node);
return value;
};
const opacityRegex = /opacity: [0-9.]+;/;
const replaceOpacity = (css, min = false, value) => {
if (min === false)
return css.replace(opacityRegex, '');
return css.replace(opacityRegex, `opacity: ${clamp(value, typeof min === 'number' ? min : 0, 1)};`);
};
/**
* Animates the height of an element from 0 to the current height for `in` transitions and from the current height to 0 for `out` transitions.
* If `freeze` is `true`, the width of the element will be frozen during the transition.
* If `skip` is `true`, the transition will be skipped.
* If `opacity` is a truthy, the opacity will be gradually increased or decreased over the duration.
* @default { easing: x => x, freeze: true, skip: false }
*/
export function height(node, { easing = x => x, freeze = true, skip = false, css, opacity, transform = _css => _css, ...params } = {}, { direction } = {}) {
const { delay, duration, css: heightCss } = slide(node, { axis: 'y', easing, ...params });
const _width = parseCSSString(node, 'width');
const _opacity = +getComputedStyle(node).opacity;
const { minimum = 0, easing: opacityEasing = easing } = typeof opacity === 'object' ? opacity : {};
return {
delay,
duration,
easing,
css: (t, u) => {
if (evaluateFn(skip, node))
return '';
let _css = css?.length ? `${css};\n` : '';
if (heightCss)
_css += heightCss(t, u);
if (opacity)
_css = replaceOpacity(_css, minimum, opacityEasing(t) * _opacity);
if (!evaluateFn(freeze, node) || direction === 'in')
return transform(_css, t, u);
return transform(`${_css};\nwidth: ${_width}px`, t, u);
},
};
}
/**
* Animates the width of an element from 0 to the current width for `in` transitions and from the current width to 0 for `out` transitions.
* If `freeze` is `true`, the width of the element will be frozen during the transition.
* If `skip` is `true`, the transition will be skipped.
* If `opacity` is a number, the opacity will be gradually increased or decreased over the duration of the transition but never below the provided value.
* @default { easing: x => x, freeze: true, skip: false }
*/
export function width(node, { easing = x => x, freeze = true, skip = false, css, opacity, transform = _css => _css, ...params } = {}, { direction } = {}) {
const { delay, duration, css: widthCss } = slide(node, { axis: 'x', easing, ...params });
const _height = parseCSSString(node, 'height');
const _opacity = +getComputedStyle(node).opacity;
const { minimum = 0, easing: opacityEasing = easing } = typeof opacity === 'object' ? opacity : {};
return {
delay,
duration,
easing,
css: (t, u) => {
if (evaluateFn(skip, node))
return '';
let _css = css?.length ? `${css};\n` : '';
if (widthCss)
_css += widthCss(t, u);
if (opacity)
_css = replaceOpacity(_css, minimum, opacityEasing(t) * _opacity);
if (!evaluateFn(freeze, node) || direction === 'in')
return transform(_css, t, u);
return transform(`${_css};\nheight: ${_height}px`, t, u);
},
};
}
/**
* Composes multiple transitions into a single transition.
* @param transitions - The transition functions and their props to compose into one.
*/
export function composeTransition(...transitions) {
return (node, params, options) => {
const _transitions = transitions.map(({ use, props }) => {
return use(node, { ...params, ...props }, options);
});
return {
delay: params.delay ?? 0,
duration: params.duration ?? 400,
easing: params.easing ?? (x => x),
css: (t, u) => {
if (params.skip && evaluateFn(params.skip, node))
return '';
const _css = _transitions
.map(transition => {
if (typeof transition === 'function')
return transition().css?.(t, u);
return transition.css?.(t, u);
})
.filter(Boolean)
.join(';\n');
if (params.transform)
return params.transform(_css, t, u);
return _css;
},
};
};
}
/**
* Animates the opacity and scale of an element.
* `in` transitions animate from an element's current (default) values to the provided values, passed as parameters.
* `out` transitions animate from the provided values to an element's default values.
* @default { duration: 400, start: 0.95, freeze: true }
*/
export function scaleFreeze(node, { duration = 400, start = 0.95, freeze = true, css, ...params } = {}, { direction } = {}) {
const { delay, easing, css: scaleCss } = scale(node, { duration, start, ...params });
let _height = parseFloat(getComputedStyle(node).height);
if (!_height || Number.isNaN(_height))
_height = 0;
let _width = parseFloat(getComputedStyle(node).width);
if (!_width || Number.isNaN(_width))
_width = 0;
return {
delay,
duration,
easing,
css: (t, u) => {
if (!freeze || direction === 'in')
return [css, scaleCss?.(t, u)].filter(Boolean).join(';\n');
return [`height: ${_height}px`, `width: ${_width}px`, scaleCss?.(t, u)].join(';\n');
},
};
}
/**
* Combines the `width` and `scale` transitions to animate the width of an element.
* @default { duration: 400, start: 0.95 }
*/
export function scaleWidth(node, { duration = 400, start = 0.95, scale: scaleParam, width: widthParam, opacity, ...params } = {}) {
const { delay, easing, css: scaleCss } = scale(node, { duration, start, opacity, ...params, ...scaleParam });
const { css: widthCss } = width(node, { duration, ...params, ...widthParam });
return {
delay,
duration,
easing,
css: (t, u) => {
return [widthCss?.(t, u), scaleCss?.(t, u)].join(';\n');
},
};
}
/**
* Combines the `height` and `scale` transitions to animate the height of an element.
* @default { duration: 400, start: 0.95 }
*/
export function scaleHeight(node, { duration = 400, start = 0.95, scale: scaleParam, height: heightParam, opacity, ...params } = {}) {
const { delay, easing, css: scaleCss } = scale(node, { duration, start, opacity, ...params, ...scaleParam });
const { css: heightCss } = height(node, { duration, ...params, ...heightParam });
return {
delay,
duration,
easing,
css: (t, u) => {
return [heightCss?.(t, u), scaleCss?.(t, u)].join(';\n');
},
};
}
export const flipToggle = (node, directions, { skip, ...params } = {}) => evaluateFn(skip, node) ? {} : flip(node, directions, params);
const regexCssUnit = /^\s*(-?[\d.]+)(\S*)\s*$/;
function splitCssUnit(value) {
if (typeof value === 'object')
return value;
if (typeof value === 'number')
return { value, unit: 'px' };
const split = value.match(regexCssUnit);
return {
value: Number.parseFloat(split?.[1] ?? '0'),
unit: split?.[2] || 'px',
};
}
function parseStartValue(node, style, start) {
const value = typeof start === 'function' ? start({ node, style }) : (start ?? {});
const { x = 0, y = 0 } = typeof value === 'object' ? value : { x: value, y: value };
const { value: x_value, unit: x_unit } = splitCssUnit(x);
const { value: y_value, unit: y_unit } = splitCssUnit(y);
return { x: `${x_value}${x_unit}`, y: `${y_value}${y_unit}` };
}
export function scaleValue(start = 0) {
const sd = 1 - start;
return (t, u) => `scale(${1 - sd * u}`;
}
/* Animates the x and y positions and the opacity of an element. `in` transitions animate from the provided values, passed as parameters to the element's default values. `out` transitions animate from the element's default values to the provided values.
*
* @param {Element} node
* @param {FlyFrom} [params]
* @returns {TransitionConfig}
*/
export function flyFrom(node, { delay = 0, duration = 400, easing = cubicOut, x = 0, y = 0, opacity = 0, start = 0, scale: scaleStart = 1 } = {}) {
const style = getComputedStyle(node);
const target_opacity = +style.opacity;
const transform = style.transform === 'none' ? '' : style.transform;
const od = target_opacity * (1 - opacity);
const { value: x_value, unit: x_unit } = splitCssUnit(x);
const { value: y_value, unit: y_unit } = splitCssUnit(y);
const { x: start_x, y: start_y } = parseStartValue(node, style, start);
return {
delay,
duration,
easing,
css: (t, u) => [
`opacity: ${target_opacity - od * u}`,
[
'transform:',
transform,
`translate(calc(${start_x} + ${(1 - t) * x_value}${x_unit}), calc(${start_y} + ${(1 - t) * y_value}${y_unit}))`,
scale === undefined ? undefined : scaleValue(scaleStart)(t, u),
]
.filter(Boolean)
.join(' '),
]
.filter(Boolean)
.join(';\n'),
};
}